mirror of
https://github.com/sprockets/sprockets.mixins.correlation.git
synced 2024-11-23 19:29:55 +00:00
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:
parent
e44ceb3cae
commit
280db40236
11 changed files with 146 additions and 70 deletions
|
@ -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
|
||||||
|
|
2
LICENSE
2
LICENSE
|
@ -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,
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
|
@ -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
|
||||||
----------------------------
|
----------------------------
|
||||||
|
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
tornado>=3.1,<4.4
|
tornado>=4.0,<5.2
|
||||||
|
|
36
setup.py
36
setup.py
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
@ -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])
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
46
tests.py
46
tests.py
|
@ -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
26
tox.ini
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue