Initial proof of concept of microversion_parse
See README.rst for details. The basic gist is that a get_version method is provided. It takes a dict or list of headers and returned a version for a service_type if it can find it.
This commit is contained in:
parent
db6fb70f72
commit
49b44934b9
4
.testr.conf
Normal file
4
.testr.conf
Normal file
@ -0,0 +1,4 @@
|
||||
[DEFAULT]
|
||||
test_command=${PYTHON:-python} -m subunit.run discover -t . ${OS_TEST_PATH:-microversion_parse} $LISTOPT $IDOPTION
|
||||
test_id_option=--load-list $IDFILE
|
||||
test_list_option=--list
|
@ -1,2 +0,0 @@
|
||||
# microversion_parse
|
||||
A simple parser for OpenStack microversion headers
|
29
README.rst
Normal file
29
README.rst
Normal file
@ -0,0 +1,29 @@
|
||||
microversion_parse
|
||||
=================
|
||||
|
||||
A simple parser for OpenStack microversion headers::
|
||||
|
||||
import microversion_parse
|
||||
|
||||
# headers is a dict of headers with folded (comma-separated
|
||||
# values) or a list of header, value tuples
|
||||
version = microversion_parse.get_version(
|
||||
headers, service_type='compute', legacy_type='nova')
|
||||
|
||||
It processes microversion headers with the standard form::
|
||||
|
||||
OpenStack-API-Version: compute 2.1
|
||||
|
||||
It also deals with several older formats, depending on the values of
|
||||
the service_type and legacy_type arguments::
|
||||
|
||||
OpenStack-compute-api-version: 2.1
|
||||
OpenStack-nova-api-version: 2.1
|
||||
X-OpenStack-nova-api-version: 2.1
|
||||
|
||||
.. note:: The X prefixed version does not currently parse for
|
||||
service type named headers, only project named headers.
|
||||
|
||||
If a version string cannot be found ``None`` will be returned. If
|
||||
the input is incorrect usual Python exceptions (ValueError,
|
||||
TypeError) are allowed to raise to the caller.
|
106
microversion_parse/__init__.py
Normal file
106
microversion_parse/__init__.py
Normal file
@ -0,0 +1,106 @@
|
||||
# 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 collections
|
||||
|
||||
|
||||
STANDARD_HEADER = 'openstack-api-version'
|
||||
|
||||
|
||||
def get_version(headers, service_type=None, legacy_type=None):
|
||||
"""Parse a microversion out of headers
|
||||
|
||||
:param headers: The headers of a request, dict or list
|
||||
:param service_type: The service type being looked for in the headers
|
||||
:param legacy_type: The project name to use when looking for fallback
|
||||
headers.
|
||||
:returns: a version string or "latest"
|
||||
:raises: ValueError
|
||||
"""
|
||||
# If headers is not a dict we assume is an iterator of
|
||||
# tuple-like headers, which we will fold into a dict.
|
||||
#
|
||||
# The flow is that we first look for the new standard singular
|
||||
# header:
|
||||
# * openstack-api-version: <service> <version>
|
||||
# If that's not present we fall back, in order, to:
|
||||
# * openstack-<service>-api-version: <version>
|
||||
# * openstack-<legacy>-api-version: <version>
|
||||
# * x-openstack-<legacy>-api-version: <version>
|
||||
#
|
||||
# Folded headers are joined by ,
|
||||
folded_headers = fold_headers(headers)
|
||||
|
||||
version = check_standard_header(folded_headers, service_type)
|
||||
if version:
|
||||
return version
|
||||
|
||||
extra_headers = build_headers(service_type, legacy_type)
|
||||
version = check_legacy_headers(folded_headers, extra_headers)
|
||||
return version
|
||||
|
||||
|
||||
def build_headers(service_type, legacy_type=None):
|
||||
"""Create the headers to be looked at."""
|
||||
headers = [
|
||||
'openstack-%s-api-version' % service_type
|
||||
]
|
||||
if legacy_type:
|
||||
legacy_headers = [
|
||||
'openstack-%s-api-version' % legacy_type,
|
||||
'x-openstack-%s-api-version' % legacy_type
|
||||
]
|
||||
headers.extend(legacy_headers)
|
||||
return headers
|
||||
|
||||
|
||||
def check_legacy_headers(headers, legacy_headers):
|
||||
"""Gather values from old headers."""
|
||||
for legacy_header in legacy_headers:
|
||||
try:
|
||||
value = headers[legacy_header]
|
||||
return value.split(',')[-1].strip()
|
||||
except KeyError:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def check_standard_header(headers, service_type):
|
||||
"""Parse the standard header to get value for service."""
|
||||
try:
|
||||
header = headers[STANDARD_HEADER]
|
||||
for header_value in reversed(header.split(',')):
|
||||
try:
|
||||
service, version = header_value.strip().split(None, 1)
|
||||
if service.lower() == service_type.lower():
|
||||
return version.strip()
|
||||
except ValueError:
|
||||
pass
|
||||
except (KeyError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def fold_headers(headers):
|
||||
"""Turn a list of headers into a folded dict."""
|
||||
if isinstance(headers, dict):
|
||||
# TODO(cdent): canonicalize? (i.e. in lower())
|
||||
return headers
|
||||
header_dict = collections.defaultdict(list)
|
||||
for header, value in headers:
|
||||
header_dict[header.lower()].append(value.strip())
|
||||
|
||||
folded_headers = {}
|
||||
for header, value in header_dict.items():
|
||||
folded_headers[header] = ','.join(value)
|
||||
|
||||
return folded_headers
|
0
microversion_parse/tests/__init__.py
Normal file
0
microversion_parse/tests/__init__.py
Normal file
225
microversion_parse/tests/test_get_version.py
Normal file
225
microversion_parse/tests/test_get_version.py
Normal file
@ -0,0 +1,225 @@
|
||||
# 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 TestBuildHeaders(testtools.TestCase):
|
||||
|
||||
def test_build_header_list_service(self):
|
||||
headers = microversion_parse.build_headers('alpha')
|
||||
|
||||
self.assertEqual(1, len(headers))
|
||||
self.assertEqual('openstack-alpha-api-version', headers[0])
|
||||
|
||||
def test_build_header_list_legacy(self):
|
||||
headers = microversion_parse.build_headers('alpha', 'beta')
|
||||
|
||||
self.assertEqual(3, len(headers))
|
||||
self.assertEqual('openstack-alpha-api-version', headers[0])
|
||||
self.assertEqual('openstack-beta-api-version', headers[1])
|
||||
self.assertEqual('x-openstack-beta-api-version', headers[2])
|
||||
|
||||
|
||||
class TestFoldHeaders(testtools.TestCase):
|
||||
|
||||
def test_dict_headers(self):
|
||||
headers = {
|
||||
'header-one': 'alpha',
|
||||
'header-two': 'beta',
|
||||
'header-three': 'gamma',
|
||||
}
|
||||
|
||||
folded_headers = microversion_parse.fold_headers(headers)
|
||||
self.assertEqual(3, len(folded_headers))
|
||||
self.assertEqual(set(headers.keys()), set(folded_headers.keys()))
|
||||
self.assertEqual('gamma', folded_headers['header-three'])
|
||||
|
||||
def test_listed_tuple_headers(self):
|
||||
headers = [
|
||||
('header-one', 'alpha'),
|
||||
('header-two', 'beta'),
|
||||
('header-one', 'gamma'),
|
||||
]
|
||||
|
||||
folded_headers = microversion_parse.fold_headers(headers)
|
||||
self.assertEqual(2, len(folded_headers))
|
||||
self.assertEqual(set(['header-one', 'header-two']),
|
||||
set(folded_headers.keys()))
|
||||
self.assertEqual('alpha,gamma', folded_headers['header-one'])
|
||||
|
||||
def test_bad_headers(self):
|
||||
headers = 'wow this is not a headers'
|
||||
self.assertRaises(ValueError, microversion_parse.fold_headers,
|
||||
headers)
|
||||
|
||||
# TODO(cdent): Test with request objects from frameworks.
|
||||
|
||||
|
||||
class TestStandardHeader(testtools.TestCase):
|
||||
|
||||
def test_simple_match(self):
|
||||
headers = {
|
||||
'header-one': 'alpha',
|
||||
'openstack-api-version': 'compute 2.1',
|
||||
'header-two': 'beta',
|
||||
}
|
||||
version = microversion_parse.check_standard_header(headers, 'compute')
|
||||
# TODO(cdent): String or number. Choosing string for now
|
||||
# since 'latest' is always a string.
|
||||
self.assertEqual('2.1', version)
|
||||
|
||||
def test_match_extra_whitespace(self):
|
||||
headers = {
|
||||
'header-one': 'alpha',
|
||||
'openstack-api-version': ' compute 2.1 ',
|
||||
'header-two': 'beta',
|
||||
}
|
||||
version = microversion_parse.check_standard_header(headers, 'compute')
|
||||
self.assertEqual('2.1', version)
|
||||
|
||||
def test_no_match_no_value(self):
|
||||
headers = {
|
||||
'header-one': 'alpha',
|
||||
'openstack-api-version': 'compute ',
|
||||
'header-two': 'beta',
|
||||
}
|
||||
version = microversion_parse.check_standard_header(headers, 'compute')
|
||||
self.assertEqual(None, version)
|
||||
|
||||
def test_no_match_wrong_service(self):
|
||||
headers = {
|
||||
'header-one': 'alpha',
|
||||
'openstack-api-version': 'network 5.9 ',
|
||||
'header-two': 'beta',
|
||||
}
|
||||
version = microversion_parse.check_standard_header(
|
||||
headers, 'compute')
|
||||
self.assertEqual(None, version)
|
||||
|
||||
def test_match_multiple_services(self):
|
||||
headers = {
|
||||
'header-one': 'alpha',
|
||||
'openstack-api-version': 'network 5.9 ,compute 2.1,telemetry 7.8',
|
||||
'header-two': 'beta',
|
||||
}
|
||||
version = microversion_parse.check_standard_header(
|
||||
headers, 'compute')
|
||||
self.assertEqual('2.1', version)
|
||||
version = microversion_parse.check_standard_header(
|
||||
headers, 'telemetry')
|
||||
self.assertEqual('7.8', version)
|
||||
|
||||
def test_match_multiple_same_service(self):
|
||||
headers = {
|
||||
'header-one': 'alpha',
|
||||
'openstack-api-version': 'compute 5.9 ,compute 2.1,compute 7.8',
|
||||
'header-two': 'beta',
|
||||
}
|
||||
version = microversion_parse.check_standard_header(
|
||||
headers, 'compute')
|
||||
self.assertEqual('7.8', version)
|
||||
|
||||
|
||||
class TestLegacyHeaders(testtools.TestCase):
|
||||
|
||||
def test_legacy_headers_straight(self):
|
||||
headers = {
|
||||
'header-one': 'alpha',
|
||||
'openstack-compute-api-version': ' 2.1 ',
|
||||
'header-two': 'beta',
|
||||
}
|
||||
version = microversion_parse.get_version(
|
||||
headers, service_type='compute')
|
||||
self.assertEqual('2.1', version)
|
||||
|
||||
def test_legacy_headers_folded(self):
|
||||
headers = {
|
||||
'header-one': 'alpha',
|
||||
'openstack-compute-api-version': ' 2.1, 9.2 ',
|
||||
'header-two': 'beta',
|
||||
}
|
||||
version = microversion_parse.get_version(
|
||||
headers, service_type='compute')
|
||||
self.assertEqual('9.2', version)
|
||||
|
||||
def test_older_legacy_headers_with_service(self):
|
||||
headers = {
|
||||
'header-one': 'alpha',
|
||||
'x-openstack-compute-api-version': ' 2.1, 9.2 ',
|
||||
'header-two': 'beta',
|
||||
}
|
||||
version = microversion_parse.get_version(
|
||||
headers, service_type='compute')
|
||||
# We don't do x- for service types.
|
||||
self.assertEqual(None, version)
|
||||
|
||||
def test_legacy_headers_project(self):
|
||||
headers = {
|
||||
'header-one': 'alpha',
|
||||
'x-openstack-nova-api-version': ' 2.1, 9.2 ',
|
||||
'header-two': 'beta',
|
||||
}
|
||||
version = microversion_parse.get_version(
|
||||
headers, service_type='compute', legacy_type='nova')
|
||||
self.assertEqual('9.2', version)
|
||||
|
||||
def test_legacy_headers_prefer(self):
|
||||
headers = {
|
||||
'header-one': 'alpha',
|
||||
'openstack-compute-api-version': '3.7',
|
||||
'x-openstack-nova-api-version': ' 2.1, 9.2 ',
|
||||
'header-two': 'beta',
|
||||
}
|
||||
version = microversion_parse.get_version(
|
||||
headers, service_type='compute', legacy_type='nova')
|
||||
self.assertEqual('3.7', version)
|
||||
|
||||
|
||||
class TestGetHeaders(testtools.TestCase):
|
||||
|
||||
def test_preference(self):
|
||||
headers = {
|
||||
'header-one': 'alpha',
|
||||
'openstack-api-version': 'compute 11.12, telemetry 9.7',
|
||||
'openstack-compute-api-version': '3.7',
|
||||
'x-openstack-nova-api-version': ' 2.1, 9.2 ',
|
||||
'header-two': 'beta',
|
||||
}
|
||||
version = microversion_parse.get_version(
|
||||
headers, service_type='compute', legacy_type='nova')
|
||||
self.assertEqual('11.12', version)
|
||||
|
||||
def test_unfolded_service(self):
|
||||
headers = [
|
||||
('header-one', 'alpha'),
|
||||
('openstack-api-version', 'compute 1.0'),
|
||||
('openstack-api-version', 'compute 2.0'),
|
||||
('openstack-api-version', '3.0'),
|
||||
]
|
||||
version = microversion_parse.get_version(
|
||||
headers, service_type='compute', legacy_type='nova')
|
||||
self.assertEqual('2.0', version)
|
||||
|
||||
def test_unfolded_in_name(self):
|
||||
headers = [
|
||||
('header-one', 'alpha'),
|
||||
('openstack-compute-api-version', '1.0'),
|
||||
('openstack-compute-api-version', '2.0'),
|
||||
('openstack-telemetry-api-version', '3.0'),
|
||||
]
|
||||
version = microversion_parse.get_version(
|
||||
headers, service_type='compute', legacy_type='nova')
|
||||
self.assertEqual('2.0', version)
|
1
requirements.txt
Normal file
1
requirements.txt
Normal file
@ -0,0 +1 @@
|
||||
|
27
setup.cfg
Normal file
27
setup.cfg
Normal file
@ -0,0 +1,27 @@
|
||||
[metadata]
|
||||
name = microversion_parse
|
||||
summary = OpenStack microversion heaader parser
|
||||
description-file = README.rst
|
||||
author = OpenStack
|
||||
author-email = openstack-dev@lists.openstack.org
|
||||
home-page = http://www.openstack.org/
|
||||
classifier =
|
||||
Environment :: OpenStack
|
||||
Intended Audience :: Information Technology
|
||||
License :: OSI Approved :: Apache Software License
|
||||
Operating System :: POSIX :: Linux
|
||||
Programming Language :: Python
|
||||
Programming Language :: Python :: 2
|
||||
Programming Language :: Python :: 2.7
|
||||
Programming Language :: Python :: 3
|
||||
Programming Language :: Python :: 3.4
|
||||
Programming Language :: Python :: 3.5
|
||||
|
||||
[files]
|
||||
packages =
|
||||
microversion_parse
|
||||
|
||||
[build_sphinx]
|
||||
all_files = 1
|
||||
build-dir = docs/build
|
||||
source-dir = docs/source
|
18
setup.py
Normal file
18
setup.py
Normal file
@ -0,0 +1,18 @@
|
||||
# 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 setuptools
|
||||
|
||||
setuptools.setup(
|
||||
setup_requires=['pbr'],
|
||||
pbr=True)
|
3
test-requirements.txt
Normal file
3
test-requirements.txt
Normal file
@ -0,0 +1,3 @@
|
||||
testtools
|
||||
testrepository
|
||||
coverage
|
40
tox.ini
Normal file
40
tox.ini
Normal file
@ -0,0 +1,40 @@
|
||||
[tox]
|
||||
minversion = 2.0
|
||||
skipsdist = True
|
||||
# If you want pypy or pypy3, do 'tox -epypy,pypy3', it might work!
|
||||
# And you can get coverage with 'tox -ecover'.
|
||||
envlist = py27,py34,py35,pep8
|
||||
|
||||
[testenv]
|
||||
deps = -r{toxinidir}/requirements.txt
|
||||
-r{toxinidir}/test-requirements.txt
|
||||
install_command = pip install -U {opts} {packages}
|
||||
setenv = OS_TEST_PATH=microversion_parse/tests/
|
||||
usedevelop = True
|
||||
commands = python setup.py testr --testr-args="{posargs}"
|
||||
|
||||
[testenv:venv]
|
||||
deps = -r{toxinidir}/requirements.txt
|
||||
-r{toxinidir}/test-requirements.txt
|
||||
commands = {posargs}
|
||||
|
||||
[testenv:pep8]
|
||||
deps = hacking
|
||||
usedevelop = False
|
||||
commands =
|
||||
flake8
|
||||
|
||||
[testenv:cover]
|
||||
commands = python setup.py testr --coverage --testr-args="{posargs}"
|
||||
|
||||
[testenv:docs]
|
||||
commands =
|
||||
rm -rf doc/build
|
||||
python setup.py build_sphinx
|
||||
whitelist_externals =
|
||||
rm
|
||||
|
||||
[flake8]
|
||||
ignore = H405,E126
|
||||
exclude=.venv,.git,.tox,dist,*egg,*.egg-info,build,examples,docs
|
||||
show-source = True
|
Loading…
Reference in New Issue
Block a user