From bd003b52a5076905ceadf514c4d360fee545bfc5 Mon Sep 17 00:00:00 2001 From: Chris Dent Date: Fri, 18 Aug 2017 16:42:20 +0100 Subject: [PATCH] Provide a helper method to get headers from environ The get_version method accepts a dict or list of tuples that represent HTTP request headers that will be processed to find a microversion header. Sometimes, for example in some middlewares, direct access to a headers dict will not be available and only the WSGI environ will be present. This change provides a utility method which creates a new dict of headers: headers_from_wsgi_environ. This mode was chosen to make it clear that a copy of the environ is being made, not the environ itself as we really don't want to be passing that as some values in it will not be simple objects (strings and numbers) and we do not know what other middleware might have done or want to do with it. Internal to get_version any attempt to get a header named 'FOO' will fall back to looking for the WSGI equivalent of 'HTTP_FOO'. README.rst has been updated to indicate the new style. This change is backwards compatible, existing clients will not notice. Change-Id: I5262031d9cde0378eabe342c1913091658c3bf9b Closes-Bug: #1579772 --- README.rst | 6 ++ microversion_parse/__init__.py | 33 +++++++++- .../tests/test_headers_from_wsgi_environ.py | 61 +++++++++++++++++++ 3 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 microversion_parse/tests/test_headers_from_wsgi_environ.py diff --git a/README.rst b/README.rst index da3c697..04fdec2 100644 --- a/README.rst +++ b/README.rst @@ -11,6 +11,12 @@ A simple parser for OpenStack microversion headers:: headers, service_type='compute', legacy_headers=['x-openstack-nova-api-version']) + # If headers are not already available, a dict of headers + # can be extracted from the WSGI environ + headers = microversion_parse.headers_from_wsgi_environ(environ) + version = microversion_parse.get_version( + headers, service_type='placement') + It processes microversion headers with the standard form:: OpenStack-API-Version: compute 2.1 diff --git a/microversion_parse/__init__.py b/microversion_parse/__init__.py index cb3fe63..6fc8071 100644 --- a/microversion_parse/__init__.py +++ b/microversion_parse/__init__.py @@ -16,6 +16,7 @@ __version__ = '0.1.1' import collections +ENVIRON_HTTP_HEADER_FMT = 'http_{}' STANDARD_HEADER = 'openstack-api-version' @@ -63,7 +64,7 @@ def check_legacy_headers(headers, legacy_headers): """Gather values from old headers.""" for legacy_header in legacy_headers: try: - value = headers[legacy_header.lower()] + value = _extract_header_value(headers, legacy_header.lower()) return value.split(',')[-1].strip() except KeyError: pass @@ -73,7 +74,7 @@ def check_legacy_headers(headers, legacy_headers): def check_standard_header(headers, service_type): """Parse the standard header to get value for service.""" try: - header = headers[STANDARD_HEADER] + header = _extract_header_value(headers, STANDARD_HEADER) for header_value in reversed(header.split(',')): try: service, version = header_value.strip().split(None, 1) @@ -100,3 +101,31 @@ def fold_headers(headers): folded_headers[header] = ','.join(value) return folded_headers + + +def headers_from_wsgi_environ(environ): + """Extract all the HTTP_ keys and values from environ to a new dict. + + Note that this does not change the keys in any way in the returned + dict. Nor is the incoming environ modified. + + :param environ: A PEP 3333 compliant WSGI environ dict. + """ + return {key: environ[key] for key in environ if key.startswith('HTTP_')} + + +def _extract_header_value(headers, header_name): + """Get the value of a header. + + The provided headers is a dict. If a key doesn't exist for + header_name, try using the WSGI environ form of the name. + + Raises KeyError if neither key is found. + """ + try: + value = headers[header_name] + except KeyError: + wsgi_header_name = ENVIRON_HTTP_HEADER_FMT.format( + header_name.replace('-', '_')) + value = headers[wsgi_header_name] + return value diff --git a/microversion_parse/tests/test_headers_from_wsgi_environ.py b/microversion_parse/tests/test_headers_from_wsgi_environ.py new file mode 100644 index 0000000..525ac6e --- /dev/null +++ b/microversion_parse/tests/test_headers_from_wsgi_environ.py @@ -0,0 +1,61 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import testtools + +import microversion_parse + + +class TestHeadersFromWSGIEnviron(testtools.TestCase): + + def test_empty_environ(self): + environ = {} + expected = {} + self.assertEqual( + expected, + microversion_parse.headers_from_wsgi_environ(environ)) + + def test_non_empty_no_headers(self): + environ = {'PATH_INFO': '/foo/bar'} + expected = {} + found_headers = microversion_parse.headers_from_wsgi_environ(environ) + self.assertEqual(expected, found_headers) + + def test_headers(self): + environ = {'PATH_INFO': '/foo/bar', + 'HTTP_OPENSTACK_API_VERSION': 'placement 2.1', + 'HTTP_CONTENT_TYPE': 'application/json'} + expected = {'HTTP_OPENSTACK_API_VERSION': 'placement 2.1', + 'HTTP_CONTENT_TYPE': 'application/json'} + found_headers = microversion_parse.headers_from_wsgi_environ(environ) + self.assertEqual(expected, found_headers) + + def test_get_version_from_environ(self): + environ = {'PATH_INFO': '/foo/bar', + 'HTTP_OPENSTACK_API_VERSION': 'placement 2.1', + 'HTTP_CONTENT_TYPE': 'application/json'} + expected_version = '2.1' + headers = microversion_parse.headers_from_wsgi_environ(environ) + version = microversion_parse.get_version(headers, 'placement') + self.assertEqual(expected_version, version) + + def test_get_version_from_environ_legacy(self): + environ = {'PATH_INFO': '/foo/bar', + 'HTTP_X_OPENSTACK_PLACEMENT_API_VERSION': '2.1', + 'HTTP_CONTENT_TYPE': 'application/json'} + expected_version = '2.1' + headers = microversion_parse.headers_from_wsgi_environ(environ) + version = microversion_parse.get_version( + headers, 'placement', + legacy_headers=['x-openstack-placement-api-version']) + self.assertEqual(expected_version, version)