From 208e5c78841864c70b5d60f38d1f1293e01628f7 Mon Sep 17 00:00:00 2001 From: Dave Shawley Date: Sat, 28 Mar 2015 12:51:48 -0400 Subject: [PATCH 1/6] Add mixins.HandlerMixin. --- HISTORY.rst | 7 +- sprockets/mixins/correlation/__init__.py | 3 + sprockets/mixins/correlation/mixins.py | 83 ++++++++++++++++++++++++ tests.py | 40 ++++++++++++ 4 files changed, 129 insertions(+), 4 deletions(-) create mode 100644 sprockets/mixins/correlation/mixins.py diff --git a/HISTORY.rst b/HISTORY.rst index ffa8a35..8e2c226 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,7 +1,6 @@ Version History --------------- -0.0.0 -~~~~~ - - Nothing to see here - +Next Release +~~~~~~~~~~~~ + - Adds ``sprockets.mixins.correlation.HandlerMixin`` diff --git a/sprockets/mixins/correlation/__init__.py b/sprockets/mixins/correlation/__init__.py index 9969ce6..876a2b8 100644 --- a/sprockets/mixins/correlation/__init__.py +++ b/sprockets/mixins/correlation/__init__.py @@ -1,2 +1,5 @@ +from .mixins import HandlerMixin + + version_info = (0, 0, 0) __version__ = '.'.join(str(v) for v in version_info[:3]) diff --git a/sprockets/mixins/correlation/mixins.py b/sprockets/mixins/correlation/mixins.py new file mode 100644 index 0000000..87986ac --- /dev/null +++ b/sprockets/mixins/correlation/mixins.py @@ -0,0 +1,83 @@ +import uuid + + +class HandlerMixin(object): + """ + Mix this in over a ``RequestHandler`` for a correlating header. + + :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(HandlerMixin, self).__init__(*args, **kwargs) + + 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. + super(HandlerMixin, self).prepare() + correlation_id = self.get_request_header(self.__header_name, None) + if correlation_id is not None: + self.correlation_id = correlation_id + + def set_default_headers(self): + # This is called during initialization as well as *AFTER* + # calling clear() when an error occurs so we need to make + # sure that our header is set again... + super(HandlerMixin, self).set_default_headers() + self.set_header(self.__header_name, self.correlation_id) + + @property + def correlation_id(self): + """Correlation header value.""" + return self.__correlation_id + + @correlation_id.setter + def correlation_id(self, value): + self.__correlation_id = value + self.set_header(self.__header_name, self.__correlation_id) + + def get_request_header(self, name, default): + """ + Retrieve the value of a request header. + + :param str name: the name of the header to retrieve + :param default: the value to return if the header is not set + + This method abstracts the act of retrieving a header value out + from the implementation. This makes it possible to implement + a *RequestHandler* that is something other than a + :class:`tornado.web.RequestHandler` by simply implementing this + method and ``set_header`` over the underlying implementation, + for example, say AMQP message properties. + + """ + return self.request.headers.get(name, default) diff --git a/tests.py b/tests.py index e69de29..9abe799 100644 --- a/tests.py +++ b/tests.py @@ -0,0 +1,40 @@ +import uuid +import unittest + +from tornado import testing, web + +from sprockets.mixins import correlation + + +class CorrelatedRequestHandler(correlation.HandlerMixin, 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): + return web.Application([ + (r'/status/(?P\d+)', CorrelatedRequestHandler), + ]) + + def test_that_correlation_id_is_returned_when_successful(self): + self.http_client.fetch(self.get_url('/status/200'), self.stop) + response = self.wait() + 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() + 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() + self.assertEqual(response.headers['correlation-id'], correlation_id) From 9e59113a151990a65b8d26e3fe3c1f23e432ce36 Mon Sep 17 00:00:00 2001 From: Dave Shawley Date: Sat, 28 Mar 2015 13:04:21 -0400 Subject: [PATCH 2/6] Add documentation. --- docs/conf.py | 2 ++ docs/index.rst | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index 1c7d13c..559b41e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -21,4 +21,6 @@ html_theme = 'sphinx_rtd_theme' html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] intersphinx_mapping = { 'python': ('https://docs.python.org/', None), + 'sprockets': ('https://sprockets.readthedocs.org/en/latest/', None), + 'tornado': ('http://www.tornadoweb.org/en/latest/', None), } diff --git a/docs/index.rst b/docs/index.rst index 949f3f3..30bedc7 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,5 +1,13 @@ .. include:: ../README.rst +API Documentation +----------------- + +correlation.HandlerMixin +~~~~~~~~~~~~~~~~~~~~~~~~ +.. autoclass:: sprockets.mixins.correlation.HandlerMixin + :members: + Contributing to this Library ---------------------------- From 16ace6cc93df1af5c68e8a45faf5cf8a26449ab7 Mon Sep 17 00:00:00 2001 From: Dave Shawley Date: Sat, 28 Mar 2015 13:04:42 -0400 Subject: [PATCH 3/6] Add testing against various Tornado versions. --- tox.ini | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 36947a8..124cd9f 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27,py33,py34,pypy,pypy3 +envlist = py27,py33,py34,pypy,pypy3,tornado31,tornado32,tornado40 toxworkdir = {toxinidir}/build/tox skip_missing_intepreters = true @@ -13,3 +13,18 @@ commands = {envbindir}/nosetests deps = {[testenv]deps} mock + +[testenv:tornado31] +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 From c01247d8706536e636cb316c0f4a6ad5638c6766 Mon Sep 17 00:00:00 2001 From: Dave Shawley Date: Sat, 28 Mar 2015 13:18:08 -0400 Subject: [PATCH 4/6] Add travis-ci control file. --- .travis.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..8713053 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,25 @@ +%YAML 1.1 +--- +language: python +python: + - '2.7' + - '3.3' + - '3.4' + - pypy +install: + - pip install -e . + - pip install -r test-requirements.txt + - pip install tornado>=4.0,<5 +script: nosetests +after_success: + - coveralls +# deploy: +# provider: pypi +# user: sprockets +# distributions: "sdist bdist_wheel" +# on: +# python: 2.7 +# tags: true +# all_branches: true +# password: +# secure: bBvJ6OIyeEb6dw7OiNiXXZwCiY6B9AqczH7uA0bTlQArF3glJxTBz7xRVaXQZpL36IWW591VFmu8YIrHiNY1eIUhIAP74BRSQyHxyvfU7HMr5R9qQrBaoBJOU From 54f9f8c84ff4fd063e85d76a0bce73f7b5d37d36 Mon Sep 17 00:00:00 2001 From: Dave Shawley Date: Sat, 28 Mar 2015 13:22:37 -0400 Subject: [PATCH 5/6] Fix travis-ci syntax. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 8713053..870a5cf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,7 @@ python: install: - pip install -e . - pip install -r test-requirements.txt - - pip install tornado>=4.0,<5 + - pip install tornado script: nosetests after_success: - coveralls From 21c791e3ca52746c8a2f3b7c86b1ad807c252958 Mon Sep 17 00:00:00 2001 From: Dave Shawley Date: Mon, 30 Mar 2015 10:12:18 -0400 Subject: [PATCH 6/6] Add deploy details to travis-ci control file. --- .travis.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.travis.yml b/.travis.yml index 870a5cf..6c01176 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,13 +13,13 @@ install: script: nosetests after_success: - coveralls -# deploy: -# provider: pypi -# user: sprockets -# distributions: "sdist bdist_wheel" -# on: -# python: 2.7 -# tags: true -# all_branches: true -# password: -# secure: bBvJ6OIyeEb6dw7OiNiXXZwCiY6B9AqczH7uA0bTlQArF3glJxTBz7xRVaXQZpL36IWW591VFmu8YIrHiNY1eIUhIAP74BRSQyHxyvfU7HMr5R9qQrBaoBJOU +deploy: + provider: pypi + user: aweber + password: + secure: fdn7NjeIHeo51VHhp+6LnviRYJtfd7As0dCMcwGf1X/STvKbTqp1b8usq9QjSacakde+ZdFsuNM1sImw/yUlft0ZbxksBE98sO+q6qdSMVnQyNtXSyc0Rifw5yyx3/lBdqI0JDGDnMzNWSL1CF5csX1GeUAZguP3kgiV3D8yChA= + distributions: "sdist bdist_wheel" + on: + python: 2.7 + tags: true + all_branches: true