Files
microversion-parse/microversion_parse/middleware.py
Stephen Finucane 6653de91d5 Add typing
Change-Id: I401b89f32a445c16f8f2194b87535c7f384a1652
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-11-19 15:23:15 +00:00

116 lines
4.1 KiB
Python

# 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.
"""WSGI middleware for getting microversion info."""
from collections.abc import Sequence
from typing import Any, Protocol, TYPE_CHECKING
import webob
import webob.dec
import webob.exc
import microversion_parse
if TYPE_CHECKING:
from _typeshed.wsgi import WSGIApplication
class _JSONFormatter(Protocol):
def __call__(
self, *, body: str, status: str, title: str, environ: dict[str, Any]
) -> Any: ...
class MicroversionMiddleware:
"""WSGI middleware for getting microversion info.
The application will get a WSGI environ with a
'SERVICE_TYPE.microversion' key that has a value of the microversion
found at an 'openstack-api-version' header that matches SERVICE_TYPE. If
no header is found, the minimum microversion will be set. If the
special keyword 'latest' is used, the maximum microversion will be
set.
If the requested microversion is not available a 406 response is
returned.
If there is an error parsing a provided header, a 400 response is
returned.
Otherwise the application is called.
"""
def __init__(
self,
application: 'WSGIApplication | None',
service_type: str,
versions: Sequence[str],
json_error_formatter: _JSONFormatter | None = None,
) -> None:
"""Create the WSGI middleware.
:param application: The application hosting the service.
:param service_type: The service type (entry in keystone catalog)
of the application.
:param versions: An ordered list of legitimate versions for the
application.
:param json_error_formatter: A Webob exception error formatter.
See Webob for details.
"""
self.application = application
self.service_type = service_type
self.microversion_environ = f'{service_type}.microversion'
self.versions = versions
self.json_error_formatter = json_error_formatter
@webob.dec.wsgify
def __call__(
self,
req: webob.request.Request,
) -> webob.response.Response | None:
try:
microversion = microversion_parse.extract_version(
req.headers, self.service_type, self.versions
)
# TODO(cdent): These error response are not formatted according to
# api-sig guidelines, unless a json_error_formatter is provided
# that can do it. For an example, see the placement service.
except ValueError as exc:
raise webob.exc.HTTPNotAcceptable(
(f'Invalid microversion: {exc}'),
json_formatter=self.json_error_formatter,
)
except TypeError as exc:
raise webob.exc.HTTPBadRequest(
(f'Invalid microversion: {exc}'),
json_formatter=self.json_error_formatter,
)
req.environ[self.microversion_environ] = microversion
microversion_header = f'{self.service_type} {microversion}'
standard_header = microversion_parse.STANDARD_HEADER
try:
response = req.get_response(self.application)
except webob.exc.HTTPError as exc:
# If there was an HTTPError in the application we still need
# to send the microversion header, so add the header and
# re-raise the exception.
exc.headers.add(standard_header, microversion_header)
raise exc
response.headers.add(standard_header, microversion_header)
response.headers.add('vary', standard_header)
return response