2.0 Release

- 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
This commit is contained in:
Gavin M. Roy 2018-11-26 15:22:51 -05:00
parent e44ceb3cae
commit 280db40236
11 changed files with 146 additions and 70 deletions

View file

@ -1,6 +1,12 @@
Version History 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) `1.0.2`_ (20-Jun-2016)
~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~
- Add support for async prepare in superclasses of ``HandlerMixin`` - Add support for async prepare in superclasses of ``HandlerMixin``
@ -9,6 +15,6 @@ Version History
~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~
- Adds ``sprockets.mixins.correlation.HandlerMixin`` - 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.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 .. _`1.0.1`: https://github.com/sprockets/sprockets.mixins.correlation/compare/0.0.0...1.0.1

View file

@ -1,4 +1,4 @@
Copyright (c) 2015 AWeber Communications Copyright (c) 2015-2018 AWeber Communications
All rights reserved. All rights reserved.
Redistribution and use in source and binary forms, with or without modification, Redistribution and use in source and binary forms, with or without modification,

View file

@ -1,6 +1,5 @@
-r requirements.txt -r requirements.txt
-r test-requirements.txt -r test-requirements.txt
flake8>=2.1,<3 flake8
sphinx>=1.2,<2 sphinx>=1.2,<2
sphinx-rtd-theme>=0.1,<1.0 sphinx-rtd-theme>=0.1,<1.0
tornado>=4.0,<5

View file

@ -8,6 +8,10 @@ correlation.HandlerMixin
.. autoclass:: sprockets.mixins.correlation.HandlerMixin .. autoclass:: sprockets.mixins.correlation.HandlerMixin
:members: :members:
.. autoclass:: sprockets.mixins.correlation.AsyncIOHandlerMixin
:members:
Contributing to this Library Contributing to this Library
---------------------------- ----------------------------

View file

@ -1 +1 @@
tornado>=3.1,<4.4 tornado>=4.0,<5.2

View file

@ -1,5 +1,6 @@
#!/usr/bin/env python #!/usr/bin/env python
import codecs import codecs
from os import path
import sys import sys
import setuptools import setuptools
@ -7,24 +8,23 @@ import setuptools
from sprockets.mixins import correlation from sprockets.mixins import correlation
def read_requirements_file(req_name): def read_requirements(name):
requirements = [] requirements = []
try: try:
with codecs.open(req_name, encoding='utf-8') as req_file: with open(path.join('requires', name)) as req_file:
for req_line in req_file: for line in req_file:
if '#' in req_line: if '#' in line:
req_line = req_line[0:req_line.find('#')].strip() line = line[:line.index('#')]
if req_line: line = line.strip()
requirements.append(req_line.strip()) if line.startswith('-r'):
requirements.extend(read_requirements(line[2:].strip()))
elif line and not line.startswith('-'):
requirements.append(line)
except IOError: except IOError:
pass pass
return requirements 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( setuptools.setup(
name='sprockets.mixins.correlation', name='sprockets.mixins.correlation',
version=correlation.__version__, version=correlation.__version__,
@ -35,16 +35,15 @@ setuptools.setup(
author_email='api@aweber.com', author_email='api@aweber.com',
license=codecs.open('LICENSE', encoding='utf-8').read(), license=codecs.open('LICENSE', encoding='utf-8').read(),
classifiers=[ classifiers=[
'Development Status :: 4 - Beta', 'Development Status :: 5 - Production/Stable',
'Intended Audience :: Developers', 'Intended Audience :: Developers',
'License :: OSI Approved :: BSD License', 'License :: OSI Approved :: BSD License',
'Natural Language :: English', 'Natural Language :: English',
'Operating System :: OS Independent', 'Operating System :: OS Independent',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: CPython',
'Programming Language :: Python :: Implementation :: PyPy', 'Programming Language :: Python :: Implementation :: PyPy',
'Topic :: Software Development :: Libraries', 'Topic :: Software Development :: Libraries',
@ -52,9 +51,8 @@ setuptools.setup(
], ],
packages=setuptools.find_packages(), packages=setuptools.find_packages(),
namespace_packages=['sprockets'], namespace_packages=['sprockets'],
install_requires=install_requires, install_requires=read_requirements('requirements.txt'),
setup_requires=setup_requires, tests_require=read_requirements('test-requirements.txt'),
tests_require=tests_require,
test_suite='nose.collector', test_suite='nose.collector',
zip_safe=True, zip_safe=True,
) )

View file

@ -1,5 +1,5 @@
try: try:
from .mixins import HandlerMixin from .mixins import HandlerMixin, AsyncIOHandlerMixin
except ImportError as error: except ImportError as error:
@ -8,5 +8,9 @@ except ImportError as error:
raise error raise error
class AsyncIOHandlerMixin(object):
def __init__(self, *args, **kwargs):
raise error
version_info = (1, 0, 2) version_info = (1, 0, 2)
__version__ = '.'.join(str(v) for v in version_info[:3]) __version__ = '.'.join(str(v) for v in version_info[:3])

View file

@ -1,15 +1,6 @@
import uuid import uuid
import tornado.gen from tornado import concurrent, gen, log
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)
class HandlerMixin(object): class HandlerMixin(object):
@ -50,13 +41,13 @@ class HandlerMixin(object):
self.__correlation_id = str(uuid.uuid4()) self.__correlation_id = str(uuid.uuid4())
super(HandlerMixin, self).__init__(*args, **kwargs) super(HandlerMixin, self).__init__(*args, **kwargs)
@tornado.gen.coroutine @gen.coroutine
def prepare(self): def prepare(self):
# Here we want to copy an incoming Correlation-ID header if # Here we want to copy an incoming Correlation-ID header if
# one exists. We also want to set it in the outgoing response # one exists. We also want to set it in the outgoing response
# which the property setter does for us. # which the property setter does for us.
maybe_future = super(HandlerMixin, self).prepare() maybe_future = super(HandlerMixin, self).prepare()
if is_future(maybe_future): if concurrent.is_future(maybe_future):
yield maybe_future yield maybe_future
correlation_id = self.get_request_header(self.__header_name, None) correlation_id = self.get_request_header(self.__header_name, None)
@ -98,6 +89,58 @@ class HandlerMixin(object):
return self.request.headers.get(name, default) 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): def correlation_id_logger(handler):
""" Custom Tornado access log writer that appends correlation-id. """ Custom Tornado access log writer that appends correlation-id.
@ -113,11 +156,11 @@ def correlation_id_logger(handler):
is processing the client request. is processing the client request.
""" """
if handler.get_status() < 400: if handler.get_status() < 400:
log_method = tornado.log.access_log.info log_method = log.access_log.info
elif handler.get_status() < 500: elif handler.get_status() < 500:
log_method = tornado.log.access_log.warning log_method = log.access_log.warning
else: else:
log_method = tornado.log.access_log.error log_method = log.access_log.error
request_time = 1000.0 * handler.request.request_time() request_time = 1000.0 * handler.request.request_time()
correlation_id = getattr(handler, "correlation_id", None) correlation_id = getattr(handler, "correlation_id", None)
if correlation_id is None: if correlation_id is None:

View file

@ -1,4 +1,4 @@
coverage>=3.7,<4 coverage>=4.5.2,<5
coveralls>=0.4,<1 coveralls>=1.5.1,<2
nose>=1.3,<2 nose>=1.3.7,<2
tox>=1.7,<2 tox>=3.5.3,<4

View file

@ -1,7 +1,7 @@
import uuid import uuid
import unittest import unittest
from tornado import testing, web from tornado import testing, version_info, web
from sprockets.mixins import correlation from sprockets.mixins import correlation
@ -15,6 +15,16 @@ class CorrelatedRequestHandler(correlation.HandlerMixin, web.RequestHandler):
self.write('status {0}'.format(status_code)) 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): class CorrelationMixinTests(testing.AsyncHTTPTestCase):
def get_app(self): def get_app(self):
@ -23,18 +33,40 @@ class CorrelationMixinTests(testing.AsyncHTTPTestCase):
]) ])
def test_that_correlation_id_is_returned_when_successful(self): def test_that_correlation_id_is_returned_when_successful(self):
self.http_client.fetch(self.get_url('/status/200'), self.stop) response = self.fetch('/status/200')
response = self.wait()
self.assertIsNotNone(response.headers.get('Correlation-ID')) self.assertIsNotNone(response.headers.get('Correlation-ID'))
def test_that_correlation_id_is_returned_in_error(self): def test_that_correlation_id_is_returned_in_error(self):
self.http_client.fetch(self.get_url('/status/500'), self.stop) response = self.fetch('/status/500')
response = self.wait()
self.assertIsNotNone(response.headers.get('Correlation-ID')) self.assertIsNotNone(response.headers.get('Correlation-ID'))
def test_that_correlation_id_is_copied_from_request(self): def test_that_correlation_id_is_copied_from_request(self):
correlation_id = uuid.uuid4().hex correlation_id = uuid.uuid4().hex
self.http_client.fetch(self.get_url('/status/200'), self.stop, 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<status_code>\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}) headers={'Correlation-Id': correlation_id})
response = self.wait()
self.assertEqual(response.headers['correlation-id'], correlation_id) self.assertEqual(response.headers['correlation-id'], correlation_id)

26
tox.ini
View file

@ -1,5 +1,5 @@
[tox] [tox]
envlist = py27,py33,py34,pypy,pypy3,tornado31,tornado32,tornado40,tornado43 envlist = py35,py36,py37,pypy3,tornado42,tornado43,torando51
toxworkdir = {toxinidir}/build/tox toxworkdir = {toxinidir}/build/tox
skip_missing_intepreters = true skip_missing_intepreters = true
@ -9,27 +9,17 @@ deps =
tornado tornado
commands = {envbindir}/nosetests commands = {envbindir}/nosetests
[testenv:py27] [testenv:tornado42]
deps =
{[testenv]deps}
mock
[testenv:tornado31]
deps = deps =
-rtest-requirements.txt -rtest-requirements.txt
tornado>=3.1,<3.2 tornado>=4.2,<4.3
[testenv:tornado32]
deps =
-rtest-requirements.txt
tornado>=3.2,<3.3
[testenv:tornado40]
deps =
-rtest-requirements.txt
tornado>=4.0,<4.1
[testenv:tornado43] [testenv:tornado43]
deps = deps =
-rtest-requirements.txt -rtest-requirements.txt
tornado>=4.3,<4.4 tornado>=4.3,<4.4
[testenv:tornado51]
deps =
-rtest-requirements.txt
tornado>=5.1,<5.2