pgbouncer-0.0.9/ 0000755 0001750 0001750 00000000000 13477671457 015233 5 ustar cjwatson cjwatson 0000000 0000000 pgbouncer-0.0.9/PKG-INFO 0000644 0001750 0001750 00000007264 13477671457 016341 0 ustar cjwatson cjwatson 0000000 0000000 Metadata-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.py 0000644 0001750 0001750 00000003520 13477671373 016742 0 ustar cjwatson cjwatson 0000000 0000000 #!/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/ 0000755 0001750 0001750 00000000000 13477671457 017217 5 ustar cjwatson cjwatson 0000000 0000000 pgbouncer-0.0.9/pgbouncer/fixture.py 0000644 0001750 0001750 00000015006 13477671203 021246 0 ustar cjwatson cjwatson 0000000 0000000 #
# 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.py 0000644 0001750 0001750 00000007117 13477671103 020725 0 ustar cjwatson cjwatson 0000000 0000000 #
# 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__.py 0000644 0001750 0001750 00000003007 13476453542 021320 0 ustar cjwatson cjwatson 0000000 0000000 #
# 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/README 0000644 0001750 0001750 00000004637 13477671103 016111 0 ustar cjwatson cjwatson 0000000 0000000 ************************************************
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