diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..460e91c --- /dev/null +++ b/.travis.yml @@ -0,0 +1,25 @@ +%YAML 1.1 +--- +language: python +python: + - 2.6 + - 2.7 + - pypy + - 3.2 + - 3.3 + - 3.4 +install: + - pip install -r dev-requirements.txt + - pip install -e . +script: nosetests +after_success: + - coveralls +deploy: + provider: pypi + user: sprockets + on: + python: 2.7 + tags: true + all_branches: true + password: + secure: VPkJqftgZd0dBxovghtRuKSeOL8dNia1eukayXAYVFfdNpP5zQktFSVpdrME3xO0E4ocw0whVHs4Q27Em9ccazlUUJq0FeNOq2jLJoaB9LkFWP2fUAS3kZwQtW+d06k60IuaLdqA94y0zvezjEVIPL8BtoZwZE7nm+Wpy990LXw= diff --git a/LICENSE b/LICENSE index 99b8e75..630f26e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,28 +1,25 @@ -Copyright (c) 2014, Sprockets +Copyright (c) 2014 AWeber Communications All rights reserved. -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the {organization} nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of Sprockets nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..2979b93 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,7 @@ +include LICENSE +include README.rst +include *requirements.txt +graft docs +graft tests.py +global-exclude __pycache__ +global-exclude *.pyc \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index 90ce19e..0000000 --- a/README.md +++ /dev/null @@ -1,2 +0,0 @@ -sprockets.client.postgresql -=========================== diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..1b133a0 --- /dev/null +++ b/README.rst @@ -0,0 +1,65 @@ +sprockets.clients.postgresql +============================ +The ``sprockets.clients.postgresql`` package wraps the +`Queries `_ package providing environment +variable based configuration for connecting to PostgreSQL. + +|Version| |Downloads| |Status| |Coverage| |License| + +Installation +------------ +sprockets.clients.postgresql is available on the +`Python Package Index `_ +and can be installed via ``pip`` or ``easy_install``: + +.. code:: bash + + pip install sprockets.clients.postgresql + +Documentation +------------- +https://sprocketsclientspostgresql.readthedocs.org + +Requirements +------------ +- `queries `_ +- `sprockets `_ + +Example +------- +The following example sets the environment variables for connecting to +PostgreSQL on localhost to the ``postgres`` database and issues a query. + +.. code:: python + + import os + + from sprockets.clients import postgresql + + os.environ['POSTGRES_HOST'] = 'localhost' + os.environ['POSTGRES_USER'] = 'postgres' + os.environ['POSTGRES_PORT'] = 5432 + os.environ['POSTGRES_DBNAME'] = 'postgres' + + session = postgresql.Session('postgres') + result = session.query('SELECT 1') + print(repr(result)) + +Version History +--------------- +Available at https://sprocketsclientspostgresql.readthedocs.org/en/latest/history.html + +.. |Version| image:: https://badge.fury.io/py/sprockets.clients.postgresql.svg? + :target: http://badge.fury.io/py/sprockets.clients.postgresql + +.. |Status| image:: https://travis-ci.org/sprockets/sprockets.clients.postgresql.svg?branch=master + :target: https://travis-ci.org/sprockets/sprockets.clients.postgresql + +.. |Coverage| image:: https://img.shields.io/coveralls/sprockets/sprockets.clients.postgresql.svg? + :target: https://coveralls.io/r/sprockets/sprockets.clients.postgresql + +.. |Downloads| image:: https://pypip.in/d/sprockets.clients.postgresql/badge.svg? + :target: https://pypi.python.org/pypi/sprockets.clients.postgresql + +.. |License| image:: https://pypip.in/license/sprockets.clients.postgresql/badge.svg? + :target: https://sprocketsclientspostgresql.readthedocs.org \ No newline at end of file diff --git a/dev-requirements.txt b/dev-requirements.txt new file mode 100644 index 0000000..dfd7aba --- /dev/null +++ b/dev-requirements.txt @@ -0,0 +1,6 @@ +-r requirements.txt +-r test-requirements.txt +flake8>=2.1,<3 +sphinx>=1.2,<2 +sphinx-rtd-theme>=0.1,<1.0 +sphinxcontrib-httpdomain>=1.2,<2 \ No newline at end of file diff --git a/docs/api.rst b/docs/api.rst new file mode 100644 index 0000000..3a662e1 --- /dev/null +++ b/docs/api.rst @@ -0,0 +1,3 @@ +.. automodule:: sprockets.clients.postgresql + :members: + :inherited-members: \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..a912371 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python +import sphinx_rtd_theme + +from sprockets.clients.postgresql import version_info, __version__ + +needs_sphinx = '1.0' +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.intersphinx', + 'sphinxcontrib.httpdomain', +] +templates_path = [] +source_suffix = '.rst' +master_doc = 'index' +project = 'sprockets.clients.postgresql' +copyright = '2014, AWeber Communications' +version = '.'.join(__version__.split('.')[0:1]) +release = __version__ +if len(version_info) > 3: + release += '-{0}'.format(str(v) for v in version_info[3:]) +exclude_patterns = [] +pygments_style = 'sphinx' +html_theme = 'sphinx_rtd_theme' +html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] +intersphinx_mapping = { + 'python': ('https://docs.python.org/', None), + 'queries': ('https://queries.readthedocs.org/en/latest/', None), + 'requests': ('https://requests.readthedocs.org/en/latest/', None), + 'sprockets': ('https://sprockets.readthedocs.org/en/latest/', None), + 'tornado': ('http://www.tornadoweb.org/en/stable/', None), +} diff --git a/docs/examples.rst b/docs/examples.rst new file mode 100644 index 0000000..7f0b30f --- /dev/null +++ b/docs/examples.rst @@ -0,0 +1,19 @@ +Examples +======== +The following example sets the environment variables for connecting to +PostgreSQL on localhost to the ``postgres`` database and issues a query. + +.. code:: python + + import os + + from sprockets.clients import postgresql + + os.environ['POSTGRES_HOST'] = 'localhost' + os.environ['POSTGRES_USER'] = 'postgres' + os.environ['POSTGRES_PORT'] = 5432 + os.environ['POSTGRES_DBNAME'] = 'postgres' + + session = postgresql.Session('postgres') + result = session.query('SELECT 1') + print(repr(result)) diff --git a/docs/history.rst b/docs/history.rst new file mode 100644 index 0000000..bf9b59a --- /dev/null +++ b/docs/history.rst @@ -0,0 +1,4 @@ +Version History +--------------- +- 1.0.0 [2014-08-29] + - Initial release diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..d26dd5d --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,68 @@ +sprockets.clients.postgresql +============================ +The ``sprockets.clients.postgresql`` package wraps the +`Queries `_ package providing environment +variable based configuration for connecting to PostgreSQL. + +|Version| |Downloads| |Status| |Coverage| |License| + +Installation +------------ +``sprockets.clients.postgresql`` is available on the +`Python Package Index `_ +and can be installed via ``pip`` or ``easy_install``: + +.. code:: bash + + pip install sprockets.clients.postgresql + +Requirements +------------ +- `queries `_ +- `sprockets `_ + +API Documentation +----------------- +.. toctree:: + :maxdepth: 2 + + api + examples + +Version History +--------------- +See :doc:`history` + +Issues +------ +Please report any issues to the Github project at `https://github.com/sprockets/sprockets.clients.postgresql/issues `_ + +Source +------ +sprockets.clients.postgresql source is available on Github at `https://github.com/sprockets/sprockets.clients.postgresql `_ + +License +------- +sprockets.clients.postgresql is released under the `3-Clause BSD license `_. + +Indices and tables +------------------ + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + +.. |Version| image:: https://badge.fury.io/py/sprockets.clients.postgresql.svg? + :target: http://badge.fury.io/py/sprockets.clients.postgresql + +.. |Status| image:: https://travis-ci.org/sprockets/sprockets.clients.postgresql.svg?branch=master + :target: https://travis-ci.org/sprockets/sprockets.clients.postgresql + +.. |Coverage| image:: https://img.shields.io/coveralls/sprockets/sprockets.clients.postgresql.svg? + :target: https://coveralls.io/r/sprockets/sprockets.clients.postgresql + +.. |Downloads| image:: https://pypip.in/d/sprockets.clients.postgresql/badge.svg? + :target: https://pypi.python.org/pypi/sprockets.clients.postgresql + +.. |License| image:: https://pypip.in/license/sprockets.clients.postgresql/badge.svg? + :target: https://sprocketsclientspostgresql.readthedocs.org \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..96281db --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +queries>=1.2.1 +sprockets>=0.1.1 +tornado>=4.0.1 \ No newline at end of file diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..ad674e0 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,12 @@ +[build_sphinx] +all-files = 1 +source-dir = docs +build-dir = build/docs + +[nosetests] +with-coverage = 1 +cover-package = sprockets.clients.postgresql +verbose = 1 + +[flake8] +exclude = build,dist,docs,env \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..b72fa9f --- /dev/null +++ b/setup.py @@ -0,0 +1,69 @@ +import codecs +import sys + +import setuptools + + +def read_requirements_file(req_name): + requirements = [] + try: + with codecs.open(req_name, encoding='utf-8') as req_file: + for req_line in req_file: + if '#' in req_line: + req_line = req_line[0:req_line.find('#')].strip() + if req_line: + requirements.append(req_line.strip()) + except IOError: + pass + return requirements + + +install_requires = read_requirements_file('requirements.txt') +setup_requires = read_requirements_file('setup-requirements.txt') +tests_require = read_requirements_file('test-requirements.txt') + +if sys.version_info < (2, 7): + tests_require.append('unittest2') +if sys.version_info < (3, 0): + tests_require.append('mock') + +setuptools.setup( + name='sprockets.clients.postgresql', + version='1.0.0', + description=('PostgreSQL client library wrapper providing environment ' + 'variable based configuration'), + long_description=codecs.open('README.rst', encoding='utf-8').read(), + url='https://github.com/sprockets/sprockets.clients.postgresql.git', + author='AWeber Communications', + author_email='api@aweber.com', + license=codecs.open('LICENSE', encoding='utf-8').read(), + classifiers=[ + 'Development Status :: 4 - Beta', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: BSD License', + 'Natural Language :: English', + 'Operating System :: OS Independent', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.2', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: Implementation :: CPython', + 'Programming Language :: Python :: Implementation :: PyPy', + 'Topic :: Software Development :: Libraries', + 'Topic :: Software Development :: Libraries :: Python Modules' + ], + packages=['sprockets', + 'sprockets.clients', + 'sprockets.clients.postgresql'], + package_data={'': ['LICENSE', 'README.md']}, + include_package_data=True, + namespace_packages=['sprockets', + 'sprockets.clients'], + install_requires=install_requires, + setup_requires=setup_requires, + tests_require=tests_require, + test_suite='nose.collector', + zip_safe=False) diff --git a/sprockets/__init__.py b/sprockets/__init__.py new file mode 100644 index 0000000..de40ea7 --- /dev/null +++ b/sprockets/__init__.py @@ -0,0 +1 @@ +__import__('pkg_resources').declare_namespace(__name__) diff --git a/sprockets/clients/__init__.py b/sprockets/clients/__init__.py new file mode 100644 index 0000000..de40ea7 --- /dev/null +++ b/sprockets/clients/__init__.py @@ -0,0 +1 @@ +__import__('pkg_resources').declare_namespace(__name__) diff --git a/sprockets/clients/postgresql/__init__.py b/sprockets/clients/postgresql/__init__.py new file mode 100644 index 0000000..53c9be6 --- /dev/null +++ b/sprockets/clients/postgresql/__init__.py @@ -0,0 +1,125 @@ +""" +PostgreSQL Session Classes +========================== +The Session classes wrap the Queries :py:class:`Session ` and +:py:class:`TornadoSession ` classes +providing environment variable based configuration. + +The environment variables should be set using the ``DBNAME_[VARIABLE]`` format +where ``[VARIABLE]`` is one of ``HOST``, ``PORT``, ``DBNAME``, ``USER``, and +``PASSWORD``. + +For example, given the environment variables: + +.. code:: python + + FOO_HOST = 'foodb' + FOO_PORT = '6000' + FOO_DBNAME = 'foo' + FOO_USER = 'bar' + FOO_PASSWORD = 'baz' + +and code for creating a :py:class:`Session` instance for the database name +``foo``: + +.. code:: python + + session = sprockets.postgresql.Session('foo') + +The uri ``postgresql://bar:baz@foodb:6000/foo`` will be used when creating the +instance of :py:class:`queries.Session`. + +""" +version_info = (1, 0, 0) +__version__ = '.'.join(str(v) for v in version_info) + +import logging +import os + +from queries import pool +import queries +from queries import tornado_session + +_ARGUMENTS = ['host', 'port', 'dbname', 'user', 'password'] + +LOGGER = logging.getLogger(__name__) + + +def _get_uri(dbname): + """Construct the URI for connecting to PostgreSQL by appending each + argument name to the dbname, delimited by an underscore and + capitalizing the new variable name. + + Values will be retrieved from the environment variable and added to a + dictionary that is then passed in as keyword arguments to the + :py:meth:`queries.uri` method to build the URI string. + + :param str dbname: The database name to construct the URI for + :return: str + + """ + kwargs = dict() + for arg in _ARGUMENTS: + value = os.getenv(('%s_%s' % (dbname, arg)).upper()) + if value: + if arg == 'port': + kwargs[arg] = int(value) + else: + kwargs[arg] = value + return queries.uri(**kwargs) + + +class Session(queries.Session): + """Extends queries.Session using configuration data that is stored + in environment variables. + + Utilizes connection pooling to ensure that multiple concurrent asynchronous + queries do not block each other. Heavily trafficked services will require + a higher ``max_pool_size`` to allow for greater connection concurrency. + + :param str dbname: PostgreSQL database name + :param queries.cursor: The cursor type to use + :param int pool_idle_ttl: How long idle pools keep connections open + :param int pool_max_size: The maximum size of the pool to use + + """ + def __init__(self, dbname, + cursor_factory=queries.RealDictCursor, + pool_idle_ttl=pool.DEFAULT_IDLE_TTL, + pool_max_size=pool.DEFAULT_MAX_SIZE): + super(Session, self).__init__(_get_uri(dbname), + cursor_factory, + pool_idle_ttl, + pool_max_size) + + +class TornadoSession(tornado_session.TornadoSession): + """Extends queries.TornadoSession using configuration data that is stored + in environment variables. + + Utilizes connection pooling to ensure that multiple concurrent asynchronous + queries do not block each other. Heavily trafficked services will require + a higher ``max_pool_size`` to allow for greater connection concurrency. + + :py:meth:`query ` and + :py:meth:`callproc ` must + call :py:meth:`Results.free ` + + :param str dbname: PostgreSQL database name + :param queries.cursor: The cursor type to use + :param int pool_idle_ttl: How long idle pools keep connections open + :param int pool_max_size: The maximum size of the pool to use + :param tornado.ioloop.IOLoop ioloop: Pass in the instance of the tornado + IOLoop you would like to use. Defaults to the global instance. + + """ + def __init__(self, dbname, + cursor_factory=queries.RealDictCursor, + pool_idle_ttl=pool.DEFAULT_IDLE_TTL, + pool_max_size=tornado_session.DEFAULT_MAX_POOL_SIZE, + io_loop=None): + super(TornadoSession, self).__init__(_get_uri(dbname), + cursor_factory, + pool_idle_ttl, + pool_max_size, + io_loop) diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000..09ac575 --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,3 @@ +coverage>=3.7,<4 +coveralls>=0.4,<1 +nose>=1.3,<2 \ No newline at end of file diff --git a/tests.py b/tests.py new file mode 100644 index 0000000..5677f8e --- /dev/null +++ b/tests.py @@ -0,0 +1,58 @@ +""" +Tests for the sprockets.clients.postgresql package + +""" +import mock +import os +try: + import unittest2 as unittest +except ImportError: + import unittest + +from sprockets.clients import postgresql + + +class TestGetURI(unittest.TestCase): + + def test_get_uri_returns_proper_values(self): + + os.environ['TEST1_HOST'] = 'test1-host' + os.environ['TEST1_PORT'] = '5436' + os.environ['TEST1_DBNAME'] = 'test1' + os.environ['TEST1_USER'] = 'foo1' + os.environ['TEST1_PASSWORD'] = 'baz1' + + self.assertEqual(postgresql._get_uri('test1'), + 'postgresql://foo1:baz1@test1-host:5436/test1') + + +class TestSession(unittest.TestCase): + + @mock.patch('queries.session.Session.__init__') + def setUp(self, mock_init): + self.mock_init = mock_init + os.environ['TEST2_HOST'] = 'db1' + os.environ['TEST2_PORT'] = '5433' + os.environ['TEST2_DBNAME'] = 'bar' + os.environ['TEST2_USER'] = 'foo' + os.environ['TEST2_PASSWORD'] = 'baz' + self.session = postgresql.Session('test2') + + def test_session_invokes_queries_session(self): + self.assertTrue(self.mock_init.called) + + +class TestTornadoSession(unittest.TestCase): + + @mock.patch('queries.tornado_session.TornadoSession.__init__') + def setUp(self, mock_init): + self.mock_init = mock_init + os.environ['TEST3_HOST'] = 'db1' + os.environ['TEST3_PORT'] = '5434' + os.environ['TEST3_DBNAME'] = 'bar' + os.environ['TEST3_USER'] = 'foo' + os.environ['TEST3_PASSWORD'] = 'baz' + self.session = postgresql.TornadoSession('test3') + + def test_session_invokes_queries_session(self): + self.assertTrue(self.mock_init.called)