From 35a4b69d9b827608a63ea9394eeebaeecce79bdc Mon Sep 17 00:00:00 2001 From: Guillaume Pujol Date: Mon, 8 Sep 2025 11:52:09 +0200 Subject: [PATCH 1/5] refactor serializers and enums into their own submodules --- requests_oauth2client/__init__.py | 8 +- .../authorization_request.py | 121 +------- requests_oauth2client/client.py | 37 +-- requests_oauth2client/deprecated.py | 1 + requests_oauth2client/dpop.py | 7 +- requests_oauth2client/enums.py | 83 +++++ requests_oauth2client/flask/auth.py | 3 +- requests_oauth2client/serializers.py | 288 ++++++++++++++++++ requests_oauth2client/tokens.py | 101 +----- tests/test_deprecated_names.py | 1 + tests/unit_tests/conftest.py | 12 + .../unit_tests/test_authorization_request.py | 27 -- tests/unit_tests/test_serializers.py | 58 ++++ tests/unit_tests/test_tokens.py | 22 +- 14 files changed, 458 insertions(+), 311 deletions(-) create mode 100644 requests_oauth2client/enums.py create mode 100644 requests_oauth2client/serializers.py create mode 100644 tests/unit_tests/test_serializers.py diff --git a/requests_oauth2client/__init__.py b/requests_oauth2client/__init__.py index a2b6039..2aac22f 100644 --- a/requests_oauth2client/__init__.py +++ b/requests_oauth2client/__init__.py @@ -18,16 +18,13 @@ ) from .authorization_request import ( AuthorizationRequest, - AuthorizationRequestSerializer, AuthorizationResponse, - CodeChallengeMethods, InvalidCodeVerifierParam, InvalidMaxAgeParam, MissingIssuerParam, PkceUtils, RequestParameterAuthorizationRequest, RequestUriParameterAuthorizationRequest, - ResponseTypes, UnsupportedCodeChallengeMethod, UnsupportedResponseTypeParam, ) @@ -36,8 +33,6 @@ BackChannelAuthenticationResponse, ) from .client import ( - Endpoints, - GrantTypes, InvalidAcrValuesParam, InvalidBackchannelAuthenticationRequestHintParam, InvalidDiscoveryDocument, @@ -93,6 +88,7 @@ RepeatedDPoPNonce, validate_dpop_proof, ) +from .enums import CodeChallengeMethods, Endpoints, GrantTypes, ResponseTypes from .exceptions import ( AccessDenied, AccountSelectionRequired, @@ -135,9 +131,9 @@ from .polling import ( BaseTokenEndpointPollingJob, ) +from .serializers import AuthorizationRequestSerializer, BearerTokenSerializer from .tokens import ( BearerToken, - BearerTokenSerializer, ExpiredAccessToken, ExpiredIdToken, IdToken, diff --git a/requests_oauth2client/authorization_request.py b/requests_oauth2client/authorization_request.py index 695e0ea..7a8c676 100644 --- a/requests_oauth2client/authorization_request.py +++ b/requests_oauth2client/authorization_request.py @@ -4,9 +4,8 @@ import re import secrets -from enum import Enum from functools import cached_property -from typing import TYPE_CHECKING, Any, Callable, ClassVar +from typing import TYPE_CHECKING, Any, ClassVar from attrs import asdict, field, fields, frozen from binapy import BinaPy @@ -14,6 +13,7 @@ from jwskate import JweCompact, Jwk, Jwt, SignatureAlgs, SignedJwt from .dpop import DPoPKey +from .enums import CodeChallengeMethods, ResponseTypes from .exceptions import ( AuthorizationResponseError, ConsentRequired, @@ -32,34 +32,6 @@ from datetime import datetime -class ResponseTypes(str, Enum): - """All standardised `response_type` values. - - Note that you should always use `code`. All other values are deprecated. - - """ - - CODE = "code" - NONE = "none" - TOKEN = "token" - IDTOKEN = "id_token" - CODE_IDTOKEN = "code id_token" - CODE_TOKEN = "code token" - CODE_IDTOKEN_TOKEN = "code id_token token" - IDTOKEN_TOKEN = "id_token token" - - -class CodeChallengeMethods(str, Enum): - """All standardised `code_challenge_method` values. - - You should always use `S256`. - - """ - - S256 = "S256" - plain = "plain" - - class UnsupportedCodeChallengeMethod(ValueError): """Raised when an unsupported `code_challenge_method` is provided.""" @@ -908,92 +880,3 @@ def __getattr__(self, item: str) -> Any: def __repr__(self) -> str: """Return the Authorization Request URI, as a `str`.""" return self.uri - - -class AuthorizationRequestSerializer: - """(De)Serializer for `AuthorizationRequest` instances. - - You might need to store pending authorization requests in session, either server-side or client- side. This class is - here to help you do that. - - """ - - def __init__( - self, - dumper: Callable[[AuthorizationRequest], str] | None = None, - loader: Callable[[str], AuthorizationRequest] | None = None, - ) -> None: - self.dumper = dumper or self.default_dumper - self.loader = loader or self.default_loader - - @staticmethod - def default_dumper(azr: AuthorizationRequest) -> str: - """Provide a default dumper implementation. - - Serialize an AuthorizationRequest as JSON, then compress with deflate, then encodes as - base64url. - - Args: - azr: the `AuthorizationRequest` to serialize - - Returns: - the serialized value - - """ - d = asdict(azr) - if azr.dpop_key: - d["dpop_key"]["private_key"] = azr.dpop_key.private_key.to_dict() - d.update(**d.pop("kwargs", {})) - return BinaPy.serialize_to("json", d).to("deflate").to("b64u").ascii() - - @staticmethod - def default_loader( - serialized: str, - azr_class: type[AuthorizationRequest] = AuthorizationRequest, - ) -> AuthorizationRequest: - """Provide a default deserializer implementation. - - This does the opposite operations than `default_dumper`. - - Args: - serialized: the serialized AuthorizationRequest - azr_class: the class to deserialize the Authorization Request to - - Returns: - an AuthorizationRequest - - """ - args = BinaPy(serialized).decode_from("b64u").decode_from("deflate").parse_from("json") - - if dpop_key := args.get("dpop_key"): - dpop_key["private_key"] = Jwk(dpop_key["private_key"]) - dpop_key.pop("jti_generator", None) - dpop_key.pop("iat_generator", None) - dpop_key.pop("dpop_token_class", None) - args["dpop_key"] = DPoPKey(**dpop_key) - - return azr_class(**args) - - def dumps(self, azr: AuthorizationRequest) -> str: - """Serialize and compress a given AuthorizationRequest for easier storage. - - Args: - azr: an AuthorizationRequest to serialize - - Returns: - the serialized AuthorizationRequest, as a str - - """ - return self.dumper(azr) - - def loads(self, serialized: str) -> AuthorizationRequest: - """Deserialize a serialized AuthorizationRequest. - - Args: - serialized: the serialized AuthorizationRequest - - Returns: - the deserialized AuthorizationRequest - - """ - return self.loader(serialized) diff --git a/requests_oauth2client/client.py b/requests_oauth2client/client.py index d2b9607..1545452 100644 --- a/requests_oauth2client/client.py +++ b/requests_oauth2client/client.py @@ -3,7 +3,6 @@ from __future__ import annotations import warnings -from enum import Enum from typing import TYPE_CHECKING, Any, Callable, ClassVar, TypeVar import requests @@ -14,16 +13,15 @@ from .authorization_request import ( AuthorizationRequest, AuthorizationResponse, - CodeChallengeMethods, MissingIssuerParam, RequestUriParameterAuthorizationRequest, - ResponseTypes, ) from .backchannel_authentication import BackChannelAuthenticationResponse from .client_authentication import ClientSecretPost, PrivateKeyJwt, client_auth_factory from .device_authorization import DeviceAuthorizationResponse from .discovery import oidc_discovery_document_url from .dpop import DPoPKey, DPoPToken, InvalidDPoPAlg, MissingDPoPNonce, RepeatedDPoPNonce +from .enums import CodeChallengeMethods, Endpoints, GrantTypes, ResponseTypes, TokenType from .exceptions import ( AccessDenied, AuthorizationPending, @@ -50,7 +48,7 @@ UnsupportedTokenType, UseDPoPNonce, ) -from .tokens import BearerToken, IdToken, TokenResponse, TokenType +from .tokens import BearerToken, IdToken, TokenResponse from .utils import InvalidUri, validate_endpoint_uri, validate_issuer_uri if TYPE_CHECKING: @@ -169,24 +167,6 @@ def __init__(self, message: str, discovery_document: dict[str, Any]) -> None: self.discovery_document = discovery_document -class Endpoints(str, Enum): - """All standardised OAuth 2.0 and extensions endpoints. - - If an endpoint is not mentioned here, then its usage is not supported by OAuth2Client. - - """ - - TOKEN = "token_endpoint" - AUTHORIZATION = "authorization_endpoint" - BACKCHANNEL_AUTHENTICATION = "backchannel_authentication_endpoint" - DEVICE_AUTHORIZATION = "device_authorization_endpoint" - INTROSPECTION = "introspection_endpoint" - REVOCATION = "revocation_endpoint" - PUSHED_AUTHORIZATION_REQUEST = "pushed_authorization_request_endpoint" - JWKS = "jwks_uri" - USER_INFO = "userinfo_endpoint" - - class MissingEndpointUri(AttributeError): """Raised when a required endpoint uri is not known.""" @@ -194,19 +174,6 @@ def __init__(self, endpoint: str) -> None: super().__init__(f"No '{endpoint}' defined for this client.") -class GrantTypes(str, Enum): - """An enum of standardized `grant_type` values.""" - - CLIENT_CREDENTIALS = "client_credentials" - AUTHORIZATION_CODE = "authorization_code" - REFRESH_TOKEN = "refresh_token" - RESOURCE_OWNER_PASSWORD = "password" - TOKEN_EXCHANGE = "urn:ietf:params:oauth:grant-type:token-exchange" - JWT_BEARER = "urn:ietf:params:oauth:grant-type:jwt-bearer" - CLIENT_INITIATED_BACKCHANNEL_AUTHENTICATION = "urn:openid:params:grant-type:ciba" - DEVICE_CODE = "urn:ietf:params:oauth:grant-type:device_code" - - @frozen(init=False) class OAuth2Client: """An OAuth 2.x Client that can send requests to an OAuth 2.x Authorization Server. diff --git a/requests_oauth2client/deprecated.py b/requests_oauth2client/deprecated.py index 1f566fb..c31bbbe 100644 --- a/requests_oauth2client/deprecated.py +++ b/requests_oauth2client/deprecated.py @@ -3,6 +3,7 @@ """Mark a class as deprecated. https://stackoverflow.com/a/52087847 + """ from warnings import warn diff --git a/requests_oauth2client/dpop.py b/requests_oauth2client/dpop.py index 4e05d48..2950363 100644 --- a/requests_oauth2client/dpop.py +++ b/requests_oauth2client/dpop.py @@ -15,7 +15,8 @@ from requests import codes from typing_extensions import Self -from .tokens import AccessTokenTypes, BearerToken, IdToken, id_token_converter +from .enums import AccessTokenTypes +from .tokens import BearerToken, IdToken, id_token_converter from .utils import accepts_expires_in if TYPE_CHECKING: @@ -205,8 +206,8 @@ class DPoPKey: alg: str = field(on_setattr=setters.frozen) private_key: jwskate.Jwk = field(on_setattr=setters.frozen, repr=False) - jti_generator: Callable[[], str] = field(on_setattr=setters.frozen, repr=False) - iat_generator: Callable[[], int] = field(on_setattr=setters.frozen, repr=False) + jti_generator: Callable[[], str] = field(on_setattr=setters.frozen, repr=False, eq=False) + iat_generator: Callable[[], int] = field(on_setattr=setters.frozen, repr=False, eq=False) jwt_typ: str = field(on_setattr=setters.frozen, repr=False) dpop_token_class: type[DPoPToken] = field(on_setattr=setters.frozen, repr=False) as_nonce: str | None diff --git a/requests_oauth2client/enums.py b/requests_oauth2client/enums.py new file mode 100644 index 0000000..42431b3 --- /dev/null +++ b/requests_oauth2client/enums.py @@ -0,0 +1,83 @@ +"""Contains enumerations of standardised OAuth-related parameters and values. + +Most are taken from https://www.iana.org/assignments/oauth-parameters/oauth-parameters.xhtml . + +""" + +from __future__ import annotations + +from enum import Enum + + +class AccessTokenTypes(str, Enum): + """An enum of standardised `access_token` types.""" + + BEARER = "Bearer" + DPOP = "DPoP" + + +class CodeChallengeMethods(str, Enum): + """All standardised `code_challenge_method` values. + + You should always use `S256`. + + """ + + S256 = "S256" + plain = "plain" + + +class Endpoints(str, Enum): + """All standardised OAuth 2.0 and extensions endpoints. + + If an endpoint is not mentioned here, then its usage is not supported by OAuth2Client. + + """ + + TOKEN = "token_endpoint" + AUTHORIZATION = "authorization_endpoint" + BACKCHANNEL_AUTHENTICATION = "backchannel_authentication_endpoint" + DEVICE_AUTHORIZATION = "device_authorization_endpoint" + INTROSPECTION = "introspection_endpoint" + REVOCATION = "revocation_endpoint" + PUSHED_AUTHORIZATION_REQUEST = "pushed_authorization_request_endpoint" + JWKS = "jwks_uri" + USER_INFO = "userinfo_endpoint" + + +class GrantTypes(str, Enum): + """An enum of standardized `grant_type` values.""" + + CLIENT_CREDENTIALS = "client_credentials" + AUTHORIZATION_CODE = "authorization_code" + REFRESH_TOKEN = "refresh_token" + RESOURCE_OWNER_PASSWORD = "password" + TOKEN_EXCHANGE = "urn:ietf:params:oauth:grant-type:token-exchange" + JWT_BEARER = "urn:ietf:params:oauth:grant-type:jwt-bearer" + CLIENT_INITIATED_BACKCHANNEL_AUTHENTICATION = "urn:openid:params:grant-type:ciba" + DEVICE_CODE = "urn:ietf:params:oauth:grant-type:device_code" + + +class ResponseTypes(str, Enum): + """All standardised `response_type` values. + + Note that you should always use `code`. All other values are deprecated. + + """ + + CODE = "code" + NONE = "none" + TOKEN = "token" + IDTOKEN = "id_token" + CODE_IDTOKEN = "code id_token" + CODE_TOKEN = "code token" + CODE_IDTOKEN_TOKEN = "code id_token token" + IDTOKEN_TOKEN = "id_token token" + + +class TokenType(str, Enum): + """An enum of standardised `token_type` values.""" + + ACCESS_TOKEN = "access_token" + REFRESH_TOKEN = "refresh_token" + ID_TOKEN = "id_token" diff --git a/requests_oauth2client/flask/auth.py b/requests_oauth2client/flask/auth.py index 30acbb6..b181d96 100644 --- a/requests_oauth2client/flask/auth.py +++ b/requests_oauth2client/flask/auth.py @@ -6,8 +6,9 @@ from flask import session +from requests_oauth2client import BearerTokenSerializer from requests_oauth2client.auth import OAuth2ClientCredentialsAuth -from requests_oauth2client.tokens import BearerToken, BearerTokenSerializer +from requests_oauth2client.tokens import BearerToken class FlaskSessionAuthMixin: diff --git a/requests_oauth2client/serializers.py b/requests_oauth2client/serializers.py new file mode 100644 index 0000000..df9f812 --- /dev/null +++ b/requests_oauth2client/serializers.py @@ -0,0 +1,288 @@ +"""Contain utility classes for serializing/deserializing objects such as `BearerToken`, `AuthorizationRequest`, etc. + +Those objects are typically stored in session when used in Web Applications, so they must be easily (de)serializable +to/from strings. + +While those classes provide default implementation that should work well for most cases, you might have to customize, +subclass or replace those classes to support custom features from your application. + +""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from datetime import datetime, timezone +from typing import TYPE_CHECKING, Any, Callable, Generic, TypeVar, override + +import jwskate +from attr import asdict, field, frozen +from binapy import BinaPy +from jwskate import Jwk + +from . import RequestParameterAuthorizationRequest, RequestUriParameterAuthorizationRequest +from .authorization_request import AuthorizationRequest +from .dpop import DPoPKey, DPoPToken +from .tokens import BearerToken + +if TYPE_CHECKING: + from collections.abc import Mapping +T = TypeVar("T") + + +@frozen +class Serializer(Generic[T], ABC): + """Abstract class for (de)serializers.""" + + dumper: Callable[[T], str] = field(repr=False) + loader: Callable[[str, Callable[[Mapping[str, Any]], type[T]]], T] = field(repr=False) + + @abstractmethod + def get_class(self, args: Mapping[str, Any]) -> type[T]: + """Based on the parsed key/val mapping,return the appropriate class to use for deserialization. + + This must be implemented by subclasses. + + Parameters: + args: a key/value mapping, parsed from the serialized string, that will be used to + + """ + + def dumps(self, token: T) -> str: + """Serialize and compress a given token for easier storage. + + Args: + token: a BearerToken to serialize + + Returns: + the serialized token, as a str + + """ + return self.dumper(token) + + def loads(self, serialized: str) -> T: + """Deserialize a serialized token. + + Args: + serialized: the serialized token + + Returns: + the deserialized token + + """ + return self.loader(serialized, self.get_class) + + +@frozen +class BearerTokenSerializer(Serializer[BearerToken]): + """A helper class to serialize Token Response returned by an AS. + + This may be used to store BearerTokens in session or cookies. + + It needs a `dumper` and a `loader` functions that will respectively serialize and deserialize + BearerTokens. Default implementations are provided with use gzip and base64url on the serialized + JSON representation. + + Args: + dumper: a function to serialize a token into a `str`. + loader: a function to deserialize a serialized token representation. + + """ + + dumper: Callable[[BearerToken], str] = field(repr=False, factory=lambda: BearerTokenSerializer.default_dumper) + loader: Callable[[str, Callable[[Mapping[str, Any]], type[BearerToken]]], BearerToken] = field( + repr=False, factory=lambda: BearerTokenSerializer.default_loader + ) + + @override + def get_class(self, args: Mapping[str, Any]) -> type[BearerToken]: + token_type = args["token_type"] + return { + "bearer": BearerToken, + "dpop": DPoPToken, + }.get(token_type.lower(), BearerToken) + + @staticmethod + def default_dumper(token: BearerToken) -> str: + """Serialize a token as JSON, then compress with deflate, then encodes as base64url. + + Args: + token: the `BearerToken` to serialize + + Returns: + the serialized value + + """ + d = asdict(token) + d.update(**d.pop("kwargs", {})) + if isinstance(token, DPoPToken): + d["dpop_key"]["private_key"] = token.dpop_key.private_key.to_dict() + d["dpop_key"].pop("jti_generator", None) + d["dpop_key"].pop("iat_generator", None) + d["dpop_key"].pop("dpop_token_class", None) + return ( + BinaPy.serialize_to("json", {k: w for k, w in d.items() if w is not None}).to("deflate").to("b64u").ascii() + ) + + @staticmethod + def default_loader(serialized: str, get_class: Callable[[Mapping[str, Any]], type[BearerToken]]) -> BearerToken: + """Deserialize a BearerToken. + + This does the opposite operations than `default_dumper`. + + Args: + serialized: The serialized token. + get_class: A callable that takes the key/value mapping as input and returns the appropriate class to use. + + Returns: + a BearerToken + + """ + args = BinaPy(serialized).decode_from("b64u").decode_from("deflate").parse_from("json") + expires_at = args.get("expires_at") + if expires_at: + args["expires_at"] = datetime.fromtimestamp(expires_at, tz=timezone.utc) + + dpop_key = args.get("dpop_key") + if "dpop_key" in args: + dpop_key["private_key"] = jwskate.Jwk(dpop_key["private_key"]) + args["_dpop_key"] = DPoPKey(**args.pop("dpop_key")) + + token_class = get_class(args) + return token_class(**args) + + +@frozen +class AuthorizationRequestSerializer( + Serializer[AuthorizationRequest | RequestParameterAuthorizationRequest | RequestUriParameterAuthorizationRequest] +): + """(De)Serializer for `AuthorizationRequest` instances. + + You might need to store pending authorization requests in session, either server-side or client- side. This class is + here to help you do that. + + """ + + dumper: Callable[ + [AuthorizationRequest | RequestParameterAuthorizationRequest | RequestUriParameterAuthorizationRequest], str + ] = field(factory=lambda: AuthorizationRequestSerializer.default_dumper) + loader: Callable[ + [ + str, + Callable[ + [Mapping[str, Any]], + type[ + AuthorizationRequest + | RequestParameterAuthorizationRequest + | RequestUriParameterAuthorizationRequest + ], + ], + ], + AuthorizationRequest | RequestParameterAuthorizationRequest | RequestUriParameterAuthorizationRequest, + ] = field(factory=lambda: AuthorizationRequestSerializer.default_loader) + + @override + def get_class( + self, args: Mapping[str, Any] + ) -> type[AuthorizationRequest | RequestParameterAuthorizationRequest | RequestUriParameterAuthorizationRequest]: + if "request" in args: + return RequestParameterAuthorizationRequest + if "request_uri" in args: + return RequestUriParameterAuthorizationRequest + return AuthorizationRequest + + @staticmethod + def default_dumper( + azr: AuthorizationRequest | RequestParameterAuthorizationRequest | RequestUriParameterAuthorizationRequest, + ) -> str: + """Provide a default dumper implementation. + + Serialize an AuthorizationRequest as JSON, then compress with deflate, then encodes as + base64url. + + Args: + azr: the `AuthorizationRequest` to serialize + + Returns: + the serialized value + + """ + d = asdict(azr) + if azr.dpop_key: + d["dpop_key"]["private_key"] = azr.dpop_key.private_key.to_dict() + d.update(**d.pop("kwargs", {})) + return BinaPy.serialize_to("json", d).to("deflate").to("b64u").ascii() + + @staticmethod + def default_loader( + serialized: str, + get_class: Callable[ + [Mapping[str, Any]], + type[AuthorizationRequest | RequestParameterAuthorizationRequest | RequestUriParameterAuthorizationRequest], + ], + ) -> AuthorizationRequest | RequestParameterAuthorizationRequest | RequestUriParameterAuthorizationRequest: + """Provide a default deserializer implementation. + + This does the opposite operations than `default_dumper`. + + Args: + serialized: the serialized AuthorizationRequest + get_class: a callable to obtain the appropriate class for deserialization + + Returns: + an AuthorizationRequest + + """ + args = BinaPy(serialized).decode_from("b64u").decode_from("deflate").parse_from("json") + + if dpop_key := args.get("dpop_key"): + dpop_key["private_key"] = Jwk(dpop_key["private_key"]) + dpop_key.pop("jti_generator", None) + dpop_key.pop("iat_generator", None) + dpop_key.pop("dpop_token_class", None) + args["dpop_key"] = DPoPKey(**dpop_key) + + azr_class = get_class(args) + + return azr_class(**args) + + +@frozen +class DPoPKeySerializer(Serializer[DPoPKey]): + """A (de)serializer for `DPoPKey` instances.""" + + dumper: Callable[[DPoPKey], str] = field(factory=lambda: DPoPKeySerializer.default_dumper) + loader: Callable[[str, Callable[[Mapping[str, Any]], type[DPoPKey]]], DPoPKey] = field( + factory=lambda: DPoPKeySerializer.default_loader + ) + + @override + def get_class(self, args: Mapping[str, Any]) -> type[DPoPKey]: + return DPoPKey + + @staticmethod + def default_dumper(dpop_key: DPoPKey) -> str: + """Provide a default dumper implementation. + + This will not serialize jti_generator, iat_generator, and dpop_token_class! + + """ + d = dpop_key.private_key.to_dict() + d.pop("jti_generator", None) + d.pop("iat_generator", None) + d.pop("dpop_token_class", None) + return BinaPy.serialize_to("json", d).to("deflate").to("b64u").ascii() + + @staticmethod + def default_loader( + serialized: str, + get_class: Callable[[Mapping[str, Any]], type[DPoPKey]], + ) -> DPoPKey: + """Provide a default deserializer implementation. + + This will not deserialize iat_generator, iat_generator, and dpop_token_class! + + """ + args = BinaPy(serialized).decode_from("b64u").decode_from("deflate").parse_from("json") + args["private_key"] = Jwk(args["private_key"]) + cls = get_class(args) + return cls(**args) diff --git a/requests_oauth2client/tokens.py b/requests_oauth2client/tokens.py index 91ec4a2..5d40c92 100644 --- a/requests_oauth2client/tokens.py +++ b/requests_oauth2client/tokens.py @@ -3,7 +3,6 @@ from __future__ import annotations from datetime import datetime, timedelta, timezone -from enum import Enum from functools import cached_property from math import ceil from typing import TYPE_CHECKING, Any, Callable, ClassVar @@ -14,6 +13,7 @@ from binapy import BinaPy from typing_extensions import Self +from .enums import AccessTokenTypes from .utils import accepts_expires_in if TYPE_CHECKING: @@ -23,21 +23,6 @@ from .client import OAuth2Client -class TokenType(str, Enum): - """An enum of standardised `token_type` values.""" - - ACCESS_TOKEN = "access_token" - REFRESH_TOKEN = "refresh_token" - ID_TOKEN = "id_token" - - -class AccessTokenTypes(str, Enum): - """An enum of standardised `access_token` types.""" - - BEARER = "Bearer" - DPOP = "DPoP" - - class UnsupportedTokenType(ValueError): """Raised when an unsupported token_type is provided.""" @@ -603,87 +588,3 @@ def access_token_jwt(self) -> jwskate.SignedJwt: """ return jwskate.SignedJwt(self.access_token) - - -class BearerTokenSerializer: - """A helper class to serialize Token Response returned by an AS. - - This may be used to store BearerTokens in session or cookies. - - It needs a `dumper` and a `loader` functions that will respectively serialize and deserialize - BearerTokens. Default implementations are provided with use gzip and base64url on the serialized - JSON representation. - - Args: - dumper: a function to serialize a token into a `str`. - loader: a function to deserialize a serialized token representation. - - """ - - def __init__( - self, - dumper: Callable[[BearerToken], str] | None = None, - loader: Callable[[str], BearerToken] | None = None, - ) -> None: - self.dumper = dumper or self.default_dumper - self.loader = loader or self.default_loader - - @staticmethod - def default_dumper(token: BearerToken) -> str: - """Serialize a token as JSON, then compress with deflate, then encodes as base64url. - - Args: - token: the `BearerToken` to serialize - - Returns: - the serialized value - - """ - d = asdict(token) - d.update(**d.pop("kwargs", {})) - return ( - BinaPy.serialize_to("json", {k: w for k, w in d.items() if w is not None}).to("deflate").to("b64u").ascii() - ) - - def default_loader(self, serialized: str, token_class: type[BearerToken] = BearerToken) -> BearerToken: - """Deserialize a BearerToken. - - This does the opposite operations than `default_dumper`. - - Args: - serialized: the serialized token - token_class: class to use to deserialize the Token - - Returns: - a BearerToken - - """ - attrs = BinaPy(serialized).decode_from("b64u").decode_from("deflate").parse_from("json") - expires_at = attrs.get("expires_at") - if expires_at: - attrs["expires_at"] = datetime.fromtimestamp(expires_at, tz=timezone.utc) - return token_class(**attrs) - - def dumps(self, token: BearerToken) -> str: - """Serialize and compress a given token for easier storage. - - Args: - token: a BearerToken to serialize - - Returns: - the serialized token, as a str - - """ - return self.dumper(token) - - def loads(self, serialized: str) -> BearerToken: - """Deserialize a serialized token. - - Args: - serialized: the serialized token - - Returns: - the deserialized token - - """ - return self.loader(serialized) diff --git a/tests/test_deprecated_names.py b/tests/test_deprecated_names.py index dc790a3..77faf75 100644 --- a/tests/test_deprecated_names.py +++ b/tests/test_deprecated_names.py @@ -1,4 +1,5 @@ """An attempt to use old class names should generate DeprecationWarning.""" + import secrets import pytest diff --git a/tests/unit_tests/conftest.py b/tests/unit_tests/conftest.py index a685b28..81d65fc 100644 --- a/tests/unit_tests/conftest.py +++ b/tests/unit_tests/conftest.py @@ -17,6 +17,8 @@ ClientSecretBasic, ClientSecretJwt, ClientSecretPost, + DPoPKey, + DPoPToken, OAuth2Client, PrivateKeyJwt, PublicApp, @@ -50,6 +52,16 @@ def bearer_auth(access_token: str) -> BearerToken: return BearerToken(access_token) +@pytest.fixture(scope="session") +def dpop_key() -> DPoPKey: + return DPoPKey.generate() + + +@pytest.fixture(scope="session") +def dpop_token(access_token: str, dpop_key: DPoPKey) -> DPoPToken: + return DPoPToken(access_token=access_token, _dpop_key=dpop_key) + + @pytest.fixture(scope="session") def target_api() -> str: return "https://myapi.local/root/" diff --git a/tests/unit_tests/test_authorization_request.py b/tests/unit_tests/test_authorization_request.py index d80edc9..578dfb5 100644 --- a/tests/unit_tests/test_authorization_request.py +++ b/tests/unit_tests/test_authorization_request.py @@ -9,10 +9,8 @@ from requests_oauth2client import ( AuthorizationRequest, - AuthorizationRequestSerializer, AuthorizationResponse, AuthorizationResponseError, - DPoPKey, InvalidMaxAgeParam, MismatchingIssuer, MismatchingState, @@ -188,31 +186,6 @@ def test_missing_issuer( authorization_request.validate_callback(authorization_response_uri) -def test_authorization_request_serializer(authorization_request: AuthorizationRequest) -> None: - serializer = AuthorizationRequestSerializer() - serialized = serializer.dumps(authorization_request) - assert serializer.loads(serialized) == authorization_request - - -def test_authorization_request_serializer_with_dpop_key() -> None: - dpop_key = DPoPKey.generate() - authorization_request = AuthorizationRequest( - "https://as.local/authorize", - client_id="foo", - redirect_uri="http://localhost/local", - scope="openid", - dpop_key=dpop_key, - ) - - serializer = AuthorizationRequestSerializer() - - serialized = serializer.dumps(authorization_request) - deserialized_request = serializer.loads(serialized) - - assert isinstance(deserialized_request.dpop_key, DPoPKey) - assert deserialized_request.dpop_key.private_key == dpop_key.private_key - - def test_request_acr_values() -> None: # you may provide acr_values as a space separated list or as a real list assert AuthorizationRequest( diff --git a/tests/unit_tests/test_serializers.py b/tests/unit_tests/test_serializers.py new file mode 100644 index 0000000..40b697e --- /dev/null +++ b/tests/unit_tests/test_serializers.py @@ -0,0 +1,58 @@ +from datetime import datetime, timedelta, timezone + +import pytest +from freezegun.api import FrozenDateTimeFactory + +from requests_oauth2client import ( + AuthorizationRequest, + AuthorizationRequestSerializer, + BearerToken, + BearerTokenSerializer, + DPoPKey, + DPoPToken, +) + + +@pytest.mark.parametrize( + "token", + [ + BearerToken("access_token"), + # note that "expires_at" is calculated when the test is ran, so before `freezer` takes effect + BearerToken("access_token", expires_in=60), + BearerToken("access_token", expires_in=-60), + DPoPToken("access_token", _dpop_key=DPoPKey.generate()), + DPoPToken("access_token", expires_in=60, _dpop_key=DPoPKey.generate()), + DPoPToken("access_token", expires_in=60, _dpop_key=DPoPKey.generate(alg="RS256")), + ], +) +def test_token_serializer(token: BearerToken, freezer: FrozenDateTimeFactory) -> None: + freezer.move_to("2024-08-01") + serializer = BearerTokenSerializer() + candidate = serializer.dumps(token) + freezer.move_to(datetime.now(tz=timezone.utc) + timedelta(days=365)) + assert serializer.loads(candidate) == token + + +def test_authorization_request_serializer(authorization_request: AuthorizationRequest) -> None: + serializer = AuthorizationRequestSerializer() + serialized = serializer.dumps(authorization_request) + assert serializer.loads(serialized) == authorization_request + + +def test_authorization_request_serializer_with_dpop_key() -> None: + dpop_key = DPoPKey.generate() + authorization_request = AuthorizationRequest( + "https://as.local/authorize", + client_id="foo", + redirect_uri="http://localhost/local", + scope="openid", + dpop_key=dpop_key, + ) + + serializer = AuthorizationRequestSerializer() + + serialized = serializer.dumps(authorization_request) + deserialized_request = serializer.loads(serialized) + + assert isinstance(deserialized_request.dpop_key, DPoPKey) + assert deserialized_request.dpop_key.private_key == dpop_key.private_key diff --git a/tests/unit_tests/test_tokens.py b/tests/unit_tests/test_tokens.py index 8c04e35..0e1705e 100644 --- a/tests/unit_tests/test_tokens.py +++ b/tests/unit_tests/test_tokens.py @@ -1,9 +1,8 @@ -from datetime import datetime, timedelta, timezone +from datetime import datetime, timezone import jwskate import pytest from freezegun import freeze_time -from freezegun.api import FrozenDateTimeFactory from jwskate import ( ExpiredJwt, InvalidClaim, @@ -15,7 +14,7 @@ SignedJwt, ) -from requests_oauth2client import BearerToken, BearerTokenSerializer, IdToken +from requests_oauth2client import BearerToken, IdToken ID_TOKEN = ( "eyJhbGciOiJSUzI1NiIsImtpZCI6Im15X2tleSJ9.eyJhY3IiOiIyIiwiYW1yIjpbInB3ZCIsIm90cCJdLCJhdWQiOiJjbGllbnRfaWQiL" @@ -271,23 +270,6 @@ def test_id_token_attributes() -> None: assert good_id_token.auth_datetime == datetime(2024, 9, 5, 9, 41, 21, tzinfo=timezone.utc) -@pytest.mark.parametrize( - "token", - [ - BearerToken("access_token"), - # note that "expires_at" is calculated when the test is ran, so before `freezer` takes effect - BearerToken("access_token", expires_in=60), - BearerToken("access_token", expires_in=-60), - ], -) -def test_token_serializer(token: BearerToken, freezer: FrozenDateTimeFactory) -> None: - freezer.move_to("2024-08-01") - serializer = BearerTokenSerializer() - candidate = serializer.dumps(token) - freezer.move_to(datetime.now(tz=timezone.utc) + timedelta(days=365)) - assert serializer.loads(candidate) == token - - @freeze_time() def test_expires_in_as_str() -> None: assert BearerToken("access_token", expires_in=60) == BearerToken("access_token", expires_in="60") From 57ff4da2eaca4698dad0f4632e236853ab18b6cf Mon Sep 17 00:00:00 2001 From: Guillaume Pujol Date: Tue, 9 Sep 2025 11:19:24 +0200 Subject: [PATCH 2/5] refactor serializers and enums into their own submodules --- requests_oauth2client/serializers.py | 122 ++++++++++++++------------- tests/unit_tests/conftest.py | 34 ++++++-- tests/unit_tests/test_serializers.py | 35 +++++++- 3 files changed, 122 insertions(+), 69 deletions(-) diff --git a/requests_oauth2client/serializers.py b/requests_oauth2client/serializers.py index df9f812..ae92140 100644 --- a/requests_oauth2client/serializers.py +++ b/requests_oauth2client/serializers.py @@ -12,15 +12,17 @@ from abc import ABC, abstractmethod from datetime import datetime, timezone -from typing import TYPE_CHECKING, Any, Callable, Generic, TypeVar, override +from typing import TYPE_CHECKING, Any, Callable, ClassVar, Generic, TypeVar, override import jwskate from attr import asdict, field, frozen from binapy import BinaPy -from jwskate import Jwk -from . import RequestParameterAuthorizationRequest, RequestUriParameterAuthorizationRequest -from .authorization_request import AuthorizationRequest +from .authorization_request import ( + AuthorizationRequest, + RequestParameterAuthorizationRequest, + RequestUriParameterAuthorizationRequest, +) from .dpop import DPoPKey, DPoPToken from .tokens import BearerToken @@ -101,8 +103,8 @@ def get_class(self, args: Mapping[str, Any]) -> type[BearerToken]: "dpop": DPoPToken, }.get(token_type.lower(), BearerToken) - @staticmethod - def default_dumper(token: BearerToken) -> str: + @classmethod + def default_dumper(cls, token: BearerToken) -> str: """Serialize a token as JSON, then compress with deflate, then encodes as base64url. Args: @@ -123,8 +125,10 @@ def default_dumper(token: BearerToken) -> str: BinaPy.serialize_to("json", {k: w for k, w in d.items() if w is not None}).to("deflate").to("b64u").ascii() ) - @staticmethod - def default_loader(serialized: str, get_class: Callable[[Mapping[str, Any]], type[BearerToken]]) -> BearerToken: + @classmethod + def default_loader( + cls, serialized: str, get_class: Callable[[Mapping[str, Any]], type[BearerToken]] + ) -> BearerToken: """Deserialize a BearerToken. This does the opposite operations than `default_dumper`. @@ -151,6 +155,48 @@ def default_loader(serialized: str, get_class: Callable[[Mapping[str, Any]], typ return token_class(**args) +@frozen +class DPoPKeySerializer(Serializer[DPoPKey]): + """A (de)serializer for `DPoPKey` instances.""" + + dumper: Callable[[DPoPKey], str] = field(factory=lambda: DPoPKeySerializer.default_dumper) + loader: Callable[[str, Callable[[Mapping[str, Any]], type[DPoPKey]]], DPoPKey] = field( + factory=lambda: DPoPKeySerializer.default_loader + ) + + @override + def get_class(self, args: Mapping[str, Any]) -> type[DPoPKey]: + return DPoPKey + + @classmethod + def default_dumper(cls, dpop_key: DPoPKey) -> str: + """Provide a default dumper implementation. + + This will not serialize jti_generator, iat_generator, and dpop_token_class! + + """ + d = dpop_key.private_key.to_dict() + d.pop("jti_generator", None) + d.pop("iat_generator", None) + d.pop("dpop_token_class", None) + return BinaPy.serialize_to("json", d).to("deflate").to("b64u").ascii() + + @classmethod + def default_loader( + cls, + serialized: str, + get_class: Callable[[Mapping[str, Any]], type[DPoPKey]], + ) -> DPoPKey: + """Provide a default deserializer implementation. + + This will not deserialize iat_generator, iat_generator, and dpop_token_class! + + """ + private_key = BinaPy(serialized).decode_from("b64u").decode_from("deflate").parse_from("json") + dpop_class = get_class({}) + return dpop_class(private_key=private_key) + + @frozen class AuthorizationRequestSerializer( Serializer[AuthorizationRequest | RequestParameterAuthorizationRequest | RequestUriParameterAuthorizationRequest] @@ -180,6 +226,8 @@ class AuthorizationRequestSerializer( AuthorizationRequest | RequestParameterAuthorizationRequest | RequestUriParameterAuthorizationRequest, ] = field(factory=lambda: AuthorizationRequestSerializer.default_loader) + dpop_key_serializer: ClassVar[Serializer[DPoPKey]] = DPoPKeySerializer() + @override def get_class( self, args: Mapping[str, Any] @@ -190,8 +238,9 @@ def get_class( return RequestUriParameterAuthorizationRequest return AuthorizationRequest - @staticmethod + @classmethod def default_dumper( + cls, azr: AuthorizationRequest | RequestParameterAuthorizationRequest | RequestUriParameterAuthorizationRequest, ) -> str: """Provide a default dumper implementation. @@ -208,12 +257,13 @@ def default_dumper( """ d = asdict(azr) if azr.dpop_key: - d["dpop_key"]["private_key"] = azr.dpop_key.private_key.to_dict() + d["dpop_key"] = cls.dpop_key_serializer.dumps(azr.dpop_key) d.update(**d.pop("kwargs", {})) return BinaPy.serialize_to("json", d).to("deflate").to("b64u").ascii() - @staticmethod + @classmethod def default_loader( + cls, serialized: str, get_class: Callable[ [Mapping[str, Any]], @@ -234,55 +284,9 @@ def default_loader( """ args = BinaPy(serialized).decode_from("b64u").decode_from("deflate").parse_from("json") - if dpop_key := args.get("dpop_key"): - dpop_key["private_key"] = Jwk(dpop_key["private_key"]) - dpop_key.pop("jti_generator", None) - dpop_key.pop("iat_generator", None) - dpop_key.pop("dpop_token_class", None) - args["dpop_key"] = DPoPKey(**dpop_key) + if args["dpop_key"]: + args["dpop_key"] = cls.dpop_key_serializer.loads(args["dpop_key"]) azr_class = get_class(args) return azr_class(**args) - - -@frozen -class DPoPKeySerializer(Serializer[DPoPKey]): - """A (de)serializer for `DPoPKey` instances.""" - - dumper: Callable[[DPoPKey], str] = field(factory=lambda: DPoPKeySerializer.default_dumper) - loader: Callable[[str, Callable[[Mapping[str, Any]], type[DPoPKey]]], DPoPKey] = field( - factory=lambda: DPoPKeySerializer.default_loader - ) - - @override - def get_class(self, args: Mapping[str, Any]) -> type[DPoPKey]: - return DPoPKey - - @staticmethod - def default_dumper(dpop_key: DPoPKey) -> str: - """Provide a default dumper implementation. - - This will not serialize jti_generator, iat_generator, and dpop_token_class! - - """ - d = dpop_key.private_key.to_dict() - d.pop("jti_generator", None) - d.pop("iat_generator", None) - d.pop("dpop_token_class", None) - return BinaPy.serialize_to("json", d).to("deflate").to("b64u").ascii() - - @staticmethod - def default_loader( - serialized: str, - get_class: Callable[[Mapping[str, Any]], type[DPoPKey]], - ) -> DPoPKey: - """Provide a default deserializer implementation. - - This will not deserialize iat_generator, iat_generator, and dpop_token_class! - - """ - args = BinaPy(serialized).decode_from("b64u").decode_from("deflate").parse_from("json") - args["private_key"] = Jwk(args["private_key"]) - cls = get_class(args) - return cls(**args) diff --git a/tests/unit_tests/conftest.py b/tests/unit_tests/conftest.py index 81d65fc..4ac8218 100644 --- a/tests/unit_tests/conftest.py +++ b/tests/unit_tests/conftest.py @@ -18,10 +18,10 @@ ClientSecretJwt, ClientSecretPost, DPoPKey, - DPoPToken, OAuth2Client, PrivateKeyJwt, PublicApp, + RequestParameterAuthorizationRequest, ) if TYPE_CHECKING: @@ -52,14 +52,11 @@ def bearer_auth(access_token: str) -> BearerToken: return BearerToken(access_token) -@pytest.fixture(scope="session") -def dpop_key() -> DPoPKey: - return DPoPKey.generate() - - -@pytest.fixture(scope="session") -def dpop_token(access_token: str, dpop_key: DPoPKey) -> DPoPToken: - return DPoPToken(access_token=access_token, _dpop_key=dpop_key) +@pytest.fixture(scope="session", params=[None, "ES256"]) +def dpop_key(request: FixtureRequest) -> DPoPKey | None: + if request.param is None: + return None + return DPoPKey.generate(alg=request.param) @pytest.fixture(scope="session") @@ -391,6 +388,7 @@ def authorization_request( # noqa: C901 code_challenge_method: str, expected_issuer: str | None, auth_request_kwargs: dict[str, Any], + dpop_key: DPoPKey, ) -> AuthorizationRequest: authorization_response_iss_parameter_supported = bool(expected_issuer) @@ -405,6 +403,7 @@ def authorization_request( # noqa: C901 code_challenge_method=code_challenge_method, authorization_response_iss_parameter_supported=authorization_response_iss_parameter_supported, issuer=expected_issuer, + dpop_key=dpop_key, **auth_request_kwargs, ) @@ -416,6 +415,7 @@ def authorization_request( # noqa: C901 assert azr.redirect_uri == redirect_uri assert azr.issuer == expected_issuer assert azr.kwargs == auth_request_kwargs + assert azr.dpop_key == dpop_key args = dict(url.args) expected_args = dict( @@ -499,6 +499,9 @@ def authorization_request( # noqa: C901 assert generated_code_challenge == code_verifier assert azr.code_verifier == code_verifier + if dpop_key: + expected_args["dpop_jkt"] = dpop_key.dpop_jkt + assert args == expected_args return azr @@ -535,3 +538,16 @@ def authorization_response( assert auth_response.code_verifier == authorization_request.code_verifier return auth_response + + +@pytest.fixture(scope="session") +def request_parameter_signing_key() -> Jwk: + return Jwk.generate(alg="ES256") + + +@pytest.fixture +def request_parameter_authorization_request( + authorization_request: AuthorizationRequest, + request_parameter_signing_key: Jwk, +) -> RequestParameterAuthorizationRequest: + return authorization_request.sign(request_parameter_signing_key) diff --git a/tests/unit_tests/test_serializers.py b/tests/unit_tests/test_serializers.py index 40b697e..1b21e46 100644 --- a/tests/unit_tests/test_serializers.py +++ b/tests/unit_tests/test_serializers.py @@ -10,6 +10,8 @@ BearerTokenSerializer, DPoPKey, DPoPToken, + RequestParameterAuthorizationRequest, + RequestUriParameterAuthorizationRequest, ) @@ -33,11 +35,42 @@ def test_token_serializer(token: BearerToken, freezer: FrozenDateTimeFactory) -> assert serializer.loads(candidate) == token -def test_authorization_request_serializer(authorization_request: AuthorizationRequest) -> None: +def test_authorization_request_serializer( + authorization_request: AuthorizationRequest, + request_parameter_authorization_request: RequestParameterAuthorizationRequest, +) -> None: serializer = AuthorizationRequestSerializer() serialized = serializer.dumps(authorization_request) assert serializer.loads(serialized) == authorization_request + request_parameter_serialized = serializer.dumps(request_parameter_authorization_request) + assert serializer.loads(request_parameter_serialized) == request_parameter_authorization_request + + +@pytest.fixture( + scope="module", params=["this_is_a_request_uri", "urn:this:is:a:request_uri", "https://foo.bar/request_uri"] +) +def request_uri_authorization_request( + authorization_endpoint: str, client_id: str, request: pytest.FixtureRequest +) -> RequestUriParameterAuthorizationRequest: + request_uri = request.param + return RequestUriParameterAuthorizationRequest( + authorization_endpoint=authorization_endpoint, + client_id=client_id, + request_uri=request_uri, + custom_param="custom_value", + ) + + +def test_request_uri_authorization_request_serializer( + request_uri_authorization_request: RequestUriParameterAuthorizationRequest, +) -> None: + serializer = AuthorizationRequestSerializer() + serialized = serializer.dumps(request_uri_authorization_request) + deserialized = serializer.loads(serialized) + assert isinstance(deserialized, RequestUriParameterAuthorizationRequest) + assert deserialized == request_uri_authorization_request + def test_authorization_request_serializer_with_dpop_key() -> None: dpop_key = DPoPKey.generate() From 22396670b0911f4b0dda346ac5705ef3c4842003 Mon Sep 17 00:00:00 2001 From: Guillaume Pujol Date: Tue, 9 Sep 2025 11:29:27 +0200 Subject: [PATCH 3/5] import `override` from `typing_extensions` --- requests_oauth2client/serializers.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/requests_oauth2client/serializers.py b/requests_oauth2client/serializers.py index ae92140..5fd48b3 100644 --- a/requests_oauth2client/serializers.py +++ b/requests_oauth2client/serializers.py @@ -12,11 +12,12 @@ from abc import ABC, abstractmethod from datetime import datetime, timezone -from typing import TYPE_CHECKING, Any, Callable, ClassVar, Generic, TypeVar, override +from typing import TYPE_CHECKING, Any, Callable, ClassVar, Generic, TypeVar import jwskate from attr import asdict, field, frozen from binapy import BinaPy +from typing_extensions import override from .authorization_request import ( AuthorizationRequest, @@ -28,6 +29,8 @@ if TYPE_CHECKING: from collections.abc import Mapping + + T = TypeVar("T") From 3f92023b0ab495db4e79fd797c447cf798e697ca Mon Sep 17 00:00:00 2001 From: Guillaume Pujol Date: Wed, 10 Sep 2025 10:34:28 +0200 Subject: [PATCH 4/5] fixes for mkdocs and Python<3.10 --- mkdocs.yml | 6 +- poetry.lock | 234 ++++++++++++++++----------- pyproject.toml | 2 + requests_oauth2client/serializers.py | 18 +-- 4 files changed, 157 insertions(+), 103 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index 674b045..66b23c2 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -60,8 +60,8 @@ plugins: handlers: python: options: - #extensions: - #- griffe_fieldz: {include_inherited: true} + extensions: + - griffe_fieldz: {include_inherited: true} filters: - "!^_" - "^__init__" @@ -69,7 +69,7 @@ plugins: members_order: source show_root_heading: true show_submodules: true - import: + inventories: - https://requests.readthedocs.io/en/master/objects.inv - https://guillp.github.io/jwskate/objects.inv extra: diff --git a/poetry.lock b/poetry.lock index 06dfa84..06029f0 100644 --- a/poetry.lock +++ b/poetry.lock @@ -167,84 +167,101 @@ files = [ [[package]] name = "cffi" -version = "1.17.1" +version = "2.0.0" description = "Foreign Function Interface for Python calling C code." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main"] markers = "platform_python_implementation != \"PyPy\"" files = [ - {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, - {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, - {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, - {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, - {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, - {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, - {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, - {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, - {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, - {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, - {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, - {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, - {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, - {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, - {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, - {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, - {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, - {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, - {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, - {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, - {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, - {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, - {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, - {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, - {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, - {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, - {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, - {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, - {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, - {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, - {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, - {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, - {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, - {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, - {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, - {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, - {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, + {file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"}, + {file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb"}, + {file = "cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a"}, + {file = "cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739"}, + {file = "cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe"}, + {file = "cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743"}, + {file = "cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5"}, + {file = "cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5"}, + {file = "cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d"}, + {file = "cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d"}, + {file = "cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba"}, + {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94"}, + {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187"}, + {file = "cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18"}, + {file = "cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5"}, + {file = "cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6"}, + {file = "cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb"}, + {file = "cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26"}, + {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c"}, + {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b"}, + {file = "cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27"}, + {file = "cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75"}, + {file = "cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91"}, + {file = "cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5"}, + {file = "cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775"}, + {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205"}, + {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1"}, + {file = "cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f"}, + {file = "cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25"}, + {file = "cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad"}, + {file = "cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9"}, + {file = "cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592"}, + {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512"}, + {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4"}, + {file = "cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e"}, + {file = "cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6"}, + {file = "cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9"}, + {file = "cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"}, + {file = "cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322"}, + {file = "cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a"}, + {file = "cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9"}, + {file = "cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529"}, ] [package.dependencies] -pycparser = "*" +pycparser = {version = "*", markers = "implementation_name != \"PyPy\""} [[package]] name = "cfgv" @@ -571,6 +588,25 @@ typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} [package.extras] test = ["pytest (>=6)"] +[[package]] +name = "fieldz" +version = "0.1.2" +description = "Utilities for providing compatibility with many dataclass-like libraries" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "fieldz-0.1.2-py3-none-any.whl", hash = "sha256:e25884d2821a2d5638ef8d4d8bce5d1039359cfcb46d0f93df8cb1f7c2eb3a2e"}, + {file = "fieldz-0.1.2.tar.gz", hash = "sha256:0448ed5dacb13eaa49da0db786e87fae298fbd2652d26c510e5d7aea6b6bebf4"}, +] + +[package.dependencies] +typing-extensions = "*" + +[package.extras] +dev = ["hatch", "ipython", "mypy", "pdbpp", "pre-commit-uv", "rich", "ruff"] +test = ["annotated-types", "attrs", "dataclassy", "msgspec", "pydantic", "pytest", "pytest-cov"] + [[package]] name = "filelock" version = "3.19.1" @@ -659,29 +695,45 @@ dev = ["flake8", "markdown", "twine", "wheel"] [[package]] name = "griffe" -version = "1.13.0" +version = "1.14.0" description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "griffe-1.13.0-py3-none-any.whl", hash = "sha256:470fde5b735625ac0a36296cd194617f039e9e83e301fcbd493e2b58382d0559"}, - {file = "griffe-1.13.0.tar.gz", hash = "sha256:246ea436a5e78f7fbf5f24ca8a727bb4d2a4b442a2959052eea3d0bfe9a076e0"}, + {file = "griffe-1.14.0-py3-none-any.whl", hash = "sha256:0e9d52832cccf0f7188cfe585ba962d2674b241c01916d780925df34873bceb0"}, + {file = "griffe-1.14.0.tar.gz", hash = "sha256:9d2a15c1eca966d68e00517de5d69dd1bc5c9f2335ef6c1775362ba5b8651a13"}, ] [package.dependencies] colorama = ">=0.4" +[[package]] +name = "griffe-fieldz" +version = "0.3.0" +description = "Griffe extension adding support for data-class like things (pydantic, attrs, etc...)" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "griffe_fieldz-0.3.0-py3-none-any.whl", hash = "sha256:52e02fdcbdf6dea3c8c95756d1e0b30861569f871d19437fda702776fde4e64d"}, + {file = "griffe_fieldz-0.3.0.tar.gz", hash = "sha256:42e7707dac51d38e26fb7f3f7f51429da9b47e98060bfeb81a4287456d5b8a89"}, +] + +[package.dependencies] +fieldz = ">=0.1.0" +griffe = ">=1.0" + [[package]] name = "identify" -version = "2.6.13" +version = "2.6.14" description = "File identification library for Python" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "identify-2.6.13-py2.py3-none-any.whl", hash = "sha256:60381139b3ae39447482ecc406944190f690d4a2997f2584062089848361b33b"}, - {file = "identify-2.6.13.tar.gz", hash = "sha256:da8d6c828e773620e13bfa86ea601c5a5310ba4bcd65edf378198b56a1f9fb32"}, + {file = "identify-2.6.14-py2.py3-none-any.whl", hash = "sha256:11a073da82212c6646b1f39bb20d4483bfb9543bd5566fec60053c4bb309bf2e"}, + {file = "identify-2.6.14.tar.gz", hash = "sha256:663494103b4f717cb26921c52f8751363dc89db64364cd836a9bf1535f53cd6a"}, ] [package.extras] @@ -991,14 +1043,14 @@ cache = ["platformdirs"] [[package]] name = "mkdocs-material" -version = "9.6.18" +version = "9.6.19" description = "Documentation that simply works" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "mkdocs_material-9.6.18-py3-none-any.whl", hash = "sha256:dbc1e146a0ecce951a4d84f97b816a54936cdc9e1edd1667fc6868878ac06701"}, - {file = "mkdocs_material-9.6.18.tar.gz", hash = "sha256:a2eb253bcc8b66f8c6eaf8379c10ed6e9644090c2e2e9d0971c7722dc7211c05"}, + {file = "mkdocs_material-9.6.19-py3-none-any.whl", hash = "sha256:7492d2ac81952a467ca8a10cac915d6ea5c22876932f44b5a0f4f8e7d68ac06f"}, + {file = "mkdocs_material-9.6.19.tar.gz", hash = "sha256:80e7b3f9acabfee9b1f68bd12c26e59c865b3d5bbfb505fd1344e970db02c4aa"}, ] [package.dependencies] @@ -1017,7 +1069,7 @@ requests = ">=2.26,<3.0" [package.extras] git = ["mkdocs-git-committers-plugin-2 (>=1.1,<3)", "mkdocs-git-revision-date-localized-plugin (>=1.2.4,<2.0)"] -imaging = ["cairosvg (>=2.6,<3.0)", "pillow (>=10.2,<11.0)"] +imaging = ["cairosvg (>=2.6,<3.0)", "pillow (>=10.2,<12.0)"] recommended = ["mkdocs-minify-plugin (>=0.7,<1.0)", "mkdocs-redirects (>=1.2,<2.0)", "mkdocs-rss-plugin (>=1.6,<2.0)"] [[package]] @@ -1271,15 +1323,15 @@ virtualenv = ">=20.10.0" [[package]] name = "pycparser" -version = "2.22" +version = "2.23" description = "C parser in Python" optional = false python-versions = ">=3.8" groups = ["main"] -markers = "platform_python_implementation != \"PyPy\"" +markers = "platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\"" files = [ - {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, - {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, + {file = "pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934"}, + {file = "pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2"}, ] [[package]] @@ -1362,23 +1414,23 @@ dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests [[package]] name = "pytest-cov" -version = "6.2.1" +version = "7.0.0" description = "Pytest plugin for measuring coverage." optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5"}, - {file = "pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2"}, + {file = "pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861"}, + {file = "pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1"}, ] [package.dependencies] -coverage = {version = ">=7.5", extras = ["toml"]} +coverage = {version = ">=7.10.6", extras = ["toml"]} pluggy = ">=1.2" -pytest = ">=6.2.5" +pytest = ">=7" [package.extras] -testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] +testing = ["process-tests", "pytest-xdist", "virtualenv"] [[package]] name = "pytest-examples" @@ -1907,4 +1959,4 @@ test = [] [metadata] lock-version = "2.1" python-versions = ">=3.9" -content-hash = "019c2720bdb6aa36c0b055d536a4341daa2ecd5a2852fc342350f00ca2527225" +content-hash = "48821d4846a1e03f6168b2f8bb8c75bfc13b339588142cc2e592860b1a6c2b68" diff --git a/pyproject.toml b/pyproject.toml index 4468394..c3497f3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,11 +58,13 @@ types-requests = ">=2.25.10" types-cryptography = ">=3.3.15" virtualenv = ">=20.30.0" pytest-examples = ">=0.0.17" +griffe-fieldz = ">=0.3.0" [tool.poetry.extras] test = ["pytest", "pytest-cov"] doc = [ + "griffe-fieldz", "mdformat", "mkdocs", "mkdocs-autorefs", diff --git a/requests_oauth2client/serializers.py b/requests_oauth2client/serializers.py index 5fd48b3..f9d0c9a 100644 --- a/requests_oauth2client/serializers.py +++ b/requests_oauth2client/serializers.py @@ -12,7 +12,7 @@ from abc import ABC, abstractmethod from datetime import datetime, timezone -from typing import TYPE_CHECKING, Any, Callable, ClassVar, Generic, TypeVar +from typing import TYPE_CHECKING, Any, Callable, ClassVar, Generic, TypeVar, Union import jwskate from attr import asdict, field, frozen @@ -84,12 +84,10 @@ class BearerTokenSerializer(Serializer[BearerToken]): This may be used to store BearerTokens in session or cookies. It needs a `dumper` and a `loader` functions that will respectively serialize and deserialize - BearerTokens. Default implementations are provided with use gzip and base64url on the serialized - JSON representation. + BearerTokens (or subclasses). - Args: - dumper: a function to serialize a token into a `str`. - loader: a function to deserialize a serialized token representation. + Default implementation uses gzip and base64url on the serialized JSON representation. + It supports `BearerToken` and `DPoPToken` instances. """ @@ -202,12 +200,14 @@ def default_loader( @frozen class AuthorizationRequestSerializer( - Serializer[AuthorizationRequest | RequestParameterAuthorizationRequest | RequestUriParameterAuthorizationRequest] + Serializer[ + Union[AuthorizationRequest, RequestParameterAuthorizationRequest, RequestUriParameterAuthorizationRequest] + ] ): """(De)Serializer for `AuthorizationRequest` instances. - You might need to store pending authorization requests in session, either server-side or client- side. This class is - here to help you do that. + Default implementation supports `AuthorizationRequest`, `RequestParameterAuthorizationRequest`, and + `RequestUriParameterAuthorizationRequest`. """ From 975028ed1d1ac2298cae08088335ca65b8aff0a0 Mon Sep 17 00:00:00 2001 From: Guillaume Pujol Date: Wed, 10 Sep 2025 11:12:21 +0200 Subject: [PATCH 5/5] extra space --- requests_oauth2client/exceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requests_oauth2client/exceptions.py b/requests_oauth2client/exceptions.py index b7d4180..7fadbae 100644 --- a/requests_oauth2client/exceptions.py +++ b/requests_oauth2client/exceptions.py @@ -16,7 +16,7 @@ class OAuth2Error(Exception): Args: response: the HTTP response containing the error - client : the OAuth2Client used to send the request + client: the OAuth2Client used to send the request description: description of the error """