typing: Annotate keystoneauth1.adapter

We introduce a new subclass to share between Adapter and
LegacyJSONAdapter, _BaseAdapter, to avoid violating the Liskov
substitution principle.

Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
Change-Id: I4512aa19fd3068bea3f7f38924947a137c7d2c26
This commit is contained in:
Stephen Finucane 2024-08-12 10:37:57 +01:00
parent 6b2c8fd891
commit 0383309ced
2 changed files with 213 additions and 69 deletions

View File

@ -10,6 +10,9 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import argparse
import collections
import logging
import os import os
import typing as ty import typing as ty
import warnings import warnings
@ -17,10 +20,14 @@ import warnings
import requests import requests
from keystoneauth1 import _fair_semaphore from keystoneauth1 import _fair_semaphore
from keystoneauth1 import discover
from keystoneauth1 import session from keystoneauth1 import session
if ty.TYPE_CHECKING:
from keystoneauth1 import plugin
class Adapter:
class _BaseAdapter:
"""An instance of a session with local variables. """An instance of a session with local variables.
A session is a global object that is shared around amongst many clients. It A session is a global object that is shared around amongst many clients. It
@ -116,38 +123,40 @@ class Adapter:
a maximum of 60 seconds is used. a maximum of 60 seconds is used.
""" """
client_name = None client_name: ty.Optional[str] = None
client_version = None client_version: ty.Optional[str] = None
def __init__( def __init__(
self, self,
session, session: session.Session,
service_type=None, service_type: ty.Optional[str] = None,
service_name=None, service_name: ty.Optional[str] = None,
interface=None, interface: ty.Optional[str] = None,
region_name=None, region_name: ty.Optional[str] = None,
endpoint_override=None, endpoint_override: ty.Optional[str] = None,
version=None, version: ty.Optional[str] = None,
auth=None, auth: ty.Optional['plugin.BaseAuthPlugin'] = None,
user_agent=None, user_agent: ty.Optional[str] = None,
connect_retries=None, connect_retries: ty.Optional[int] = None,
logger=None, logger: ty.Optional[logging.Logger] = None,
allow=None, allow: ty.Optional[ty.Dict[str, ty.Any]] = None,
additional_headers=None, additional_headers: ty.Optional[
client_name=None, collections.abc.MutableMapping[str, str]
client_version=None, ] = None,
allow_version_hack=None, client_name: ty.Optional[str] = None,
global_request_id=None, client_version: ty.Optional[str] = None,
min_version=None, allow_version_hack: ty.Optional[bool] = None,
max_version=None, global_request_id: ty.Optional[str] = None,
default_microversion=None, min_version: ty.Optional[str] = None,
status_code_retries=None, max_version: ty.Optional[str] = None,
retriable_status_codes=None, default_microversion: ty.Optional[str] = None,
raise_exc=None, status_code_retries: ty.Optional[int] = None,
rate_limit=None, retriable_status_codes: ty.Optional[ty.List[int]] = None,
concurrency=None, raise_exc: ty.Optional[bool] = None,
connect_retry_delay=None, rate_limit: ty.Optional[float] = None,
status_code_retry_delay=None, concurrency: ty.Optional[int] = None,
connect_retry_delay: ty.Optional[float] = None,
status_code_retry_delay: ty.Optional[float] = None,
): ):
if version and (min_version or max_version): if version and (min_version or max_version):
raise TypeError( raise TypeError(
@ -199,7 +208,9 @@ class Adapter:
concurrency, rate_delay concurrency, rate_delay
) )
def _set_endpoint_filter_kwargs(self, kwargs): def _set_endpoint_filter_kwargs(
self, kwargs: ty.Dict[str, object]
) -> None:
if self.service_type: if self.service_type:
kwargs.setdefault('service_type', self.service_type) kwargs.setdefault('service_type', self.service_type)
if self.service_name: if self.service_name:
@ -217,10 +228,12 @@ class Adapter:
if self.allow_version_hack is not None: if self.allow_version_hack is not None:
kwargs.setdefault('allow_version_hack', self.allow_version_hack) kwargs.setdefault('allow_version_hack', self.allow_version_hack)
def request(self, url, method, **kwargs): def _request(
self, url: str, method: str, **kwargs: ty.Any
) -> requests.Response:
endpoint_filter = kwargs.setdefault('endpoint_filter', {}) endpoint_filter = kwargs.setdefault('endpoint_filter', {})
self._set_endpoint_filter_kwargs(endpoint_filter) self._set_endpoint_filter_kwargs(endpoint_filter)
# NOTE(gmann): Convert r initlize the headers to # NOTE(gmann): Convert or initlize the headers to
# CaseInsensitiveDict to make sure headers are # CaseInsensitiveDict to make sure headers are
# case insensitive. # case insensitive.
if kwargs.get('headers'): if kwargs.get('headers'):
@ -282,7 +295,9 @@ class Adapter:
return self.session.request(url, method, **kwargs) return self.session.request(url, method, **kwargs)
def get_token(self, auth=None): def get_token(
self, auth: ty.Optional['plugin.BaseAuthPlugin'] = None
) -> ty.Optional[str]:
"""Return a token as provided by the auth plugin. """Return a token as provided by the auth plugin.
:param auth: The auth plugin to use for token. Overrides the plugin :param auth: The auth plugin to use for token. Overrides the plugin
@ -297,7 +312,11 @@ class Adapter:
""" """
return self.session.get_token(auth or self.auth) return self.session.get_token(auth or self.auth)
def get_endpoint(self, auth=None, **kwargs): def get_endpoint(
self,
auth: ty.Optional['plugin.BaseAuthPlugin'] = None,
**kwargs: ty.Any,
) -> ty.Optional[str]:
"""Get an endpoint as provided by the auth plugin. """Get an endpoint as provided by the auth plugin.
:param auth: The auth plugin to use for token. Overrides the plugin on :param auth: The auth plugin to use for token. Overrides the plugin on
@ -316,7 +335,9 @@ class Adapter:
self._set_endpoint_filter_kwargs(kwargs) self._set_endpoint_filter_kwargs(kwargs)
return self.session.get_endpoint(auth or self.auth, **kwargs) return self.session.get_endpoint(auth or self.auth, **kwargs)
def get_endpoint_data(self, auth=None): def get_endpoint_data(
self, auth: ty.Optional['plugin.BaseAuthPlugin'] = None
) -> ty.Optional['discover.EndpointData']:
"""Get the endpoint data for this Adapter's endpoint. """Get the endpoint data for this Adapter's endpoint.
:param auth: The auth plugin to use for token. Overrides the plugin on :param auth: The auth plugin to use for token. Overrides the plugin on
@ -337,7 +358,11 @@ class Adapter:
return self.session.get_endpoint_data(auth or self.auth, **kwargs) return self.session.get_endpoint_data(auth or self.auth, **kwargs)
def get_all_version_data(self, interface='public', region_name=None): def get_all_version_data(
self, interface: str = 'public', region_name: ty.Optional[str] = None
) -> ty.Dict[
str, ty.Dict[str, ty.Dict[str, ty.List[discover.VersionData]]]
]:
"""Get data about all versions of a service. """Get data about all versions of a service.
:param interface: :param interface:
@ -358,7 +383,11 @@ class Adapter:
service_type=self.service_type, service_type=self.service_type,
) )
def get_api_major_version(self, auth=None, **kwargs): def get_api_major_version(
self,
auth: ty.Optional['plugin.BaseAuthPlugin'] = None,
**kwargs: ty.Any,
) -> ty.Optional[ty.Tuple[ty.Union[int, float], ...]]:
"""Get the major API version as provided by the auth plugin. """Get the major API version as provided by the auth plugin.
:param auth: The auth plugin to use for token. Overrides the plugin on :param auth: The auth plugin to use for token. Overrides the plugin on
@ -377,11 +406,20 @@ class Adapter:
return self.session.get_api_major_version(auth or self.auth, **kwargs) return self.session.get_api_major_version(auth or self.auth, **kwargs)
def invalidate(self, auth=None): def invalidate(
"""Invalidate an authentication plugin.""" self, auth: ty.Optional['plugin.BaseAuthPlugin'] = None
) -> bool:
"""Invalidate an authentication plugin.
:param auth: The auth plugin to invalidate. Overrides the plugin on the
session. (optional)
:type auth: keystoneauth1.plugin.BaseAuthPlugin
"""
return self.session.invalidate(auth or self.auth) return self.session.invalidate(auth or self.auth)
def get_user_id(self, auth=None): def get_user_id(
self, auth: ty.Optional['plugin.BaseAuthPlugin'] = None
) -> ty.Optional[str]:
"""Return the authenticated user_id as provided by the auth plugin. """Return the authenticated user_id as provided by the auth plugin.
:param auth: The auth plugin to use for token. Overrides the plugin :param auth: The auth plugin to use for token. Overrides the plugin
@ -398,7 +436,9 @@ class Adapter:
""" """
return self.session.get_user_id(auth or self.auth) return self.session.get_user_id(auth or self.auth)
def get_project_id(self, auth=None): def get_project_id(
self, auth: ty.Optional['plugin.BaseAuthPlugin'] = None
) -> ty.Optional[str]:
"""Return the authenticated project_id as provided by the auth plugin. """Return the authenticated project_id as provided by the auth plugin.
:param auth: The auth plugin to use for token. Overrides the plugin :param auth: The auth plugin to use for token. Overrides the plugin
@ -415,27 +455,13 @@ class Adapter:
""" """
return self.session.get_project_id(auth or self.auth) return self.session.get_project_id(auth or self.auth)
def get(self, url, **kwargs):
return self.request(url, 'GET', **kwargs)
def head(self, url, **kwargs):
return self.request(url, 'HEAD', **kwargs)
def post(self, url, **kwargs):
return self.request(url, 'POST', **kwargs)
def put(self, url, **kwargs):
return self.request(url, 'PUT', **kwargs)
def patch(self, url, **kwargs):
return self.request(url, 'PATCH', **kwargs)
def delete(self, url, **kwargs):
return self.request(url, 'DELETE', **kwargs)
# TODO(efried): Move this to loading.adapter.Adapter # TODO(efried): Move this to loading.adapter.Adapter
@classmethod @classmethod
def register_argparse_arguments(cls, parser, service_type=None): def register_argparse_arguments(
cls,
parser: argparse.ArgumentParser,
service_type: ty.Optional[str] = None,
) -> None:
"""Attach arguments to a given argparse Parser for Adapters. """Attach arguments to a given argparse Parser for Adapters.
:param parser: The argparse parser to attach options to. :param parser: The argparse parser to attach options to.
@ -492,7 +518,9 @@ class Adapter:
# TODO(efried): Move this to loading.adapter.Adapter # TODO(efried): Move this to loading.adapter.Adapter
@classmethod @classmethod
def register_service_argparse_arguments(cls, parser, service_type): def register_service_argparse_arguments(
cls, parser: argparse.ArgumentParser, service_type: str
) -> None:
"""Attach arguments to a given argparse Parser for Adapters. """Attach arguments to a given argparse Parser for Adapters.
:param parser: The argparse parser to attach options to. :param parser: The argparse parser to attach options to.
@ -561,7 +589,56 @@ class Adapter:
) )
class LegacyJsonAdapter(Adapter): class Adapter(_BaseAdapter):
def request(
self, url: str, method: str, **kwargs: ty.Any
) -> requests.Response:
return self._request(url, method, **kwargs)
def get(self, url: str, **kwargs: ty.Any) -> requests.Response:
"""Perform a GET request.
This calls :py:meth:`.request()` with ``method`` set to ``GET``.
"""
return self.request(url, 'GET', **kwargs)
def head(self, url: str, **kwargs: ty.Any) -> requests.Response:
"""Perform a HEAD request.
This calls :py:meth:`.request()` with ``method`` set to ``HEAD``.
"""
return self.request(url, 'HEAD', **kwargs)
def post(self, url: str, **kwargs: ty.Any) -> requests.Response:
"""Perform a POST request.
This calls :py:meth:`.request()` with ``method`` set to ``POST``.
"""
return self.request(url, 'POST', **kwargs)
def put(self, url: str, **kwargs: ty.Any) -> requests.Response:
"""Perform a PUT request.
This calls :py:meth:`.request()` with ``method`` set to ``PUT``.
"""
return self.request(url, 'PUT', **kwargs)
def patch(self, url: str, **kwargs: ty.Any) -> requests.Response:
"""Perform a PATCH request.
This calls :py:meth:`.request()` with ``method`` set to ``PATCH``.
"""
return self.request(url, 'PATCH', **kwargs)
def delete(self, url: str, **kwargs: ty.Any) -> requests.Response:
"""Perform a DELETE request.
This calls :py:meth:`.request()` with ``method`` set to ``DELETE``.
"""
return self.request(url, 'DELETE', **kwargs)
class LegacyJsonAdapter(_BaseAdapter):
"""Make something that looks like an old HTTPClient. """Make something that looks like an old HTTPClient.
A common case when using an adapter is that we want an interface similar to A common case when using an adapter is that we want an interface similar to
@ -570,7 +647,9 @@ class LegacyJsonAdapter(Adapter):
You probably don't want this if you are starting from scratch. You probably don't want this if you are starting from scratch.
""" """
def request(self, *args, **kwargs): def request(
self, url: str, method: str, **kwargs: ty.Any
) -> ty.Tuple[requests.Response, object]:
headers = kwargs.setdefault('headers', {}) headers = kwargs.setdefault('headers', {})
headers.setdefault('Accept', 'application/json') headers.setdefault('Accept', 'application/json')
@ -579,7 +658,7 @@ class LegacyJsonAdapter(Adapter):
except KeyError: except KeyError:
pass pass
resp = super().request(*args, **kwargs) resp = self._request(url, method, **kwargs)
try: try:
body = resp.json() body = resp.json()
@ -588,14 +667,76 @@ class LegacyJsonAdapter(Adapter):
return resp, body return resp, body
def get(
self, url: str, **kwargs: ty.Any
) -> ty.Tuple[requests.Response, object]:
"""Perform a GET request.
This calls :py:meth:`.request()` with ``method`` set to ``GET``.
"""
return self.request(url, 'GET', **kwargs)
def head(
self, url: str, **kwargs: ty.Any
) -> ty.Tuple[requests.Response, object]:
"""Perform a HEAD request.
This calls :py:meth:`.request()` with ``method`` set to ``HEAD``.
"""
return self.request(url, 'HEAD', **kwargs)
def post(
self, url: str, **kwargs: ty.Any
) -> ty.Tuple[requests.Response, object]:
"""Perform a POST request.
This calls :py:meth:`.request()` with ``method`` set to ``POST``.
"""
return self.request(url, 'POST', **kwargs)
def put(
self, url: str, **kwargs: ty.Any
) -> ty.Tuple[requests.Response, object]:
"""Perform a PUT request.
This calls :py:meth:`.request()` with ``method`` set to ``PUT``.
"""
return self.request(url, 'PUT', **kwargs)
def patch(
self, url: str, **kwargs: ty.Any
) -> ty.Tuple[requests.Response, object]:
"""Perform a PATCH request.
This calls :py:meth:`.request()` with ``method`` set to ``PATCH``.
"""
return self.request(url, 'PATCH', **kwargs)
def delete(
self, url: str, **kwargs: ty.Any
) -> ty.Tuple[requests.Response, object]:
"""Perform a DELETE request.
This calls :py:meth:`.request()` with ``method`` set to ``DELETE``.
"""
return self.request(url, 'DELETE', **kwargs)
# TODO(efried): Deprecate this in favor of # TODO(efried): Deprecate this in favor of
# loading.adapter.register_argparse_arguments # loading.adapter.register_argparse_arguments
def register_adapter_argparse_arguments(*args, **kwargs): def register_adapter_argparse_arguments(
return Adapter.register_argparse_arguments(*args, **kwargs) parser: argparse.ArgumentParser, service_type: ty.Optional[str] = None
) -> None:
return Adapter.register_argparse_arguments(
parser=parser, service_type=service_type
)
# TODO(efried): Deprecate this in favor of # TODO(efried): Deprecate this in favor of
# loading.adapter.register_service_argparse_arguments # loading.adapter.register_service_argparse_arguments
def register_service_adapter_argparse_arguments(*args, **kwargs): def register_service_adapter_argparse_arguments(
return Adapter.register_service_argparse_arguments(*args, **kwargs) parser: argparse.ArgumentParser, service_type: str
) -> None:
return Adapter.register_service_argparse_arguments(
parser=parser, service_type=service_type
)

View File

@ -102,6 +102,9 @@ exclude = (?x)(
[mypy-keystoneauth1.tests.unit.*] [mypy-keystoneauth1.tests.unit.*]
ignore_errors = true ignore_errors = true
[mypy-keystoneauth1.adapter]
disallow_untyped_defs = true
[mypy-keystoneauth1.discover] [mypy-keystoneauth1.discover]
disallow_untyped_defs = true disallow_untyped_defs = true