pgbouncer-0.0.9/0000755000175000017500000000000013477671457015233 5ustar cjwatsoncjwatson00000000000000pgbouncer-0.0.9/PKG-INFO0000644000175000017500000000726413477671457016341 0ustar cjwatsoncjwatson00000000000000Metadata-Version: 1.1 Name: pgbouncer Version: 0.0.9 Summary: Fixture to bring up temporary pgbouncer instance. Home-page: https://launchpad.net/python-pgbouncer Author: Launchpad Developers Author-email: launchpad-dev@lists.launchpad.net License: UNKNOWN Description: ************************************************ python-pgbouncer: Python glue to drive pgbouncer ************************************************ Copyright (c) 2011, Canonical Ltd This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . python-pgbouncer provides a python interface to setup and run a pgbouncer instance. Dependencies ============ * Python 2.6+ * pgbouncer * python-fixtures (https://launchpad.net/python-fixtures or https://pypi.org/project/fixtures) * testtools (https://pypi.org/project/testtools) Testing Dependencies ==================== In addition to the above, the tests also depend on: * postgresfixture (https://pypi.org/project/postgresfixture) * psycopg2 (https://pypi.org/project/psycopg2) * subunit (https://pypi.org/project/python-subunit) (optional) * testscenarios (https://pypi.org/project/testscenarios) Usage ===== Create a PGBouncerFixture - a context manager with an extended protocol supporting access to logs etc. Customise it with database definitions, user credentials, and then when you enter the context it will create a transient pgbouncer setup in a temporary directory and run it for the duration that the context is open. For instance:: >>> from pgbouncer import PGBouncerFixture >>> bouncer = PGBouncerFixture() >>> bouncer.databases['mydb'] = 'host=hostname dbname=foo' >>> bouncer.users['user1'] = 'credentials' >>> with bouncer: ... # Can now connect to bouncer.host port=bouncer.port user=user1 Any settings required for pgbouncer to work will be given sane defaults. Installation ============ Either run setup.py in an environment with all the dependencies available, or add the working directory to your PYTHONPATH. Development =========== Upstream development takes place at https://launchpad.net/python-pgbouncer. To run the tests, run: $ tox Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: GNU Affero General Public License v3 Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 3 pgbouncer-0.0.9/setup.py0000644000175000017500000000352013477671373016742 0ustar cjwatsoncjwatson00000000000000#!/usr/bin/env python # # Copyright (c) 2011, Canonical Ltd # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . from distutils.core import setup import os.path with open(os.path.join(os.path.dirname(__file__), 'README')) as f: description = f.read() setup(name="pgbouncer", version="0.0.9", description="Fixture to bring up temporary pgbouncer instance.", long_description=description, maintainer="Launchpad Developers", maintainer_email="launchpad-dev@lists.launchpad.net", url="https://launchpad.net/python-pgbouncer", packages=['pgbouncer'], package_dir={'': '.'}, classifiers=[ 'Development Status :: 4 - Beta', 'Intended Audience :: Developers', 'License :: OSI Approved :: GNU Affero General Public License v3', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 3', ], install_requires=[ 'fixtures', 'psycopg2', ], extras_require=dict( test=[ 'postgresfixture', 'testscenarios', 'testtools', ] ), ) pgbouncer-0.0.9/pgbouncer/0000755000175000017500000000000013477671457017217 5ustar cjwatsoncjwatson00000000000000pgbouncer-0.0.9/pgbouncer/fixture.py0000644000175000017500000001500613477671203021246 0ustar cjwatsoncjwatson00000000000000# # Copyright (c) 2011, Canonical Ltd # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . __all__ = [ 'PGBouncerFixture', ] import itertools import os.path import socket import subprocess import time from fixtures import Fixture, TempDir from testtools.content import content_from_file def countdown(duration=60, sleep=0.1): """Provide a countdown iterator that sleeps between iterations. Yields the current iteration count, starting from 1. The duration can be in fractional seconds. """ start = time.time() stop = start + duration for iteration in itertools.count(1): now = time.time() if now < stop: yield iteration time.sleep(sleep) else: break def _allocate_ports(n=1): """Allocate `n` unused ports. There is a small race condition here (between the time we allocate the port, and the time it actually gets used), but for the purposes for which this function gets used it isn't a problem in practice. """ sockets = [socket.socket() for _ in range(n)] try: for s in sockets: s.bind(('localhost', 0)) return [s.getsockname()[1] for s in sockets] finally: for s in sockets: s.close() class PGBouncerFixture(Fixture): """Programmatically configure and run pgbouncer. Minimal usage: >>> bouncer = PGBouncerFixture() >>> bouncer.databases['mydb'] = 'host=hostname dbname=foo' >>> bouncer.users['user1'] = 'credentials' >>> with bouncer: ... connection = psycopg2.connect( ... database="mydb", host=bouncer.host, port=bouncer.port, ... user="user1", password="credentials") """ def __init__(self): super(PGBouncerFixture, self).__init__() # defaults # pgbouncer -> path to pgbouncer executable self.pgbouncer = 'pgbouncer' # dbname -> connect string self.databases = {} # username -> details self.users = {} # list of usernames that can all console queries self.admin_users = [] # list of usernames that can run readonly console queries self.stats_users = [] self.pool_mode = 'session' self.unix_socket_dir = None self.process = None def setUp(self): super(PGBouncerFixture, self).setUp() self.addCleanup(self.stop) self.host = '127.0.0.1' self.port = _allocate_ports()[0] self.configdir = self.useFixture(TempDir()) self.auth_type = 'trust' self.setUpConf() self.start() def setUpConf(self): """Create a pgbouncer.ini file.""" self.inipath = os.path.join(self.configdir.path, 'pgbouncer.ini') self.authpath = os.path.join(self.configdir.path, 'users.txt') self.logpath = os.path.join(self.configdir.path, 'pgbouncer.log') self.pidpath = os.path.join(self.configdir.path, 'pgbouncer.pid') self.outputpath = os.path.join(self.configdir.path, 'output') with open(self.inipath, 'wt') as inifile: inifile.write('[databases]\n') for item in self.databases.items(): inifile.write('%s = %s\n' % item) inifile.write('[pgbouncer]\n') inifile.write('pool_mode = %s\n' % (self.pool_mode,)) inifile.write('listen_port = %s\n' % (self.port,)) inifile.write('listen_addr = %s\n' % (self.host,)) if self.unix_socket_dir is not None: inifile.write( 'unix_socket_dir = %s\n' % (self.unix_socket_dir,)) inifile.write('auth_type = %s\n' % (self.auth_type,)) inifile.write('auth_file = %s\n' % (self.authpath,)) inifile.write('logfile = %s\n' % (self.logpath,)) inifile.write('pidfile = %s\n' % (self.pidpath,)) adminusers = ','.join(self.admin_users) inifile.write('admin_users = %s\n' % (adminusers,)) statsusers = ','.join(self.stats_users) inifile.write('stats_users = %s\n' % (statsusers,)) with open(self.authpath, 'wt') as authfile: for user_creds in self.users.items(): authfile.write('"%s" "%s"\n' % user_creds) @property def is_running(self): return ( # pgbouncer has been started. self.process is not None and # pgbouncer has not yet exited. self.process.poll() is None) def stop(self): if not self.is_running: return self.process.terminate() for iteration in countdown(): if self.process.poll() is not None: break else: raise Exception( 'Time-out waiting for pgbouncer to exit.') def start(self): if self.is_running: return # Add /usr/sbin if necessary to the PATH for magic just-works # behavior with Ubuntu. env = os.environ.copy() if not self.pgbouncer.startswith('/'): path = env['PATH'].split(os.pathsep) if '/usr/sbin' not in path: path.append('/usr/sbin') env['PATH'] = os.pathsep.join(path) with open(self.outputpath, "wb") as outputfile: with open(os.devnull, "rb") as devnull: self.process = subprocess.Popen( [self.pgbouncer, self.inipath], env=env, stdin=devnull, stdout=outputfile, stderr=outputfile) self.addDetail( os.path.basename(self.outputpath), content_from_file(self.outputpath)) # Wait for the PID file to appear. for iteration in countdown(): if os.path.isfile(self.pidpath): with open(self.pidpath, "rb") as pidfile: if pidfile.read().strip().isdigit(): break else: raise Exception( 'Time-out waiting for pgbouncer to create PID file.') pgbouncer-0.0.9/pgbouncer/tests.py0000644000175000017500000000711713477671103020725 0ustar cjwatsoncjwatson00000000000000# # Copyright (c) 2011, Canonical Ltd # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . from contextlib import closing import fixtures from postgresfixture import ClusterFixture from postgresfixture.cluster import PG_VERSIONS import psycopg2 import testscenarios import testtools from pgbouncer.fixture import PGBouncerFixture class TestFixture(testscenarios.WithScenarios, testtools.TestCase): scenarios = sorted( (version, {'version': version}) for version in PG_VERSIONS) def setUp(self): super(TestFixture, self).setUp() datadir = self.useFixture(fixtures.TempDir()).path self.dbname = 'test_pgbouncer' self.cluster = self.useFixture( ClusterFixture(datadir, version=self.version)) self.cluster.createdb(self.dbname) with closing(self.cluster.connect()) as conn: with closing(conn.cursor()) as cur: cur.execute('DROP USER IF EXISTS user1') cur.execute('CREATE USER user1') self.bouncer = PGBouncerFixture() self.bouncer.databases[self.dbname] = 'host=' + datadir self.bouncer.users['user1'] = '' def connect(self, host=None): return psycopg2.connect( host=(self.bouncer.host if host is None else host), port=self.bouncer.port, database=self.dbname, user='user1') def test_dynamic_port_allocation(self): self.useFixture(self.bouncer) self.connect().close() def test_stop_start_facility(self): # Once setup the fixture can be stopped, and started again, retaining # its configuration. [Note that dynamically allocated ports could # potentially be used by a different process, so this isn't perfect, # but its pretty reliable as a test helper, and manual port allocation # outside the dynamic range should be fine. self.useFixture(self.bouncer) self.bouncer.stop() self.assertRaises(psycopg2.OperationalError, self.connect) self.bouncer.start() self.connect().close() def test_unix_sockets(self): unix_socket_dir = self.useFixture(fixtures.TempDir()).path self.bouncer.unix_socket_dir = unix_socket_dir self.useFixture(self.bouncer) # Connect to pgbouncer via a Unix domain socket. We don't # care how pgbouncer connects to PostgreSQL. self.connect(host=unix_socket_dir).close() def test_is_running(self): # The is_running property indicates if pgbouncer has been started and # has not yet exited. self.assertFalse(self.bouncer.is_running) with self.bouncer: self.assertTrue(self.bouncer.is_running) self.assertFalse(self.bouncer.is_running) def test_dont_start_if_already_started(self): # If pgbouncer is already running, don't start another one. self.useFixture(self.bouncer) bouncer_pid = self.bouncer.process.pid self.bouncer.start() self.assertEqual(bouncer_pid, self.bouncer.process.pid) pgbouncer-0.0.9/pgbouncer/__init__.py0000644000175000017500000000300713476453542021320 0ustar cjwatsoncjwatson00000000000000# # Copyright (c) 2011, Canonical Ltd # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # same format as sys.version_info: "A tuple containing the five components of # the version number: major, minor, micro, releaselevel, and serial. All # values except releaselevel are integers; the release level is 'alpha', # 'beta', 'candidate', or 'final'. The version_info value corresponding to the # Python version 2.0 is (2, 0, 0, 'final', 0)." Additionally we use a # releaselevel of 'dev' for unreleased under-development code. # # If the releaselevel is 'alpha' then the major/minor/micro components are not # established at this point, and setup.py will use a version of next-$(revno). # If the releaselevel is 'final', then the tarball will be major.minor.micro. # Otherwise it is major.minor.micro~$(revno). __version__ = (0, 0, 7, 'beta', 0) __all__ = [ 'PGBouncerFixture', ] from pgbouncer.fixture import PGBouncerFixture pgbouncer-0.0.9/README0000644000175000017500000000463713477671103016111 0ustar cjwatsoncjwatson00000000000000************************************************ python-pgbouncer: Python glue to drive pgbouncer ************************************************ Copyright (c) 2011, Canonical Ltd This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . python-pgbouncer provides a python interface to setup and run a pgbouncer instance. Dependencies ============ * Python 2.6+ * pgbouncer * python-fixtures (https://launchpad.net/python-fixtures or https://pypi.org/project/fixtures) * testtools (https://pypi.org/project/testtools) Testing Dependencies ==================== In addition to the above, the tests also depend on: * postgresfixture (https://pypi.org/project/postgresfixture) * psycopg2 (https://pypi.org/project/psycopg2) * subunit (https://pypi.org/project/python-subunit) (optional) * testscenarios (https://pypi.org/project/testscenarios) Usage ===== Create a PGBouncerFixture - a context manager with an extended protocol supporting access to logs etc. Customise it with database definitions, user credentials, and then when you enter the context it will create a transient pgbouncer setup in a temporary directory and run it for the duration that the context is open. For instance:: >>> from pgbouncer import PGBouncerFixture >>> bouncer = PGBouncerFixture() >>> bouncer.databases['mydb'] = 'host=hostname dbname=foo' >>> bouncer.users['user1'] = 'credentials' >>> with bouncer: ... # Can now connect to bouncer.host port=bouncer.port user=user1 Any settings required for pgbouncer to work will be given sane defaults. Installation ============ Either run setup.py in an environment with all the dependencies available, or add the working directory to your PYTHONPATH. Development =========== Upstream development takes place at https://launchpad.net/python-pgbouncer. To run the tests, run: $ tox