From 4cf69d61a2d1787ce7cfa8025cb6f34e76d0ab13 Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Thu, 13 Mar 2025 12:28:36 +0800 Subject: [PATCH] Add client basic auth and pkce --- fastapi_oauth20/clients/feishu.py | 31 ++- fastapi_oauth20/clients/gitee.py | 27 +-- fastapi_oauth20/clients/github.py | 37 ++- fastapi_oauth20/clients/google.py | 28 +-- fastapi_oauth20/clients/linuxdo.py | 51 +--- fastapi_oauth20/clients/oschina.py | 26 +-- fastapi_oauth20/errors.py | 41 +++- fastapi_oauth20/oauth20.py | 155 ++++++++---- pdm.lock | 363 ++++++++++++++++++++++++----- 9 files changed, 517 insertions(+), 242 deletions(-) diff --git a/fastapi_oauth20/clients/feishu.py b/fastapi_oauth20/clients/feishu.py index c6633ae..f63415d 100644 --- a/fastapi_oauth20/clients/feishu.py +++ b/fastapi_oauth20/clients/feishu.py @@ -2,36 +2,31 @@ # -*- coding: utf-8 -*- import httpx +from fastapi_oauth20.errors import GetUserInfoError from fastapi_oauth20.oauth20 import OAuth20Base -AUTHORIZE_ENDPOINT = 'https://passport.feishu.cn/suite/passport/oauth/authorize' -ACCESS_TOKEN_ENDPOINT = 'https://passport.feishu.cn/suite/passport/oauth/token' -REFRESH_TOKEN_ENDPOINT = AUTHORIZE_ENDPOINT -REVOKE_TOKEN_ENDPOINT = None -DEFAULT_SCOPES = ['contact:user.employee_id:readonly', 'contact:user.base:readonly', 'contact:user.email:readonly'] -PROFILE_ENDPOINT = 'https://passport.feishu.cn/suite/passport/oauth/userinfo' - class FeiShuOAuth20(OAuth20Base): def __init__(self, client_id: str, client_secret: str): super().__init__( client_id=client_id, client_secret=client_secret, - authorize_endpoint=AUTHORIZE_ENDPOINT, - access_token_endpoint=ACCESS_TOKEN_ENDPOINT, - refresh_token_endpoint=REFRESH_TOKEN_ENDPOINT, - revoke_token_endpoint=REVOKE_TOKEN_ENDPOINT, + authorize_endpoint='https://passport.feishu.cn/suite/passport/oauth/authorize', + access_token_endpoint='https://passport.feishu.cn/suite/passport/oauth/token', + refresh_token_endpoint='https://passport.feishu.cn/suite/passport/oauth/authorize', oauth_callback_route_name='feishu', - default_scopes=DEFAULT_SCOPES, + default_scopes=[ + 'contact:user.employee_id:readonly', + 'contact:user.base:readonly', + 'contact:user.email:readonly', + ], ) async def get_userinfo(self, access_token: str) -> dict: """Get user info from FeiShu""" headers = {'Authorization': f'Bearer {access_token}'} async with httpx.AsyncClient() as client: - response = await client.get(PROFILE_ENDPOINT, headers=headers) - await self.raise_httpx_oauth20_errors(response) - - res = response.json() - - return res + response = await client.get('https://passport.feishu.cn/suite/passport/oauth/userinfo', headers=headers) + self.raise_httpx_oauth20_errors(response) + result = self.get_json_result(response, err_class=GetUserInfoError) + return result diff --git a/fastapi_oauth20/clients/gitee.py b/fastapi_oauth20/clients/gitee.py index c6860a8..4df22d9 100644 --- a/fastapi_oauth20/clients/gitee.py +++ b/fastapi_oauth20/clients/gitee.py @@ -2,36 +2,27 @@ # -*- coding: utf-8 -*- import httpx +from fastapi_oauth20.errors import GetUserInfoError from fastapi_oauth20.oauth20 import OAuth20Base -AUTHORIZE_ENDPOINT = 'https://gitee.com/oauth/authorize' -ACCESS_TOKEN_ENDPOINT = 'https://gitee.com/oauth/token' -REFRESH_TOKEN_ENDPOINT = ACCESS_TOKEN_ENDPOINT -REVOKE_TOKEN_ENDPOINT = None -DEFAULT_SCOPES = ['user_info'] -PROFILE_ENDPOINT = 'https://gitee.com/api/v5/user' - class GiteeOAuth20(OAuth20Base): def __init__(self, client_id: str, client_secret: str): super().__init__( client_id=client_id, client_secret=client_secret, - authorize_endpoint=AUTHORIZE_ENDPOINT, - access_token_endpoint=ACCESS_TOKEN_ENDPOINT, - refresh_token_endpoint=REFRESH_TOKEN_ENDPOINT, - revoke_token_endpoint=REVOKE_TOKEN_ENDPOINT, + authorize_endpoint='https://gitee.com/oauth/authorize', + access_token_endpoint='https://gitee.com/oauth/token', + refresh_token_endpoint='https://gitee.com/oauth/token', oauth_callback_route_name='gitee', - default_scopes=DEFAULT_SCOPES, + default_scopes=['user_info'], ) async def get_userinfo(self, access_token: str) -> dict: """Get user info from Gitee""" headers = {'Authorization': f'Bearer {access_token}'} async with httpx.AsyncClient() as client: - response = await client.get(PROFILE_ENDPOINT, headers=headers) - await self.raise_httpx_oauth20_errors(response) - - res = response.json() - - return res + response = await client.get('https://gitee.com/api/v5/user', headers=headers) + self.raise_httpx_oauth20_errors(response) + result = self.get_json_result(response, err_class=GetUserInfoError) + return result diff --git a/fastapi_oauth20/clients/github.py b/fastapi_oauth20/clients/github.py index a41380e..819fb1f 100644 --- a/fastapi_oauth20/clients/github.py +++ b/fastapi_oauth20/clients/github.py @@ -2,46 +2,35 @@ # -*- coding: utf-8 -*- import httpx +from fastapi_oauth20.errors import GetUserInfoError from fastapi_oauth20.oauth20 import OAuth20Base -AUTHORIZE_ENDPOINT = 'https://github.com/login/oauth/authorize' -ACCESS_TOKEN_ENDPOINT = 'https://github.com/login/oauth/access_token' -REFRESH_TOKEN_ENDPOINT = None -REVOKE_TOKEN_ENDPOINT = None -DEFAULT_SCOPES = ['user', 'user:email'] -PROFILE_ENDPOINT = 'https://api.github.com/user' - class GitHubOAuth20(OAuth20Base): def __init__(self, client_id: str, client_secret: str): super().__init__( client_id=client_id, client_secret=client_secret, - authorize_endpoint=AUTHORIZE_ENDPOINT, - access_token_endpoint=ACCESS_TOKEN_ENDPOINT, - refresh_token_endpoint=REFRESH_TOKEN_ENDPOINT, - revoke_token_endpoint=REVOKE_TOKEN_ENDPOINT, + authorize_endpoint='https://github.com/login/oauth/authorize', + access_token_endpoint='https://github.com/login/oauth/access_token', oauth_callback_route_name='github', - default_scopes=DEFAULT_SCOPES, + default_scopes=['user', 'user:email'], ) async def get_userinfo(self, access_token: str) -> dict: """Get user info from GitHub""" headers = {'Authorization': f'Bearer {access_token}'} async with httpx.AsyncClient(headers=headers) as client: - response = await client.get(PROFILE_ENDPOINT) - await self.raise_httpx_oauth20_errors(response) - - res = response.json() + response = await client.get('https://api.github.com/user') + self.raise_httpx_oauth20_errors(response) + result = self.get_json_result(response, err_class=GetUserInfoError) - email = res.get('email') + email = result.get('email') if email is None: - response = await client.get(f'{PROFILE_ENDPOINT}/emails') - await self.raise_httpx_oauth20_errors(response) - - emails = response.json() - + response = await client.get('https://api.github.com/user/emails') + self.raise_httpx_oauth20_errors(response) + emails = self.get_json_result(response, err_class=GetUserInfoError) email = next((email['email'] for email in emails if email.get('primary')), emails[0]['email']) - res['email'] = email + result['email'] = email - return res + return result diff --git a/fastapi_oauth20/clients/google.py b/fastapi_oauth20/clients/google.py index e361302..b951352 100644 --- a/fastapi_oauth20/clients/google.py +++ b/fastapi_oauth20/clients/google.py @@ -2,36 +2,28 @@ # -*- coding: utf-8 -*- import httpx +from fastapi_oauth20.errors import GetUserInfoError from fastapi_oauth20.oauth20 import OAuth20Base -AUTHORIZE_ENDPOINT = 'https://accounts.google.com/o/oauth2/v2/auth' -ACCESS_TOKEN_ENDPOINT = 'https://oauth2.googleapis.com/token' -REFRESH_TOKEN_ENDPOINT = ACCESS_TOKEN_ENDPOINT -REVOKE_TOKEN_ENDPOINT = 'https://accounts.google.com/o/oauth2/revoke' -DEFAULT_SCOPES = ['email', 'openid', 'profile'] -PROFILE_ENDPOINT = 'https://www.googleapis.com/oauth2/v1/userinfo' - class GoogleOAuth20(OAuth20Base): def __init__(self, client_id: str, client_secret: str): super().__init__( client_id=client_id, client_secret=client_secret, - authorize_endpoint=AUTHORIZE_ENDPOINT, - access_token_endpoint=ACCESS_TOKEN_ENDPOINT, - refresh_token_endpoint=REFRESH_TOKEN_ENDPOINT, - revoke_token_endpoint=REVOKE_TOKEN_ENDPOINT, + authorize_endpoint='https://accounts.google.com/o/oauth2/v2/auth', + access_token_endpoint='https://oauth2.googleapis.com/token', + refresh_token_endpoint='https://oauth2.googleapis.com/token', + revoke_token_endpoint='https://accounts.google.com/o/oauth2/revoke', oauth_callback_route_name='google', - default_scopes=DEFAULT_SCOPES, + default_scopes=['email', 'openid', 'profile'], ) async def get_userinfo(self, access_token: str) -> dict: """Get user info from Google""" headers = {'Authorization': f'Bearer {access_token}'} async with httpx.AsyncClient() as client: - response = await client.get(PROFILE_ENDPOINT, headers=headers) - await self.raise_httpx_oauth20_errors(response) - - res = response.json() - - return res + response = await client.get('https://www.googleapis.com/oauth2/v1/userinfo', headers=headers) + self.raise_httpx_oauth20_errors(response) + result = self.get_json_result(response, err_class=GetUserInfoError) + return result diff --git a/fastapi_oauth20/clients/linuxdo.py b/fastapi_oauth20/clients/linuxdo.py index 73b0258..b63e25d 100644 --- a/fastapi_oauth20/clients/linuxdo.py +++ b/fastapi_oauth20/clients/linuxdo.py @@ -2,60 +2,27 @@ # -*- coding: utf-8 -*- import httpx +from fastapi_oauth20.errors import GetUserInfoError from fastapi_oauth20.oauth20 import OAuth20Base -AUTHORIZE_ENDPOINT = 'https://connect.linux.do/oauth2/authorize' -ACCESS_TOKEN_ENDPOINT = 'https://connect.linux.do/oauth2/token' -REFRESH_TOKEN_ENDPOINT = ACCESS_TOKEN_ENDPOINT -REVOKE_TOKEN_ENDPOINT = None -DEFAULT_SCOPES = None -PROFILE_ENDPOINT = 'https://connect.linux.do/api/user' - class LinuxDoOAuth20(OAuth20Base): def __init__(self, client_id: str, client_secret: str): super().__init__( client_id=client_id, client_secret=client_secret, - authorize_endpoint=AUTHORIZE_ENDPOINT, - access_token_endpoint=ACCESS_TOKEN_ENDPOINT, - refresh_token_endpoint=REFRESH_TOKEN_ENDPOINT, - revoke_token_endpoint=REVOKE_TOKEN_ENDPOINT, + authorize_endpoint='https://connect.linux.do/oauth2/authorize', + access_token_endpoint='https://connect.linux.do/oauth2/token', + refresh_token_endpoint='https://connect.linux.do/oauth2/token', oauth_callback_route_name='linuxdo', - default_scopes=DEFAULT_SCOPES, + token_endpoint_basic_auth=True, ) - async def get_access_token(self, code: str, redirect_uri: str, code_verifier: str | None = None) -> dict: - """Obtain the token based on the Linux do authorization method""" - data = { - 'code': code, - 'redirect_uri': redirect_uri, - 'grant_type': 'authorization_code', - } - - auth = httpx.BasicAuth(self.client_id, self.client_secret) - - if code_verifier: - data.update({'code_verifier': code_verifier}) - async with httpx.AsyncClient(auth=auth) as client: - response = await client.post( - self.access_token_endpoint, - data=data, - headers=self.request_headers, - ) - await self.raise_httpx_oauth20_errors(response) - - res = response.json() - - return res - async def get_userinfo(self, access_token: str) -> dict: """Get user info from Linux Do""" headers = {'Authorization': f'Bearer {access_token}'} async with httpx.AsyncClient() as client: - response = await client.get(PROFILE_ENDPOINT, headers=headers) - await self.raise_httpx_oauth20_errors(response) - - res = response.json() - - return res + response = await client.get('https://connect.linux.do/api/user', headers=headers) + self.raise_httpx_oauth20_errors(response) + result = self.get_json_result(response, err_class=GetUserInfoError) + return result diff --git a/fastapi_oauth20/clients/oschina.py b/fastapi_oauth20/clients/oschina.py index 4211249..90e84ef 100644 --- a/fastapi_oauth20/clients/oschina.py +++ b/fastapi_oauth20/clients/oschina.py @@ -2,36 +2,26 @@ # -*- coding: utf-8 -*- import httpx +from fastapi_oauth20.errors import GetUserInfoError from fastapi_oauth20.oauth20 import OAuth20Base -AUTHORIZE_ENDPOINT = 'https://www.oschina.net/action/oauth2/authorize' -ACCESS_TOKEN_ENDPOINT = 'https://www.oschina.net/action/openapi/token' -REFRESH_TOKEN_ENDPOINT = ACCESS_TOKEN_ENDPOINT -REVOKE_TOKEN_ENDPOINT = None -DEFAULT_SCOPES = None -PROFILE_ENDPOINT = 'https://www.oschina.net/action/openapi/user' - class OSChinaOAuth20(OAuth20Base): def __init__(self, client_id: str, client_secret: str): super().__init__( client_id=client_id, client_secret=client_secret, - authorize_endpoint=AUTHORIZE_ENDPOINT, - access_token_endpoint=ACCESS_TOKEN_ENDPOINT, - refresh_token_endpoint=REFRESH_TOKEN_ENDPOINT, - revoke_token_endpoint=REVOKE_TOKEN_ENDPOINT, + authorize_endpoint='https://www.oschina.net/action/oauth2/authorize', + access_token_endpoint='https://www.oschina.net/action/openapi/token', + refresh_token_endpoint='https://www.oschina.net/action/openapi/token', oauth_callback_route_name='oschina', - default_scopes=DEFAULT_SCOPES, ) async def get_userinfo(self, access_token: str) -> dict: """Get user info from OSChina""" headers = {'Authorization': f'Bearer {access_token}'} async with httpx.AsyncClient() as client: - response = await client.get(PROFILE_ENDPOINT, headers=headers) - await self.raise_httpx_oauth20_errors(response) - - res = response.json() - - return res + response = await client.get('https://www.oschina.net/action/openapi/user', headers=headers) + self.raise_httpx_oauth20_errors(response) + result = self.get_json_result(response, err_class=GetUserInfoError) + return result diff --git a/fastapi_oauth20/errors.py b/fastapi_oauth20/errors.py index 6abf87b..5407d3a 100644 --- a/fastapi_oauth20/errors.py +++ b/fastapi_oauth20/errors.py @@ -1,30 +1,57 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- +import httpx class FastAPIOAuth20BaseError(Exception): - pass + """The fastapi-oauth20 base error.""" + + msg: str + + def __init__(self, msg: str) -> None: + self.msg = msg + super().__init__(msg) + +class OAuth20RequestError(FastAPIOAuth20BaseError): + """OAuth2 httpx request error""" -class HTTPXOAuth20Error(FastAPIOAuth20BaseError): + def __init__(self, msg: str, response: httpx.Response | None = None) -> None: + self.response = response + super().__init__(msg) + + +class HTTPXOAuth20Error(OAuth20RequestError): """OAuth2 error for httpx raise for status""" pass -class RefreshTokenError(FastAPIOAuth20BaseError): - """Refresh token error if the refresh endpoint is missing""" +class AccessTokenError(OAuth20RequestError): + """Error raised when get access token fail.""" + + pass + + +class RefreshTokenError(OAuth20RequestError): + """Refresh token error when refresh token fail.""" + + pass + + +class RevokeTokenError(OAuth20RequestError): + """Revoke token error when revoke token fail.""" pass -class RevokeTokenError(FastAPIOAuth20BaseError): - """Revoke token error if the revoke endpoint is missing""" +class GetUserInfoError(OAuth20RequestError): + """Get user info error when get user info fail.""" pass -class RedirectURIError(FastAPIOAuth20BaseError): +class RedirectURIError(OAuth20RequestError): """Redirect URI set error""" pass diff --git a/fastapi_oauth20/oauth20.py b/fastapi_oauth20/oauth20.py index 29a6331..b74bf48 100644 --- a/fastapi_oauth20/oauth20.py +++ b/fastapi_oauth20/oauth20.py @@ -1,12 +1,20 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- import abc +import json -from urllib.parse import urlencode, urljoin +from typing import Literal, cast +from urllib.parse import urlencode import httpx -from fastapi_oauth20.errors import HTTPXOAuth20Error, RefreshTokenError, RevokeTokenError +from fastapi_oauth20.errors import ( + AccessTokenError, + HTTPXOAuth20Error, + OAuth20RequestError, + RefreshTokenError, + RevokeTokenError, +) class OAuth20Base: @@ -14,13 +22,30 @@ def __init__( self, client_id: str, client_secret: str, + *, authorize_endpoint: str, access_token_endpoint: str, refresh_token_endpoint: str | None = None, revoke_token_endpoint: str | None = None, oauth_callback_route_name: str = 'oauth20', default_scopes: list[str] | None = None, + token_endpoint_basic_auth: bool = False, + revoke_token_endpoint_basic_auth: bool = False, ): + """ + Base OAuth2 client. + + :param client_id: The client ID provided by the OAuth2 provider. + :param client_secret: The client secret provided by the OAuth2 provider. + :param authorize_endpoint: The authorization endpoint URL. + :param access_token_endpoint: The access token endpoint URL. + :param refresh_token_endpoint: The refresh token endpoint URL. + :param revoke_token_endpoint: The revoke token endpoint URL. + :param oauth_callback_route_name: + :param default_scopes: + :param token_endpoint_basic_auth: + :param revoke_token_endpoint_basic_auth: + """ self.client_id = client_id self.client_secret = client_secret self.authorize_endpoint = authorize_endpoint @@ -29,6 +54,8 @@ def __init__( self.revoke_token_endpoint = revoke_token_endpoint self.oauth_callback_route_name = oauth_callback_route_name self.default_scopes = default_scopes + self.token_endpoint_basic_auth = token_endpoint_basic_auth + self.revoke_token_endpoint_basic_auth = revoke_token_endpoint_basic_auth self.request_headers = { 'Accept': 'application/json', @@ -39,15 +66,19 @@ async def get_authorization_url( redirect_uri: str, state: str | None = None, scope: list[str] | None = None, - extras_params: dict = None, + code_challenge: str | None = None, + code_challenge_method: Literal['plain', 'S256'] | None = None, + **kwargs, ) -> str: """ Get authorization url for given. - :param redirect_uri: redirect uri - :param state: state to use - :param scope: scopes to use - :param extras_params: authorization url params + :param redirect_uri: redirected after authorization. + :param state: An opaque value used by the client to maintain state between the request and the callback. + :param scope: The scopes to be requested. + :param code_challenge: [PKCE](https://datatracker.ietf.org/doc/html/rfc7636) code challenge. + :param code_challenge_method: [PKCE](https://datatracker.ietf.org/doc/html/rfc7636) code challenge method. + :param kwargs: Additional arguments passed to the OAuth2 client. :return: """ params = { @@ -57,100 +88,136 @@ async def get_authorization_url( } if state is not None: - params.update({'state': state}) + params['state'] = state _scope = scope or self.default_scopes if _scope is not None: - params.update({'scope': ' '.join(_scope)}) + params['scope'] = ' '.join(_scope) + + if code_challenge is not None: + params['code_challenge'] = code_challenge - if extras_params is not None: - params = params.update(extras_params) + if code_challenge_method is not None: + params['code_challenge_method'] = code_challenge_method - authorization_url = urljoin(self.authorize_endpoint, '?' + urlencode(params)) + if kwargs: + params.update(kwargs) - return authorization_url + return f'{self.authorize_endpoint}?{urlencode(params)}' async def get_access_token(self, code: str, redirect_uri: str, code_verifier: str | None = None) -> dict: """ Get access token for given. - :param code: authorization code - :param redirect_uri: redirect uri - :param code_verifier: the code verifier for the PKCE request + :param code: The authorization code. + :param redirect_uri: redirect uri after authorization. + :param code_verifier: the code verifier for the [PKCE](https://datatracker.ietf.org/doc/html/rfc7636). :return: """ data = { 'code': code, - 'client_id': self.client_id, - 'client_secret': self.client_secret, 'redirect_uri': redirect_uri, 'grant_type': 'authorization_code', } + auth = None + if not self.token_endpoint_basic_auth: + data.update({'client_id': self.client_id, 'client_secret': self.client_secret}) + else: + auth = httpx.BasicAuth(self.client_id, self.client_secret) + if code_verifier: data.update({'code_verifier': code_verifier}) - async with httpx.AsyncClient() as client: + + async with httpx.AsyncClient(auth=auth) as client: response = await client.post( self.access_token_endpoint, data=data, headers=self.request_headers, ) - await self.raise_httpx_oauth20_errors(response) - - res = response.json() - - return res + self.raise_httpx_oauth20_errors(response) + result = self.get_json_result(response, err_class=AccessTokenError) + return result async def refresh_token(self, refresh_token: str) -> dict: - """Refresh the access token""" + """ + Get new access token by refresh token. + + :param refresh_token: The refresh token. + :return: + """ if self.refresh_token_endpoint is None: raise RefreshTokenError('The refresh token address is missing') + data = { - 'client_id': self.client_id, - 'client_secret': self.client_secret, 'refresh_token': refresh_token, 'grant_type': 'refresh_token', } - async with httpx.AsyncClient() as client: + + auth = None + if not self.token_endpoint_basic_auth: + data.update({'client_id': self.client_id, 'client_secret': self.client_secret}) + else: + auth = httpx.BasicAuth(self.client_id, self.client_secret) + + async with httpx.AsyncClient(auth=auth) as client: response = await client.post( self.refresh_token_endpoint, data=data, headers=self.request_headers, ) - await self.raise_httpx_oauth20_errors(response) - - res = response.json() - - return res + self.raise_httpx_oauth20_errors(response) + result = self.get_json_result(response, err_class=RefreshTokenError) + return result async def revoke_token(self, token: str, token_type_hint: str | None = None) -> None: - """Revoke the access token""" + """ + Revoke a token. + + :param token: A token or refresh token to revoke. + :param token_type_hint: Usually either `token` or `refresh_token`. + :return: + """ if self.revoke_token_endpoint is None: raise RevokeTokenError('The revoke token address is missing') - async with httpx.AsyncClient() as client: - data = {'token': token} + data = {'token': token} - if token_type_hint is not None: - data.update({'token_type_hint': token_type_hint}) + if token_type_hint is not None: + data.update({'token_type_hint': token_type_hint}) + async with httpx.AsyncClient() as client: response = await client.post( self.revoke_token_endpoint, data=data, headers=self.request_headers, ) - - await self.raise_httpx_oauth20_errors(response) + self.raise_httpx_oauth20_errors(response) @staticmethod - async def raise_httpx_oauth20_errors(response: httpx.Response) -> None: + def raise_httpx_oauth20_errors(response: httpx.Response) -> None: """Raise HTTPXOAuth20Error if the response is invalid""" try: response.raise_for_status() except httpx.HTTPStatusError as e: - raise HTTPXOAuth20Error(e) from e + raise HTTPXOAuth20Error(str(e), e.response) from e + except httpx.HTTPError as e: + raise HTTPXOAuth20Error(str(e)) from e + + @staticmethod + def get_json_result(response: httpx.Response, *, err_class: type[OAuth20RequestError]) -> dict: + """Get response json""" + try: + return cast(dict, response.json()) + except json.decoder.JSONDecodeError as e: + raise err_class('Result serialization failed.', response) from e @abc.abstractmethod async def get_userinfo(self, access_token: str) -> dict: - """Get user info""" - ... + """ + Get user info from the API provider + + :param access_token: The access token. + :return: + """ + raise NotImplementedError() diff --git a/pdm.lock b/pdm.lock index f78cd81..5d61b6c 100644 --- a/pdm.lock +++ b/pdm.lock @@ -2,7 +2,7 @@ # It is not intended for manual editing. [metadata] -groups = ["default", "lint"] +groups = ["default", "dev", "lint"] strategy = ["inherit_metadata"] lock_version = "4.5.0" content_hash = "sha256:2fa2e086cddb12e69177b0912ae3ab7df1ce946ea65d3c6e871932ab162bcc8f" @@ -11,31 +11,45 @@ content_hash = "sha256:2fa2e086cddb12e69177b0912ae3ab7df1ce946ea65d3c6e871932ab1 requires_python = ">=3.10" [[package]] -name = "anyio" -version = "4.2.0" +name = "annotated-types" +version = "0.7.0" requires_python = ">=3.8" +summary = "Reusable constraint types to use with typing.Annotated" +groups = ["dev"] +dependencies = [ + "typing-extensions>=4.0.0; python_version < \"3.9\"", +] +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + +[[package]] +name = "anyio" +version = "4.8.0" +requires_python = ">=3.9" summary = "High level compatibility layer for multiple asynchronous event loop implementations" -groups = ["default"] +groups = ["default", "dev"] dependencies = [ "exceptiongroup>=1.0.2; python_version < \"3.11\"", "idna>=2.8", "sniffio>=1.1", - "typing-extensions>=4.1; python_version < \"3.11\"", + "typing-extensions>=4.5; python_version < \"3.13\"", ] files = [ - {file = "anyio-4.2.0-py3-none-any.whl", hash = "sha256:745843b39e829e108e518c489b31dc757de7d2131d53fac32bd8df268227bfee"}, - {file = "anyio-4.2.0.tar.gz", hash = "sha256:e1875bb4b4e2de1669f4bc7869b6d3f54231cdced71605e6e64c9be77e3be50f"}, + {file = "anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a"}, + {file = "anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a"}, ] [[package]] name = "certifi" -version = "2024.2.2" +version = "2025.1.31" requires_python = ">=3.6" summary = "Python package for providing Mozilla's CA Bundle." groups = ["default"] files = [ - {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, - {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, + {file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"}, + {file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"}, ] [[package]] @@ -49,6 +63,18 @@ files = [ {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, ] +[[package]] +name = "colorama" +version = "0.4.6" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +summary = "Cross-platform colored terminal text." +groups = ["dev"] +marker = "sys_platform == \"win32\"" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + [[package]] name = "distlib" version = "0.3.9" @@ -61,14 +87,30 @@ files = [ [[package]] name = "exceptiongroup" -version = "1.2.0" +version = "1.2.2" requires_python = ">=3.7" summary = "Backport of PEP 654 (exception groups)" -groups = ["default"] +groups = ["default", "dev"] marker = "python_version < \"3.11\"" files = [ - {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, - {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, +] + +[[package]] +name = "fastapi" +version = "0.115.11" +requires_python = ">=3.8" +summary = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +groups = ["dev"] +dependencies = [ + "pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4", + "starlette<0.47.0,>=0.40.0", + "typing-extensions>=4.8.0", +] +files = [ + {file = "fastapi-0.115.11-py3-none-any.whl", hash = "sha256:32e1541b7b74602e4ef4a0260ecaf3aadf9d4f19590bba3e1bf2ac4666aa2c64"}, + {file = "fastapi-0.115.11.tar.gz", hash = "sha256:cc81f03f688678b92600a65a5e618b93592c65005db37157147204d8924bf94f"}, ] [[package]] @@ -98,7 +140,7 @@ files = [ [[package]] name = "httpcore" -version = "1.0.2" +version = "1.0.7" requires_python = ">=3.8" summary = "A minimal low-level HTTP client." groups = ["default"] @@ -107,8 +149,8 @@ dependencies = [ "h11<0.15,>=0.13", ] files = [ - {file = "httpcore-1.0.2-py3-none-any.whl", hash = "sha256:096cc05bca73b8e459a1fc3dcf585148f63e534eae4339559c9b8a8d6399acc7"}, - {file = "httpcore-1.0.2.tar.gz", hash = "sha256:9fc092e4799b26174648e54b74ed5f683132a464e95643b226e00c2ed2fa6535"}, + {file = "httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd"}, + {file = "httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c"}, ] [[package]] @@ -130,24 +172,35 @@ files = [ [[package]] name = "identify" -version = "2.6.8" +version = "2.6.9" requires_python = ">=3.9" summary = "File identification library for Python" groups = ["lint"] files = [ - {file = "identify-2.6.8-py2.py3-none-any.whl", hash = "sha256:83657f0f766a3c8d0eaea16d4ef42494b39b34629a4b3192a9d020d349b3e255"}, - {file = "identify-2.6.8.tar.gz", hash = "sha256:61491417ea2c0c5c670484fd8abbb34de34cdae1e5f39a73ee65e48e4bb663fc"}, + {file = "identify-2.6.9-py2.py3-none-any.whl", hash = "sha256:c98b4322da415a8e5a70ff6e51fbc2d2932c015532d77e9f8537b4ba7813b150"}, + {file = "identify-2.6.9.tar.gz", hash = "sha256:d40dfe3142a1421d8518e3d3985ef5ac42890683e32306ad614a29490abeb6bf"}, ] [[package]] name = "idna" -version = "3.6" -requires_python = ">=3.5" +version = "3.10" +requires_python = ">=3.6" summary = "Internationalized Domain Names in Applications (IDNA)" -groups = ["default"] +groups = ["default", "dev"] files = [ - {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, - {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +requires_python = ">=3.7" +summary = "brain-dead simple config-ini parsing" +groups = ["dev"] +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] [[package]] @@ -161,6 +214,17 @@ files = [ {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, ] +[[package]] +name = "packaging" +version = "24.2" +requires_python = ">=3.8" +summary = "Core utilities for Python packages" +groups = ["dev"] +files = [ + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, +] + [[package]] name = "platformdirs" version = "4.3.6" @@ -172,6 +236,17 @@ files = [ {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, ] +[[package]] +name = "pluggy" +version = "1.5.0" +requires_python = ">=3.8" +summary = "plugin and hook calling mechanisms for python" +groups = ["dev"] +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + [[package]] name = "pre-commit" version = "4.1.0" @@ -190,6 +265,132 @@ files = [ {file = "pre_commit-4.1.0.tar.gz", hash = "sha256:ae3f018575a588e30dfddfab9a05448bfbd6b73d78709617b5a2b853549716d4"}, ] +[[package]] +name = "pydantic" +version = "2.10.6" +requires_python = ">=3.8" +summary = "Data validation using Python type hints" +groups = ["dev"] +dependencies = [ + "annotated-types>=0.6.0", + "pydantic-core==2.27.2", + "typing-extensions>=4.12.2", +] +files = [ + {file = "pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584"}, + {file = "pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236"}, +] + +[[package]] +name = "pydantic-core" +version = "2.27.2" +requires_python = ">=3.8" +summary = "Core functionality for Pydantic validation and serialization" +groups = ["dev"] +dependencies = [ + "typing-extensions!=4.7.0,>=4.6.0", +] +files = [ + {file = "pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa"}, + {file = "pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236"}, + {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962"}, + {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9"}, + {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af"}, + {file = "pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4"}, + {file = "pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31"}, + {file = "pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc"}, + {file = "pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d"}, + {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b"}, + {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474"}, + {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6"}, + {file = "pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c"}, + {file = "pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc"}, + {file = "pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4"}, + {file = "pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0"}, + {file = "pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4"}, + {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3"}, + {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4"}, + {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57"}, + {file = "pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc"}, + {file = "pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9"}, + {file = "pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b"}, + {file = "pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b"}, + {file = "pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4"}, + {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27"}, + {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee"}, + {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1"}, + {file = "pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130"}, + {file = "pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee"}, + {file = "pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151"}, + {file = "pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39"}, +] + +[[package]] +name = "pytest" +version = "8.3.5" +requires_python = ">=3.8" +summary = "pytest: simple powerful testing with Python" +groups = ["dev"] +dependencies = [ + "colorama; sys_platform == \"win32\"", + "exceptiongroup>=1.0.0rc8; python_version < \"3.11\"", + "iniconfig", + "packaging", + "pluggy<2,>=1.5", + "tomli>=1; python_version < \"3.11\"", +] +files = [ + {file = "pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820"}, + {file = "pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845"}, +] + +[[package]] +name = "pytest-asyncio" +version = "0.25.3" +requires_python = ">=3.9" +summary = "Pytest support for asyncio" +groups = ["dev"] +dependencies = [ + "pytest<9,>=8.2", +] +files = [ + {file = "pytest_asyncio-0.25.3-py3-none-any.whl", hash = "sha256:9e89518e0f9bd08928f97a3482fdc4e244df17529460bc038291ccaf8f85c7c3"}, + {file = "pytest_asyncio-0.25.3.tar.gz", hash = "sha256:fc1da2cf9f125ada7e710b4ddad05518d4cee187ae9412e9ac9271003497f07a"}, +] + [[package]] name = "pyyaml" version = "6.0.2" @@ -238,52 +439,108 @@ files = [ [[package]] name = "ruff" -version = "0.9.9" +version = "0.9.10" requires_python = ">=3.7" summary = "An extremely fast Python linter and code formatter, written in Rust." groups = ["lint"] files = [ - {file = "ruff-0.9.9-py3-none-linux_armv6l.whl", hash = "sha256:628abb5ea10345e53dff55b167595a159d3e174d6720bf19761f5e467e68d367"}, - {file = "ruff-0.9.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b6cd1428e834b35d7493354723543b28cc11dc14d1ce19b685f6e68e07c05ec7"}, - {file = "ruff-0.9.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5ee162652869120ad260670706f3cd36cd3f32b0c651f02b6da142652c54941d"}, - {file = "ruff-0.9.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3aa0f6b75082c9be1ec5a1db78c6d4b02e2375c3068438241dc19c7c306cc61a"}, - {file = "ruff-0.9.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:584cc66e89fb5f80f84b05133dd677a17cdd86901d6479712c96597a3f28e7fe"}, - {file = "ruff-0.9.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abf3369325761a35aba75cd5c55ba1b5eb17d772f12ab168fbfac54be85cf18c"}, - {file = "ruff-0.9.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:3403a53a32a90ce929aa2f758542aca9234befa133e29f4933dcef28a24317be"}, - {file = "ruff-0.9.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:18454e7fa4e4d72cffe28a37cf6a73cb2594f81ec9f4eca31a0aaa9ccdfb1590"}, - {file = "ruff-0.9.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fadfe2c88724c9617339f62319ed40dcdadadf2888d5afb88bf3adee7b35bfb"}, - {file = "ruff-0.9.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6df104d08c442a1aabcfd254279b8cc1e2cbf41a605aa3e26610ba1ec4acf0b0"}, - {file = "ruff-0.9.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d7c62939daf5b2a15af48abbd23bea1efdd38c312d6e7c4cedf5a24e03207e17"}, - {file = "ruff-0.9.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:9494ba82a37a4b81b6a798076e4a3251c13243fc37967e998efe4cce58c8a8d1"}, - {file = "ruff-0.9.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:4efd7a96ed6d36ef011ae798bf794c5501a514be369296c672dab7921087fa57"}, - {file = "ruff-0.9.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ab90a7944c5a1296f3ecb08d1cbf8c2da34c7e68114b1271a431a3ad30cb660e"}, - {file = "ruff-0.9.9-py3-none-win32.whl", hash = "sha256:6b4c376d929c25ecd6d87e182a230fa4377b8e5125a4ff52d506ee8c087153c1"}, - {file = "ruff-0.9.9-py3-none-win_amd64.whl", hash = "sha256:837982ea24091d4c1700ddb2f63b7070e5baec508e43b01de013dc7eff974ff1"}, - {file = "ruff-0.9.9-py3-none-win_arm64.whl", hash = "sha256:3ac78f127517209fe6d96ab00f3ba97cafe38718b23b1db3e96d8b2d39e37ddf"}, - {file = "ruff-0.9.9.tar.gz", hash = "sha256:0062ed13f22173e85f8f7056f9a24016e692efeea8704d1a5e8011b8aa850933"}, + {file = "ruff-0.9.10-py3-none-linux_armv6l.whl", hash = "sha256:eb4d25532cfd9fe461acc83498361ec2e2252795b4f40b17e80692814329e42d"}, + {file = "ruff-0.9.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:188a6638dab1aa9bb6228a7302387b2c9954e455fb25d6b4470cb0641d16759d"}, + {file = "ruff-0.9.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5284dcac6b9dbc2fcb71fdfc26a217b2ca4ede6ccd57476f52a587451ebe450d"}, + {file = "ruff-0.9.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47678f39fa2a3da62724851107f438c8229a3470f533894b5568a39b40029c0c"}, + {file = "ruff-0.9.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:99713a6e2766b7a17147b309e8c915b32b07a25c9efd12ada79f217c9c778b3e"}, + {file = "ruff-0.9.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:524ee184d92f7c7304aa568e2db20f50c32d1d0caa235d8ddf10497566ea1a12"}, + {file = "ruff-0.9.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:df92aeac30af821f9acf819fc01b4afc3dfb829d2782884f8739fb52a8119a16"}, + {file = "ruff-0.9.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de42e4edc296f520bb84954eb992a07a0ec5a02fecb834498415908469854a52"}, + {file = "ruff-0.9.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d257f95b65806104b6b1ffca0ea53f4ef98454036df65b1eda3693534813ecd1"}, + {file = "ruff-0.9.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b60dec7201c0b10d6d11be00e8f2dbb6f40ef1828ee75ed739923799513db24c"}, + {file = "ruff-0.9.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d838b60007da7a39c046fcdd317293d10b845001f38bcb55ba766c3875b01e43"}, + {file = "ruff-0.9.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ccaf903108b899beb8e09a63ffae5869057ab649c1e9231c05ae354ebc62066c"}, + {file = "ruff-0.9.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f9567d135265d46e59d62dc60c0bfad10e9a6822e231f5b24032dba5a55be6b5"}, + {file = "ruff-0.9.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5f202f0d93738c28a89f8ed9eaba01b7be339e5d8d642c994347eaa81c6d75b8"}, + {file = "ruff-0.9.10-py3-none-win32.whl", hash = "sha256:bfb834e87c916521ce46b1788fbb8484966e5113c02df216680102e9eb960029"}, + {file = "ruff-0.9.10-py3-none-win_amd64.whl", hash = "sha256:f2160eeef3031bf4b17df74e307d4c5fb689a6f3a26a2de3f7ef4044e3c484f1"}, + {file = "ruff-0.9.10-py3-none-win_arm64.whl", hash = "sha256:5fd804c0327a5e5ea26615550e706942f348b197d5475ff34c19733aee4b2e69"}, + {file = "ruff-0.9.10.tar.gz", hash = "sha256:9bacb735d7bada9cfb0f2c227d3658fc443d90a727b47f206fb33f52f3c0eac7"}, ] [[package]] name = "sniffio" -version = "1.3.0" +version = "1.3.1" requires_python = ">=3.7" summary = "Sniff out which async library your code is running under" -groups = ["default"] +groups = ["default", "dev"] files = [ - {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, - {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + +[[package]] +name = "starlette" +version = "0.46.1" +requires_python = ">=3.9" +summary = "The little ASGI library that shines." +groups = ["dev"] +dependencies = [ + "anyio<5,>=3.6.2", + "typing-extensions>=3.10.0; python_version < \"3.10\"", +] +files = [ + {file = "starlette-0.46.1-py3-none-any.whl", hash = "sha256:77c74ed9d2720138b25875133f3a2dae6d854af2ec37dceb56aef370c1d8a227"}, + {file = "starlette-0.46.1.tar.gz", hash = "sha256:3c88d58ee4bd1bb807c0d1acb381838afc7752f9ddaec81bbe4383611d833230"}, +] + +[[package]] +name = "tomli" +version = "2.2.1" +requires_python = ">=3.8" +summary = "A lil' TOML parser" +groups = ["dev"] +marker = "python_version < \"3.11\"" +files = [ + {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, + {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, + {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, + {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, + {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, + {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, + {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, + {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, + {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, ] [[package]] name = "typing-extensions" -version = "4.9.0" +version = "4.12.2" requires_python = ">=3.8" summary = "Backported and Experimental Type Hints for Python 3.8+" -groups = ["default"] -marker = "python_version < \"3.11\"" +groups = ["default", "dev"] files = [ - {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"}, - {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] [[package]]