Codebase list python-microversion-parse / 91bd703
Merge tag '0.2.1' into debian/rocky tag for 0.2.1 release Ondřej Nový 5 years ago
15 changed file(s) with 694 addition(s) and 18 deletion(s). Raw diff Collapse all Expand all
5959
6060 #Ipython Notebook
6161 .ipynb_checkpoints
62
63 .stestr
64 cover
0 [DEFAULT]
1 test_path=./microversion_parse/tests
2 top_dir=./
3 # This regex ensures each yaml file used by gabbi is run in only one
4 # process.
5 group_regex=microversion_parse\.tests\.test_middleware(?:\.|_)([^_]+)
+0
-4
.testr.conf less more
0 [DEFAULT]
1 test_command=${PYTHON:-python} -m subunit.run discover -t . ${OS_TEST_PATH:-microversion_parse} $LISTOPT $IDOPTION
2 test_id_option=--load-list $IDFILE
3 test_list_option=--list
00 microversion_parse
11 ==================
2
3 A small set of functions to manage OpenStack `microversion`_ headers that can
4 be used in middleware, application handlers and decorators to effectively
5 manage microversions.
6
7 Also included, in the ``middleware`` module, is a ``MicroversionMiddleware``
8 that will process incoming microversion headers.
9
10 get_version
11 -----------
212
313 A simple parser for OpenStack microversion headers::
414
1020 headers, service_type='compute',
1121 legacy_headers=['x-openstack-nova-api-version'])
1222
23 # If headers are not already available, a dict of headers
24 # can be extracted from the WSGI environ
25 headers = microversion_parse.headers_from_wsgi_environ(environ)
26 version = microversion_parse.get_version(
27 headers, service_type='placement')
28
1329 It processes microversion headers with the standard form::
1430
1531 OpenStack-API-Version: compute 2.1
1632
33 In that case, the response will be '2.1'.
34
1735 If provided with a ``legacy_headers`` argument, this is treated as
18 a list of headers to check for microversions. Some examples of
36 a list of additional headers to check for microversions. Some examples of
1937 headers include::
2038
2139 OpenStack-telemetry-api-version: 2.1
2543 If a version string cannot be found, ``None`` will be returned. If
2644 the input is incorrect usual Python exceptions (ValueError,
2745 TypeError) are allowed to raise to the caller.
46
47 parse_version_string
48 --------------------
49
50 A function to turn a version string into a ``Version``, a comparable
51 ``namedtuple``::
52
53 version_tuple = microversion_parse.parse_version_string('2.1')
54
55 If the provided string is not a valid microversion string, ``TypeError``
56 is raised.
57
58 extract_version
59 ---------------
60
61 Combines ``get_version`` and ``parse_version_string`` to find and validate
62 a microversion for a given service type in a collection of headers::
63
64 version_tuple = microversion_parse.extract_version(
65 headers, # a representation of headers, as accepted by get_version
66 service_type, # service type identify to match in headers
67 versions_list, # an ordered list of strings of version numbers that
68 # are the valid versions presented by this service
69 )
70
71 ``latest`` will be translated to whatever the max version is in versions_list.
72
73 If the found version is not in versions_list a ``ValueError`` is raised.
74
75 Note that ``extract_version`` does not support ``legacy_headers``.
76
77 MicroversionMiddleware
78 ----------------------
79
80 A WSGI middleware that can wrap an application that needs to be microversion
81 aware. The application will get a WSGI environ with a
82 'SERVICE_TYPE.microversion' key that has a value of the microversion found at
83 an 'openstack-api-version' header that matches SERVICE_TYPE. If no header is
84 found, the minimum microversion will be set. If the special keyword 'latest' is
85 used, the maximum microversion will be set.
86
87 If the requested microversion is not available a 406 response is returned.
88
89 If there is an error parsing a provided header, a 400 response is returned.
90
91 Otherwise the application is called.
92
93 The middleware is configured when it is created. Three parameters are required:
94
95 app
96 The next WSGI middleware or application in the stack.
97
98 service_type
99 The service type of the application, used to identify microversion headers.
100
101 versions_list
102 An ordered list of legitimate microversions (as strings) for the application.
103 It's assumed that any application that is using microversions will have such
104 a list for its own housekeeping and documentation.
105
106 One named parameter is optional:
107
108 json_error_formatter
109 A Webob error formatter that can be used to structure the response when JSON
110 is expected.
111
112 For example::
113
114 def app():
115 app = middleware.MicroversionMiddleware(
116 MyWSGIApp(), 'cats', ['1.0', '1.1', '1.2'])
117 return app
118
119
120 .. _microversion: http://specs.openstack.org/openstack/api-wg/guidelines/microversion_specification.html
1010 # See the License for the specific language governing permissions and
1111 # limitations under the License.
1212
13 __version__ = '0.1.1'
14
1513 import collections
1614
1715
16 ENVIRON_HTTP_HEADER_FMT = 'http_{}'
1817 STANDARD_HEADER = 'openstack-api-version'
18
19
20 class Version(collections.namedtuple('Version', 'major minor')):
21 """A namedtuple containing major and minor values.
22
23 Since it is a tuple, it is automatically comparable.
24 """
25
26 def __new__(cls, major, minor):
27 """Add mix and max version attributes to the tuple."""
28 self = super(Version, cls).__new__(cls, major, minor)
29 self.max_version = (-1, 0)
30 self.min_version = (-1, 0)
31 return self
32
33 def __str__(self):
34 return '%s.%s' % (self.major, self.minor)
35
36 def matches(self, min_version=None, max_version=None):
37 """Is this version within min_version and max_version.
38 """
39 # NOTE(cdent): min_version and max_version are expected
40 # to be set by the code that is creating the Version, if
41 # they are known.
42 if min_version is None:
43 min_version = self.min_version
44 if max_version is None:
45 max_version = self.max_version
46 return min_version <= self <= max_version
1947
2048
2149 def get_version(headers, service_type, legacy_headers=None):
6290 """Gather values from old headers."""
6391 for legacy_header in legacy_headers:
6492 try:
65 value = headers[legacy_header.lower()]
93 value = _extract_header_value(headers, legacy_header.lower())
6694 return value.split(',')[-1].strip()
6795 except KeyError:
6896 pass
72100 def check_standard_header(headers, service_type):
73101 """Parse the standard header to get value for service."""
74102 try:
75 header = headers[STANDARD_HEADER]
103 header = _extract_header_value(headers, STANDARD_HEADER)
76104 for header_value in reversed(header.split(',')):
77105 try:
78106 service, version = header_value.strip().split(None, 1)
88116 """Turn a list of headers into a folded dict."""
89117 # If it behaves like a dict, return it. Webob uses objects which
90118 # are not dicts, but behave like them.
91 if hasattr(headers, 'keys'):
119 try:
92120 return dict((k.lower(), v) for k, v in headers.items())
121 except AttributeError:
122 pass
93123 header_dict = collections.defaultdict(list)
94124 for header, value in headers:
95125 header_dict[header.lower()].append(value.strip())
99129 folded_headers[header] = ','.join(value)
100130
101131 return folded_headers
132
133
134 def headers_from_wsgi_environ(environ):
135 """Extract all the HTTP_ keys and values from environ to a new dict.
136
137 Note that this does not change the keys in any way in the returned
138 dict. Nor is the incoming environ modified.
139
140 :param environ: A PEP 3333 compliant WSGI environ dict.
141 """
142 return {key: environ[key] for key in environ if key.startswith('HTTP_')}
143
144
145 def _extract_header_value(headers, header_name):
146 """Get the value of a header.
147
148 The provided headers is a dict. If a key doesn't exist for
149 header_name, try using the WSGI environ form of the name.
150
151 Raises KeyError if neither key is found.
152 """
153 try:
154 value = headers[header_name]
155 except KeyError:
156 wsgi_header_name = ENVIRON_HTTP_HEADER_FMT.format(
157 header_name.replace('-', '_'))
158 value = headers[wsgi_header_name]
159 return value
160
161
162 def parse_version_string(version_string):
163 """Turn a version string into a Version
164
165 :param version_string: A string of two numerals, X.Y.
166 :returns: a Version
167 :raises: TypeError
168 """
169 try:
170 # The combination of int and a limited split with the
171 # named tuple means that this incantation will raise
172 # ValueError, TypeError or AttributeError when the incoming
173 # data is poorly formed but will, however, naturally adapt to
174 # extraneous whitespace.
175 return Version(*(int(value) for value
176 in version_string.split('.', 1)))
177 except (ValueError, TypeError, AttributeError) as exc:
178 raise TypeError('invalid version string: %s; %s' % (
179 version_string, exc))
180
181
182 def extract_version(headers, service_type, versions_list):
183 """Extract the microversion from the headers.
184
185 There may be multiple headers and some which don't match our
186 service.
187
188 If no version is found then the extracted version is the minimum
189 available version.
190
191 :param headers: Request headers as dict list or WSGI environ
192 :param service_type: The service_type as a string
193 :param versions_list: List of all possible microversions as strings,
194 sorted from earliest to latest version.
195 :returns: a Version with the optional min_version and max_version
196 attributes set.
197 :raises: ValueError
198 """
199 found_version = get_version(headers, service_type=service_type)
200 min_version_string = versions_list[0]
201 max_version_string = versions_list[-1]
202
203 # If there was no version found in the headers, choose the minimum
204 # available version.
205 version_string = found_version or min_version_string
206 if version_string == 'latest':
207 version_string = max_version_string
208 request_version = parse_version_string(version_string)
209 request_version.max_version = parse_version_string(max_version_string)
210 request_version.min_version = parse_version_string(min_version_string)
211 # We need a version that is in versions_list. This gives us the option
212 # to administratively disable a version if we really need to.
213 if str(request_version) in versions_list:
214 return request_version
215 raise ValueError('Unacceptable version header: %s' % version_string)
0 # Licensed under the Apache License, Version 2.0 (the "License");
1 # you may not use this file except in compliance with the License.
2 # You may obtain a copy of the License at
3 #
4 # http://www.apache.org/licenses/LICENSE-2.0
5 #
6 # Unless required by applicable law or agreed to in writing, software
7 # distributed under the License is distributed on an "AS IS" BASIS,
8 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
9 # implied.
10 # See the License for the specific language governing permissions and
11 # limitations under the License.
12 """WSGI middleware for getting microversion info."""
13
14 import webob
15 import webob.dec
16
17 import microversion_parse
18
19
20 class MicroversionMiddleware(object):
21 """WSGI middleware for getting microversion info.
22
23 The application will get a WSGI environ with a
24 'SERVICE_TYPE.microversion' key that has a value of the microversion
25 found at an 'openstack-api-version' header that matches SERVICE_TYPE. If
26 no header is found, the minimum microversion will be set. If the
27 special keyword 'latest' is used, the maximum microversion will be
28 set.
29
30 If the requested microversion is not available a 406 response is
31 returned.
32
33 If there is an error parsing a provided header, a 400 response is
34 returned.
35
36 Otherwise the application is called.
37 """
38
39 def __init__(self, application, service_type, versions,
40 json_error_formatter=None):
41 """Create the WSGI middleware.
42
43 :param application: The application hosting the service.
44 :param service_type: The service type (entry in keystone catalog)
45 of the application.
46 :param versions: An ordered list of legitimate versions for the
47 application.
48 :param json_error_formatter: A Webob exception error formatter.
49 See Webob for details.
50 """
51 self.application = application
52 self.service_type = service_type
53 self.microversion_environ = '%s.microversion' % service_type
54 self.versions = versions
55 self.json_error_formatter = json_error_formatter
56
57 @webob.dec.wsgify
58 def __call__(self, req):
59 try:
60 microversion = microversion_parse.extract_version(
61 req.headers, self.service_type, self.versions)
62 # TODO(cdent): These error response are not formatted according to
63 # api-sig guidelines, unless a json_error_formatter is provided
64 # that can do it. For an example, see the placement service.
65 except ValueError as exc:
66 raise webob.exc.HTTPNotAcceptable(
67 ('Invalid microversion: %(error)s') % {'error': exc},
68 json_formatter=self.json_error_formatter)
69 except TypeError as exc:
70 raise webob.exc.HTTPBadRequest(
71 ('Invalid microversion: %(error)s') % {'error': exc},
72 json_formatter=self.json_error_formatter)
73
74 req.environ[self.microversion_environ] = microversion
75 microversion_header = '%s %s' % (self.service_type, microversion)
76 standard_header = microversion_parse.STANDARD_HEADER
77
78 try:
79 response = req.get_response(self.application)
80 except webob.exc.HTTPError as exc:
81 # If there was an HTTPError in the application we still need
82 # to send the microversion header, so add the header and
83 # re-raise the exception.
84 exc.headers.add(standard_header, microversion_header)
85 raise exc
86
87 response.headers.add(standard_header, microversion_header)
88 response.headers.add('vary', standard_header)
89 return response
0 # Tests that the middleware does microversioning as we expect
1 # The min version of the service is 1.0, the max is 1.2,
2 # the service type is "cats" (because the internet is for cats).
3
4 defaults:
5 request_headers:
6 # We must guard against webob requiring an accept header.
7 # We don't want to do this in the middleware itself as
8 # we don't know what the application would prefer as a
9 # default.
10 accept: application/json
11
12 tests:
13
14 - name: min default
15 GET: /good
16 response_headers:
17 openstack-api-version: cats 1.0
18
19 - name: max latest
20 GET: /good
21 request_headers:
22 openstack-api-version: cats latest
23 response_headers:
24 openstack-api-version: cats 1.2
25
26 - name: explict
27 GET: /good
28 request_headers:
29 openstack-api-version: cats 1.1
30 response_headers:
31 openstack-api-version: cats 1.1
32
33 - name: out of range
34 GET: /good
35 request_headers:
36 openstack-api-version: cats 1.9
37 status: 406
38 response_strings:
39 - Unacceptable version header
40
41 - name: invalid format
42 GET: /good
43 request_headers:
44 openstack-api-version: cats 1.9.5
45 status: 400
46 response_strings:
47 - invalid literal
48
49 - name: different service
50 desc: end up with default microversion
51 GET: /good
52 request_headers:
53 openstack-api-version: dogs 1.9
54 response_headers:
55 openstack-api-version: cats 1.0
56
57 - name: multiple services
58 GET: /good
59 request_headers:
60 openstack-api-version: dogs 1.9, cats 1.1
61 response_headers:
62 openstack-api-version: cats 1.1
63
64 - name: header present on exception
65 GET: /bad
66 request_headers:
67 openstack-api-version: dogs 1.9, cats 1.1
68 response_headers:
69 openstack-api-version: cats 1.1
70 status: 404
71 response_strings:
72 - /bad not found
73
74
0 # tests that the SimpleWSGI app is present
1
2 tests:
3
4 - name: get good
5 GET: /good
6 status: 200
7 response_strings:
8 - good
9
10 - name: get bad
11 GET: /bad
12 status: 404
13 response_strings:
14 - not found
0 # Licensed under the Apache License, Version 2.0 (the "License");
1 # you may not use this file except in compliance with the License.
2 # You may obtain a copy of the License at
3 #
4 # http://www.apache.org/licenses/LICENSE-2.0
5 #
6 # Unless required by applicable law or agreed to in writing, software
7 # distributed under the License is distributed on an "AS IS" BASIS,
8 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
9 # implied.
10 # See the License for the specific language governing permissions and
11 # limitations under the License.
12
13
14 import testtools
15
16 import microversion_parse
17
18
19 class TestVersion(testtools.TestCase):
20
21 def setUp(self):
22 super(TestVersion, self).setUp()
23 self.version = microversion_parse.Version(1, 5)
24
25 def test_version_is_tuple(self):
26 self.assertEqual((1, 5), self.version)
27
28 def test_version_stringifies(self):
29 self.assertEqual('1.5', str(self.version))
30
31 def test_version_matches(self):
32 max_version = microversion_parse.Version(1, 20)
33 min_version = microversion_parse.Version(1, 3)
34
35 self.assertTrue(self.version.matches(min_version, max_version))
36 self.assertFalse(self.version.matches(max_version, min_version))
37
38 def test_version_matches_inclusive(self):
39 max_version = microversion_parse.Version(1, 5)
40 min_version = microversion_parse.Version(1, 5)
41
42 self.assertTrue(self.version.matches(min_version, max_version))
43
44 def test_version_matches_no_extremes(self):
45 """If no extremes are present, never match."""
46 self.assertFalse(self.version.matches())
47
48 def test_version_zero_can_match(self):
49 """If a version is '0.0' we want to it be able to match."""
50 version = microversion_parse.Version(0, 0)
51 min_version = microversion_parse.Version(0, 0)
52 max_version = microversion_parse.Version(0, 0)
53 version.min_version = min_version
54 version.max_version = max_version
55
56 self.assertTrue(version.matches())
57
58 def test_version_zero_no_defaults(self):
59 """Any version, even 0.0, should never match without a min
60 and max being set.
61 """
62 version = microversion_parse.Version(0, 0)
63
64 self.assertFalse(version.matches())
65
66 def test_version_init_failure(self):
67 self.assertRaises(TypeError, microversion_parse.Version, 1, 2, 3)
68
69
70 class TestParseVersionString(testtools.TestCase):
71
72 def test_good_version(self):
73 version = microversion_parse.parse_version_string('1.1')
74 self.assertEqual((1, 1), version)
75 self.assertEqual(microversion_parse.Version(1, 1), version)
76
77 def test_adapt_whitespace(self):
78 version = microversion_parse.parse_version_string(' 1.1 ')
79 self.assertEqual((1, 1), version)
80 self.assertEqual(microversion_parse.Version(1, 1), version)
81
82 def test_non_numeric(self):
83 self.assertRaises(TypeError,
84 microversion_parse.parse_version_string,
85 'hello')
86
87 def test_mixed_alphanumeric(self):
88 self.assertRaises(TypeError,
89 microversion_parse.parse_version_string,
90 '1.a')
91
92 def test_too_many_numeric(self):
93 self.assertRaises(TypeError,
94 microversion_parse.parse_version_string,
95 '1.1.1')
96
97 def test_not_string(self):
98 self.assertRaises(TypeError,
99 microversion_parse.parse_version_string,
100 1.1)
101
102
103 class TestExtractVersion(testtools.TestCase):
104
105 def setUp(self):
106 super(TestExtractVersion, self).setUp()
107 self.headers = [
108 ('OpenStack-API-Version', 'service1 1.2'),
109 ('OpenStack-API-Version', 'service2 1.5'),
110 ('OpenStack-API-Version', 'service3 latest'),
111 ('OpenStack-API-Version', 'service4 2.5'),
112 ]
113 self.version_list = ['1.1', '1.2', '1.3', '1.4',
114 '2.1', '2.2', '2.3', '2.4']
115
116 def test_simple_extract(self):
117 version = microversion_parse.extract_version(
118 self.headers, 'service1', self.version_list)
119 self.assertEqual((1, 2), version)
120
121 def test_default_min(self):
122 version = microversion_parse.extract_version(
123 self.headers, 'notlisted', self.version_list)
124 self.assertEqual((1, 1), version)
125
126 def test_latest(self):
127 version = microversion_parse.extract_version(
128 self.headers, 'service3', self.version_list)
129 self.assertEqual((2, 4), version)
130
131 def test_min_max_extract(self):
132 version = microversion_parse.extract_version(
133 self.headers, 'service1', self.version_list)
134
135 # below min
136 self.assertFalse(version.matches((1, 3)))
137 # at min
138 self.assertTrue(version.matches((1, 2)))
139 # within extremes
140 self.assertTrue(version.matches())
141 # explicit max
142 self.assertTrue(version.matches(max_version=(2, 3)))
143 # explicit min
144 self.assertFalse(version.matches(min_version=(2, 3)))
145 # explicit both
146 self.assertTrue(version.matches(min_version=(0, 3),
147 max_version=(1, 5)))
148
149 def test_version_disabled(self):
150 self.assertRaises(ValueError, microversion_parse.extract_version,
151 self.headers, 'service2', self.version_list)
152
153 def test_version_out_of_range(self):
154 self.assertRaises(ValueError, microversion_parse.extract_version,
155 self.headers, 'service4', self.version_list)
0 # Licensed under the Apache License, Version 2.0 (the "License");
1 # you may not use this file except in compliance with the License.
2 # You may obtain a copy of the License at
3 #
4 # http://www.apache.org/licenses/LICENSE-2.0
5 #
6 # Unless required by applicable law or agreed to in writing, software
7 # distributed under the License is distributed on an "AS IS" BASIS,
8 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
9 # implied.
10 # See the License for the specific language governing permissions and
11 # limitations under the License.
12
13 import testtools
14
15 import microversion_parse
16
17
18 class TestHeadersFromWSGIEnviron(testtools.TestCase):
19
20 def test_empty_environ(self):
21 environ = {}
22 expected = {}
23 self.assertEqual(
24 expected,
25 microversion_parse.headers_from_wsgi_environ(environ))
26
27 def test_non_empty_no_headers(self):
28 environ = {'PATH_INFO': '/foo/bar'}
29 expected = {}
30 found_headers = microversion_parse.headers_from_wsgi_environ(environ)
31 self.assertEqual(expected, found_headers)
32
33 def test_headers(self):
34 environ = {'PATH_INFO': '/foo/bar',
35 'HTTP_OPENSTACK_API_VERSION': 'placement 2.1',
36 'HTTP_CONTENT_TYPE': 'application/json'}
37 expected = {'HTTP_OPENSTACK_API_VERSION': 'placement 2.1',
38 'HTTP_CONTENT_TYPE': 'application/json'}
39 found_headers = microversion_parse.headers_from_wsgi_environ(environ)
40 self.assertEqual(expected, found_headers)
41
42 def test_get_version_from_environ(self):
43 environ = {'PATH_INFO': '/foo/bar',
44 'HTTP_OPENSTACK_API_VERSION': 'placement 2.1',
45 'HTTP_CONTENT_TYPE': 'application/json'}
46 expected_version = '2.1'
47 headers = microversion_parse.headers_from_wsgi_environ(environ)
48 version = microversion_parse.get_version(headers, 'placement')
49 self.assertEqual(expected_version, version)
50
51 def test_get_version_from_environ_legacy(self):
52 environ = {'PATH_INFO': '/foo/bar',
53 'HTTP_X_OPENSTACK_PLACEMENT_API_VERSION': '2.1',
54 'HTTP_CONTENT_TYPE': 'application/json'}
55 expected_version = '2.1'
56 headers = microversion_parse.headers_from_wsgi_environ(environ)
57 version = microversion_parse.get_version(
58 headers, 'placement',
59 legacy_headers=['x-openstack-placement-api-version'])
60 self.assertEqual(expected_version, version)
0 # Licensed under the Apache License, Version 2.0 (the "License");
1 # you may not use this file except in compliance with the License.
2 # You may obtain a copy of the License at
3 #
4 # http://www.apache.org/licenses/LICENSE-2.0
5 #
6 # Unless required by applicable law or agreed to in writing, software
7 # distributed under the License is distributed on an "AS IS" BASIS,
8 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
9 # implied.
10 # See the License for the specific language governing permissions and
11 # limitations under the License.
12
13 # The microversion_parse middlware is tests using gabbi to run real
14 # http requests through it. To do that, we need a simple WSGI
15 # application running under wsgi-intercept (handled by gabbi).
16
17 import os
18
19 from gabbi import driver
20 import webob
21
22 from microversion_parse import middleware
23
24
25 TESTS_DIR = 'gabbits'
26 SERVICE_TYPE = 'cats'
27 VERSIONS = [
28 '1.0', # initial version
29 '1.1', # now with kittens
30 '1.2', # added breeds
31 ]
32
33
34 class SimpleWSGI(object):
35 """A WSGI application that can be contiained within a middlware."""
36
37 def __call__(self, environ, start_response):
38 path_info = environ['PATH_INFO']
39 if path_info == '/good':
40 start_response('200 OK', [('content-type', 'text/plain')])
41 return [b'good']
42
43 raise webob.exc.HTTPNotFound('%s not found' % path_info)
44
45
46 def app():
47 app = middleware.MicroversionMiddleware(
48 SimpleWSGI(), SERVICE_TYPE, VERSIONS)
49 return app
50
51
52 def load_tests(loader, tests, pattern):
53 """Provide a TestSuite to the discovery process."""
54 test_dir = os.path.join(os.path.dirname(__file__), TESTS_DIR)
55 return driver.build_tests(
56 test_dir, loader, test_loader_name=__name__, intercept=app)
0
0 WebOb>=1.2.3 # MIT
1313 Programming Language :: Python :: 2
1414 Programming Language :: Python :: 2.7
1515 Programming Language :: Python :: 3
16 Programming Language :: Python :: 3.4
1716 Programming Language :: Python :: 3.5
17 Programming Language :: Python :: 3.6
1818
1919 [files]
2020 packages =
2424 all_files = 1
2525 build-dir = doc/build
2626 source-dir = doc/source
27
28 [bdist_wheel]
29 universal=1
11 coverage>=3.6 # Apache-2.0
22 sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2 # BSD
33 oslosphinx!=3.4.0,>=2.5.0 # Apache-2.0
4 testrepository>=0.0.18 # Apache-2.0/BSD
4 stestr>=1.0.0 # Apache-2.0
55 testtools>=1.4.0 # MIT
6 WebOb>=1.2.3 # MIT
6 gabbi>=1.35.0 # Apache-2.0
22 skipsdist = True
33 # If you want pypy or pypy3, do 'tox -epypy,pypy3', it might work!
44 # And you can get coverage with 'tox -ecover'.
5 envlist = py27,py34,py35,pep8
5 envlist = py27,py36,py35,pep8
66
77 [testenv]
88 deps = -r{toxinidir}/requirements.txt
99 -r{toxinidir}/test-requirements.txt
1010 install_command = pip install -U {opts} {packages}
11 setenv = OS_TEST_PATH=microversion_parse/tests/
1211 usedevelop = True
13 commands = python setup.py testr --testr-args="{posargs}"
12 commands = stestr run {posargs}
1413
1514 [testenv:venv]
1615 deps = -r{toxinidir}/requirements.txt
2423 flake8
2524
2625 [testenv:cover]
27 commands = python setup.py testr --coverage --testr-args="{posargs}"
26 setenv = PYTHON=coverage run --source microversion_parse --parallel-mode
27 commands =
28 coverage erase
29 find . -type f -name "*.pyc" -delete
30 stestr run {posargs}
31 coverage combine
32 coverage html -d cover
33 whitelist_externals =
34 find
2835
2936 [testenv:docs]
3037 commands =