From 9670f833391de4ba47e6b9a895e9f9979267cced Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pazderka?= Date: Mon, 20 Feb 2023 12:42:46 +0100 Subject: [PATCH] Improve settings by using pydantic --- CHANGELOG.md | 4 ++ doc/conf.py | 7 +++ setup.py | 3 +- src/oic/utils/settings.py | 104 ++++++++++---------------------------- 4 files changed, 39 insertions(+), 79 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 36b8b977e..86e9079a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on the [KeepAChangeLog] project. [KeepAChangeLog]: https://keepachangelog.com/ ## Unreleased +### Changed +- [#847] Using pydantic for settings instead of custom class + +[#847]: https://github.com/CZ-NIC/pyoidc/pull/847 ## 1.5.0 [2022-12-14] diff --git a/doc/conf.py b/doc/conf.py index 4eef9c4d4..7351e8368 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -1,12 +1,19 @@ import alabaster +import os +import sys + +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'src'))) extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.napoleon', + 'sphinxcontrib.autodoc_pydantic', ] autoclass_content = 'both' # Merge the __init__ docstring into the class docstring. autodoc_member_order = 'bysource' # Order by source ordering +autodoc_pydantic_model_show_config = True +autodoc_pydantic_settings_show_json = False templates_path = ['_templates'] diff --git a/setup.py b/setup.py index 8b14d65da..a56df08d2 100755 --- a/setup.py +++ b/setup.py @@ -78,7 +78,7 @@ def run_tests(self): extras_require={ 'develop': ["cherrypy==3.2.4", "pyOpenSSL"], 'testing': tests_requires, - 'docs': ['Sphinx', 'sphinx-autobuild', 'alabaster'], + 'docs': ['Sphinx', 'sphinx-autobuild', 'alabaster', 'autodoc_pydantic'], 'quality': ['pylama', 'isort', 'eradicate', 'mypy', 'black', 'bandit', 'readme_renderer[md]'], 'types': ['types-requests'], 'ldap_authn': ['python-ldap'], @@ -87,6 +87,7 @@ def run_tests(self): install_requires=[ "requests", "pycryptodomex", + "pydantic", "pyjwkest>=1.3.6", "mako", "cryptography", diff --git a/src/oic/utils/settings.py b/src/oic/utils/settings.py index 0a2730e26..c4cda7e21 100644 --- a/src/oic/utils/settings.py +++ b/src/oic/utils/settings.py @@ -8,97 +8,48 @@ In order to configure some objects in PyOIDC, you need a settings object. If you need to add some settings, make sure that you settings class inherits from the appropriate class in this module. + +The settings make use of `pydantic `_ library. +It is possible to instance them directly or use environment values to fill the settings. """ -import typing from typing import Optional from typing import Tuple from typing import Union import requests +from pydantic import BaseSettings -class SettingsException(Exception): - """Exception raised by misconfigured settings class.""" - +class PyoidcSettings(BaseSettings): + """Main class for all settings shared among consumer and client.""" -class PyoidcSettings: + verify_ssl: Union[bool, str] = True """ - Main class for all settings shared among consumer and client. - - Keyword Args: - verify_ssl - Control TLS server certificate validation. - If set to True the certificate is validated against the global settings, - if set to False, no validation is performed. - If set to a filename this is used as a certificate bundle in openssl format. - If set to a directory name this is used as a CA directory in the openssl format. - client_cert - Local cert to use as client side certificate. - Can be a single file (containing the private key and the certificate) or a tuple of both file's path. - timeout - Timeout for requests library. - Can be specified either as a single float or as a tuple of floats. - For more details, refer to ``requests`` documentation. + Control TLS server certificate validation: + * If set to True the certificate is validated against the global settings, + * If set to False, no validation is performed. + * If set to a filename this is used as a certificate bundle in openssl format. + * If set to a directory name this is used as a CA directory in the openssl format. """ - - def __init__( - self, - verify_ssl: Union[bool, str] = True, - client_cert: Union[None, str, Tuple[str, str]] = None, - timeout: Union[float, Tuple[float, float]] = 5, - ): - self.verify_ssl = verify_ssl - self.client_cert = client_cert - self.timeout = timeout - - def __setattr__(self, name, value): - """This attempts to check if value matches the expected value.""" - annotation = typing.get_type_hints(self.__init__)[name] # type: ignore - # Expand Union -> Since 3.8, this can be written as typing.get_origin - if getattr(annotation, "__origin__", annotation) is Union: - expanded = tuple(an for an in annotation.__args__) - else: - expanded = (annotation,) - # Convert Generics - # FIXME: this doesn't check the args of the generic - resolved = tuple(getattr(an, "__origin__", an) for an in expanded) - # Add int if float is present - if float in resolved: - resolved = resolved + (int,) - # FIXME: Add more valid substitution - if isinstance(value, resolved): - # FIXME: Handle bool being an instance of int... - super().__setattr__(name, value) - else: - raise SettingsException( - "%s has a type of %s, expected any of %s." - % (name, type(value), resolved), - ) - - -class ClientSettings(PyoidcSettings): + client_cert: Union[None, str, Tuple[str, str]] = None + """ + Local cert to use as client side certificate. + Can be a single file (containing the private key and the certificate) or a tuple of both file's path. + """ + timeout: Union[float, Tuple[float, float]] = 5 + """ + Timeout for requests library. + Can be specified either as a single float or as a tuple of floats. + For more details, refer to ``requests`` documentation. """ - Base settings for consumer shared among OAuth 2.0 and OpenID Connect. - Keyword Args: - requests_session - Instance of `requests.Session` with configuration options. - """ +class ClientSettings(PyoidcSettings): + """Base settings for consumer shared among OAuth 2.0 and OpenID Connect.""" - def __init__( - self, - verify_ssl: Union[bool, str] = True, - client_cert: Union[None, str, Tuple[str, str]] = None, - timeout: Union[float, Tuple[float, float]] = 5, - requests_session: Optional[requests.Session] = None, - ): - super().__init__( - verify_ssl=verify_ssl, client_cert=client_cert, timeout=timeout - ) - # For session persistence - self.requests_session = requests_session + requests_session: Optional[requests.Session] = None + """Instance of `requests.Session` with configuration options.""" class OauthClientSettings(ClientSettings): @@ -131,6 +82,3 @@ class OauthProviderSettings(OauthServerSettings): class OicProviderSettings(OicServerSettings): """Specific settings for OpenID Connect provider.""" - - # TODO: Decide on inheritance... - # It might be better to have a mixin providing OIC specific stuff?