From 280db40236faa80086e29a42766f53c0372cecd9 Mon Sep 17 00:00:00 2001 From: "Gavin M. Roy" Date: Mon, 26 Nov 2018 15:22:51 -0500 Subject: [PATCH] 2.0 Release MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Drop support for Python 2.7, 3.3, 3.4 as we no longer support these versions internally and nothing should be using them - Drop support for Tornado < 4.2 since this is also out of date - Add support for Tornado 5.1 and async with ``AsyncIOHandlerMixin`` - Update tests to include tests for the new ``AsyncIOHandlerMixin`` - Clean up code style a bit to make namespaces a bit more clear - Cleaup setup.py a minor bit and have it ignore ‘-r’ includes in requirements - Update PINs for requirements - Update LICENSE copyright dates --- HISTORY.rst | 8 ++- LICENSE | 2 +- dev-requirements.txt | 3 +- docs/index.rst | 4 ++ requirements.txt | 2 +- setup.py | 36 ++++++------ sprockets/mixins/correlation/__init__.py | 6 +- sprockets/mixins/correlation/mixins.py | 73 +++++++++++++++++++----- test-requirements.txt | 8 +-- tests.py | 48 +++++++++++++--- tox.ini | 26 +++------ 11 files changed, 146 insertions(+), 70 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 1a621c3..f37329e 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,6 +1,12 @@ Version History --------------- +`2.0.0`_ (26-Nov-2018) +~~~~~~~~~~~~~~~~~~~~~~ +- Drop support for Python 2.7, 3.3, 3.4 +- Drop support for Tornado < 4.2 +- Add support for Tornado 5.1 and async with ``AsyncIOHandlerMixin`` + `1.0.2`_ (20-Jun-2016) ~~~~~~~~~~~~~~~~~~~~~~ - Add support for async prepare in superclasses of ``HandlerMixin`` @@ -9,6 +15,6 @@ Version History ~~~~~~~~~~~~~~~~~~~~~~ - Adds ``sprockets.mixins.correlation.HandlerMixin`` - +.. _`2.0.0`: https://github.com/sprockets/sprockets.mixins.correlation/compare/1.0.2...2.0.0 .. _`1.0.2`: https://github.com/sprockets/sprockets.mixins.correlation/compare/1.0.1...1.0.2 .. _`1.0.1`: https://github.com/sprockets/sprockets.mixins.correlation/compare/0.0.0...1.0.1 diff --git a/LICENSE b/LICENSE index 3c72859..4fa05e6 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2015 AWeber Communications +Copyright (c) 2015-2018 AWeber Communications All rights reserved. Redistribution and use in source and binary forms, with or without modification, diff --git a/dev-requirements.txt b/dev-requirements.txt index 4e8c0ae..8870054 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,6 +1,5 @@ -r requirements.txt -r test-requirements.txt -flake8>=2.1,<3 +flake8 sphinx>=1.2,<2 sphinx-rtd-theme>=0.1,<1.0 -tornado>=4.0,<5 diff --git a/docs/index.rst b/docs/index.rst index 30bedc7..0d0efc5 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -8,6 +8,10 @@ correlation.HandlerMixin .. autoclass:: sprockets.mixins.correlation.HandlerMixin :members: +.. autoclass:: sprockets.mixins.correlation.AsyncIOHandlerMixin + :members: + + Contributing to this Library ---------------------------- diff --git a/requirements.txt b/requirements.txt index 3f55f96..b33c704 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -tornado>=3.1,<4.4 +tornado>=4.0,<5.2 diff --git a/setup.py b/setup.py index 27435c0..c9c7f31 100755 --- a/setup.py +++ b/setup.py @@ -1,5 +1,6 @@ #!/usr/bin/env python import codecs +from os import path import sys import setuptools @@ -7,24 +8,23 @@ import setuptools from sprockets.mixins import correlation -def read_requirements_file(req_name): +def read_requirements(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()) + with open(path.join('requires', name)) as req_file: + for line in req_file: + if '#' in line: + line = line[:line.index('#')] + line = line.strip() + if line.startswith('-r'): + requirements.extend(read_requirements(line[2:].strip())) + elif line and not line.startswith('-'): + requirements.append(line) 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') - setuptools.setup( name='sprockets.mixins.correlation', version=correlation.__version__, @@ -35,16 +35,15 @@ setuptools.setup( author_email='api@aweber.com', license=codecs.open('LICENSE', encoding='utf-8').read(), classifiers=[ - 'Development Status :: 4 - Beta', + 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Natural Language :: English', 'Operating System :: OS Independent', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.3', - 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Software Development :: Libraries', @@ -52,9 +51,8 @@ setuptools.setup( ], packages=setuptools.find_packages(), namespace_packages=['sprockets'], - install_requires=install_requires, - setup_requires=setup_requires, - tests_require=tests_require, + install_requires=read_requirements('requirements.txt'), + tests_require=read_requirements('test-requirements.txt'), test_suite='nose.collector', zip_safe=True, ) diff --git a/sprockets/mixins/correlation/__init__.py b/sprockets/mixins/correlation/__init__.py index 0dfe0ff..aaa1b3f 100644 --- a/sprockets/mixins/correlation/__init__.py +++ b/sprockets/mixins/correlation/__init__.py @@ -1,5 +1,5 @@ try: - from .mixins import HandlerMixin + from .mixins import HandlerMixin, AsyncIOHandlerMixin except ImportError as error: @@ -8,5 +8,9 @@ except ImportError as error: raise error + class AsyncIOHandlerMixin(object): + def __init__(self, *args, **kwargs): + raise error + version_info = (1, 0, 2) __version__ = '.'.join(str(v) for v in version_info[:3]) diff --git a/sprockets/mixins/correlation/mixins.py b/sprockets/mixins/correlation/mixins.py index 121c4d6..581170f 100644 --- a/sprockets/mixins/correlation/mixins.py +++ b/sprockets/mixins/correlation/mixins.py @@ -1,15 +1,6 @@ import uuid -import tornado.gen -import tornado.log - -if tornado.version_info[0] >= 4: - from tornado.concurrent import is_future -else: - import tornado.concurrent - - def is_future(maybe_future): - return isinstance(maybe_future, tornado.concurrent.Future) +from tornado import concurrent, gen, log class HandlerMixin(object): @@ -50,13 +41,13 @@ class HandlerMixin(object): self.__correlation_id = str(uuid.uuid4()) super(HandlerMixin, self).__init__(*args, **kwargs) - @tornado.gen.coroutine + @gen.coroutine def prepare(self): # Here we want to copy an incoming Correlation-ID header if # one exists. We also want to set it in the outgoing response # which the property setter does for us. maybe_future = super(HandlerMixin, self).prepare() - if is_future(maybe_future): + if concurrent.is_future(maybe_future): yield maybe_future correlation_id = self.get_request_header(self.__header_name, None) @@ -98,6 +89,58 @@ class HandlerMixin(object): return self.request.headers.get(name, default) +class AsyncIOHandlerMixin(HandlerMixin): + """ + Mix this in over a ``RequestHandler`` for a correlating header for use + with AsyncIO when using ``async def`` and ``await`` style asynchronous + request handlers. + + :keyword str correlation_header: the name of the header to use + for correlation. If this keyword is omitted, then the header + is named ``Correlation-ID``. + + This mix-in ensures that responses include a header that correlates + requests and responses. If there header is set on the incoming + request, then it will be copied to the outgoing response. Otherwise, + a new UUIDv4 will be generated and inserted. The value can be + examined or modified via the ``correlation_id`` property. + + The MRO needs to contain something that resembles a standard + :class:`tornado.web.RequestHandler`. Specifically, we need the + following things to be available: + + - :meth:`~tornado.web.RequestHandler.prepare` needs to be called + appropriately + - :meth:`~tornado.web.RequestHandler.set_header` needs to exist in + the MRO and it needs to overwrite the header value + - :meth:`~tornado.web.RequestHandler.set_default_headers` should be + called to establish the default header values + - ``self.request`` is a object that has a ``headers`` property that + contains the request headers as a ``dict``. + + """ + def __init__(self, *args, **kwargs): + # correlation_id is used from within set_default_headers + # which is called from within super().__init__() so we need + # to make sure that it is set *BEFORE* we call super. + self.__header_name = kwargs.pop( + 'correlation_header', 'Correlation-ID') + self.__correlation_id = str(uuid.uuid4()) + super(AsyncIOHandlerMixin, self).__init__(*args, **kwargs) + + async def prepare(self): + # Here we want to copy an incoming Correlation-ID header if + # one exists. We also want to set it in the outgoing response + # which the property setter does for us. + maybe_future = super(HandlerMixin, self).prepare() + if concurrent.is_future(maybe_future): + await maybe_future + + correlation_id = self.get_request_header(self.__header_name, None) + if correlation_id is not None: + self.correlation_id = correlation_id + + def correlation_id_logger(handler): """ Custom Tornado access log writer that appends correlation-id. @@ -113,11 +156,11 @@ def correlation_id_logger(handler): is processing the client request. """ if handler.get_status() < 400: - log_method = tornado.log.access_log.info + log_method = log.access_log.info elif handler.get_status() < 500: - log_method = tornado.log.access_log.warning + log_method = log.access_log.warning else: - log_method = tornado.log.access_log.error + log_method = log.access_log.error request_time = 1000.0 * handler.request.request_time() correlation_id = getattr(handler, "correlation_id", None) if correlation_id is None: diff --git a/test-requirements.txt b/test-requirements.txt index dc209eb..27a5c8e 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,4 +1,4 @@ -coverage>=3.7,<4 -coveralls>=0.4,<1 -nose>=1.3,<2 -tox>=1.7,<2 +coverage>=4.5.2,<5 +coveralls>=1.5.1,<2 +nose>=1.3.7,<2 +tox>=3.5.3,<4 diff --git a/tests.py b/tests.py index 9abe799..2987449 100644 --- a/tests.py +++ b/tests.py @@ -1,7 +1,7 @@ import uuid import unittest -from tornado import testing, web +from tornado import testing, version_info, web from sprockets.mixins import correlation @@ -15,6 +15,16 @@ class CorrelatedRequestHandler(correlation.HandlerMixin, web.RequestHandler): self.write('status {0}'.format(status_code)) +class AsyncIOCorrelatedRequestHandler(correlation.AsyncIOHandlerMixin, + web.RequestHandler): + + def get(self, status_code): + status_code = int(status_code) + if status_code >= 300: + raise web.HTTPError(status_code) + self.write('status {0}'.format(status_code)) + + class CorrelationMixinTests(testing.AsyncHTTPTestCase): def get_app(self): @@ -23,18 +33,40 @@ class CorrelationMixinTests(testing.AsyncHTTPTestCase): ]) def test_that_correlation_id_is_returned_when_successful(self): - self.http_client.fetch(self.get_url('/status/200'), self.stop) - response = self.wait() + response = self.fetch('/status/200') self.assertIsNotNone(response.headers.get('Correlation-ID')) def test_that_correlation_id_is_returned_in_error(self): - self.http_client.fetch(self.get_url('/status/500'), self.stop) - response = self.wait() + response = self.fetch('/status/500') self.assertIsNotNone(response.headers.get('Correlation-ID')) def test_that_correlation_id_is_copied_from_request(self): correlation_id = uuid.uuid4().hex - self.http_client.fetch(self.get_url('/status/200'), self.stop, - headers={'Correlation-Id': correlation_id}) - response = self.wait() + response = self.fetch('/status/500', + headers={'Correlation-Id': correlation_id}) + self.assertEqual(response.headers['correlation-id'], correlation_id) + + +class AsyncIOCorrelationMixinTests(testing.AsyncHTTPTestCase): + + def get_app(self): + return web.Application([ + (r'/status/(?P\d+)', AsyncIOCorrelatedRequestHandler), + ]) + + @unittest.skipIf(version_info < (4,3,0,0), 'tornado < 4.3') + def test_that_correlation_id_is_returned_when_successful(self): + response = self.fetch('/status/200') + self.assertIsNotNone(response.headers.get('Correlation-ID')) + + @unittest.skipIf(version_info < (4,3,0,0), 'tornado < 4.3') + def test_that_correlation_id_is_returned_in_error(self): + response = self.fetch('/status/500') + self.assertIsNotNone(response.headers.get('Correlation-ID')) + + @unittest.skipIf(version_info < (4,3,0,0), 'tornado < 4.3') + def test_that_correlation_id_is_copied_from_request(self): + correlation_id = uuid.uuid4().hex + response = self.fetch('/status/500', + headers={'Correlation-Id': correlation_id}) self.assertEqual(response.headers['correlation-id'], correlation_id) diff --git a/tox.ini b/tox.ini index 1f3b989..e4a4a32 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27,py33,py34,pypy,pypy3,tornado31,tornado32,tornado40,tornado43 +envlist = py35,py36,py37,pypy3,tornado42,tornado43,torando51 toxworkdir = {toxinidir}/build/tox skip_missing_intepreters = true @@ -9,27 +9,17 @@ deps = tornado commands = {envbindir}/nosetests -[testenv:py27] -deps = - {[testenv]deps} - mock - -[testenv:tornado31] +[testenv:tornado42] deps = -rtest-requirements.txt - tornado>=3.1,<3.2 - -[testenv:tornado32] -deps = - -rtest-requirements.txt - tornado>=3.2,<3.3 - -[testenv:tornado40] -deps = - -rtest-requirements.txt - tornado>=4.0,<4.1 + tornado>=4.2,<4.3 [testenv:tornado43] deps = -rtest-requirements.txt tornado>=4.3,<4.4 + +[testenv:tornado51] +deps = + -rtest-requirements.txt + tornado>=5.1,<5.2