Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

First commit.

  • Loading branch information...
commit 7dd8ec1d1195b904689696234e370de359a01981 0 parents
Frank Smit authored March 11, 2011
19  LICENSE
... ...
@@ -0,0 +1,19 @@
  1
+Copyright (C) 2011 by Frank Smit <frank@61924.nl>
  2
+
  3
+Permission is hereby granted, free of charge, to any person obtaining a copy
  4
+of this software and associated documentation files (the "Software"), to deal
  5
+in the Software without restriction, including without limitation the rights
  6
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  7
+copies of the Software, and to permit persons to whom the Software is
  8
+furnished to do so, subject to the following conditions:
  9
+
  10
+The above copyright notice and this permission notice shall be included in
  11
+all copies or substantial portions of the Software.
  12
+
  13
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  14
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  15
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  16
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  17
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  18
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  19
+THE SOFTWARE.
50  examples/example1.py
... ...
@@ -0,0 +1,50 @@
  1
+#!/usr/bin/env python
  2
+
  3
+import tornado.httpserver
  4
+import tornado.ioloop
  5
+import tornado.options
  6
+import tornado.web
  7
+
  8
+from momoko import Pool
  9
+
  10
+
  11
+class BaseHandler(tornado.web.RequestHandler):
  12
+    @property
  13
+    def db(self):
  14
+        if not hasattr(self.application, 'db'):
  15
+            self.application.db = Pool(1, 20, 10, **{
  16
+                'host': 'localhost',
  17
+                'database': 'infunadb',
  18
+                'user': 'infuna',
  19
+                'password': 'password',
  20
+                'async': 1
  21
+            })
  22
+        return self.application.db
  23
+
  24
+
  25
+class MainHandler(BaseHandler):
  26
+    @tornado.web.asynchronous
  27
+    def get(self):
  28
+        self.db.execute('SELECT 42, 12, 40, 11;', callback=self._on_response)
  29
+
  30
+    def _on_response(self, cursor):
  31
+        self.write('Query results: %s' % cursor.fetchall())
  32
+        self.finish()
  33
+
  34
+
  35
+def main():
  36
+    try:
  37
+        tornado.options.parse_command_line()
  38
+        application = tornado.web.Application([
  39
+            (r'/', MainHandler),
  40
+        ])
  41
+        http_server = tornado.httpserver.HTTPServer(application)
  42
+        http_server.bind(8888)
  43
+        http_server.start(0) # Forks multiple sub-processes
  44
+        tornado.ioloop.IOLoop.instance().start()
  45
+    except KeyboardInterrupt:
  46
+        print 'Exit'
  47
+
  48
+
  49
+if __name__ == '__main__':
  50
+    main()
3  momoko/__init__.py
... ...
@@ -0,0 +1,3 @@
  1
+#!/usr/bin/env python
  2
+
  3
+from momoko import *
169  momoko/momoko.py
... ...
@@ -0,0 +1,169 @@
  1
+#!/usr/bin/env python
  2
+
  3
+__authors__ = ('Frank Smit <frank@61924.nl>',)
  4
+__version__ = '0.1.0'
  5
+__license__ = 'MIT'
  6
+
  7
+
  8
+import functools
  9
+
  10
+import psycopg2
  11
+from tornado.ioloop import IOLoop, PeriodicCallback
  12
+
  13
+
  14
+class Pool(object):
  15
+    """A connection pool that manages PostgreSQL connections.
  16
+    """
  17
+    def __init__(self, min_conn, max_conn, cleanup_timeout, *args, **kwargs):
  18
+        self.min_conn = min_conn
  19
+        self.max_conn = max_conn
  20
+        self.closed = False
  21
+
  22
+        self._args = args
  23
+        self._kwargs = kwargs
  24
+
  25
+        self._pool = []
  26
+
  27
+        for i in range(self.min_conn):
  28
+            self._new_conn()
  29
+
  30
+        # Create a periodic callback that tries to close inactive connections
  31
+        if cleanup_timeout > 0:
  32
+            self._cleaner = PeriodicCallback(self._clean_pool,
  33
+                cleanup_timeout * 1000)
  34
+            self._cleaner.start()
  35
+
  36
+    def _new_conn(self, new_cursor_args={}):
  37
+        """Create a new connection. If `new_cursor_args` is provided a new
  38
+        cursor is created when the callback is executed.
  39
+        """
  40
+        if len(self._pool) > self.max_conn:
  41
+            raise PoolError('connection pool exausted')
  42
+        conn = psycopg2.connect(*self._args, **self._kwargs)
  43
+        add_conn = functools.partial(self._add_conn, conn)
  44
+
  45
+        if new_cursor_args:
  46
+            new_cursor_args['connection'] = conn
  47
+            new_cursor = functools.partial(self._new_cursor, **new_cursor_args)
  48
+            Poller(conn, (add_conn, new_cursor)).start()
  49
+        else:
  50
+            Poller(conn, (add_conn,)).start()
  51
+
  52
+    def _add_conn(self, conn):
  53
+        """Add a connection to the pool. This function is used by `_new_conn`
  54
+        as a callback to add the created connection to the pool.
  55
+        """
  56
+        self._pool.append(conn)
  57
+
  58
+    def _new_cursor(self, function, func_args=(), callback=None, connection=None):
  59
+        """Create a new cursor. If there's no connection available, a new
  60
+        connection will be created and `_new_cursor` will be called again after
  61
+        the connection has been made.
  62
+        """
  63
+        if not connection:
  64
+            connection = self._get_free_conn()
  65
+            if not connection:
  66
+                new_cursor_args = {
  67
+                    'function': function,
  68
+                    'func_args': func_args,
  69
+                    'callback': callback
  70
+                }
  71
+                self._new_conn(new_cursor_args)
  72
+                return
  73
+
  74
+        cursor = connection.cursor()
  75
+        getattr(cursor, function)(*func_args)
  76
+
  77
+        # Callbacks from cursor fucntion always get the cursor back
  78
+        callback = functools.partial(callback, cursor)
  79
+        Poller(cursor.connection, (callback,)).start()
  80
+
  81
+    def _get_free_conn(self):
  82
+        """Look for a free connection and return it. `None` is returned when no
  83
+        free connection can be found.
  84
+        """
  85
+        if self.closed:
  86
+            raise PoolError('connection pool is closed')
  87
+        for conn in self._pool:
  88
+            if not conn.isexecuting():
  89
+                return conn
  90
+        return None
  91
+
  92
+    def _clean_pool(self):
  93
+        """Try to close the number of connections that exceeds the number in
  94
+        `min_conn`. This method loops throught the connections in `_pool` and
  95
+        if it finds a free connection it closes it.
  96
+        """
  97
+        if self.closed:
  98
+            raise PoolError('connection pool is closed')
  99
+        if len(self._pool) > self.min_conn:
  100
+            conns = len(self._pool) - self.min_conn
  101
+            indexes = []
  102
+            for i, conn in enumerate(self._pool):
  103
+                if not conn.isexecuting():
  104
+                    conn.close()
  105
+                    conns = conns - 1
  106
+                    indexes.append(i)
  107
+                    if conns == 0:
  108
+                        break
  109
+            for i in indexes:
  110
+                self._pool.pop(i)
  111
+
  112
+    def execute(self, operation, parameters=(), callback=None):
  113
+        """http://initd.org/psycopg/docs/cursor.html#cursor.execute
  114
+        """
  115
+        self._new_cursor('execute', (operation, parameters), callback)
  116
+
  117
+    def executemany(self, operation, parameters=None, callback=None):
  118
+        """http://initd.org/psycopg/docs/cursor.html#cursor.executemany
  119
+        """
  120
+        self._new_cursor('executemany', (operation, parameters), callback)
  121
+
  122
+    def callproc(self, procname, parameters=None, callback=None):
  123
+        """http://initd.org/psycopg/docs/cursor.html#cursor.callproc
  124
+        """
  125
+        self._new_cursor('callproc', (procname, parameters), callback)
  126
+
  127
+    def close(self):
  128
+        """Close all open connections.
  129
+        """
  130
+        if self.closed:
  131
+            raise PoolError('connection pool is closed')
  132
+        for conn in self._pool:
  133
+            if not conn.closed:
  134
+                conn.close()
  135
+        self._pool = []
  136
+        self.closed = True
  137
+
  138
+
  139
+class Poller(object):
  140
+    """A poller that polls the PostgreSQL connection and calls the callbacks
  141
+    when the connection state is `POLL_OK`.
  142
+    """
  143
+    def __init__(self, connection, callbacks=()):
  144
+        self._ioloop = IOLoop.instance()
  145
+        self._connection = connection
  146
+        self._callbacks = callbacks
  147
+
  148
+    def start(self):
  149
+        """Start polling the connection.
  150
+        """
  151
+        self._update_handler()
  152
+
  153
+    def _update_handler(self):
  154
+        state = self._connection.poll()
  155
+        if state == psycopg2.extensions.POLL_OK:
  156
+            for callback in self._callbacks:
  157
+                callback()
  158
+        elif state == psycopg2.extensions.POLL_READ:
  159
+            self._ioloop.add_handler(self._connection.fileno(), self._io_callback, IOLoop.READ)
  160
+        elif state == psycopg2.extensions.POLL_WRITE:
  161
+            self._ioloop.add_handler(self._connection.fileno(), self._io_callback, IOLoop.WRITE)
  162
+
  163
+    def _io_callback(self, *args):
  164
+        self._ioloop.remove_handler(self._connection.fileno())
  165
+        self._update_handler()
  166
+
  167
+
  168
+class PoolError(Exception):
  169
+    pass
14  setup.py
... ...
@@ -0,0 +1,14 @@
  1
+#!/usr/bin/env python
  2
+
  3
+from distutils.core import setup
  4
+
  5
+
  6
+setup(
  7
+    name='Momoko',
  8
+    version='0.1.0',
  9
+    description='Asynchronous Psycopg wrapper for Tornado.',
  10
+    author='Frank Smit',
  11
+    author_email='frank@61924.nl',
  12
+    url='https://github.com/FSX/momoko',
  13
+    packages=['momoko']
  14
+)

0 notes on commit 7dd8ec1

Please sign in to comment.
Something went wrong with that request. Please try again.