From 1e6c1e87ee5abb800cd058bdb4f0c16c5e2d4ac6 Mon Sep 17 00:00:00 2001 From: avantol Date: Wed, 30 Sep 2026 14:42:51 -0500 Subject: [PATCH 01/14] fix(security): address various security findings and bugs --- .dockerignore | 49 + .secrets.baseline | 6 +- dockerrun.bash | 5 + fence/auth.py | 16 - fence/blueprints/data/blueprint.py | 44 + fence/blueprints/data/bulk_helpers.py | 53 +- fence/blueprints/login/base.py | 105 +- fence/blueprints/oauth2.py | 2 +- fence/blueprints/register.py | 11 +- fence/blueprints/user.py | 2 +- fence/jwt/blacklist.py | 41 +- fence/oidc/grants/authorization_code_grant.py | 155 -- fence/resources/ga4gh/passports.py | 73 +- fence/sync/sync_users.py | 39 +- fence/utils.py | 13 +- poetry.lock | 2459 +++++++++-------- pyproject.toml | 2 +- tests/conftest.py | 38 +- tests/data/test_bulk_authz.py | 95 + .../data/test_upload_existing_record_authz.py | 170 ++ tests/dbgap_sync/test_download_dir.py | 125 + tests/ga4gh/test_passport_account_state.py | 103 + tests/ga4gh/test_passport_cache_isolation.py | 174 ++ tests/ga4gh/test_passport_issuer_allowlist.py | 136 + tests/jwt/test_blacklist.py | 63 +- tests/login/test_google_login.py | 8 +- tests/login/test_login_redirect.py | 24 + tests/login/test_oauth_state.py | 134 + tests/test_metrics.py | 13 +- 29 files changed, 2806 insertions(+), 1352 deletions(-) create mode 100644 .dockerignore delete mode 100644 fence/oidc/grants/authorization_code_grant.py create mode 100644 tests/data/test_bulk_authz.py create mode 100644 tests/data/test_upload_existing_record_authz.py create mode 100644 tests/dbgap_sync/test_download_dir.py create mode 100644 tests/ga4gh/test_passport_account_state.py create mode 100644 tests/ga4gh/test_passport_cache_isolation.py create mode 100644 tests/ga4gh/test_passport_issuer_allowlist.py create mode 100644 tests/login/test_oauth_state.py diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..352583cbe --- /dev/null +++ b/.dockerignore @@ -0,0 +1,49 @@ +# Signing keys. `keys/` is gitignored, so a clean checkout never has it, but a +# developer's working tree does and `COPY . /fence` would bake it into the image. +keys/ +*.pem +*.key +jwt-keys.tar + +# Deployment secrets and local config +fence-config.yaml +*config*.local.yaml +.env +.netrc + +# Local virtualenvs and caches, which would shadow the image's /venv +.venv/ +venv/ +ENV/ +__pycache__/ +*.py[cod] +.mypy_cache/ +.pytest_cache/ +.cache/ +.coverage +.coverage.* +htmlcov/ +coverage.xml +.tox/ +.hypothesis/ + +# Build artifacts +build/ +dist/ +*.egg-info/ +.eggs/ + +# Editor, OS, and agent scratch +.idea/ +.vscode/ +.DS_Store +.claude/ +.serena/ + +# Compose files and docs, not needed at runtime +docker-compose*.yml +*.md +!README.md + +# Note: `.git` is deliberately NOT ignored - the builder stage runs +# `git rev-parse HEAD` and `git describe` to generate version_data.py. diff --git a/.secrets.baseline b/.secrets.baseline index a506bd690..47b3d0863 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -214,14 +214,14 @@ "filename": "tests/conftest.py", "hashed_secret": "1348b145fa1a555461c1b790a2f66614781091e9", "is_verified": false, - "line_number": 1628 + "line_number": 1654 }, { "type": "Base64 High Entropy String", "filename": "tests/conftest.py", "hashed_secret": "227dea087477346785aefd575f91dd13ab86c108", "is_verified": false, - "line_number": 1652 + "line_number": 1678 } ], "tests/credentials/google/test_credentials.py": [ @@ -279,5 +279,5 @@ } ] }, - "generated_at": "2026-06-23T22:51:27Z" + "generated_at": "2026-09-30T19:42:34Z" } diff --git a/dockerrun.bash b/dockerrun.bash index 59ebbd683..2b408e68e 100755 --- a/dockerrun.bash +++ b/dockerrun.bash @@ -8,6 +8,11 @@ if [ -f /fence/jwt-keys.tar ]; then cd /fence tar xvf jwt-keys.tar if [ -d jwt-keys ]; then + # Replace keys/ rather than merging into it. Anything already there came + # from the image build rather than from the mounted secret, and must not + # survive alongside - fence loads every keypair it finds and publishes + # each one in the JWKS. + rm -rf keys mkdir -p keys mv jwt-keys/* keys/ fi diff --git a/fence/auth.py b/fence/auth.py index 42b0c4957..bd6985901 100644 --- a/fence/auth.py +++ b/fence/auth.py @@ -356,22 +356,6 @@ def logout(next_url, force_era_global_logout=False): return redirect_response -def check_scope(scope): - def wrapper(f): - @wraps(f) - def check_scope_and_call(*args, **kwargs): - if "_all" in flask.g.scopes or scope in flask.g.scopes: - return f(*args, **kwargs) - else: - raise Unauthorized( - "Requested scope {} can't access this endpoint".format(scope) - ) - - return check_scope_and_call - - return wrapper - - def login_required(scope=None): """ Create decorator to require a user session diff --git a/fence/blueprints/data/blueprint.py b/fence/blueprints/data/blueprint.py index 4f179f65e..ff2ce18f1 100755 --- a/fence/blueprints/data/blueprint.py +++ b/fence/blueprints/data/blueprint.py @@ -177,6 +177,14 @@ def upload_data_file(): uploader=uploader, guid=guid, ) + + if guid: + # A supplied GUID can resolve to an existing record instead of creating one + # (See BlankIndex.index_document code), and since + # the signed URL below writes to that record's storage key, we need to + # not just auth the authz provided by the user, but check on the existing record. + _authorize_upload_to_existing_record(blank_index.index_document) + default_expires_in = flask.current_app.config.get("MAX_PRESIGNED_URL_TTL", 3600) expires_in = get_valid_expiration( @@ -202,6 +210,42 @@ def upload_data_file(): return flask.jsonify(response), 201 +def _authorize_upload_to_existing_record(index_document: dict) -> None: + """ + Confirm the caller may write to an indexd record that already exists. + + Args: + index_document (dict): the indexd record the supplied GUID resolved to + + Raises: + Forbidden: if the caller may not write to this record + """ + record_authz = index_document.get("authz") + if record_authz: + authorized = flask.current_app.arborist.auth_request( + jwt=get_jwt(), + service="fence", + methods=["write-storage"], + resources=record_authz, + ) + if not authorized: + logger.error( + "Auth error on upload to existing record. User must have " + "'write-storage' access on {}.".format(record_authz) + ) + raise Forbidden( + "You do not have write-storage access to this record's authz resources." + ) + return + + uploader = index_document.get("uploader") + if not uploader or uploader != current_token["context"]["user"]["name"]: + raise Forbidden( + "You cannot upload to this record because it has no authz resources and " + "the uploader field indicates it does not belong to you." + ) + + @blueprint.route("/multipart/init", methods=["POST"]) @require_auth_header(scope={"data"}) @login_required({"data"}) diff --git a/fence/blueprints/data/bulk_helpers.py b/fence/blueprints/data/bulk_helpers.py index 31781fe1e..eb5877b57 100644 --- a/fence/blueprints/data/bulk_helpers.py +++ b/fence/blueprints/data/bulk_helpers.py @@ -74,21 +74,62 @@ def collect_authz_needing_check(guids, index_document, auth_roles): return authz_needing_check +def _authz_key_resources(authz_key): + """Return the list of resource paths an authz key stands for.""" + return list(authz_key) if isinstance(authz_key, tuple) else [authz_key] + + +def _is_authorized_for_read_storage(auth_mapping, authz_key, jwt=None, user_id=None): + """ + Determine whether the caller may read-storage every resource in an authz key. + + Arborist's ``auth_request`` over the full resource list is the decision, matching + the single-object path: a record carrying several resources requires access to + all of them. The auth mapping only screens out keys with no matching resource at + all, so arborist is not called for keys that cannot pass. + + Args: + auth_mapping: Arborist authorization mapping for a user or token. + authz_key: Normalized authz key from indexd metadata. + jwt: Bearer token to authorize, or None when authorizing by user_id or + anonymously. + user_id: Username to authorize, for passport-based access. + + Returns: + True if arborist authorizes fence read-storage on every resource in the key. + """ + if not _auth_mapping_has_read_storage(auth_mapping, authz_key): + return False + + return bool( + flask.current_app.arborist.auth_request( + jwt=jwt, + user_id=user_id, + service="fence", + methods="read-storage", + resources=_authz_key_resources(authz_key), + ) + ) + + def _auth_mapping_has_read_storage(auth_mapping, authz_key): """Determine whether the Arborist auth mapping grants read-storage for an authz key. + This is a pre-filter, not an authorization decision: it passes when any one + resource in the key matches. + Args: auth_mapping: Arborist authorization mapping for a user or token. authz_key: Normalized authz key from indexd metadata. Returns: - True if the auth_mapping contains a fence read-storage permission for the key. + True if the auth_mapping contains a fence read-storage permission for any + resource in the key. """ if auth_mapping is None: return False - authz_list = list(authz_key) if isinstance(authz_key, tuple) else [authz_key] - for resource in authz_list: + for resource in _authz_key_resources(authz_key): methods_list = auth_mapping.get(resource) if not methods_list: continue @@ -133,7 +174,9 @@ def build_authz_to_authorized_username(authz_needing_check, users_from_passports for authz_key in authz_needing_check: if authz_key in authz_to_authorized_username: continue - if _auth_mapping_has_read_storage(auth_mapping, authz_key): + if _is_authorized_for_read_storage( + auth_mapping, authz_key, user_id=username + ): authz_to_authorized_username[authz_key] = username else: try: @@ -148,7 +191,7 @@ def build_authz_to_authorized_username(authz_needing_check, users_from_passports auth_mapping = {} for authz_key in authz_needing_check: - if _auth_mapping_has_read_storage(auth_mapping, authz_key): + if _is_authorized_for_read_storage(auth_mapping, authz_key, jwt=token): authz_to_authorized_username[authz_key] = None return authz_to_authorized_username diff --git a/fence/blueprints/login/base.py b/fence/blueprints/login/base.py index 81d5bf354..0fc30fe3d 100644 --- a/fence/blueprints/login/base.py +++ b/fence/blueprints/login/base.py @@ -1,6 +1,7 @@ import time import base64 import json +import secrets from urllib.parse import urlparse, urlencode, parse_qsl import jwt @@ -50,22 +51,13 @@ def get(self): current_url += f"?{flask.request.query_string.decode('utf-8')}" flask.session["post_registration_redirect"] = current_url - mock_login = ( - config["OPENID_CONNECT"].get(self.idp_name.lower(), {}).get("mock", False) - ) - - # to support older cfgs, new cfgs should use the `mock` field in OPENID_CONNECT - legacy_mock_login = config.get( - "MOCK_{}_AUTH".format(self.idp_name.upper()), False - ) - mock_default_user = ( config["OPENID_CONNECT"] .get(self.idp_name.lower(), {}) .get("mock_default_user", "test@example.com") ) - if mock_login or legacy_mock_login: + if _mock_login_enabled(self.idp_name): # prefer dev cookie for mocked username, fallback on configuration username = flask.request.cookies.get( config.get("DEV_LOGIN_COOKIE_NAME"), mock_default_user @@ -75,7 +67,9 @@ def get(self): prepare_login_log(self.idp_name) return resp - return flask.redirect(self.client.get_auth_url()) + state = secrets.token_urlsafe(32) + flask.session[state_session_key(self.idp_name)] = state + return flask.redirect(_with_state_param(self.client.get_auth_url(), state)) class DefaultOAuth2Callback(Resource): @@ -131,6 +125,8 @@ def __init__( ]["is_authz_groups_sync_enabled"] def get(self): + self._validate_state() + # Check if user granted access if flask.request.args.get("error"): @@ -211,6 +207,34 @@ def get(self): return resp + def _validate_state(self) -> None: + """ + Confirm this callback belongs to a login that this browser started. + + Runs before the error branch and before the code is exchanged, so neither is + reachable by a cross-site request. The state is single-use: it is popped + whether or not it matches. + + Raises: + Unauthorized: if no state was issued for this IdP in this session, or the + state the IdP echoed back does not match the one issued + """ + if _mock_login_enabled(self.idp_name): + return + + expected_state = flask.session.pop(state_session_key(self.idp_name), None) + received_state = flask.request.args.get("state") + if ( + not expected_state + or not received_state + or not secrets.compare_digest(expected_state, received_state) + ): + logger.warning( + f"Rejecting {self.idp_name} login callback: the state parameter was " + "missing or did not match the one issued for this session." + ) + raise Unauthorized("Login state is missing or invalid; please log in again") + def extract_exp(self, refresh_token): """ Extract the expiration time (`exp`) from a refresh token. @@ -309,6 +333,23 @@ def post_login(self, user=None, token_result=None, **kwargs): ), f"Could not revoke user {username} policy mfa_policy" +def state_session_key(idp_name: str) -> str: + """ + Session key holding the OAuth state issued for one IdP's login. + + Scoped per IdP, and kept distinct from ``state`` (owned by the fence-as-IdP flow) + and ``google_link_state`` (owned by Google account linking), because one session + can have more than one of those flows in progress at once. + + Args: + idp_name (str): name for the identity provider + + Returns: + str: the session key + """ + return "oauth2_state_{}".format(idp_name.lower()) + + def prepare_login_log(idp_name): x_forwarded_headers = [ f"{header}:{value}" for header, value in flask.request.headers if "X-" in header @@ -325,6 +366,48 @@ def prepare_login_log(idp_name): } +def _mock_login_enabled(idp_name: str) -> bool: + """ + Whether an IdP is configured to skip the real authorization code flow. + + Args: + idp_name (str): name for the identity provider + + Returns: + bool: True if either the per-IdP ``mock`` field or the legacy + ``MOCK__AUTH`` setting is enabled + """ + mock_login = config["OPENID_CONNECT"].get(idp_name.lower(), {}).get("mock", False) + # to support older cfgs, new cfgs should use the `mock` field in OPENID_CONNECT + legacy_mock_login = config.get("MOCK_{}_AUTH".format(idp_name.upper()), False) + return bool(mock_login or legacy_mock_login) + + +def _with_state_param(url: str, state: str) -> str: + """ + Return an authorization URL carrying exactly one ``state`` parameter. + + Any existing ``state`` is replaced rather than appended: authlib puts its own + generated state in the URL and returns it for the caller to discard, and leaving + two would let the IdP decide which one it echoes back. + + Args: + url (str): authorization URL the user is about to be sent to + state (str): value binding that login to this session + + Returns: + str: the URL with ``state`` set + """ + parsed = urlparse(url) + params = [ + (key, value) + for key, value in parse_qsl(parsed.query, keep_blank_values=True) + if key != "state" + ] + params.append(("state", state)) + return parsed._replace(query=urlencode(params)).geturl() + + def _login_and_register( username, idp_name, diff --git a/fence/blueprints/oauth2.py b/fence/blueprints/oauth2.py index 221315f78..fca3b2411 100644 --- a/fence/blueprints/oauth2.py +++ b/fence/blueprints/oauth2.py @@ -243,7 +243,7 @@ def _get_auth_response_for_prompts(prompts, grant, user, client, scope): # For a POST, return the redirect in JSON instead of headers. if flask.request.method == "POST": redirect_response = flask.make_response( - flask.jsonify({"redirect": response.headers["Location"]}) + flask.jsonify({"redirect": flask.url_for(".authorize")}) ) else: redirect_response = flask.make_response( diff --git a/fence/blueprints/register.py b/fence/blueprints/register.py index 854b3022b..2e5f79739 100644 --- a/fence/blueprints/register.py +++ b/fence/blueprints/register.py @@ -8,7 +8,7 @@ from flask import current_app from flask_wtf import FlaskForm from wtforms import StringField -from wtforms.validators import DataRequired, Email, ValidationError +from wtforms.validators import DataRequired, Email from cdislogging import get_logger @@ -86,7 +86,7 @@ def register_user(): # and we check that one and only one of user.email or form.email is non-empty. if user.email: if flask.request.form.get("email"): - raise ValidationError( + raise UserError( "Received unexpected 'email' field; this user is already associated with the " "email '{}', so the form should not have had an email field".format( user.email @@ -119,14 +119,17 @@ def register_user(): def add_user_registration_info_to_database( user: User, firstname: str, lastname: str, organization: str, email: str ): - user.additional_info = user.additional_info or {} registration_info = { "firstname": firstname, "lastname": lastname, "org": organization, "email": email, } - user.additional_info["registration_info"] = registration_info + # Rebind rather than mutate: `additional_info` is a plain JSONB column, so + # SQLAlchemy only notices the change if the attribute is assigned a new object. + additional_info = dict(user.additional_info or {}) + additional_info["registration_info"] = registration_info + user.additional_info = additional_info current_app.scoped_session().add(user) current_app.scoped_session().commit() diff --git a/fence/blueprints/user.py b/fence/blueprints/user.py index 1307e9ef6..d902c6a9c 100644 --- a/fence/blueprints/user.py +++ b/fence/blueprints/user.py @@ -106,7 +106,7 @@ def upload_certificate(certificate): certificates = flask.g.user.application.certificates_uploaded if set(REQUIRED_CERTIFICATES.keys()).issubset(set(c.name for c in certificates)): title = "User application for {}".format(flask.g.user.username) - if getattr(flask.g, "client"): + if getattr(flask.g, "client", None): title += " from {}".format(flask.g.client) if "EMAIL_SERVER" in config: content = "Application for user: {}\n" "email: {}".format( diff --git a/fence/jwt/blacklist.py b/fence/jwt/blacklist.py index 87b83d4b4..6f8ad69ac 100644 --- a/fence/jwt/blacklist.py +++ b/fence/jwt/blacklist.py @@ -107,8 +107,8 @@ def blacklist_encoded_token(encoded_token, public_key=None): - Add entry with ``jti`` to ``BlacklistedToken`` table """ # Decode token and get claims. - public_key = public_key or keys.default_public_key() try: + public_key = public_key or _public_key_for_token(encoded_token) claims = jwt.decode( encoded_token, public_key, @@ -169,8 +169,12 @@ def is_token_blacklisted(encoded_token, public_key=None): Return: token: decoded JWT claims bool: whether JWT is blacklisted + + Raises: + jwt.InvalidTokenError: if the token cannot be decoded, or no loaded keypair + matches its ``kid`` """ - public_key = public_key or keys.default_public_key() + public_key = public_key or _public_key_for_token(encoded_token) token = jwt.decode( encoded_token, public_key, @@ -178,3 +182,36 @@ def is_token_blacklisted(encoded_token, public_key=None): options={"verify_aud": False, "verify_exp": False}, ) return token, is_blacklisted(token["jti"]) + + +def _public_key_for_token(encoded_token: str) -> str: + """ + Resolve the public key matching a token's ``kid``. + + Every loaded keypair is published in the JWKS and is accepted when a token is + validated, so revocation has to consider all of them rather than only the current + signing key: after a key rotation the previous keypair stays loaded so that + outstanding refresh tokens and API keys keep working, and those are exactly the + credentials a caller needs to be able to revoke. + + Args: + encoded_token (str): the JWT whose signature is about to be checked + + Returns: + str: PEM-encoded public key for the token's ``kid`` + + Raises: + jwt.InvalidTokenError: if no loaded keypair matches the token's ``kid`` + """ + # Read the kid from the header rather than via authutils' helpers: those decode + # the claims with exp verification on, and an expired token still has to be + # checkable against the denylist. + kid = jwt.get_unverified_header(encoded_token).get("kid") + if not kid: + return keys.default_public_key() + + for keypair in flask.current_app.keypairs: + if keypair.kid == kid: + return keypair.public_key + + raise jwt.InvalidTokenError("no loaded keypair matches kid {}".format(kid)) diff --git a/fence/oidc/grants/authorization_code_grant.py b/fence/oidc/grants/authorization_code_grant.py deleted file mode 100644 index adb5f0e6b..000000000 --- a/fence/oidc/grants/authorization_code_grant.py +++ /dev/null @@ -1,155 +0,0 @@ -import bcrypt - -from authlib.common.security import generate_token -from authlib.oauth2.rfc6749.errors import InvalidClientError, UnauthorizedClientError -from authlib.oauth2.rfc6749.grants import ( - AuthorizationCodeGrant as AuthlibAuthorizationCodeGrant, -) -from authlib.oauth2.rfc6749.util import get_obj_value -import flask - -from fence.models import AuthorizationCode - - -class AuthorizationCodeGrant(AuthlibAuthorizationCodeGrant): - def __init__(self, uri, params, headers, client_model, token_generator): - super(AuthorizationCodeGrant, self).__init__( - uri, params, headers, client_model, token_generator - ) - - def create_authorization_code(self, client, user, **kwargs): - """ - Create an ``AuthorizationCode`` model for the current OAuth request - from the given client and user. - - Certain parameters in the ``AuthorizationCode`` are filled out using - the arguments passed from the OAuth request (the redirect URI, scope, - and nonce). - """ - code = AuthorizationCode( - code=generate_token(50), - client_id=client.client_id, - redirect_uri=kwargs.get("redirect_uri", ""), - scope=kwargs.get("scope", ""), - user_id=user.id, - nonce=kwargs.get("nonce"), - ) - - with flask.current_app.db.session as session: - session.add(code) - session.commit() - - return code.code - - def parse_authorization_code(self, code, client): - """ - Search for an ``AuthorizationCode`` matching the given code string and - client. - - Args: - code (str): the code string for the ``AuthorizationCode`` - client (Client): the client the code was issued to - - Return: - AuthorizationCode - """ - with flask.current_app.db.session as session: - authorization_code = ( - session.query(AuthorizationCode) - .filter_by(code=code, client_id=client.client_id) - .first() - ) - if not authorization_code or authorization_code.is_expired(): - return None - return authorization_code - - def delete_authorization_code(self, authorization_code): - """ - Delete a saved authorization code. - - Args: - authorization_code (AuthorizationCode): - the ``AuthorizationCode`` to delete - - Return: - None - """ - with flask.current_app.db.session as session: - session.delete(authorization_code) - session.commit() - - def create_access_token(self, token, client, authorization_code): - """ - Create an "access token" model from the given token. - - The JWTs are stateless, so just pass. - """ - pass - - def create_access_token_response(self): - """ - Create the token response. - - NOTE: overrides the method from authlib in order to pass the ``nonce`` - parameter to the token generator. - - Return: - Tuple[int, dict, dict]: tuple of (status_code, body, headers) - """ - client = self._authenticated_client - token = self.token_generator( - client, - self.GRANT_TYPE, - scope=get_obj_value(self._authorization_code, "scope"), - nonce=self._authorization_code.nonce, - ) - self.create_access_token(token, client, self._authorization_code) - self.delete_authorization_code(self._authorization_code) - return 200, token, self.TOKEN_RESPONSE_HEADER - - def authenticate_client(self): - """Parse the authenticated client. - - For example, the client makes the following HTTP request using TLS: - - .. code-block:: http - - POST /token HTTP/1.1 - Host: server.example.com - Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW - Content-Type: application/x-www-form-urlencoded - - grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA - &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb - - To authenticate client with other means, re-implement this method in - subclass. - - :return: client - """ - client_params = self.parse_basic_auth_header() - if client_params: - # authenticate the client if client authentication is included - client_id, client_secret = client_params - client = self.get_and_validate_client(client_id) - # Client secrets are stored as hash. - hashed = client.client_secret - if ( - bcrypt.hashpw( - client_secret.encode("utf-8"), hashed.encode("utf-8") - ).decode("utf-8") - != hashed - ): - raise InvalidClientError(uri=self.uri) - - return client - - # require client authentication for confidential clients or for any - # client that was issued client credentials (or with other - # authentication requirements) - client_id = self.params.get("client_id") - client = self.get_and_validate_client(client_id) - if client.check_client_type("confidential") or client.client_secret: - raise UnauthorizedClientError(uri=self.uri) - - return client diff --git a/fence/resources/ga4gh/passports.py b/fence/resources/ga4gh/passports.py index 0dcf13965..36ce9690d 100644 --- a/fence/resources/ga4gh/passports.py +++ b/fence/resources/ga4gh/passports.py @@ -17,6 +17,7 @@ from fence.jwt.validate import validate_jwt from fence.config import config +from fence.errors import Unauthorized from fence.models import ( create_user, query_for_user, @@ -158,12 +159,16 @@ def sync_gen3_users_authz_from_ga4gh_passports( for user in users_from_current_passport: users_from_all_passports[user.username] = user - put_gen3_usernames_for_passport_into_cache( - passport=passport, - user_ids_from_passports=list(users_from_all_passports.keys()), - expires_at=min_visa_expiration, - db_session=db_session, - ) + usernames_from_current_passport = [ + user.username for user in users_from_current_passport + ] + if usernames_from_current_passport: + put_gen3_usernames_for_passport_into_cache( + passport=passport, + user_ids_from_passports=usernames_from_current_passport, + expires_at=min_visa_expiration, + db_session=db_session, + ) db_session.commit() @@ -202,6 +207,17 @@ def get_unvalidated_visas_from_valid_passport(passport, pkey_cache=None): # ignore malformed/invalid passports return [] + # Check the issuer before anything reads a key. validate_jwt resolves the signing + # key by fetching the issuer's discovery document and then whatever jwks_uri that + # document names, and only compares `iss` against the allowlist afterwards - so + # without this gate an unverified `iss` chooses an outbound request target. + if not _issuer_is_allowed(passport_issuer): + logger.error( + "Passport issuer {} is not in GA4GH_VISA_ISSUER_ALLOWLIST. " + "Discarding passport.".format(passport_issuer) + ) + return [] + public_key = pkey_cache.get(passport_issuer, {}).get(passport_kid) try: @@ -248,6 +264,14 @@ def validate_visa(raw_visa, pkey_cache=None): '"jku" in the header. Only Visa Access Tokens are supported.' ) + # The issuer decides where validate_jwt goes looking for a key, + # so it is checked before that happens. + visa_issuer = get_iss(raw_visa) + if not _issuer_is_allowed(visa_issuer): + raise Exception( + f"Visa issuer {visa_issuer} is not in GA4GH_VISA_ISSUER_ALLOWLIST" + ) + logger.info("Attempting to validate visa") decoded_visa = validate_jwt( @@ -333,6 +357,17 @@ def get_or_create_gen3_user_from_iss_sub(issuer, subject_id, db_session=None): idp_name = IssSubPairToUser.ISSUER_TO_IDP.get(issuer) logger.debug(f"issuer_to_idp: {IssSubPairToUser.ISSUER_TO_IDP}") if not gen3_user: + if not config["ALLOW_NEW_USER_ON_LOGIN"]: + # Deployments that turn this off expect accounts to arrive only + # through their own provisioning process; a visa identity must not be + # able to create one here either. + logger.info( + f"Refusing to create a Fence user for issuer {issuer} because " + "ALLOW_NEW_USER_ON_LOGIN is disabled" + ) + raise Unauthorized( + "New user is not yet authorized/activated in the system" + ) gen3_user = create_user(db_session, logger, username, idp_name=idp_name) if not idp_name: logger.info( @@ -363,7 +398,17 @@ def get_or_create_gen3_user_from_iss_sub(issuer, subject_id, db_session=None): db_session.add(iss_sub_pair_to_user) db_session.commit() - return iss_sub_pair_to_user.user + gen3_user = iss_sub_pair_to_user.user + if gen3_user.active == False: + # Returning this user would re-grant and refresh their arborist policies, + # undoing a deactivation rather than merely failing to notice it. + logger.info( + f"Refusing to resolve deactivated Fence user {gen3_user.username} " + "from a visa identity" + ) + raise Unauthorized("User is known but not authorized/activated in the system") + + return gen3_user def _sync_validated_visa_authorization( @@ -524,6 +569,20 @@ def put_gen3_usernames_for_passport_into_cache( ) +def _issuer_is_allowed(issuer: str) -> bool: + """ + Whether an issuer is trusted to sign passports and visas for this deployment. + + Args: + issuer (str): the ``iss`` claim, which is unverified at the point this is + consulted + + Returns: + bool: True if the issuer appears in GA4GH_VISA_ISSUER_ALLOWLIST + """ + return issuer in config.get("GA4GH_VISA_ISSUER_ALLOWLIST", []) + + # TODO to be called after login def map_gen3_iss_sub_pair_to_user(gen3_issuer, gen3_subject_id, gen3_user): pass diff --git a/fence/sync/sync_users.py b/fence/sync/sync_users.py index a10769e7f..6d1b346bc 100644 --- a/fence/sync/sync_users.py +++ b/fence/sync/sync_users.py @@ -58,6 +58,9 @@ def _format_policy_id(path, privilege): def download_dir(sftp, remote_dir, local_dir): """ Recursively download file from remote_dir to local_dir + + Entries whose filename cannot be used safely are logged and skipped. + Args: remote_dir(str) local_dir(str) @@ -66,14 +69,46 @@ def download_dir(sftp, remote_dir, local_dir): dir_items = sftp.listdir_attr(remote_dir) for item in dir_items: + # Filenames come off the wire, and paramiko filters only the exact names "." + # and ".." - it passes "../x" and "/abs/x" straight through, and os.path.join + # would honor either, letting a hostile server choose where we write. + local_path = _safe_local_path(local_dir, item.filename) + if not local_path: + logger.warning( + "usersync: refusing remote entry with unsafe filename %r under %r", + item.filename, + remote_dir, + ) + continue + remote_path = remote_dir + "/" + item.filename - local_path = os.path.join(local_dir, item.filename) if S_ISDIR(item.st_mode): download_dir(sftp, remote_path, local_path) else: sftp.get(remote_path, local_path) +def _safe_local_path(local_dir: str, remote_filename: str) -> str | None: + """ + Resolve a server-supplied filename to a destination inside local_dir. + + Containment rests on reducing the name to its basename: the result holds no path + separators, so it cannot traverse out of local_dir however the server spelled it. + + Args: + local_dir(str): the directory the download is confined to + remote_filename(str): filename as reported by the remote server + + Returns: + str: the destination path, or None if the filename cannot be used safely + """ + filename = os.path.basename(remote_filename.replace("\\", "/")) + if not filename or filename in (os.curdir, os.pardir): + return None + + return os.path.join(local_dir, filename) + + def arborist_role_for_permission(permission): """ For the programs/projects in the existing fence access control model, in order to @@ -374,7 +409,7 @@ def _match_pattern(filepath, id_patterns, encrypted=True): # when converting the YAML from fence-config, # python reads it as Python string literal. So "\" turns into "\\" # which messes with the regex match - pattern.replace("\\\\", "\\") + pattern = pattern.replace("\\\\", "\\") if re.match(pattern, os.path.basename(filepath)): return True return False diff --git a/fence/utils.py b/fence/utils.py index 06c1352b0..1aac59a41 100644 --- a/fence/utils.py +++ b/fence/utils.py @@ -502,7 +502,7 @@ def allowed_login_redirects(): Return: List[str]: allowed redirect URLs """ - allowed = config.get("LOGIN_REDIRECT_WHITELIST", []) + allowed = list(config.get("LOGIN_REDIRECT_WHITELIST", [])) allowed.append(config["BASE_URL"]) with flask.current_app.db.session as session: clients = session.query(Client).all() @@ -518,9 +518,18 @@ def domain(url): """ Return just the domain for a URL, no schema or path etc. This is to consistently compare different URLs from flask, the config, and from the user. + + A URL whose authority is present but whose scheme is omitted ("//host/path") names + a different host while still starting with a slash, so it is parsed rather than + treated as a path on this application. Browsers normalize backslashes to forward + slashes in the authority position, so "/\\host" and "\\\\host" get the same + treatment. """ if not url: return "" - if url.startswith("/"): + normalized = url.replace("\\", "/") + if normalized.startswith("//"): + return urlparse(normalized).netloc + if normalized.startswith("/"): return urlparse(config["BASE_URL"]).netloc return urlparse(url).netloc diff --git a/poetry.lock b/poetry.lock index c79642253..beb19263b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.4.1 and should not be changed by hand. [[package]] name = "addict" @@ -14,19 +14,19 @@ files = [ [[package]] name = "alembic" -version = "1.18.5" +version = "1.20.0" description = "A database migration tool for SQLAlchemy." optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "alembic-1.18.5-py3-none-any.whl", hash = "sha256:06d8ba9d04558022f5395e9317de03d270f3dced49cee01f89fe7a13c26f14bc"}, - {file = "alembic-1.18.5.tar.gz", hash = "sha256:1554982221dd17e9a749b53902407578eb305e453f71999e8c7f0a48389fff8e"}, + {file = "alembic-1.20.0-py3-none-any.whl", hash = "sha256:77eb101048d95f982c0353e9233404889dcd7a6fc244c107836c0e2fc9cf7d9d"}, + {file = "alembic-1.20.0.tar.gz", hash = "sha256:db505480647bc60386c5369402f4a57a506b7539c9e9ef5e270d45cbbe4939bf"}, ] [package.dependencies] Mako = "*" -SQLAlchemy = ">=1.4.23" +SQLAlchemy = ">=2.0" typing-extensions = ">=4.12" [package.extras] @@ -49,42 +49,43 @@ dev = ["black", "coverage", "isort", "pre-commit", "pyenchant", "pylint"] [[package]] name = "annotated-doc" -version = "0.0.4" +version = "0.0.5" description = "Document parameters, class attributes, return types, and variables inline, with Annotated." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320"}, - {file = "annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4"}, + {file = "annotated_doc-0.0.5-py3-none-any.whl", hash = "sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101"}, + {file = "annotated_doc-0.0.5.tar.gz", hash = "sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb"}, ] [[package]] name = "annotated-types" -version = "0.7.0" +version = "0.8.0" description = "Reusable constraint types to use with typing.Annotated" optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" groups = ["main"] 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"}, + {file = "annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0"}, + {file = "annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7"}, ] [[package]] name = "anyio" -version = "4.14.1" +version = "4.15.1" description = "High-level concurrency and networking framework on top of asyncio or Trio" optional = false python-versions = ">=3.10" groups = ["main", "dev"] files = [ - {file = "anyio-4.14.1-py3-none-any.whl", hash = "sha256:4e5533c5b8ff0a24f5d7a176cbe6877129cd183893f66b537f8f227d10527d72"}, - {file = "anyio-4.14.1.tar.gz", hash = "sha256:8d648a3544c1a700e3ff78615cd679e4c5c3f149904287e73687b2596963629e"}, + {file = "anyio-4.15.1-py3-none-any.whl", hash = "sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101"}, + {file = "anyio-4.15.1.tar.gz", hash = "sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94"}, ] [package.dependencies] idna = ">=2.8" +typing_extensions = {version = ">=4.16.0", markers = "python_version < \"3.15\""} [package.extras] trio = ["trio (>=0.32.0)"] @@ -106,14 +107,14 @@ cryptography = "*" [[package]] name = "authutils" -version = "8.0.0" +version = "8.0.1" description = "Gen3 auth utility functions" optional = false python-versions = "<4.0,>=3.13" groups = ["main"] files = [ - {file = "authutils-8.0.0-py3-none-any.whl", hash = "sha256:4781c24dac4e1349288a660fedc6fe78be396ebc53139ed20e138844b7bc4892"}, - {file = "authutils-8.0.0.tar.gz", hash = "sha256:a4491d9d7d7d240f7ebc15037c45c60c2da092c30068002003337101e31a04b8"}, + {file = "authutils-8.0.1-py3-none-any.whl", hash = "sha256:dc7878947d68ab7a05407236464dc302478ae7c7d359d65729c02b699631bf45"}, + {file = "authutils-8.0.1.tar.gz", hash = "sha256:af35bc4d3d4c03cbbcd76076de474bbfcde5edc5f4de887bd4736dcdbd463433"}, ] [package.dependencies] @@ -121,7 +122,7 @@ authlib = ">=1.6.6" cached-property = ">=1.4,<2.0" cdiserrors = "*" cdislogging = "*" -cryptography = ">=48.0.1" +cryptography = ">=50.0.0" httpx = ">=0.23.0,<1.0.0" pyjwt = {version = ">=2.4.0,<3.0", extras = ["crypto"]} starlette = "*" @@ -155,14 +156,14 @@ tracing = ["opentelemetry-api (>=1.26,<2.0)"] [[package]] name = "azure-storage-blob" -version = "12.30.0" +version = "12.30.2" description = "Microsoft Azure Blob Storage Client Library for Python" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "azure_storage_blob-12.30.0-py3-none-any.whl", hash = "sha256:d415ac50b67a8da6b3ae7e9f1014b1b55cd7aafa0b8d4ca9b380568dc7360423"}, - {file = "azure_storage_blob-12.30.0.tar.gz", hash = "sha256:2cd74d4d5731e5eb6b8d5c5056ee115a5e88f8fdf22517b739836fda685018be"}, + {file = "azure_storage_blob-12.30.2-py3-none-any.whl", hash = "sha256:057d1f78c7cf27a0b7ec54ffdbe2436c63474be0a0e25cc55c9ce6addcd13bae"}, + {file = "azure_storage_blob-12.30.2.tar.gz", hash = "sha256:77f8e38c319d0b0efe9444c29179277157c5c2e5ddc40ec39f1f82faacebdcb4"}, ] [package.dependencies] @@ -188,99 +189,99 @@ files = [ [[package]] name = "backports-zstd" -version = "1.6.0" +version = "1.7.0" description = "Backport of compression.zstd" optional = false python-versions = "<3.14,>=3.10" groups = ["dev"] markers = "python_version == \"3.13\"" files = [ - {file = "backports_zstd-1.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:73000459db113a658c4fb0510100ef0e79137b5828bf957b7709aacae4eb1b87"}, - {file = "backports_zstd-1.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d6e78d5e28f812b39f92397806ecddd4a6f3bf35531a8c039a1f187abc931af8"}, - {file = "backports_zstd-1.6.0-cp310-cp310-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:32f04d54ec1fdf3aa648b24a10b1c9234ed2046cc4af7a8850cbc236c05d42f3"}, - {file = "backports_zstd-1.6.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83415af3c64550a56cc20b4cce59bbaa81f21d28466d7adf98feff011ecbc66d"}, - {file = "backports_zstd-1.6.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a3c17e6a267d13de9cbf14bf2ebfa87e03d26692456fc67d2dbed9da4f479b18"}, - {file = "backports_zstd-1.6.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:75578c71644b031118ce938855a53530708db7f4af6e83e2f8840d5a1de990f8"}, - {file = "backports_zstd-1.6.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a4ae7ed5a6d813450cc2d818284ea3db9721edcef50a56aae42ea06feec38c6e"}, - {file = "backports_zstd-1.6.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5e9a8370c8ed873083d5de956d6b2e60adbad31e52d7a11111c96ef01d1910ae"}, - {file = "backports_zstd-1.6.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c2d1ccfe088e8279d605011a3575619a74526c261be357695b3258c0f636115a"}, - {file = "backports_zstd-1.6.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e73a550dbeb84e8fa50f8385f7735e9a4735b465851ef617d02f80ab10e44e7e"}, - {file = "backports_zstd-1.6.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:84f92e5a60a78c72ccda79d0417d311a1f6da18f446423ed411726d545bf7b56"}, - {file = "backports_zstd-1.6.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0eb4281f402b94d397b7482f6d9efd04c28274e4ed6eb57eb1f87bdd091a6a87"}, - {file = "backports_zstd-1.6.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:d6b9b06323e3ba947c0003b2d70e02f33c90c36bc6262a92eb8201afc4a1aa08"}, - {file = "backports_zstd-1.6.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8872a0e9f1af975966b5be6af7eebd3dc4046f15e470b719316516dc3d137cd6"}, - {file = "backports_zstd-1.6.0-cp310-cp310-win32.whl", hash = "sha256:c14fa5dc39a804f1b92d63506f450eca5c59647a18d197d1a564b89dac1be1ce"}, - {file = "backports_zstd-1.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:8219d6fceae6b39535c4ac323dba0923d10f781d59962ff3504e693fdcafa92c"}, - {file = "backports_zstd-1.6.0-cp310-cp310-win_arm64.whl", hash = "sha256:b7bc9a0b66097f03820a54316d2fdd0beb38859cf98f10d63e94c55450ed8920"}, - {file = "backports_zstd-1.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c4fc41b2df5529cad5ceb230319e82728096d4b353ce8d4df68a2ec37e291bb8"}, - {file = "backports_zstd-1.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:83391ef5935cc0f329b1abca414ae20ffe40d335fc21a4b5e664f08a74317d5f"}, - {file = "backports_zstd-1.6.0-cp311-cp311-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:7d3f64c503af7b60115b97c16feaf75bd191ef2c978d5c0c7725a6682bef63c5"}, - {file = "backports_zstd-1.6.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0308990ffc998df3c7ed35276bde049728b5c3956203cae40d80893576a41459"}, - {file = "backports_zstd-1.6.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8c298785e2fadeab82342040f2d9ce764ce500e6da6a6d99a2de514e63580b5a"}, - {file = "backports_zstd-1.6.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ae106fe16e36efc60ab098d02478d30aa0e31e1420eb4ecf0116459253bc6361"}, - {file = "backports_zstd-1.6.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7293fefe15f0e5852bdb4ad1e0e26f3cbd4d3e61c19f751ecc4ff34bc1eb237d"}, - {file = "backports_zstd-1.6.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ece8e7288db5b827ef8c64b2f78519f1a173a8991a625978fce02eccd7654fe9"}, - {file = "backports_zstd-1.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:28eef3881164f3c23ce58ed59e4684103bdd279583eb2d299858c9e9b72fde9a"}, - {file = "backports_zstd-1.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:481a1e9bd8f419fdc625307aa20234687f99368c75df511ef589693c5fea4c6f"}, - {file = "backports_zstd-1.6.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:3b6713371f8987a1178df93cb36f29eef191f224021e2d656b2f11ce60d26816"}, - {file = "backports_zstd-1.6.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:b0ddbcd2866b8ff1a2836e4b0e4d44788f5b992d83fac75a38cda8f9a2bee079"}, - {file = "backports_zstd-1.6.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2914abea516704bdafb2090acd3f15b5f9debecfabd15b8dd8285b2ad3b92209"}, - {file = "backports_zstd-1.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dd085eafa2aac6f883afd28210a3231f717f25409a1e44a39bb7b04c8c5b5646"}, - {file = "backports_zstd-1.6.0-cp311-cp311-win32.whl", hash = "sha256:b81b4cf3d6e0ad7ac92bef248f49fafc954262c5fb0f7e19d6aac497e5a856b2"}, - {file = "backports_zstd-1.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:10b61850c4112952e05aa6e6cce8c9a5936fbeadb321e154216705cc76a14afa"}, - {file = "backports_zstd-1.6.0-cp311-cp311-win_arm64.whl", hash = "sha256:068ef3d8c18815a2e3a752f766313e19910e7c50939b956923748d9c04ebcb1b"}, - {file = "backports_zstd-1.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0466b14723f3b7697669c00ee66fe16e30e25636b286b0a923fa86fa3d8a753c"}, - {file = "backports_zstd-1.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1d146926e997d2d3de8212bdcbf4985344a2622ca3bec458d8908000a84fd883"}, - {file = "backports_zstd-1.6.0-cp312-cp312-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:460fd6b3f338c659507ae36cfd6b58ac9942a2ff233c5cf574416dfec0451a84"}, - {file = "backports_zstd-1.6.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c2b1f4a640c51130caa92cef5bf72bd3c3dbbcfbf814c37403aa0601b1811b0"}, - {file = "backports_zstd-1.6.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:beb43e9885202c8d4f3762319ed4d5e98e197622afbff8439fbbdd81d08938b9"}, - {file = "backports_zstd-1.6.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fbb746522ebfc11155f1cd688e2c48ef3d74125e38b63eabdaab068a055c3e88"}, - {file = "backports_zstd-1.6.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a99710fbb225d459d66def4dc2bb2cd4a9a0bdc8b799fc0621cfdd863be9c93"}, - {file = "backports_zstd-1.6.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f69365ee2b836939137de024a302395a1cb8654fb6dc5ffef6381105259c8f87"}, - {file = "backports_zstd-1.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:66cf8038893c7708ec345ffb3ac63c775d10f430f323ac2f0334fdb6a397c57c"}, - {file = "backports_zstd-1.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e514c71ca72f3b56bd8fbda1a6a5b7d1100a2764b42a3c74a38841f25f9b00ab"}, - {file = "backports_zstd-1.6.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7741e44f7938ec94f9a52678c8d19b7bc548522ffdc39c9e4481af8db545fa9a"}, - {file = "backports_zstd-1.6.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:97e8a9674652496c7612b528085dd5a296c052a2edc466ca1bfb7b0b27820413"}, - {file = "backports_zstd-1.6.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:23a793f2fed4dbf0517319759a2cded0b0dd8e8d3797fe30badd5693e320c175"}, - {file = "backports_zstd-1.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b951113113ed4b8d173418a4f155c14b739dace626b3fa3f82be1831958d39e4"}, - {file = "backports_zstd-1.6.0-cp312-cp312-win32.whl", hash = "sha256:6430b34a2ae6fcc604672f4f913102563473d9a015bdca1ce8c95041cc1f2677"}, - {file = "backports_zstd-1.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:08793876172551a930ce4d65c712cd516184d1a97070d4a1193e05bf0cf7040d"}, - {file = "backports_zstd-1.6.0-cp312-cp312-win_arm64.whl", hash = "sha256:03b7c59c71f7a597e2bcb3f8368371e9a660a1bdf1c37afc1f1ad1496a013c19"}, - {file = "backports_zstd-1.6.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:2ace939e4d620e119423606f2d3d7115f8707733bf57f279ad9a9383f875986f"}, - {file = "backports_zstd-1.6.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:4c68a9ed2df0cca51d774c521e68a34d2e3d9ebfc687ef8096adfd4f345b551d"}, - {file = "backports_zstd-1.6.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:30576f49b82328ec8af16c11100efe52ca88526f71bbe100ef6b4e707dc13bf2"}, - {file = "backports_zstd-1.6.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b4bddfcfb6679215d6f4dc5f79a1f9301af339480d70527a14b57a1f2e6b6cbf"}, - {file = "backports_zstd-1.6.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:65048ed08c5124f05ff9f355ab9703014bb2dbe7f8d9948ce193685b1775f442"}, - {file = "backports_zstd-1.6.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5918fc6b31437208721276964323933cd86077b8d5b469c59c1b3fd2c8220a05"}, - {file = "backports_zstd-1.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4b6c8b02ab0ccb2431bb7bc238be91d158b308915e7b07937388e540466fe7e7"}, - {file = "backports_zstd-1.6.0-cp313-cp313-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:711e6b98f8924e8b4a61ff97ab6321f33de024e1ed6a32f5123763aeda8459be"}, - {file = "backports_zstd-1.6.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2ba9ac10fc393e5123a08802e0e895a107cb4a66b9973d2844dbd8a343111e59"}, - {file = "backports_zstd-1.6.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2f723219335387d7546412d8141e0303590600949b4184a1391a0c6a3c756058"}, - {file = "backports_zstd-1.6.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:64b94d7a836568926a3309ff510c7f8261b881b341fd4992cabf4f0998878f8a"}, - {file = "backports_zstd-1.6.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e39258a09b1c7ca70b5e94a5c5ccfe4700b4250b8077cfeab31d0f79565d4c9b"}, - {file = "backports_zstd-1.6.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:15b1aae0f64cd742df4bba1d989d0a09a6ec619202543fdba684640454541fd3"}, - {file = "backports_zstd-1.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:25b5ddc789480072551af571a746e9500356b2aff0499861cf2ca07ea7431e68"}, - {file = "backports_zstd-1.6.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a13cfa3410a75e4cb87abdb669aaf79da861cb79299159054ff8f77b9671bc40"}, - {file = "backports_zstd-1.6.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:2ddab55a5f54dec8acfad68ef70f1c704fd21919990ddc238afbd6f496e61c6a"}, - {file = "backports_zstd-1.6.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fa305a84087e10d7a85e8a8a3dcba8cdbda4868f2180173b264b7b488fd37c55"}, - {file = "backports_zstd-1.6.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:df27b57d214a3124fbe4e933ef5a903d4567f154260d9aece8c797a987f2a205"}, - {file = "backports_zstd-1.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:28fecd73459d74910ae1987ab84b7bef690d3dd860948430dd5555108b006daf"}, - {file = "backports_zstd-1.6.0-cp313-cp313-win32.whl", hash = "sha256:3e689af303df287142770abe3a48bbefd24dab4a09da5807d0e1fa8c75bab026"}, - {file = "backports_zstd-1.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:b067b1ef9c8e41fb0882c828aa37829938b5c0dab067eca72b23fc24c563b9da"}, - {file = "backports_zstd-1.6.0-cp313-cp313-win_arm64.whl", hash = "sha256:a838296f5b84c920172fb579cac894d255c1fc25457c7234613ddcfa385e49b7"}, - {file = "backports_zstd-1.6.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6c73ae37dbf9207727ac095dedef864c05d836eaec962a47b3b64eaadaf1c6b6"}, - {file = "backports_zstd-1.6.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:839faf90a7eb525a401978dc925df8c44bd12526e8ba1529b9f8a7106e729637"}, - {file = "backports_zstd-1.6.0-pp310-pypy310_pp73-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:f8f5c1c7c69a4b00889e52d9304a918a5b49010f9645768eb5fd0ad404f790ba"}, - {file = "backports_zstd-1.6.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e80bceebc9b58e959bede9b26cafe15b5b9526f3533a6dd06330c5da73cb9329"}, - {file = "backports_zstd-1.6.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:79284c1dd702f4f24ed1a36e51555c907dd237b6c0d829595978f4089a2aeea9"}, - {file = "backports_zstd-1.6.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:1e20b3ecd0a711be82e964aca28554eabbc31ee69a20e5e7b8fd42268af46212"}, - {file = "backports_zstd-1.6.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:aeef8563b82ed4af328f98e5041c1b4800d86f68f857ffd1577d4d47dc9aa6cd"}, - {file = "backports_zstd-1.6.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9cb75e33131946fabd6319061df3b8b1d588fe0963183280e9b5f49f7772fc09"}, - {file = "backports_zstd-1.6.0-pp311-pypy311_pp73-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:ef132cfb638e9a86bd5dc07fb4e1cb895bc55bce6bb5e759366e8b160d0747e2"}, - {file = "backports_zstd-1.6.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab70eace272d6f122b121c057e436709b50a28abf30d97aab28433c08f4a4095"}, - {file = "backports_zstd-1.6.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:17efb3d11137de5166dd51eedab9c36ad633402acba386eee8d715213ea47e49"}, - {file = "backports_zstd-1.6.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:994167ff6551b9c1ce226e0aab16295b98c94507b5701aa60d2c32b7d50796b1"}, - {file = "backports_zstd-1.6.0.tar.gz", hash = "sha256:80a7859ffe70bf239d7a2ce15293bdeb5b4280ff7dc326ffab312b0e254dbb24"}, + {file = "backports_zstd-1.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2c4f557bd8579d38316344c205b2a540e84b1014fb3721205eb6c3eb5322e9d9"}, + {file = "backports_zstd-1.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:68ee21f0efa3f06d3d3cbb5f291c177497fc550ebef732b0a38599de8db1ee32"}, + {file = "backports_zstd-1.7.0-cp310-cp310-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:beb8d6cf5ab3c27cca3a5fdcfeeb228885083d606f0709ffc0a698aabc4f13ee"}, + {file = "backports_zstd-1.7.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09f1ecac082932870df519818e88eb938a03573245f629e34979141583112123"}, + {file = "backports_zstd-1.7.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0469fbe83c85f5a1fb83657242477ed612d4d4d3c000b67f8a67bc839115b09"}, + {file = "backports_zstd-1.7.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3e0f53a23a40d25236aab6e0e817f2cbbf27e6f8fe976fedaa6b9ee53fc809b9"}, + {file = "backports_zstd-1.7.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0b7929cbaff68c124d2366d803dc654347f37637a3df73a2a0a8f2dbee4819cc"}, + {file = "backports_zstd-1.7.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e4108fa7b126fb4b08853670bb32c4a812aab355b8264aa1a27b7bb724ae6ce0"}, + {file = "backports_zstd-1.7.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1a868cff3de171b4961acd9fcf9e843cc966783aa0b2bdfdba876ec20023e24f"}, + {file = "backports_zstd-1.7.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e833fb85673c0a8c880dc3f759c87726680f953492e9275f666fcbfd127c6e8e"}, + {file = "backports_zstd-1.7.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:3c32951fe1ae22f6f059d3c02cfdc21155cee83be456c424d955bf493ba2a9dc"}, + {file = "backports_zstd-1.7.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d312ff5018199e1f889ca470a98361feaf2d194f82091cbbd366bb539e7c3583"}, + {file = "backports_zstd-1.7.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a43e03d7769a06b5ccf4cad5fcf4b3e690b1b36476632d3e1bc923e12579963f"}, + {file = "backports_zstd-1.7.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ff08fbdf4090c8075bcc0f3ffccf3098e4fd6a0d9a4c5c2078398ea5bb2ddd1f"}, + {file = "backports_zstd-1.7.0-cp310-cp310-win32.whl", hash = "sha256:e9e7bf426a21772b3ac1fe5c967678063d7bfcb58d2f559b98bf4c9c6c52f95f"}, + {file = "backports_zstd-1.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:02c4458f25f884131c59d54549a3bdfd649ca3384f1dd15204762171d9e24739"}, + {file = "backports_zstd-1.7.0-cp310-cp310-win_arm64.whl", hash = "sha256:db2fb308ca3669e2913e66aae9173d9a9d5c448caaa2f1bdd12efbfe480f0fde"}, + {file = "backports_zstd-1.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:165a8898c5514b69533edf4ab1f4f4b4bacc62a137a76f36889b473150ec28a5"}, + {file = "backports_zstd-1.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:700ebb797956767679dbca38e45eaa5c21630e460e31ef53bb4b849125bb5d87"}, + {file = "backports_zstd-1.7.0-cp311-cp311-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:47f14a24428a2bc070e26c402f8d6d25676345c32fa116b16b60167a2925df2a"}, + {file = "backports_zstd-1.7.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c358e72e5ff8f23e9f3ec778be4d67ddc5ced3e6d8f03521db29d7357a773fc3"}, + {file = "backports_zstd-1.7.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6c8c183027eae38f5b0643d153f7f91e569d22ee8db25639aea0745677a38ed8"}, + {file = "backports_zstd-1.7.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5d8493f71d9c5c05d18554afc6bb9a319a6674478e8f3042c7e22900c3a06f4d"}, + {file = "backports_zstd-1.7.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b2e505d8923e1e9224cf249b99c92cf728e9eb91fbd1e07a9c2816013621fad3"}, + {file = "backports_zstd-1.7.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5d1bdc293267200e31baf35aa142c6d0ac3e8cce650f79c84e6a32980dfbfd5c"}, + {file = "backports_zstd-1.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d85c18170e8cdba339edc67a5021cf79ccc858f5fda6aeae71f9015c5e463f6"}, + {file = "backports_zstd-1.7.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:96a6f8d3f4cefb6b11ebfc30fc0d970430ecfb169a6555990734a2a46977ec4b"}, + {file = "backports_zstd-1.7.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c2c01cb823ed1b2422905a9791759bdc986e44e7a12b4661e9d712d5c8946016"}, + {file = "backports_zstd-1.7.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:86785aef2b4663a97c932d829ddc9565354cc628e2ae61764d9d93c8b186d65f"}, + {file = "backports_zstd-1.7.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:633ceee3ba86f696fc4e992f7bce558c132c26d04d64d0bb8c2f5d487d5b3aee"}, + {file = "backports_zstd-1.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4a80bc6a8c9aeaad76cc3ecd58067ec038a764807186b0df970c760df39b89c7"}, + {file = "backports_zstd-1.7.0-cp311-cp311-win32.whl", hash = "sha256:1713271e2faea852a1682a6143c19c3506cd2e1b71f60a8924c59a9d2554d2b2"}, + {file = "backports_zstd-1.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:ae840be71108f6020567dd389c973e70a4374a6c0b03c02d3242c8a98a9b3cdb"}, + {file = "backports_zstd-1.7.0-cp311-cp311-win_arm64.whl", hash = "sha256:8827a5601c749a986faa163f3b59d59eedc5947812be114f7132c3d4ad153fee"}, + {file = "backports_zstd-1.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5548a857bb0fcc5449cc3687353547396c6b1ecd4bd882f1cd34fa8d29e70ca"}, + {file = "backports_zstd-1.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bab192b934fdf5a03df4752556d9c8af2d058163fdfbafd4a253cdfe25449a6f"}, + {file = "backports_zstd-1.7.0-cp312-cp312-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:8344260bed9842c415a93d9bfe23ea834e5f27758827d56933d8c0d06db507a2"}, + {file = "backports_zstd-1.7.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c55e55e1e9dee312bc5e186386e6aa5207482a6d2242bd7c14709ded254f87f"}, + {file = "backports_zstd-1.7.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cf609af3735c7e697ccd13f6b0c88da57c201b6ea63c6afbfe81d6f9b50e298c"}, + {file = "backports_zstd-1.7.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:676a37971f676830d4f90cee8fdf4e438781596fb2f2d1984ac76c9b3eb39a69"}, + {file = "backports_zstd-1.7.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:470895d0bcddc850766e593d1b26764fb138c2feed149f515a2627ef9587d54c"}, + {file = "backports_zstd-1.7.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:02f2f6649a342d0901ddb35596ddadb7c3bb1cf6bb54d691e5e0285f1fa0674f"}, + {file = "backports_zstd-1.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:132ba81fad59d44958b7d10da31545e7128c469cfbc2e268d0eaab96daa64175"}, + {file = "backports_zstd-1.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a3e1c6ce0b232ee6703ed24ee126e8186107f5a4e56edbd21cd1aa5a8c6bfd12"}, + {file = "backports_zstd-1.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d7a7cb964eb8d1bb5d039970b16fe54802ea47dc935ae96d9874844a126bf8ff"}, + {file = "backports_zstd-1.7.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:12a9842a2ec2854cbec7f252ab29d44c2b772788a9bbafded743ca4bf73b115f"}, + {file = "backports_zstd-1.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:138154eea8ced84394991bf0e819dba6b690306a178dd528c28eee724b7d4aec"}, + {file = "backports_zstd-1.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:468b636ed365627b364c94be1c35a52858e13b5bc1fa3f068bbc71b1af65f3d7"}, + {file = "backports_zstd-1.7.0-cp312-cp312-win32.whl", hash = "sha256:f026fe2e89b7ff01ba6ebec6abaff34c6063919151a32afb68714cf139e17c50"}, + {file = "backports_zstd-1.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:2ea62ba2f1a6e6c9e6dc108921f9ae881969ca72e073162fa488d0de3eb2713f"}, + {file = "backports_zstd-1.7.0-cp312-cp312-win_arm64.whl", hash = "sha256:cefb983345c55ccaa20423a4eb96434730e6d640ffa2db9b60e5bedb0fbef94e"}, + {file = "backports_zstd-1.7.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:a3fbcbf819bee2b06b8666b13742098d0f40663ee34e64a12bc360ec0f5e3d89"}, + {file = "backports_zstd-1.7.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:efee02f18e04c2e9e6d694c5cf9b7457c4bda3ea96f48b1ee69769e06bb9d89f"}, + {file = "backports_zstd-1.7.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:ecc95fa0e91d92951d74468e7789afdf91d9e702f40af2d0fcbf0ded4d0f650a"}, + {file = "backports_zstd-1.7.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:34154d82fc0246738159084d146401073f9ac9cfd755b66bb8853ca06037810c"}, + {file = "backports_zstd-1.7.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:44b687b1c0be5cb279693d2682f91ff84c559d679b2ef2fbe501fe4b2db2c4bb"}, + {file = "backports_zstd-1.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dcdbd368659f46b570114eeea36b75347716523870d71f6bc5d7801862aefd6e"}, + {file = "backports_zstd-1.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eda97fa535d4651a4ccdeed4ee7dde3978369046abc8a7456a7117d4271f9333"}, + {file = "backports_zstd-1.7.0-cp313-cp313-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:7e3999b5141d7f85171822d06112f70f7f317d162f0120530dd2c7a28dbf8add"}, + {file = "backports_zstd-1.7.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69367726f4075c2574746f5883b0dc045805c5b02a81fdf8c829c26d33969de3"}, + {file = "backports_zstd-1.7.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:15e97edfd173ade365c01bac7d9d297fa906686015cdbcb5f32a0d410887826b"}, + {file = "backports_zstd-1.7.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:32a94cdcf16b44395cd55086ea38877395ca6bf3362cb507b0eb86db2a45a6a4"}, + {file = "backports_zstd-1.7.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3f4887a8a1fd1290017fe5a1d29a7d1dc5c57f9477fbd64f119316a7e3ae769"}, + {file = "backports_zstd-1.7.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e590313ce156f1d8986dff3107e8ed1651d6d106a56b3a95f965ff8d845ba979"}, + {file = "backports_zstd-1.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:565270b0d6497970fa97a0df59593ae0d225e4176678bbce851d39e5f8aa422b"}, + {file = "backports_zstd-1.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:37ef23c6c522fe935726c8fba6344350973c4a23b06d10194d90d0868b09ff7a"}, + {file = "backports_zstd-1.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b3975330159f1efdd1fba76afe1c7b84f66f26e2bf209b32630fb148d647e0d5"}, + {file = "backports_zstd-1.7.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b40bc8cd0a86cbbe8263a9c3a2bf2e34897483516c6d799725412a19524c32e3"}, + {file = "backports_zstd-1.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f37e12ef10747f76901b1f20ef70d33221e861de177dba5ba08552242c6fd4bd"}, + {file = "backports_zstd-1.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5992143b2a8b71b4d17afed20cce2df50f8718228e31d6e716493b1fe9201712"}, + {file = "backports_zstd-1.7.0-cp313-cp313-win32.whl", hash = "sha256:31ae30d216ffae9243dfa607bcb995f94a70de5765bb8fae1e35ea1ad6497959"}, + {file = "backports_zstd-1.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:8086b4a7443bb2863f7ef8edb317b715d5f3ccec6c5512619bd23d57661ba1b7"}, + {file = "backports_zstd-1.7.0-cp313-cp313-win_arm64.whl", hash = "sha256:7eaceeec75e1dbdce40b81fb0ed1ffdb7ce492d970db7f8aabd6a95ccd6c3dd3"}, + {file = "backports_zstd-1.7.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8d59b145d379745c4461adbe9a9afc647f90ca164f50ea2566c08d6601531d1c"}, + {file = "backports_zstd-1.7.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d3c3cda113aacabe7fd0594ad2832b7023a5fee84009406fe4d230906d80fb25"}, + {file = "backports_zstd-1.7.0-pp310-pypy310_pp73-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:f8da4758af21788a9a90f56b7f658a35d33034e55d416fd40e8bcfbb347b90c2"}, + {file = "backports_zstd-1.7.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e27916c92272ab4285d8d2e02eebe5f4198da10d82250b6edfa3ce372aff6f79"}, + {file = "backports_zstd-1.7.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:76e205599a60acc0824bf03522fb9ad25449492535e1efba18f047e2ce48e745"}, + {file = "backports_zstd-1.7.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2feaefcca77c6ac97a46a64f9d41c429caa135a837c54b46398022716abd8184"}, + {file = "backports_zstd-1.7.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:de58be0a3109cfb83b4e61e2b6eb770201cc132ee5a7c677cd8e0140ad2be80c"}, + {file = "backports_zstd-1.7.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:c13f73d0389cdc88b02c05e8175d8ad3030e9e70ee079748763166aa843b647d"}, + {file = "backports_zstd-1.7.0-pp311-pypy311_pp73-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:a2e30ea49c673533d40eb73d0f7abc0ebe9d2e4fc6dbada5ad60b42ff98ffa86"}, + {file = "backports_zstd-1.7.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3e3f760ee9d16378e3cde9d862e1c9ced577a86736763fb486b9f731d5116807"}, + {file = "backports_zstd-1.7.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25caf23dc36de3b839d16c25893751323cf51a8c986f2d01478c16b25133e2e8"}, + {file = "backports_zstd-1.7.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a64e796c7eee69dfe45827b2e003b7731785ec890c73ea5f5fbc30a1c362fcad"}, + {file = "backports_zstd-1.7.0.tar.gz", hash = "sha256:1a967189c1822b6e85a2e550fdfc88a3272c17633ea0a4732dac5911a8034f2b"}, ] [[package]] @@ -374,18 +375,18 @@ files = [ [[package]] name = "boto3" -version = "1.43.44" -description = "The AWS SDK for Python" +version = "1.43.99" +description = "The AWS SDK for Python (Boto3)" optional = false python-versions = ">=3.10" groups = ["main", "dev"] files = [ - {file = "boto3-1.43.44-py3-none-any.whl", hash = "sha256:621113f7850caec7c8405a07baa26075f700f78844a7f2fdf4d1f879bd3cc3c1"}, - {file = "boto3-1.43.44.tar.gz", hash = "sha256:035d73afe3e29bf271a5b27b30476ff8eefa5ae36f6702eada8e412d5c1420aa"}, + {file = "boto3-1.43.99-py3-none-any.whl", hash = "sha256:3becba8d7bdf80ea0010c68bc6b0772adb8be344f06bdcea77629002be19beb7"}, + {file = "boto3-1.43.99.tar.gz", hash = "sha256:328c8640e7b7dbdaebc271e6eea0f7301218872d67d0661a7beda7d3a1799c07"}, ] [package.dependencies] -botocore = ">=1.43.44,<1.44.0" +botocore = ">=1.43.99,<1.44.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.19.0,<0.20.0" @@ -394,14 +395,14 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.43.44" +version = "1.43.99" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">=3.10" groups = ["main", "dev"] files = [ - {file = "botocore-1.43.44-py3-none-any.whl", hash = "sha256:6afb846fd93815133a53baf1578f1d6016ddbeda247623360379758ca2e3f338"}, - {file = "botocore-1.43.44.tar.gz", hash = "sha256:cc2a95a53efdcf0ce34a51bca28058e72fcc3b10dd625eb1ad900c5ca3eb8bef"}, + {file = "botocore-1.43.99-py3-none-any.whl", hash = "sha256:6c9d66621b31c9f9af2b85a0b4c0c208e17d59d7d49f91c9111baca6d41d0299"}, + {file = "botocore-1.43.99.tar.gz", hash = "sha256:fb8b6e9b193a7c2ac1a22832f7635fe44c452f405aab82eca1e1b629b81dabb7"}, ] [package.dependencies] @@ -410,18 +411,18 @@ python-dateutil = ">=2.1,<3.0.0" urllib3 = ">=1.25.4,<2.2.0 || >2.2.0,<3" [package.extras] -crt = ["awscrt (==0.32.2)"] +crt = ["awscrt (==0.36.0)"] [[package]] name = "build" -version = "1.5.1" +version = "1.6.1" description = "A simple, correct Python build frontend" optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "build-1.5.1-py3-none-any.whl", hash = "sha256:f1a58fe2e5af5b0238a07b9e70207492c79ddebbdb1ad954fc86d62a56be3e0d"}, - {file = "build-1.5.1.tar.gz", hash = "sha256:94e17f1db803ab22f46049376c44c8437c52090f0dfdf1adc43df56542d644fb"}, + {file = "build-1.6.1-py3-none-any.whl", hash = "sha256:ecd351a4be9d35a9eaaba244a7687143c9c7d4aea6ac964e7e7ddab20cbcf4e7"}, + {file = "build-1.6.1.tar.gz", hash = "sha256:51cc11666391ab6f092070437ac747002ff46f3e4113a3622177ee6b488bfc53"}, ] [package.dependencies] @@ -470,16 +471,24 @@ files = [ [[package]] name = "cachelib" -version = "0.14.0" +version = "0.17.0" description = "A collection of cache libraries in the same API interface." optional = false -python-versions = ">=3.8" +python-versions = ">=3.11" groups = ["main"] files = [ - {file = "cachelib-0.14.0-py3-none-any.whl", hash = "sha256:4671000b032baa8fac47ad19850f4f522785cee764b4e04c5cfe8955a18d67de"}, - {file = "cachelib-0.14.0.tar.gz", hash = "sha256:73fedcadd0ba818fb2bb9f3c7cd5fcc2a71e86286f1842f55f28d500faee17f1"}, + {file = "cachelib-0.17.0-py3-none-any.whl", hash = "sha256:f83909b6f78741c3a5d76d292d13bf24964ffb13e00ea1d18f92e20599766ce0"}, + {file = "cachelib-0.17.0.tar.gz", hash = "sha256:f3c7dc8d3c1132ab699681ffdf8a52d341d9425ac1401c538cf0b1d87b1677c8"}, ] +[package.extras] +dynamodb = ["boto3 (>=1.43.4)"] +memcached = ["pylibmc (>=1.6.3)"] +mongodb = ["pymongo (>=4.11)"] +redis = ["redis (>=6.0.0)"] +uwsgi = ["uwsgi (>=2.0.28)"] +valkey = ["valkey (>=6.1.0)"] + [[package]] name = "cdiserrors" version = "2.1.0" @@ -553,124 +562,124 @@ resolved_reference = "74a607736ca4af5ec35f17830ab9b78b5db15837" [[package]] name = "certifi" -version = "2026.6.17" +version = "2026.7.22" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.7" groups = ["main", "dev"] files = [ - {file = "certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db"}, - {file = "certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432"}, + {file = "certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775"}, + {file = "certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55"}, ] [[package]] name = "cffi" -version = "2.1.0" +version = "2.1.1" description = "Foreign Function Interface for Python calling C code." optional = false python-versions = ">=3.10" groups = ["main", "dev"] files = [ - {file = "cffi-2.1.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:b65f590ef2a44640f9a05dbb548a429b4ade77913ce683ac8b1480777658a6c0"}, - {file = "cffi-2.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:164bff1657b2a74f0b6d54e11c9b375bc97b931f2ca9c43fcf875838da1570dd"}, - {file = "cffi-2.1.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:c941bb58d5a6e1c3892d86e42927ed6c180302f07e6d395d08c416e594b98b46"}, - {file = "cffi-2.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a016194dbe13d14ee9556e734b772d8d67b947092b268d757fd4290e3ba2dfc2"}, - {file = "cffi-2.1.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:03e9810d18c646077e501f661b682fbf5dee4676048527ca3cffe66faa9960dd"}, - {file = "cffi-2.1.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:19c54ac121cad98450b4896fa9a43ee0180d57bc4bc911a33db6cab1efab6cd3"}, - {file = "cffi-2.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d433a51f1870e43a13b6732f92aaf540ff77c2015097c78556f75a2d6c030e0"}, - {file = "cffi-2.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3d7f118b5adbfdfead90c25822690b02bc8074fba949bb7858bec4ebd55adb43"}, - {file = "cffi-2.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c5f5df567f6eb216de69be06ce55c8b714090fae02b18a3b40da8163b8c5fa9c"}, - {file = "cffi-2.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:11b3fb55f4f8ad92274ed26705f65d8f91457de71f5380061eb6d125a768fecd"}, - {file = "cffi-2.1.0-cp310-cp310-win32.whl", hash = "sha256:9d72af0cf10a76a600a9690078fe31c63b9588c8e86bf9fd353f713c84b5db0f"}, - {file = "cffi-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:fb62edb5bb52cca65fab91a63afa7561607120d26090a7e8fda6fb9f064726da"}, - {file = "cffi-2.1.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:02cb7ff33ded4f1532476731f89ede53e2e488a8e6205515a82144246ffa7dcc"}, - {file = "cffi-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f5bce581e6b8c235e566a14768a943b172ada3ed73537bb0c0be1edee312d4e7"}, - {file = "cffi-2.1.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:30b65779d598c370374fefabf138d456fd6f3216bfa7bedfab1ba82025b0cd93"}, - {file = "cffi-2.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88023dfe18799507b73f1dbb0d14326a17465de1bc9c9c7655c22845e9ddc3a2"}, - {file = "cffi-2.1.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:0a96b74cda968eebbad56d973efe5098974f0a9fb323865bf99ea1fd24e3e64c"}, - {file = "cffi-2.1.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a5781494d4d400a3f47f8f1da94b324f6e6b440a53387774002890a2a2f4b50f"}, - {file = "cffi-2.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aa7a1b53a2a4452ada2d1b5dade9960b2522f1e61293a811a077439e39029565"}, - {file = "cffi-2.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9d8272c0e483b024e1b9ad029821470ed8ec65631dbd90217469da0e7cd89f1c"}, - {file = "cffi-2.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7762faa47e8ff7eb80bd261d9a7d8eea2d8baa69de5e95b70c1f338bbe712f02"}, - {file = "cffi-2.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:89095c1968b4ba8285840e131bf2891b09ae137fe2146905acae0354fbce1b5e"}, - {file = "cffi-2.1.0-cp311-cp311-win32.whl", hash = "sha256:64c753a0f87a256020004f37a1c8c02c480e725f910f0b2a0f3f07debd1b2479"}, - {file = "cffi-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:4f26194e3d95e06501b942642855aed4f953d55e95d7d01b7c4483db3ecff458"}, - {file = "cffi-2.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:35aaea0c7ee0e58a5cd8c2fd1a48fdf7ece0d2699b7ecdda08194e9ce5dd9b3d"}, - {file = "cffi-2.1.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:df2b82571a1b30f58a87bf4e5a9e78d2b1eff6c6ce8fd3aa3757221f93f0863f"}, - {file = "cffi-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78474632761faa0fb96f30b1c928c84ebcf68713cbb80d15bab09dfe61640fde"}, - {file = "cffi-2.1.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:5972433ad71a9e46516584ef60a0fda12d9dc459938d1539c3ddecf9bdc1368d"}, - {file = "cffi-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b6422532152adf4e59b110cb2808cee7a033800952f5c036b4af047ee43199e7"}, - {file = "cffi-2.1.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:46b1c8db8f6122420f32d02fffb924c2fe9bc772d228c7c711748fff56aabb2b"}, - {file = "cffi-2.1.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9fafc5aa2e2a39aaf7f8cc0c1f044a9b07fca12e558dca53a3cc5c654ad67a7"}, - {file = "cffi-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e9f50d192a3e525b15a75ab5114e442d83d657b7ec29182a991bc9a88fd3a66"}, - {file = "cffi-2.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:98fff996e983a36d3aa2eca83af40c5821202e7e6f32d13ae94e3d2286f10cfe"}, - {file = "cffi-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:379de10ce1ba048b1448599d1b37b24caee16309d1ac98d3982fc997f768700b"}, - {file = "cffi-2.1.0-cp312-cp312-win32.whl", hash = "sha256:9b8f0f26ca4e7513c534d351eca551947d053fac438f2a04ac96d882909b0d3a"}, - {file = "cffi-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:c97f080ea627e2863524c5af3836e2270b5f5dfff1f104392b959f8df0c5d384"}, - {file = "cffi-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:6d194185eabd279f1c05ebe3504265ddfc5ad2b58d0714f7db9f01da592e9eb6"}, - {file = "cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:10537b1df4967ca26d21e5072d7d54188354483b91dc75058968d3f0cf13fbda"}, - {file = "cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a95b05f9baf29b91171b3a8bd2020b028835243e7b0ff6bb23e2a3c228518b1b"}, - {file = "cffi-2.1.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:15faec4adfff450819f3aee0e2e02c812de6edb88203aa58807955db2003472a"}, - {file = "cffi-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:716ff8ec22f20b4d988b12884086bcef0fc99737043e503f7a3935a6be99b1ea"}, - {file = "cffi-2.1.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:63960549e4f8dc41e31accb97b975abaecfc44c03e396c093a6436763c2ea7db"}, - {file = "cffi-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ff067a8d8d880e7809e4ac88eb009bb848870115317b306666502ccad30b147f"}, - {file = "cffi-2.1.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3b926723c13eba9f81d2ef3820d63aeceec3b2d4639906047bf675cb8a7a500d"}, - {file = "cffi-2.1.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:47ff3a8bfd8cb9da1af7524b965127095055654c177fcfc7578debcb015eecd0"}, - {file = "cffi-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:799416bae98336e400981ff6e532d67d5c709cfb30afb79865a1315f94b0e224"}, - {file = "cffi-2.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:961be50688f7fba2fa65f63712d3b9b341a22311f5253460ce933f52f0de1c8c"}, - {file = "cffi-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bf5c6cf48238b0eb4c086978c492ad1cbc22373fc5b2d7353b3a598ce6db887a"}, - {file = "cffi-2.1.0-cp313-cp313-win32.whl", hash = "sha256:db3eb7d46527159a878ec3460e9d40615bc25ba337d477db681aea6e4f05c5d2"}, - {file = "cffi-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:8e74a6135550c4748af665b1b1118b6aab33b1fc6a16f9aff630af107c3b4512"}, - {file = "cffi-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:2282cd5e38aa8accd03e99d1256af8411c84cdbee6a89d841b563fdbd1f3e50f"}, - {file = "cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d2117334c3af3bdcb9a88522b844a2bdb5efdc4f71c6c822df55486ae1c3347a"}, - {file = "cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:702c436735fbe99d59ada02a1f65cfc0d31c0ee8b7290912f8fbc5cd1e4b16c3"}, - {file = "cffi-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1ff3456eab0d889592d1936d6125bbfbc7ae4d3354a700f8bd80450a66445d4d"}, - {file = "cffi-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c4165821e131d6d4ca444347c2b694e2311bcfa3fe5a861cc72968f28867beac"}, - {file = "cffi-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:276f20fffd7b396e12516ba8edf9509210ac248cbbc5acbc39cd512f9f59ebe6"}, - {file = "cffi-2.1.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7d5980a3433d4b71a5e120f9dd551403d7824e31e2e67124fe2769c404c06913"}, - {file = "cffi-2.1.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6ca4919c6e4f89aa99c42510b42cf54596892c00b3f9077f6bdd1505e24b9c8d"}, - {file = "cffi-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d53d10f7da99ae46f7373b9150393e9c5eab9b224909982b43832668de4779f5"}, - {file = "cffi-2.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c351efb95e832a853a29361675f33a7ce53de1a109cd73fd47af0712213aa4ce"}, - {file = "cffi-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:dbf7c7a88e2bac086f06d14577332760bdeecc42bdec8ac4077f6260557d9326"}, - {file = "cffi-2.1.0-cp314-cp314-win32.whl", hash = "sha256:1854b724d00f6654c742097d5387569021be12d3a0f770eae1df8f8acfcc6acd"}, - {file = "cffi-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:1b96bfe2c4bd825681b7d311ad6d9b7280a091f43e8f63da5729638083cd3bfb"}, - {file = "cffi-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:7d28dff1db6764108bc30788d85d61c876beff416d9a49cb9dd7c5a9f34f5804"}, - {file = "cffi-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7ea6b3e2c4250ff1de21c630fe72d0f63eb95c2c32ffbf64a358cf4a8836d714"}, - {file = "cffi-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6af371f3767faeffc6ac1ef57cdfd25844403e9d3f476c5537caee499de96376"}, - {file = "cffi-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb4e8997a49aa2c08a3e43c9045d224448b8941d88e7ac163c7d383e560cbf98"}, - {file = "cffi-2.1.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf01d8c84cbea96b944c73b22182e6c7c432b3475632b8111dbfdc95ddad6e13"}, - {file = "cffi-2.1.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:33eb1ad83ebe8f313e0df035c406227d55a79456704a863fad9842136af5ad7d"}, - {file = "cffi-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ac0f1a2d0cfa7eea3f2aaf006ab6e70e8feeb16b75d65b7e5939982ca2f11056"}, - {file = "cffi-2.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c16914df9fb7f500e440e6875fa23ff5e0b31db01fa9c06af98d59a91f0dc2e4"}, - {file = "cffi-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5ecbd0499275d57506d397eebe1981cee87b47fcd9ef5c22cab7ed7644a39a94"}, - {file = "cffi-2.1.0-cp314-cp314t-win32.whl", hash = "sha256:7d034dcffa09e9a46c93fa3a3be402096cb5354ac6e41ab8e5cc9cd8b642ad76"}, - {file = "cffi-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0582a58f3051372229ca8e7f5f589f9e5632678208d8636fea3676711fdf7fe5"}, - {file = "cffi-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:510aeeeac94811b138077451da1fb18b308a5feab47dd2b603af55804155e1c8"}, - {file = "cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:2e9dabb9abcb7ad15938c7196ad5c1718a4e6d33cc79b4c0209bdb64c4a54a5c"}, - {file = "cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:37f525a7e7e50c017fdebe58b787be310ad59357ae43a053943a6e1a6c526001"}, - {file = "cffi-2.1.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:95f2954c2c9473d892eca6e0409f3568b37ab62a8eedb122461f73cc273476e3"}, - {file = "cffi-2.1.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:cdf2448aab5f661c9315308ec8b93f4e8a1a67a3c733f8631067a2b67d5913dc"}, - {file = "cffi-2.1.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:90bec57cf82089383bd06a605b3eb8daebf7e5a668520beaf6e327a83a947699"}, - {file = "cffi-2.1.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6274dcb2d15cef48daa73ed1be5a40d501d74dccd0cd6db364776d12cb6ba022"}, - {file = "cffi-2.1.0-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:2b71d409cccee78310ab5dec549aed052aaea483346e282c7b02362596e01bb0"}, - {file = "cffi-2.1.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7d3538f9c0e50670f4deb93dbb696576e60590369cae2faf7de681e597a8a1f1"}, - {file = "cffi-2.1.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:8f9ec95b8a043d3dfbc74d9abc6f7baf524dd27a8dc160b0a32ff9cdab650c28"}, - {file = "cffi-2.1.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:af5e2915d41fe6c961694d7bfdc8562942638200f3ce2765dfb8b745cf997629"}, - {file = "cffi-2.1.0-cp315-cp315-win32.whl", hash = "sha256:0a42c688d19fca6e095a53c6a6e2295a5b050a8b289f109adab02a9e61a25de6"}, - {file = "cffi-2.1.0-cp315-cp315-win_amd64.whl", hash = "sha256:bccbbb5ee76a61f9d99b5bf3846a51d7fca4b6a732fe46f89295610edaf41853"}, - {file = "cffi-2.1.0-cp315-cp315-win_arm64.whl", hash = "sha256:8d35c139744adb3e727cd51b1a18324bbe44b8bd41bf8322bca4d41289f48eda"}, - {file = "cffi-2.1.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:f9912624a0c0b834b7520d7769b3644453aabc0a7e1c839da7359f050750e9bc"}, - {file = "cffi-2.1.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:df92f2aba50eb4d96718b68ef76f2e57a57b54f2fa62333496d16c6d585a85ca"}, - {file = "cffi-2.1.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0520e1f4c35f44e209cbbb421b67eec42e6a157f59444dfb6058874ff3610e5d"}, - {file = "cffi-2.1.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3681e031db29958a7502f5c0c9d6bbc4c36cb20f7b104086fa642d1799631ff8"}, - {file = "cffi-2.1.0-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:762f99479dcb369f60ab9017ad4ab97a36a1dd7c1ee5a3b15db0f4b8659120cd"}, - {file = "cffi-2.1.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0611e7ebf90573a535ebdc33ae9da222d037853983e13359f580fab781ca017f"}, - {file = "cffi-2.1.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:86cf8755a791f72c85dc287128cc62d4f24d392e3f1e15837245623f4a33cccc"}, - {file = "cffi-2.1.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:ba00f661f8ba35d075c937174e27c2c421cec3942fd2e0ea3e66996757c0fdd9"}, - {file = "cffi-2.1.0-cp315-cp315t-win32.whl", hash = "sha256:cb96698e3c7413d906ce83f8ffd245ec1bd94707541f299d0ce4d6b0193e982b"}, - {file = "cffi-2.1.0-cp315-cp315t-win_amd64.whl", hash = "sha256:f146d154428a2523f9cc7936c02353c2459b8f6cf07d3cd1ee1c0a611109c5d5"}, - {file = "cffi-2.1.0-cp315-cp315t-win_arm64.whl", hash = "sha256:cbb7640ce37159548d2147b5b8c241f962143d4c71231431820783f4dc78f210"}, - {file = "cffi-2.1.0.tar.gz", hash = "sha256:efc1cdd798b1aaf39b4610bba7aad28c9bea9b910f25c784ccf9ec1fa719d1f9"}, + {file = "cffi-2.1.1-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:baed1e86cc735622097354b9d1281406caf42ff42a886d29faa8e8d1630333be"}, + {file = "cffi-2.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ca82be1a1d406ecfe1d25dc16cb33488e5a16bf4438c9fb590484ea29d92478b"}, + {file = "cffi-2.1.1-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:42e2f76b9455f5a9a844f770bf3e200ed3da0e15f5df3db9c31fe80b04b3d004"}, + {file = "cffi-2.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5a59cc1c4442bc3d5c703bf720b51138d0bfc173618807c9ee2490a7541dd3d9"}, + {file = "cffi-2.1.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:9f8d177621de5cb38ee3e731eda45d421db093ec0739f46a5594babda7987a98"}, + {file = "cffi-2.1.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:75f80557d1389eddbd0de2681f6a390a0c5338c31ddaa821381c203fc3fd50d9"}, + {file = "cffi-2.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:194cffa889098ced9976c3fc6340305e43f6303657d298da55366907c05c22d6"}, + {file = "cffi-2.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5bb4e7ea95dcd6a014a6fef62e62467d67d8e582326443f3d68e71d6320a9fcf"}, + {file = "cffi-2.1.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3d22a20b1fb1632cc72c22f95f7b0d2961c3e1c235f245ba4c606c4771035659"}, + {file = "cffi-2.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1dea0e4d7d4f11f619fe8c1d76caf49e24405b4b5743c0e3be16a500ecd930c9"}, + {file = "cffi-2.1.1-cp310-cp310-win32.whl", hash = "sha256:7ce713ace7c0e4520535b42b77eaa742c16dab813978064913e5a3cf82973b41"}, + {file = "cffi-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a48d62ab9d6f4f98c983223a547af44be6ca3691074c31cecced6facd3ba2dc1"}, + {file = "cffi-2.1.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:c8d2c9fd1f2d16f780d15127abb050d13d1a76c03a4bd87d7e4980e45e511e12"}, + {file = "cffi-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:398aff33cee2767e3e781d2554c54bd0dff386bb437581e0d8011fde1a942ec1"}, + {file = "cffi-2.1.1-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:154852545011f779917b11c78db2358d095da62a9a172b78ad0a583ee5adc0d0"}, + {file = "cffi-2.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3311ed60d36f83378794e1009ac6258bafbf81f7888b4caa7b35a521e3f95813"}, + {file = "cffi-2.1.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6e192623c49c94421616a5778fba35cf0d5a8d000650c1967ef4448ee5cdd990"}, + {file = "cffi-2.1.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a6e721d4b0e45d5b65e87534470e67b18dcd092c83f68fba09f152b9cbc061af"}, + {file = "cffi-2.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:34e261f78cb6ceaaa36f42f2613f4380d94d9c759a9c73c769ee6e0247364632"}, + {file = "cffi-2.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7225e4514edb64eb6740324353e0da0711954fd8d7da4576755b1c6e09b697cd"}, + {file = "cffi-2.1.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:df913725b79db7bcf03448f36b7bf8815363417d5b58deecf9305e3e30f0f21a"}, + {file = "cffi-2.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f5cfbc5fe74540d335175b656c725d74d90e3730c626d92575eea35029d9afaa"}, + {file = "cffi-2.1.1-cp311-cp311-win32.whl", hash = "sha256:f8ec5e643a9a937f64e1999eb9f75d072263751912dc5cd06d3c85f8f44be7c3"}, + {file = "cffi-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:42f6930c31dc7f50732c9ae793c2786c7b6b044195967bbdde40bb9be81c4cc0"}, + {file = "cffi-2.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:c7659f22557c5a0bc4855cd635f55edec690cc008a40768527762cb9fb263455"}, + {file = "cffi-2.1.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0"}, + {file = "cffi-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf"}, + {file = "cffi-2.1.1-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a"}, + {file = "cffi-2.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890"}, + {file = "cffi-2.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50"}, + {file = "cffi-2.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e"}, + {file = "cffi-2.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf"}, + {file = "cffi-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517"}, + {file = "cffi-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735"}, + {file = "cffi-2.1.1-cp312-cp312-win32.whl", hash = "sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e"}, + {file = "cffi-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a"}, + {file = "cffi-2.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80"}, + {file = "cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e"}, + {file = "cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c"}, + {file = "cffi-2.1.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6"}, + {file = "cffi-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971"}, + {file = "cffi-2.1.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c"}, + {file = "cffi-2.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125"}, + {file = "cffi-2.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264"}, + {file = "cffi-2.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3"}, + {file = "cffi-2.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2"}, + {file = "cffi-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b"}, + {file = "cffi-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7"}, + {file = "cffi-2.1.1-cp313-cp313-win32.whl", hash = "sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac"}, + {file = "cffi-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d"}, + {file = "cffi-2.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973"}, + {file = "cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c"}, + {file = "cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb"}, + {file = "cffi-2.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54"}, + {file = "cffi-2.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72"}, + {file = "cffi-2.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1"}, + {file = "cffi-2.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062"}, + {file = "cffi-2.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03"}, + {file = "cffi-2.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96"}, + {file = "cffi-2.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527"}, + {file = "cffi-2.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13"}, + {file = "cffi-2.1.1-cp314-cp314-win32.whl", hash = "sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c"}, + {file = "cffi-2.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48"}, + {file = "cffi-2.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836"}, + {file = "cffi-2.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3"}, + {file = "cffi-2.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2"}, + {file = "cffi-2.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94"}, + {file = "cffi-2.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc"}, + {file = "cffi-2.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29"}, + {file = "cffi-2.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676"}, + {file = "cffi-2.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e"}, + {file = "cffi-2.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f"}, + {file = "cffi-2.1.1-cp314-cp314t-win32.whl", hash = "sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4"}, + {file = "cffi-2.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e"}, + {file = "cffi-2.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5"}, + {file = "cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d"}, + {file = "cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b"}, + {file = "cffi-2.1.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4"}, + {file = "cffi-2.1.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8"}, + {file = "cffi-2.1.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6"}, + {file = "cffi-2.1.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80"}, + {file = "cffi-2.1.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779"}, + {file = "cffi-2.1.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399"}, + {file = "cffi-2.1.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688"}, + {file = "cffi-2.1.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7"}, + {file = "cffi-2.1.1-cp315-cp315-win32.whl", hash = "sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac"}, + {file = "cffi-2.1.1-cp315-cp315-win_amd64.whl", hash = "sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960"}, + {file = "cffi-2.1.1-cp315-cp315-win_arm64.whl", hash = "sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1"}, + {file = "cffi-2.1.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc"}, + {file = "cffi-2.1.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab"}, + {file = "cffi-2.1.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e"}, + {file = "cffi-2.1.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358"}, + {file = "cffi-2.1.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231"}, + {file = "cffi-2.1.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6"}, + {file = "cffi-2.1.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94"}, + {file = "cffi-2.1.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5"}, + {file = "cffi-2.1.1-cp315-cp315t-win32.whl", hash = "sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66"}, + {file = "cffi-2.1.1-cp315-cp315t-win_amd64.whl", hash = "sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3"}, + {file = "cffi-2.1.1-cp315-cp315t-win_arm64.whl", hash = "sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692"}, + {file = "cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be"}, ] markers = {main = "platform_python_implementation != \"PyPy\"", dev = "platform_python_implementation != \"PyPy\" or sys_platform == \"darwin\""} @@ -679,105 +688,184 @@ pycparser = {version = "*", markers = "implementation_name != \"PyPy\""} [[package]] name = "charset-normalizer" -version = "3.4.9" +version = "3.5.1" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7" groups = ["main", "dev"] files = [ - {file = "charset_normalizer-3.4.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd6280cf040f233bd7d3407b743b4b4c74f70e8e1c4199cb112a62c941c0772a"}, - {file = "charset_normalizer-3.4.9-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616"}, - {file = "charset_normalizer-3.4.9-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c1225416b463483160e4af85d5fc3a9690ccb53fd4b1865a6437825f5ede3209"}, - {file = "charset_normalizer-3.4.9-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:16d10d789dd9bcca1173c95af82c58433122564b7bc39385124be735a35cbe99"}, - {file = "charset_normalizer-3.4.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bb41182d93ea91f60b4bc8fbf4c820c69ef8a12ab2d917f3f1834f1acad07e8"}, - {file = "charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:bcf74c1df76758a395bf0af608c04c82257523f55c9868b334f06270d0f2112b"}, - {file = "charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b5314963fce9b0b12743891de876e724997864ee22aa496f903f426c7e2fa5b2"}, - {file = "charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e9701d0049d92c16703a42771b98d560b95248949f23f8cf7b4eddd201814fb9"}, - {file = "charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:65a7ff3f705e57d392f7261b6d0550fe137c3019477431f1c355e0db0a7d3e15"}, - {file = "charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79580094b00d1789d1f93ea55bc43cb2f611910c72235b7657f3482ddcc1b22d"}, - {file = "charset_normalizer-3.4.9-cp310-cp310-win32.whl", hash = "sha256:432786d3561e69aeeae6c7e8648964ce0ad05736120135601f87ac26b9c83381"}, - {file = "charset_normalizer-3.4.9-cp310-cp310-win_amd64.whl", hash = "sha256:8c041122946b7ba21bb32c45b1aa57b1be35527690aeb3c5c234521085632eee"}, - {file = "charset_normalizer-3.4.9-cp310-cp310-win_arm64.whl", hash = "sha256:375b83ed0aecfce76c16d198fbc21f3b11b337d68662bea0a995046682a11419"}, - {file = "charset_normalizer-3.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5"}, - {file = "charset_normalizer-3.4.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2"}, - {file = "charset_normalizer-3.4.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a"}, - {file = "charset_normalizer-3.4.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29"}, - {file = "charset_normalizer-3.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c"}, - {file = "charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b"}, - {file = "charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db"}, - {file = "charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993"}, - {file = "charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da"}, - {file = "charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3"}, - {file = "charset_normalizer-3.4.9-cp311-cp311-win32.whl", hash = "sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d"}, - {file = "charset_normalizer-3.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1"}, - {file = "charset_normalizer-3.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec"}, - {file = "charset_normalizer-3.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0"}, - {file = "charset_normalizer-3.4.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9"}, - {file = "charset_normalizer-3.4.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44"}, - {file = "charset_normalizer-3.4.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9"}, - {file = "charset_normalizer-3.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd"}, - {file = "charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84"}, - {file = "charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b"}, - {file = "charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde"}, - {file = "charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39"}, - {file = "charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62"}, - {file = "charset_normalizer-3.4.9-cp312-cp312-win32.whl", hash = "sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642"}, - {file = "charset_normalizer-3.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0"}, - {file = "charset_normalizer-3.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2"}, - {file = "charset_normalizer-3.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614"}, - {file = "charset_normalizer-3.4.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698"}, - {file = "charset_normalizer-3.4.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b"}, - {file = "charset_normalizer-3.4.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9"}, - {file = "charset_normalizer-3.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33"}, - {file = "charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63"}, - {file = "charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0"}, - {file = "charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe"}, - {file = "charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35"}, - {file = "charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8"}, - {file = "charset_normalizer-3.4.9-cp313-cp313-win32.whl", hash = "sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9"}, - {file = "charset_normalizer-3.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115"}, - {file = "charset_normalizer-3.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012"}, - {file = "charset_normalizer-3.4.9-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380"}, - {file = "charset_normalizer-3.4.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9"}, - {file = "charset_normalizer-3.4.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4"}, - {file = "charset_normalizer-3.4.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a"}, - {file = "charset_normalizer-3.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046"}, - {file = "charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81"}, - {file = "charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917"}, - {file = "charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41"}, - {file = "charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1"}, - {file = "charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf"}, - {file = "charset_normalizer-3.4.9-cp314-cp314-win32.whl", hash = "sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48"}, - {file = "charset_normalizer-3.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b"}, - {file = "charset_normalizer-3.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519"}, - {file = "charset_normalizer-3.4.9-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198"}, - {file = "charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32"}, - {file = "charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632"}, - {file = "charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf"}, - {file = "charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990"}, - {file = "charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d"}, - {file = "charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e"}, - {file = "charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c"}, - {file = "charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2"}, - {file = "charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534"}, - {file = "charset_normalizer-3.4.9-cp314-cp314t-win32.whl", hash = "sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226"}, - {file = "charset_normalizer-3.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177"}, - {file = "charset_normalizer-3.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501"}, - {file = "charset_normalizer-3.4.9-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:253a4a220747e8b5faf57ec320c4f5efb0cef05f647420bf267143ec15dba10a"}, - {file = "charset_normalizer-3.4.9-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:68ce9f4d6b26d5ccbf7fd4459bf75f74a0a146677ebba80597df60cbdb20e6f4"}, - {file = "charset_normalizer-3.4.9-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:58150c9f9b9a552505912d182ccdf26f6396fb6094816ceebcbb20eecabaed94"}, - {file = "charset_normalizer-3.4.9-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:df7276909358e5635ae203673ab7e509ddd224225a8d6b0790bf13eb2bde1cc5"}, - {file = "charset_normalizer-3.4.9-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3c09a49d6cde137258beb3d551994a2927fd35ad5cf96aed573f61bbd67c5f84"}, - {file = "charset_normalizer-3.4.9-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:231ddcbb35e2ff8973e1365db41fe0572662893b99a05deb183b68ad4c0c8bd4"}, - {file = "charset_normalizer-3.4.9-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:920079c3f7456fa213e0829ed2073aaa727fd39d889ead5b4f35d0de5460d04f"}, - {file = "charset_normalizer-3.4.9-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0fa1aec2d32bcc03c8fa0f6f1712caad1adc38509f31142112e5c9daf5b9c833"}, - {file = "charset_normalizer-3.4.9-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:ad41ba96094304aa090f5a30cb6e4fb3b3f1c264c523394b4c39bbacc4dc92ba"}, - {file = "charset_normalizer-3.4.9-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:43b9e366a31fdd1c87d0eb08f579b4a82b723ea54338f040d6b4e518a026ea29"}, - {file = "charset_normalizer-3.4.9-cp39-cp39-win32.whl", hash = "sha256:93d59d504b230e83c7a843251681959a0b6a9cd76f6e146ce1b8a80eb8739af9"}, - {file = "charset_normalizer-3.4.9-cp39-cp39-win_amd64.whl", hash = "sha256:ddf4af30b417d9fe16481e9b81c27ab2a7cde1ff7ba3e85653b02db7d145dc7b"}, - {file = "charset_normalizer-3.4.9-cp39-cp39-win_arm64.whl", hash = "sha256:476743fe6dfe14a2da12e3ac79125dc84a3b2cf8094369a47a1529b0cd8549fe"}, - {file = "charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5"}, - {file = "charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1ee1e296209fdce05b81b663250eefa02213a2da7b41bf26f7829b8ba3545aa"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9fbdce1e47394b09bc9f26ab117dfc8d6491977a11d86f592bb42c779db2fda"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:00668ebb0609751758682eb0b5857e7c35b9f00e84dfdef062e103244ec94d45"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba2f37ee79e6338845261a3c5b1784e5d1acdff2c0785b284f1b633033d136ab"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ce854f5f478050ade5a238731c4ca985a7d3b3cb53ff600a9b5c3b689b5f0a7a"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:96eefc178f8636b9c760c5829345307fd81cfae9ab1e80997dbddeb0f54ee9a3"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:366ec70f5547c640d3ce1985722490f23faf4eb5216a7eeba78277490e78dacb"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:950f23cb393f85543777b0433f082cddd25b51ab398eac7971146495679efe5f"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c1dcc36dcb96abc02236e182d17e0f71430152a6c2c7447421da2d2dc144edea"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:07ffd07412fc5d5e84cd8952acf9ff7e4ed7a708e69d1bada19d8ba91711353f"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:f5542f9b941279d82d41eb0aa9f98eba36fe4df5c7086c651df7944935b37182"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a545775cfe815855ea32d7c27731d79da358ef2055b4a25830231b1622dd18aa"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:494b70049a4d69aec6e8137c13af4cf8db8c9f9820a1392ac293b0dd2987a818"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-win32.whl", hash = "sha256:94fbf1c0c6cc0d3d5e50f9a9313a8cdca90dd696d34b381cd1704f8c9e939f20"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:be47f99644b208bff7766314013f9acf57b056b04191d570d68ad14022cf5b1d"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-win_arm64.whl", hash = "sha256:a6d095662e73e74f0a49988e0593373e243e3a52e27bfeea0a859e88acf4a0f5"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eda059b6bc8bc0812d626fd91a7ce01bf583df0a61296eff390fd94141a34e30"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa2bb0b37202dca27175591f761108b5d34096ade1191ffe4808bdf6b1571488"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b2b1b3fa5670c127b246df1d0c059defd41f689a868a3b9d79df9b1cac42d22"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6e5e4d73d588ca5ed09df1b7dcd1b203d1df3c542e3f50d126c947d432b10731"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b54e7e13267d49ffbfe68e25b3cbd774dab38fa37238f71265e91b36146eb21c"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7b742bf31c88566b4bb6335a7f393bb322e580b6bb98df7bd0c25e6e3519ce8"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6ba32c4d2abf1d2fe7cf27d280f4cca5664233b0f885549c7761719eb977f486"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0722590aabf9dc6a6c0343d523c05458fa2b5047dbe6302fd526bb570600753f"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:aa1099b956fb795e686d073568f6dc002a0bb89765ea6d5b055dd7d9bf1b116c"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:bd6c173f04743d483881bffa1478d5a4624475b8cd1d2194956a75548e191c18"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f298e218441525d3794428b4c8b8fb8662c6d3ea79925d4807ee6b9a96a3bca5"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:6e2912d4babbc65196ac13c2f53468dc57fb8b9c25ef913e8c59ddf7c6dc0e1b"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3d27167433c0d5f18dc850f07d0b3816221984fecdc405d6c157a6f0b8f8e9e6"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-win32.whl", hash = "sha256:ac00177c4831ffa650f8609e4bdddd5fe09c03b1c0c47acece7e6ea20421598b"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:f9b1e28d0e8dbfa858abdba91d6b547beaf2df1a59bec6da6faae7b96a4991a9"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:ae31a1a1db2ee6cc2942fccaf695c934bc7f3db9f2133a3fef1f367cf1a4ab10"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5b6d1386bf0096d26d3a863dc0a487a5b4eb9aa93cf5ba69683d29dde6b9d60f"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4582c27e8c889d64811987b5967fbd3ae0c823fe1fd933b543d55ac20bb475fa"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1d1c7a53a6c2103925cdd6d7229f8c567379f211c869793df679f2e9f738c369"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e6621fb2a4988d6e53eedc455e5903e2679f3967b8acb3d639f1b63c14a2e893"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7c0c10730342b0c9b35dd1d619beb8214e520bd96a1f870f452680b238aab3e0"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9af956078716df40d985fb0dfeb2c2120c5ca92ba4ff4b388acfd01cdc14d08"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9f8405c2c758532c74fed975dbee57be1f31a6e865c031870c79a6ed3212ada"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:96fef3e886d6a9874b14f27fc193fbdc69d5d8035783d86aa4e1cea594e695f9"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5d8531a6569d025f68e2321e7638fb7978f23db58e5f69f56913837aae03816e"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:aae2ee51122d3ae968a3837d97dc24a0aeebb0dea23694422cd172bd30017cd6"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7235dc28fc6dd9d832ac7c7bce95367dedb85929f17368a0c2bee1e080b9acbf"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4abdc5f9ad448c1ecbfae2974b820535d6bc6e7eef63babbab3d81cf46968c71"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ba501e667c17d8411f98e67a022d9604ef179aff0e459b7e292c796837c13573"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-win32.whl", hash = "sha256:cfa1c0cc3a8f9f53f1243a5a99ac36fd003880199383b37672e86ddda9cb07e2"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:3617ac3cfd8b9888f145ad89dd6e692285834b0201c6074a5eeaad3fd4d668c2"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:88e85ab89cb822c1e635f51d6d32e488f94e002e70e2f492bdb8b945543f345a"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:4f298bdadb8f0b9e5672877f647d1be9373ef5320c9e2f049795e26cad28b6a9"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-android_24_x86_64.whl", hash = "sha256:88ca277405c2d3b71c4e1c2ee0e7966e807bcba86a69d11e19ba199d18ae4491"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9362dd90aa7dab48c0054a21187791ccf05473f7dba5d92b8033ae62164675e7"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:977cdbd483a9cff38179bea4fd754289a6f2195c7abd414aba85410b3e66cc5e"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e90251c0c7bdd54a100a0dce3c07b7e637278c93af29dbf78ebb89a58c4bac7d"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94d78ecec2605a8d0398b0f365d5f12a63248438516f5dac536a5eff7337df4a"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d59b75732e9b6f27388e10c14b0259cc5f2e48c78627d185e6a177b58ad3cffe"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0d929fc574b4d6fd9e7c0f5c2ede8716a41911923aa7fa5fce38e0818aa4a1ac"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:394fea06235c8543390050ed5f529187074b029fb027213f6c46ac11ab5d950e"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62b55f6722735a6c472f88361cde6640608773d9443cebdbb51abf436a1fcdd3"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa48b1b63d639f9483e0633e092f5851e2348c352f1f9bb6c8182f87884ef876"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c71fb0d56c920c269cd3e2e3fe7c610e3f1fdb21a6ce60efa6430ff63676cea6"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:485a0d363cafefcd2538a73c7c838daa2035f09b2c9f9b5e3133f80c6aeb84c2"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c0ea61a470e070686aa30892fed79e297d2c8d0ab46b8bcdf027d38c51da591"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:90b7481fb62fbe172c558bc6fd1c4c98d82004a54a7551f20e11ac9bf0b8708c"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:35fe081843b35aad20ffeccec3eeffbe637b15d14f3fb22cc1b59cd8ec17e93c"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fd0350afdc3aabd5576f60ea109228bd5538139713c7b094c5cd27c73a98bc6f"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:9d9a0dc7cbe9bec24c3f767c9122c41fe5a1bc43f47cd099d00d393e09769de4"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-win32.whl", hash = "sha256:d63600d620ad0064c3a748b950ac5ea38a80190e5498532efefa4b7b3f1da1f3"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:aea996a6aba25260827c9ea511d1addfde2da9eb686ac961838509086188b7e6"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:fd0a274c0e5f9a21565cd9d3dd749b61f96b7aa1e20a93aa1ba4029518f2e5c0"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:774d157f112367ff4abd29019f38f023c24e00e56edc7829c20e358a5a913ad8"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:26422d45fd13551cf564c58932f7d72b4f58b93b0fcf18c35ba6be12b46bb102"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:09a7bba9f739468c8e78c36a75c33768e53cb1959fc638f510454c14683f00d5"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4c9548dc78002099910abaebc0a72ac58b7d30931869e0351c09b507dff4ece3"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c428c6c31eb5f4277d7f8eccaf767fbd548ddd5ce3c8b4f4cbbfab3d96b5904c"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f06b7eae9dbe77fe1d644ca244dad508de8d302870a43f3c559b521270938a0"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b7430cf5728e68f6c462254009a6ef4086e1bea43cf2f57aa9c55fb4f50ff96"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab743e9bc90c1f73552ec33e10e3331315acd2c397b36065b591b0181de533cc"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6f7deae3feb4edfa2efaf7c574fe88cbf055038a6abdb40188e4fff66d5699f"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15f024313246a4ed976c60f440bb8d257815513a681d212ff74fd46f7d715a90"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:823f82903d189af463d7df250ef1f7f696f3cee08cc8d91deb565e8d425f6506"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:01e93745f7f219b703b60ba7afead36cfc4242782be5af484673fc500df12da5"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:329fc3ccb63ad22d867d84c2adea759a64079a37ba4a343433b02c7a2816871e"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:bb57753e36e4855b8ca375069482250a6246372331a3e4f3407eaebb007443f5"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fce8cbd4997efeb450bd298b54f755dcdff18d496f7a5ddbb4867c6d7c88fdc3"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6c9cdde8becb25a7fde49924511aa2644d6f8081cc8df8e9452724303348d8e3"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9ac4444d8d4fd4c4bd08bf451ed3167aa9e7ec6cdb41b648794f1d1103652e36"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:f03ac127268b43ef4fe9e6ab6794a6794b49485a0cc0c1db79876d2f33f75bc7"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-win32.whl", hash = "sha256:1f5883d77fd409a261abb5dc8ccbe335720d798b1de4abb3b1d47ccbbc76b53b"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:c658c50ac0c98cd755a2dd50b7977d3bca7df401dcc47fbdfa87db53ef7d4e8b"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:4bea7f8ebe90bbd7f0e4a2de42ca6924ba23e3e76418c408ff82f1d46fabd687"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:fbc597639158fd7c14d55e808718848319540f51b0e6746e3eefa59723a4a348"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e71c909f353863b2b89c83de2ebed71ea6d0df8a6ef65a128193c5e650766bef"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7ac76cf9afd34929d76eb7fcb63be476a4853d8a96f0dcf2d0db68a0cbdf9885"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a3a370082ce34d0612f421e15fe011c53bb1feff21a26d06ad4fb244dab5a375"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:256dd4d85d9e4dc595e2bc983c980e73f62ddeb3165c58b4c3dfe78c5c8548c1"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:58d4aa13a59c969dbfdf9e6a9560e242cbfd9e8a8f50c2747714df1a423adf65"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0c6dfb5ca6723eeed15aa8e564a014d69fcb8812f94eef11fe3631e0508199f5"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c010f5581d9c612804cc59fcf7b524b707fbcb72828551237ab545bb5c7034af"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:52ec005752a56ae79547a05c0139ca2501a0c866390b6115008456b9f0e7cde1"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2bced4061f000f7187254a02ad3433ae17eaf991747ceea2f478422590a5bba9"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9eea3ab2597a5e65fe65296e2d6a84570845a6b55532d90333d740d48bbc850a"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:496846868fea80e479324862fa877f02411f2fd0f83b79ccee2607aa68b2a032"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:85d5855daafc240cc045c026d7a15fd198a09b0fc8ff6f5ecbb5297b509cb11e"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-win32.whl", hash = "sha256:58d3e12c88e0950bca850ae1f7c256055c097639c2edb9eb123af9807d8b15e4"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:acaf604462bf330b0d07e7a07c1d6e4adac79e5fb13e9c5140590542cafacc00"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:fdb8a068947befafba9952162645dc2fecaeb400e64584829ed5e9b2fbe21a7f"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:9085f87b0e38a2b92b8923059b4e8789fe40d9279712d15dcc670048d77079af"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2679de311c7946dde5d3b6f44941844133ff5c7cb86099c0061ab1e8901c20a8"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:baf3775a2635e5a11fbd5e4e64ee69c7e86875d224a5c72aca4c141064589a90"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ac8c94b6539074e0f40899301273ac8402b9b3e01c7b7ba269ff30340aaaf20"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fe532b3c966d1fb794e0698e4589d0444017ae77fc0b31edea13c0e35bcc449"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5c84bec0ab5ae0c64bfe73a7d2adcb5ce73b467523fc27fd6a28ab2aa6cbe35a"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:854066be00447fa8de2ccbbe893e2ffc4b123ef16d897af794c1e18bd4a714b0"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:21b82d8082f6f5e7f456ef0bd16323d08de1266efbfeb476e64b2a91d1471a4e"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:838648accb3a7fd9803fd45c87bce8509648eb0c11bc34e216141300977244f2"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:195ce897c6153c0700078142cf8efe3e6454ca4cf4357499e4078dfd83396626"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:978eab16f55b4ab2c2a745be9a0a840bf8f09a7f227d9c76eb30214d078865a5"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:cc0329df4caaceb950d2f580b5ac716a377f7059624a0bafaeaf8a218c6ed774"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:687c9ca3035544b113bea2055e180af96fb63c0c476e22a9180f51925186e7b7"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-win32.whl", hash = "sha256:706bfd38730a5ac7a365793269a00f4e988178cec121391f4248d84ad8c972e9"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:92caef967d287a407085d61176fce4012b1dd62daed4eb6d5ceb26d3d2538712"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:5fc45d653ea8c9a20479167e11d4a0f8cb2fa3470737ab6f9c827532313187b7"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:59171c6e45bf07d0d5cab3b0bf81d945035530f6873398b3b531c31184d46663"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9dbdd9205662134957cf0c324f639bdc5031c0ca056e2369e238db75187c0f11"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e4b018dc5a0eee4676e38fe84a47a427816c590b93b55d9025274ec4d6ffc2dc"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ced3fdd71aaa83ce593746c2edb42b7a59cb4c19c8b5c407781c72e493aae55a"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:19a3dd5aa73cef1c99687c4fc57db016a9c17104ae1185da88ba566a5d3bebe4"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc5d36d96478aa9c60654bd932525bf32964c62a7281eafdf16d85003a8d6004"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:04368edf83514385ffc3e1cfd4546e595f4f1272dd23ba437a93a9cc3741d47b"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:9b5db6052055d34d41230fb78d7c439c23dc536a9896f6cb039e8dd92cfc1263"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:252d099029bcbea642f2a06c4ed5046bdf8b5a8150b64afa5e027e88b106e5ee"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:6199d5606e2bbf2b096cf64d03f8b6790c91081d5ac866b8e7bb6422738cc60c"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:77efcff2b23071c349402ac1066667a3d011f62398d81408c9b88ad991747c9e"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:a5cbd90ecf0fc62e64726917ad083b73001f0563657a87ec3c0b504e277dc90d"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:4d26f14f041e83dd8edfd61f4cd4fa7285d31798b5bf1f28e70c367ba6c41d61"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-win32.whl", hash = "sha256:ac13b004224fb341e1e25a1ed5e19d32f57cdb2a403e01f003b46f051a550f6f"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:35aea775dc2bd5f54cd84a1cd2696cc3207c479cb9cf0bd346f0d343e4300ddb"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-win_arm64.whl", hash = "sha256:fb78f6e7fcd8ad785d28cd577168bc1aaee827b25bb8755638f694794ea98f0a"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:41876ee62a3dddf48ff1121ad8f0798032aa03f2fd35f21f34a4cab14f18d8d2"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6dac12ff6b846103483683f60c5f8fee205121adc58ffd87e90a90a3af69e99"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cee5dd7c6fb5dd52a0fe2a740f9bc6e3593f5f8b1788bde49de02086f30182b2"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:343fb4f2821043bd87095f7b08a1a181febc8e36ac64212143bbfd0a0e1bc235"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ae4a097991662cd4fff0ddc74e0fe7874f82e00042fa0ea00855645ed0c79598"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b599739b93b2cbeded49645ae3c8d1405c29ddfbceac1545c87a3f9580a9e96"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b39b69b347e5e47a3b5b8cfc005c68c1ba347474e3960236c4944a8ecd174962"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a2028475ba855475b8b4d3cfeb4994269c967aea8b9892dfba907f4263a863a3"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:36047af20e17097c3bb9476c2b7655f2f7aa51322c0ba58c07695bedf755a950"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4c4fb141a727957c93edfe5c32a26ceb6b5f6461d67146e2d39f51e16170bea8"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:2f293479cce755c75f1697e87c409b7ae4c555c7dfecb6e988ad13abba943031"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_s390x.whl", hash = "sha256:3588e376b3ea2eea84976f67273d679f229e24c66dce7b82ae45aef04ff6e072"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e199fb99720074809a7720f1c0b4d919eea8b87e88713e0f8f602f7bef543d9d"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-win32.whl", hash = "sha256:dd732602a7009217f658d5863d12d79d373a4de0eebc111094bcdd3bb8e0a6cc"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-win_amd64.whl", hash = "sha256:70055ff39b97c99e7ae40ea3e393fb62aa2e44dbd9b29f8d14f42fb0025c3959"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-win_arm64.whl", hash = "sha256:87e4f41d375c0b9be2fb5251aee4b8a689169e134535aed81bf085c3b647451e"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:85de3134b5379856e323ba37c19c9256d39425f7b76a63af52b09fb4664c2e8f"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3e5e1224c0a6a90e05843e07adfec669edebec17801c67072f51e59561d63c0b"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5e2d0e146dcb57034f8b97dc58d2d512cb90aba253960ce449f695fec6a82c6f"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e06efa066f7dbadbc84ebc126a97c452a6451dfcf589d89d788484949e1cf795"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:994e883d17c559cdfd38c84003c8b27d25424a1077272a17e7cd27bfe0bf57b2"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:789b8982559ae28dad2356519f841655756cdcd96616410590ae0b17454ee64f"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a951ad59cad9145664a730d3036b40b844e74d2d3683da40111463cd3a83845d"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:55261ac0d2941c42f196dd576f543d87a8ee03cd6f5e30dfb4d807b2e3b9121a"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5ca0555312ae2fe82715cada7fac375530c2f3349e1eaa1bcb33d0283ac79a18"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2e9cf9253119d8e5d111f05d71626786fd3d6193817316eab1ca088cdb8593cf"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:433c5a81eade63b47e522303bad236f59dba55ea6951746f5558355eeed8c75d"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:13e3afe97712e8887cd516e960c63f0b93122971e5b5e4b2622fe7701771e838"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb12fb2ba69ffa05f8695f61c69e591dc4b4a12ac3757ac8af8adb259bf56d17"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-win32.whl", hash = "sha256:56490c595a28b1bb27dfc583e816152a9767721ef58b2c03b13f954d2f707420"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:012a22b88a77ca2e59b98ac5889b0deb604147666032f45e6d6e217634d2550d"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-win_arm64.whl", hash = "sha256:29880d17a8eb0b5cfdfd8944b468322928059aa35f1f5fa8ff22b149ec0b42f8"}, + {file = "charset_normalizer-3.5.1-py3-none-any.whl", hash = "sha256:6df0ec430f9a831772c23ca5a224cba36517a58a84bb32c32bb59a9fa67c47f6"}, + {file = "charset_normalizer-3.5.1.tar.gz", hash = "sha256:6117b84ea48435e5356dc737f5121485c30920ba43375fa7b434fd753df0eac3"}, ] [[package]] @@ -798,19 +886,16 @@ rapidfuzz = ">=3.0.0,<4.0.0" [[package]] name = "click" -version = "8.4.2" +version = "8.5.0" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.10" groups = ["main", "dev"] files = [ - {file = "click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76"}, - {file = "click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6"}, + {file = "click-8.5.0-py3-none-any.whl", hash = "sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360"}, + {file = "click-8.5.0.tar.gz", hash = "sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34"}, ] -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - [[package]] name = "codacy-coverage" version = "1.3.11" @@ -836,112 +921,142 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -groups = ["main", "dev"] +groups = ["dev"] +markers = "platform_system == \"Windows\" or sys_platform == \"win32\" or os_name == \"nt\"" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] -markers = {main = "platform_system == \"Windows\"", dev = "platform_system == \"Windows\" or sys_platform == \"win32\" or os_name == \"nt\""} [[package]] name = "coverage" -version = "7.15.0" +version = "7.16.1" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "coverage-7.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:50913d4bf5ddafa6ca3693da5e4dd833dd1b772e0283c99ca7f7d287db67331a"}, - {file = "coverage-7.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:359e141ccd33893ce3f1ad5525f8b96083003677c82182e5907d62d4ea5799fc"}, - {file = "coverage-7.15.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3200b6204935f928c64b2ca1f923ab8c1acb7c9de45ec61569711b34d25cccaf"}, - {file = "coverage-7.15.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:be616bf61346883b2cfdc5178669647e03531d81ab761a7e378558b7e8bcb628"}, - {file = "coverage-7.15.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc7bafc3fe1059463a8fdd97ca79972d6e2bf819d775c7d54991b5b1971201d6"}, - {file = "coverage-7.15.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b713aa7fcf325a01d4184d848acb46fd84f78fdb0978470c636b23a06a753d91"}, - {file = "coverage-7.15.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e38e6fba2d56652fdfaf0231f8f78aeb805234a912de25dc291ee5cce5b8faa4"}, - {file = "coverage-7.15.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:884499f42e382675be80770391983b90e0c0c774d87dbeeebf5f991cf6612b20"}, - {file = "coverage-7.15.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:840481b12e083dbcbafab14794a8781a958edf327c8d3d70b4eee42f9b8253aa"}, - {file = "coverage-7.15.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:276646e9481703d09f854f3b2f018f24e19fd7049ae670a92570043eb97203b1"}, - {file = "coverage-7.15.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4de4b4d3f5545aa6c60dc4efd9c63b5b5dcc3bf00fe83146b2bdfffb8f6613bd"}, - {file = "coverage-7.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5c504097b2a89b1e85bc6070d920df77daec701337e3aeef2c17775a5dd0ca90"}, - {file = "coverage-7.15.0-cp310-cp310-win32.whl", hash = "sha256:f6e80ed91f98316e86b9c137206b04b2bcfbffccbdff49bd2eb09dddb1cf14e0"}, - {file = "coverage-7.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:b3b3e22030f3f6f5e01a5ce69936552a5c0f6992b7698777377b99041961031f"}, - {file = "coverage-7.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:003fff99412ea848c0aaebcc78ed2b6ce7d8a1227ed17e68470672770b78a02a"}, - {file = "coverage-7.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5cbd804bf2784ce7b45114516050f346ecd50f960c4bb630a7ee9e1d78fa2118"}, - {file = "coverage-7.15.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8773e15c23305b58882a4611fb9b2755977eae0dc2e515366a1b6c98866cc4c2"}, - {file = "coverage-7.15.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f50e40081494c1dc4239ebb202014cbcc3306ea96fb6302a34c8cc0967fc5ae8"}, - {file = "coverage-7.15.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:daf96f37f5fc3a7b6c6da862eb4aee61c426bd63da236ed4a73ef0e503b4bca5"}, - {file = "coverage-7.15.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:51aa20f6ae2788fd197747766edf4cd8234fd9423309b934257fa6b21a592723"}, - {file = "coverage-7.15.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03d1f922757662eb7af586e77834792274cff776bc7b1d1a0b66a49ea9d84735"}, - {file = "coverage-7.15.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a6d6acc9a7666245e6133dd15144ca038a85a9cd5026bb06d6bbae9e77440dc9"}, - {file = "coverage-7.15.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1ac2c4c27c7df851dc9a017c2d7de00b69147e84ba3d96f37a530b0b6fb51035"}, - {file = "coverage-7.15.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b761a1d504fd4bd1f20f418753964dca9f5862a511fc854dac58296b3b223671"}, - {file = "coverage-7.15.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:e43b045e11c16e897895758ae90e4a90cf99e93d58549e2f90c0e2272e155695"}, - {file = "coverage-7.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:589b54513e901739f4b4582c705ce96b80c96f57641b1464607e2367a270e540"}, - {file = "coverage-7.15.0-cp311-cp311-win32.whl", hash = "sha256:106781b8482749162d0b47056937ba0933508e5d9447f65a5e7d5c422f0d6bb4"}, - {file = "coverage-7.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:821e92b3631d762a339695824cadbbc73020354eba2a23a551a99ad34938fbe6"}, - {file = "coverage-7.15.0-cp311-cp311-win_arm64.whl", hash = "sha256:309990eb5fb8014b9f67cb211f7fd41876ec8a88a88d3ae76de0ed1d611e3640"}, - {file = "coverage-7.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b75ee5e8cb7575636ac598719b4307ac529ec8fcd79608a35c3cd4d4dada812d"}, - {file = "coverage-7.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffb31267816b93b075302248cc1737506081b4f163df4401e9df1a6424aafabe"}, - {file = "coverage-7.15.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e4d0bb73455bf97ab243a8f12c37c686ccf1c13bb614b7b85f1d062f06f42b2c"}, - {file = "coverage-7.15.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:20d9ccc4ebd0edc434d86dfd2a1dd2a8efa6b6b3073d0485a394fee86459ebb4"}, - {file = "coverage-7.15.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20c8a976c365c8cb12f0cbd099508772ea41fb5fa80657a8506df0e11bd278c5"}, - {file = "coverage-7.15.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f948fd5ba1b9cbca91f0ae08b4c1ce2b139509149a435e2585d056d57d70bf01"}, - {file = "coverage-7.15.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f58185f06edf6ad68ec9fb155d63ef650c82f3fbd7e1770e2867751fb13158f4"}, - {file = "coverage-7.15.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:02adc79a920c73c647c5d117f55747df7f2de94571884758ce8bc58e04f0a796"}, - {file = "coverage-7.15.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6eb7c300fbed667fd6e3588eba71c1904cdb06110ca6fdf908c26bdd88b8e382"}, - {file = "coverage-7.15.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b5fb23fa2de9dce1f5c36c09066d8fcda16cd96e8e26686caa2d7cb9b567d65c"}, - {file = "coverage-7.15.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:cec79341dbe6281484024979976d0c7f22beae08b4a254655decd25d42cbe766"}, - {file = "coverage-7.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c664c5444b1d970b1b2a450e21fb19ee5c9cfdf151ded2dda37260031cca0da"}, - {file = "coverage-7.15.0-cp312-cp312-win32.whl", hash = "sha256:5f764a3fa339bde6b3aa97657f5a6a3a9451e4a5b4ea98a2892c773a43525f77"}, - {file = "coverage-7.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:52f9a4d2c4c56c8848bc2f524916698354b0211488b38c49ad9ae54f6cafbff6"}, - {file = "coverage-7.15.0-cp312-cp312-win_arm64.whl", hash = "sha256:31e5c3e70c85307ea35a12964e2e40f56ca2ee4b1c8c721ccf4609d17071080b"}, - {file = "coverage-7.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5be4caf3b28836f078abe700f8944dac4a65d78f16d6c600c89cb624e5535782"}, - {file = "coverage-7.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dd58ad1404704303ca8d4f4b8a1095e7cbc7040ef17a66df1e6619aa10176430"}, - {file = "coverage-7.15.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bbcbb317c2e5ded5b21104af81c29f391be2af98d065693ffbe8d23949b948e5"}, - {file = "coverage-7.15.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:27f31ecb458da3f859aab3f15ada871eb7a7768807d88df4a9f186bb17737970"}, - {file = "coverage-7.15.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fb759be317fdc62e0f56bffdf61cfcb45c7761ad6b71e3e583e71a67ae753c"}, - {file = "coverage-7.15.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d5cf007add5ab4bb8fa9f4c77e3732127c9e6cad501d7db43355fbfafca0be84"}, - {file = "coverage-7.15.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cc78d9843bd576fbe2118248258d485e968dc535f95ed504a7b0867ba9b51389"}, - {file = "coverage-7.15.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a263060f1de0b4b74b4e089c2a70b8003b3781c733329a9c8fd54995328f9950"}, - {file = "coverage-7.15.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c48decf16e0dfd5b049c7d5e82200c23c08126719142998d4f172444e3d0529e"}, - {file = "coverage-7.15.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:08fb028000ed0aaa0a4cbdfbb98be7cb42f370db973fbbb469733505ab20e13e"}, - {file = "coverage-7.15.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fb7dc0c3b7d8a1077abea0b8546ebc5e26d6ef6ecefc2f0f5ad2b8a53bdad837"}, - {file = "coverage-7.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6cb3602054ccbe9f0d8c2dc04bbeba90d5719236e2cd06e042ddd6d3fc7b6e37"}, - {file = "coverage-7.15.0-cp313-cp313-win32.whl", hash = "sha256:0bf781da64326b677be344df505171435b6f58716108606621d5d27d964fff8b"}, - {file = "coverage-7.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:2c57a275078ee3fa185f83e400f765bc764a549de66d99b47881645cbd4ea629"}, - {file = "coverage-7.15.0-cp313-cp313-win_arm64.whl", hash = "sha256:3812c61afc6685c7999b39320779ab8f43b7a3081fdb0def39976e56fbdb9a21"}, - {file = "coverage-7.15.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:41cb79af843222e11da87127ad0ecbfa878abadd0f770a4a99391a27d3887324"}, - {file = "coverage-7.15.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7d2008989ef8fe54188d3f3bfa2e3099b025af11e90a6a1b9e7dc433d04263d8"}, - {file = "coverage-7.15.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:769e8ece11a596315ebf5aa7ec383aeeed016c091d2bf6363ffb996d41529092"}, - {file = "coverage-7.15.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:65a6b6164ee5c39e2f3803f314292d6c61a607ba7fee253d1e03c42dc3903502"}, - {file = "coverage-7.15.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:75128817f95a5c45bb01d65fd2d8b9cb54bbe03d81608fb70e3e14b437ad56c2"}, - {file = "coverage-7.15.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9887bb428fe2d4cd4bee89bac1a6c9932f484afd5b36fbd4ff6ea5f825bb1f5e"}, - {file = "coverage-7.15.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0bfc0be1f702042207a93a00523b1065ee1fe951e96edf311581c0bbc2e34888"}, - {file = "coverage-7.15.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f64627d55def5a43282d70e08396672692f77e4da610a5bb8bb4060b432b6859"}, - {file = "coverage-7.15.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:2c6f0fa473003905c6d5bac328ee4eba9fbea654f15bc24b8a3274b23363fa99"}, - {file = "coverage-7.15.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2bcf9afaf064172c6ec3c58a325a9957ad1178c05dd934e25f253321776e0676"}, - {file = "coverage-7.15.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:baf06bc987115d6fb938d403f7eab684a057766c490367999a2b71a6883110c6"}, - {file = "coverage-7.15.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f0405f2ff97b1c4c0e782cb32e02f32369bcf2e6b618b591d67e1ea754575dfe"}, - {file = "coverage-7.15.0-cp314-cp314-win32.whl", hash = "sha256:ab282853ed5fbd64bbb162f19cb8fcb7087187508a6374b4f9c34ec1577c4e8f"}, - {file = "coverage-7.15.0-cp314-cp314-win_amd64.whl", hash = "sha256:3bb3040e9f4bbe26fcb0cd7cc85ac63e630d3f3a9c74f027abf4caa27e706663"}, - {file = "coverage-7.15.0-cp314-cp314-win_arm64.whl", hash = "sha256:346771144d34f7fa84ec28386f78e0f31653f33cf35e19d253d5b35f9e8201da"}, - {file = "coverage-7.15.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d34a010905fb6401324ba016b5da03d574967f7b21ce48ea41e66f0f1f95f641"}, - {file = "coverage-7.15.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bb25d825d885ca8036795dacfc3924d33091fc76d71ebc99420c6b79e77d96fa"}, - {file = "coverage-7.15.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:94c9686bfe8a9a6810297aecbd99beaa3445f9e8dc2f80b1382cca0d86b64461"}, - {file = "coverage-7.15.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9bd671c25f9d85f09d7ec481d0e43d5139f486c06a37139847a7ce569788af72"}, - {file = "coverage-7.15.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:110cbdf8d2e216577312cf06ccf85539c0e5a5420ef747e4a4719b5e483c88cd"}, - {file = "coverage-7.15.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2c5d4619214f1d9993e7b00a8600d14614b7e9d84e89507460b126aa5e6559e5"}, - {file = "coverage-7.15.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:781a704516e2d8346fbbd5be6c6f3412dd824785146528b3a01816f26c081007"}, - {file = "coverage-7.15.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd4a1b44bcb65ee29e947ac92bbee04956df3a6bfc6143641bb6cae7ede00fc9"}, - {file = "coverage-7.15.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0e4950c9d6d3e39c64c991814ff315e2d0b9cb8152363594212c9e55208c0a8f"}, - {file = "coverage-7.15.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:fe9c87ff42e5472d80d21704972e1f96e104a0a599d77c5e35db5a3c562e2571"}, - {file = "coverage-7.15.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f00d5ae1dd2fe13fb8186e3e7d37bcbd8b25c0d764ff7d1b32cef9be058510a8"}, - {file = "coverage-7.15.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:363ab38cc78b615f11c9cac3cf1d7eef950c18b9fdedfb9066f59461dcf84d68"}, - {file = "coverage-7.15.0-cp314-cp314t-win32.whl", hash = "sha256:54fd9c53a5fafff509195f1b6a3f9be615d8e8362a3629ff1de23d270c03c86b"}, - {file = "coverage-7.15.0-cp314-cp314t-win_amd64.whl", hash = "sha256:87b47553097ba185ed964866078e7e63adea9f5f51b5f39691c34f30afd21080"}, - {file = "coverage-7.15.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aeefb2dd178fe7eee79f0ad25d75855cb35ee9ed472db2c5ea06f5b4fd00cec5"}, - {file = "coverage-7.15.0-py3-none-any.whl", hash = "sha256:56da6a4cbe8f7e9e80bd072ca9cefe67d7106a440a7ec06519ec6507ac94ad19"}, - {file = "coverage-7.15.0.tar.gz", hash = "sha256:9ac3fe7a1435986463eaa8ee253ae2f2a268709ba4ae5c7dd1f52a05391ad78f"}, + {file = "coverage-7.16.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4f12a9e27ca7b65e40a8475d27899b2d45064d9020e6a89148939e01987b5853"}, + {file = "coverage-7.16.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f590d46c30d9e4c1fda3efefe5443f4c2f6a4192c5ca2ba403653e9ebadf097"}, + {file = "coverage-7.16.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3df82f0a3cef4e1bcfc799436056f0b978dda319d0bfd4460c6e479b2802d98a"}, + {file = "coverage-7.16.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb462d59146656e278d1e8ed913ce374d0ba68a4e081acde1867f6d3377fc881"}, + {file = "coverage-7.16.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7db888dd0a1df1a653cae7f99d4047430d2187a3626eda51bc847b0fd6b9b43"}, + {file = "coverage-7.16.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:38a7e16f061504ac2b45370bf5bf97e8250d8d3f25e37385bae884978166554b"}, + {file = "coverage-7.16.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2b26b55b18e1a53e1a159dd743728c4ddbbef28ba19000a91aaff5ce023197ec"}, + {file = "coverage-7.16.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fdb2f528b50953e29d22033b3256c396a700193c6e45b2490222ef9c333cbbf9"}, + {file = "coverage-7.16.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3284754371dc78592aa3ae4d661d30ee20e02b2b6b0de3590a181a936d0d3b38"}, + {file = "coverage-7.16.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:802d1246c540e07486d4ee1adfa19a797e4b33529ffc371dc140644e8f27da0a"}, + {file = "coverage-7.16.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:6fc735d6fe6d57f803e7ba021be4dde48e43e6aff94e6954350555d5332b0594"}, + {file = "coverage-7.16.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ed5ade1bb18f62edace1bd198c66f9d4c75a8385d5fd24e87d917ea1a5958773"}, + {file = "coverage-7.16.1-cp310-cp310-win32.whl", hash = "sha256:0c309096926b119543dc16438a11ef4c80783d2f4e59ff94f7f462651a944cdc"}, + {file = "coverage-7.16.1-cp310-cp310-win_amd64.whl", hash = "sha256:7562f8067ed9360e8b9739e5703403a7686dd1b36bf0f89fc538047c54cdea90"}, + {file = "coverage-7.16.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:72e013665e25cf9d44779f01f340af26319756f9a76822b7c94ce6b1d93813da"}, + {file = "coverage-7.16.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cb05c0ff98b56ba6969adf35556bc43bcb8d094df8bc9cb403acff53460c4e07"}, + {file = "coverage-7.16.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0d0ececb32090e3fbb03e0d352b973a0485879b4de6c58daf47227b9988b99e5"}, + {file = "coverage-7.16.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f0ba3892d81aacf36996c52f16bca04e39af31a6c5de930b7688ab617f4a6475"}, + {file = "coverage-7.16.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6410b75fe07d5271eaa95fc24bd0a9177ed588d9d1c10c0cf67829adb8f0567"}, + {file = "coverage-7.16.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d1039cb2de093225d597109342ce1675626bd127565e80b3044f4eca07c15b2e"}, + {file = "coverage-7.16.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cf047bc39fde5425be2628666d0f435ed8817859111c3aacc84b32d858069f5d"}, + {file = "coverage-7.16.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c05913d0d5badf7ac83200f35dcf9514cce5df16a7cc89e7d1d7fff0461813b"}, + {file = "coverage-7.16.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4027bf6d7bc0a16df058ce913b69f10c5687f8e1ca668f08caa659ce101744bf"}, + {file = "coverage-7.16.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4f48b345f831eaf4402ab6333c2dc3e2e2b5bc7b9c1b8fe12680dee3f0538f01"}, + {file = "coverage-7.16.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8643baeb590726c558b2faed6cd59b0917480f9367fcc692026f1a86d824fd08"}, + {file = "coverage-7.16.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d06dcc420b570bf683cdb647cc8fe62b672d9e429ef711c3cbbb7a6880ca1572"}, + {file = "coverage-7.16.1-cp311-cp311-win32.whl", hash = "sha256:946f58aa59b08bcd6afcc6a7bd0ff54ed5eee844f32ad69fe6814836d15856a2"}, + {file = "coverage-7.16.1-cp311-cp311-win_amd64.whl", hash = "sha256:684c7ee9b4c04358fe6ac8b517ab51ec35fcd79d08ff0f105dd8bcd96885bbb7"}, + {file = "coverage-7.16.1-cp311-cp311-win_arm64.whl", hash = "sha256:1b24f79e25bcf6c73931aeca7a3dfc7595c0cb5e9364aba3fdf387a3de4b1c22"}, + {file = "coverage-7.16.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b7f2c26ce6ce0b1e0ca0d5fae96ea510e3a2e78b7207f06e76b7f2c87fa3d0af"}, + {file = "coverage-7.16.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070acb9da788dff743a4d36fc015feee12d68f0349959017542017c79f59c21c"}, + {file = "coverage-7.16.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e366587b370bc9b8b51b7b7272c610c56db5d5b4795b9e4a29d28ff2f440f809"}, + {file = "coverage-7.16.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e82e10b9d290f60b63459cfb245a841aec347603997206296b93881463a93dcf"}, + {file = "coverage-7.16.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d73bb1f85c4150ac208fb0755beb04b2e44897bad81414de9380f98dd74729f"}, + {file = "coverage-7.16.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3397b9032553d281ad6a9253b12675b65e0cc8cd7a3b0633cf48872c9eb13360"}, + {file = "coverage-7.16.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:77890395cf37026a5907d3ad32376aa51f41c0f163b7477fdbd4f94966cc1d08"}, + {file = "coverage-7.16.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b0944dc3bee3091039bf970d73caaf930c906013128a421bdc132e797494d941"}, + {file = "coverage-7.16.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b89d22a89d5bc05dd95b64e08295b8394aa96dc88e08f8ba210c9ebfebbe0489"}, + {file = "coverage-7.16.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:550a2a1faf7559f13d5344f12d1eb886ad87955155d7dfab2a3fe5c8ec8fe776"}, + {file = "coverage-7.16.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:55eb268e5b81aefac759766c9162625b06c1bedb7b77d936225bafc4f038a6f6"}, + {file = "coverage-7.16.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:65a8fc80898c9ce59f04349fe8b4849b1f9787f14e52ead990e5f849ff4727a0"}, + {file = "coverage-7.16.1-cp312-cp312-win32.whl", hash = "sha256:528a61be40977c340cf201d23b69bd6a6bab507da60e9dbda85f8b30e935d70d"}, + {file = "coverage-7.16.1-cp312-cp312-win_amd64.whl", hash = "sha256:d0f02c633630e2b74522108ee95a84ad6e1204a8016a6cca5297f335ea27147e"}, + {file = "coverage-7.16.1-cp312-cp312-win_arm64.whl", hash = "sha256:2959978f9d1d20a2c0c15d0a68baaeccf615ac1aa214cf4a05a10d6f568926c8"}, + {file = "coverage-7.16.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ee5465db6e9152a7d09f3215309326878c6aa3ac509195a369f9d264ff4bfbd9"}, + {file = "coverage-7.16.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2b8256f8b525ba233d2e4cdcdce0d6673c66fc9bf70df1fd5e67c54a74e2d245"}, + {file = "coverage-7.16.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d57cc400275b9a2892e905fc893f732b21ddb95271bf96406c88e2f6367848b5"}, + {file = "coverage-7.16.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6b3fd0f3435ebb7a7183b32a6062a8b755f08242ced1f3f22761d30b56b3c2a5"}, + {file = "coverage-7.16.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e5eb1762e7eb5fad34ef913e8107c7788a66f19d328e598ce95bf7217f9e5c8f"}, + {file = "coverage-7.16.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:46cd3a73e9140410de62cceb66214bce0e08fb3922b9176fbfc1522fec151b41"}, + {file = "coverage-7.16.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d1ba5142d68dd2cb775cbd0ac8601819298152047803c8efe4eec6d7d7aa7878"}, + {file = "coverage-7.16.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c389c6f9d1d518e1249ddcb8a7f158135644ce2c508fa6cc17b680777dad5bf2"}, + {file = "coverage-7.16.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cdc57746c7ac0ea063351b4d651c3bb4dd4fd35e64dbb8e90c10e14eb03c4080"}, + {file = "coverage-7.16.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5597180ed7670cc94c04c65347418a467d3a43d5f0cf52fcac647f5425f42037"}, + {file = "coverage-7.16.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:a9647a0ac46255b8fef59a433a2161f03e5483f3a35e1cbd9dfe4600baff0c6b"}, + {file = "coverage-7.16.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e306e98186b9cd109121f3583aeb7978797ad21d948f22944c5c08845cd554d0"}, + {file = "coverage-7.16.1-cp313-cp313-win32.whl", hash = "sha256:48a78a66fcce49d7f6156524bf979c0ac633d584199717c68c6ffa949fc14e6a"}, + {file = "coverage-7.16.1-cp313-cp313-win_amd64.whl", hash = "sha256:7af03247d598a353bbbbe1b925deb735276e4d845e7197c4073dc89352b236fa"}, + {file = "coverage-7.16.1-cp313-cp313-win_arm64.whl", hash = "sha256:166adae25b05b04c9a84135912066d9c97482115af38df1a419a38aacc6b6f5d"}, + {file = "coverage-7.16.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:cc0b37fe6f5ce5f1ccc62ad4fa9b1ad201d8e9b6027fd5e0170877beee4b2d15"}, + {file = "coverage-7.16.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6618f481053b63fc6121faf8fc676bd9b7163c2a19d9e984a2e850002c28ab57"}, + {file = "coverage-7.16.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fa02d561eb1d8d2f8ba43ba6e3cef4c6c402a3b632a9460fa329fcadcd5df6a3"}, + {file = "coverage-7.16.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bc5354a124799f1f87b7637bbe6f18cd4bc66a1f37f6aa2b5db40f9adad531dc"}, + {file = "coverage-7.16.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34bafe9f4094315248573e6223e11af0ec1b25f9cbca43bf0e9a26a189ba2751"}, + {file = "coverage-7.16.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:29c4d3e32a3b5efa420a3dc627c7e570deb80ef997def52c7686a474f5edc7ab"}, + {file = "coverage-7.16.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2066c447fdd0bca39a9633a082d8ce67bf9a539a203b85059a364a405dc9fe9"}, + {file = "coverage-7.16.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fd8ac10cd2458b3c6343aac082fb9bd0e3fa806cb2c4975f2280153474b88412"}, + {file = "coverage-7.16.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9d8c54ec32e5c102b9241f75d88ae26538b53662868ca491736611db448d9c7a"}, + {file = "coverage-7.16.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6dd8dda3402a01a1a8fe8b753a282466f615128574a5590a9108acd07b1f8540"}, + {file = "coverage-7.16.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:79afa9726438912e5cddd1fe541815cea9763c92935f594835e4c432565b68a9"}, + {file = "coverage-7.16.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3db3978211c3cead5437a80136ca0556bab8bc7828de15a762884b0598c41361"}, + {file = "coverage-7.16.1-cp314-cp314-win32.whl", hash = "sha256:49c39c7068a494f8eb427155f5682f44feee43f9b3107fd54b1e52465379c54b"}, + {file = "coverage-7.16.1-cp314-cp314-win_amd64.whl", hash = "sha256:c510dad19552d912058e4c3e3cbec3fb155dbe8d0ce0ceb7e7dbf5c5822bae0b"}, + {file = "coverage-7.16.1-cp314-cp314-win_arm64.whl", hash = "sha256:b7d4d7e6dcaf33e85f1919f03346403bdcc27437c420a78835f3805bca0ab71f"}, + {file = "coverage-7.16.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:3d0a3681c12d3e0bcdea3d9414b04087828d6c1a482802d6f7f42c37ed530152"}, + {file = "coverage-7.16.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3f3b4469d3da3ecced775d1a8c9c5d9fc80f259e30b7b89f9fed0700d6035ecb"}, + {file = "coverage-7.16.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c08ae35c1be2fe1ce4b4c628df5c6fc0dc9a87f8e5fe8e20238d249678984741"}, + {file = "coverage-7.16.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8ee71a38c54bb2676bbe762b8b0943a79ccb1c2fd6a52054f66e63eda392f8c1"}, + {file = "coverage-7.16.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76491917771f179f9772efe218c5ccc65950dbdb35f4439298d8a8dfc6ec1f72"}, + {file = "coverage-7.16.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4aa0b0a6f81fa3deb211e643f6954e78b4376b62b9c218271236cfa757664e8"}, + {file = "coverage-7.16.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:756ba2d96d073c5a2a55d67fa22784763710fadbe22c41adde2d9cfa4dd78a8c"}, + {file = "coverage-7.16.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:99bf9ea435cefcefd220f8687c3ddbbf78dc2de0bd11b57c3ae9fbbdf8d5561a"}, + {file = "coverage-7.16.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:35cbc81f937fc402971df45c897d2df2bfb2014efcd990360032aa0a651635da"}, + {file = "coverage-7.16.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:8fae08e85b334ac6ac886002b5041396a31bcf805225bbe19847627203da99e2"}, + {file = "coverage-7.16.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:83362b64e215ef00b0ba33fcf13655ace6c9fdd144d5ad2ab59ac86c2daf166e"}, + {file = "coverage-7.16.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:33300f2e140ccf26af3d8152e62bff71993f9310cfc63ba7a20940b0d246a0ae"}, + {file = "coverage-7.16.1-cp314-cp314t-win32.whl", hash = "sha256:5539304fdbb2cc144df684d35a33b81145334d23e1c2367b5a923d25107f70b2"}, + {file = "coverage-7.16.1-cp314-cp314t-win_amd64.whl", hash = "sha256:715dcb72c3280c428c3a20134b87e42c29acec9669136e899ab2de69ca86218d"}, + {file = "coverage-7.16.1-cp314-cp314t-win_arm64.whl", hash = "sha256:dac8b84c03e6029d272b8249c77018db83de59ca009a9adef7c144b4a62ee5e6"}, + {file = "coverage-7.16.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:a337dc2d54c74430cd2febb8ee04f7c508ba8b3b412bf0463f077a66cfc73743"}, + {file = "coverage-7.16.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:3acd1d78397dead78dd1b011b5fc19cc823c190349acd549e63856dff649c80e"}, + {file = "coverage-7.16.1-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:73a32694603a34ad01d7e51a481a4023410d8099e1d0757e067945695c10f0ae"}, + {file = "coverage-7.16.1-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fbbe8265736659a6be2e6042b6a35be13545d14b243cc1d7ecf65f90d788a370"}, + {file = "coverage-7.16.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b0359eb4c62f9993e176bc8f50450fc736a6b90dbcc05bb8584e948812699ae"}, + {file = "coverage-7.16.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:da506e669a8a851b59e122b4b219ea70996a6296f44f3a9348a852526ff961de"}, + {file = "coverage-7.16.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:64a2a5985d81810ed605ff0dc4ccd6555efcb5700353825532a9a0aea65826e1"}, + {file = "coverage-7.16.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:66d70132b69b861805dc1ca46cdd733e54c416890e8f1371d2fd103f70b59c9c"}, + {file = "coverage-7.16.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:db651a9cf325a542bc2b7b8cc8f1b2bdc6492739bae3103731b2f1c85b96cff6"}, + {file = "coverage-7.16.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:4184e78a4465dcda359fb403172b8951dd220929cb0984c02fabca1742fff06f"}, + {file = "coverage-7.16.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:bc53c3f3adaa939b7a063533ffe0ae1259e7073393c618043a99a6970a87e3df"}, + {file = "coverage-7.16.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:b44308854ef210b9b78df9cdfd4e159513382a859f5ef8464306d14a54c2a040"}, + {file = "coverage-7.16.1-cp315-cp315-win32.whl", hash = "sha256:dccc142614d3419ed71857deb43f1d757829a4c7fce9994464b71e7e38309827"}, + {file = "coverage-7.16.1-cp315-cp315-win_amd64.whl", hash = "sha256:961fc424e9d5229a99f8f1189942d8e7f4e1519147c3af64842f944aca03914d"}, + {file = "coverage-7.16.1-cp315-cp315-win_arm64.whl", hash = "sha256:681a9488c5a234397c4f013da065aa9e53eb7af4c78f1f80c6f15e7208acb855"}, + {file = "coverage-7.16.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:8bb09a2d19b04db1fa0e087a7ca4f12458f7e0e7364cfcd838441d86fb1c61f6"}, + {file = "coverage-7.16.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:2270a794600b635ca9452ce4c32e2fe81a35f9caa17ffac0eba99f14f275bd4d"}, + {file = "coverage-7.16.1-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a4eff405b545dfcf79cf0d9d3ff750e5c5a887aa066114175193a81d249c5ee6"}, + {file = "coverage-7.16.1-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ee1d5fc9e3bd6a217906929cc97880239a91d20dae7746f538eb0eefee705ab1"}, + {file = "coverage-7.16.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4ec944947de098ad5a1738413f9364689a57067ecbc328e9de37218aa1e5cc1"}, + {file = "coverage-7.16.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3f73ee3956fde2d461c9e2955dd48166e4821fc8587d135e8780fb84da2a098b"}, + {file = "coverage-7.16.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1ec9a4ee989c0d06ad95add0dbfdbb72b00ef53f43431ca0b612384e7878e5de"}, + {file = "coverage-7.16.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:7e5727b2508f817f3126d6c33327dda32fe69d15514badfcd61db8bc4209ecef"}, + {file = "coverage-7.16.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:19a3ea2f364012ef06678118fffdc92442a16bef4a5c8ad4f4019dd8f9ac8876"}, + {file = "coverage-7.16.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:996c2b891b441ec2b39725ee3e8386e2f11b4894b92be225fdfd54a3eeada2c8"}, + {file = "coverage-7.16.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:a125fac1f6b1e88488d208a86b578e1790e3c4937f2e1568d23356141d236220"}, + {file = "coverage-7.16.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:b10095528b866d322d33d6bf1709b7f8cbf959f12e8cb2ba22fc59c8717866b0"}, + {file = "coverage-7.16.1-cp315-cp315t-win32.whl", hash = "sha256:531d9be377fdcc05593b974656872eb82e808ebeb42a72515e3aaeb8bb7166f5"}, + {file = "coverage-7.16.1-cp315-cp315t-win_amd64.whl", hash = "sha256:46a88f51770df7c9bc376bd57d3f86cdc7624b8e16ac4b585a655c22b7a1b4db"}, + {file = "coverage-7.16.1-cp315-cp315t-win_arm64.whl", hash = "sha256:7580432cbe1e8b762660ae5806f04f869e1c02e519836a43f8094437e561e9f0"}, + {file = "coverage-7.16.1-py3-none-any.whl", hash = "sha256:3d8bd4e58b6a5c2018d808f297905393c6c61da466a48c3f0596a76a4900ebe4"}, + {file = "coverage-7.16.1.tar.gz", hash = "sha256:f83981779bcf9dfa06fa0a8d4cb43e0faec1706328ce07aa3e7b665b4ac0f210"}, ] [package.extras] @@ -1133,54 +1248,68 @@ wmi = ["wmi (>=1.5.1) ; platform_system == \"Windows\""] [[package]] name = "dulwich" -version = "1.2.10" +version = "1.2.15" description = "Python Git Library" optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "dulwich-1.2.10-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:07f81ec4773c89d5989d173a077ed54b400719b4990b2f46a7b5604f16da46d1"}, - {file = "dulwich-1.2.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:afb850b4f6ff6f1c7178bfbefbf079a0d0d03014bb385929a0934732ac7dd0c0"}, - {file = "dulwich-1.2.10-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:86c81a30340ca15aa1d275f42abc1f297e0af2a2d6cc213b5e9e40f52c61ff4f"}, - {file = "dulwich-1.2.10-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:51d97aaa382b6e9ee0adffcd0028c430ff133be4d6bbff53ee1aeedcb3fcff07"}, - {file = "dulwich-1.2.10-cp310-cp310-win32.whl", hash = "sha256:93299fd523892d39f69e08bf71984267798d76cb914163c48c998181891a24a2"}, - {file = "dulwich-1.2.10-cp310-cp310-win_amd64.whl", hash = "sha256:f91eb2908d900288da261f05fb436a384fc94b30980869a2db4db7b52b30047a"}, - {file = "dulwich-1.2.10-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f6d989605955bfbb678425a2e8710100ef35960340bfb55faaca902006c69163"}, - {file = "dulwich-1.2.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af4a750597d1a6e77bca74b9246bf84b62a9bfa7e48783af4ed0e9f04f0dbfbd"}, - {file = "dulwich-1.2.10-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:6c715289e8c5667978ff28a1847736633766ca1c7082144ba0ee95e459be3f97"}, - {file = "dulwich-1.2.10-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:f21c7f2dc4de8353b59b82bc0245838f3f94ff9332b71f6394642e3bb012c71b"}, - {file = "dulwich-1.2.10-cp311-cp311-win32.whl", hash = "sha256:ca1fbe5f4b2ee48154b24c68eeb689b848394a857f7cb3dea3de0b86c464454b"}, - {file = "dulwich-1.2.10-cp311-cp311-win_amd64.whl", hash = "sha256:9874964ac78713c640c42c9dc27a74dd2d4169d99821a57301d37e5673409d27"}, - {file = "dulwich-1.2.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4bf4671061aa98e7f4d420fe5238b2ca2cb2a00a280c8f549fa92d34569971a8"}, - {file = "dulwich-1.2.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:43315f1e95f7d5c1a6f2695f5534263b80678ccbfd9756101f07759a4e65850e"}, - {file = "dulwich-1.2.10-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:699edc94fddd0a2346e41e405fd7c537072d75cb9ddb39bd47f2e8fbd56d1e4e"}, - {file = "dulwich-1.2.10-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b2dc7c46cfeb40022d2fca9d23c3ca1aba602ee99bfb6554278f05f2aa890a0e"}, - {file = "dulwich-1.2.10-cp312-cp312-win32.whl", hash = "sha256:9c01a6b29cedb054a16fa04adf834a6531a39450888fc7ce2b3bf0fb6cba70c4"}, - {file = "dulwich-1.2.10-cp312-cp312-win_amd64.whl", hash = "sha256:96981c6704def4fd0f91fb9be9f613cf99ba327e2a655b8ef987f5f5245420f3"}, - {file = "dulwich-1.2.10-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:bd859654ed2e8c61c03066b739fc758b70edfcfce41344fae07f5f7f41b998b4"}, - {file = "dulwich-1.2.10-cp313-cp313-android_24_x86_64.whl", hash = "sha256:adf9a780add1b6da7e6df1b609afca66733380188a3a257ea340bc080ef4637c"}, - {file = "dulwich-1.2.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f337fd7dffe5e6cbff5e9a4694339b7575ed948babcc7675321efac075cb0a0e"}, - {file = "dulwich-1.2.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ded6744fcd2fb0e1f66c811492aefcdbee169010057f2a2e034a463eb0118c80"}, - {file = "dulwich-1.2.10-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:a16ea1a6d96db53a0089f7f6c4e6b942eff143e4de26b786aa894bf978ac148d"}, - {file = "dulwich-1.2.10-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:dfe2683285a7363a0d4b16f7f6fc9ee8a57a7b1cb1c390695475b76d8ce1b0f5"}, - {file = "dulwich-1.2.10-cp313-cp313-win32.whl", hash = "sha256:0840968ec6636a677b48bc6e38bf4ce2326e8f0d60cd9c9753cceeff5e23e92c"}, - {file = "dulwich-1.2.10-cp313-cp313-win_amd64.whl", hash = "sha256:a2e884a93452e8e1fb4a4886d14b328bae65c7a2fa88466c0b38faa63204f34d"}, - {file = "dulwich-1.2.10-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:fd86069664383fc456eeb291d67dc541c7f6ab163c59da1f8e18c72283f120c5"}, - {file = "dulwich-1.2.10-cp314-cp314-android_24_x86_64.whl", hash = "sha256:e445229de65ad6c3a8e2d9934fb807453c09b94f1469d32804e19fdfdcf6fd94"}, - {file = "dulwich-1.2.10-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:09c36bed6f2695cf34e606c403a00c6511f87f31ed20cac68f0bacf6df21cc4b"}, - {file = "dulwich-1.2.10-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ea42aa414a53d71ae0e294003a10b17ad454ff73d345a82e47da93e977fd2ff1"}, - {file = "dulwich-1.2.10-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:5b60380225742d7292703e573ce5a86be6f69fb9088ef2965c43830479df9f90"}, - {file = "dulwich-1.2.10-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:c321650fc9fa0b3e614607f0a229523491153d0ab00502366ac93b022d3c7f6c"}, - {file = "dulwich-1.2.10-cp314-cp314-win32.whl", hash = "sha256:834df23dca29e5adc5310b1b73eb573ad805fd0f39987e86afda2d81bfe24ab1"}, - {file = "dulwich-1.2.10-cp314-cp314-win_amd64.whl", hash = "sha256:abd8ffeec9c151cea62aafd25260ac7141a7ea56ce363dfc72cddf63396e8d02"}, - {file = "dulwich-1.2.10-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d5fd557539c982aabe15f7a14cff3d8786cdb62578d6f90f7871379fa3453260"}, - {file = "dulwich-1.2.10-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:66cb00f1298e4e0dd254ff6a68554577020b4b810b4a31083409baea62f8b1df"}, - {file = "dulwich-1.2.10-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:eca947467a28992088a0461c408791b405153294887accb92f64d60f9cc04758"}, - {file = "dulwich-1.2.10-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:7712da64d7cad219c77aa4216d46a1b14876ed078b2a88621462d2834f4f85b3"}, - {file = "dulwich-1.2.10-cp314-cp314t-win32.whl", hash = "sha256:52cfbd70f178b5ad770b9a1aee8a757aeda0e7282ae72cd546a98e6b9b0816cb"}, - {file = "dulwich-1.2.10-cp314-cp314t-win_amd64.whl", hash = "sha256:38478aa5d1821fd3d41e1ec7c65fb9a55d302d041635079db11118b79109054f"}, - {file = "dulwich-1.2.10-py3-none-any.whl", hash = "sha256:b9f18afbe8e6f6a380b199df3d2da14312b4eac4ee34ee77c6f61c56bd2693ed"}, - {file = "dulwich-1.2.10.tar.gz", hash = "sha256:106546960f592216315e56d32a92e72e67f1e86671335722f833f5ecd5b73314"}, + {file = "dulwich-1.2.15-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:9e5020cf329bac177c93f1f11e34d6064331f2778a5f9c5d0c24a90242e6f55e"}, + {file = "dulwich-1.2.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dda0e1a72501c0a573f4bfc5f5b698e1a832008419563f087b63e7b1c8f20b15"}, + {file = "dulwich-1.2.15-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:7427d25e0c6c91d84e8c45daf4618112fb38c2adc543e2501b3f3d1f2b65bba8"}, + {file = "dulwich-1.2.15-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:04f50ba0235ec1b3ab9d7dfb5aa4e1dbf139ef9a051448fa511f427457ea13ae"}, + {file = "dulwich-1.2.15-cp310-cp310-win32.whl", hash = "sha256:f115f03bd211020df7ec0cc1fd2faf69fcc92feda747a70db676b31127b9711f"}, + {file = "dulwich-1.2.15-cp310-cp310-win_amd64.whl", hash = "sha256:292444e3f520cb48020d02aaa15111e776a4ca740e9164780c5ca72ee50a1d06"}, + {file = "dulwich-1.2.15-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:efcc1d6553cad7b357822a46a64ad0626fa57870f480c4405450bd9b68190e9f"}, + {file = "dulwich-1.2.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c8cb3ed383a79bd485306f9bcdcaeafdaf7f82744f4dfc44ef5167fa4e2e4746"}, + {file = "dulwich-1.2.15-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:b83f95cd0ef83ab9c9acd79b51349e955964c8ec8c8f6b011e8a1e839bac2746"}, + {file = "dulwich-1.2.15-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d191b698e86f3acf75439bfe463553319ad687dfeedc78adf8f2660a07fa8280"}, + {file = "dulwich-1.2.15-cp311-cp311-win32.whl", hash = "sha256:744083ff962a30a4ad8cc6704f8f50daaf1b8c5db00208696cefe2cd15376823"}, + {file = "dulwich-1.2.15-cp311-cp311-win_amd64.whl", hash = "sha256:361c0057fa8818e722d4ada4e6b51cf12d260344542bfe98b87283d9fc0d3ed6"}, + {file = "dulwich-1.2.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:58c7498c42b90fe4e2d66d50757826218a1f795a0804c9f317cbe93496f113d7"}, + {file = "dulwich-1.2.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cd1d7c3261d0ebe8e2b59ab6ab1f68f520d0c062ce179af17dd8e853f2c32660"}, + {file = "dulwich-1.2.15-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3731e3e2e42326154bdbe67033347d0c7ff2de0db43409a4cc5998e15361f717"}, + {file = "dulwich-1.2.15-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d384c3a26fe6f369671b74331aaf25f956c88aeaba50b70bfd3aa6a12d431423"}, + {file = "dulwich-1.2.15-cp312-cp312-win32.whl", hash = "sha256:38104752e93b460d52a6511081902bcd7287999397d55e3e2cde33fe7735163c"}, + {file = "dulwich-1.2.15-cp312-cp312-win_amd64.whl", hash = "sha256:ea8587514932be2de0315a8c1143408db3ba9cbc07fe118b9125807994d7d40f"}, + {file = "dulwich-1.2.15-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:6364ed238f6957087b3c544b3e0fdae0fcf9c936a50c94dccbf063d4de6470ce"}, + {file = "dulwich-1.2.15-cp313-cp313-android_24_x86_64.whl", hash = "sha256:0442ef51c02ed31773b3f279b3d05060f90625a3c8bc14b4adb75c3fe18672f4"}, + {file = "dulwich-1.2.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c37e9acc1060cee69187e34c29fb02cc74f859d82d855deb7866dc3011c8131e"}, + {file = "dulwich-1.2.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ff49c76ed843f43c025fa39a4491af360852ca2926111ad1ce91090b5b5c6ff4"}, + {file = "dulwich-1.2.15-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:38b66910c0bc78c59964ef665df657833a4372ab6a4e5ae2969d8c49d9769687"}, + {file = "dulwich-1.2.15-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:82d4511bec54b72baca40b728f3fcff3b00ab3d2281cfe4fd21c59d7e689406e"}, + {file = "dulwich-1.2.15-cp313-cp313-win32.whl", hash = "sha256:4a802aa1cca5eef1487a490dc9515714493032b0b972b37accd2d42a3bf7ef70"}, + {file = "dulwich-1.2.15-cp313-cp313-win_amd64.whl", hash = "sha256:156673b2486a12ae45a0f3150e329d0907ef6af38b73d034c5c06c8047a49b7f"}, + {file = "dulwich-1.2.15-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:d833ba0f3ecb34f8743bba7357ec0efaf9b496ba01f24bbe46c7f953b2128ce2"}, + {file = "dulwich-1.2.15-cp314-cp314-android_24_x86_64.whl", hash = "sha256:390af2baa9b5114258f6770855b70c573cbc16466eb5a1a49d210c1dbadc9dbd"}, + {file = "dulwich-1.2.15-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0e3f08530f9e4006a6ecb5179f4c3e37f6cf93c93449084e2c15a3c388fc0973"}, + {file = "dulwich-1.2.15-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4ff2a7180f6a9f7b9ba50c785b0af03973871fede6aa31ba0ba561750eca8830"}, + {file = "dulwich-1.2.15-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:9d5fa7a5e26ab776311af4534a93b77a2d36d4f1b52ba3ab74a87edd20583996"}, + {file = "dulwich-1.2.15-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:cf77898f405f258a31d597cba179dea05eefb50dae59a3221c50a7f9113cc691"}, + {file = "dulwich-1.2.15-cp314-cp314-win32.whl", hash = "sha256:038850cbb88b6d0f9553f30b31f4aeb59c9497a55077d21da08a1a429f867fb4"}, + {file = "dulwich-1.2.15-cp314-cp314-win_amd64.whl", hash = "sha256:60e46a95cd866b01cbc1843541cf11033dd189c8b4767c05bd7b4ee0600cf6ab"}, + {file = "dulwich-1.2.15-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bbb42d5fdc17df2a7fe4f0a8d7439be2b06d97769c4ac1079280e6fa9a28ef05"}, + {file = "dulwich-1.2.15-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9ab042f02da1e4b19b0777e9a3236f77a43faf043f78ec2b9c8add08db0836a2"}, + {file = "dulwich-1.2.15-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:ba9412fad4e3548971f0a4c8967341b356771578035876a4f01a762a42f2527b"}, + {file = "dulwich-1.2.15-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:5fe361c6db07463c32a4c0841abdd7285e3686674e3eb4b9dc5944b531e74489"}, + {file = "dulwich-1.2.15-cp314-cp314t-win32.whl", hash = "sha256:601da25f9d39cc802c3523cc299310c93f50a4de70c529fc0f14c7b15bd7a8c0"}, + {file = "dulwich-1.2.15-cp314-cp314t-win_amd64.whl", hash = "sha256:06aa6bad114e3823d4bb380f1369655189cdf5b7b50d7d5a23445946cd3ea9ff"}, + {file = "dulwich-1.2.15-cp315-cp315-android_24_arm64_v8a.whl", hash = "sha256:28d21f59e914aff3ace677255d2436a282f3844c95db5f27b9bef2d0e2e7b378"}, + {file = "dulwich-1.2.15-cp315-cp315-android_24_x86_64.whl", hash = "sha256:24a8e2705b839b5992d2899a55d5c0a9609a21a8b9a825debf952fd42b748481"}, + {file = "dulwich-1.2.15-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:9d24079adc550acbc9cf69ca9b33bfc9dd80f00cae779197fbd5535b3e873c6d"}, + {file = "dulwich-1.2.15-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:caeec3ce1f713120a33ac48ff11cfc78cd2c148371ecaf5dc7a68d28adcf0ef6"}, + {file = "dulwich-1.2.15-cp315-cp315-manylinux_2_28_aarch64.whl", hash = "sha256:f157812ce877d50a93336136a1b9dd2b368395801a96cd8484c257a4f4665c23"}, + {file = "dulwich-1.2.15-cp315-cp315-manylinux_2_28_x86_64.whl", hash = "sha256:5b6f10587589f759747d167d1b6750de62961b17b4fb8c2be4ead49557c50ab9"}, + {file = "dulwich-1.2.15-cp315-cp315-win32.whl", hash = "sha256:e57dc844abfc7973baaa8dd7fe248ebc0b4ee1c879d49c041599d4eb7148fa24"}, + {file = "dulwich-1.2.15-cp315-cp315-win_amd64.whl", hash = "sha256:0b9f8583e72942233ea79d0f4cf931e963cf8b47255971ae4ab197129824c7e5"}, + {file = "dulwich-1.2.15-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:2d87c76063cb29ee334e7f29a74dddf2dae36447208c84d21576451c24a815a0"}, + {file = "dulwich-1.2.15-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:ed792ce5c5b21ec209ea7376e2a22b4a2891208b7cc8a88febf539d4c9013c24"}, + {file = "dulwich-1.2.15-cp315-cp315t-manylinux_2_28_aarch64.whl", hash = "sha256:63074719524d876e0688cd522dbe9c84aaea999a7018507db24a8fc134036d73"}, + {file = "dulwich-1.2.15-cp315-cp315t-manylinux_2_28_x86_64.whl", hash = "sha256:c18578b0af944eae2198bd3c81084a2f2cb8eb4b993fb5ce95ec823764d5b2f0"}, + {file = "dulwich-1.2.15-cp315-cp315t-win32.whl", hash = "sha256:18dc286482e7c76df4865421385bf98ed93aacedf5947bfe12b3d1e2528dd36c"}, + {file = "dulwich-1.2.15-cp315-cp315t-win_amd64.whl", hash = "sha256:53fa50546d72a7c8ebda380c80c399bcc3e46509caa9762791f202058303cf2e"}, + {file = "dulwich-1.2.15-py3-none-any.whl", hash = "sha256:5c863992962bab0fc5f75be132399a16f670f2a9283faf9aaeb953fd8891db83"}, + {file = "dulwich-1.2.15.tar.gz", hash = "sha256:e98f775601846fb4d44a384003e93c53c42b0c9796c8fe0cc3128b8e2e0bbf88"}, ] [package.dependencies] @@ -1189,7 +1318,7 @@ urllib3 = ">=2.2.2" [package.extras] aiohttp = ["aiohttp"] colordiff = ["rich"] -dev = ["codespell (==2.4.2)", "dissolve (>=0.1.1)", "mypy (==2.1.0)", "ruff (==0.15.20)"] +dev = ["codespell (==2.4.3)", "dissolve (>=0.1.1)", "mypy (==2.3.1)", "ruff (==0.16.5)"] fastimport = ["fastimport"] fuzzing = ["atheris"] https = ["urllib3 (>=2.2.2)"] @@ -1237,14 +1366,14 @@ idna = ">=2.0.0" [[package]] name = "fastjsonschema" -version = "2.21.2" +version = "2.22.2" description = "Fastest Python implementation of JSON schema" optional = false -python-versions = "*" +python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "fastjsonschema-2.21.2-py3-none-any.whl", hash = "sha256:1c797122d0a86c5cace2e54bf4e819c36223b552017172f32c5c024a6b77e463"}, - {file = "fastjsonschema-2.21.2.tar.gz", hash = "sha256:b1eb43748041c880796cd077f1a07c3d94e93ae84bba5ed36800a33554ae05de"}, + {file = "fastjsonschema-2.22.2-py3-none-any.whl", hash = "sha256:0fb3915616adac85ccfdd737d26be1089845d2019819505b42d39888458f74d4"}, + {file = "fastjsonschema-2.22.2.tar.gz", hash = "sha256:72064e12356a7d6ef02165be2946b9abadbdf238536e07eb587e3dbaa33099cf"}, ] [package.extras] @@ -1252,14 +1381,14 @@ devel = ["colorama", "json-spec", "jsonschema", "pylint", "pytest", "pytest-benc [[package]] name = "filelock" -version = "3.29.7" +version = "3.32.7" description = "A platform independent file lock." optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "filelock-3.29.7-py3-none-any.whl", hash = "sha256:987db6f789a3a2a59f55081801b2b3697cb97e2a736b5f1a9e99b559285fbc51"}, - {file = "filelock-3.29.7.tar.gz", hash = "sha256:5b481979797ae69e72f0b389d89a80bdd585c260c5b3f1fb9c0a5ba9bb3f195d"}, + {file = "filelock-3.32.7-py3-none-any.whl", hash = "sha256:65ff0d0190ea42038b32bda4b77834fb05be2cad4c5b9b01aa4dfb3614536e52"}, + {file = "filelock-3.32.7.tar.gz", hash = "sha256:37b8a3d9811b0f9aef7e5ec5c71bb320de52df51e6ca9bcd6f5ad81187660da7"}, ] [[package]] @@ -1380,14 +1509,14 @@ sniffio = ">=1.3.1" [[package]] name = "gen3cirrus" -version = "4.0.1" +version = "4.1.0" description = "" optional = false python-versions = "<4.0,>=3.13" groups = ["main"] files = [ - {file = "gen3cirrus-4.0.1-py3-none-any.whl", hash = "sha256:db8aaf9b0134d1285e9d9d3efcdd29abc244b3bbc5b9410b4ad5211cd92d0b6a"}, - {file = "gen3cirrus-4.0.1.tar.gz", hash = "sha256:afca76ac435cb765bac6f8e902e873204602494accf27860ae45805d3adca9ec"}, + {file = "gen3cirrus-4.1.0-py3-none-any.whl", hash = "sha256:37d7b277799ac725b07c649fc74bfef15489dd17f143df4a8a1f87fabd250ee3"}, + {file = "gen3cirrus-4.1.0.tar.gz", hash = "sha256:908607ccd134f5348c5bbc08b37646037676ae65aa526af827315c985d9cf00d"}, ] [package.dependencies] @@ -1441,37 +1570,40 @@ requests = "*" [[package]] name = "google-api-core" -version = "2.31.0" +version = "2.38.0" description = "Google API client core library" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "google_api_core-2.31.0-py3-none-any.whl", hash = "sha256:ef79fb3784c71cbac89cbd03301ba0c8fb8ad2aa95d7f9204dd9628f7adf59ab"}, - {file = "google_api_core-2.31.0.tar.gz", hash = "sha256:2be84ee0f584c48e6bde1b36766e23348b361fb7e55e56135fc76ce1c397f9c2"}, + {file = "google_api_core-2.38.0-py3-none-any.whl", hash = "sha256:8db2730375eba434bd75041fbe06b5374287407689ce132ac5650c8c8f042fcc"}, + {file = "google_api_core-2.38.0.tar.gz", hash = "sha256:31e326eafa31b34f1db7a715f50f61dbca7e6e37277762f252ed90e6ce94c246"}, ] [package.dependencies] google-auth = ">=2.14.1,<3.0.0" -googleapis-common-protos = ">=1.63.2,<2.0.0" -proto-plus = {version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""} -protobuf = ">=5.29.6,<8.0.0" +googleapis-common-protos = ">=1.69.2,<2.0.0" +opentelemetry-api = ">=1.44.0,<2.0.0" +proto-plus = ">=1.26.1,<2.0.0" +protobuf = ">=6.33.5,<8.0.0" requests = ">=2.33.0,<3.0.0" [package.extras] async-rest = ["aiohttp (>=3.13.4)", "google-auth[aiohttp] (>=2.14.1,<3.0.0)"] -grpc = ["grpcio (>=1.41.0,<2.0.0)", "grpcio (>=1.49.1,<2.0.0) ; python_version >= \"3.11\"", "grpcio (>=1.75.1,<2.0.0) ; python_version >= \"3.14\"", "grpcio-status (>=1.41.0,<2.0.0)", "grpcio-status (>=1.49.1,<2.0.0) ; python_version >= \"3.11\"", "grpcio-status (>=1.75.1,<2.0.0) ; python_version >= \"3.14\""] +grpc = ["grpcio (>=1.59.0,<2.0.0)", "grpcio (>=1.75.1,<2.0.0) ; python_version >= \"3.14\"", "grpcio-status (>=1.59.0,<2.0.0)", "grpcio-status (>=1.75.1,<2.0.0) ; python_version >= \"3.14\""] +testing = ["opentelemetry-sdk (>=1.44.0,<2.0.0)"] +tracing = ["opentelemetry-instrumentation-grpc (>=0.65b0,<1.0.0)"] [[package]] name = "google-api-python-client" -version = "2.198.0" +version = "2.200.0" description = "Google API Client Library for Python" optional = false -python-versions = ">=3.7" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "google_api_python_client-2.198.0-py3-none-any.whl", hash = "sha256:fabac935474e817da5e662ff61bf7139439d6f92b32d332a7318a2d45931e03e"}, - {file = "google_api_python_client-2.198.0.tar.gz", hash = "sha256:dfe3e16fb241af6e9c460a33f65085b3450e05cea09364f6b5d8997fb7e43e2a"}, + {file = "google_api_python_client-2.200.0-py3-none-any.whl", hash = "sha256:c0c2c4072a110b7fcc30ac151744d4410cddaba99c53cb4e815463308952b638"}, + {file = "google_api_python_client-2.200.0.tar.gz", hash = "sha256:82aa18b851328ea04867fd51c5a0c8da2e1b86ec45ce08487e902e7726d4ee50"}, ] [package.dependencies] @@ -1483,78 +1615,81 @@ uritemplate = ">=3.0.1,<5" [[package]] name = "google-auth" -version = "2.55.2" +version = "2.58.0" description = "Google Authentication Library" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "google_auth-2.55.2-py3-none-any.whl", hash = "sha256:d715f265f2cafc6a5f1bf0dc19870d20e3119f6f6682785a250bce3d03d38a3b"}, - {file = "google_auth-2.55.2.tar.gz", hash = "sha256:97ae7790ff740f2bc9db60eb864a7804f4ac19f5f02c38b3d942f2fea6e9b9ae"}, + {file = "google_auth-2.58.0-py3-none-any.whl", hash = "sha256:8a9c4645bb4c8e91668fb1934b95ae6a8687084232753639220ba9bf04a1610d"}, + {file = "google_auth-2.58.0.tar.gz", hash = "sha256:55e30cf15e737de92c5323d78cda8a83fcd57e7ffbaf900c4600039fd60a80fd"}, ] [package.dependencies] -cryptography = ">=38.0.3" +cryptography = [ + {version = ">=38.0.3", markers = "python_version < \"3.14\""}, + {version = ">=41.0.5", markers = "python_version >= \"3.14\""}, +] pyasn1-modules = ">=0.2.1" [package.extras] -aiohttp = ["aiohttp (>=3.8.0,<4.0.0)", "requests (>=2.20.0,<3.0.0)"] -cryptography = ["cryptography (>=38.0.3)"] -enterprise-cert = ["cryptography (>=38.0.3)"] +aiohttp = ["aiohttp (>=3.8.0,<4.0.0) ; python_version < \"3.14\"", "aiohttp (>=3.9.0,<4.0.0) ; python_version >= \"3.14\"", "requests (>=2.30.0,<3.0.0)"] +cryptography = ["cryptography (>=38.0.3) ; python_version < \"3.14\"", "cryptography (>=41.0.5) ; python_version >= \"3.14\""] +enterprise-cert = ["cryptography (>=38.0.3) ; python_version < \"3.14\"", "cryptography (>=41.0.5) ; python_version >= \"3.14\""] grpc = ["grpcio (>=1.59.0,<2.0.0) ; python_version < \"3.14\"", "grpcio (>=1.75.1,<2.0.0) ; python_version >= \"3.14\""] pyjwt = ["pyjwt (>=2.0)"] -pyopenssl = ["cryptography (>=38.0.3)"] +pyopenssl = ["cryptography (>=38.0.3) ; python_version < \"3.14\"", "cryptography (>=41.0.5) ; python_version >= \"3.14\""] reauth = ["pyu2f (>=0.1.5)"] -requests = ["requests (>=2.20.0,<3.0.0)"] -rsa = ["rsa (>=3.1.4,<5)"] -testing = ["aiohttp (>=3.8.0,<4.0.0)", "aioresponses", "flask", "freezegun", "grpcio (>=1.59.0,<2.0.0) ; python_version < \"3.14\"", "grpcio (>=1.75.1,<2.0.0) ; python_version >= \"3.14\"", "packaging", "pyjwt (>=2.0)", "pytest", "pytest-asyncio", "pytest-cov", "pytest-localserver", "pyu2f (>=0.1.5)", "requests (>=2.20.0,<3.0.0)", "responses", "urllib3"] -urllib3 = ["packaging", "urllib3"] +requests = ["requests (>=2.30.0,<3.0.0)"] +rsa = ["rsa (>=4.0.0,<5)"] +testing = ["aiohttp (>=3.8.0,<4.0.0) ; python_version < \"3.14\"", "aiohttp (>=3.9.0,<4.0.0) ; python_version >= \"3.14\"", "aioresponses", "flask", "freezegun", "grpcio (>=1.59.0,<2.0.0) ; python_version < \"3.14\"", "grpcio (>=1.75.1,<2.0.0) ; python_version >= \"3.14\"", "packaging (>=20.0)", "pyjwt (>=2.0)", "pytest", "pytest-asyncio", "pytest-cov", "pytest-localserver", "pyu2f (>=0.1.5)", "requests (>=2.30.0,<3.0.0)", "responses", "urllib3 (>=1.26.15,<3.0.0)"] +urllib3 = ["packaging (>=20.0)", "urllib3 (>=1.26.15,<3.0.0)"] [[package]] name = "google-auth-httplib2" -version = "0.4.0" +version = "0.4.2" description = "Google Authentication Library: httplib2 transport" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "google_auth_httplib2-0.4.0-py3-none-any.whl", hash = "sha256:8e55cfafa3358cba85f6cad4a886138e88e158d71e7e5c9ee5936a5c1507fb91"}, - {file = "google_auth_httplib2-0.4.0.tar.gz", hash = "sha256:d5b030a204b7a4b4d553ba9ca701b62481ee2b74419325580be70f7d85ffed35"}, + {file = "google_auth_httplib2-0.4.2-py3-none-any.whl", hash = "sha256:4298dd6b1415972d0c464b7177e6a69a595e7aec5b972222ecdca342f6009647"}, + {file = "google_auth_httplib2-0.4.2.tar.gz", hash = "sha256:916225a6367e613c9af44d83f41688a599d3f687777846b8b91bec65085ed1f1"}, ] [package.dependencies] -google-auth = ">=1.32.0,<3.0.0" +google-auth = ">=2.14.1,<3.0.0" httplib2 = ">=0.19.0,<1.0.0" [[package]] name = "google-cloud-core" -version = "2.6.0" +version = "2.7.0" description = "Google Cloud API client core library" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "google_cloud_core-2.6.0-py3-none-any.whl", hash = "sha256:6d63ac8e5eca6d9e4319d0a1e2265fadcd7f1049904378caecfa01cf52dd869e"}, - {file = "google_cloud_core-2.6.0.tar.gz", hash = "sha256:e76149739f90fac1fc6757c09f47eaccb3145b54adbd7759b0f7c4b235f46c83"}, + {file = "google_cloud_core-2.7.0-py3-none-any.whl", hash = "sha256:c18a250904cfdda021eb3ae8b8238c9f9ca272a4cbbfb5cba946b3fe3022eed1"}, + {file = "google_cloud_core-2.7.0.tar.gz", hash = "sha256:874aaf89765db87a9b911b7a2ca7c5068554868eed9e75c7766affe342a2913d"}, ] [package.dependencies] -google-api-core = ">=2.11.0,<3.0.0" +google-api-core = ">=2.28.0,<3.0.0" google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0" [package.extras] -grpc = ["grpcio (>=1.47.0,<2.0.0) ; python_version < \"3.14\"", "grpcio (>=1.75.1,<2.0.0) ; python_version >= \"3.14\"", "grpcio-status (>=1.47.0,<2.0.0)"] +grpc = ["grpcio (>=1.59.0,<2.0.0)", "grpcio (>=1.75.1,<2.0.0) ; python_version >= \"3.14\"", "grpcio-status (>=1.59.0,<2.0.0)", "grpcio-status (>=1.75.1,<2.0.0) ; python_version >= \"3.14\""] [[package]] name = "google-cloud-storage" -version = "3.12.1" +version = "3.14.1" description = "Google Cloud Storage API client library" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "google_cloud_storage-3.12.1-py3-none-any.whl", hash = "sha256:9297ae0c2ce3f5400b1f2bb3a3e6d2cd256614366e03cd30600871df8e903afb"}, - {file = "google_cloud_storage-3.12.1.tar.gz", hash = "sha256:1d81491c7663bc26c5056d00b834356f2253b910ef467f9cf9928a87fca1e04b"}, + {file = "google_cloud_storage-3.14.1-py3-none-any.whl", hash = "sha256:8f0fe2b74bd80ddcbd67247a3b47b81035a0a4f448569ed358fa40a46c940118"}, + {file = "google_cloud_storage-3.14.1.tar.gz", hash = "sha256:b24e74b493c60b19b83462933a83bb831e45b6ca4924b0d75eac8e176b58b3a7"}, ] [package.dependencies] @@ -1566,8 +1701,8 @@ google-resumable-media = ">=2.7.2,<3.0.0" requests = ">=2.22.0,<3.0.0" [package.extras] -grpc = ["google-api-core[grpc] (>=2.27.0,<3.0.0)", "grpc-google-iam-v1 (>=0.14.0,<1.0.0)", "grpcio (>=1.59.0,<2.0.0) ; python_version < \"3.14\"", "grpcio (>=1.75.1,<2.0.0) ; python_version >= \"3.14\"", "grpcio-status (>=1.59.0,<2.0.0) ; python_version < \"3.14\"", "grpcio-status (>=1.75.1,<2.0.0) ; python_version >= \"3.14\"", "proto-plus (>=1.22.3,<2.0.0) ; python_version < \"3.13\"", "proto-plus (>=1.25.0,<2.0.0) ; python_version >= \"3.13\"", "protobuf (>=4.25.8,<8.0.0)"] -protobuf = ["protobuf (>=3.20.2,<7.0.0)"] +grpc = ["google-api-core[grpc] (>=2.27.0,<3.0.0)", "grpc-google-iam-v1 (>=0.14.2,<1.0.0)", "grpcio (>=1.59.0,<2.0.0)", "grpcio (>=1.75.1,<2.0.0) ; python_version >= \"3.14\"", "grpcio-status (>=1.59.0,<2.0.0)", "grpcio-status (>=1.75.1,<2.0.0) ; python_version >= \"3.14\"", "proto-plus (>=1.26.1,<2.0.0)", "protobuf (>=6.33.5,<8.0.0)"] +protobuf = ["protobuf (>=6.33.5,<8.0.0)"] testing = ["PyYAML", "black", "brotli", "coverage", "flake8", "google-cloud-iam", "google-cloud-kms", "google-cloud-pubsub", "google-cloud-testutils", "google-cloud-testutils", "mock", "numpy", "opentelemetry-sdk", "psutil", "py-cpuinfo", "pyopenssl", "pytest", "pytest-asyncio", "pytest-benchmark", "pytest-cov", "pytest-rerunfailures", "pytest-xdist"] tracing = ["opentelemetry-api (>=1.1.0,<2.0.0)"] @@ -1616,129 +1751,129 @@ files = [ [[package]] name = "google-resumable-media" -version = "2.10.0" +version = "2.10.2" description = "Utilities for Google Media Downloads and Resumable Uploads" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "google_resumable_media-2.10.0-py3-none-any.whl", hash = "sha256:88152884bee37b2bf36a0ab81ad8c7fd12212c9803dd981d77c1b35b02d34e7c"}, - {file = "google_resumable_media-2.10.0.tar.gz", hash = "sha256:e324bc9d0fdae4c52a08ae90456edc4e71ece858399e1217ac0eb3a51d6bc6ee"}, + {file = "google_resumable_media-2.10.2-py3-none-any.whl", hash = "sha256:e3cedc827a4ea41e216582d74346f1fb9fceb625a8c3c53912f2ca1d663334d7"}, + {file = "google_resumable_media-2.10.2.tar.gz", hash = "sha256:1de441703cd298d75a419bfdc0066e9fc7b0a1de630df96eea8ce8f5c759358c"}, ] [package.dependencies] google-crc32c = ">=1.0.0,<2.0.0" [package.extras] -aiohttp = ["aiohttp (>=3.6.2,<4.0.0)", "google-auth (>=1.22.0,<2.0.0)"] +aiohttp = ["aiohttp (>=3.6.2,<4.0.0)", "google-auth (>=2.14.1,<3.0.0)"] requests = ["requests (>=2.18.0,<3.0.0)"] [[package]] name = "googleapis-common-protos" -version = "1.75.0" +version = "1.75.3" description = "Common protobufs used in Google APIs" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "googleapis_common_protos-1.75.0-py3-none-any.whl", hash = "sha256:961ed60399c457ceb0ee8f285a84c870aabc9c6a832b9d37bb281b5bebde43ed"}, - {file = "googleapis_common_protos-1.75.0.tar.gz", hash = "sha256:53a062ff3c32552fbd62c11fe23768b78e4ddf0494d5e5fd97d3f4689c75fbbd"}, + {file = "googleapis_common_protos-1.75.3-py3-none-any.whl", hash = "sha256:a018d2bf098ca9fb6faa08d5bb780e2a2c2f73c566f069761331386c9596d3f2"}, + {file = "googleapis_common_protos-1.75.3.tar.gz", hash = "sha256:57c435ac2c68b108999b6db075d9053e4d7a936ba57b4a3d45667b1346f1738a"}, ] [package.dependencies] -protobuf = ">=4.25.8,<8.0.0" +protobuf = ">=6.33.5,<8.0.0" [package.extras] -grpc = ["grpcio (>=1.44.0,<2.0.0)"] +grpc = ["grpcio (>=1.59.0,<2.0.0)"] [[package]] name = "greenlet" -version = "3.5.3" +version = "3.5.6" description = "Lightweight in-process concurrent programming" optional = false python-versions = ">=3.10" groups = ["main"] markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\"" files = [ - {file = "greenlet-3.5.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:c180d22d325fb613956b443c3c6f4406eb70e6defc70d3974da2a7b59e06f48c"}, - {file = "greenlet-3.5.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:483d08c11181c83a6ce1a7a61df0f624a208ec40817a3bb2302714592eee4f04"}, - {file = "greenlet-3.5.3-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1dae6e0091eae084317e411f047f0b7cb241c6db570f7c45fd6b900a274914ce"}, - {file = "greenlet-3.5.3-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0f6ff50ff8dbd51fae9b37f4101648b04ea0df19b3f50ab2beb5061e7716a5c8"}, - {file = "greenlet-3.5.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bcd2d72ccd70a1ec68ba6ef93e7fbb4420ef9997dabc7010d893bd4015e0bec"}, - {file = "greenlet-3.5.3-cp310-cp310-manylinux_2_39_riscv64.whl", hash = "sha256:37bf9c538f5ae6e63d643f88dec37c0c83bdf0e2ebc62961dedcf458822f7b71"}, - {file = "greenlet-3.5.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:73f152c895e09907e0dbe24f6c2db37beb085cd63db91c3825a0fcd0064124a8"}, - {file = "greenlet-3.5.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8bdb43e1a1d1873721acab2be99c5befd4d2044ddfd52e4d610801019880a702"}, - {file = "greenlet-3.5.3-cp310-cp310-win_amd64.whl", hash = "sha256:0909f9355a9f24845d3299f3112e266a06afb68302041989fd26bd68894933db"}, - {file = "greenlet-3.5.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:aca9b4ce85b152b5524ef7d88170efdff80dc0032aa8b75f9aaf7f3479ea95b4"}, - {file = "greenlet-3.5.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f71be4920368fe1fabeeaa53d1e3548337e2b223d9565f8ad5e392a75ba23fc"}, - {file = "greenlet-3.5.3-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d77e67f65f98449e3fb83f795b5d0a8437aead2f874ca89c96576caf4be3af6"}, - {file = "greenlet-3.5.3-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e18619ba655ac05d78d80fc83cac4ba892bd6927b99e3b8237aee861aaacc8bb"}, - {file = "greenlet-3.5.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8540f1e6205bd13ca0ce685581037219ca54a1b41a0a15d228c6c9b8ad5903d7"}, - {file = "greenlet-3.5.3-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:d27c0c653a60d9535f690226474a5cc1036a8b0d7b57504d1c4f89c44a07a80c"}, - {file = "greenlet-3.5.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7ef56fe650f50575bf843acde967b9c567687f3c22340941a899b7bc56e956a8"}, - {file = "greenlet-3.5.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5121af01cf911e70056c00d4b46d5e9b5d1415550038573d744138bacb59e6b8"}, - {file = "greenlet-3.5.3-cp311-cp311-win_amd64.whl", hash = "sha256:0f41e4a05a3c0cb31b17023eff28dd111e1d16bf7d7d00406cd7df23f31398a7"}, - {file = "greenlet-3.5.3-cp311-cp311-win_arm64.whl", hash = "sha256:ec6f1af59f6b5f3fc9678e2ea062d8377d22ac644f7844cb7a292910cf12ff44"}, - {file = "greenlet-3.5.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:719757059f5a53fd0dde23f78cffeafcdd97b21c850ddb7ca684a3c1a1f122e2"}, - {file = "greenlet-3.5.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:efa9f765dd09f9d0cdac651ffdf631ee59ec5dc6ee7a73e0c012ba9c52fbdf5b"}, - {file = "greenlet-3.5.3-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7faba15ac005376e02a0384504e0243be3370ce010296a44a820feb342b505ab"}, - {file = "greenlet-3.5.3-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5795cd1101371140551c645f2d408b8d3c01a5a29cf8a9bce6e759c983682d23"}, - {file = "greenlet-3.5.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:87142215824be6ac05e2e8e2786eec307ccbc27c36723c3881959df654af6861"}, - {file = "greenlet-3.5.3-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:af4923b3096e26a36d7e9cf24ab88083a20f97d191e3b97f253731ce9b41b28c"}, - {file = "greenlet-3.5.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:215275b1b49320987352e6c1b054acca0064f965a2c66992bed9a6f7d913f149"}, - {file = "greenlet-3.5.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6b1b0eed82364b0e32c4ea0f221452d33e6bb17ae094d9f72aed9851812747ea"}, - {file = "greenlet-3.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:cde8adafa2365676f74a979744629589999093bc86e2484214f58e61df08902c"}, - {file = "greenlet-3.5.3-cp312-cp312-win_arm64.whl", hash = "sha256:c4e7b79d83805475f0102008843f6eb45fd3bb0b2e88c774adab5fbaab27117d"}, - {file = "greenlet-3.5.3-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:c8d87c2134d871df96ecdea9cec7cbaab286dadab0f56476e57aaf9e8ac11550"}, - {file = "greenlet-3.5.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2d185dd1621757e70c3861cceffd5317ab4e7ed7eb09c82994828468527ade5"}, - {file = "greenlet-3.5.3-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1c514a468149bf8fbbab874188a3535cd8a48a3e353eb53a3d424296f8dbacd3"}, - {file = "greenlet-3.5.3-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9ad04dd75458c6300b047c61b8639092433d205a25a14e310d6582a480efcca1"}, - {file = "greenlet-3.5.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:915f887cf2682b66419b879423a2e072634aa7b7dce6f3ada4957cfced3f1e9a"}, - {file = "greenlet-3.5.3-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:afaabdd554cd7ae9bbb3ca070b0d7fdfd207dbf1d16865f7233837709d354bda"}, - {file = "greenlet-3.5.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:766cfd421c13e450feb340cd472a3ed9957d438727b7b4593ad7c76c5d2b0deb"}, - {file = "greenlet-3.5.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2ecda9ec22edf38fa389369eaed8c3d37c05f3c54e69f69438dbb2cc1de1458b"}, - {file = "greenlet-3.5.3-cp313-cp313-win_amd64.whl", hash = "sha256:c82304750f057167ff60d188df1d0cc1764ce9567eadf03e6a7443bcedd0b30b"}, - {file = "greenlet-3.5.3-cp313-cp313-win_arm64.whl", hash = "sha256:dc133a1569ee667b2a6ef56ce551084aeefd87a5acbc4736d336d1e2edc6cfc4"}, - {file = "greenlet-3.5.3-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:fd2e02fa07485778536a036222d616ab957b1d533f36b3ed98ce725d9c9d3117"}, - {file = "greenlet-3.5.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df0a0628d1597eb0897b62f55d1343f772405fd25f3b2a796c76874b0c2e22e8"}, - {file = "greenlet-3.5.3-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ebd933a6adabc298bab47731a130fe6bfb888bd934eee37810f151159544540d"}, - {file = "greenlet-3.5.3-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8d19fe6c39ebff9259f07bcc685d3290f8fa4ea2278e51dd0008e4d6b0f2d814"}, - {file = "greenlet-3.5.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b9d501b40e80b70e32323c799dd9b420a5577a9601469d362ae1ffb690f3a7c"}, - {file = "greenlet-3.5.3-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:962c5df2db8cb446da51edf1ca5296c389d93b99c9d8aa2ee4c7d0d8f1218260"}, - {file = "greenlet-3.5.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a1fad1d11e7d6aab184107baa8e4ece11ccba3ec9599cd7efa5ff4d70d43256a"}, - {file = "greenlet-3.5.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fad5aec764399f1b5cc347ad250a59660f20c8f8888ea6bae1f93b769cce1154"}, - {file = "greenlet-3.5.3-cp314-cp314-win_amd64.whl", hash = "sha256:7669aa24cf2a1041d6f7899575b494a3ab4cf68bfcc8609b1dc0be7272db835e"}, - {file = "greenlet-3.5.3-cp314-cp314-win_arm64.whl", hash = "sha256:5b4807c4082c9d1b6d9eed56fcd041863e37f2228106eef24c30ca096e238605"}, - {file = "greenlet-3.5.3-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:271a8ea7c1024e8a0d7dd2be66dd66dda8a07193f41a17b9e924f7600f5b62be"}, - {file = "greenlet-3.5.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19131729ae0ddc3c2e1ef85e650169b5e37ee32e400f215f78b94d7b0d567310"}, - {file = "greenlet-3.5.3-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1540dd8e5fc2a5aec40fbb98ef8e149fa47c89a4b4a1cf2575a14d3d1869d7a8"}, - {file = "greenlet-3.5.3-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b897d97759425953f69a9c0fac67f8fe333ec0ce7377ef186fb2b0c3ad5e354d"}, - {file = "greenlet-3.5.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e81fa194a1d20967877bdf9c7794db2bc99063e5be36aee710c08f04c5bb087f"}, - {file = "greenlet-3.5.3-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:3236754d423955ea08e9bb5f6c04a7895f9e22c290b66aa7653fcb922d839eb0"}, - {file = "greenlet-3.5.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:55cf4d777485d43110e47133cbba6d74a8885a87ec1227ef0267f9ee80c5aa21"}, - {file = "greenlet-3.5.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:12a248ba75f6a9a236375f52296c498c89ff1d8badf32deb9eca7abd5853f7da"}, - {file = "greenlet-3.5.3-cp314-cp314t-win_amd64.whl", hash = "sha256:efc6bd60ea02e085862c74a3ef64b147ffc6f1a5ea7d9f26e7a939943f68c1e3"}, - {file = "greenlet-3.5.3-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:ea03f2f04367845d6b58eeed276e1e56e51f0b97d8ad5a88a7d20a91dc9056cc"}, - {file = "greenlet-3.5.3-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78dbef602fda6d97d957eb7937f70c9ce9e9527330347f8f6b6f9e554a9e7a47"}, - {file = "greenlet-3.5.3-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f73857adb8fee13fa56c172bd11262f888c0c648f9fea113e777bb2c7904a81"}, - {file = "greenlet-3.5.3-cp315-cp315-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cefa9cef4b371f9844c6053db71f1138bc6807bab1578b0dae5149c1f1141357"}, - {file = "greenlet-3.5.3-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:232fec92e823addaf02d9472cf7381e24a1d046a6ced1103c5caa4c21b9dfc1d"}, - {file = "greenlet-3.5.3-cp315-cp315-manylinux_2_39_riscv64.whl", hash = "sha256:6219b6d04dbf6ba6084d77dc609e8473060dc55f759cbf626d512122781fa128"}, - {file = "greenlet-3.5.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:2421c3564da9429d5586d46ca31ebb26516b5498a802cf65c041a8e8a8980d34"}, - {file = "greenlet-3.5.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e0f0d160f0b2e558e6c75f7930967183255dc9735e5f5b8cae58ee09c9576d8b"}, - {file = "greenlet-3.5.3-cp315-cp315-win_amd64.whl", hash = "sha256:dd99329bbc15ca78dcc583dba05d0b1b0bae01ab6c2174989f5aaee3e41ac930"}, - {file = "greenlet-3.5.3-cp315-cp315-win_arm64.whl", hash = "sha256:499fef2acede88c1864a57bb586b4bf533c81e1b82df7ab93451cdb47dfec227"}, - {file = "greenlet-3.5.3-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:176bc16a721fa5fc294d70b87b4dfa5fbdd251b3da5d5372735ecef9bd7d6d0c"}, - {file = "greenlet-3.5.3-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:629b614d2b786e89c50440e246f33eea78f58a962d0bdbbcc809e6d13605903f"}, - {file = "greenlet-3.5.3-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2b2e857ae16f5f72142edf75f9f176fe7526ba19a2841df1420516f83831c9f2"}, - {file = "greenlet-3.5.3-cp315-cp315t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:16d192579ed281051396dddd7f7754dac6259e6b1fb26378c87b66622f8e3f91"}, - {file = "greenlet-3.5.3-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e515757e2e36bcbf1fad09a46e1557e8b1ae1797d4b44d09da7deed88ad28608"}, - {file = "greenlet-3.5.3-cp315-cp315t-manylinux_2_39_riscv64.whl", hash = "sha256:4399eb8d041f20b68d943918bc55502a93d6fdc0a37c14da7881c04139acee9d"}, - {file = "greenlet-3.5.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:b363d46ed1ea431825fdb01471bb024fc08399bad1572a616e853c7684415adb"}, - {file = "greenlet-3.5.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:e44da2f5bbdaabaf7d80b73dbb430c7035771e9f244e3c8b769715c9d8fa0a16"}, - {file = "greenlet-3.5.3-cp315-cp315t-win_amd64.whl", hash = "sha256:8ff8bed3e3baa20a3ea261ce00526f1898ad4801d4886fd2220580ee0ad8fadf"}, - {file = "greenlet-3.5.3-cp315-cp315t-win_arm64.whl", hash = "sha256:b7068bd09f761f3f5b4d214c2bed063186b2a86148c740b3873e3f56d79bac31"}, - {file = "greenlet-3.5.3.tar.gz", hash = "sha256:a61efc018fd3eb317eeca31aba90ee9e7f26f22884a79b6c6ec715bf71bb62f1"}, + {file = "greenlet-3.5.6-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:95e7c44d072db623a1aab04ce488cf9533294a77ed9d072cd503a3596f4106ac"}, + {file = "greenlet-3.5.6-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7d501d5eb5d4f67207df364752ad697465b834268744be7581c18d81d35d41d"}, + {file = "greenlet-3.5.6-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a364c1ea75dc51b83a17f52fe0c79cf8bc4ddf740403bebd4581c7666eea017d"}, + {file = "greenlet-3.5.6-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5599b380c1f28efeb724e81569eac80cd92f99a85bd9775456caaf3225d40b11"}, + {file = "greenlet-3.5.6-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eed88b64a5e5da72d6a71cdc5aaeefaa5ced9b748f8d19f89800b339961dad39"}, + {file = "greenlet-3.5.6-cp310-cp310-manylinux_2_39_riscv64.whl", hash = "sha256:5bbda3c70dd35d60671bc33b01916802707a052130d9e50cdb871d34594d35cb"}, + {file = "greenlet-3.5.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:874cea8bb1ec1ddccbacbd027856f6bf496f6bc18aba97a918c20e067edab236"}, + {file = "greenlet-3.5.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:128813fc29f2336a21b4d06eedd5e16bcc7ea46f59e9ff1cb30ea70e48195d88"}, + {file = "greenlet-3.5.6-cp310-cp310-win_amd64.whl", hash = "sha256:dad3d233d441a022c1f7155f0fb9d5aff7b97c1ea8c7dfa02cce586b16ab2d0b"}, + {file = "greenlet-3.5.6-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:a6a4b98a9132e0f45c9fc245a63894cfd8c45fb7a0d6bffc5eab3ec327cf7324"}, + {file = "greenlet-3.5.6-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45bfd2b51e38aaa5f9849f114d9c7c1d75f69187c849b3549cd64c465283abfa"}, + {file = "greenlet-3.5.6-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3c6dede9133e1da41d561bc3fb14e92b47e2ce39ae60edefaad145658ea7c5e2"}, + {file = "greenlet-3.5.6-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4fb8e59f68845d56c23c031dcd79c329f345e4a9d2ffac91c3d1ab366bdc457b"}, + {file = "greenlet-3.5.6-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c20ea32a73d17b9b60e3371240e17b0068120c98a5ec01a224a7dd8c89733ba"}, + {file = "greenlet-3.5.6-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:d701eab36200c36224833d07dbdb709adb7fd4253429548ddb5e547b8ed40586"}, + {file = "greenlet-3.5.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5a0b2791239c99992a86c1b635b787fe2a877d9eaaa26f8891ce943832b585ae"}, + {file = "greenlet-3.5.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:188bf333769b7145e2b0b4a7f09615ec550ed44d3a2a8395fb7b36f0e9901e13"}, + {file = "greenlet-3.5.6-cp311-cp311-win_amd64.whl", hash = "sha256:a6b4ff33f7e011bbaa148238d131c4fd4f8afbab3c104ddfbdb2b12b74ff7016"}, + {file = "greenlet-3.5.6-cp311-cp311-win_arm64.whl", hash = "sha256:59deccd347735a7774223b05a93773fddbb298aba3cea21be4337fb4752dbe32"}, + {file = "greenlet-3.5.6-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:a5876d0a60355af98d535c47f6cd6eb0f8a432396dab26845d380b92f8412422"}, + {file = "greenlet-3.5.6-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e85880b538e59a59f55117b81f208a6660ad5ac328aad9305f812d9b8bc67a0f"}, + {file = "greenlet-3.5.6-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f0ba7c2a329d650628f4c8572fd1db29f0a59dd70a3e3e0710dcf18a35cce9d8"}, + {file = "greenlet-3.5.6-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ee7d9da3bf493909cf811a3f038840cb34fab5ae2956b8a263919f6e289ab188"}, + {file = "greenlet-3.5.6-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:975736b002ed080d124cf81a79cb7e05cb26d6b3f5c7a7b651c0fcce70353aa1"}, + {file = "greenlet-3.5.6-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:71890d5247020c25c21a6b65202782bfc281d4e6e244842419d30e3492bb6dcc"}, + {file = "greenlet-3.5.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0616b8f878098c5681fd8f0dc92d887551717402342a70f0abcbfea5f5ad8a44"}, + {file = "greenlet-3.5.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3dbb4596a6a4e5d47121a33ff20533a81e60f302d9e67b69909a8bc21a43f0a7"}, + {file = "greenlet-3.5.6-cp312-cp312-win_amd64.whl", hash = "sha256:7ac4abb3877c43af320392c664774eef6fa2cc063c79a55fc02d844a3cbe7395"}, + {file = "greenlet-3.5.6-cp312-cp312-win_arm64.whl", hash = "sha256:301102a49120b095e72a7838792b41233975fc1c155daec6d98f81c00c9280e0"}, + {file = "greenlet-3.5.6-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:f96f0e30b5a95c7631b12bfe214cbc90ec8fe8cfa36920596c10514a65743519"}, + {file = "greenlet-3.5.6-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c75116c9de79949de23006e2d9b35ee82874c594fcf5c0311b439acaa14b8441"}, + {file = "greenlet-3.5.6-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cad5782f93f7f738b62c6527b6f32a60694d924029f299a8b524758cfa53d815"}, + {file = "greenlet-3.5.6-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a93ee7c6e8fd0f8a83525a51bd777be57ee17787e91d805bd8d6faf9dcada18e"}, + {file = "greenlet-3.5.6-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f98e8215e172f567ce80eeaed9107fb4d32b6c44f26983d9b8334658136a205a"}, + {file = "greenlet-3.5.6-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:7f731ebac68ea06d628658295cb2d217b10186329fcf9a3b6a149045059bf92e"}, + {file = "greenlet-3.5.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:df19e2d0b1620039af5102563fbd96e8938c7f5c3f5828528d641d9fc585525e"}, + {file = "greenlet-3.5.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:06c0e933290fba8ffe53ead4ae1b8044b0e9754b75cebf381aa2bc3e50d82fac"}, + {file = "greenlet-3.5.6-cp313-cp313-win_amd64.whl", hash = "sha256:5b602b4201b965a8354d74e232364a66ff243dd142e350d035f46169bb36e13d"}, + {file = "greenlet-3.5.6-cp313-cp313-win_arm64.whl", hash = "sha256:876077e7ebb8c84ed068e2b23d4c62ebb010d60df84b9591af1be2f39010ffb2"}, + {file = "greenlet-3.5.6-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8cddea1b8339451c2fb3388e138347b6126744f33b611bdb55b7357361cfef46"}, + {file = "greenlet-3.5.6-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c59acfa8eb73a1e0d484392dc002bdf001fd4ce73394e0132df3d1ab6093d7cb"}, + {file = "greenlet-3.5.6-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a3b4a01c6da07ef9f80d4fe8933b994bc99747bcea3eab0330a9c34d3c12655b"}, + {file = "greenlet-3.5.6-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dd0b83bed3405b586a3133629f1d1a5bc7bfd64822a3b7ab342bdc68e6dbc61b"}, + {file = "greenlet-3.5.6-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9a09d59bef1db94f384b5bcc2d523694d338f3df6b757aeeaf7baca5d0c0be88"}, + {file = "greenlet-3.5.6-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:fdacf26402389bdd89857ad3c045a26fe8f3314f9a8b28226f82f88463a65b77"}, + {file = "greenlet-3.5.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8b7c73d1cef3d9ae963e9ff03f6222df43efbb9054ffd2f1969c935b7fc84c02"}, + {file = "greenlet-3.5.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8b27df301f56e3b3d2298095c8f7d6b68f2521f6b1693e901fa039bdbae34424"}, + {file = "greenlet-3.5.6-cp314-cp314-win_amd64.whl", hash = "sha256:f8f0bd690e1a41294ac87905e8121c81a3761ec2583c768f13467428606c8c7a"}, + {file = "greenlet-3.5.6-cp314-cp314-win_arm64.whl", hash = "sha256:8cda13494d86a4f12429641117cb6ac4bbbc9c30a33f711f7d3a2e5fbe4b0b7e"}, + {file = "greenlet-3.5.6-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:97c5a53e8c1754df58e73f047a99e287d4da1bdfe64b0072fb25c87000897951"}, + {file = "greenlet-3.5.6-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fea4427d1ffdb3b523d7daa6712038428a4c16c450b9777bdd1221cfee0eab49"}, + {file = "greenlet-3.5.6-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:73a29b5ba642e35433166a03a3e02935e7238c4b3467fbd77523b99edea23e5b"}, + {file = "greenlet-3.5.6-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:61a61b4a95a4f97922c3a6f5606d3e360851584bd47e500a5161373c53810e3d"}, + {file = "greenlet-3.5.6-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:460e70b033aba8ed47e2ac9b5d0d2157b05a34fbfa30a241400aef4118902cdc"}, + {file = "greenlet-3.5.6-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:fe3170a69fe039b18ad18171e66faa9a75f6fe9d78f968fd9b54e09fbd714d81"}, + {file = "greenlet-3.5.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ca80a49b53ed1d22f7282da7255f7bb2fd1935fd0f623d8613fda38745f18961"}, + {file = "greenlet-3.5.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:916f92f2a8db10508f739d0b5e00b83defe5d1115a997c54532a6d7cf8c95404"}, + {file = "greenlet-3.5.6-cp314-cp314t-win_amd64.whl", hash = "sha256:886bcf1870af74c32bc310fd00a6b803445e17e51b7d5a107c7b35c0f362cc16"}, + {file = "greenlet-3.5.6-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:3ac3494c381dab876cad7d0b22f3a722f3e0c8deb3a65b9e7f35ad7f58b8fcb3"}, + {file = "greenlet-3.5.6-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:602024dae6d77e161f4b89491b62ca1d4f19949d79d47b2db057e476d21179d6"}, + {file = "greenlet-3.5.6-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f8e63209c3e1e828ee6a457529b4a6d8b05d050fe0ae03a7ae49e967c5d312e0"}, + {file = "greenlet-3.5.6-cp315-cp315-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9133d68624b1f2e89ec2f554d56aea8a5b0d7168cd9320200ba58d4d794845a4"}, + {file = "greenlet-3.5.6-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccadce0130fd813ec86ebfe969a6c58b42acc1d0fe55a47525375b740e07b605"}, + {file = "greenlet-3.5.6-cp315-cp315-manylinux_2_39_riscv64.whl", hash = "sha256:5adcbbfe78bdc242c71740a02e0991cc1b2f34d33c8bb15ca45eee8fd1140942"}, + {file = "greenlet-3.5.6-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:9297fb9c39b9a2c039dbcd306c410bd6906b95244dec3bba4318d36c718c164c"}, + {file = "greenlet-3.5.6-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:b374e79ffa7511afc11773aef40a4ccea6191fba1c856ea2f9c56738dca69d7a"}, + {file = "greenlet-3.5.6-cp315-cp315-win_amd64.whl", hash = "sha256:7969bffa322c097bd46ae595ada6a931cefda613f18ba64587e9cff4cb320756"}, + {file = "greenlet-3.5.6-cp315-cp315-win_arm64.whl", hash = "sha256:8dba0129b93e7091dfefaf4cf7000172741bff7f47bf6326fcf17f32fbb54d6b"}, + {file = "greenlet-3.5.6-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:de3de000d459402cda015068fd135aa50c0bf6f2477a80d4da1e646f123b4e78"}, + {file = "greenlet-3.5.6-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45663c01a4de48b9a64a2ee1509d92d1dfd3afb02b2ccfc9333029d11aef996a"}, + {file = "greenlet-3.5.6-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3deccbb57a481e3a408fe61cdfd5c13e0678fc0a30fdd09597917ca87b4be877"}, + {file = "greenlet-3.5.6-cp315-cp315t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:63aff70fe5aac59c72215f42ec39fcb59ff46774fa966e717f8ecb6ee2273577"}, + {file = "greenlet-3.5.6-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:311018b46472fb26ee85870847fb89eb64cc8aaddb617400789d87076f7cfeec"}, + {file = "greenlet-3.5.6-cp315-cp315t-manylinux_2_39_riscv64.whl", hash = "sha256:520648db8fb92eef7b3e6013f5a6f901cdf0d6685f639c2f7a245879f865bef7"}, + {file = "greenlet-3.5.6-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:7f924a5a9d5890649566f2f6682e0d8ad8ca23028bacffbbac36dbd7fd680176"}, + {file = "greenlet-3.5.6-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:de9923832f2d8c1a5ecd8d7260465a6ca5a86888a0d129e3bd5cf0406d2fc5bf"}, + {file = "greenlet-3.5.6-cp315-cp315t-win_amd64.whl", hash = "sha256:2ab5f42ac6c238eb71770715e6e909ad9a1a92b6c681ccb64cd5a0f07edb953f"}, + {file = "greenlet-3.5.6-cp315-cp315t-win_arm64.whl", hash = "sha256:f9fe868463ec7e1363733af77e38a5fda3e9b63940337048c945d69e0c80ff24"}, + {file = "greenlet-3.5.6.tar.gz", hash = "sha256:8e67c43bdfc88d5fee6db0d3e40175b362fc95fb85f0412d233b9b203c53a575"}, ] [package.extras] @@ -1747,26 +1882,23 @@ test = ["objgraph", "psutil", "setuptools"] [[package]] name = "gunicorn" -version = "26.0.0" +version = "26.2.0" description = "WSGI HTTP Server for UNIX" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "gunicorn-26.0.0-py3-none-any.whl", hash = "sha256:40233d26a5f0d1872916188c276e21641155111c2853f0c2cd55260aec0d24fc"}, - {file = "gunicorn-26.0.0.tar.gz", hash = "sha256:ca9346f85e3a4aeeb64d491045c16b9a35647abd37ea15efe53080eb8b090baf"}, + {file = "gunicorn-26.2.0-py3-none-any.whl", hash = "sha256:bd249d0b3f7972f7432f0a6b6ff3b3ee2d129f70cd1ff6c09a9dd9e29a2b88e3"}, + {file = "gunicorn-26.2.0.tar.gz", hash = "sha256:62b864895d9ebff0b2f9867ba04fe811c93121596540830c9c916d0769668447"}, ] -[package.dependencies] -packaging = "*" - [package.extras] -fast = ["gunicorn_h1c (>=0.6.5)"] -gevent = ["gevent (>=24.10.1)"] -http2 = ["h2 (>=4.1.0)"] +fast = ["gunicorn_h1c (>=0.6.9)"] +gevent = ["gevent (>=24.10.1)", "packaging"] +http2 = ["h2 (>=4.4.1)"] setproctitle = ["setproctitle"] -testing = ["coverage", "gevent (>=24.10.1)", "h2 (>=4.1.0)", "httpx[http2]", "pytest", "pytest-asyncio", "pytest-cov", "uvloop (>=0.19.0)"] -tornado = ["tornado (>=6.5.0)"] +testing = ["coverage", "gevent (>=24.10.1)", "h2 (>=4.4.1)", "httpx[http2] (>=0.23.0)", "inotify (>=0.2.10) ; sys_platform == \"linux\"", "packaging", "pytest (>=9.0.3)", "pytest-asyncio", "pytest-cov", "uvloop (>=0.19.0)"] +tornado = ["tornado (>=6.5.7)"] [[package]] name = "h11" @@ -1844,18 +1976,18 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "idna" -version = "3.18" +version = "3.20" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.9" groups = ["main", "dev"] files = [ - {file = "idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2"}, - {file = "idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848"}, + {file = "idna-3.20-py3-none-any.whl", hash = "sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c"}, + {file = "idna-3.20.tar.gz", hash = "sha256:a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44"}, ] [package.extras] -all = ["mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] +all = ["coverage (>=7.10.0)", "hypothesis (>=6.141.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.16.0)", "ty (>=0.0.37)"] [[package]] name = "iniconfig" @@ -1958,14 +2090,14 @@ type = ["pytest-mypy (>=1.0.1) ; platform_python_implementation != \"PyPy\""] [[package]] name = "jaraco-functools" -version = "4.5.0" +version = "4.6.0" description = "Functools like those found in stdlib" optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "jaraco_functools-4.5.0-py3-none-any.whl", hash = "sha256:79ce39246eddbde4b3a03b77ea5f0f7878dc669b166a66cf3fa8e266aa3fa2f4"}, - {file = "jaraco_functools-4.5.0.tar.gz", hash = "sha256:3bb5665ea4a020cf78a7040e89154c77edadb3ca74f366479669c5999aa70b03"}, + {file = "jaraco_functools-4.6.0-py3-none-any.whl", hash = "sha256:99e3dc0060c5cbe8fcd1cdb36258e2a65ca40f1566b2033b12abb1bb44dd3c30"}, + {file = "jaraco_functools-4.6.0.tar.gz", hash = "sha256:880c577ec9720b3a052d5bc611fb9f2269b3d87902ef42440df443b88e443280"}, ] [package.dependencies] @@ -2057,34 +2189,34 @@ type = ["pygobject-stubs", "pytest-mypy (>=1.0.1)", "shtab", "types-pywin32"] [[package]] name = "mako" -version = "1.3.12" +version = "1.4.1" description = "A super-fast templating language that borrows the best ideas from the existing templating languages." optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "mako-1.3.12-py3-none-any.whl", hash = "sha256:8f61569480282dbf557145ce441e4ba888be453c30989f879f0d652e39f53ea9"}, - {file = "mako-1.3.12.tar.gz", hash = "sha256:9f778e93289bd410bb35daadeb4fc66d95a746f0b75777b942088b7fd7af550a"}, + {file = "mako-1.4.1-py3-none-any.whl", hash = "sha256:a359d9a94a541213958742b2698d0a7757bb83551767bc468a74b9905aba9617"}, + {file = "mako-1.4.1.tar.gz", hash = "sha256:d7904710b662996425a21627710c4777c45053146942cf8a7aebf757c92b8c27"}, ] [package.dependencies] -MarkupSafe = ">=0.9.2" +MarkupSafe = ">=2.0" [package.extras] babel = ["Babel"] -lingua = ["lingua"] +lingua = ["lingua (>=4.16)"] testing = ["pytest"] [[package]] name = "markdown" -version = "3.10.2" +version = "3.10.3" description = "Python implementation of John Gruber's Markdown." optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36"}, - {file = "markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950"}, + {file = "markdown-3.10.3-py3-none-any.whl", hash = "sha256:fa6c92a00a4a3c98b22728c64a935ae1928250ae65058a6ded814d2cc29a4cea"}, + {file = "markdown-3.10.3.tar.gz", hash = "sha256:3589362618f743188b4d955b874402bc814f4f83f544dc207719f4baa7d9c45f"}, ] [package.extras] @@ -2257,14 +2389,14 @@ files = [ [[package]] name = "moto" -version = "5.2.2" +version = "5.2.3" description = "A library that allows you to easily mock out tests based on AWS infrastructure" optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "moto-5.2.2-py3-none-any.whl", hash = "sha256:3817f1e39721ca833579b921e53e3b68547ace6a34d848c9486fbb5905808de9"}, - {file = "moto-5.2.2.tar.gz", hash = "sha256:aac8023a429e125e91c91f8f4730a67b54f518cda587352f7e67252fe3168f75"}, + {file = "moto-5.2.3-py3-none-any.whl", hash = "sha256:5ec31b3cdbb383a19d46030157bd82aaaa7a9cc5f564200f995663d347bd4728"}, + {file = "moto-5.2.3.tar.gz", hash = "sha256:a9e95c3218b6eda18e74571f1ced11cb1bc3151467d562c1da6037b9d19832cb"}, ] [package.dependencies] @@ -2301,78 +2433,100 @@ xray = ["aws-xray-sdk (>=0.93,!=0.96)", "setuptools"] [[package]] name = "msgpack" -version = "1.2.1" +version = "1.2.2" description = "MessagePack serializer" optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "msgpack-1.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8c7b398c56ff125feae96c2737abfec5595f1fa0aa186df60c56040b8accb95c"}, - {file = "msgpack-1.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1548006a91aa93c5da81f3bdcebc1a0d10cea2d25969754fbe848da622b2b895"}, - {file = "msgpack-1.2.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1dabedcd0f23559f3596428c6589c1cd8c6eaed3a0d720795b07b0225d769203"}, - {file = "msgpack-1.2.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83efa1c898e0fc5380fc0cabbf75164c52e3b5cbb45973710d75821928380c73"}, - {file = "msgpack-1.2.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01e2dd6c9b19d333a00282330cc8a73d38d8dabc306dc5b42cd668c3ac82e833"}, - {file = "msgpack-1.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:350cb813d0af6e65d2f7ef0d729f7ff5be5a8bce03665892f43e5883d4ecc1b8"}, - {file = "msgpack-1.2.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:ee1d9ed27d0497b848923746cf762ed2e7db24f4be7eec8e5cbe8c766aa707b7"}, - {file = "msgpack-1.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:633727297ed063441fd1cda2288865487f33ad14eeb8831afb5f0c396a62cfce"}, - {file = "msgpack-1.2.1-cp310-cp310-win32.whl", hash = "sha256:298872ecf9e61950f1c6af4ca969b859ee91783bb920ef6e6172697d0c8aad74"}, - {file = "msgpack-1.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:2ff164c1b0bcb740b073b99e945234d0212852fa378e44a208c425379140dbeb"}, - {file = "msgpack-1.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:29a3f6e9667868429d8240dfd063ea5ffdc1321c13d783aa23827a38de0dcb22"}, - {file = "msgpack-1.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aded5bdf32609dc7987a49bbbd15a8ef096193f96dd8bbeb791de729e650acf5"}, - {file = "msgpack-1.2.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:146ee4e9ce80b365c6d4c47073da9da7bcec473e58194ceee5dd7620ace77e06"}, - {file = "msgpack-1.2.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a28d076ca7c82b9c8728ad90b7147489449557038bed50e4241eb832395169b4"}, - {file = "msgpack-1.2.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7d31c0ac0c640f877804c67cb2bc9f4e23dc2db97e96c2e67fa27d38283b41f8"}, - {file = "msgpack-1.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8ff92d7feeaf5bc26c51495b69e2f99ed97ab79346fb6555f44be7dd2ac6503b"}, - {file = "msgpack-1.2.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:779197a6513bab3c3632265e3d0f7cb3227e62510841a6f34f1eaa37efbb345e"}, - {file = "msgpack-1.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:67f6dd22fa72a93752643f07889796d62739a13415ee630169a8ce764f86cf9f"}, - {file = "msgpack-1.2.1-cp311-cp311-win32.whl", hash = "sha256:91054a783328e0ea7954b8771095705c8d2243b814743fbaadf14552c9c52c5d"}, - {file = "msgpack-1.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2eda0b7ebb1283a98d3e4492ac933c8af6aff59fd3df1c3ed024f536af4b1dc8"}, - {file = "msgpack-1.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:6ee967f7c7e1df2890c671ff2ee51a28ded0efc95da3e507176dee881ce36c66"}, - {file = "msgpack-1.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2ef59c659f289eddf8aa6623823f19fa2f40a4029266889eac7a2505dd210c35"}, - {file = "msgpack-1.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d3567748a5107cb40cdf66a275430c2f87c07777698f4bfd25c35f44d533258c"}, - {file = "msgpack-1.2.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60926b75d00c8e816ef98f3034f484a8bc64242d66839cef4cf7e503142316a0"}, - {file = "msgpack-1.2.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:020e881a764b20d8d7ca1a54fc01b8175519d108e3c3f194fddc200bda95951a"}, - {file = "msgpack-1.2.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4202c74688ca06591f78cb18988228bd4cca2cc75d57b60008372892d2f1e6e6"}, - {file = "msgpack-1.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8b267ce94efb76fbd1b3373511420074ee3187f0f7811bf394531de13294735a"}, - {file = "msgpack-1.2.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f1d0f8f98ade9634e01fb704a408f9336c0a8f1117b369f5db83dc7551d8b1"}, - {file = "msgpack-1.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f02cf17a6ca1abe29b5f980644f7551f94d71f2011509b26d8625ce038f0df64"}, - {file = "msgpack-1.2.1-cp312-cp312-win32.whl", hash = "sha256:0c0d9802354507bcba62af19c17918e3eb437cc25e6f50657d511b5856a77aac"}, - {file = "msgpack-1.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:5c24aa15d5963051e1a5c62b12c50cd705992502b5ec1f3bece6046f33c9fc24"}, - {file = "msgpack-1.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:4227224aaec8f7fbcbfbd4272319347b2bb4030366502600f8c45588c5187b07"}, - {file = "msgpack-1.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0a70e3cf2804a300d921bb0940426e35f4e489a23adfb77a808892241db0a064"}, - {file = "msgpack-1.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:491cc39455ca765fad51fb451bf2915eb2cf41192ab5801ce8d67c1d614fe056"}, - {file = "msgpack-1.2.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f310233ef7fb9c14e201c93639fe5f5260b005f56f0b29048e999c30935596cc"}, - {file = "msgpack-1.2.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:787c9bebb5833e8f6fc8abca3c0597683d8d87f56a8842b6b89c75a5f3176e2d"}, - {file = "msgpack-1.2.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dc871b997a9370d855b7394465f2f350e847a5b806dd38dcc9c989e7d87da155"}, - {file = "msgpack-1.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:85f57e960d877f2977f6430896191b04a21f8901b3b4baf2e4604329f4db5402"}, - {file = "msgpack-1.2.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:1233ee2dd0cefba127583de50ea654677277047d238303521db35def3d7b2e7c"}, - {file = "msgpack-1.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e3dc2feb0876209d9c38aa56cb1de169bd6c4348f1aa48271f241226590993e6"}, - {file = "msgpack-1.2.1-cp313-cp313-win32.whl", hash = "sha256:6d09badf350af2be9d189184e04e64cf54ad93569ab3d96fca58bd3e84aad707"}, - {file = "msgpack-1.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:33f14fba63278b714efe6ad07e50ea5f03d91537aa6a1c5f1ceca4cf44013ca9"}, - {file = "msgpack-1.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:afc5febcd4c99effbc02b528e49d6fd0760b2b7d48c05239e345a5fa6e743d9a"}, - {file = "msgpack-1.2.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:05f340e47e7e47d2da8db9b53e1bb1d294369e9ef45a747441309f6650b8351d"}, - {file = "msgpack-1.2.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:810b916696c86ef0deb3b74588480224df4c1b071136c34183e4a2a4284d7ac7"}, - {file = "msgpack-1.2.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ca0dacff965c47afdc3749a8469d7302a8f801d6a28758d55120d75e66ce6889"}, - {file = "msgpack-1.2.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e2bf9280bceb5efca998435904b5d3e9fdbcc11d90dc9df30aec7973252b720"}, - {file = "msgpack-1.2.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa6c4be5d1c02a42b066ca6ddb71adf36432868fdcdb6ee87e634e86e0674190"}, - {file = "msgpack-1.2.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec0e675d59150a6269ddc9139087c722292664a37d071a849c05c473350f1f2d"}, - {file = "msgpack-1.2.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:dd3bfe82d53edfe4b7fc9a7ec9761e23a7a5b1dac22264505af428253c29ed24"}, - {file = "msgpack-1.2.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5ad5467fc3f68b5468e06c5f788d712e9f8ffc8b0cd1bcb160c105c1ee92dae7"}, - {file = "msgpack-1.2.1-cp314-cp314-win32.whl", hash = "sha256:98b58bdb89c46190e4609bb36abe17c6d4105ad13f9c5f8f6f64d320f8ced3fb"}, - {file = "msgpack-1.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:74847557e28ce71bd3c438a447ca90e4b507e997ddbdef8a12a7b283b86c156b"}, - {file = "msgpack-1.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:b50b727bd652bdc37d950336c848ef20ec54a4cafc38dce19b1cd86ad625d0f7"}, - {file = "msgpack-1.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8d00f177ca88a77c1cf848d204a38f249751650b601cb6532acc68805d8a8273"}, - {file = "msgpack-1.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5bb9c386f0a329c035ddbab4b72d1028bf9627add8dda41070288563d57ed1b1"}, - {file = "msgpack-1.2.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20466cca18c49c7292a8984bc15d65857b171e7264bdcb5f96baf8be238791fc"}, - {file = "msgpack-1.2.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:196300e7e5d6e74d50f1607ab9c06c4a1484c383cd22defd727902591f7e8dde"}, - {file = "msgpack-1.2.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575957e79cd51903a4e8495a242442949641e08f1efd5197b43bebd3ea7682b4"}, - {file = "msgpack-1.2.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8c2ed1e48cc0f460bf3c7780e7137ff21a4e18433451916f2442c1b21036cd7d"}, - {file = "msgpack-1.2.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5f6277e5f783c36786a145e0247fc189a03f35f84b251646e53592d2bc12b355"}, - {file = "msgpack-1.2.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9389552ecf4784886345ead0647e4edc96bee37cbab05b75540f542f766c48c"}, - {file = "msgpack-1.2.1-cp314-cp314t-win32.whl", hash = "sha256:c1c79a604a2969a868a78b6ebd27a887e00c624f14f66b3038e0590cb23332d1"}, - {file = "msgpack-1.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f12038a35fabd52e56a3547bab42401af49a45caa6dd00b34c44de235bc93ee2"}, - {file = "msgpack-1.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:0adcf06ffde0777c0e1a9b771a2b1c4226ba1bbf748c8efcc02fcdeca3299107"}, - {file = "msgpack-1.2.1.tar.gz", hash = "sha256:04c721c2c7448767e9e3f2520a475663d8ee0f09c31890f6d2bd70fd636a9647"}, + {file = "msgpack-1.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7afa5431f6f3487c584187ca6c8e2a34e9b106529893b3e720eabb068f6ac970"}, + {file = "msgpack-1.2.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a9b4cf3685a135666d27d0d7a73fece74e2fad01d9b508fded89e843512f0e90"}, + {file = "msgpack-1.2.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4710d881d8fb047deed2485707409116722af2b992d3fefd73c7667c4e350839"}, + {file = "msgpack-1.2.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:58ce37a4a54577115922385d37201d9a44d66d0167dfbbf4770a2e9bf8ea7ba3"}, + {file = "msgpack-1.2.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:86f173a584f72f6164801f31866d22a581f60c991572cf922aed9ab8eb422b77"}, + {file = "msgpack-1.2.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e05a94a0442de86818a30281c6cc2cb9cc7aa148386fd3541c4d4774b73cb3a9"}, + {file = "msgpack-1.2.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:9bd3d1557c3fe1a095068210708a03e3e4795973392af6f4047060e70abd9a6c"}, + {file = "msgpack-1.2.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:46ec851571d8f1b6e29794ebb9dd36f785008da6d14f57c702e60781d6caf648"}, + {file = "msgpack-1.2.2-cp310-cp310-win32.whl", hash = "sha256:1f3af0baafd184436501004828bb3df64eeb2fc49dfe9d89abcf604956094563"}, + {file = "msgpack-1.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:82b1bdf293267afaadcc608b125e7fc6576bb0785a60c4fa7d07c7ab76ed76ec"}, + {file = "msgpack-1.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9d7fb25b4442fae0cb2590272d06ab4f6caa526ee36a994edb81e946b874813e"}, + {file = "msgpack-1.2.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7fe374ba76eb0ecca13a1703daa8fa85825a6ddddbb52d4c1a732fa524194683"}, + {file = "msgpack-1.2.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9b0c1f2aa7b0026b4bd50718100e8b04175e4f36e160aa852502377b5e572e7"}, + {file = "msgpack-1.2.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f11e09f10210a91c169e39c7a5a1f9090eaa73ad75555fafad5023c3053c47ba"}, + {file = "msgpack-1.2.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8b1415d02e9bf722672af8a90f90813265a0cd0b14163187261e54a5592bc949"}, + {file = "msgpack-1.2.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:42fd9260416885b4815caca5bdd14dfd5dda6cdade732d6c09104ef8f6228761"}, + {file = "msgpack-1.2.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:336525cc2688e43ea77dfb1a4ce012c8cde561835913801dbfcfdcf4111d8abb"}, + {file = "msgpack-1.2.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cdb6cc6e1127d15879c47a8b3270716243da82d3e7feab1f5946872c75b3d60f"}, + {file = "msgpack-1.2.2-cp311-cp311-win32.whl", hash = "sha256:cf66fb38703e61a486b01b56d43bb1f50698fbe99b6bd90feba10f24fab60b3b"}, + {file = "msgpack-1.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:0883a1578168929fd1640fbbc4614773f1a130e419a8a817dc2918d9af1b651c"}, + {file = "msgpack-1.2.2-cp311-cp311-win_arm64.whl", hash = "sha256:4955accbd87f27beebef5f3ecc27503aa74cb016fb4f640868e749fd93194a35"}, + {file = "msgpack-1.2.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a4348705be86e029d04e741cf9ed0dfe03e942d7d3b92e838fa80d3aa2c3ebc"}, + {file = "msgpack-1.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0a652ceeededf71d3fa40c303a02a149d42338d310162367b91c539d4bd6e0a3"}, + {file = "msgpack-1.2.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90986cc9aab9d7d1d8f38bcbf65d3f7ac83bdd90c35765db7d691b4829698cba"}, + {file = "msgpack-1.2.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:77c2e018417dc1d66f235e383877ee885b60ade9d29e494dd581e08af2cb1923"}, + {file = "msgpack-1.2.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0e91332144f69bc3018c91232fac26da580ef748fb8eaddd7914d4458001cc4f"}, + {file = "msgpack-1.2.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e915d390d7068b257ca8b62f3fc59fad135c8631d1017ab03b0b924b07c5367"}, + {file = "msgpack-1.2.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c522420d78db2431887d45b518e304d86e27b9ad0b30f24e3806a6ad5d8bdbfc"}, + {file = "msgpack-1.2.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4b554d8164ebb526892194f71dcd96ef1fefe0c250087498785d3ffc04a80be3"}, + {file = "msgpack-1.2.2-cp312-cp312-win32.whl", hash = "sha256:0e3315de5a4b2920ccef48d96b4448025e064a10d0f5a250f6584477d839c8d4"}, + {file = "msgpack-1.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:b68614fba0570349833b7dd999ff0aed4e5cc8d9eb6e3a7d4527be33c65e33d3"}, + {file = "msgpack-1.2.2-cp312-cp312-win_arm64.whl", hash = "sha256:59d5b93efa45fd09f620d0c9ba81cde339a2c9937af3eea42ee9653094ce6640"}, + {file = "msgpack-1.2.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8b2a281b556f120a43e591ea39915741b7ad54d4727b9c4350a0a11692252533"}, + {file = "msgpack-1.2.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1e8cdd1f3e7cc52c751092a9bf740e81e6919ab109cd376ae2d965dad0bbae34"}, + {file = "msgpack-1.2.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1814f92306ae7862908e9ece7cfd90e0dc87ded3e89b6ae7ffdd1175d6376fdc"}, + {file = "msgpack-1.2.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d24b38a825bcca41bb956de50eb98451ef291304a8607fad99e619043d3e79b9"}, + {file = "msgpack-1.2.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34e83e345194a2a51d8bd447dea9de2104f91e75b247f4735f14f04529f0746b"}, + {file = "msgpack-1.2.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:682804bf31e43d46e51a9a33bd575b51e839d715ce6bd5612c055f7b28ad637b"}, + {file = "msgpack-1.2.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:9b659d77f8726fa5e7038967dda6b68d53cf34472c094cfa5b845454713b90d5"}, + {file = "msgpack-1.2.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4d9a562aec0a92fe536da2e533d313b3d2a6b929157b1dec7ff623446dc0a8ab"}, + {file = "msgpack-1.2.2-cp313-cp313-win32.whl", hash = "sha256:a4161eee7799863aee237c35c90427861f7b994416dd81ae829f560b0a81bdcd"}, + {file = "msgpack-1.2.2-cp313-cp313-win_amd64.whl", hash = "sha256:b07c03f0da7e5279170df7745ddc732d526c8a198208936ec1a95c11ed2b2d5f"}, + {file = "msgpack-1.2.2-cp313-cp313-win_arm64.whl", hash = "sha256:d13d07efbf655f9ae7a2352b630c52727b359005b21ba08a507585c9ac8c0896"}, + {file = "msgpack-1.2.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e497ee34e8a3342bbde51b27c22d8db05a651df3361dd3daef5b3ab0d66f3e04"}, + {file = "msgpack-1.2.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0dd9173c5ebaf5ecc5ca86e7ae1db92934e1d57b856f3dd90698941431f4fd77"}, + {file = "msgpack-1.2.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8dc4487097571f7311188c3eca2a3e86cd1f1db4c37c7a017bcc3fd38486cbfe"}, + {file = "msgpack-1.2.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:73b0e05c32c3cfc3cd84994908e57430c0ebc6813abf905d3f18ff115d54df3f"}, + {file = "msgpack-1.2.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa1120c653b76d8eafa50423b5eba06b5c9737f8692c74fa3afe03e84b8978ea"}, + {file = "msgpack-1.2.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccfd880988f8438d1c91c77d7edc58e70f4d2012e999167bc154c64c6f06ea6b"}, + {file = "msgpack-1.2.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:6195257a107bf25872ef84aab7295078271eea3ac6413f0506b631f6c9586ed5"}, + {file = "msgpack-1.2.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b8dd6c71d20c28d2d0eb0c51e7cccf3584afde3b1364f6629596186c9025bd54"}, + {file = "msgpack-1.2.2-cp314-cp314-win32.whl", hash = "sha256:d242f3c4ccf55b056e6cf901720dccde58f1df117898f2bbf3bcd6e38ec7c248"}, + {file = "msgpack-1.2.2-cp314-cp314-win_amd64.whl", hash = "sha256:1510f24612d4b983dff6935d9273e02c320cfd525727fbcb58836a75f589fdbc"}, + {file = "msgpack-1.2.2-cp314-cp314-win_arm64.whl", hash = "sha256:7826f16edc763e768404f55605ef85dfcf5857e729c1ed29e0d7c180be4fe6d8"}, + {file = "msgpack-1.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f466049b8e1ec0854287bbe9a074316826fe0e08dcf707245f98b1ae49e92650"}, + {file = "msgpack-1.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f6b6f8deb07d49090e1808c6ef9cb7d23ca17bef3aa6ed3e5e03df16606e60c"}, + {file = "msgpack-1.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b542ffc0a5c531eedc40419f291f1bd659aa8d4223408a5b51c88a2796083fd3"}, + {file = "msgpack-1.2.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d095df2627e5dd59ac7b0c5ad627a671c76e6020171e03cbe4621a61f0562c3"}, + {file = "msgpack-1.2.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ffdd2f4950daf7815490f23087963e3420175b9609520b7ff5df64d351159c22"}, + {file = "msgpack-1.2.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:652d1bf13d01bac8fd569def0fe76745e55bcda01e30aa6332d5947ea3788839"}, + {file = "msgpack-1.2.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9bf452ff4d4981f25a18e9476e002bcc9263e7928024aa4d7148e25f7be3f929"}, + {file = "msgpack-1.2.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:55faa6f8395e23b848c535ad5dcb96b3462f37f5e7f4ac500d500434f7345da7"}, + {file = "msgpack-1.2.2-cp314-cp314t-win32.whl", hash = "sha256:419a45c67a5c04213172a14b1864657e014665b77d7081b107a51707923dd39e"}, + {file = "msgpack-1.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:935b1cfad9b908b0fa845010f4271df4c2f04e1cd26e3f18acd61a45f93c9e36"}, + {file = "msgpack-1.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11e8c421e117d1c36728b423d0402555cccbf0c6f53e288f0e75b6b12100d70f"}, + {file = "msgpack-1.2.2-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:e1b99ad34613d5f8477fa5cf99bc4eaeaf27965588007c102370cd9a78fe9de5"}, + {file = "msgpack-1.2.2-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:0fbc1bed8a535389b41882cfae66376e248cd1680eaa94fd83193c73e1d24986"}, + {file = "msgpack-1.2.2-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:06d95f61de7afe4f4ff908a6feebfcb070d0582ac87c9cf3cedf8551cf634516"}, + {file = "msgpack-1.2.2-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b5c696ae7cd7166b3657261adb855b461ff31f07823fdbae9de8bf80adfccc21"}, + {file = "msgpack-1.2.2-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0708afbf6a9587f0bfe479a9825c141d14d91e2f6a5c8103cf28bc96f4edb5d9"}, + {file = "msgpack-1.2.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:226a62ffe99fe54c5c61d910ec64c3449b7766c3280bd286bf6c94838dde239a"}, + {file = "msgpack-1.2.2-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:9fd7f32e2f0fb334e7ecc5adb5cf0458785bd3a9d9d86f950e1715f101cebce5"}, + {file = "msgpack-1.2.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:9db1ba1c1e6a84245a9dd866265b56b8a1e9461549cc72ed296d8cbfbd32961b"}, + {file = "msgpack-1.2.2-cp315-cp315-win32.whl", hash = "sha256:e2eb7ea0ac3911a7aac9d8aaa36d40f216d99455b3274cd3fac38181bcd910cf"}, + {file = "msgpack-1.2.2-cp315-cp315-win_amd64.whl", hash = "sha256:9352e6cdb510a7b1a5d3ccaccec730e82e50cf3484a3af7bdaab19e23b9589ff"}, + {file = "msgpack-1.2.2-cp315-cp315-win_arm64.whl", hash = "sha256:29cc2d5291711a52956a79a51f41c732329df39ad727c886bd8f0b5b9237a808"}, + {file = "msgpack-1.2.2-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:d886baa46b2532135e7320067e6a44edb09ba5883a6096b0f9c044533984b8a8"}, + {file = "msgpack-1.2.2-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:53679573c75cce5f82359e0bd4e6a97809a6b9a9b7a48fd1ba592f4a82cddc84"}, + {file = "msgpack-1.2.2-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d3c247d457ae9079974c7ce3c665396754a6d2baff7eaa51332212a8a5a3f13b"}, + {file = "msgpack-1.2.2-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:352ed831042549cca8be23780e1fe7c9177e65ff02bf183509c4b4d33f671782"}, + {file = "msgpack-1.2.2-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f80361592c13d7226b4379c8941529b63fe1a9d0e05d2de8f3306b70e522b53f"}, + {file = "msgpack-1.2.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:68df2947921d449f6dcfeafd86cb2cdde13327a8b447534bbe4ee5aaf32a5695"}, + {file = "msgpack-1.2.2-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:51dd39d23cfdea0400ed3ff2d29d1e83bd951d3aea79dc89be5b701a09edfe23"}, + {file = "msgpack-1.2.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:b13b59e66f107cca1ba708dd5307179870ca1b15b19fcee7ccf722e5308d9212"}, + {file = "msgpack-1.2.2-cp315-cp315t-win32.whl", hash = "sha256:8c6321a414f8b4a8dc43976b2fa8349156434ca9adedd9a187b796f7e1d3d3fc"}, + {file = "msgpack-1.2.2-cp315-cp315t-win_amd64.whl", hash = "sha256:6f53285f20d592ed309ee19e509cc4c77a3bda1db02ad67e8a0949bb227a5a6d"}, + {file = "msgpack-1.2.2-cp315-cp315t-win_arm64.whl", hash = "sha256:a378e12ccc06d76efde115caf4073b7e5ff3cc18291d1341f9e65fb882e3f754"}, + {file = "msgpack-1.2.2.tar.gz", hash = "sha256:9eb0b0e602064527a045ea28c4f174ed69383587e29cebe28947e3b84106eb2a"}, ] [[package]] @@ -2394,16 +2548,31 @@ pyasn1-modules = ">=0.0.5" rsa = ">=3.1.4" six = ">=1.6.1" +[[package]] +name = "opentelemetry-api" +version = "1.44.0" +description = "OpenTelemetry Python API" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "opentelemetry_api-1.44.0-py3-none-any.whl", hash = "sha256:94b98c893a91b88657eaac1e3ba89618cdb85be6918196705354f34728b2cdef"}, + {file = "opentelemetry_api-1.44.0.tar.gz", hash = "sha256:67647e5e9566edcf421166fdf022b3537f818635daa852b289e34604dc6fb33a"}, +] + +[package.dependencies] +typing-extensions = ">=4.5.0" + [[package]] name = "packaging" -version = "26.2" +version = "26.3" description = "Core utilities for Python packages" optional = false -python-versions = ">=3.8" -groups = ["main", "dev"] +python-versions = ">=3.9" +groups = ["dev"] files = [ - {file = "packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e"}, - {file = "packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661"}, + {file = "packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c"}, + {file = "packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79"}, ] [[package]] @@ -2426,14 +2595,14 @@ pynacl = ">=1.5" [[package]] name = "pbs-installer" -version = "2026.6.10" +version = "2026.9.1" description = "Installer for Python Build Standalone" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "pbs_installer-2026.6.10-py3-none-any.whl", hash = "sha256:c8d7faa06d529ec8db53975da810376fff44e850e5b7b440a29f3b88edd62bd6"}, - {file = "pbs_installer-2026.6.10.tar.gz", hash = "sha256:85f6665692aeb4297e1295761c9f4a6c7f047e0bb6947847ab3abf6de9650a10"}, + {file = "pbs_installer-2026.9.1-py3-none-any.whl", hash = "sha256:d2c320a11e0313f559f4338fad3f698345d200d0506053c75d266730b2507dcb"}, + {file = "pbs_installer-2026.9.1.tar.gz", hash = "sha256:b21eb21a53557d197899e1bf3d8b7f4f264feb1c546327106fdfcc967cb3a704"}, ] [package.dependencies] @@ -2447,14 +2616,14 @@ install = ["backports.zstd (>=1.0.0) ; python_version < \"3.14\""] [[package]] name = "pkginfo" -version = "1.12.1.2" +version = "1.13" description = "Query metadata from sdists / bdists / installed packages." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "pkginfo-1.12.1.2-py3-none-any.whl", hash = "sha256:c783ac885519cab2c34927ccfa6bf64b5a704d7c69afaea583dd9b7afe969343"}, - {file = "pkginfo-1.12.1.2.tar.gz", hash = "sha256:5cd957824ac36f140260964eba3c6be6442a8359b8c48f4adf90210f33a04b7b"}, + {file = "pkginfo-1.13-py3-none-any.whl", hash = "sha256:37e1bfb7b5b149829e6f140a225b517c4c8808d8d164f68137b9492023874061"}, + {file = "pkginfo-1.13.tar.gz", hash = "sha256:4f70471c643f71fe84a06230c0e33c5c5b12a33b2fd21bff14ddaef5a974adb1"}, ] [package.extras] @@ -2462,14 +2631,14 @@ testing = ["pytest", "pytest-cov", "wheel"] [[package]] name = "platformdirs" -version = "4.10.0" +version = "4.11.12" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a"}, - {file = "platformdirs-4.10.0.tar.gz", hash = "sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7"}, + {file = "platformdirs-4.11.12-py3-none-any.whl", hash = "sha256:b1ba966ac76153d9985a0787ffab951e7c18416189873d4e4ddb2c8a04a0a254"}, + {file = "platformdirs-4.11.12.tar.gz", hash = "sha256:e8dc1cb58f1153fd7f61db1374317770baababec2480b37b8f01c6cc25b45267"}, ] [[package]] @@ -2490,14 +2659,14 @@ testing = ["coverage", "pytest", "pytest-benchmark"] [[package]] name = "poetry" -version = "2.4.1" +version = "2.5.1" description = "Python dependency management and packaging made easy." optional = false python-versions = "<4.0,>=3.10" groups = ["dev"] files = [ - {file = "poetry-2.4.1-py3-none-any.whl", hash = "sha256:a91f13279a3c9add0d12c5ca5c7cb173622930a5c8272fee68c751cb5c72f951"}, - {file = "poetry-2.4.1.tar.gz", hash = "sha256:189399b80347ecf908244b2564a7b1d92b648fa1fe2a204888f94a472fec0cac"}, + {file = "poetry-2.5.1-py3-none-any.whl", hash = "sha256:3f7910abc667ce0c33a67e306baf017acdec1c8eeec4aa64d176e530d8338a61"}, + {file = "poetry-2.5.1.tar.gz", hash = "sha256:19fbd566efced512be58989a756aa1333cdc9b6aabb4fc84fd1ec9d29ee74874"}, ] [package.dependencies] @@ -2513,7 +2682,7 @@ packaging = ">=24.2" pbs-installer = {version = ">=2025.6.10", extras = ["download", "install"]} pkginfo = ">=1.12,<2.0" platformdirs = ">=3.0.0,<5" -poetry-core = "2.4.0" +poetry-core = "2.5.0" pyproject-hooks = ">=1.0.0,<2.0.0" requests = ">=2.26,<3.0" requests-toolbelt = ">=1.0.0,<2.0.0" @@ -2525,26 +2694,26 @@ xattr = {version = ">=1.0.0,<2.0.0", markers = "sys_platform == \"darwin\""} [[package]] name = "poetry-core" -version = "2.4.0" +version = "2.5.0" description = "Poetry PEP 517 Build Backend" optional = false python-versions = "<4.0,>=3.10" groups = ["dev"] files = [ - {file = "poetry_core-2.4.0-py3-none-any.whl", hash = "sha256:4305848477da00272bebd3f615bbec87f64bd117cdb858ab660b626a06a9d96c"}, - {file = "poetry_core-2.4.0.tar.gz", hash = "sha256:4e8c7496cf797998ffc493f2e23eba4b038c894c08eadacdcdf688945de6b43a"}, + {file = "poetry_core-2.5.0-py3-none-any.whl", hash = "sha256:98ee465b66f7e14bb9fbe68fb5e00d0cff184f036993ec94610ae4b5612f3add"}, + {file = "poetry_core-2.5.0.tar.gz", hash = "sha256:81d04c9253b19d0604718268d781867c8f7b2128e5b25bbf1e84141eec6b89c4"}, ] [[package]] name = "prometheus-client" -version = "0.25.0" +version = "0.26.0" description = "Python client for the Prometheus monitoring system." optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "prometheus_client-0.25.0-py3-none-any.whl", hash = "sha256:d5aec89e349a6ec230805d0df882f3807f74fd6c1a2fa86864e3c2279059fed1"}, - {file = "prometheus_client-0.25.0.tar.gz", hash = "sha256:5e373b75c31afb3c86f1a52fa1ad470c9aace18082d39ec0d2f918d11cc9ba28"}, + {file = "prometheus_client-0.26.0-py3-none-any.whl", hash = "sha256:fa93d06737aa02bacd05794768508bb97d2fbee28cb3bca04eaae92f0ca953d6"}, + {file = "prometheus_client-0.26.0.tar.gz", hash = "sha256:04a91bcf94e2cf74a44a1a874d651a2e853ed354b6e822f3b7487751465d5c2b"}, ] [package.extras] @@ -2554,115 +2723,115 @@ twisted = ["twisted"] [[package]] name = "proto-plus" -version = "1.28.1" +version = "1.28.4" description = "Beautiful, Pythonic protocol buffers" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "proto_plus-1.28.1-py3-none-any.whl", hash = "sha256:6660f5f1970874bdcfc3088b435188a36a37bd3596668f7d726417c4ae8cfbed"}, - {file = "proto_plus-1.28.1.tar.gz", hash = "sha256:832e68e7fe064cf90ab153b6e5eb935b27891bb89aaeb68b115e9b702f6cb168"}, + {file = "proto_plus-1.28.4-py3-none-any.whl", hash = "sha256:4b01341272f8a348db3f003b6143109f83ab43091019d5181b3fcdf500ab32aa"}, + {file = "proto_plus-1.28.4.tar.gz", hash = "sha256:5ff7ecad828e032a491fcb86947801768e32237f99dd049b649965b892ae9a63"}, ] [package.dependencies] -protobuf = ">=4.25.8,<8.0.0" +protobuf = ">=6.33.5,<8.0.0" [package.extras] -testing = ["google-api-core (>=1.31.5)"] +testing = ["google-api-core (>=2.25.0)"] [[package]] name = "protobuf" -version = "7.35.1" +version = "7.36.2" description = "" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "protobuf-7.35.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:24f857477359a85c0c235261b8ba905fd51b2562f4a64ca1df5473f29850cbf6"}, - {file = "protobuf-7.35.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:11d6b0ec246892d85215b0a13ca6e0233cf5284b68f0ac02646427f4ff88a799"}, - {file = "protobuf-7.35.1-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:b73f9489a4b8b1c9cb1f8ed951c736392592edb24b9d6819f36d2e10b171d5b4"}, - {file = "protobuf-7.35.1-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:74758715c53d7158fb76caf4f0cfdacc5329a4b1bb994f865d6cf302d413a1c4"}, - {file = "protobuf-7.35.1-cp310-abi3-win32.whl", hash = "sha256:353652e4efd0bca5b5fc2656abf8307ef351f0cf938c9eba09f0e09c20a25c30"}, - {file = "protobuf-7.35.1-cp310-abi3-win_amd64.whl", hash = "sha256:230a75ddfc2de4806e56696ce9640c1cdfdb6543b7cfce98d42a4c0a0e7bdb87"}, - {file = "protobuf-7.35.1-py3-none-any.whl", hash = "sha256:4bc97768d8fe4ad6743c8a19403e314511ed9f6d13205b687e52421c023ac1b9"}, - {file = "protobuf-7.35.1.tar.gz", hash = "sha256:ce115a26fe0c39a2c29973d914d327e516a6455464489fe3cd1e51a1b354f81a"}, + {file = "protobuf-7.36.2-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:cbc70b17ee27e28894c7fee8bb04be1abead49e936bc70eb60052531eee2079e"}, + {file = "protobuf-7.36.2-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:e11e1f0180583a2af89db6a2ecd9e8dc40aa6d2988ca175bfd0e6d12ea72d74e"}, + {file = "protobuf-7.36.2-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:f4fee11ec330d238b34a05c9b675f693c20415d1c5bd7d5320cc2f8a798eb9cf"}, + {file = "protobuf-7.36.2-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:89f23aa53c24553a2416fd4fd1ec06f74fa42b14b546d8883128813f775bbfd2"}, + {file = "protobuf-7.36.2-cp310-abi3-win32.whl", hash = "sha256:912c1221170e16c08d1f086762f563dd61ff83c18b5fa6652952dfaded66f728"}, + {file = "protobuf-7.36.2-cp310-abi3-win_amd64.whl", hash = "sha256:a300819d441e078a5608c0d3c709796bb548136058fda017ae51d425b44fd353"}, + {file = "protobuf-7.36.2-py3-none-any.whl", hash = "sha256:bdb3a345d48db958e6ce1f18e508beb0cc981d64f24088427549c866cd039f1e"}, + {file = "protobuf-7.36.2.tar.gz", hash = "sha256:497d0463ff3316681da6c0b9e8d06cb465d61abce00b613ab42226175644d1bb"}, ] [[package]] name = "psycopg2-binary" -version = "2.9.12" +version = "2.9.13" description = "psycopg2 - Python-PostgreSQL Database Adapter" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "psycopg2_binary-2.9.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9b818ceff717f98851a64bffd4c5eb5b3059ae280276dcecc52ac658dcf006a4"}, - {file = "psycopg2_binary-2.9.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d2fa0d7caca8635c56e373055094eeda3208d901d55dd0ff5abc1d4e47f82b56"}, - {file = "psycopg2_binary-2.9.12-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:864c261b3690e1207d14bbfe0a61e27567981b80c47a778561e49f676f7ce433"}, - {file = "psycopg2_binary-2.9.12-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c5ee5213445dd45312459029b8c4c0a695461eb517b753d2582315bd07995f5e"}, - {file = "psycopg2_binary-2.9.12-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6f9cae1f848779b5b01f417e762c40d026ea93eb0648249a604728cda991dde3"}, - {file = "psycopg2_binary-2.9.12-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:63a3ebbd543d3d1eda088ac99164e8c5bac15293ee91f20281fd17d050aee1c4"}, - {file = "psycopg2_binary-2.9.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d6fcbba8c9fed08a73b8ac61ea79e4821e45b1e92bb466230c5e746bbf3d5256"}, - {file = "psycopg2_binary-2.9.12-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:36512911ebb2b60a0c3e44d0bb5048c1980aced91235d133b7874f3d1d93487c"}, - {file = "psycopg2_binary-2.9.12-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:8ffdb59fe88f99589e34354a130217aa1fd2d615612402d6edc8b3dbc7a44463"}, - {file = "psycopg2_binary-2.9.12-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a46fe069b65255df410f856d842bc235f90e22ffdf532dda625fd4213d3fd9b1"}, - {file = "psycopg2_binary-2.9.12-cp310-cp310-win_amd64.whl", hash = "sha256:ab29414b25dcb698bf26bf213e3348abdcd07bbd5de032a5bec15bd75b298b03"}, - {file = "psycopg2_binary-2.9.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5c8ce6c61bd1b1f6b9c24ee32211599f6166af2c55abb19456090a21fd16554b"}, - {file = "psycopg2_binary-2.9.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b4a9eaa6e7f4ff91bec10aa3fb296878e75187bced5cc4bafe17dc40915e1326"}, - {file = "psycopg2_binary-2.9.12-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c6528cefc8e50fcc6f4a107e27a672058b36cc5736d665476aeb413ba88dbb06"}, - {file = "psycopg2_binary-2.9.12-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e4e184b1fb6072bf05388aa41c697e1b2d01b3473f107e7ec44f186a32cfd0b8"}, - {file = "psycopg2_binary-2.9.12-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4766ab678563054d3f1d064a4db19cc4b5f9e3a8d9018592a8285cf200c248f3"}, - {file = "psycopg2_binary-2.9.12-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5a0253224780c978746cb9be55a946bcdaf40fe3519c0f622924cdabdafe2c39"}, - {file = "psycopg2_binary-2.9.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0dc9228d47c46bda253d2ecd6bb93b56a9f2d7ad33b684a1fa3622bf74ffe30c"}, - {file = "psycopg2_binary-2.9.12-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f921f3cd87035ef7df233383011d7a53ea1d346224752c1385f1edfd790ceb6a"}, - {file = "psycopg2_binary-2.9.12-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3d999bd982a723113c1a45b55a7a6a90d64d0ed2278020ed625c490ff7bef96c"}, - {file = "psycopg2_binary-2.9.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29d4d134bd0ab46ffb04e94aa3c5fa3ef582e9026609165e2f758ff76fc3a3be"}, - {file = "psycopg2_binary-2.9.12-cp311-cp311-win_amd64.whl", hash = "sha256:cb4a1dacdd48077150dc762a9e5ddbf32c256d66cb46f80839391aa458774936"}, - {file = "psycopg2_binary-2.9.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5cdc05117180c5fa9c40eea8ea559ce64d73824c39d928b7da9fb5f6a9392433"}, - {file = "psycopg2_binary-2.9.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d3227a3bc228c10d21011a99245edca923e4e8bf461857e869a507d9a41fe9f6"}, - {file = "psycopg2_binary-2.9.12-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:995ce929eede89db6254b50827e2b7fd61e50d11f0b116b29fffe4a2e53c4580"}, - {file = "psycopg2_binary-2.9.12-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9fe06d93e72f1c048e731a2e3e7854a5bfaa58fc736068df90b352cefe66f03f"}, - {file = "psycopg2_binary-2.9.12-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40e7b28b63aaf737cb3a1edc3a9bbc9a9f4ad3dcb7152e8c1130e4050eddcb7d"}, - {file = "psycopg2_binary-2.9.12-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:89d19a9f7899e8eb0656a2b3a08e0da04c720a06db6e0033eab5928aabe60fa9"}, - {file = "psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:612b965daee295ae2da8f8218ce1d274645dc76ef3f1abf6a0a94fd57eff876d"}, - {file = "psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b9a339b79d37c1b45f3235265f07cdeb0cb5ad7acd2ac7720a5920989c17c24e"}, - {file = "psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:3471336e1acfd9c7fe507b8bad5af9317b6a89294f9eb37bd9a030bb7bebcdc6"}, - {file = "psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7af18183109e23502c8b2ae7f6926c0882766f35b5175a4cd737ad825e4d7a1b"}, - {file = "psycopg2_binary-2.9.12-cp312-cp312-win_amd64.whl", hash = "sha256:398fcd4db988c7d7d3713e2b8e18939776fd3fb447052daae4f24fa39daede4c"}, - {file = "psycopg2_binary-2.9.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7c729a73c7b1b84de3582f73cdd27d905121dc2c531f3d9a3c32a3011033b965"}, - {file = "psycopg2_binary-2.9.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4413d0caef93c5cf50b96863df4c2efe8c269bf2267df353225595e7e15e8df7"}, - {file = "psycopg2_binary-2.9.12-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4dfcf8e45ebb0c663be34a3442f65e17311f3367089cd4e5e3a3e8e62c978777"}, - {file = "psycopg2_binary-2.9.12-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c41321a14dd74aceb6a9a643b9253a334521babfa763fa873e33d89cfa122fb5"}, - {file = "psycopg2_binary-2.9.12-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83946ba43979ebfdc99a3cd0ee775c89f221df026984ba19d46133d8d75d3cd9"}, - {file = "psycopg2_binary-2.9.12-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:411e85815652d13560fbe731878daa5d92378c4995a22302071890ec3397d019"}, - {file = "psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c8ad4c08e00f7679559eaed7aff1edfffc60c086b976f93972f686384a95e2c"}, - {file = "psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:00814e40fa23c2b37ef0a1e3c749d89982c73a9cb5046137f0752a22d432e82f"}, - {file = "psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:98062447aebc20ed20add1f547a364fd0ef8933640d5372ff1873f8deb9b61be"}, - {file = "psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:66a7685d7e548f10fb4ce32fb01a7b7f4aa702134de92a292c7bd9e0d3dbd290"}, - {file = "psycopg2_binary-2.9.12-cp313-cp313-win_amd64.whl", hash = "sha256:b6937f5fe4e180aeee87de907a2fa982ded6f7f15d7218f78a083e4e1d68f2a0"}, - {file = "psycopg2_binary-2.9.12-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6f3b3de8a74ef8db215f22edffb19e32dc6fa41340456de7ec99efdc8a7b3ec2"}, - {file = "psycopg2_binary-2.9.12-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1006fb62f0f0bc5ce256a832356c6262e91be43f5e4eb15b5eaf38079464caf2"}, - {file = "psycopg2_binary-2.9.12-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:840066105706cd2eb29b9a1c2329620056582a4bf3e8169dec5c447042d0869f"}, - {file = "psycopg2_binary-2.9.12-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:863f5d12241ebe1c76a72a04c2113b6dc905f90b9cef0e9be0efd994affd9354"}, - {file = "psycopg2_binary-2.9.12-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a99eaab34a9010f1a086b126de467466620a750634d114d20455f3a824aae033"}, - {file = "psycopg2_binary-2.9.12-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ffdd7dc5463ccd61845ac37b7012d0f35a1548df9febe14f8dd549be4a0bc81e"}, - {file = "psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:54a0dfecab1b48731f934e06139dfe11e24219fb6d0ceb32177cf0375f14c7b5"}, - {file = "psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:96937c9c5d891f772430f418a7a8b4691a90c3e6b93cf72b5bd7cad8cbca32a5"}, - {file = "psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:77b348775efd4cdab410ec6609d81ccecd1139c90265fa583a7255c8064bc03d"}, - {file = "psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:527e6342b3e44c2f0544f6b8e927d60de7f163f5723b8f1dfa7d2a84298738cd"}, - {file = "psycopg2_binary-2.9.12-cp314-cp314-win_amd64.whl", hash = "sha256:f12ae41fcafadb39b2785e64a40f9db05d6de2ac114077457e0e7c597f3af980"}, - {file = "psycopg2_binary-2.9.12-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ee2d84ef5eb6c04702d2e9c372ad557fb027f26a5d82804f749dfb14c7fdd2ab"}, - {file = "psycopg2_binary-2.9.12-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cfa2517c94ea3af6deb46f81e1bbd884faa63e28481eb2f889989dd8d95e5f03"}, - {file = "psycopg2_binary-2.9.12-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:ba3df2fc42a1cfa45b72cf096d4acb2b885937eedc61461081d53538d4a82a86"}, - {file = "psycopg2_binary-2.9.12-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:718e1fc18edf573b02cb8aea868de8d8d33f99ce9620206aa9144b67b0985e94"}, - {file = "psycopg2_binary-2.9.12-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5c7cb4cbf894a1d36c720d713de507952c7c58f66d30834708f03dbe5c822ccf"}, - {file = "psycopg2_binary-2.9.12-cp39-cp39-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:049366c6d884bdcd65d66e6ca1fdbebe670b56c6c9ba46f164e6667e90881964"}, - {file = "psycopg2_binary-2.9.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fb1828cf3da68f99e45ebce1355d65d2d12b6a78fb5dfb16247aad6bdef5f5d2"}, - {file = "psycopg2_binary-2.9.12-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:127467c6e476dd876634f17c3d870530e73ff454ff99bff73d36e80af28e1115"}, - {file = "psycopg2_binary-2.9.12-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:ace94261f43850e9e79f6c56636c5e0147978ab79eda5e5e5ebf13ae146fc8fe"}, - {file = "psycopg2_binary-2.9.12-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a7e39a65b7d2a20e4ba2e0aaad1960b61cc2888d6ab047769f8347bd3c9ad915"}, - {file = "psycopg2_binary-2.9.12-cp39-cp39-win_amd64.whl", hash = "sha256:f625abb7020e4af3432d95342daa1aa0db3fa369eed19807aa596367ba791b10"}, - {file = "psycopg2_binary-2.9.12.tar.gz", hash = "sha256:5ac9444edc768c02a6b6a591f070b8aae28ff3a99be57560ac996001580f294c"}, + {file = "psycopg2_binary-2.9.13-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c519e406287085f43aa0d3061936edf1ba51286093532f215315c6ab8ba92c3b"}, + {file = "psycopg2_binary-2.9.13-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:086659ab083119f7ee87a779e31b94211cf162b708fc9a6bec771f75c73ac3e6"}, + {file = "psycopg2_binary-2.9.13-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1f4c7bdbafdf9dc018efbc29213b73f8308332888ba76a4cf503f560bfd21705"}, + {file = "psycopg2_binary-2.9.13-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d2fc9342aad969b9a28490a4c3eaba94b35beb2d26e9a39b31d1430378aa71b2"}, + {file = "psycopg2_binary-2.9.13-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f124954a32640dfb5c000d33028f48053930d7ff226bc74cde5fb316f9c6fcb6"}, + {file = "psycopg2_binary-2.9.13-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c24c98fe1a113db287dfb1958771eafca97b7db812f23b7897c2a12b6b904c22"}, + {file = "psycopg2_binary-2.9.13-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f4cdfe41149dcc5583a3b7a2f0ad433f75bb3afd1c7a7332e63df89b05e34666"}, + {file = "psycopg2_binary-2.9.13-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:33a6d3c47f9655b481b2cdc1b4bf71c235e054e55663d3066036b6ce5fbe5165"}, + {file = "psycopg2_binary-2.9.13-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:202dedd5cadb3e5dfd4d0415ab2fc5d5b44f4208de5308938e3e74ae222b638e"}, + {file = "psycopg2_binary-2.9.13-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:db31cf7f617a51625f1473d8a66fc35dac159af8b28e80bc014ed3ee994a9fbf"}, + {file = "psycopg2_binary-2.9.13-cp310-cp310-win_amd64.whl", hash = "sha256:28eb30bf4a52c1117406f45771038faa96f882fdeeeb0ce43b960a1dbc6c1fd2"}, + {file = "psycopg2_binary-2.9.13-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d19aec88857d2a52f99eefcefdbbb45921fb2f777bee5186a355a23d9cf8a0b9"}, + {file = "psycopg2_binary-2.9.13-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:32cd049095135d2b69e824aea9056745a4aaaa9115a9febbc65584793665d0d0"}, + {file = "psycopg2_binary-2.9.13-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6e696297891b56ff0115f0665de6ad774e1e301e4f60745b8d5024001ae7c2f6"}, + {file = "psycopg2_binary-2.9.13-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:930e7e58b33a4f9c39e7532d7a40147925cf3372baed4229cbebe0cf3ba9ce6b"}, + {file = "psycopg2_binary-2.9.13-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3aea95340825f5ff236e7b40f0b5602c2c77a1e95943f71fae34909834043d29"}, + {file = "psycopg2_binary-2.9.13-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:27e539b4cafd5e03dcd32921db1b12dd72fe549dd06bae6d4d2a5b5838465f24"}, + {file = "psycopg2_binary-2.9.13-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a6444ac48e2c04f691c2ddd542b38ba30c89463a2d446b3d74ec7d8fc90c964"}, + {file = "psycopg2_binary-2.9.13-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:8cb734989420c18ca1b71a82da880e11988f5ff3fcdaadd669161de3e98794ac"}, + {file = "psycopg2_binary-2.9.13-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f47f23db2d70db39cfb714b64fd5df76595b51b2ec0a669710a78f2dceb0c3f8"}, + {file = "psycopg2_binary-2.9.13-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f28b5f2fa8154d0d97e97a664136f58d1639ca008d45d6e09e69fff24826abee"}, + {file = "psycopg2_binary-2.9.13-cp311-cp311-win_amd64.whl", hash = "sha256:70d091f5c3a6177fac50c0da20181ce0e0c053f1e43c872d5f75bd6d9429c020"}, + {file = "psycopg2_binary-2.9.13-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2bf9f97a6df69a5d89d054b8cf5257a0916096c479800715fbfe7974dbcb3a26"}, + {file = "psycopg2_binary-2.9.13-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:07b7bd9f410650c34c3532162cc329f112368d78a3fc8668cb1ea9df61bc11bf"}, + {file = "psycopg2_binary-2.9.13-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:0463c00f946517f3e69192a59e6601e023ff9de45ad0a875eda3d6b1bebeb7ce"}, + {file = "psycopg2_binary-2.9.13-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e3861eba31f8ea8663fd876166b032fd89179e42aa63764d6feb281f13f9eb60"}, + {file = "psycopg2_binary-2.9.13-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3dc3372b3731b3ef23407fe06b94f640ef87a2bda242fa386033d5589c87514a"}, + {file = "psycopg2_binary-2.9.13-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0405dd4d97720e7ab177aa02e493f524907c4cb3c445ac173e2627948d3d0528"}, + {file = "psycopg2_binary-2.9.13-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b6ae51708201f501a171b02419d0c30878a743c369c9054eb1289f0f8d5979e2"}, + {file = "psycopg2_binary-2.9.13-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:81682c227cc1849c4a6adf7b85274229073bb4c9d6ad5697222c695dcea5a8a7"}, + {file = "psycopg2_binary-2.9.13-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:13d955f6054a705a19554364fe9888d0a6e8b0746dc7ebc08a447c7b4fd4145c"}, + {file = "psycopg2_binary-2.9.13-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7e2405196a8cfe6cd3e54172a54452dcf85c241eaf2e9dde7190d7469f7f5ef7"}, + {file = "psycopg2_binary-2.9.13-cp312-cp312-win_amd64.whl", hash = "sha256:376ebf7d8aee4b7386b2bac31fdc27911e7e57cd0a88f1e038b8b149398ac008"}, + {file = "psycopg2_binary-2.9.13-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4d66bfd44a46eb88cff0287929a4193fb45166b6c1f84bb1b233cc17ece0813c"}, + {file = "psycopg2_binary-2.9.13-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f818161d2302b3b3e9c75d5a1d0a5c5679e92e45cfec6432b9d5432dde5ff1f1"}, + {file = "psycopg2_binary-2.9.13-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:31db6cba66df5231dfd91d9f69188bec3fe6c8baae384e93a0ce792067ee2d98"}, + {file = "psycopg2_binary-2.9.13-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f04ada42bcd537adbaf8b7f3140237a204e452a88d0c1831cfce69f7d2e59f4e"}, + {file = "psycopg2_binary-2.9.13-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa37089795bd9701576edc2eb5849ce77a439eda9dfdfa47857449332cfa5292"}, + {file = "psycopg2_binary-2.9.13-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:41c2eb569ebd0e1b02d30d361a46932923b193fe1b5e641fb4d547c75e218955"}, + {file = "psycopg2_binary-2.9.13-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3f699a5225094a5c61402984e2fc1eca20e940223e76767c88189efb0c313f69"}, + {file = "psycopg2_binary-2.9.13-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5f04ae99c9fbb94c3197ec88599ed7db921f6adcddfe83687a74c7ead4037c22"}, + {file = "psycopg2_binary-2.9.13-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:81404c37e0344ebcf10aac127d33d35137e5dbab1daf9f3deee46188fd5879c2"}, + {file = "psycopg2_binary-2.9.13-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:feb7b1856f6ca805cc0e08739858f6cdfed8ce903390126af30343c62899a389"}, + {file = "psycopg2_binary-2.9.13-cp313-cp313-win_amd64.whl", hash = "sha256:691da68ae5dd7c3ac77514357d35ece7b1ba8b5f3e6c92735198aa6159c355c8"}, + {file = "psycopg2_binary-2.9.13-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2ca263643ae37998ae04d18e431df34d0d61f12b47640dab585f14b6dbe00798"}, + {file = "psycopg2_binary-2.9.13-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c0214c7da18a28d108aa7108c8a3cca8035c7911ec97ef9ec0827569c9a2720"}, + {file = "psycopg2_binary-2.9.13-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5d89e064bb12b40cad696cf4975e6da86f8c60f14cd06cb6c1bc0a7f5d01761f"}, + {file = "psycopg2_binary-2.9.13-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:190c18b97d9ef72f2e88c451b6588af90d6bd7bf54cb94b963280dc86a2c7076"}, + {file = "psycopg2_binary-2.9.13-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c00ebe9a2f31151aade0db233dc1446513a95e92c39ce055ee097af0ae86be1c"}, + {file = "psycopg2_binary-2.9.13-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5085f7ff7b1e890f279577cedeb8c628957869a340fa34a39f7f406500b3c916"}, + {file = "psycopg2_binary-2.9.13-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4e55357d1943673d491bbabb171c891704fc6a22441fea539e05a5c27a79ea3c"}, + {file = "psycopg2_binary-2.9.13-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:3e60b06ec7f9dc3e5f1106d12706514b6d6b92c3dc438fcdf4e43e65cc660d1b"}, + {file = "psycopg2_binary-2.9.13-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:dde942b46ce20f6c4464cdf551f3293207f803f4e4354454eb1f5599c3eb1fa1"}, + {file = "psycopg2_binary-2.9.13-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:215777c62ce81c3b487cefdb6a41969944eb982309f91349ff3ca0323d6f17ed"}, + {file = "psycopg2_binary-2.9.13-cp314-cp314-win_amd64.whl", hash = "sha256:f3088eb80f58ed933c62d87128741d31e786edc862e23266d3c286763d646de0"}, + {file = "psycopg2_binary-2.9.13-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:38397def2d794ffde9db80f63d6820253e61b17483112652a318355f51a56f50"}, + {file = "psycopg2_binary-2.9.13-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:dff5c70ed9789ccb0d97ff4a7da51dc523a255c4ec95df188fa5d44adcae4ea8"}, + {file = "psycopg2_binary-2.9.13-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:08d3b81a6a91775c937abf97d4c58fc9142e8e35fb91c387d24f81d15c98e6cf"}, + {file = "psycopg2_binary-2.9.13-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:541a487a9ccd72b5e38f37f27b0ce78cb7eb3e336e7b5277d45463010c03a7a8"}, + {file = "psycopg2_binary-2.9.13-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:562fe2a43b30e781848dce63d9080c15414c777c96df348c4342558338cc7bf3"}, + {file = "psycopg2_binary-2.9.13-cp315-cp315-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dddfe650e7dda464d676c27fbedb5061f1ad05e1604627f54c770d7f799d36e9"}, + {file = "psycopg2_binary-2.9.13-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:4ff0f575cbb14f30445858dcfdd751e043486f5290915df78a9818bc74042eff"}, + {file = "psycopg2_binary-2.9.13-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:d79530b4c1af657d5620a1d21b8e39f2996aa06821d5564d05b22d6b8cd413d0"}, + {file = "psycopg2_binary-2.9.13-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:6ede8595767e19d30a7e8a84a7d47bfde6176d45d194fed08dbb68d1584a780b"}, + {file = "psycopg2_binary-2.9.13-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:0ebcf3c4266a695df9d0ef51296155f60c86ac51cf82f0d0dd2e827255a891c5"}, + {file = "psycopg2_binary-2.9.13-cp315-cp315-win_amd64.whl", hash = "sha256:1752b9821f1377404d65ac43af03d59a1eccc57fb2c1eb8305f9a3fe8eb7a8ba"}, + {file = "psycopg2_binary-2.9.13.tar.gz", hash = "sha256:e324ecf60f952d21dd11413b8bbed0951bbd99579a06fd06f28bfc37737cd373"}, ] [[package]] @@ -2719,19 +2888,19 @@ markers = {main = "platform_python_implementation != \"PyPy\" and implementation [[package]] name = "pydantic" -version = "2.13.4" +version = "2.13.5" description = "Data validation using Python type hints" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba"}, - {file = "pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6"}, + {file = "pydantic-2.13.5-py3-none-any.whl", hash = "sha256:346a034f080da3755d8e9cb5e00e8b07de1d39e4f6e2c87d8ab7cafa0b269a73"}, + {file = "pydantic-2.13.5.tar.gz", hash = "sha256:51a9c5f7b2f8e636f04c6cada605d9b6a3bf1348fdf945a3d8869b19bba0ee08"}, ] [package.dependencies] annotated-types = ">=0.6.0" -pydantic-core = "2.46.4" +pydantic-core = "2.46.5" typing-extensions = ">=4.14.1" typing-inspection = ">=0.4.2" @@ -2741,132 +2910,132 @@ timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows [[package]] name = "pydantic-core" -version = "2.46.4" +version = "2.46.5" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4"}, - {file = "pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5"}, - {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba"}, - {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b"}, - {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c"}, - {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50"}, - {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd"}, - {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01"}, - {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d"}, - {file = "pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4"}, - {file = "pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f"}, - {file = "pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39"}, - {file = "pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d"}, - {file = "pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf"}, - {file = "pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594"}, - {file = "pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c"}, - {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826"}, - {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04"}, - {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e"}, - {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3"}, - {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4"}, - {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398"}, - {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3"}, - {file = "pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848"}, - {file = "pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3"}, - {file = "pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109"}, - {file = "pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda"}, - {file = "pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33"}, - {file = "pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d"}, - {file = "pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2"}, - {file = "pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f"}, - {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7"}, - {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7"}, - {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712"}, - {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4"}, - {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce"}, - {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987"}, - {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b"}, - {file = "pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458"}, - {file = "pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b"}, - {file = "pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c"}, - {file = "pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894"}, - {file = "pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89"}, - {file = "pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a"}, - {file = "pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008"}, - {file = "pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4"}, - {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76"}, - {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3"}, - {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76"}, - {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4"}, - {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a"}, - {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262"}, - {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e"}, - {file = "pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd"}, - {file = "pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be"}, - {file = "pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d"}, - {file = "pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb"}, - {file = "pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292"}, - {file = "pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d"}, - {file = "pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb"}, - {file = "pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462"}, - {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9"}, - {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4"}, - {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914"}, - {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28"}, - {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b"}, - {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c"}, - {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb"}, - {file = "pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898"}, - {file = "pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e"}, - {file = "pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519"}, - {file = "pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4"}, - {file = "pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac"}, - {file = "pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a"}, - {file = "pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0"}, - {file = "pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d"}, - {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b"}, - {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000"}, - {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e"}, - {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd"}, - {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3"}, - {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7"}, - {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff"}, - {file = "pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424"}, - {file = "pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6"}, - {file = "pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565"}, - {file = "pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02"}, - {file = "pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5"}, - {file = "pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596"}, - {file = "pydantic_core-2.46.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:fd8b3d9fd264be37976686c7f65cd52a83f5e84f4bfd2adf9c1d469676bbb6ae"}, - {file = "pydantic_core-2.46.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9f444c499b3eefd3a92e348059471ea0c3a6e303d9c1cec09fa748fd9f895201"}, - {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3447661d99f75a3683a4cf5c87da72f2161964611864dbbeac7fbb118bb4bfc0"}, - {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b9bab013d1c7a79d3501ff86d0bc9c31bf587db4551677b96bec07df78c6b15"}, - {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d995260fdf4e1db774581b4900e0f832abe3c7c84996726bbc161b19c8f29e76"}, - {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f13a646d65d09fbf1bc6b3a9635d30095c8e7e5cc419ff35ecc563c5fd04cd49"}, - {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432c179df7874eeb73307aad2df0755e1ae0efa61ff0ea89b93e194411ae3928"}, - {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:e68b7a074f65a2fd746c52a7ce6142ab7006074ac269ace0c25cd8ba171f8066"}, - {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4a05d69cba51d852c5c3e92758653245a50c0b646ced0cf05bd793ed592839d6"}, - {file = "pydantic_core-2.46.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:228ee9bae8bef5b1e97ec58302f80357c37199e0d0a99174e138d28e6957b9d9"}, - {file = "pydantic_core-2.46.4-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:10e17cbb10a330363733efc4d7c4d0dd827ac0909b8f6a6542298fed1ea62f29"}, - {file = "pydantic_core-2.46.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:91a06d2e259ecfbd8c901d70c3c507900458498142b3026a296b7de4d1322cc9"}, - {file = "pydantic_core-2.46.4-cp39-cp39-win32.whl", hash = "sha256:d80ee3d731373b24cebbc10d689ca4ee1875caf0d5703a245db18efd4dd37fc1"}, - {file = "pydantic_core-2.46.4-cp39-cp39-win_amd64.whl", hash = "sha256:3be77f45df024d789a672ae34f8b06fb346c4f9f46ea714956660ea4862e89ac"}, - {file = "pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c"}, - {file = "pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b"}, - {file = "pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b"}, - {file = "pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea"}, - {file = "pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7"}, - {file = "pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df"}, - {file = "pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526"}, - {file = "pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0"}, - {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0"}, - {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7"}, - {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2"}, - {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9"}, - {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf"}, - {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30"}, - {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc"}, - {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983"}, - {file = "pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1"}, + {file = "pydantic_core-2.46.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:657b40d6240c0a7b6a64b30f22d1e3aa631c7e846c621b0c0f6d1d75e2e15ea6"}, + {file = "pydantic_core-2.46.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ecb42011e12ee19cafbc312887cbf3546959fe02fbad44f272d4be5baa997615"}, + {file = "pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4dedce55295becb61921e386b99d4f2706045306e7fa52249a33004c837379fb"}, + {file = "pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9f47b8a949e60f027f0aa0a6f6c7b7e9c55cbf4380d10b344e282fa4e7ab1e1b"}, + {file = "pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:200aa3dc9f8d54f0754f43247c0bad0999fdcfbfd2488384dd44f37279271fe6"}, + {file = "pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6d30e1a4f138b8951063e9a394752a9179b51da288ffa507b1e659222f4c1793"}, + {file = "pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:850a08d167dde16db8702c274f320c7be9d7da6f6dff2b58b18f9e815bd94f5b"}, + {file = "pydantic_core-2.46.5-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:c3471e5c4a949c26ec00a77f01df59096aa9495877de76fd60a980f8ee6be461"}, + {file = "pydantic_core-2.46.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3a3e26b6a8274211bddee2d0e4d0d42778f17a34510f49d2ec44b58abfc41736"}, + {file = "pydantic_core-2.46.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:fc5d783bd4a2387e97b8a2d5ec781cfb92b3d893bf82370548e99db5915935d3"}, + {file = "pydantic_core-2.46.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:356c8368cbc321050b169595683a2e1d63413b1e0e2868b330af9fc14c616d3f"}, + {file = "pydantic_core-2.46.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:eb7d8d0e5886a89a55d2eef490e272fa965a9d57c6b29a5b5088a7997ec2cad1"}, + {file = "pydantic_core-2.46.5-cp310-cp310-win32.whl", hash = "sha256:4d44cf99ddebf875f9b68cc267aa684c99b7b44fe63ee1cac4ec163807290069"}, + {file = "pydantic_core-2.46.5-cp310-cp310-win_amd64.whl", hash = "sha256:1e5aad1220a1192c42341c8fd4a8686657e73ab2a920c970bdc4de334fe3193d"}, + {file = "pydantic_core-2.46.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a1dee1b804ff4d11c663636cf15d2ea47e9f79cd56c033fb1cbf08924842a48f"}, + {file = "pydantic_core-2.46.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d625a186a65201c23a9e3b8ed9c47e90a026e03256608cc91851c6709096844f"}, + {file = "pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f8507560a9284e1370bb048ed4282012fbef4e8d109875b95e884d228552061"}, + {file = "pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f93c5fe914d75fbec9a49209b00da5f08e9e467d69da2b1510c81940cfd10be"}, + {file = "pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c767f552b21b10f774aeac128e828eafb796adfa1b666a18bf6321453c3a"}, + {file = "pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:701b2e04b560eeb4bddf7a25ab8ca476176e34fdbd9a0e18196f0d12d4685f0b"}, + {file = "pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49776eab08766a08dfff7012f8b422dcd7e25e43b316eedf0477c24fcfa84b7c"}, + {file = "pydantic_core-2.46.5-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:a2468d93d181667a7abd66e1b64bb9f76f361b0fef8faddf687456453576f5ee"}, + {file = "pydantic_core-2.46.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:53feb344243bb9510a9dec7bf3cf1b64d88a98af5dc7872a5160465f8b198c8e"}, + {file = "pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:cd5214352ae68f3b5e9af7768bdc5253695ee069675db3480518420b3be881f2"}, + {file = "pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:9432f3598db432cb51c5b37fdbf29a60fcccc79e30d37a05022776a6bc4ab689"}, + {file = "pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8feeac04b5794e513e710af2f9c87d49f31a6dc47967bb264a1fed61a8989bec"}, + {file = "pydantic_core-2.46.5-cp311-cp311-win32.whl", hash = "sha256:892a881d5f68c2b9ea304b7a6c2c60d9343df578a311b0f86b94bc8f1ffe8129"}, + {file = "pydantic_core-2.46.5-cp311-cp311-win_amd64.whl", hash = "sha256:40375c2d05acec10323e45dfe2077ac44bc74659008614af5069034e2cfc781c"}, + {file = "pydantic_core-2.46.5-cp311-cp311-win_arm64.whl", hash = "sha256:28a6a556cd3b6066bea827857f9d9cce027c96f776e512f544a581f9e42161f8"}, + {file = "pydantic_core-2.46.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b9fe6fb92520e3fd61f2e49000b6911b188824f089b75973ea06d6267f0b476d"}, + {file = "pydantic_core-2.46.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a39ac25a9a2fa4072efdb429833c4a4c8009a51ff9eea3eeae131713cd27991e"}, + {file = "pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fdc8b93a41521988916eeaa271173fcca7fa0803d62f87675aac8dcec1c8e29"}, + {file = "pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b98134087d9de723658d17a42c7d0da8d6e2ef08015dee7dc93889047315f5e4"}, + {file = "pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e652ab17569c94bff5475520f907b7148b8c24036a8ebbe5cf7cf7493d28579a"}, + {file = "pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d925f3d9afd05a8c0fb3a1031463a8d59ebe5e2afad297e29c78be19e13b4e62"}, + {file = "pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fc5be0abd4a407e200d844b404e33639a554e7bd0d448e7b9ae181be4789ac2"}, + {file = "pydantic_core-2.46.5-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:816ff0a6550ffc06c098ccd2e0698600f9aa7da192a79eaa6f9af504a35db869"}, + {file = "pydantic_core-2.46.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c7ea57fc63aa7da93a1bd2d644e6577befae10c52c4e36377635eea1056a74f5"}, + {file = "pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:efd62a42486f1bda5d24cb4f63d15a3c7768375fe83d36f9417b4ad7a2fb20b3"}, + {file = "pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:2bc9419666990c06d7397831f2126a1ecc3594aaa3ff7de5bf2d066802f4e07b"}, + {file = "pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:18a09e1e1011b462f2e32774f25859ef1223d5c2b0546a633cf56654710721e0"}, + {file = "pydantic_core-2.46.5-cp312-cp312-win32.whl", hash = "sha256:5cb482e9e84c851f4e623fe4acc1ced89168cf1fe18f7089db4548c8f5bbb65b"}, + {file = "pydantic_core-2.46.5-cp312-cp312-win_amd64.whl", hash = "sha256:5e81740c09e310f5aa5cbd3e434a01c154d4bef93241c7877b39f211d2b78ba8"}, + {file = "pydantic_core-2.46.5-cp312-cp312-win_arm64.whl", hash = "sha256:f7b0ec93a2893de856652154d73b7ba622f26fa97726487dcac373de5f4c6084"}, + {file = "pydantic_core-2.46.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:b7ca9034437b6022f941f4857459562ee00a560b97e7cce8a0ec5a74fc6766e0"}, + {file = "pydantic_core-2.46.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f332f0e72a5a0400141f830744e141bf9f97917878dbe968669e8a7fefea78ff"}, + {file = "pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:193375f3548919d3f0b60936ca113ada3e38f264f91b9b8e0508efaad57be931"}, + {file = "pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:79bdfa52f843137045b2d081cc05c120ba6665d29b7559c2c47690906f39279f"}, + {file = "pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:24922243639cbdac66c75fcb6fd6495a9cb52b213d62f9a0d16f0310b1ff8038"}, + {file = "pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c76fe65e607be28c7fd4d56fc3c42b1583aa058ce3408b7ad0fd540171d31f9f"}, + {file = "pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f7b393a8b3da82f5c1fc0751e6d01ac6c55b93c18226a60bdfba4a724efafd1"}, + {file = "pydantic_core-2.46.5-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:7ac031912d54f3d83ef3b3eb98dfabc1608802e2202263d25957eeed40b94761"}, + {file = "pydantic_core-2.46.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:837b396ca3d7b74091ca623f6cbd8351bd42d670a79c2683e79fb089f06a2de5"}, + {file = "pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:5ee239d575f80b08eca11f6e20f90c4c695de7825c67eefe6091fbf20dda648e"}, + {file = "pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:e80675d75ae2cd14372cb65cad5400d9347a3d3f6c13000183f22dfd027283ed"}, + {file = "pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:9c4b71f10dd532fb7a5cbc8f58707779e64f03a258c2bf8bfbaecfcd9970b519"}, + {file = "pydantic_core-2.46.5-cp313-cp313-win32.whl", hash = "sha256:97bf8de4d541598c94a59344eeb988a94c08ff76b5723c41f6567ec18c7892ea"}, + {file = "pydantic_core-2.46.5-cp313-cp313-win_amd64.whl", hash = "sha256:15f4a94963c95accac15b7b657bb177d3ad82bb90b0d0526d9a9b85079925db5"}, + {file = "pydantic_core-2.46.5-cp313-cp313-win_arm64.whl", hash = "sha256:d22a945598fb91236b4dd793a6e42e4f3dd7740bb5aace5ebd7d4c08d13bb575"}, + {file = "pydantic_core-2.46.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c1c43ad4339643d70ebb8124e1305a7dab423001eff58bb41a0f731adbc98355"}, + {file = "pydantic_core-2.46.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1a353f84de772f423b5ffb11d7ae352fbbef0f446f3c0b0af0f8236d7233606e"}, + {file = "pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5086029a57366b8cf81b130a43908738095c270c21a8d7f0e8bdfdb89718e2f3"}, + {file = "pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46c25dda9d092a06c08db76ffe0a197107904d0dfac653f7d5306bbcd6d6119c"}, + {file = "pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:37ea7b83c935e5b0d68c9449b82651accf78a10828b2c02b2f2d9e9496446c21"}, + {file = "pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e64e88d5585bea9ce95861079de72006c7fa6d3df4e3a3b65ba31eb979c15c9f"}, + {file = "pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d510bac3ee52247af28ed4bb18a1e799f040ac60fd2bf5ccd4c92f1fbe786f"}, + {file = "pydantic_core-2.46.5-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:a2a5e1d0ff29adddc9f6d6821a66302e4493f8ca898b715b6b1182c2c201ea0a"}, + {file = "pydantic_core-2.46.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:03b9666e41e35d8909852ba191a0607520f81b74eaf12ccf8737005dbb313821"}, + {file = "pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:a91c17edf6eea2402cb5457b4c89e99bc5ed1004aa34c4adf1d4258c1a5c22c2"}, + {file = "pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b49924c73a235e969511bf2aabdff3beebf9820931f646c80274d5d780010c47"}, + {file = "pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:2cbd9a5eff05e51c447c34dfa4632145b26b09120cf04bd0c871e44c1a5e1c9a"}, + {file = "pydantic_core-2.46.5-cp314-cp314-win32.whl", hash = "sha256:2d5d76654becf5efd62c9e51c3756c67b49498b0c9a40884934c40807adbd074"}, + {file = "pydantic_core-2.46.5-cp314-cp314-win_amd64.whl", hash = "sha256:fa10ef4112775900e7a0661068635eb67b2ab824fbde764de6e0e21982a93db0"}, + {file = "pydantic_core-2.46.5-cp314-cp314-win_arm64.whl", hash = "sha256:045ab3b6d308439e32b81cc173bba5b9018bc6ed896afd0c65b3b009b1699af5"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8816f3d218beb4b787de5c9759c259b8fa61f9dec42dc7811f320a33771778b7"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bce57638e08ac148e5778cce7feb968307a727d66f8e2274a543d0cf0c9ad6a3"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:976e1128455aa595ea04c79ccfedff1aaeab96ee013fcc916bed120c4f0ad94f"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b891faeedeafba41b2983e5001a81b6a915b69544c7e7570d1989ce1c36ac7"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f194189415698233dd1114a093a9b56e61e2c57e11b469be3b0506f46f0771c"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82a36973cf8a2ef5406f4fe2edbf8ed0c99629535d959e0b100c76a32535a111"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdbb78909f52b981d3b2d56b97328d71eb0b974c36bd77c920123a7ebb192829"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:52e24eacdb536cade636aa90fb851835222becff8484b7001fdc78cb0290f2aa"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:37ae34309d7bd8c0d61ab839668058f2a7962ea1fc51d105d2db228fe0618034"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:0cdbada856a1c69a7624a64d3d9aefe79300bd6ef827b43a4f265010b9b55184"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:545f26c504b27c3758439a5e6d9349931f0a04f855668d5fe323c89e82300a38"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:ff218293c9c806138dca139765e3b067621be52bcd93cdc14c7711be7ddc90a9"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-win32.whl", hash = "sha256:97cf3eb53a8cccacf9d46686a0926186c9bfb5574f2ed66d3639d5fe117cd3a9"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-win_amd64.whl", hash = "sha256:d2f9fc07a8042a8f95925b35c4f04f469707c981fc33245b6ca187cf5d2dd290"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-win_arm64.whl", hash = "sha256:acf8a67ba51f4ca9ddbd0e6b3000a65ac51ab734661778b3e7ba64d99a710f2f"}, + {file = "pydantic_core-2.46.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c583b927a8838dab890706a6fa7573fbb8b70e24000ef9f7238e2d6f6435a5ed"}, + {file = "pydantic_core-2.46.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cdc8b74ecc48c0cb1e9607a05ec4e9e88db60a19ffcc9a1d5f9088ede40c8dc0"}, + {file = "pydantic_core-2.46.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b10e3e8fd7ddc2bd915848a2768e44c15b22936f1cc54c462ad1164deb02655"}, + {file = "pydantic_core-2.46.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f077d0b97ab11fa7dcc633fca53515f290bca8a8a633e966d5b6d1879d9ed01a"}, + {file = "pydantic_core-2.46.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7b0fc826b16c55e561e5d2a0c5c77b051ba1d92808118c4e4b5390f5e0cf191d"}, + {file = "pydantic_core-2.46.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef3fbbf161dc9351a2fe0422e51b129f9e97e42385bd0320b309c15f7d287dd8"}, + {file = "pydantic_core-2.46.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:978e7b97d4824b5be09c69fb70507cbde3b0323fc147332ca40a94d9a6a0ebbf"}, + {file = "pydantic_core-2.46.5-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:9b68938dd5b0c783d88ff8e2dcc69451b5eb936fe212d516b21b9d5567f6d464"}, + {file = "pydantic_core-2.46.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:771cf63ae0b1b50dd22e5f3e3549fab5f3f4ff1635d352a9e1a97fe01c7b2e64"}, + {file = "pydantic_core-2.46.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7c6be839a5a8312626b32029a415644a0846b420bc8b52b95b28cd92da162168"}, + {file = "pydantic_core-2.46.5-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:895395f8918627b04efb1ad2a4cf605387143300ba03304cd1dfa6d03f5e095e"}, + {file = "pydantic_core-2.46.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:fc8515076c11f3cfdf4fb142dcca0fe384b1230a3b5415458ac84f3e0903ec13"}, + {file = "pydantic_core-2.46.5-cp39-cp39-win32.whl", hash = "sha256:3d2652072b2d774947ba5cf78a9e59644ac62ee572daf6dd2e1dfe905e15b2b7"}, + {file = "pydantic_core-2.46.5-cp39-cp39-win_amd64.whl", hash = "sha256:3aa166e99c4f2985407fb8714aebede877ecb5455cf321b606adca926d30d5a0"}, + {file = "pydantic_core-2.46.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:c14ad3bdc85ee7f318742c457ca3968a92126d144b15721c759033bfb06296c2"}, + {file = "pydantic_core-2.46.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0bddb4020d8f04175865ccd17eff3040874fc11fb593f424edb452653b4b947c"}, + {file = "pydantic_core-2.46.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2471fd51c61c610e1dcf7de44d7299283661654d11264ab4802b303368d69c47"}, + {file = "pydantic_core-2.46.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b10ec717381bdbfafef34607824db4c91de69ff085e4fca3b2af91b4fa17e68a"}, + {file = "pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:013d6f3483d81e02e7c328831808f336c8596ee33b4bd4026b9ffb1e960b8942"}, + {file = "pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:e9c134bb666dd54b778b9fc0d2b50cbb7f979b9e3716f26a88c9ab3b6fc1dd0f"}, + {file = "pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:347ec774390c87326a2e4929d58d3f7e8763a104d5d35f4cd595a4c952366433"}, + {file = "pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e24d8f05fa2d28513d94e877e9c75ad66175376209b3977f916e240e623193c"}, + {file = "pydantic_core-2.46.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ab4b66edffb32d9e951efb3814bd104b8367a7501b81b955cacb5726d897389f"}, + {file = "pydantic_core-2.46.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:337639ba62a11acde6ef3aeb08c8ea755f8ef1fe5e513356c0f36a2b0d7568b0"}, + {file = "pydantic_core-2.46.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:413a717a410d0c817ef5b786a059415550b3794e1d0c2abffd9efb93a3d9f7b4"}, + {file = "pydantic_core-2.46.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1e449def1945a462c464331254e5a44fca7c3b4f9aedf59ec2f50f8066dd8e25"}, + {file = "pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:a445486499897b88a7d6c310c88ed64dd37b1b59bfd7ae9107490bbb362f47d6"}, + {file = "pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:2d330aaba8621b1edcec8ae2c4050f63b84ccf6d98723a8f212e9684713abf0e"}, + {file = "pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b6acfb46a814762367fb7ba0828b0a17d441b92ce249a0e007474c9072662dda"}, + {file = "pydantic_core-2.46.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d0a24b40877af2de4950252be9d21eaf7fb07660f3c2cae1f56c6b599ada5266"}, + {file = "pydantic_core-2.46.5.tar.gz", hash = "sha256:10416c15b8839ecc4ef4d0885da76da6fd0f67333a0eb8aff6d93c4b8f2910fc"}, ] [package.dependencies] @@ -2874,14 +3043,14 @@ typing-extensions = ">=4.14.1" [[package]] name = "pygments" -version = "2.20.0" +version = "2.21.0" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"}, - {file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"}, + {file = "pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9"}, + {file = "pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c"}, ] [package.extras] @@ -2889,14 +3058,14 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pyjwt" -version = "2.13.0" +version = "2.14.0" description = "JSON Web Token implementation in Python" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728"}, - {file = "pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423"}, + {file = "pyjwt-2.14.0-py3-none-any.whl", hash = "sha256:ad0cef71c756a56e74863c2919cf0985f72decbcfcb550ee2f422e7c62b5eedc"}, + {file = "pyjwt-2.14.0.tar.gz", hash = "sha256:77283c83fb56ecf566a886c757a714bc83668e38156de2cce8263302f42e0b86"}, ] [package.dependencies] @@ -2949,14 +3118,14 @@ tests = ["hypothesis (>=3.27.0)", "pytest (>=7.4.0)", "pytest-cov (>=2.10.1)", " [[package]] name = "pyparsing" -version = "3.3.2" +version = "3.3.3" description = "pyparsing - Classes and methods to define and execute parsing grammars" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d"}, - {file = "pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc"}, + {file = "pyparsing-3.3.3-py3-none-any.whl", hash = "sha256:ece8c00a69cf01b45d0b1dedabb469c90d8caf996d4fda40f147627a122849a4"}, + {file = "pyparsing-3.3.3.tar.gz", hash = "sha256:928ae7e20211f3b6f3915a72f06a0cfd29ab9d24279dd6346b6b1a7146397d36"}, ] [package.extras] @@ -2964,14 +3133,14 @@ diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pyproject-hooks" -version = "1.2.0" +version = "1.3.3" description = "Wrappers to call pyproject.toml-based build backend hooks." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913"}, - {file = "pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8"}, + {file = "pyproject_hooks-1.3.3-py3-none-any.whl", hash = "sha256:5fc53fdac9f7bd63fbcdc868fb5f90b4784d78a53a3d3388cd738b807441a20b"}, + {file = "pyproject_hooks-1.3.3.tar.gz", hash = "sha256:defda19b854fa0d3bd4f76ea4ddcba8abd7dcfcdd585a6690ade050744fc5f43"}, ] [[package]] @@ -3053,23 +3222,18 @@ six = ">=1.5" [[package]] name = "python-discovery" -version = "1.4.4" +version = "1.6.1" description = "Python interpreter discovery" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "python_discovery-1.4.4-py3-none-any.whl", hash = "sha256:abebe9120b43453b68c908acfb1e72a19d1a959ed2cb620ad38fc57d08056dbe"}, - {file = "python_discovery-1.4.4.tar.gz", hash = "sha256:5cad33982d412c1f3ffb8f9ca4ea292c9680bca3942451d30b69c37fce53a4a3"}, + {file = "python_discovery-1.6.1-py3-none-any.whl", hash = "sha256:d43fcdef879fe795352bd13ccf8d185ba5a9f86f36cfcd00529f596e737442b3"}, + {file = "python_discovery-1.6.1.tar.gz", hash = "sha256:cf87d3627dfb4412437fdd5b13eae402607722998d21567993aedbc59b23c15e"}, ] [package.dependencies] filelock = ">=3.15.4" -platformdirs = ">=4.3.6,<5" - -[package.extras] -docs = ["furo (>=2025.12.19)", "sphinx (>=9.1)", "sphinx-autodoc-typehints (>=3.6.3)", "sphinxcontrib-mermaid (>=2)", "sphinxcontrib-towncrier (>=0.4)", "towncrier (>=25.8)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.5.4)", "pytest (>=8.3.5)", "pytest-mock (>=3.14)", "setuptools (>=75.1)"] [[package]] name = "python-jose" @@ -3096,14 +3260,14 @@ test = ["pytest", "pytest-cov"] [[package]] name = "pytz" -version = "2026.2" +version = "2026.3.post1" description = "World timezone definitions, modern and historical" optional = false python-versions = "*" groups = ["main"] files = [ - {file = "pytz-2026.2-py2.py3-none-any.whl", hash = "sha256:04156e608bee23d3792fd45c94ae47fae1036688e75032eea2e3bf0323d1f126"}, - {file = "pytz-2026.2.tar.gz", hash = "sha256:0e60b47b29f21574376f218fe21abc009894a2321ea16c6754f3cad6eb7cdd6a"}, + {file = "pytz-2026.3.post1-py2.py3-none-any.whl", hash = "sha256:dd95840dd199baea12d9cc096a1d452caa6596a1c1e4b5f3dbd1541855d5e815"}, + {file = "pytz-2026.3.post1.tar.gz", hash = "sha256:2211d3fcf9a797d3405cac96ac7f61d80e6a644f72a3309607282fe8a2010c5d"}, ] [[package]] @@ -3204,95 +3368,115 @@ files = [ [[package]] name = "rapidfuzz" -version = "3.14.5" +version = "3.14.6" description = "rapid fuzzy string matching" optional = false -python-versions = ">=3.10" +python-versions = ">=3.11" groups = ["dev"] files = [ - {file = "rapidfuzz-3.14.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:071d96b957a33b9296b9284b6350a0fb6d030b154a04efd7c15e56b98b79a517"}, - {file = "rapidfuzz-3.14.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:667f40fe9c81ad129b198d236881b00dd9e8314d9cc72d03c3e16bdfe5879051"}, - {file = "rapidfuzz-3.14.5-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9fff308486bbd2c8c24f25e8e152c7594d3fe8db265a2d6a1ce24d58671127f"}, - {file = "rapidfuzz-3.14.5-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dfa552338f51aec280f17b02d28bace1e162d1a84ccd80e3339a57f98aedb56b"}, - {file = "rapidfuzz-3.14.5-cp310-cp310-manylinux_2_39_riscv64.whl", hash = "sha256:068b3e965ca9d9ee4debe40001ae7c3938ba646308afd33cf0c66618147db65c"}, - {file = "rapidfuzz-3.14.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:88b7d31ff1cc5e9bc0e4406e6b1fa00b6d37163d50bb58091e9b976ff1129faa"}, - {file = "rapidfuzz-3.14.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:eacb434410b8d9ca99a8d42352ef085cf423e3c76c1f0b86be2fcba3bff2952c"}, - {file = "rapidfuzz-3.14.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:649712823f3abcdc48427147a5384fac15623ba435d0013959b52e6462521397"}, - {file = "rapidfuzz-3.14.5-cp310-cp310-win32.whl", hash = "sha256:13cb79c23ef5516e4c4e3830877be8b19aa75203636be1163d690d37803f6504"}, - {file = "rapidfuzz-3.14.5-cp310-cp310-win_amd64.whl", hash = "sha256:f2073495a7f9b75e57e600747ac09510d67683fd64d3228e009740b7ef88f9fe"}, - {file = "rapidfuzz-3.14.5-cp310-cp310-win_arm64.whl", hash = "sha256:8166efddea49fdbc61185559f47593239e4794fd7c9044dd5a789d1a90af852d"}, - {file = "rapidfuzz-3.14.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e251126d48615e1f02b4a178f2cd0cd4f0332b8a019c01a2e10480f7552554b4"}, - {file = "rapidfuzz-3.14.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5ab449c9abd0d4e1f8145dce0798a4c822a1a1933d613c764a641bea88b8bdab"}, - {file = "rapidfuzz-3.14.5-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cb2829fedd672dd7107267189dabe2bbe07972801d636014417c6861eb89e358"}, - {file = "rapidfuzz-3.14.5-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3d50e5861872935fece391351cbb5ba21d1bced277cf5e1143d207a0a35f1925"}, - {file = "rapidfuzz-3.14.5-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:7092a216728f80c960bd6b3807275d1ee318b168986bd5dc523349581d4890b8"}, - {file = "rapidfuzz-3.14.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9669753caef7fdc6529f6adcc5883ed98d65976445d9322e7dbdb6b697feee13"}, - {file = "rapidfuzz-3.14.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:823b1b9d9230809d8edcc18872770764bfe8ef4357995e16744047c8ccf0e489"}, - {file = "rapidfuzz-3.14.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f0b2af76b7e7060c09e1a0dfa9410eb19369cbe6164509bff2ef94094b54d2b6"}, - {file = "rapidfuzz-3.14.5-cp311-cp311-win32.whl", hash = "sha256:c5801a89604c65ab4cc9e91b23bc4076d0ca80efd8c976fb63843d7879a85d7f"}, - {file = "rapidfuzz-3.14.5-cp311-cp311-win_amd64.whl", hash = "sha256:d7ca16637c0ede8243f84074044bd0b2335a0341421f8227c85756de2d18c819"}, - {file = "rapidfuzz-3.14.5-cp311-cp311-win_arm64.whl", hash = "sha256:8c90cdf8516d9057e502aa6003cea71cf5ec27cc44699ca52412b502a04761bb"}, - {file = "rapidfuzz-3.14.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0d3378f471ef440473a396ce2f8e97ee12f89a78b495540e0a5617bbfe895638"}, - {file = "rapidfuzz-3.14.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e910eebca9fd0eba245c0555e764597e8a0cccb673a92da2dc2397050725f48"}, - {file = "rapidfuzz-3.14.5-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:01550fe5f60fd176aa66b7611289d46dc4aa4b1b904874c7b6d1d54e581c5ec1"}, - {file = "rapidfuzz-3.14.5-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:48bee0b91bebfaec41e1081e351000659ab7570cc4598d617aa04d5bf827f9e6"}, - {file = "rapidfuzz-3.14.5-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:7e580cb04ad849ae9b786fa21383c6b994b6e6c1444ad1cb9f22392759d72741"}, - {file = "rapidfuzz-3.14.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:09d6c9ba091854f07817055d795d604179c12a8f308ba4c7d56f3719dfea1646"}, - {file = "rapidfuzz-3.14.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:1e989f86113be66574113b9c7bdf4793f3f863d248e47d911b355e05ca6b6b10"}, - {file = "rapidfuzz-3.14.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ebd1a18e2e47bc0b292a07e6ed9c3642f8aaa672d12253885f599b50807a4f9"}, - {file = "rapidfuzz-3.14.5-cp312-cp312-win32.whl", hash = "sha256:9981d38a703b86f0e315a3cd229fd1906fe1d91c989ed121fb975b3c849f89f5"}, - {file = "rapidfuzz-3.14.5-cp312-cp312-win_amd64.whl", hash = "sha256:d8375e3da319593389727c3187ccaf3e0e84199accc530866b8e0f2b79af05e9"}, - {file = "rapidfuzz-3.14.5-cp312-cp312-win_arm64.whl", hash = "sha256:478b59bb018a6780d73f33e38d0b3ec5e968a6c1ed42876b993dd456b7aa20e8"}, - {file = "rapidfuzz-3.14.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ebd8fd343bf8492a1e60bcb6dc99f90f74f65d98d8241a6b3e1fed225b76ecd6"}, - {file = "rapidfuzz-3.14.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6737b35d5af7479c5bf9710f7b17edd9d2c43128d974d25fb4ea653e42c64609"}, - {file = "rapidfuzz-3.14.5-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b002c7994cc9f2bc9d9856f0fbaee6e8072c983873846c92f25cefba5b2a925f"}, - {file = "rapidfuzz-3.14.5-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:17a34330cd2a538c1ce5d400b61ba358c5b72c654b928ff87b362e88f8b864c7"}, - {file = "rapidfuzz-3.14.5-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:95d937e74c1a7a1287dfb03b62a827be08ede10a155cf1af73bbf47f2b73ee6e"}, - {file = "rapidfuzz-3.14.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:46b92a9970dcc34f0096901c792644094cab49554ac3547f35e3aebbdf0a3610"}, - {file = "rapidfuzz-3.14.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e012177c8e8a8a0754ae0d6027d63042aa5ff036d9f40f07cb3466a6082e21b8"}, - {file = "rapidfuzz-3.14.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a2ae6f53f99c9a0eca7a0afc5b4e45fc73bc1dd4ac74c00509031d76df80ed98"}, - {file = "rapidfuzz-3.14.5-cp313-cp313-win32.whl", hash = "sha256:4a60f0057231188e3bd30216f7b4e0f279b11fa4ec818bb6c1d9f014d1562fbc"}, - {file = "rapidfuzz-3.14.5-cp313-cp313-win_amd64.whl", hash = "sha256:11bfc2ed8fbe4ab86bd516fadefab126f90e6dcadffa761739fcb304707dfd35"}, - {file = "rapidfuzz-3.14.5-cp313-cp313-win_arm64.whl", hash = "sha256:b486b5218808f6f4dc471b114b1054e63553db69705c97da0271f47bd706aedd"}, - {file = "rapidfuzz-3.14.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:39ef8658aaf67d51667e7bdaf7096f432333377d8302ac43c70b5df8a4cf89b8"}, - {file = "rapidfuzz-3.14.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9ad37a0be705b544af6296da8edddc260d10a8ae5462530fc9991f66498bb1f9"}, - {file = "rapidfuzz-3.14.5-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d45e06f60729e07d9b20c205f7e5cff90b6ef2584e852eecf46e045aea69627d"}, - {file = "rapidfuzz-3.14.5-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e52da10236aa6212de71b9e170bace65b64b129c0dea7fc243d6c9ce976f5074"}, - {file = "rapidfuzz-3.14.5-cp313-cp313t-manylinux_2_39_riscv64.whl", hash = "sha256:440d30faaf682ca496170a7f0cc5453ec942e3e079f0fd802c9a7f938dfb50a3"}, - {file = "rapidfuzz-3.14.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:56227a61fd3d17b0cd9793132431f3a3d07c8654be96794ba9f89fe0fc8b2d09"}, - {file = "rapidfuzz-3.14.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:2e83cd2e25bb4edd97b689d9979d9c3acccdaaf26ceac08212ceece202febcfa"}, - {file = "rapidfuzz-3.14.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:af3b859726cd3374287e405e14b9634563c078c5531a4f62375508addebddad1"}, - {file = "rapidfuzz-3.14.5-cp313-cp313t-win32.whl", hash = "sha256:8ce1d850b3c0178440efde9e884d98421b5e87ff925f364d6d79e23910d7593f"}, - {file = "rapidfuzz-3.14.5-cp313-cp313t-win_amd64.whl", hash = "sha256:c84af70bcf34e99aee894e46a0f1ac77f17d0ef828179c387407642e2466d28a"}, - {file = "rapidfuzz-3.14.5-cp313-cp313t-win_arm64.whl", hash = "sha256:aac0ad28c686a5e72b81668b906c030ee28050b244544b8af68e12fb32543895"}, - {file = "rapidfuzz-3.14.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1a31cc6d7d03e7318a0974c038959c59e19c752b81115f2e9138b3331cd64d45"}, - {file = "rapidfuzz-3.14.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0298d357e2bc59d572da4db0bc631009b6f8f6c9bc8c11e99a12b833f16b6575"}, - {file = "rapidfuzz-3.14.5-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:59b3dba758661a318995655435c6ab20a04ade79fa51e75bc8dc107cac8df280"}, - {file = "rapidfuzz-3.14.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4900143d82071bdda533b00300c40b14b963ff826b3642cc463b6dd0f036585e"}, - {file = "rapidfuzz-3.14.5-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:feedf219672eef83ea6be6f3bb093bba396a8560fc75be85ba225f082903df0a"}, - {file = "rapidfuzz-3.14.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:419e4397a36e2665ec992d8d64c20ba4b2a42500c76ecadeca78a4f19cb9cc32"}, - {file = "rapidfuzz-3.14.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:97131ab2be39043054ee28d99e09efe316e6d53449b7e962dfcf3c2de8b2b246"}, - {file = "rapidfuzz-3.14.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:593c00dac4e30231c35bf3b4f1da8ec0998762e9e94425586a5d636fcd57f9d0"}, - {file = "rapidfuzz-3.14.5-cp314-cp314-win32.whl", hash = "sha256:0084b687b02b4e569b46d8d6d4ad25659528e6081cd6d067ca453a69035f07e4"}, - {file = "rapidfuzz-3.14.5-cp314-cp314-win_amd64.whl", hash = "sha256:5dfa89d78f22cd773054caff44827b846161a29f2dcf7e78b8f90d086621e502"}, - {file = "rapidfuzz-3.14.5-cp314-cp314-win_arm64.whl", hash = "sha256:67f3f9d2b444268ab53e47d31bab89954888d23c04c6789f2c727e51fe4b1d13"}, - {file = "rapidfuzz-3.14.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:77eac0526899b3c3ad1454bb2b03cdb491d67358ec8ef0c9c48bd61b632b431d"}, - {file = "rapidfuzz-3.14.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b9c6bd754d11f6e78ac54e3d86b4b11dc1ba2f13e5fc958899574532897f5a99"}, - {file = "rapidfuzz-3.14.5-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:738c96944d076deeaff70e92b65696ab4f7ecb8081d7791c5403a3257dfaf8ff"}, - {file = "rapidfuzz-3.14.5-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4c1bca487a17fe4226b4ffb2d30e799d2b274d692cffa76bd0746f56235fca3"}, - {file = "rapidfuzz-3.14.5-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:af6a90a4ed2a48fa1a2d17e9d824e6c7c950bea5bad0b707c77fd55751e6bfef"}, - {file = "rapidfuzz-3.14.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bf5018938208d4597b2e679a4f8cff9fd252f1df53583130ae56281a21801b64"}, - {file = "rapidfuzz-3.14.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c0919d1f89ddf91129906705723118ea09754171e4116f5a5dbc667c7bc9b261"}, - {file = "rapidfuzz-3.14.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:93d8da883a35116d6813432177f35e570db5b0a5e30ecb0cbd7cb39c815735df"}, - {file = "rapidfuzz-3.14.5-cp314-cp314t-win32.whl", hash = "sha256:0f23e37019ec07712d58976b1ab2b889f8649a7f7c2f626a2f34ea9139e79279"}, - {file = "rapidfuzz-3.14.5-cp314-cp314t-win_amd64.whl", hash = "sha256:7d5ca9c7832e6879a707296d1463685f7c243a27846227044504741640caec66"}, - {file = "rapidfuzz-3.14.5-cp314-cp314t-win_arm64.whl", hash = "sha256:3e91dcd2549b8f8d843f98ba03a17e01f3d8b72ce942adbbb6761bc58ffce813"}, - {file = "rapidfuzz-3.14.5-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:578e6051f6d5e6200c259b47a103cf06bb875ab5814d17333fc0b5c290b22f4c"}, - {file = "rapidfuzz-3.14.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fbf1b8bb2695415b347f3727da1addca2acb82c9b97ac86bebf8b1bead1eb12d"}, - {file = "rapidfuzz-3.14.5-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f4a8f5cc84c7ad6bffa0e9947b33eb343ad66e6b53e94fe54378a5508c5ed53"}, - {file = "rapidfuzz-3.14.5-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:97c6d85283629646fa87acc22c66b30ea9d4de7f6fdf887daa2e30fa041829b5"}, - {file = "rapidfuzz-3.14.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:dfef96543ced67d9513a422755db422ae1dc34dade0a1485e0b43e7342ed3ebf"}, - {file = "rapidfuzz-3.14.5.tar.gz", hash = "sha256:ba10ac57884ce82112f7ed910b67e7fb6072d8ef2c06e30dc63c0f604a112e0e"}, + {file = "rapidfuzz-3.14.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1c0dd0d765184366b6e213a8af3b0b3bb39dad27943bbfb193515d4ff96ac82a"}, + {file = "rapidfuzz-3.14.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0c61cade182f130c9903231946bd1074539121721693a918e7b70382ae802bd8"}, + {file = "rapidfuzz-3.14.6-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3781cf14f9fc933d7198c2b25a8bbbd1a62b752746d5cd26de14957edc0e802f"}, + {file = "rapidfuzz-3.14.6-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:71a5bbfd00da1963f27dd1432068929694cf0e00007ae2b9c1ad2a187ec29a16"}, + {file = "rapidfuzz-3.14.6-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:eabaf06ca4896c59cfd9162480f0d37a15a2304ce2efe83ae2bbcfa1cf13534e"}, + {file = "rapidfuzz-3.14.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3d5d90bae3c6fb7ea34da968c9f23070e8440edb827a28b242580e0108110b14"}, + {file = "rapidfuzz-3.14.6-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:d6b58daadbe6974884ec39aee30cfb8bd2e126f8d03503f0069f70d5e84656a3"}, + {file = "rapidfuzz-3.14.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ab4386ef7c2cb3e5eb46e815be49715dfcd301bb9f0a431f18da7aa0007de54f"}, + {file = "rapidfuzz-3.14.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:33a2f7faedaa3608c4876c41b448fc786d54e6cd7c6e732f7de466319b5a73c2"}, + {file = "rapidfuzz-3.14.6-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:adb160a100f6122aa45c78d686e198da3f9e815d4182e0c4fe730608479f7f9c"}, + {file = "rapidfuzz-3.14.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ad60297c001d15af24338440bca85dfee8710e9e3222733c906b33e89d986166"}, + {file = "rapidfuzz-3.14.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3d5b1cfa67bbe6239a643bca1d986f8a07e0a045286c674946e1648c132baa46"}, + {file = "rapidfuzz-3.14.6-cp311-cp311-win32.whl", hash = "sha256:46ddb42af4cad3ac9d5e0c97ee1e687500c529a1ad5cbf9c949ce35f6edd4537"}, + {file = "rapidfuzz-3.14.6-cp311-cp311-win_amd64.whl", hash = "sha256:737a57cbca3e5c16decac86e205727bcd4b99c52f77c48bb44123078c5cd9a7a"}, + {file = "rapidfuzz-3.14.6-cp311-cp311-win_arm64.whl", hash = "sha256:19c1cda8198cc57ffd4ff69a1c02cbe4297e9ca7b506bca03ec584da0a9fe1ff"}, + {file = "rapidfuzz-3.14.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b46cecf27025e7a934332ade033e6a394da8a493f19fa1d835e3b2968a4ff7da"}, + {file = "rapidfuzz-3.14.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1901414b135afb1a7f4b1ef940b95523b49cc5642aecf02af740f37567e98137"}, + {file = "rapidfuzz-3.14.6-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96a548979cd939b2c69358a0f5088a408524fbf7454f04bf90939fa971e64310"}, + {file = "rapidfuzz-3.14.6-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b22ef7e5e2341efc6216b666491022027b984e5aef93446064742f43f3c1d926"}, + {file = "rapidfuzz-3.14.6-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f0d2d95c787d812b9106cfbcb94ad37a49f59df9287e00a75eb61afc246e8759"}, + {file = "rapidfuzz-3.14.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0debb5f43662ea84d2f0228a0c7407ff647f9c3d13f3b692efff0cde46eebce0"}, + {file = "rapidfuzz-3.14.6-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:1d253e1fe44648242a0029b42ba23adf238ed2a7eb3d8ed0a03731a23f074ae0"}, + {file = "rapidfuzz-3.14.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e06c6050c9bf6cd72305e3e6a293918b2b92cf2a067007585a53898624902e3c"}, + {file = "rapidfuzz-3.14.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d85a6e9180e53cde95c95dfeb05a2ac94ead4d9d803a8fd186d2719a678b8483"}, + {file = "rapidfuzz-3.14.6-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:35db2670f69fa3a4eb4741055581477ff92f2cf39e7e06f43ebcb97c2192fe7c"}, + {file = "rapidfuzz-3.14.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:f9d93e5424d1e4c103b57906b8beba270e680afda3ffdff7ea3bc6173b37083c"}, + {file = "rapidfuzz-3.14.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9b0a501f37fb852c54469375baa25874246b3bbc8b6e21fb4cd186a32335868"}, + {file = "rapidfuzz-3.14.6-cp312-cp312-win32.whl", hash = "sha256:9e974251a9833791bc557b46f975676a56c2d58946f795cd2964b095496dfdcc"}, + {file = "rapidfuzz-3.14.6-cp312-cp312-win_amd64.whl", hash = "sha256:cfca36e4612208875e08611a779164b6cb8900ab8bbd3d82d4cfdfae9efbfac9"}, + {file = "rapidfuzz-3.14.6-cp312-cp312-win_arm64.whl", hash = "sha256:96bbd5a1c67d135334d02fae74f1d933fdda204ea03d544a59dab6b1cbfbf565"}, + {file = "rapidfuzz-3.14.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:55dc9a55924b4ecfcf4a60a701bcfae7d9daf0129c41dc16139270d75be0996c"}, + {file = "rapidfuzz-3.14.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bba0e9fad4dbea80227cde9cef3aaa984a934a84aec5f7505532e19838b14769"}, + {file = "rapidfuzz-3.14.6-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b34b7ee4f4f760690d6477163aabbec05705b5dd764cb6c3a6ba95aa1fffc42"}, + {file = "rapidfuzz-3.14.6-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:abe92a70134c8b40790bb5c78b2a0a790686c26e83b6e99a456127ca141fe06a"}, + {file = "rapidfuzz-3.14.6-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:659b41570fcc6e02631ac361c47cc8db9ad26d740e4be2177df1b63005a49174"}, + {file = "rapidfuzz-3.14.6-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6bb896f89a387219c671ebc33c4a636b222010cc3c5c83884a7fc8707bf0bbf9"}, + {file = "rapidfuzz-3.14.6-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:11d76bb2b2cd038df708ae18f521fb3a50af477cc5a0dffce812da43a2f1beb3"}, + {file = "rapidfuzz-3.14.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:28e9ce91bd41a8203185887ef9b1541a891aa61c5c1cb2e46f1689cd4288d372"}, + {file = "rapidfuzz-3.14.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:864658e5a10d249a2277374e800f944fe990346d70eea6f3a51b712b6dd01984"}, + {file = "rapidfuzz-3.14.6-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:3c2444f5cd757ded2c3ba8b1734253b801b9b2ba9ecb3ee40cd505cebbfa7341"}, + {file = "rapidfuzz-3.14.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2cc9b5dde0ac89f7856f997ef917cac8e18e9dea473e9b3090a84bd600de6a91"}, + {file = "rapidfuzz-3.14.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:faebff9b9a287fb673f9a66465a7e03043601c9bfe5e71c3f91b3f2e7b8a37f6"}, + {file = "rapidfuzz-3.14.6-cp313-cp313-win32.whl", hash = "sha256:4406b2517b85febcf9419f8fbcdfbd534872ea32608050f9562224933ca49a4c"}, + {file = "rapidfuzz-3.14.6-cp313-cp313-win_amd64.whl", hash = "sha256:c69fb0e064d10c79908dcda76d7ca8ecdf8393a39acbb74dbad3f709f2c60e95"}, + {file = "rapidfuzz-3.14.6-cp313-cp313-win_arm64.whl", hash = "sha256:a0c8bef04f6b1d9fdbb319576350af53151a64692d477db7d4844c220bc8e212"}, + {file = "rapidfuzz-3.14.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0f8d6718e7edacdb16455c0472e7552fd518decb91e91250c58784fd6163f54f"}, + {file = "rapidfuzz-3.14.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8fa7d45388dec34a86038f2a38380f4922b74b5dd8991247f629a531178db10f"}, + {file = "rapidfuzz-3.14.6-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:760ee152af5e8b4d241a469f933ba2d7215248618ae19770fec7d80d9e149db6"}, + {file = "rapidfuzz-3.14.6-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dbe3378db3ae0453accf6196e2ed943f43d416cfacdcb8883db105bc14a0130f"}, + {file = "rapidfuzz-3.14.6-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9ddb0ddf3ee616fdc066add4ef05639c5cf59b58d83779b6023488e5435f6191"}, + {file = "rapidfuzz-3.14.6-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:08bc63b88048376114d1e66cf8fa6926495d03bb873eb87854fa74cf6848a70b"}, + {file = "rapidfuzz-3.14.6-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:50cd6718bcda7ec5293635a9d0b3fb5906251013d3b99ca403ba9dfa8965f661"}, + {file = "rapidfuzz-3.14.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:63b0e84faec3c5706cae8ae51246ff103407d54efa32a615a548b7b67392ebcf"}, + {file = "rapidfuzz-3.14.6-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9080a730fdcf3cb8a07464c90f9cf40c1b4ffc73a8375b56a8898aba619dda30"}, + {file = "rapidfuzz-3.14.6-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:178557c7a50c8c8d65369ede7f3d845bf23590a951c9a368caf166b105d58cf3"}, + {file = "rapidfuzz-3.14.6-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:44f1cddbc2010700e2d88063d0ab64183efe2578d9b52770ce1cd283dda230c5"}, + {file = "rapidfuzz-3.14.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:17081a0e904c12bb4ed49619a2bbb6528f6af00fe850e7ace22487bfd2aea455"}, + {file = "rapidfuzz-3.14.6-cp314-cp314-win32.whl", hash = "sha256:9e00c8c9500aacbc0c52b66369f54533ecbdcb92e5aa87e160fc8e293000a696"}, + {file = "rapidfuzz-3.14.6-cp314-cp314-win_amd64.whl", hash = "sha256:41ee893c4d7d0fb1844f6cad966540a833784b3bad2c239a0d80195d9231cef4"}, + {file = "rapidfuzz-3.14.6-cp314-cp314-win_arm64.whl", hash = "sha256:10576c39fe6a49fad0bf1069371a77300ce166a3f36d2900d2d0bae08f297104"}, + {file = "rapidfuzz-3.14.6-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1b0a9546a7328d3cfc2f1385501db7c4c374fb566dc1a3b22ad56092846c0134"}, + {file = "rapidfuzz-3.14.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9989280902b9c4ecf7de95fbb906e94df0d8c047290ed315c7aa1760cec9b3de"}, + {file = "rapidfuzz-3.14.6-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fc166efa4ca2fc9cc52e43784a54cbea95fc0e03e533f8266ef66b1c04c7cb76"}, + {file = "rapidfuzz-3.14.6-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:32352a3ed1aad9c097d31fd4f2eece3030169e2de3dedde7a2fadc2652b768ad"}, + {file = "rapidfuzz-3.14.6-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ecb45d616002751b58914d5b7c2e66acd39e12242be12717a1258148a1b36526"}, + {file = "rapidfuzz-3.14.6-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f9ad513e3a3e045b60b421d5cd3887ae0a33b38fc6c6db3ea5e27c0a2e0412c"}, + {file = "rapidfuzz-3.14.6-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:f35723caef8cc31b6f34209708fb172fc88bab0077c12e9b36bbb829baaf1b16"}, + {file = "rapidfuzz-3.14.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:408b2e8e8c1ac71b57f0923cf964d6932539725e07b69e70ec66f22c4a403891"}, + {file = "rapidfuzz-3.14.6-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5667c56fdc902fa1e12449b5c042e8b1c7e9b30040db20c396fbdb3d0a750866"}, + {file = "rapidfuzz-3.14.6-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:76a122fc573df603deb5fb827df31bb5efbd0826b50bb7aeca8535a6e8c70cf9"}, + {file = "rapidfuzz-3.14.6-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:e221366e24709b9d41d5f9cc99053b04cfc575d429e956a82cfbc4c4e9e8860a"}, + {file = "rapidfuzz-3.14.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:36710ff214b7a8049d26a9c81d99948026593cacb47663742c4119072b651ecd"}, + {file = "rapidfuzz-3.14.6-cp314-cp314t-win32.whl", hash = "sha256:66ece6f5e2586c742fc3e0b8487e06783d27c6c24adcdcfdd7f306afbd8b5737"}, + {file = "rapidfuzz-3.14.6-cp314-cp314t-win_amd64.whl", hash = "sha256:cab4a932cec02d09471e2c9f1434049ef5bfe1f6e646ff10939c222dc610ad60"}, + {file = "rapidfuzz-3.14.6-cp314-cp314t-win_arm64.whl", hash = "sha256:b056ce19eaea2ea70c6a6fb387a605ca2af8979de5b9d507597e8012820ddb14"}, + {file = "rapidfuzz-3.14.6-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:bc3d74d18543ddfbc8babe1faadb19927a7999fd0d01181cce9e721c14c36ab6"}, + {file = "rapidfuzz-3.14.6-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:aaa83b633d877a05d549d2073629134998d1b3b9dbc114873d3ff4277984979f"}, + {file = "rapidfuzz-3.14.6-cp315-cp315-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cbe6a62f71fcbca72acbf5a30e53380600369f257f951d664d81d30c0c598595"}, + {file = "rapidfuzz-3.14.6-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b82c21c30568e096ef2a9dda7d45c379e6141694e0472dac73bc4372ce13ccee"}, + {file = "rapidfuzz-3.14.6-cp315-cp315-manylinux_2_39_riscv64.whl", hash = "sha256:fc950bb77105a2717d03d9f9c9e21e9ace7df2b8e864dd91edef7e32fa143be2"}, + {file = "rapidfuzz-3.14.6-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:c53a269bdbd71ffbc856d3db9e609478251001ee272507578fa838bc2bd421fe"}, + {file = "rapidfuzz-3.14.6-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:bf4fb0f19c9dfce7a908c3e309753602ce3edb83bb74e9ff997e278765bf89df"}, + {file = "rapidfuzz-3.14.6-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:189ce2bf14938bfa003fbbe7e6da7584ed6ebbc4c560686255dbc20e2829f470"}, + {file = "rapidfuzz-3.14.6-cp315-cp315-win32.whl", hash = "sha256:7ca0f498bf771a87557e6d8b573aa6cf3daded58ae2eaeb6973618ce3e1615ad"}, + {file = "rapidfuzz-3.14.6-cp315-cp315-win_amd64.whl", hash = "sha256:d4c5adb921b67dd79ffc0a14f92b9f8df3d012e66aab340b154ed87014229d93"}, + {file = "rapidfuzz-3.14.6-cp315-cp315-win_arm64.whl", hash = "sha256:c9d135fb93709d707577da8a7a8ffc7283525a5b6d0ce55aa3724be5639ed65b"}, + {file = "rapidfuzz-3.14.6-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:dd89abd1c4b3776c3471a817216830bd275441c8344bbda5d51a3bffe1e0fbdf"}, + {file = "rapidfuzz-3.14.6-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:eab2d4680d7f438dbb1d484b187d59a943edea9c83f792c764a0c148a417a60a"}, + {file = "rapidfuzz-3.14.6-cp315-cp315t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8683fefdd3484d64a191b3efbc8cbe9162c3eac891fd62d0a1b70e117ffcd434"}, + {file = "rapidfuzz-3.14.6-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2bc7af3a699371a941aac86dc8a79ac92adeb3c2add2aab02230e76068a0029e"}, + {file = "rapidfuzz-3.14.6-cp315-cp315t-manylinux_2_39_riscv64.whl", hash = "sha256:40c2753e2d4dc96b25f8a25adc23ab0bb6cfd8bc8125a1753ac4b037d6ff6511"}, + {file = "rapidfuzz-3.14.6-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:36a37ddc729c33618d89fa221d3333b9b956dc38cf15d31301e6169d962399a3"}, + {file = "rapidfuzz-3.14.6-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:635f242f4bdf05d1477fa409815bd73e5f78896773ace84997bc472ffeef685f"}, + {file = "rapidfuzz-3.14.6-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:40d0cd9c82083aeb30bae8dee265ae571e6748d0d7b222ddd777f33d95a3b712"}, + {file = "rapidfuzz-3.14.6-cp315-cp315t-win32.whl", hash = "sha256:15da2b258908eb38853c1a6a58a1d09d9aad9c721e03a68c8ba691cd31dff739"}, + {file = "rapidfuzz-3.14.6-cp315-cp315t-win_amd64.whl", hash = "sha256:3d502769263318690d4f6638b08483979d1b88cdc7c6f087482eea935fde4031"}, + {file = "rapidfuzz-3.14.6-cp315-cp315t-win_arm64.whl", hash = "sha256:07c7aa0b1e4b9999a54f9e73317d6743ff85442c8ef7b7fbbe6b190fd37d9e75"}, + {file = "rapidfuzz-3.14.6-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0844066900cdc9909ce4ab4fb5ba1d8e0c021252d770f2ea476f3443df1d22ef"}, + {file = "rapidfuzz-3.14.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:1398bd2c197b79bfc40b615999fd3599dc60265fdd5b59edc18156ae048c4cde"}, + {file = "rapidfuzz-3.14.6-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e2fc748d1fde4109e5d0dab27f1e61f53b3136a235dfee5a4fb579da44808b6a"}, + {file = "rapidfuzz-3.14.6-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b42536675c930cb76b7998bfc4d8e59cb35d8df47f2103020265743b6b2ccd2a"}, + {file = "rapidfuzz-3.14.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:1e6911e3a14971719ddc35af98f181d2e5369ab273a5a3488ab7685d23c31ad5"}, + {file = "rapidfuzz-3.14.6.tar.gz", hash = "sha256:e13a8160d017b499ec7a2fa9d0ce1ae2e7377080815785819f966fb235d4eb60"}, ] [package.extras] @@ -3352,14 +3536,14 @@ packaging = ">=23.2" [[package]] name = "responses" -version = "0.26.2" +version = "0.26.3" description = "A utility library for mocking out the `requests` Python library." optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "responses-0.26.2-py3-none-any.whl", hash = "sha256:6fdfeabd58e5ec473b98dfe02e6d46d3173bd8dd573eff2ccccf1a05a5135364"}, - {file = "responses-0.26.2.tar.gz", hash = "sha256:9c9259b46a8349197edebf43cfa68a87e1a2802ef503ff8b2fecbabc0b45afd8"}, + {file = "responses-0.26.3-py3-none-any.whl", hash = "sha256:74474f799334ac4f37d93b6437ecc3bb1bb5c77a8d31780a338643be2dce0af8"}, + {file = "responses-0.26.3.tar.gz", hash = "sha256:b0c11ca8131b8b227b8d5108e6ed39772222bd5aab030ed430e8f99057c4c409"}, ] [package.dependencies] @@ -3422,21 +3606,21 @@ pyasn1 = ">=0.1.3" [[package]] name = "s3transfer" -version = "0.19.0" +version = "0.19.2" description = "An Amazon S3 Transfer Manager" optional = false python-versions = ">=3.10" groups = ["main", "dev"] files = [ - {file = "s3transfer-0.19.0-py3-none-any.whl", hash = "sha256:777cc2415536f1debadb5c2ef7779275d0fc0fe0e042411cdd6caebeb2685262"}, - {file = "s3transfer-0.19.0.tar.gz", hash = "sha256:ce436931687addc4c1712d52d40b32f53e88315723f107ffa20ba82b05a0f685"}, + {file = "s3transfer-0.19.2-py3-none-any.whl", hash = "sha256:d8168eccca828cbb2cd573675333f3bddd254313a9c42494b84c76b539e8ba25"}, + {file = "s3transfer-0.19.2.tar.gz", hash = "sha256:ba0309fd86be3c27dbf78cdd813c13c5e1df16e5874b99d2535ebbdfb9892993"}, ] [package.dependencies] -botocore = ">=1.37.4,<2.0a.0" +botocore = ">=1.37.4,<2.0a0" [package.extras] -crt = ["botocore[crt] (>=1.37.4,<2.0a.0)"] +crt = ["botocore[crt] (>=1.37.4,<2.0a0)"] [[package]] name = "secretstorage" @@ -3493,70 +3677,70 @@ files = [ [[package]] name = "sqlalchemy" -version = "2.0.51" +version = "2.0.54" description = "Database Abstraction Library" optional = false python-versions = ">=3.7" groups = ["main"] files = [ - {file = "sqlalchemy-2.0.51-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e8203d2fbd5c6254692ef0a72c740d75b2f3c7ca345404f4c1a4604813c77c0"}, - {file = "sqlalchemy-2.0.51-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1af05726b3d0cdba1c55284bf408fd3b792e690fe2399bfb8304565551cda652"}, - {file = "sqlalchemy-2.0.51-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e54ff2dd657f2e3e0fbf2b097db1182f7bfea263eca4353f00065bae2a67c3d"}, - {file = "sqlalchemy-2.0.51-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1e47b1199c2e832e325eacabc8d32d2487f58c9358f97e9a00f5eb93c5680d84"}, - {file = "sqlalchemy-2.0.51-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c68568f3facf8f66fa76c60e0ced69b67666ffa9941d1d0a3756fda196049080"}, - {file = "sqlalchemy-2.0.51-cp310-cp310-win32.whl", hash = "sha256:0592bdadf86ddcabfd72d9ab66ea8a5d8d2cc6be1cc51fa7e66c03868ac5eac1"}, - {file = "sqlalchemy-2.0.51-cp310-cp310-win_amd64.whl", hash = "sha256:740cf6f35351b1ac3d82369152acf1d51d37e3dcf85d4dc0a22ca01410eabe2a"}, - {file = "sqlalchemy-2.0.51-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1aa10c0daee6705294d181daadaa793221e1a59ed55000a3fab1d42b088ce4ba"}, - {file = "sqlalchemy-2.0.51-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5b2ed6d828f1f09bd812861f4f59ca3bc3803f9df871f4555187f0faf018604"}, - {file = "sqlalchemy-2.0.51-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:436728ce18a80f6951a1e11cc6112c2ede9faf20766f1a26195a7c441ca12dbd"}, - {file = "sqlalchemy-2.0.51-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dc261707bf5739aea8a541593f3cc1d463c2701fb05fbcbba0ce031b69a21260"}, - {file = "sqlalchemy-2.0.51-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a6d26094615306d116dd5e4a51b0304c99dd2356fc569eed6922a80a6bd3b265"}, - {file = "sqlalchemy-2.0.51-cp311-cp311-win32.whl", hash = "sha256:ca8435d13829b92f4a97362d91975154a4015db3a2634154e1754e9a915e6b86"}, - {file = "sqlalchemy-2.0.51-cp311-cp311-win_amd64.whl", hash = "sha256:4a011ea4510683319ce4ed274b56ee05194b39b6da9d09ca7a39388f0fa84dcc"}, - {file = "sqlalchemy-2.0.51-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d78702b26ba1c18b2d0fb2ea940ba7f17a9581b42e8361ff93920ebbee1235a"}, - {file = "sqlalchemy-2.0.51-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581921d849d6e6f994d560389192955e80e2950e18fcdfe2ccea863e01158e6e"}, - {file = "sqlalchemy-2.0.51-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d21ce524ab86c23046e992a5b81cb54c21079c6df6e78b8fc77d77cac70a6b9"}, - {file = "sqlalchemy-2.0.51-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c5d98a2709840027f5a347c3af0a7c3d5f6c1ff93af2ca1c54494e23cba8f389"}, - {file = "sqlalchemy-2.0.51-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1181256e0f16479691b5616d36375dc2620ad8332b25978763c3d206ad3f3f1d"}, - {file = "sqlalchemy-2.0.51-cp312-cp312-win32.whl", hash = "sha256:9f380393be5abeb6815f68fd39271b95127173511b6706b0a630a9995d53f8f5"}, - {file = "sqlalchemy-2.0.51-cp312-cp312-win_amd64.whl", hash = "sha256:2cf39aabdf48e87c1c2c2ed6d20d33ffa0733b3071ce9c5f66357947dd009080"}, - {file = "sqlalchemy-2.0.51-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c2056838b6685b72fdb36c99996cf862753461a62f2e84f4196371d3b2d6a07"}, - {file = "sqlalchemy-2.0.51-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:483b11bd46bf35fc14c52faf338b04300c9e6ce554bce9b11be85bfec3bc3195"}, - {file = "sqlalchemy-2.0.51-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1bed1ee8b01da6088210aa9412023326fb98a599ba502e6118308601dcbef77f"}, - {file = "sqlalchemy-2.0.51-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:72ca54c952107ba5cd58854b67a5a6268631289d21651a1235396f3b98b47400"}, - {file = "sqlalchemy-2.0.51-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b3e693d15533a45cd5906f0589f9c35090bef6ef45bf1e8195c424aa0ae06a8d"}, - {file = "sqlalchemy-2.0.51-cp313-cp313-win32.whl", hash = "sha256:b93ab07b5292dbe7e6b8da89475275e7042744283921344b56105f3eeb0f828b"}, - {file = "sqlalchemy-2.0.51-cp313-cp313-win_amd64.whl", hash = "sha256:0f053118c30e53161857a953e4de667d90e274980dccbe5dd3829bbbeece72a5"}, - {file = "sqlalchemy-2.0.51-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6ea306caaae6bd5afd0a46050003c88f6bf33227377a49298c498c3cb88ff491"}, - {file = "sqlalchemy-2.0.51-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c45a496d6bc05dec41dcd4c3a2b183723f47473255c159cd80b503c8f246424d"}, - {file = "sqlalchemy-2.0.51-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4004ada0aafe8ae1991b2cd1d99c6d9146126e123bd6f883c260d974aa012e54"}, - {file = "sqlalchemy-2.0.51-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0f6bcad487aee1c638d707235682fc96f741de00663619881ab235400d03289e"}, - {file = "sqlalchemy-2.0.51-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:39a76529db6305693d8d4affa58ad5b5e2e18edd62daea628b29b97930b3513d"}, - {file = "sqlalchemy-2.0.51-cp314-cp314-win32.whl", hash = "sha256:08a204d8b5638717c26a24df18fcf40af45a6b22e35b70b1d62f0113c2e278e8"}, - {file = "sqlalchemy-2.0.51-cp314-cp314-win_amd64.whl", hash = "sha256:96747bfbadb055466e5b46d572618170046b45ce5a4879167f50d70a5319a499"}, - {file = "sqlalchemy-2.0.51-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5ea1a213be1fcd5e49d9904c3b9939211ded90bc2a64e93f4c01963474285de"}, - {file = "sqlalchemy-2.0.51-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c6b36ed71f41942bdcd2ad2522be46bfce09d5705be5640ecf19bbc7660e4b7"}, - {file = "sqlalchemy-2.0.51-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c2c62877097e1a0db401fba5cb4debee33265e5b2a55c4ccb489c02c53b4f72"}, - {file = "sqlalchemy-2.0.51-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0378d055e9e8cd6ce4d8dff683bdd3d7d413533c4ee51d67a2b1e0f9eacc0f23"}, - {file = "sqlalchemy-2.0.51-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6e46fc36029eff666391e0531e5387b62ce6c4f1d8e50b3fb3099eaca1b42522"}, - {file = "sqlalchemy-2.0.51-cp314-cp314t-win32.whl", hash = "sha256:9161cfc9efce70d1715f47d6ff40f79c6778c00d53be4fbc09d70301e4b83ba7"}, - {file = "sqlalchemy-2.0.51-cp314-cp314t-win_amd64.whl", hash = "sha256:159bb6ba32059f57ad7375a8f50d844dd2f19d14954ecf820cd33e20debd46b2"}, - {file = "sqlalchemy-2.0.51-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bb1f5062f98b0b3290e72b707747fdd7e0f22d6956b236ba7ca7f5c9971d2da2"}, - {file = "sqlalchemy-2.0.51-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:247acaa29ccef6250dfd6a3eedf8f94ddf23564180a39fe362e32ae9dbdbde46"}, - {file = "sqlalchemy-2.0.51-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c95ef01f53233a305a874a44a63fbfb1d81cd79b49de0f8529b3548cde437e37"}, - {file = "sqlalchemy-2.0.51-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:fa268106c8987639a17a18514cfe0cd9bf17420ab887e1e1bf486da8836135b1"}, - {file = "sqlalchemy-2.0.51-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:b7f08588854bbb724041d9ae9d980d40040c922382e1d9a2ecb390edc4fd5032"}, - {file = "sqlalchemy-2.0.51-cp38-cp38-win32.whl", hash = "sha256:6b588fd681ddf0c196b8df1ea49a8913514894b2b8f945a9511b4b48871f99c8"}, - {file = "sqlalchemy-2.0.51-cp38-cp38-win_amd64.whl", hash = "sha256:ca216e8af5c05e326efc7e28716ac2381a7cf9791749f5ee1849dccdc99c9b00"}, - {file = "sqlalchemy-2.0.51-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:aa18ae738b5170e253ad0bb6c4b0f07585081e8a6e50893e4d911d47b39a0904"}, - {file = "sqlalchemy-2.0.51-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:59cab3686b1bc039dd9cded2f8d0c08a246e84e76bd4ab5b4f18c7cdae293825"}, - {file = "sqlalchemy-2.0.51-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:111604e637da87031255ddc26c7d7bc22bc6af6f5d459ccff3af1b4660233a85"}, - {file = "sqlalchemy-2.0.51-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ad30ae663711786303fbcd46a47516302d201ee49a877cb3fac61f672895110a"}, - {file = "sqlalchemy-2.0.51-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b21f0e7efc7a5c509e953784e9d1575ebb8b4318960e7e7d7a93bb803626cf64"}, - {file = "sqlalchemy-2.0.51-cp39-cp39-win32.whl", hash = "sha256:a42ad6afcbaaa777241e347aa2e29155993045a0d6b7db74da61053ffe875fe0"}, - {file = "sqlalchemy-2.0.51-cp39-cp39-win_amd64.whl", hash = "sha256:2a97eaad21c84b4ef8010b11eeba9fe6153eb0b3df3ff8b6abc309df1b978ef7"}, - {file = "sqlalchemy-2.0.51-py3-none-any.whl", hash = "sha256:bb024d8b621d0be75f4f44ecc7c950450026e76d66dc8f791bb5331d7fed59d5"}, - {file = "sqlalchemy-2.0.51.tar.gz", hash = "sha256:804dccd8a4a6242c4e30ad961e540e18a588f6527202f2d6791b01845d59fdc9"}, + {file = "sqlalchemy-2.0.54-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:24ae093dec196ba37fc2beb0316de53e7871d3d246a50faecbbb53034e41ded2"}, + {file = "sqlalchemy-2.0.54-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f8cc6532f930c27974e9239e5ce5abebe7600ba9807cea4fcf42f1b6cab18fe7"}, + {file = "sqlalchemy-2.0.54-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e7a76d5dce712ce50435d0f97181eb955ec27d138c004176f01282e063bac52"}, + {file = "sqlalchemy-2.0.54-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f5c09090b1a7c4d389d1431f820931e8df318f82caafc53f9a72c872fef467c5"}, + {file = "sqlalchemy-2.0.54-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:762cfe4d340c56368256d936a98b620a9a5650e49c1c84eba51d6edd17ffefb2"}, + {file = "sqlalchemy-2.0.54-cp310-cp310-win32.whl", hash = "sha256:6b6d4e601c4f6d85e99bb3416107cc9418c5603ca73d4ee0f5f8d79c2a1ed9e8"}, + {file = "sqlalchemy-2.0.54-cp310-cp310-win_amd64.whl", hash = "sha256:03cbf8d9a67da618bd65500a5eb3ddac89caf4c61e99b2f03fa4a1952a0725a9"}, + {file = "sqlalchemy-2.0.54-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7d03084f3352dd92048cb19c71d90f116d076c9c7937e0ebc7752c4685de6d38"}, + {file = "sqlalchemy-2.0.54-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92622fbbda1b1fe1632f3402a6e516a93c0e41d9158839c6b3dfb12117f26b72"}, + {file = "sqlalchemy-2.0.54-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5800ddea045c2c860ef1d359a07a3066c7c0c426f45e3abc3874e116cb3c6937"}, + {file = "sqlalchemy-2.0.54-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1019abef05a4b5eafc8eae6fb483167fa28a4dbe5f518d577b744f31a5276a37"}, + {file = "sqlalchemy-2.0.54-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b67749f7da3985a529cefbb1474783cb91ef44371cb9713630bade3de908760d"}, + {file = "sqlalchemy-2.0.54-cp311-cp311-win32.whl", hash = "sha256:2f61a70b3b82e2ec7ad6a4f2301422b9ca93ff06917983e41317bcae878bddf6"}, + {file = "sqlalchemy-2.0.54-cp311-cp311-win_amd64.whl", hash = "sha256:1d887fbd5d248e250807bd801e697fc73e3b44866ce5f093dbc90512e75bde25"}, + {file = "sqlalchemy-2.0.54-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffba7eb2d67c7505e82a0902aa854d8824b74c28a183820d6a8bd3cfd0f812c2"}, + {file = "sqlalchemy-2.0.54-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:63cae7210fea9899e0bf35c1f1ae55d3ddd9c6d47cae8b6b43d945afa79dd65b"}, + {file = "sqlalchemy-2.0.54-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:68d994e9b0d0423a02a20039631fa6fcbb7fa829a992f7605025774940305d19"}, + {file = "sqlalchemy-2.0.54-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3de32cc6721eb42c3aad35bcfb244bb7a18f66c00f3582aae6281d6287a339b5"}, + {file = "sqlalchemy-2.0.54-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d31a2bc06a854ee52dd86b455be4df7c750b28817e2d1b884e31fff126c4fd7b"}, + {file = "sqlalchemy-2.0.54-cp312-cp312-win32.whl", hash = "sha256:32de6deded25e8b9b11d07428d496ff24dfbc882b8e990c177266948cb5f3d9e"}, + {file = "sqlalchemy-2.0.54-cp312-cp312-win_amd64.whl", hash = "sha256:d65f8ca742ef1e1e14bc417ef59dc2ddf207a7b66b30cfdc6152447314e030cf"}, + {file = "sqlalchemy-2.0.54-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b374e3bc91e246a942592a98ba6a23be76fff21358b00546ac8c0ebc0fd0e00b"}, + {file = "sqlalchemy-2.0.54-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:31d5458672a6f72db2c087f4a5098b3c8503ea0254186ff29205d63afa9401a4"}, + {file = "sqlalchemy-2.0.54-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cad78d04254967bdbcccbed5e631d88fe4868530946ab0929aa45e9032849518"}, + {file = "sqlalchemy-2.0.54-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:48611087a75d26d798003645c688c7d3cfc26b89dbe4a2c568d6b378d330deae"}, + {file = "sqlalchemy-2.0.54-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d6adf80277372a89910a0f3ccfe960b846d279dc55b366dd5c5ec07f41c84758"}, + {file = "sqlalchemy-2.0.54-cp313-cp313-win32.whl", hash = "sha256:264460333ed0b177cbb1956355d0ee4e0cab83fb415c934ce12a25db2e7be39c"}, + {file = "sqlalchemy-2.0.54-cp313-cp313-win_amd64.whl", hash = "sha256:cf89e92bf0d4204a6afcc17af27b9271ed9c7e34e17d6f80c085d431ea4a1747"}, + {file = "sqlalchemy-2.0.54-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:abd6b21bc58e91c1932eb5d6d7f1bd44a551dfec7b6a7f517c3638ccd67233a0"}, + {file = "sqlalchemy-2.0.54-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5417322b3c025dd82918725d3bf09ec105fac95efc195722b8b06e1d9c381139"}, + {file = "sqlalchemy-2.0.54-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f84099e4b04a5c2d44500a2a8302eee5af4bc6fee63e8c6e9cf6786e747280e"}, + {file = "sqlalchemy-2.0.54-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a0956dc754d3884da7fe60097110ec7a8a105d26afa2f0844468f4b1598c6912"}, + {file = "sqlalchemy-2.0.54-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:87ba8834318b0d8dc94fc6f405d071b5c08be32a6c3fd68107fd6952ee949615"}, + {file = "sqlalchemy-2.0.54-cp314-cp314-win32.whl", hash = "sha256:842540e4382472f23c79589995752648d14696a8200d0807ed8c5c59c92ade44"}, + {file = "sqlalchemy-2.0.54-cp314-cp314-win_amd64.whl", hash = "sha256:f4e8f955d13af83fb4e35c3472e5377ee22d3445eada1e5e48199588edb69835"}, + {file = "sqlalchemy-2.0.54-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ca05f4e7852cf48083b0cf157e4f9504b7068780422a50fa82f45353b8c5e14a"}, + {file = "sqlalchemy-2.0.54-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18a8b6417cbb7b735cf91c2b59453c2a554cefa0a8d7bd15aa35740739410d77"}, + {file = "sqlalchemy-2.0.54-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4e55a0b96a1577a1e108c91ccdeeb9cd92768f28ce206597311c3bf6d6423abd"}, + {file = "sqlalchemy-2.0.54-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:69cab115c40fd02c5a22c68e4ee630fa6ef9a1650f1de944419aab1f7096fc4f"}, + {file = "sqlalchemy-2.0.54-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e08397c6c42f53b2488acde9108b8bfefd52d7afd1bf2f03d2ffcab7a204aceb"}, + {file = "sqlalchemy-2.0.54-cp314-cp314t-win32.whl", hash = "sha256:b9086b8ad48280ef6a7ba68262d5e44f7db1c4cb1973e8cdae8a9f467ae66f51"}, + {file = "sqlalchemy-2.0.54-cp314-cp314t-win_amd64.whl", hash = "sha256:b67c1744e453af833667fc1b84de07adb4a64f3536ef52a8ec5ac2b941d43970"}, + {file = "sqlalchemy-2.0.54-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:330d35f9ce815d35cb1daab038d4d7ec0e907f4d7ed0fc8bcb2411d1f23d0b50"}, + {file = "sqlalchemy-2.0.54-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1f455db400289f77ba2f7b62fffafe8875153812d0e3777aa4ff2b34a0fc1f7"}, + {file = "sqlalchemy-2.0.54-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4e8a4afcc7d714cc3c8a57facdff4c3529f5f93d71e54b7da1e03e022c9089c9"}, + {file = "sqlalchemy-2.0.54-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:34e10af7d274a5c4b7cd0fced5e7361008c5e07d97dd48a93852d5b2f1142a1c"}, + {file = "sqlalchemy-2.0.54-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:7108f410f596c5ac22fe43ba467e864d27c4e1477ae89e90c6c87120b2c1be23"}, + {file = "sqlalchemy-2.0.54-cp38-cp38-win32.whl", hash = "sha256:c1a3455a88f66e4851792bedb098ed942912253d31caed1dbc58afbfa9e875cd"}, + {file = "sqlalchemy-2.0.54-cp38-cp38-win_amd64.whl", hash = "sha256:f3ea33bcf0aa599c1511fe5c9fb126f45aa450419084c4823f786155fe4c79f1"}, + {file = "sqlalchemy-2.0.54-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b6c419c83a87fd901f0b1b5338ffcb82471c3ac32a86bb8883688c18f8eb85d3"}, + {file = "sqlalchemy-2.0.54-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:415239eb2ddbbc508ba4cac97affb91c0f210548fd1731edda6e529b0bb93015"}, + {file = "sqlalchemy-2.0.54-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:279bde5bfedb0f3e0f1bdbcffa2daa39c6c54d90f9408ef3b1802001597199f0"}, + {file = "sqlalchemy-2.0.54-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7b973e4facc2f80e42f5a27b841feb7e202661881a6320580abbe597a28a007f"}, + {file = "sqlalchemy-2.0.54-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:deeab253fe01a770f634c7007c73702df2324c868a79ae756507a9a1a76294fe"}, + {file = "sqlalchemy-2.0.54-cp39-cp39-win32.whl", hash = "sha256:d566099d60cded87d175d4171dc899b9613d2e3b663573364565ca1b27ccd241"}, + {file = "sqlalchemy-2.0.54-cp39-cp39-win_amd64.whl", hash = "sha256:744fb219a390561a57dbbd59cd69a22b5b5b2facfde794c1f79236dd847fa67a"}, + {file = "sqlalchemy-2.0.54-py3-none-any.whl", hash = "sha256:7e33a631ab1474f8fe6b910bd1a07b7b8009c4c78cdd3fb18001b03e3bc2e1d2"}, + {file = "sqlalchemy-2.0.54.tar.gz", hash = "sha256:baa8521e8ee9f24e75dfc7aaabc08020e551ef0d48d7c3e3536f5cddf277586b"}, ] [package.dependencies] @@ -3568,7 +3752,7 @@ aiomysql = ["aiomysql (>=0.2.0)", "greenlet (>=1)"] aioodbc = ["aioodbc", "greenlet (>=1)"] aiosqlite = ["aiosqlite", "greenlet (>=1)", "typing_extensions (!=3.10.0.1)"] asyncio = ["greenlet (>=1)"] -asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (>=1)"] +asyncmy = ["asyncmy (>=0.2.12)", "greenlet (>=1)"] mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5,!=1.1.10)"] mssql = ["pyodbc"] mssql-pymssql = ["pymssql"] @@ -3590,14 +3774,14 @@ sqlcipher = ["sqlcipher3_binary"] [[package]] name = "starlette" -version = "1.3.1" +version = "1.6.0" description = "The little ASGI library that shines." optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6"}, - {file = "starlette-1.3.1.tar.gz", hash = "sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0"}, + {file = "starlette-1.6.0-py3-none-any.whl", hash = "sha256:a86dd39d14bb45f85a3d18525215a9ef0cfd1f192ac793220e72598c90335f0c"}, + {file = "starlette-1.6.0.tar.gz", hash = "sha256:d4e3ac5e546444960c710297a3c9fc3f7ebae1b7e963f3d36173b49da535be9b"}, ] [package.dependencies] @@ -3666,38 +3850,38 @@ files = [ [[package]] name = "tomlkit" -version = "0.15.0" +version = "0.15.1" description = "Style preserving TOML library" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "tomlkit-0.15.0-py3-none-any.whl", hash = "sha256:4dbc8f0fc024412b57ced8757ac7461305126a648ff8c2c807fcb8e133a78738"}, - {file = "tomlkit-0.15.0.tar.gz", hash = "sha256:7d1a9ecba3086638211b13814ea79c90dd54dd11993564376f3aa92271f5c7a3"}, + {file = "tomlkit-0.15.1-py3-none-any.whl", hash = "sha256:177a05aece5a8ca5266fd3c448abb47b8d352f09d477d3ca8332db4d89b24304"}, + {file = "tomlkit-0.15.1.tar.gz", hash = "sha256:e25bbf38843005246210a12982776f27f99cb9be67160e14434d0c0d21ee1e97"}, ] [[package]] name = "trove-classifiers" -version = "2026.6.1.19" +version = "2026.9.21.13" description = "Canonical source for classifiers on PyPI (pypi.org)." optional = false python-versions = "*" groups = ["dev"] files = [ - {file = "trove_classifiers-2026.6.1.19-py3-none-any.whl", hash = "sha256:ab4c4ec93cc4a4e7815fa759906e05e6bb3f2fbd92ea0f897288c6a43efd15b3"}, - {file = "trove_classifiers-2026.6.1.19.tar.gz", hash = "sha256:c5132b4b61a829d11cfbd2d72e97f20a45ed6edb95e45c5efdeb5e00836b2745"}, + {file = "trove_classifiers-2026.9.21.13-py3-none-any.whl", hash = "sha256:8b1ff4f9c191b1040b71c37f1e445ab99732911e3cd91de52838453a854d7a17"}, + {file = "trove_classifiers-2026.9.21.13.tar.gz", hash = "sha256:0a9ebc8d4e2f3e8a22848c5258033035bec17a3012ac3fea16dbaa764489eb71"}, ] [[package]] name = "typer" -version = "0.26.8" +version = "0.27.2" description = "Typer, build great CLIs. Easy to code. Based on Python type hints." optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "typer-0.26.8-py3-none-any.whl", hash = "sha256:3512ca79ac5c11113414b36e80281b872884477722440691c89d1112e321a49c"}, - {file = "typer-0.26.8.tar.gz", hash = "sha256:c244a6bd558886fe3f8780efb6bdd28bb9aff005a94eedebaa5cb32926fe2f7e"}, + {file = "typer-0.27.2-py3-none-any.whl", hash = "sha256:b3a5fc4342d5fc8fda8fc3010b1cf117e9249aab7fae800c2eff62fd3842d97d"}, + {file = "typer-0.27.2.tar.gz", hash = "sha256:269b7eb9d3c202ca84b4bc9618cb04ebb43d3d4d1e567e4c768607232c05f945"}, ] [package.dependencies] @@ -3712,26 +3896,27 @@ version = "4.16.0" description = "Backported and Experimental Type Hints for Python 3.9+" optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8"}, {file = "typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5"}, ] +markers = {dev = "python_version < \"3.15\""} [[package]] name = "typing-inspection" -version = "0.4.2" +version = "0.4.4" description = "Runtime typing introspection tools" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7"}, - {file = "typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464"}, + {file = "typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147"}, + {file = "typing_inspection-0.4.4.tar.gz", hash = "sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47"}, ] [package.dependencies] -typing-extensions = ">=4.12.0" +typing-extensions = ">=4.15.0" [[package]] name = "uritemplate" @@ -3747,14 +3932,14 @@ files = [ [[package]] name = "urllib3" -version = "2.7.0" +version = "2.8.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.10" groups = ["main", "dev"] files = [ - {file = "urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897"}, - {file = "urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c"}, + {file = "urllib3-2.8.0-py3-none-any.whl", hash = "sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3"}, + {file = "urllib3-2.8.0.tar.gz", hash = "sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63"}, ] [package.extras] @@ -3781,21 +3966,21 @@ sqlalchemy = ">=1.3.0" [[package]] name = "virtualenv" -version = "21.6.0" +version = "21.9.1" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "virtualenv-21.6.0-py3-none-any.whl", hash = "sha256:bce9d097950fef9d81129b333babfb7767072850c2f1acce0ec536708401bfd1"}, - {file = "virtualenv-21.6.0.tar.gz", hash = "sha256:e18a4d750f2b64dea73e72ffde3922f3c52365fabdc8157ebd3da20d031c4734"}, + {file = "virtualenv-21.9.1-py3-none-any.whl", hash = "sha256:7dab4bee7733e85ae3742d349abe119050b00a31718105486ce11ff757156c35"}, + {file = "virtualenv-21.9.1.tar.gz", hash = "sha256:4d2256a16b2c99cb30e58abe96d009a4328e215d64f229c7738fc5adad83cd1a"}, ] [package.dependencies] distlib = ">=0.3.7,<1" filelock = {version = ">=3.24.2,<4", markers = "python_version >= \"3.10\""} platformdirs = ">=3.9.1,<5" -python-discovery = ">=1.4.2" +python-discovery = ">=1.6" [[package]] name = "werkzeug" diff --git a/pyproject.toml b/pyproject.toml index 7aaab7e9a..633a74d5e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "fence" -version = "13.3.0" +version = "14.0.0" description = "Gen3 AuthN/AuthZ OIDC Service" authors = ["CTDS UChicago "] license = "Apache-2.0" diff --git a/tests/conftest.py b/tests/conftest.py index 55f109c6f..f2a7e5608 100755 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -605,6 +605,32 @@ def drop_all(): return app.db +LOGIN_STATE_FOR_TESTS = "test-issued-login-state" + + +def seed_login_state(client, idp_name, state=LOGIN_STATE_FOR_TESTS): + """ + Give the test client a session holding an issued login state for an IdP. + + Call this immediately before the callback request. Fence rewrites the session + cookie on every response, so any intervening request through the same client + discards a state seeded earlier. + + Args: + client: the Flask test client to seed + idp_name (str): name of the identity provider, as the callback knows it + state (str): the state value to issue + + Returns: + str: the state that was issued + """ + from fence.blueprints.login.base import state_session_key + + with client.session_transaction() as test_session: + test_session[state_session_key(idp_name)] = state + return state + + @pytest.fixture def prometheus_metrics_before(client): resp = client.get("/metrics") @@ -1881,7 +1907,9 @@ def mock_authn_user_flask_context(app): @pytest.fixture -def mocks_for_idp_oauth2_callbacks(idp, rsa_private_key, mock_arborist_requests): +def mocks_for_idp_oauth2_callbacks( + idp, rsa_private_key, mock_arborist_requests, client +): """ Setup mocks necessary to make calls to OAuth2 callback endpoints, depending on which IdP is being used. Return values needed by the calling test. @@ -1964,6 +1992,14 @@ def mocks_for_idp_oauth2_callbacks(idp, rsa_private_key, mock_arborist_requests) ) get_auth_info_patch.start() + # `DefaultOAuth2Callback` only accepts a callback carrying the login state it + # issued. The fence-as-IdP and Shibboleth callbacks override `get` and manage + # their own session state, so they are left alone here. + if idp not in ("fence", "shibboleth"): + seed_login_state(client, idp_name) + separator = "&" if "?" in callback_endpoint else "?" + callback_endpoint += f"{separator}state={LOGIN_STATE_FOR_TESTS}" + yield idp_name, username, callback_endpoint, headers if get_auth_info_patch: diff --git a/tests/data/test_bulk_authz.py b/tests/data/test_bulk_authz.py new file mode 100644 index 000000000..f5239f232 --- /dev/null +++ b/tests/data/test_bulk_authz.py @@ -0,0 +1,95 @@ +""" +Bulk DRS signed URL requests must authorize each record the same way the +single-object path does: a record whose `authz` lists several resources requires +read-storage on all of them, not just one. +""" + +import mock +import pytest + +from fence.blueprints.data.bulk_helpers import process_bulk_signed_urls + + +OPEN = "/programs/open" +CONTROLLED = "/programs/controlled" +PASSPORT_USER = "passport_user" + + +class FakeArborist(object): + """Arborist stand-in whose auth_request requires every resource to be granted.""" + + def __init__(self, granted_resources): + self.granted_resources = set(granted_resources) + + def auth_mapping(self, username=None, jwt=None): + return { + resource: [{"service": "fence", "method": "read-storage"}] + for resource in self.granted_resources + } + + def auth_request(self, jwt, service, methods, resources, user_id=None): + return all(resource in self.granted_resources for resource in resources) + + +class FakeBulk(object): + """Minimal BulkIndexedFiles stand-in that signs any URL it is asked for.""" + + def __init__(self, index_document): + self.guids = list(index_document) + self.index_document = index_document + self.auth_roles = [] + + def _get_signed_urls(self, protocol, file_id, *args): + return f"https://signed.example.com/{file_id}" + + +def _signed_guids(app, granted_resources, index_document, users_from_passports): + """Run a bulk request as a caller holding granted_resources; return signed GUIDs.""" + with mock.patch.object(app, "arborist", FakeArborist(granted_resources)): + with app.test_request_context(): + result = process_bulk_signed_urls( + FakeBulk(index_document), + protocol="s3", + expires_in=3600, + force_signed_url=True, + r_pays_project=None, + users_from_passports=users_from_passports, + acl_authorization_check=lambda file_id: False, + ) + return {entry["drs_object_id"] for entry in result.signed_urls} + + +@pytest.mark.parametrize( + "users_from_passports", + [None, {PASSPORT_USER: {}}], + ids=["bearer", "passport"], +) +def test_multi_resource_record_denied_with_partial_access(app, users_from_passports): + """A caller holding only one of a record's resources gets no signed URL.""" + index_document = {"guid-both": {"did": "guid-both", "authz": [OPEN, CONTROLLED]}} + + assert _signed_guids(app, {OPEN}, index_document, users_from_passports) == set() + + +@pytest.mark.parametrize( + "users_from_passports", + [None, {PASSPORT_USER: {}}], + ids=["bearer", "passport"], +) +def test_multi_resource_record_allowed_with_full_access(app, users_from_passports): + """A caller holding every one of a record's resources gets a signed URL.""" + index_document = {"guid-both": {"did": "guid-both", "authz": [OPEN, CONTROLLED]}} + + assert _signed_guids( + app, {OPEN, CONTROLLED}, index_document, users_from_passports + ) == {"guid-both"} + + +def test_partial_access_only_signs_fully_authorized_records(app): + """In a mixed batch, only records whose every resource is granted are signed.""" + index_document = { + "guid-open": {"did": "guid-open", "authz": [OPEN]}, + "guid-both": {"did": "guid-both", "authz": [OPEN, CONTROLLED]}, + } + + assert _signed_guids(app, {OPEN}, index_document, None) == {"guid-open"} diff --git a/tests/data/test_upload_existing_record_authz.py b/tests/data/test_upload_existing_record_authz.py new file mode 100644 index 000000000..93598cff7 --- /dev/null +++ b/tests/data/test_upload_existing_record_authz.py @@ -0,0 +1,170 @@ +""" +`POST /data/upload` accepts a caller-supplied GUID. When that GUID names a record that +already exists, the presigned PUT that comes back writes to that record's storage key, +so the caller has to be authorized against that record - holding the generic upload +permission is not enough. +""" + +import copy +import json + +import mock + + +VICTIM_AUTHZ = "/programs/VICTIM_ONLY" +UPLOAD_RESOURCE = "/data_file" + +EXISTING_RECORD = { + "did": "existing-guid", + "rev": "abcd1234", + "baseid": "base-id", + "file_name": "victim_file.txt", + "authz": [VICTIM_AUTHZ], + "acl": [], + "urls": ["s3://bucket1/existing-guid/victim_file.txt"], + "uploader": "someone_else", + "hashes": {}, + "metadata": {}, + "size": 10, + "form": "object", + "created_date": "", + "updated_date": "", +} + + +class MockResponse(object): + """Mock response for the requests and httpx libraries.""" + + def __init__(self, data, status_code=200): + self.data = data + self.status_code = status_code + + def json(self): + return self.data + + def text(self): + return self.data + + +def _arborist_allowing(allowed_resources): + """ + Build an arborist stub that authorizes only the given resource paths. + + Answering per-resource keeps these tests honest: a stub that denied everything + would also deny the generic upload check, so a 403 would not show that the + record's own authz was consulted. + + Args: + allowed_resources (set): resource paths to return ``auth: True`` for + + Returns: + callable: a replacement for httpx.Client.request + """ + + def handle_request(*args, **kwargs): + body = kwargs.get("json") or {} + auth_requests = body.get("requests") or [] + if not auth_requests: + return MockResponse({"auth": True}, 200) + authorized = all( + entry.get("resource") in allowed_resources for entry in auth_requests + ) + return MockResponse({"auth": authorized}, 200) + + return handle_request + + +def _post_upload_for_existing_guid( + client, encoded_creds_jwt, record, allowed_resources +): + """Request an upload URL for an existing GUID and return the response.""" + data_requests_mocker = mock.patch( + "fence.blueprints.data.indexd.requests", new_callable=mock.Mock + ) + arborist_requests_mocker = mock.patch( + "gen3authz.client.arborist.client.httpx.Client.request", new_callable=mock.Mock + ) + with data_requests_mocker as data_requests, arborist_requests_mocker as arborist_requests: + data_requests.get.return_value = MockResponse(record) + data_requests.get.return_value.status_code = 200 + arborist_requests.side_effect = _arborist_allowing(allowed_resources) + + return client.post( + "/data/upload", + headers={ + "Authorization": "Bearer " + encoded_creds_jwt.jwt, + "Content-Type": "application/json", + }, + data=json.dumps({"file_name": record["file_name"], "guid": record["did"]}), + ) + + +def test_upload_denied_without_permission_on_the_existing_record( + app, client, auth_client, encoded_creds_jwt, user_client, aws_signed_url +): + """A caller with only the generic upload permission cannot target another record.""" + response = _post_upload_for_existing_guid( + client, + encoded_creds_jwt, + EXISTING_RECORD, + allowed_resources={UPLOAD_RESOURCE}, + ) + + assert response.status_code == 403 + + +def test_upload_allowed_with_permission_on_the_existing_record( + app, client, auth_client, encoded_creds_jwt, user_client, aws_signed_url +): + """A caller authorized on the record's authz may request its upload URL.""" + response = _post_upload_for_existing_guid( + client, + encoded_creds_jwt, + EXISTING_RECORD, + allowed_resources={UPLOAD_RESOURCE, VICTIM_AUTHZ}, + ) + + assert response.status_code == 201 + assert "url" in response.json + + +def test_upload_to_unauthz_record_denied_for_other_uploader( + app, client, auth_client, encoded_creds_jwt, user_client, aws_signed_url +): + """With no authz on the record, a caller who is not the uploader is refused.""" + record = copy.deepcopy(EXISTING_RECORD) + record["authz"] = [] + record["uploader"] = "someone_else" + + response = _post_upload_for_existing_guid( + client, + encoded_creds_jwt, + record, + allowed_resources={UPLOAD_RESOURCE}, + ) + + assert response.status_code == 403 + + +def test_upload_to_unauthz_record_allowed_for_its_uploader( + app, client, auth_client, encoded_creds_jwt, user_client, aws_signed_url +): + """ + With no authz on the record, its own uploader may still request an upload URL. + + This is the blank-record flow: a record created by this user carries `uploader` + and no authz, and re-requesting its upload URL has to keep working. + """ + record = copy.deepcopy(EXISTING_RECORD) + record["authz"] = [] + record["uploader"] = user_client.username + + response = _post_upload_for_existing_guid( + client, + encoded_creds_jwt, + record, + allowed_resources={UPLOAD_RESOURCE}, + ) + + assert response.status_code == 201 + assert "url" in response.json diff --git a/tests/dbgap_sync/test_download_dir.py b/tests/dbgap_sync/test_download_dir.py new file mode 100644 index 000000000..9697408f3 --- /dev/null +++ b/tests/dbgap_sync/test_download_dir.py @@ -0,0 +1,125 @@ +""" +`download_dir` copies files off a remote SFTP server, so every filename it receives is +untrusted input. A malicious or compromised server can answer a directory listing with +path separators or an absolute path, which must not redirect the local write. +""" + +import os +from stat import S_IFDIR, S_IFREG + +import pytest + +from fence.sync.sync_users import download_dir + + +class FakeSFTPAttributes(object): + """Stand-in for paramiko's SFTPAttributes carrying only what `download_dir` reads.""" + + def __init__(self, filename: str, is_dir: bool = False) -> None: + self.filename = filename + self.st_mode = (S_IFDIR if is_dir else S_IFREG) | 0o644 + + +class FakeSFTPClient(object): + """ + Minimal SFTP client that serves a scripted listing and records local writes. + + `get` writes real bytes so that a traversal which escapes the download directory + leaves observable evidence on disk rather than only showing up in an assertion. + """ + + def __init__(self, listings: dict) -> None: + self.listings = listings + self.written = [] + + def listdir_attr(self, remote_dir: str) -> list: + return self.listings.get(remote_dir, []) + + def get(self, remote_path: str, local_path: str) -> None: + self.written.append(local_path) + # paramiko does not create missing parents; this harness does so that a + # traversal into a subdirectory is observable as a write rather than an + # unrelated FileNotFoundError. + os.makedirs(os.path.dirname(local_path), exist_ok=True) + with open(local_path, "wb") as local_file: + local_file.write(b"payload") + + +def _is_inside(path: str, directory: str) -> bool: + """Whether `path` resolves to a location within `directory`.""" + resolved_dir = os.path.realpath(directory) + return os.path.realpath(path).startswith(resolved_dir + os.sep) + + +@pytest.fixture +def local_dir(tmp_path): + """An existing, empty download destination.""" + destination = tmp_path / "download" + destination.mkdir() + return destination + + +def test_benign_filename_is_downloaded(local_dir): + """An ordinary filename is still fetched into the download directory.""" + sftp = FakeSFTPClient({"./": [FakeSFTPAttributes("telemetry.csv")]}) + + download_dir(sftp, "./", str(local_dir)) + + assert (local_dir / "telemetry.csv").read_bytes() == b"payload" + + +def test_absolute_remote_filename_cannot_escape(tmp_path, local_dir): + """An absolute filename from the server does not relocate the write.""" + escape_target = tmp_path / "escaped_absolute" + sftp = FakeSFTPClient({"./": [FakeSFTPAttributes(str(escape_target))]}) + + download_dir(sftp, "./", str(local_dir)) + + assert not escape_target.exists() + assert all(_is_inside(path, str(local_dir)) for path in sftp.written) + + +def test_relative_traversal_cannot_escape(tmp_path, local_dir): + """A `..` sequence in a server-supplied filename does not escape the directory.""" + escape_target = tmp_path / "escaped_relative" + sftp = FakeSFTPClient( + {"./": [FakeSFTPAttributes("../../download/../escaped_relative")]} + ) + + download_dir(sftp, "./", str(local_dir)) + + assert not escape_target.exists() + assert all(_is_inside(path, str(local_dir)) for path in sftp.written) + + +def test_traversing_directory_entry_cannot_escape(tmp_path, local_dir): + """A traversing directory entry does not place its children outside the directory.""" + sftp = FakeSFTPClient( + { + "./": [FakeSFTPAttributes("../escaped_dir", is_dir=True)], + ".//../escaped_dir": [FakeSFTPAttributes("payload.csv")], + } + ) + + download_dir(sftp, "./", str(local_dir)) + + assert not (tmp_path / "escaped_dir").exists() + assert all(_is_inside(path, str(local_dir)) for path in sftp.written) + + +def test_hostile_entry_does_not_prevent_benign_download(tmp_path, local_dir): + """A rejected entry is skipped without aborting the rest of the listing.""" + escape_target = tmp_path / "escaped_absolute" + sftp = FakeSFTPClient( + { + "./": [ + FakeSFTPAttributes(str(escape_target)), + FakeSFTPAttributes("telemetry.csv"), + ] + } + ) + + download_dir(sftp, "./", str(local_dir)) + + assert not escape_target.exists() + assert (local_dir / "telemetry.csv").read_bytes() == b"payload" diff --git a/tests/ga4gh/test_passport_account_state.py b/tests/ga4gh/test_passport_account_state.py new file mode 100644 index 000000000..7796e4546 --- /dev/null +++ b/tests/ga4gh/test_passport_account_state.py @@ -0,0 +1,103 @@ +""" +Resolving a visa identity to a Fence user grants that user's access and refreshes their +arborist policies, so it has to honor the same account-state decisions the login path +makes: a deactivated account stays deactivated, and a deployment that does not admit +new users on login does not gain them here either. +""" + +import time + +import jwt +import pytest + +from fence.config import config +from fence.errors import Unauthorized +from fence.models import query_for_user +from fence.resources.ga4gh.passports import get_or_create_gen3_user_from_iss_sub + + +ISSUER = "https://stsstg.nih.gov" +SUBJECT_ID = "account-state-subject" +USERNAME = SUBJECT_ID + ISSUER[len("https://") :] + + +def test_new_user_is_created_when_allowed(app, db_session): + """With new users allowed on login, an unseen visa identity gets an account.""" + with app.app_context(): + user = get_or_create_gen3_user_from_iss_sub( + ISSUER, SUBJECT_ID, db_session=db_session + ) + + assert user.username == USERNAME + + +def test_existing_active_user_is_returned(app, db_session): + """An active account is resolved on a later request.""" + with app.app_context(): + created = get_or_create_gen3_user_from_iss_sub( + ISSUER, SUBJECT_ID, db_session=db_session + ) + db_session.commit() + + resolved = get_or_create_gen3_user_from_iss_sub( + ISSUER, SUBJECT_ID, db_session=db_session + ) + + assert resolved.id == created.id + + +def test_deactivated_user_is_refused(app, db_session): + """A deactivated account is not resolved, so its access is not refreshed.""" + with app.app_context(): + user = get_or_create_gen3_user_from_iss_sub( + ISSUER, SUBJECT_ID, db_session=db_session + ) + user.active = False + db_session.commit() + + with pytest.raises(Unauthorized): + get_or_create_gen3_user_from_iss_sub( + ISSUER, SUBJECT_ID, db_session=db_session + ) + + +def test_new_user_is_not_created_when_disallowed(app, db_session, monkeypatch): + """ + With ALLOW_NEW_USER_ON_LOGIN off, an unseen visa identity does not get an account. + + The config comment for that setting promises the user can only log in once added + to the Fence database by a separate process, which this path would otherwise + sidestep entirely. + """ + monkeypatch.setitem(config, "ALLOW_NEW_USER_ON_LOGIN", False) + + with app.app_context(): + with pytest.raises(Unauthorized): + get_or_create_gen3_user_from_iss_sub( + ISSUER, SUBJECT_ID, db_session=db_session + ) + + assert not query_for_user(session=db_session, username=USERNAME) + + +def test_existing_user_still_resolves_when_new_users_disallowed( + app, db_session, monkeypatch +): + """ + ALLOW_NEW_USER_ON_LOGIN gates creation only, not resolution. + + Guards against the new check being written so broadly that it locks out the + pre-registered users such a deployment does expect to serve. + """ + with app.app_context(): + created = get_or_create_gen3_user_from_iss_sub( + ISSUER, SUBJECT_ID, db_session=db_session + ) + db_session.commit() + + monkeypatch.setitem(config, "ALLOW_NEW_USER_ON_LOGIN", False) + resolved = get_or_create_gen3_user_from_iss_sub( + ISSUER, SUBJECT_ID, db_session=db_session + ) + + assert resolved.id == created.id diff --git a/tests/ga4gh/test_passport_cache_isolation.py b/tests/ga4gh/test_passport_cache_isolation.py new file mode 100644 index 000000000..df62723f2 --- /dev/null +++ b/tests/ga4gh/test_passport_cache_isolation.py @@ -0,0 +1,174 @@ +""" +A GA4GH passport cache entry is keyed by one passport's hash and is trusted without +re-validation when it is hit again, so an entry must describe only the identities that +that passport itself asserts. +""" + +import hashlib +import json +import time +from unittest.mock import patch + +import flask +import httpx +import jwt +import responses + +from gen3authz.client.arborist.client import ArboristClient + +from fence.config import config +from fence.models import GA4GHPassportCache +from tests.conftest import NoAsyncMagicMock + + +VICTIM_SUB = "victim-sub-0001" +ATTACKER_SUB = "attacker-sub-0002" +ISSUER = "https://stsstg.nih.gov" + +VICTIM_USERNAME = VICTIM_SUB + ISSUER[len("https://") :] +ATTACKER_USERNAME = ATTACKER_SUB + ISSUER[len("https://") :] + +INDEXD_RECORD = { + "did": "1", + "baseid": "", + "rev": "", + "size": 10, + "file_name": "file1", + "urls": ["s3://bucket1/key", "gs://bucket1/key"], + "hashes": {}, + "metadata": {}, + "authz": ["/orgA/programs/phs000991.c1"], + "acl": ["*"], + "form": "", + "created_date": "", + "updated_date": "", +} + + +def _encode_passport(sub, private_key, kid, current_time, phs_id): + """Build a signed passport carrying one signed dbGaP visa for `sub`.""" + decoded_visa = { + "iss": ISSUER, + "sub": sub, + "iat": current_time, + "exp": current_time + 1000, + "scope": "openid ga4gh_passport_v1 email profile", + "jti": f"jti-{sub}", + "txn": f"txn-{sub}", + "name": "", + "ga4gh_visa_v1": { + "type": "https://ras.nih.gov/visas/v1.1", + "asserted": current_time, + "value": "https://stsstg.nih.gov/passport/dbgap/v1.1", + "source": "https://ncbi.nlm.nih.gov/gap", + }, + "ras_dbgap_permissions": [ + { + "consent_name": "Health/Medical/Biomedical", + "phs_id": phs_id, + "version": "v1", + "participant_set": "p1", + "consent_group": "c1", + "role": "designated user", + "expiration": current_time + 1000, + } + ], + } + encoded_visa = jwt.encode( + decoded_visa, key=private_key, headers={"kid": kid}, algorithm="RS256" + ) + + passport = { + "iss": ISSUER, + "sub": sub, + "iat": current_time, + "scope": "openid ga4gh_passport_v1 email profile", + "exp": current_time + 1000, + "ga4gh_passport_v1": [encoded_visa], + } + return jwt.encode( + passport, + key=private_key, + headers={"type": "JWT", "alg": "RS256", "kid": kid}, + algorithm="RS256", + ) + + +def _passport_hash(encoded_passport): + """The cache key for an encoded passport.""" + return hashlib.sha256(encoded_passport.encode("utf-8")).hexdigest() + + +@responses.activate +@patch("httpx.get") +@patch("fence.resources.google.utils._create_proxy_group") +@patch("fence.scripting.fence_create.ArboristClient") +def test_cache_entry_holds_only_its_own_passports_users( + mock_arborist, + mock_google_proxy_group, + mock_httpx_get, + client, + indexd_client, + kid, + rsa_private_key, + rsa_public_key, + indexd_client_accepting_record, + mock_arborist_requests, + google_proxy_group, + primary_google_service_account, + cloud_manager, + google_signed_url, + db_session, + monkeypatch, +): + """ + Submitting two passports in one request caches each one against only its own user. + + If an entry accumulated the other passport's identity, that passport alone would + later stand in for both users, since a cache hit skips re-validation. + """ + passport_cache = {} + from fence.resources.ga4gh import passports as passports_module + + monkeypatch.setattr(passports_module, "PASSPORT_CACHE", passport_cache) + db_session.query(GA4GHPassportCache).delete() + db_session.commit() + + monkeypatch.setitem(config, "GA4GH_PASSPORTS_TO_DRS_ENABLED", True) + indexd_client_accepting_record(INDEXD_RECORD) + mock_arborist_requests({"arborist/auth/request": {"POST": ({"auth": True}, 200)}}) + mock_arborist.return_value = NoAsyncMagicMock(ArboristClient) + mock_google_proxy_group.return_value = google_proxy_group + + keys = [keypair.public_key_to_jwk() for keypair in flask.current_app.keypairs] + mock_httpx_get.return_value = httpx.Response(200, json={"keys": keys}) + + now = int(time.time()) + victim_passport = _encode_passport( + VICTIM_SUB, rsa_private_key, kid, now, "phs000991" + ) + attacker_passport = _encode_passport( + ATTACKER_SUB, rsa_private_key, kid, now, "phs000961" + ) + + usernames = passports_module.sync_gen3_users_authz_from_ga4gh_passports( + [victim_passport, attacker_passport], + db_session=db_session, + ) + assert set(usernames) == {VICTIM_USERNAME, ATTACKER_USERNAME} + + cached_by_hash = { + row.passport_hash: row.user_ids + for row in db_session.query(GA4GHPassportCache).all() + } + + for encoded_passport, expected_username in ( + (victim_passport, VICTIM_USERNAME), + (attacker_passport, ATTACKER_USERNAME), + ): + entry_hash = _passport_hash(encoded_passport) + assert cached_by_hash.get(entry_hash) == [expected_username] + + in_memory_entry = passport_cache.get(entry_hash) + assert in_memory_entry + assert list(in_memory_entry[0]) == [expected_username] diff --git a/tests/ga4gh/test_passport_issuer_allowlist.py b/tests/ga4gh/test_passport_issuer_allowlist.py new file mode 100644 index 000000000..5f9a5e67f --- /dev/null +++ b/tests/ga4gh/test_passport_issuer_allowlist.py @@ -0,0 +1,136 @@ +""" +Passport and visa validation resolves a signing key over the network from the token's +own ``iss`` claim, which could be attacker-controlled until the signature is checked. The +issuer allowlist must therefore be consulted before any key retrieval, or a submitted +token can steer outbound requests regardless of whether it is ultimately rejected. +""" + +import time +from unittest.mock import patch + +import jwt +import pytest + +from fence.config import config +from fence.resources.ga4gh.passports import ( + get_unvalidated_visas_from_valid_passport, + validate_visa, +) + + +EVIL_ISSUER = "https://evil.example.com/oidc" + + +def _encode_token(private_key, kid, claims): + """Sign `claims` with the given key so the token is well formed but foreign.""" + return jwt.encode(claims, key=private_key, headers={"kid": kid}, algorithm="RS256") + + +@pytest.fixture +def evil_passport(rsa_private_key, kid): + """A structurally valid passport whose issuer is not on the allowlist.""" + now = int(time.time()) + return _encode_token( + rsa_private_key, + kid, + { + "iss": EVIL_ISSUER, + "sub": "attacker", + "iat": now, + "exp": now + 1000, + "scope": "openid ga4gh_passport_v1", + "ga4gh_passport_v1": [], + }, + ) + + +@pytest.fixture +def evil_visa(rsa_private_key, kid): + """A structurally valid visa whose issuer is not on the allowlist.""" + now = int(time.time()) + return _encode_token( + rsa_private_key, + kid, + { + "iss": EVIL_ISSUER, + "sub": "attacker", + "iat": now, + "exp": now + 1000, + "scope": "openid ga4gh_passport_v1", + "ga4gh_visa_v1": { + "type": "https://ras.nih.gov/visas/v1.1", + "asserted": now, + "value": "https://stsstg.nih.gov/passport/dbgap/v1.1", + "source": "https://ncbi.nlm.nih.gov/gap", + }, + }, + ) + + +def test_passport_with_foreign_issuer_yields_no_visas(app, evil_passport): + """A passport from an issuer outside the allowlist contributes no visas.""" + with app.app_context(): + assert get_unvalidated_visas_from_valid_passport(evil_passport) == [] + + +def test_passport_with_foreign_issuer_makes_no_outbound_request(app, evil_passport): + """Rejecting a foreign-issuer passport does not fetch keys from its issuer.""" + with patch("httpx.get") as mock_get: + with app.app_context(): + get_unvalidated_visas_from_valid_passport(evil_passport) + + assert not mock_get.called + + +def test_visa_with_foreign_issuer_is_rejected(app, evil_visa): + """A visa from an issuer outside the allowlist is rejected.""" + with app.app_context(): + with pytest.raises(Exception): + validate_visa(evil_visa) + + +def test_visa_with_foreign_issuer_makes_no_outbound_request(app, evil_visa): + """Rejecting a foreign-issuer visa does not fetch keys from its issuer.""" + with patch("httpx.get") as mock_get: + with app.app_context(): + with pytest.raises(Exception): + validate_visa(evil_visa) + + assert not mock_get.called + + +def test_allowlisted_issuer_is_not_rejected_by_the_precheck(app, rsa_private_key, kid): + """ + An allowlisted issuer still reaches signature validation. + + Guards against the issuer pre-check being written so strictly that it rejects the + issuers the deployment actually trusts. + """ + # A token issued by this application never triggers a key refresh, so the positive + # control needs an allowlisted issuer other than BASE_URL to observe the fetch. + allowed_issuer = next( + issuer + for issuer in config["GA4GH_VISA_ISSUER_ALLOWLIST"] + if issuer != config["BASE_URL"] + ) + now = int(time.time()) + passport = _encode_token( + rsa_private_key, + kid, + { + "iss": allowed_issuer, + "sub": "someone", + "iat": now, + "exp": now + 1000, + "scope": "openid ga4gh_passport_v1", + "ga4gh_passport_v1": [], + }, + ) + + with patch("httpx.get") as mock_get: + mock_get.return_value.status_code = 200 + mock_get.return_value.json.return_value = {"keys": []} + with app.app_context(): + get_unvalidated_visas_from_valid_passport(passport) + + assert mock_get.called diff --git a/tests/jwt/test_blacklist.py b/tests/jwt/test_blacklist.py index fdf02f9b1..1ac1d6fa0 100644 --- a/tests/jwt/test_blacklist.py +++ b/tests/jwt/test_blacklist.py @@ -1,8 +1,42 @@ -from fence.jwt.blacklist import blacklist_token, is_blacklisted, is_token_blacklisted +import jwt as pyjwt +import pytest + +from fence.config import config +from fence.jwt.blacklist import ( + blacklist_encoded_token, + blacklist_token, + is_blacklisted, + is_token_blacklisted, +) from tests import utils +def _encode_refresh_token(keypair): + """Sign a revocable refresh token with a specific keypair.""" + iat, exp = utils.iat_and_exp() + jti = utils.new_jti() + claims = { + "pur": "refresh", + "aud": "openid", + "scope": ["openid", "user"], + "sub": "1", + "iss": config["BASE_URL"], + "iat": iat, + "exp": exp, + "jti": jti, + "azp": "", + "context": {"user": {"name": "test"}}, + } + encoded_token = pyjwt.encode( + claims, + keypair.private_key, + algorithm="RS256", + headers={"kid": keypair.kid}, + ) + return encoded_token, jti, exp + + def test_jti_not_blacklisted(app): """ Test checking a ``jti`` which has not been blacklisted. @@ -27,3 +61,30 @@ def test_normal_token_not_blacklisted(app, encoded_jwt_refresh_token): """ _, is_blacklisted = is_token_blacklisted(encoded_jwt_refresh_token) assert not is_blacklisted + + +@pytest.mark.parametrize("keypair_index", [0, 1]) +def test_token_from_any_loaded_keypair_can_be_revoked(app, db_session, keypair_index): + """ + Revoking a token signed by any loaded keypair records it on the denylist. + + Every loaded keypair is published in the JWKS and accepted at validation, so + revocation has to cover the non-current ones too: otherwise a routine key rotation + silently leaves every outstanding token unrevokable. + """ + encoded_token, jti, _ = _encode_refresh_token(app.keypairs[keypair_index]) + + blacklist_encoded_token(encoded_token) + + assert is_blacklisted(jti) + + +@pytest.mark.parametrize("keypair_index", [0, 1]) +def test_denylist_check_covers_any_loaded_keypair(app, db_session, keypair_index): + """A revoked token reads as revoked whichever loaded keypair signed it.""" + encoded_token, jti, exp = _encode_refresh_token(app.keypairs[keypair_index]) + blacklist_token(jti, exp) + + _, token_is_blacklisted = is_token_blacklisted(encoded_token) + + assert token_is_blacklisted diff --git a/tests/login/test_google_login.py b/tests/login/test_google_login.py index f7550b47d..efca15f0d 100644 --- a/tests/login/test_google_login.py +++ b/tests/login/test_google_login.py @@ -1,12 +1,18 @@ import os +from fence.blueprints.login.base import state_session_key from fence.resources.storage.cdis_jwt import create_session_token from fence.config import config from fence.sync.sync_users import UserSyncer def test_google_login_error_handling(client): - r = client.get("/login/google/login?code=abc") + """An unusable authorization code is reported as a client error.""" + issued_state = "test-issued-login-state" + with client.session_transaction() as test_session: + test_session[state_session_key("google")] = issued_state + + r = client.get(f"/login/google/login?code=abc&state={issued_state}") assert r.status_code == 400 diff --git a/tests/login/test_login_redirect.py b/tests/login/test_login_redirect.py index 24ab53848..9eadbc6e7 100644 --- a/tests/login/test_login_redirect.py +++ b/tests/login/test_login_redirect.py @@ -175,3 +175,27 @@ def test_invalid_redirect_fails(client, idp): "/login/{}?redirect=https://evil-site.net".format(get_idp_route_name(idp)) ) assert response.status_code == 400 + + +@pytest.mark.parametrize( + "redirect", + [ + "//evil-site.net", + "//evil-site.net/steal", + "/\\evil-site.net", + "\\\\evil-site.net", + ], +) +@pytest.mark.parametrize("idp", all_available_idps()) +def test_authority_relative_redirect_fails(client, idp, redirect): + """ + Check that an authority-relative redirect is rejected rather than being mistaken + for a path on this application. + + A URL beginning ``//`` names a different host but still starts with a slash, and + browsers normalize a leading ``/\\`` or ``\\\\`` the same way. + """ + response = client.get( + "/login/{}?redirect={}".format(get_idp_route_name(idp), redirect) + ) + assert response.status_code == 400 diff --git a/tests/login/test_oauth_state.py b/tests/login/test_oauth_state.py new file mode 100644 index 000000000..6cfa93fcf --- /dev/null +++ b/tests/login/test_oauth_state.py @@ -0,0 +1,134 @@ +""" +The IdP login callback issues a Fence session for whatever identity an authorization +code resolves to, so the callback has to be bound to a login this browser actually +started. That binding is the OAuth `state` parameter. +""" + +from urllib.parse import parse_qsl, urlparse + +import pytest + +from fence.config import config +from fence.resources.storage.cdis_jwt import create_session_token + + +IDP_LOGIN_ROUTE = "/login/google" +IDP_ROUTE = "/login/google/login" +STATE_SESSION_KEY = "oauth2_state_google" +ISSUED_STATE = "issued-state-value" + + +@pytest.fixture +def session_with_state(app, client): + """Give the test client a session carrying an issued login state for Google.""" + + def seed(state=ISSUED_STATE): + session_jwt = create_session_token( + app.keypairs[0], + config.get("SESSION_TIMEOUT"), + context={STATE_SESSION_KEY: state}, + ) + client.set_cookie( + config["SESSION_COOKIE_NAME"], + session_jwt, + httponly=True, + samesite="Lax", + ) + + return seed + + +@pytest.fixture(autouse=True) +def real_google_login(monkeypatch): + """Exercise the real callback rather than the mock-login shortcut.""" + monkeypatch.setitem(config, "MOCK_GOOGLE_AUTH", False) + monkeypatch.setitem( + config["OPENID_CONNECT"].setdefault("google", {}), "mock", False + ) + + +def test_callback_rejects_code_with_no_state(client, session_with_state): + """A callback carrying only a code is refused.""" + session_with_state() + + response = client.get(IDP_ROUTE + "?code=ATTACKER_CODE") + + assert response.status_code == 401 + + +def test_callback_rejects_forged_state(client, session_with_state): + """A callback whose state was not issued by this session is refused.""" + session_with_state() + + response = client.get(IDP_ROUTE + "?code=ATTACKER_CODE&state=attacker-chosen") + + assert response.status_code == 401 + + +def test_callback_rejects_state_with_no_session(client): + """A callback arriving with no login in progress is refused.""" + response = client.get(IDP_ROUTE + f"?code=ATTACKER_CODE&state={ISSUED_STATE}") + + assert response.status_code == 401 + + +def test_callback_accepts_issued_state(client, session_with_state): + """ + A callback carrying the state this session was issued gets past the state check. + + The request then fails further along because the code is not redeemable here, so + this asserts only that the state check is not what rejected it. + """ + session_with_state() + + response = client.get(IDP_ROUTE + f"?code=ATTACKER_CODE&state={ISSUED_STATE}") + + assert response.status_code != 401 + + +def test_issued_state_is_single_use(client, session_with_state): + """Replaying a state that was already consumed is refused.""" + session_with_state() + + first = client.get(IDP_ROUTE + f"?code=ATTACKER_CODE&state={ISSUED_STATE}") + second = client.get(IDP_ROUTE + f"?code=ATTACKER_CODE&state={ISSUED_STATE}") + + assert first.status_code != 401 + assert second.status_code == 401 + + +def test_state_issued_by_login_is_accepted_by_callback(app, client, monkeypatch): + """ + A state minted by the login endpoint is accepted by the callback. + + Covers the whole hop rather than a seeded session: the login response has to + persist the state in a session cookie for a caller that arrived without one, and + the callback has to read it back out. + """ + monkeypatch.setattr( + app.google_client, "get_auth_url", lambda: "https://idp.example.com/authorize" + ) + + login_response = client.get(IDP_LOGIN_ROUTE + "?redirect=" + config["BASE_URL"]) + assert login_response.status_code == 302 + issued_state = dict(parse_qsl(urlparse(login_response.headers["Location"]).query))[ + "state" + ] + + response = client.get(IDP_ROUTE + f"?code=ATTACKER_CODE&state={issued_state}") + + assert response.status_code != 401 + + +def test_error_branch_requires_state(client, session_with_state): + """ + The IdP-error branch is also behind the state check. + + That branch redirects using a session value and runs before the code exchange, so + leaving it unguarded would keep an unauthenticated redirect primitive open. + """ + session_with_state() + + response = client.get(IDP_ROUTE + "?error=access_denied") + + assert response.status_code == 401 diff --git a/tests/test_metrics.py b/tests/test_metrics.py index ca53010c5..d785fcd8b 100644 --- a/tests/test_metrics.py +++ b/tests/test_metrics.py @@ -40,6 +40,7 @@ from fence.resources.audit.client import AUDIT_SCHEMA_CACHE, AuditServiceClient from fence.errors import InternalError from tests import utils +from tests.conftest import LOGIN_STATE_FOR_TESTS, seed_login_state from tests.conftest import all_available_idps # `reset_prometheus_metrics` must be imported even if not used so the autorun fixture gets triggered @@ -665,15 +666,22 @@ def test_login_log_login_endpoint( data={}, status_code=201, ) + # `prometheus_metrics_before` issues a request of its own, which rewrites the + # session cookie and drops the state the fixture seeded. + if idp not in ("fence", "shibboleth"): + seed_login_state(client, idp_name) + path = f"/login/{get_idp_route_name(idp)}/{callback_endpoint}" response = client.get(path, headers=headers) print(f"Response: {response.status_code}, Body: {response.data}") assert response.status_code == 200, response.text + # audit logs redact the `code` and `state` query parameters + logged_path = path.replace(LOGIN_STATE_FOR_TESTS, "redacted") user_sub = db_session.query(User).filter(User.username == username).first().id audit_service_requests.post.assert_called_once_with( "http://audit-service/log/login", json={ - "request_url": path, + "request_url": logged_path, "status_code": 200, "username": username, "sub": user_sub, @@ -829,7 +837,8 @@ def test_login_log_push_to_sqs( ) get_auth_info_patch.start() - path = "/login/google/login" + state = seed_login_state(client, "google") + path = f"/login/google/login?state={state}" response = client.get(path) assert response.status_code == 200, response.text # not checking the parameters here because we can't json.dumps "sub: ANY" From bee1ea877bbf77a57b9e4b488882846ce22404fc Mon Sep 17 00:00:00 2001 From: vincentvanhees Date: Mon, 5 Oct 2026 13:24:42 +0200 Subject: [PATCH 02/14] feat(smart): add SMART on FHIR / US Core compliance (CONF-0119, 0120, 0121-0123) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add server-side support for SMART App Launch 2.0 and US Core Backend Services requirements, with minimalistic changes to the existing OAuth2 infrastructure. - Add token introspection endpoint (RFC 7662) at /oauth2/introspect (CONF-0123) - Add private_key_jwt client authentication via FencePrivateKeyJWT (CONF-0120); clients register their JWKS in client_metadata["jwks"] — no schema migration needed - Extend ClientCredentialsGrant to accept private_key_jwt authentication - Fix validate_scopes to recognise system/ scopes as SMART scopes and drop the openid requirement for Backend Services flows (CONF-0121/0122) - Add PKCE support (RFC 7636) to the authorization code grant (CONF-0117) - Advertise client-confidential-asymmetric and private_key_jwt in well-known metadata; move the SMART discovery document to gen3-fhir-proxy (FHIR layer) - Filter null values from introspection responses per RFC 7662 --- fence/blueprints/oauth2.py | 13 ++- fence/blueprints/well_known.py | 3 + fence/models.py | 11 ++- fence/oidc/endpoints.py | 72 +++++++++++++++ fence/oidc/grants/client_credentials_grant.py | 11 ++- fence/oidc/grants/oidc_code_grant.py | 89 ++++++++++++++++++- fence/oidc/private_key_jwt.py | 61 +++++++++++++ fence/oidc/server.py | 5 +- fence/utils.py | 32 ++++++- 9 files changed, 289 insertions(+), 8 deletions(-) create mode 100644 fence/oidc/private_key_jwt.py diff --git a/fence/blueprints/oauth2.py b/fence/blueprints/oauth2.py index 221315f78..76787ba87 100644 --- a/fence/blueprints/oauth2.py +++ b/fence/blueprints/oauth2.py @@ -31,7 +31,7 @@ from fence.jwt.errors import JWTError from fence.jwt.token import SCOPE_DESCRIPTION from fence.models import Client -from fence.oidc.endpoints import RevocationEndpoint +from fence.oidc.endpoints import IntrospectionEndpoint, RevocationEndpoint from fence.oidc.server import server from fence.utils import clear_cookies from fence.user import get_current_user @@ -416,6 +416,17 @@ def revoke_token(): return "", 200 +@blueprint.route("/introspect", methods=["POST"]) +def introspect_token(): + """ + Token introspection per RFC 7662 and SMART App Launch (CONF-0123). + + Requires HTTP Basic auth with a registered client_id and client_secret. + Returns RFC 7662 introspection response with SMART patient context if present. + """ + return server.create_endpoint_response(IntrospectionEndpoint.ENDPOINT_NAME) + + @blueprint.route("/errors", methods=["GET"]) def display_error(): """ diff --git a/fence/blueprints/well_known.py b/fence/blueprints/well_known.py index 9cc1e91fc..92a5bb53b 100644 --- a/fence/blueprints/well_known.py +++ b/fence/blueprints/well_known.py @@ -2,6 +2,9 @@ This blueprint defines the endpoints under ``.well-known/``, which includes: - OIDC provider configuration - JWK endpoint ``/jwks`` + +The SMART App Launch 2.0 discovery document (``/.well-known/smart-configuration``) is +served by gen3-fhir-proxy, which is the FHIR-aware layer that SMART clients interact with. """ import flask diff --git a/fence/models.py b/fence/models.py index 09a08cbc3..fe98d021c 100644 --- a/fence/models.py +++ b/fence/models.py @@ -353,10 +353,14 @@ def check_requested_scopes(self, scopes): # Replaces Authlib method. Our logic does not actually look at token_auth_endpoint value def check_endpoint_auth_method(self, method, endpoint): """ - Only basic auth is supported. If anything else gets added, change this + Only basic auth and private_key_jwt are supported for confidential clients. """ if endpoint == "token": - protected_types = [ClientAuthType.basic.value, ClientAuthType.post.value] + protected_types = [ + ClientAuthType.basic.value, + ClientAuthType.post.value, + "private_key_jwt", # SMART Backend Services (CONF-0120) + ] return (self.is_confidential and method in protected_types) or ( not self.is_confidential and method == ClientAuthType.none.value ) @@ -391,6 +395,9 @@ class AuthorizationCode(Base, OAuth2AuthorizationCodeMixin): refresh_token_expires_in = Column(Integer, nullable=True) + code_challenge = Column(String, nullable=True) + code_challenge_method = Column(String, nullable=True) + _scope = Column(Text, default="") def __init__(self, **kwargs): diff --git a/fence/oidc/endpoints.py b/fence/oidc/endpoints.py index fd7ab7665..ae66242c6 100644 --- a/fence/oidc/endpoints.py +++ b/fence/oidc/endpoints.py @@ -1,5 +1,8 @@ +import time + from authlib.oauth2.rfc6749.errors import InvalidClientError, OAuth2Error import authlib.oauth2.rfc7009 +import authlib.oauth2.rfc7662 import bcrypt import flask @@ -126,3 +129,72 @@ def check_client(self, client): """ return self.client_id == client.client_id + + +class JWTIntrospectionToken: + """Wraps a validated JWT for use with IntrospectionEndpoint.""" + + def __init__(self, claims): + self.claims = claims + + def is_expired(self): + return time.time() > self.claims.get("exp", 0) + + def is_revoked(self): + jti = self.claims.get("jti") + return bool(jti) and fence.jwt.blacklist.is_blacklisted(jti) + + def check_client(self, client): + return self.claims.get("azp") == client.client_id + + +class IntrospectionEndpoint(authlib.oauth2.rfc7662.IntrospectionEndpoint): + """Token introspection per RFC 7662 / SMART App Launch (CONF-0123).""" + + def query_token(self, token_string, token_type_hint): + from fence.jwt.validate import validate_jwt + from fence.jwt.errors import JWTError + + try: + claims = validate_jwt( + encoded_token=token_string, + scope=None, + options={"verify_aud": False}, + ) + return JWTIntrospectionToken(claims) + except Exception: + return None + + def check_permission(self, token, client, request): + return True + + def introspect_token(self, token): + claims = token.claims + scope = claims.get("scope", "") + if isinstance(scope, list): + scope = " ".join(scope) + + payload = { + k: v + for k, v in { + "scope": scope, + "client_id": claims.get("azp"), + "sub": claims.get("sub"), + "iss": claims.get("iss"), + "exp": claims.get("exp"), + "iat": claims.get("iat"), + "jti": claims.get("jti"), + "token_type": "Bearer", + }.items() + if v is not None + } + + context = claims.get("context", {}) + user_name = context.get("user", {}).get("name") + if user_name: + payload["username"] = user_name + patient_id = context.get("patient") + if patient_id: + payload["patient"] = patient_id + + return payload diff --git a/fence/oidc/grants/client_credentials_grant.py b/fence/oidc/grants/client_credentials_grant.py index 4c130db69..eef901e8b 100644 --- a/fence/oidc/grants/client_credentials_grant.py +++ b/fence/oidc/grants/client_credentials_grant.py @@ -1 +1,10 @@ -from authlib.oauth2.rfc6749.grants import ClientCredentialsGrant +from authlib.oauth2.rfc6749.grants import ( + ClientCredentialsGrant as BaseClientCredentialsGrant, +) + + +class ClientCredentialsGrant(BaseClientCredentialsGrant): + # Extend the default ["client_secret_basic"] to also accept private_key_jwt + # so that SMART Backend Services clients can authenticate asymmetrically + # (CONF-0120). + TOKEN_ENDPOINT_AUTH_METHODS = ["client_secret_basic", "private_key_jwt"] diff --git a/fence/oidc/grants/oidc_code_grant.py b/fence/oidc/grants/oidc_code_grant.py index c6382269a..8a9de4ad7 100644 --- a/fence/oidc/grants/oidc_code_grant.py +++ b/fence/oidc/grants/oidc_code_grant.py @@ -38,7 +38,10 @@ def create_authorization_code(client, grant_user, request): Certain parameters in the ``AuthorizationCode`` are filled out using the arguments passed from the OAuth request (the redirect URI, scope, - and nonce). + nonce, and PKCE code_challenge). + + For PKCE support (RFC 7636), we persist the code_challenge and + code_challenge_method if provided by the client. """ # requested lifetime (in seconds) for the refresh token @@ -48,6 +51,11 @@ def create_authorization_code(client, grant_user, request): default=config["REFRESH_TOKEN_EXPIRES_IN"], ) + # Extract PKCE parameters from the request (RFC 7636) + # code_challenge is required for public clients, optional for confidential + code_challenge = request.data.get("code_challenge") + code_challenge_method = request.data.get("code_challenge_method", "S256") + code = AuthorizationCode( code=generate_token(50), client_id=client.client_id, @@ -55,6 +63,8 @@ def create_authorization_code(client, grant_user, request): scope=request.scope, user_id=grant_user.id, nonce=request.data.get("nonce"), + code_challenge=code_challenge, + code_challenge_method=code_challenge_method, refresh_token_expires_in=refresh_token_expires_in, ) @@ -73,6 +83,9 @@ def save_authorization_code(self, code, request): Returns: authorization code string + + For PKCE support (RFC 7636), we persist the code_challenge and + code_challenge_method if provided by the client. """ # requested lifetime (in seconds) for the refresh token refresh_token_expires_in = get_valid_expiration_from_request( @@ -82,6 +95,12 @@ def save_authorization_code(self, code, request): ) client = request.client + # Extract PKCE parameters from the request (RFC 7636) + code_challenge = request.payload.data.get("code_challenge") + code_challenge_method = request.payload.data.get( + "code_challenge_method", "S256" + ) + code = AuthorizationCode( code=code, client_id=client.client_id, @@ -89,6 +108,8 @@ def save_authorization_code(self, code, request): scope=request.payload.scope, user_id=request.user.id, nonce=request.payload.data.get("nonce"), + code_challenge=code_challenge, + code_challenge_method=code_challenge_method, refresh_token_expires_in=refresh_token_expires_in, ) @@ -224,13 +245,15 @@ def validate_request_prompt(self, end_user, redirect_uri): def validate_token_request(self): """ Validate token request by checking allowed grant type, - making sure authorization code is found, and redirect URI is valid + making sure authorization code is found, redirect URI is valid, + and PKCE code_verifier matches the stored code_challenge. Raises: UnauthorizedClientError: if grant type is incorrect InvalidRequestError: if authorization code is absent InvalidGrantError: if authorization code is invalid InvalidGrantError: if redirect_uri is invalid + InvalidGrantError: if PKCE verification fails """ # authenticate the client if client authentication is included logger.debug("Authenticating token client..") @@ -260,7 +283,69 @@ def validate_token_request(self): if original_redirect_uri and redirect_uri != original_redirect_uri: raise InvalidGrantError("Invalid 'redirect_uri' in request.") + # PKCE validation (RFC 7636) + # If code_challenge was stored during authorization, + # verify that the code_verifier in the token request matches + code_challenge = authorization_code.code_challenge + if code_challenge: + code_verifier = self.request.payload.data.get("code_verifier") + if not code_verifier: + raise InvalidGrantError( + "PKCE code_verifier is required (code_challenge was provided during authorization)" + ) + + # Validate the code_verifier matches the stored code_challenge + self._validate_code_verifier( + code_verifier, code_challenge, authorization_code.code_challenge_method + ) + # save for create_token_response self.request.client = client self.request.authorization_code = authorization_code self.execute_hook("after_validate_token_request") + + def _validate_code_verifier( + self, code_verifier, code_challenge, code_challenge_method + ): + """ + Validate the code_verifier against the stored code_challenge per RFC 7636 PKCE. + + Args: + code_verifier: The code_verifier from the token request + code_challenge: The stored code_challenge from the authorization code + code_challenge_method: The challenge method used ("S256" or "plain") + + Raises: + InvalidGrantError: If PKCE verification fails + """ + import hashlib + import base64 + + # RFC 7636 Section 4.2: code_verifier = ABD... (random string) + # RFC 7636 Section 4.3: code_challenge = base64url(sha256(code_verifier)) for S256 + # code_challenge = code_verifier for plain + + try: + if code_challenge_method == "S256": + # Calculate SHA256 hash of code_verifier and encode as base64url + sha256_hash = hashlib.sha256(code_verifier.encode("utf-8")).digest() + expected_challenge = ( + base64.urlsafe_b64encode(sha256_hash).decode("utf-8").rstrip("=") + ) + elif code_challenge_method == "plain": + expected_challenge = code_verifier + else: + # Unsupported challenge method - treat as validation failure + logger.warning( + f"Unsupported PKCE code_challenge_method: {code_challenge_method}" + ) + raise InvalidGrantError("Invalid PKCE code_challenge_method") + + # Compare challenges + if code_challenge != expected_challenge: + logger.warning(f"PKCE verification failed: code_challenge mismatch") + raise InvalidGrantError("Invalid PKCE code_verifier") + + except (ValueError, UnicodeDecodeError) as e: + logger.warning(f"PKCE verification error: {e}") + raise InvalidGrantError("Invalid PKCE code_verifier") diff --git a/fence/oidc/private_key_jwt.py b/fence/oidc/private_key_jwt.py new file mode 100644 index 000000000..de5a9d0f8 --- /dev/null +++ b/fence/oidc/private_key_jwt.py @@ -0,0 +1,61 @@ +""" +Private Key JWT client authentication for SMART Backend Services (CONF-0120). + +Clients register their JWKS in `client_metadata["jwks"]` and authenticate by +sending a JWT signed with their private key to the token endpoint. + +Limitation: JTI replay prevention is not implemented because it requires a +shared cache (e.g. Redis) across all fence worker processes. The `exp` claim +still limits the window of reuse to the token's lifetime. +""" + +from authlib.jose import JsonWebKey +from authlib.oauth2.rfc6749 import InvalidClientError +from authlib.oauth2.rfc7523.client import JWTBearerClientAssertion + +from fence.config import config + + +class FencePrivateKeyJWT(JWTBearerClientAssertion): + """Server-side handler for private_key_jwt client authentication. + + Registered as a client auth method on the OIDC server so that Backend + Services clients can authenticate without a shared secret. + """ + + CLIENT_AUTH_METHOD = "private_key_jwt" + + def __init__(self): + super().__init__(token_url=None, validate_jti=False) + + def create_claims_options(self): + token_url = config["BASE_URL"].rstrip("/") + "/oauth2/token" + return { + # iss MUST equal sub per RFC 7523 §3 + "iss": { + "essential": True, + "validate": lambda claims, iss: claims.get("sub") == iss, + }, + "sub": {"essential": True}, + "aud": {"essential": True, "value": token_url}, + "exp": {"essential": True}, + } + + def resolve_client_public_key(self, client, headers): + """Look up the client's public key from its registered JWKS.""" + jwks = client.jwks + if not jwks: + raise InvalidClientError( + "Client has no public keys registered. Set 'jwks' in client metadata." + ) + key_set = JsonWebKey.import_key_set(jwks) + kid = headers.get("kid") + if kid: + key = key_set.find_by_kid(kid) + if not key: + raise InvalidClientError(f"No key with kid={kid!r} in client JWKS") + return key + keys = list(key_set) + if not keys: + raise InvalidClientError("Client JWKS contains no keys") + return keys[0] diff --git a/fence/oidc/server.py b/fence/oidc/server.py index 67d0a6089..156f023e4 100644 --- a/fence/oidc/server.py +++ b/fence/oidc/server.py @@ -7,7 +7,7 @@ """ from fence.oidc.client import authenticate_public_client, query_client -from fence.oidc.endpoints import RevocationEndpoint +from fence.oidc.endpoints import IntrospectionEndpoint, RevocationEndpoint from fence.oidc.grants import ( AuthorizationCodeGrant, ImplicitGrant, @@ -15,6 +15,7 @@ ClientCredentialsGrant, ) from fence.oidc.oidc_server import OIDCServer +from fence.oidc.private_key_jwt import FencePrivateKeyJWT server = OIDCServer(query_client=query_client, save_token=lambda *_: None) @@ -23,4 +24,6 @@ server.register_grant(RefreshTokenGrant) server.register_grant(ClientCredentialsGrant) server.register_endpoint(RevocationEndpoint) +server.register_endpoint(IntrospectionEndpoint) server.register_client_auth_method("none", authenticate_public_client) +server.register_client_auth_method("private_key_jwt", FencePrivateKeyJWT()) diff --git a/fence/utils.py b/fence/utils.py index 06c1352b0..60efc5c41 100644 --- a/fence/utils.py +++ b/fence/utils.py @@ -457,13 +457,43 @@ def get_SQLAlchemyDriver(db_conn_url): } +# SMART scope prefixes (SMART App Launch 2.0 + SMART Backend Services) +_SMART_SCOPE_PREFIXES = ("patient/", "user/", "system/", "launch/") +_SMART_SCOPE_LITERALS = frozenset( + ["openid", "fhirUser", "profile", "online_access", "offline_access", "launch"] +) + + +def _is_smart_scope(scope: str) -> bool: + return scope.startswith(_SMART_SCOPE_PREFIXES) or scope in _SMART_SCOPE_LITERALS + + +def _requires_user_context(scope: str) -> bool: + """Return True for SMART scopes that require a logged-in user (not Backend Services).""" + return _is_smart_scope(scope) and not scope.startswith("system/") + + def validate_scopes(request_scopes, client): if not client: raise Exception("Client object is None") if request_scopes: scopes = scope_to_list(request_scopes) - # can we get some debug logs here that log the client, what scopes they have, and what scopes were requested + + is_smart_request = any(_is_smart_scope(scope) for scope in scopes) + + if is_smart_request: + # Backend Services use only system/ scopes — openid is not required or expected. + # User-facing SMART flows (authorization_code, EHR launch) MUST include openid. + needs_user_context = any(_requires_user_context(scope) for scope in scopes) + if needs_user_context and "openid" not in scopes: + logger.debug( + "SMART user-facing request lacks 'openid'. Requested: %s", + " ".join(scopes), + ) + raise InvalidScopeError("Failed to Authorize due to unsupported scope") + return True + if not client.check_requested_scopes(set(scopes)): logger.debug( "Request Scope are " From d94fbe9071e057a331ab71de8c541b0d35575146 Mon Sep 17 00:00:00 2001 From: vincentvanhees Date: Tue, 6 Oct 2026 18:56:38 +0200 Subject: [PATCH 03/14] refactor(oidc): make extra-scope allowlist and extra-claims config-driven MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fence previously hardcoded SMART on FHIR scope prefixes (patient/, user/, system/, launch/) and literals (fhirUser, openid, etc.) in create_client(), and hardcoded fhirUser ID-token claim generation in generate_id_token(). This couples fence to FHIR semantics it should not know about. Replace with two generic config mechanisms: EXTRA_ALLOWED_SCOPE_PREFIXES / EXTRA_ALLOWED_SCOPES Lists of scope prefixes and exact literals that bypass the CLIENT_ALLOWED_SCOPES check when registering OAuth clients. FHIR deployments set these in their fence-config.yaml; a stock fence deployment leaves them empty (defaults: []). SCOPE_EXTRA_CLAIMS Map of scope-name → {base_url_env, user_tag}. When a matching scope is requested and the authenticated user has the named tag, the claim is added to the ID token as "{env_var_value}/{tag_value}". FHIR deployments configure this for the fhirUser claim; fence itself carries no FHIR knowledge. Also include "scope" in token responses per RFC 6749 §5.1. --- fence/config-default.yaml | 21 +++++++++++++++++++++ fence/jwt/token.py | 11 +++++++++++ fence/oidc/jwt_generator.py | 2 ++ fence/utils.py | 11 ++++++++++- 4 files changed, 44 insertions(+), 1 deletion(-) diff --git a/fence/config-default.yaml b/fence/config-default.yaml index 2639bfde4..abce80d62 100755 --- a/fence/config-default.yaml +++ b/fence/config-default.yaml @@ -25,6 +25,27 @@ APP_NAME: 'Gen3 Data Commons' # Where fence microservice is deployed BASE_URL: 'http://localhost/user' + +# Extra scope prefixes (e.g. "patient/", "user/") and exact scope literals +# (e.g. "fhirUser", "launch") that are allowed when registering OAuth clients, +# in addition to CLIENT_ALLOWED_SCOPES. Scopes matching these patterns bypass +# the CLIENT_ALLOWED_SCOPES allowlist check. +EXTRA_ALLOWED_SCOPE_PREFIXES: [] +EXTRA_ALLOWED_SCOPES: [] + +# Adds extra claims to ID tokens when a matching scope is requested. +# Each key is both the scope name and the claim name added to the token. +# base_url_env: environment variable holding the base URL prefix. +# user_tag: user tag key whose value is appended to the base URL. +# The claim is set to "{base_url}/{tag_value}" when scope is present and the +# user has the named tag. Leave empty ({}) to disable. +# +# Example (SMART on FHIR fhirUser claim): +# SCOPE_EXTRA_CLAIMS: +# fhirUser: +# base_url_env: MY_FHIR_BASE_URL +# user_tag: fhir_user_id +SCOPE_EXTRA_CLAIMS: {} # postgres db to connect to # connection url format: # postgresql://[user[:password]@][netloc][:port][/dbname] diff --git a/fence/jwt/token.py b/fence/jwt/token.py index b1840e30f..f73ac1715 100644 --- a/fence/jwt/token.py +++ b/fence/jwt/token.py @@ -538,6 +538,17 @@ def generate_id_token( if user.tags: claims["context"]["user"]["tags"] = {tag.key: tag.value for tag in user.tags} + import os + + for claim_name, claim_cfg in config.get("SCOPE_EXTRA_CLAIMS", {}).items(): + if claim_name not in (scopes or []): + continue + base_url = os.environ.get(claim_cfg.get("base_url_env", ""), "").rstrip("/") + tag_value = claims["context"]["user"]["tags"].get( + claim_cfg.get("user_tag", "") + ) + if base_url and tag_value: + claims[claim_name] = f"{base_url}/{tag_value}" linked_google_email = kwargs.get("linked_google_email") linked_google_account_exp = kwargs.get("linked_google_account_exp") diff --git a/fence/oidc/jwt_generator.py b/fence/oidc/jwt_generator.py index 2787add77..8ee04af43 100644 --- a/fence/oidc/jwt_generator.py +++ b/fence/oidc/jwt_generator.py @@ -254,6 +254,7 @@ def generate_token_response( "access_token": access_token, "refresh_token": refresh_token, "expires_in": expires_in, + "scope": " ".join(scope), } @@ -293,4 +294,5 @@ def generate_client_response(client, expires_in=None, scope=None, **kwargs): "token_type": "Bearer", "access_token": access_token, "expires_in": expires_in, + "scope": " ".join(scope), } diff --git a/fence/utils.py b/fence/utils.py index 60efc5c41..409c7f63f 100644 --- a/fence/utils.py +++ b/fence/utils.py @@ -100,7 +100,16 @@ def create_client( auth_method = "client_secret_basic" if confidential else "none" allowed_scopes = allowed_scopes or config["CLIENT_ALLOWED_SCOPES"] - if not set(allowed_scopes).issubset(set(config["CLIENT_ALLOWED_SCOPES"])): + _extra_prefixes = tuple(config.get("EXTRA_ALLOWED_SCOPE_PREFIXES", [])) + _extra_literals = frozenset(config.get("EXTRA_ALLOWED_SCOPES", [])) + non_extra_scopes = [ + s + for s in allowed_scopes + if not ( + (_extra_prefixes and s.startswith(_extra_prefixes)) or s in _extra_literals + ) + ] + if not set(non_extra_scopes).issubset(set(config["CLIENT_ALLOWED_SCOPES"])): raise ValueError( "Each allowed scope must be one of: {}".format( config["CLIENT_ALLOWED_SCOPES"] From 6af61d693e45c4251e155dca7c306109d8e70517 Mon Sep 17 00:00:00 2001 From: avantol Date: Thu, 8 Oct 2026 12:40:11 -0500 Subject: [PATCH 04/14] feat(security): address review comments, more tests --- .dockerignore | 10 ++- fence/blueprints/data/blueprint.py | 3 + fence/blueprints/login/redirect.py | 13 +++ fence/resources/ga4gh/passports.py | 6 +- fence/utils.py | 5 ++ .../data/test_upload_existing_record_authz.py | 86 +++++++++++++++---- tests/dbgap_sync/test_download_dir.py | 2 +- tests/ga4gh/test_passport_account_state.py | 71 ++++++++++++++- tests/ga4gh/test_passport_cache_isolation.py | 24 +++--- tests/ga4gh/test_passport_issuer_allowlist.py | 44 +++++----- tests/login/test_login_redirect.py | 35 +++++++- tests/login/test_oauth_state.py | 14 +-- 12 files changed, 241 insertions(+), 72 deletions(-) diff --git a/.dockerignore b/.dockerignore index 352583cbe..be99ecada 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,8 +1,8 @@ # Signing keys. `keys/` is gitignored, so a clean checkout never has it, but a # developer's working tree does and `COPY . /fence` would bake it into the image. keys/ -*.pem -*.key +**/*.pem +**/*.key jwt-keys.tar # Deployment secrets and local config @@ -15,8 +15,8 @@ fence-config.yaml .venv/ venv/ ENV/ -__pycache__/ -*.py[cod] +**/__pycache__/ +**/*.py[cod] .mypy_cache/ .pytest_cache/ .cache/ @@ -42,6 +42,8 @@ dist/ # Compose files and docs, not needed at runtime docker-compose*.yml +# Root-only on purpose: `fence/static/privacy_policy.md` is served by +# /privacy-policy and must stay in the image, so this must not become `**/*.md`. *.md !README.md diff --git a/fence/blueprints/data/blueprint.py b/fence/blueprints/data/blueprint.py index ff2ce18f1..ba65940cb 100755 --- a/fence/blueprints/data/blueprint.py +++ b/fence/blueprints/data/blueprint.py @@ -264,6 +264,9 @@ def init_multipart_upload(): blank_index = BlankIndex(file_name=params["file_name"], guid=guid) + if guid: + _authorize_upload_to_existing_record(blank_index.index_document) + default_expires_in = flask.current_app.config.get("MAX_PRESIGNED_URL_TTL", 3600) expires_in = get_valid_expiration( params.get("expires_in"), diff --git a/fence/blueprints/login/redirect.py b/fence/blueprints/login/redirect.py index 590b53ffb..0f23edab6 100644 --- a/fence/blueprints/login/redirect.py +++ b/fence/blueprints/login/redirect.py @@ -3,6 +3,8 @@ folder). """ +import re + from cdislogging import get_logger import flask @@ -12,6 +14,9 @@ logger = get_logger(__name__) +# ASCII control characters: C0 (0x00-0x1F) and DEL (0x7F). +_CONTROL_CHARS = re.compile(r"[\x00-\x1f\x7f]") + def validate_redirect(url): """ @@ -33,6 +38,14 @@ def validate_redirect(url): Raises: UserError: if redirect URL in the request is disallowed """ + # Werkzeug's Location handling (via urlsplit) deletes tab, CR and LF anywhere in + # the URL, so "/\t/external-site.com" would be checked here as a local path but + # sent to the browser as "//external-site.com". Reject rather than strip, so the + # URL that passes this check is exactly the one stored for the redirect. + if url and _CONTROL_CHARS.search(url): + logger.error("invalid redirect {!r}: contains control characters".format(url)) + raise UserError("invalid login redirect URL") + allowed_redirects = allowed_login_redirects() if domain(url) not in allowed_redirects: logger.error( diff --git a/fence/resources/ga4gh/passports.py b/fence/resources/ga4gh/passports.py index 36ce9690d..684cf57ad 100644 --- a/fence/resources/ga4gh/passports.py +++ b/fence/resources/ga4gh/passports.py @@ -67,12 +67,14 @@ def sync_gen3_users_authz_from_ga4gh_passports( if cached_usernames: # there's a chance a given username exists in the cache but no longer in # the database. if not all are in db, ignore the cache and actually parse - # and validate the passport + # and validate the passport. + # A deactivated user is treated the same way, so that the full path + # refuses them instead of the cache handing back their access. all_users_exist_in_db = True usernames_to_update = {} for username in cached_usernames: user = query_for_user(session=db_session, username=username) - if not user: + if not user or user.active == False: all_users_exist_in_db = False continue usernames_to_update[user.username] = user diff --git a/fence/utils.py b/fence/utils.py index 1aac59a41..aec2dd94d 100644 --- a/fence/utils.py +++ b/fence/utils.py @@ -524,6 +524,11 @@ def domain(url): treated as a path on this application. Browsers normalize backslashes to forward slashes in the authority position, so "/\\host" and "\\\\host" get the same treatment. + + Control characters are not handled here: a URL like "/\\t/host" reads as a path + to this function but not to Werkzeug, which is why + ``fence.blueprints.login.redirect.validate_redirect`` rejects them before + calling this. """ if not url: return "" diff --git a/tests/data/test_upload_existing_record_authz.py b/tests/data/test_upload_existing_record_authz.py index 93598cff7..7038a8cc2 100644 --- a/tests/data/test_upload_existing_record_authz.py +++ b/tests/data/test_upload_existing_record_authz.py @@ -1,27 +1,35 @@ """ -`POST /data/upload` accepts a caller-supplied GUID. When that GUID names a record that -already exists, the presigned PUT that comes back writes to that record's storage key, -so the caller has to be authorized against that record - holding the generic upload -permission is not enough. +`POST /data/upload` and `POST /data/multipart/init` accept a caller-supplied GUID. When +that GUID names a record that already exists, the upload that comes back writes to that +record's storage key, so the caller has to be authorized against that record - holding +the generic upload permission is not enough. """ import copy import json import mock +import pytest -VICTIM_AUTHZ = "/programs/VICTIM_ONLY" +EXISTING_RECORD_AUTHZ = "/programs/EXISTING_RECORD_PROGRAM" UPLOAD_RESOURCE = "/data_file" +# Each upload endpoint that accepts a GUID, and the response field that proves the +# upload was handed out. +UPLOAD_ENDPOINTS = { + "/data/upload": "url", + "/data/multipart/init": "uploadId", +} + EXISTING_RECORD = { "did": "existing-guid", "rev": "abcd1234", "baseid": "base-id", - "file_name": "victim_file.txt", - "authz": [VICTIM_AUTHZ], + "file_name": "existing_file.txt", + "authz": [EXISTING_RECORD_AUTHZ], "acl": [], - "urls": ["s3://bucket1/existing-guid/victim_file.txt"], + "urls": ["s3://bucket1/existing-guid/existing_file.txt"], "uploader": "someone_else", "hashes": {}, "metadata": {}, @@ -74,23 +82,35 @@ def handle_request(*args, **kwargs): return handle_request +@pytest.fixture(params=sorted(UPLOAD_ENDPOINTS)) +def upload_endpoint(request): + """Each upload endpoint that accepts a caller-supplied GUID.""" + return request.param + + def _post_upload_for_existing_guid( - client, encoded_creds_jwt, record, allowed_resources + client, encoded_creds_jwt, record, allowed_resources, endpoint ): - """Request an upload URL for an existing GUID and return the response.""" + """Request an upload for an existing GUID from `endpoint` and return the response.""" data_requests_mocker = mock.patch( "fence.blueprints.data.indexd.requests", new_callable=mock.Mock ) arborist_requests_mocker = mock.patch( "gen3authz.client.arborist.client.httpx.Client.request", new_callable=mock.Mock ) - with data_requests_mocker as data_requests, arborist_requests_mocker as arborist_requests: + multipart_init_mocker = mock.patch( + "fence.blueprints.data.indexd.BlankIndex.init_multipart_upload", + return_value="test-upload-id", + ) + with data_requests_mocker as data_requests, arborist_requests_mocker as arborist_requests, ( + multipart_init_mocker + ): data_requests.get.return_value = MockResponse(record) data_requests.get.return_value.status_code = 200 arborist_requests.side_effect = _arborist_allowing(allowed_resources) return client.post( - "/data/upload", + endpoint, headers={ "Authorization": "Bearer " + encoded_creds_jwt.jwt, "Content-Type": "application/json", @@ -100,7 +120,13 @@ def _post_upload_for_existing_guid( def test_upload_denied_without_permission_on_the_existing_record( - app, client, auth_client, encoded_creds_jwt, user_client, aws_signed_url + app, + client, + auth_client, + encoded_creds_jwt, + user_client, + aws_signed_url, + upload_endpoint, ): """A caller with only the generic upload permission cannot target another record.""" response = _post_upload_for_existing_guid( @@ -108,28 +134,42 @@ def test_upload_denied_without_permission_on_the_existing_record( encoded_creds_jwt, EXISTING_RECORD, allowed_resources={UPLOAD_RESOURCE}, + endpoint=upload_endpoint, ) assert response.status_code == 403 def test_upload_allowed_with_permission_on_the_existing_record( - app, client, auth_client, encoded_creds_jwt, user_client, aws_signed_url + app, + client, + auth_client, + encoded_creds_jwt, + user_client, + aws_signed_url, + upload_endpoint, ): """A caller authorized on the record's authz may request its upload URL.""" response = _post_upload_for_existing_guid( client, encoded_creds_jwt, EXISTING_RECORD, - allowed_resources={UPLOAD_RESOURCE, VICTIM_AUTHZ}, + allowed_resources={UPLOAD_RESOURCE, EXISTING_RECORD_AUTHZ}, + endpoint=upload_endpoint, ) assert response.status_code == 201 - assert "url" in response.json + assert UPLOAD_ENDPOINTS[upload_endpoint] in response.json def test_upload_to_unauthz_record_denied_for_other_uploader( - app, client, auth_client, encoded_creds_jwt, user_client, aws_signed_url + app, + client, + auth_client, + encoded_creds_jwt, + user_client, + aws_signed_url, + upload_endpoint, ): """With no authz on the record, a caller who is not the uploader is refused.""" record = copy.deepcopy(EXISTING_RECORD) @@ -141,13 +181,20 @@ def test_upload_to_unauthz_record_denied_for_other_uploader( encoded_creds_jwt, record, allowed_resources={UPLOAD_RESOURCE}, + endpoint=upload_endpoint, ) assert response.status_code == 403 def test_upload_to_unauthz_record_allowed_for_its_uploader( - app, client, auth_client, encoded_creds_jwt, user_client, aws_signed_url + app, + client, + auth_client, + encoded_creds_jwt, + user_client, + aws_signed_url, + upload_endpoint, ): """ With no authz on the record, its own uploader may still request an upload URL. @@ -164,7 +211,8 @@ def test_upload_to_unauthz_record_allowed_for_its_uploader( encoded_creds_jwt, record, allowed_resources={UPLOAD_RESOURCE}, + endpoint=upload_endpoint, ) assert response.status_code == 201 - assert "url" in response.json + assert UPLOAD_ENDPOINTS[upload_endpoint] in response.json diff --git a/tests/dbgap_sync/test_download_dir.py b/tests/dbgap_sync/test_download_dir.py index 9697408f3..4c080698c 100644 --- a/tests/dbgap_sync/test_download_dir.py +++ b/tests/dbgap_sync/test_download_dir.py @@ -1,6 +1,6 @@ """ `download_dir` copies files off a remote SFTP server, so every filename it receives is -untrusted input. A malicious or compromised server can answer a directory listing with +untrusted input. A misbehaving or compromised server can answer a directory listing with path separators or an absolute path, which must not redirect the local write. """ diff --git a/tests/ga4gh/test_passport_account_state.py b/tests/ga4gh/test_passport_account_state.py index 7796e4546..f788f78a6 100644 --- a/tests/ga4gh/test_passport_account_state.py +++ b/tests/ga4gh/test_passport_account_state.py @@ -12,8 +12,13 @@ from fence.config import config from fence.errors import Unauthorized -from fence.models import query_for_user -from fence.resources.ga4gh.passports import get_or_create_gen3_user_from_iss_sub +from fence.models import GA4GHPassportCache, query_for_user +from fence.resources.ga4gh import passports as passports_module +from fence.resources.ga4gh.passports import ( + get_or_create_gen3_user_from_iss_sub, + put_gen3_usernames_for_passport_into_cache, + sync_gen3_users_authz_from_ga4gh_passports, +) ISSUER = "https://stsstg.nih.gov" @@ -21,6 +26,34 @@ USERNAME = SUBJECT_ID + ISSUER[len("https://") :] +@pytest.fixture +def cached_passport_for_user(app, db_session, monkeypatch): + """ + Create the visa identity's user and cache a passport as already resolving to them. + + The cached passport is not a valid JWT: a cache hit is trusted without + re-validation, so anything that falls through to full validation is discarded. + That makes the result show whether the cache alone resolved the user. + """ + monkeypatch.setattr(passports_module, "PASSPORT_CACHE", {}) + db_session.query(GA4GHPassportCache).delete() + db_session.commit() + + passport = "previously-validated-passport" + with app.app_context(): + user = get_or_create_gen3_user_from_iss_sub( + ISSUER, SUBJECT_ID, db_session=db_session + ) + put_gen3_usernames_for_passport_into_cache( + passport=passport, + user_ids_from_passports=[USERNAME], + expires_at=int(time.time()) + 1000, + db_session=db_session, + ) + db_session.commit() + return passport, user + + def test_new_user_is_created_when_allowed(app, db_session): """With new users allowed on login, an unseen visa identity gets an account.""" with app.app_context(): @@ -101,3 +134,37 @@ def test_existing_user_still_resolves_when_new_users_disallowed( ) assert resolved.id == created.id + + +def test_cached_passport_resolves_active_user( + app, db_session, cached_passport_for_user +): + """A cache hit for an active user resolves that user without re-validation.""" + passport, _ = cached_passport_for_user + + with app.app_context(): + users = sync_gen3_users_authz_from_ga4gh_passports( + [passport], db_session=db_session + ) + + assert list(users) == [USERNAME] + + +def test_cached_passport_does_not_resolve_deactivated_user( + app, db_session, cached_passport_for_user +): + """ + A cache hit does not resolve a user deactivated after their passport was cached. + + Otherwise deactivation would not take effect until the cache entry expired. + """ + passport, user = cached_passport_for_user + user.active = False + db_session.commit() + + with app.app_context(): + users = sync_gen3_users_authz_from_ga4gh_passports( + [passport], db_session=db_session + ) + + assert USERNAME not in users diff --git a/tests/ga4gh/test_passport_cache_isolation.py b/tests/ga4gh/test_passport_cache_isolation.py index df62723f2..031e6747c 100644 --- a/tests/ga4gh/test_passport_cache_isolation.py +++ b/tests/ga4gh/test_passport_cache_isolation.py @@ -21,12 +21,12 @@ from tests.conftest import NoAsyncMagicMock -VICTIM_SUB = "victim-sub-0001" -ATTACKER_SUB = "attacker-sub-0002" +USER_A_SUB = "user-a-sub-0001" +USER_B_SUB = "user-b-sub-0002" ISSUER = "https://stsstg.nih.gov" -VICTIM_USERNAME = VICTIM_SUB + ISSUER[len("https://") :] -ATTACKER_USERNAME = ATTACKER_SUB + ISSUER[len("https://") :] +USER_A_USERNAME = USER_A_SUB + ISSUER[len("https://") :] +USER_B_USERNAME = USER_B_SUB + ISSUER[len("https://") :] INDEXD_RECORD = { "did": "1", @@ -144,18 +144,18 @@ def test_cache_entry_holds_only_its_own_passports_users( mock_httpx_get.return_value = httpx.Response(200, json={"keys": keys}) now = int(time.time()) - victim_passport = _encode_passport( - VICTIM_SUB, rsa_private_key, kid, now, "phs000991" + user_a_passport = _encode_passport( + USER_A_SUB, rsa_private_key, kid, now, "phs000991" ) - attacker_passport = _encode_passport( - ATTACKER_SUB, rsa_private_key, kid, now, "phs000961" + user_b_passport = _encode_passport( + USER_B_SUB, rsa_private_key, kid, now, "phs000961" ) usernames = passports_module.sync_gen3_users_authz_from_ga4gh_passports( - [victim_passport, attacker_passport], + [user_a_passport, user_b_passport], db_session=db_session, ) - assert set(usernames) == {VICTIM_USERNAME, ATTACKER_USERNAME} + assert set(usernames) == {USER_A_USERNAME, USER_B_USERNAME} cached_by_hash = { row.passport_hash: row.user_ids @@ -163,8 +163,8 @@ def test_cache_entry_holds_only_its_own_passports_users( } for encoded_passport, expected_username in ( - (victim_passport, VICTIM_USERNAME), - (attacker_passport, ATTACKER_USERNAME), + (user_a_passport, USER_A_USERNAME), + (user_b_passport, USER_B_USERNAME), ): entry_hash = _passport_hash(encoded_passport) assert cached_by_hash.get(entry_hash) == [expected_username] diff --git a/tests/ga4gh/test_passport_issuer_allowlist.py b/tests/ga4gh/test_passport_issuer_allowlist.py index 5f9a5e67f..c6a86cc26 100644 --- a/tests/ga4gh/test_passport_issuer_allowlist.py +++ b/tests/ga4gh/test_passport_issuer_allowlist.py @@ -1,8 +1,8 @@ """ Passport and visa validation resolves a signing key over the network from the token's -own ``iss`` claim, which could be attacker-controlled until the signature is checked. The -issuer allowlist must therefore be consulted before any key retrieval, or a submitted -token can steer outbound requests regardless of whether it is ultimately rejected. +own ``iss`` claim, which is untrusted until the signature is checked. The issuer +allowlist must therefore be consulted before any key retrieval, or a submitted token can +steer outbound requests regardless of whether it is ultimately rejected. """ import time @@ -18,24 +18,24 @@ ) -EVIL_ISSUER = "https://evil.example.com/oidc" +UNLISTED_ISSUER = "https://unlisted-issuer.example.com/oidc" def _encode_token(private_key, kid, claims): - """Sign `claims` with the given key so the token is well formed but foreign.""" + """Sign `claims` with the given key so the token is well formed but unlisted.""" return jwt.encode(claims, key=private_key, headers={"kid": kid}, algorithm="RS256") @pytest.fixture -def evil_passport(rsa_private_key, kid): +def unlisted_passport(rsa_private_key, kid): """A structurally valid passport whose issuer is not on the allowlist.""" now = int(time.time()) return _encode_token( rsa_private_key, kid, { - "iss": EVIL_ISSUER, - "sub": "attacker", + "iss": UNLISTED_ISSUER, + "sub": "unlisted-user", "iat": now, "exp": now + 1000, "scope": "openid ga4gh_passport_v1", @@ -45,15 +45,15 @@ def evil_passport(rsa_private_key, kid): @pytest.fixture -def evil_visa(rsa_private_key, kid): +def unlisted_visa(rsa_private_key, kid): """A structurally valid visa whose issuer is not on the allowlist.""" now = int(time.time()) return _encode_token( rsa_private_key, kid, { - "iss": EVIL_ISSUER, - "sub": "attacker", + "iss": UNLISTED_ISSUER, + "sub": "unlisted-user", "iat": now, "exp": now + 1000, "scope": "openid ga4gh_passport_v1", @@ -67,34 +67,36 @@ def evil_visa(rsa_private_key, kid): ) -def test_passport_with_foreign_issuer_yields_no_visas(app, evil_passport): +def test_passport_with_unlisted_issuer_yields_no_visas(app, unlisted_passport): """A passport from an issuer outside the allowlist contributes no visas.""" with app.app_context(): - assert get_unvalidated_visas_from_valid_passport(evil_passport) == [] + assert get_unvalidated_visas_from_valid_passport(unlisted_passport) == [] -def test_passport_with_foreign_issuer_makes_no_outbound_request(app, evil_passport): - """Rejecting a foreign-issuer passport does not fetch keys from its issuer.""" +def test_passport_with_unlisted_issuer_makes_no_outbound_request( + app, unlisted_passport +): + """Rejecting an unlisted-issuer passport does not fetch keys from its issuer.""" with patch("httpx.get") as mock_get: with app.app_context(): - get_unvalidated_visas_from_valid_passport(evil_passport) + get_unvalidated_visas_from_valid_passport(unlisted_passport) assert not mock_get.called -def test_visa_with_foreign_issuer_is_rejected(app, evil_visa): +def test_visa_with_unlisted_issuer_is_rejected(app, unlisted_visa): """A visa from an issuer outside the allowlist is rejected.""" with app.app_context(): with pytest.raises(Exception): - validate_visa(evil_visa) + validate_visa(unlisted_visa) -def test_visa_with_foreign_issuer_makes_no_outbound_request(app, evil_visa): - """Rejecting a foreign-issuer visa does not fetch keys from its issuer.""" +def test_visa_with_unlisted_issuer_makes_no_outbound_request(app, unlisted_visa): + """Rejecting an unlisted-issuer visa does not fetch keys from its issuer.""" with patch("httpx.get") as mock_get: with app.app_context(): with pytest.raises(Exception): - validate_visa(evil_visa) + validate_visa(unlisted_visa) assert not mock_get.called diff --git a/tests/login/test_login_redirect.py b/tests/login/test_login_redirect.py index 9eadbc6e7..3813193c9 100644 --- a/tests/login/test_login_redirect.py +++ b/tests/login/test_login_redirect.py @@ -180,10 +180,10 @@ def test_invalid_redirect_fails(client, idp): @pytest.mark.parametrize( "redirect", [ - "//evil-site.net", - "//evil-site.net/steal", - "/\\evil-site.net", - "\\\\evil-site.net", + "//external-site.net", + "//external-site.net/path", + "/\\external-site.net", + "\\\\external-site.net", ], ) @pytest.mark.parametrize("idp", all_available_idps()) @@ -199,3 +199,30 @@ def test_authority_relative_redirect_fails(client, idp, redirect): "/login/{}?redirect={}".format(get_idp_route_name(idp), redirect) ) assert response.status_code == 400 + + +@pytest.mark.parametrize( + "encoded_redirect", + [ + "/%09/external-site.net", + "/%09/%09external-site.net", + "/%0A/external-site.net", + "/%0D/external-site.net", + "%09//external-site.net", + "/%7F/external-site.net", + ], +) +@pytest.mark.parametrize("idp", all_available_idps()) +def test_control_character_redirect_fails(client, idp, encoded_redirect): + """ + Check that a redirect containing ASCII control characters is rejected. + + Werkzeug deletes tab, CR and LF when it writes the ``Location`` header, so + ``//external-site.net`` would otherwise pass as a local path and leave as + ``//external-site.net``. The redirect is sent percent-encoded, as a crafted link + would carry it, so the decoded value fence sees holds the raw character. + """ + response = client.get( + "/login/{}?redirect={}".format(get_idp_route_name(idp), encoded_redirect) + ) + assert response.status_code == 400 diff --git a/tests/login/test_oauth_state.py b/tests/login/test_oauth_state.py index 6cfa93fcf..bcad993ee 100644 --- a/tests/login/test_oauth_state.py +++ b/tests/login/test_oauth_state.py @@ -51,7 +51,7 @@ def test_callback_rejects_code_with_no_state(client, session_with_state): """A callback carrying only a code is refused.""" session_with_state() - response = client.get(IDP_ROUTE + "?code=ATTACKER_CODE") + response = client.get(IDP_ROUTE + "?code=UNSOLICITED_CODE") assert response.status_code == 401 @@ -60,14 +60,14 @@ def test_callback_rejects_forged_state(client, session_with_state): """A callback whose state was not issued by this session is refused.""" session_with_state() - response = client.get(IDP_ROUTE + "?code=ATTACKER_CODE&state=attacker-chosen") + response = client.get(IDP_ROUTE + "?code=UNSOLICITED_CODE&state=unissued-state") assert response.status_code == 401 def test_callback_rejects_state_with_no_session(client): """A callback arriving with no login in progress is refused.""" - response = client.get(IDP_ROUTE + f"?code=ATTACKER_CODE&state={ISSUED_STATE}") + response = client.get(IDP_ROUTE + f"?code=UNSOLICITED_CODE&state={ISSUED_STATE}") assert response.status_code == 401 @@ -81,7 +81,7 @@ def test_callback_accepts_issued_state(client, session_with_state): """ session_with_state() - response = client.get(IDP_ROUTE + f"?code=ATTACKER_CODE&state={ISSUED_STATE}") + response = client.get(IDP_ROUTE + f"?code=UNSOLICITED_CODE&state={ISSUED_STATE}") assert response.status_code != 401 @@ -90,8 +90,8 @@ def test_issued_state_is_single_use(client, session_with_state): """Replaying a state that was already consumed is refused.""" session_with_state() - first = client.get(IDP_ROUTE + f"?code=ATTACKER_CODE&state={ISSUED_STATE}") - second = client.get(IDP_ROUTE + f"?code=ATTACKER_CODE&state={ISSUED_STATE}") + first = client.get(IDP_ROUTE + f"?code=UNSOLICITED_CODE&state={ISSUED_STATE}") + second = client.get(IDP_ROUTE + f"?code=UNSOLICITED_CODE&state={ISSUED_STATE}") assert first.status_code != 401 assert second.status_code == 401 @@ -115,7 +115,7 @@ def test_state_issued_by_login_is_accepted_by_callback(app, client, monkeypatch) "state" ] - response = client.get(IDP_ROUTE + f"?code=ATTACKER_CODE&state={issued_state}") + response = client.get(IDP_ROUTE + f"?code=UNSOLICITED_CODE&state={issued_state}") assert response.status_code != 401 From 4b4642045cc539cd74f2af196a72b09ac50dc7d0 Mon Sep 17 00:00:00 2001 From: avantol Date: Thu, 8 Oct 2026 13:53:39 -0500 Subject: [PATCH 05/14] fix(passports): gather and validate users for ALL passports in request before processing/syncing any --- fence/resources/ga4gh/passports.py | 33 +++++++--- tests/ga4gh/test_passport_account_state.py | 72 ++++++++++++++++++++++ 2 files changed, 97 insertions(+), 8 deletions(-) diff --git a/fence/resources/ga4gh/passports.py b/fence/resources/ga4gh/passports.py index 684cf57ad..ca7d05b73 100644 --- a/fence/resources/ga4gh/passports.py +++ b/fence/resources/ga4gh/passports.py @@ -54,11 +54,18 @@ def sync_gen3_users_authz_from_ga4gh_passports( Return: list: a list of users, each corresponding to a valid visa identity embedded within the passports passed in + + Raises: + Unauthorized: if any visa identity maps to a deactivated user, or to a new + user while ALLOW_NEW_USER_ON_LOGIN is off. Nothing is synced to Arborist + for any passport in that case. """ db_session = db_session or current_app.scoped_session() # {"username": user, "username2": user2} users_from_all_passports = {} + + resolved_passports = [] for passport in passports: try: cached_usernames = get_gen3_usernames_for_passport_from_cache( @@ -129,12 +136,25 @@ def sync_gen3_users_authz_from_ga4gh_passports( ) continue - users_from_current_passport = [] - for (issuer, subject_id), visas in identity_to_visas.items(): - gen3_user = get_or_create_gen3_user_from_iss_sub( - issuer, subject_id, db_session=db_session + users_and_visas = [ + ( + get_or_create_gen3_user_from_iss_sub( + issuer, subject_id, db_session=db_session + ), + visas, ) - + for (issuer, subject_id), visas in identity_to_visas.items() + ] + for gen3_user, _ in users_and_visas: + users_from_all_passports[gen3_user.username] = gen3_user + resolved_passports.append((passport, users_and_visas, min_visa_expiration)) + + # Every identity in every passport is resolved above before any authorization is + # synced here, so a refused identity (which raises Unauthorized) fails the request + # without having granted arborist access to the identities resolved before it + for passport, users_and_visas, min_visa_expiration in resolved_passports: + users_from_current_passport = [] + for gen3_user, visas in users_and_visas: ga4gh_visas = [ GA4GHVisaV1( user=gen3_user, @@ -158,9 +178,6 @@ def sync_gen3_users_authz_from_ga4gh_passports( ) users_from_current_passport.append(gen3_user) - for user in users_from_current_passport: - users_from_all_passports[user.username] = user - usernames_from_current_passport = [ user.username for user in users_from_current_passport ] diff --git a/tests/ga4gh/test_passport_account_state.py b/tests/ga4gh/test_passport_account_state.py index f788f78a6..e43f5ed7a 100644 --- a/tests/ga4gh/test_passport_account_state.py +++ b/tests/ga4gh/test_passport_account_state.py @@ -6,7 +6,9 @@ """ import time +from unittest.mock import patch +import httpx import jwt import pytest @@ -24,6 +26,37 @@ ISSUER = "https://stsstg.nih.gov" SUBJECT_ID = "account-state-subject" USERNAME = SUBJECT_ID + ISSUER[len("https://") :] +DEACTIVATED_SUBJECT_ID = "deactivated-subject" + + +def _encode_passport(sub, private_key, kid, current_time): + """Build a signed passport carrying one signed visa for `sub`.""" + visa = { + "iss": ISSUER, + "sub": sub, + "iat": current_time, + "exp": current_time + 1000, + "scope": "openid ga4gh_passport_v1 email profile", + "ga4gh_visa_v1": { + "type": "https://ras.nih.gov/visas/v1.1", + "asserted": current_time, + "value": "https://stsstg.nih.gov/passport/dbgap/v1.1", + "source": "https://ncbi.nlm.nih.gov/gap", + }, + } + passport = { + "iss": ISSUER, + "sub": sub, + "iat": current_time, + "exp": current_time + 1000, + "scope": "openid ga4gh_passport_v1 email profile", + "ga4gh_passport_v1": [ + jwt.encode(visa, key=private_key, headers={"kid": kid}, algorithm="RS256") + ], + } + return jwt.encode( + passport, key=private_key, headers={"kid": kid}, algorithm="RS256" + ) @pytest.fixture @@ -168,3 +201,42 @@ def test_cached_passport_does_not_resolve_deactivated_user( ) assert USERNAME not in users + + +@patch("httpx.get") +def test_deactivated_identity_blocks_sync_for_every_passport( + mock_httpx_get, app, db_session, kid, rsa_private_key, monkeypatch +): + """ + A passport for a deactivated user refuses the request before any access is synced. + + That includes an active user's passport submitted ahead of it in the same request, + whose arborist access would otherwise be granted and left in place. + """ + monkeypatch.setattr(passports_module, "PASSPORT_CACHE", {}) + db_session.query(GA4GHPassportCache).delete() + db_session.commit() + keys = [keypair.public_key_to_jwk() for keypair in app.keypairs] + mock_httpx_get.return_value = httpx.Response(200, json={"keys": keys}) + + now = int(time.time()) + with app.app_context(): + deactivated = get_or_create_gen3_user_from_iss_sub( + ISSUER, DEACTIVATED_SUBJECT_ID, db_session=db_session + ) + deactivated.active = False + db_session.commit() + + passports = [ + _encode_passport(SUBJECT_ID, rsa_private_key, kid, now), + _encode_passport(DEACTIVATED_SUBJECT_ID, rsa_private_key, kid, now), + ] + with patch.object( + passports_module, "_sync_validated_visa_authorization" + ) as mock_sync: + with pytest.raises(Unauthorized): + sync_gen3_users_authz_from_ga4gh_passports( + passports, db_session=db_session + ) + + mock_sync.assert_not_called() From 80fb69445ee7dc4ce378bc5bc89983ad36dc4ff3 Mon Sep 17 00:00:00 2001 From: vincentvanhees Date: Mon, 12 Oct 2026 15:32:54 +0200 Subject: [PATCH 06/14] fix(oidc): validate non-SMART scopes against client allowlist validate_scopes() returned True early for any request containing a SMART scope (including openid), bypassing client.check_requested_scopes() for the entire request. This allowed arbitrary unknown scopes to pass through unchecked whenever openid was also present. Non-SMART scopes are now explicitly validated against the client's allowed scopes even when the request is identified as a SMART flow. --- fence/utils.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/fence/utils.py b/fence/utils.py index 409c7f63f..8bbde3ba0 100644 --- a/fence/utils.py +++ b/fence/utils.py @@ -501,6 +501,18 @@ def validate_scopes(request_scopes, client): " ".join(scopes), ) raise InvalidScopeError("Failed to Authorize due to unsupported scope") + # SMART scopes are allowed beyond CLIENT_ALLOWED_SCOPES, but any + # non-SMART scopes in the request still must be on the client allowlist. + non_smart_scopes = {s for s in scopes if not _is_smart_scope(s)} + if non_smart_scopes and not set(client.allowed_scopes).issuperset( + non_smart_scopes + ): + logger.debug( + "Non-SMART scopes %s not in client allowed scopes %s", + non_smart_scopes, + client.scope, + ) + raise InvalidScopeError("Failed to Authorize due to unsupported scope") return True if not client.check_requested_scopes(set(scopes)): From 2947eeccc63886d212f42348c76e3b2f42fa0582 Mon Sep 17 00:00:00 2001 From: vincentvanhees Date: Tue, 13 Oct 2026 15:03:05 +0200 Subject: [PATCH 07/14] test(oidc): add test coverage for SMART/US Core branch and fix two bugs Fix resolve_client_public_key() in private_key_jwt.py for authlib 1.6.12 compatibility: list(key_set) is not supported (use key_set.keys), and find_by_kid() raises ValueError on a miss rather than returning None. Set PROMETHEUS_MULTIPROC_DIR in tests/conftest.py before fence is imported so the test suite runs without requiring the env var to be set manually. Add 74 new tests across 5 files targeting the new SMART on FHIR / US Core compliance code on this branch: - tests/oidc/test_private_key_jwt.py: create_claims_options and all resolve_client_public_key branches (no JWKS, matching kid, unknown kid, no kid, empty key set) - tests/oidc/test_endpoints.py: JWTIntrospectionToken, IntrospectionEndpoint payload building and query_token, revoke and introspect HTTP endpoints - tests/oidc/test_jwt_generator.py: generate_client_response scope handling, client_credentials dispatch, implicit scope-as-string path - tests/oidc/test_oidc_code_grant.py: PKCE S256/plain full flows and _validate_code_verifier branches, exists_nonce, validate_request_prompt (none/consent/select_account prompts), token request error paths - tests/oidc/test_oauth2_blueprint.py: auto_approve, consent denial, prompt=none error cases, unauthenticated authorize paths, revoke with invalid token, display_error endpoint --- .secrets.baseline | 6 +- fence/oidc/private_key_jwt.py | 11 +- tests/conftest.py | 4 + tests/oidc/test_endpoints.py | 283 ++++++++++++++++++++ tests/oidc/test_jwt_generator.py | 175 ++++++++++++ tests/oidc/test_oauth2_blueprint.py | 381 ++++++++++++++++++++++++++ tests/oidc/test_oidc_code_grant.py | 397 ++++++++++++++++++++++++++++ tests/oidc/test_private_key_jwt.py | 149 +++++++++++ 8 files changed, 1398 insertions(+), 8 deletions(-) create mode 100644 tests/oidc/test_endpoints.py create mode 100644 tests/oidc/test_jwt_generator.py create mode 100644 tests/oidc/test_oauth2_blueprint.py create mode 100644 tests/oidc/test_oidc_code_grant.py create mode 100644 tests/oidc/test_private_key_jwt.py diff --git a/.secrets.baseline b/.secrets.baseline index a506bd690..918d5ac52 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -214,14 +214,14 @@ "filename": "tests/conftest.py", "hashed_secret": "1348b145fa1a555461c1b790a2f66614781091e9", "is_verified": false, - "line_number": 1628 + "line_number": 1632 }, { "type": "Base64 High Entropy String", "filename": "tests/conftest.py", "hashed_secret": "227dea087477346785aefd575f91dd13ab86c108", "is_verified": false, - "line_number": 1652 + "line_number": 1656 } ], "tests/credentials/google/test_credentials.py": [ @@ -279,5 +279,5 @@ } ] }, - "generated_at": "2026-06-23T22:51:27Z" + "generated_at": "2026-10-13T13:03:00Z" } diff --git a/fence/oidc/private_key_jwt.py b/fence/oidc/private_key_jwt.py index de5a9d0f8..cebeb39e7 100644 --- a/fence/oidc/private_key_jwt.py +++ b/fence/oidc/private_key_jwt.py @@ -51,11 +51,12 @@ def resolve_client_public_key(self, client, headers): key_set = JsonWebKey.import_key_set(jwks) kid = headers.get("kid") if kid: - key = key_set.find_by_kid(kid) - if not key: + try: + key = key_set.find_by_kid(kid) + except (ValueError, KeyError): raise InvalidClientError(f"No key with kid={kid!r} in client JWKS") return key - keys = list(key_set) - if not keys: + # No kid: return the first key in the set + if not key_set.keys: raise InvalidClientError("Client JWKS contains no keys") - return keys[0] + return key_set.keys[0] diff --git a/tests/conftest.py b/tests/conftest.py index 55f109c6f..d085b8a87 100755 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,6 +7,10 @@ from collections import OrderedDict import json import os + +# Must be set before fence (and prometheus_client) is imported. +os.makedirs("/tmp/prometheus_metrics", exist_ok=True) +os.environ.setdefault("PROMETHEUS_MULTIPROC_DIR", "/tmp/prometheus_metrics") import copy import time import flask diff --git a/tests/oidc/test_endpoints.py b/tests/oidc/test_endpoints.py new file mode 100644 index 000000000..84ac8192a --- /dev/null +++ b/tests/oidc/test_endpoints.py @@ -0,0 +1,283 @@ +""" +Tests for fence.oidc.endpoints. + +Covers: +- JWTIntrospectionToken: is_expired, is_revoked, check_client +- IntrospectionEndpoint.introspect_token: payload fields, scope handling, context claims +- IntrospectionEndpoint.query_token: valid JWT → token object, invalid JWT → None +- RevocationEndpoint.query_token and revoke_token via HTTP +- JWTToken: client matching via revoke endpoint +""" + +import time +from unittest.mock import MagicMock, patch + +import pytest + +from fence.oidc.endpoints import JWTIntrospectionToken, IntrospectionEndpoint +from tests.utils.oauth2 import create_basic_header + + +# --------------------------------------------------------------------------- +# JWTIntrospectionToken unit tests +# --------------------------------------------------------------------------- + + +def test_jwt_introspection_token_expired_when_exp_in_past(): + token = JWTIntrospectionToken({"exp": int(time.time()) - 60}) + assert token.is_expired() is True + + +def test_jwt_introspection_token_not_expired_when_exp_in_future(): + token = JWTIntrospectionToken({"exp": int(time.time()) + 3600}) + assert token.is_expired() is False + + +def test_jwt_introspection_token_not_expired_without_exp(): + """No exp claim → treated as epoch 0, which is in the past → expired.""" + token = JWTIntrospectionToken({}) + assert token.is_expired() is True + + +def test_jwt_introspection_token_is_revoked_when_jti_blacklisted(): + with patch("fence.jwt.blacklist.is_blacklisted", return_value=True): + token = JWTIntrospectionToken({"jti": "some-jti"}) + assert token.is_revoked() is True + + +def test_jwt_introspection_token_not_revoked_when_jti_not_blacklisted(): + with patch("fence.jwt.blacklist.is_blacklisted", return_value=False): + token = JWTIntrospectionToken({"jti": "some-jti"}) + assert token.is_revoked() is False + + +def test_jwt_introspection_token_not_revoked_when_no_jti(): + """Without a jti, the token is considered not revoked.""" + token = JWTIntrospectionToken({}) + assert token.is_revoked() is False + + +def test_jwt_introspection_token_check_client_matches(): + client = MagicMock() + client.client_id = "my-client" + token = JWTIntrospectionToken({"azp": "my-client"}) + assert token.check_client(client) is True + + +def test_jwt_introspection_token_check_client_mismatches(): + client = MagicMock() + client.client_id = "other-client" + token = JWTIntrospectionToken({"azp": "my-client"}) + assert token.check_client(client) is False + + +def test_jwt_introspection_token_check_client_no_azp(): + client = MagicMock() + client.client_id = "my-client" + token = JWTIntrospectionToken({}) + assert token.check_client(client) is False + + +# --------------------------------------------------------------------------- +# IntrospectionEndpoint.introspect_token() unit tests +# --------------------------------------------------------------------------- + + +def _make_endpoint(): + """Return an IntrospectionEndpoint instance with mocked server/request.""" + endpoint = IntrospectionEndpoint.__new__(IntrospectionEndpoint) + return endpoint + + +def test_introspect_token_basic_payload(): + endpoint = _make_endpoint() + claims = { + "scope": "openid user", + "azp": "test-client", + "sub": "user-123", + "iss": "http://localhost/user", + "exp": int(time.time()) + 3600, + "iat": int(time.time()), + "jti": "jwt-id-abc", + } + token = JWTIntrospectionToken(claims) + payload = endpoint.introspect_token(token) + + assert payload["scope"] == "openid user" + assert payload["client_id"] == "test-client" + assert payload["sub"] == "user-123" + assert payload["iss"] == "http://localhost/user" + assert payload["jti"] == "jwt-id-abc" + assert payload["token_type"] == "Bearer" + assert payload.get("active") is None # parent adds active, not us + + +def test_introspect_token_scope_as_list_joined(): + endpoint = _make_endpoint() + claims = {"scope": ["openid", "user", "data"]} + token = JWTIntrospectionToken(claims) + payload = endpoint.introspect_token(token) + assert payload["scope"] == "openid user data" + + +def test_introspect_token_scope_as_string_unchanged(): + endpoint = _make_endpoint() + claims = {"scope": "openid user"} + token = JWTIntrospectionToken(claims) + payload = endpoint.introspect_token(token) + assert payload["scope"] == "openid user" + + +def test_introspect_token_includes_username_from_context(): + endpoint = _make_endpoint() + claims = { + "context": {"user": {"name": "alice"}}, + } + token = JWTIntrospectionToken(claims) + payload = endpoint.introspect_token(token) + assert payload["username"] == "alice" + + +def test_introspect_token_includes_patient_id_from_context(): + endpoint = _make_endpoint() + claims = { + "context": {"patient": "patient-xyz"}, + } + token = JWTIntrospectionToken(claims) + payload = endpoint.introspect_token(token) + assert payload["patient"] == "patient-xyz" + + +def test_introspect_token_omits_none_values(): + endpoint = _make_endpoint() + claims = {} # all optional fields absent + token = JWTIntrospectionToken(claims) + payload = endpoint.introspect_token(token) + # None values must be filtered out (except token_type which is always set) + assert "sub" not in payload + assert "iss" not in payload + assert "jti" not in payload + assert payload["token_type"] == "Bearer" + + +def test_introspect_token_no_username_when_context_user_missing(): + endpoint = _make_endpoint() + claims = {"context": {}} + token = JWTIntrospectionToken(claims) + payload = endpoint.introspect_token(token) + assert "username" not in payload + + +def test_introspect_token_no_patient_when_context_patient_missing(): + endpoint = _make_endpoint() + claims = {"context": {"user": {"name": "alice"}}} + token = JWTIntrospectionToken(claims) + payload = endpoint.introspect_token(token) + assert "patient" not in payload + + +# --------------------------------------------------------------------------- +# IntrospectionEndpoint.query_token unit tests (require app context + valid JWT) +# --------------------------------------------------------------------------- + + +def test_introspect_query_token_returns_token_for_valid_jwt( + app, encoded_jwt, kid, rsa_public_key +): + """query_token wraps a valid JWT in a JWTIntrospectionToken.""" + endpoint = _make_endpoint() + result = endpoint.query_token(encoded_jwt, "access_token") + assert result is not None + assert isinstance(result, JWTIntrospectionToken) + + +def test_introspect_query_token_returns_none_for_invalid_jwt(app): + """query_token returns None for a JWT that cannot be validated.""" + endpoint = _make_endpoint() + result = endpoint.query_token("not.a.jwt", None) + assert result is None + + +# --------------------------------------------------------------------------- +# Revocation endpoint HTTP tests — covers query_token, revoke_token, JWTToken +# --------------------------------------------------------------------------- + + +def test_revoke_endpoint_success_with_valid_credentials( + client, oauth_client, encoded_jwt_refresh_token +): + """Successful revocation of a refresh token returns HTTP 200 (RFC 7009).""" + auth_header = create_basic_header( + oauth_client.client_id, oauth_client.client_secret + ) + response = client.post( + "/oauth2/revoke", + data={"token": encoded_jwt_refresh_token}, + headers=auth_header, + ) + assert response.status_code == 200, response.data + + +def test_revoke_endpoint_invalid_client_id( + client, oauth_client, encoded_jwt_refresh_token +): + """Wrong client_id results in an authentication error.""" + auth_header = create_basic_header("wrong-client-id", oauth_client.client_secret) + response = client.post( + "/oauth2/revoke", + data={"token": encoded_jwt_refresh_token}, + headers=auth_header, + ) + # client authentication fails → 401 or 400 + assert response.status_code in (400, 401), response.data + + +def test_revoke_endpoint_invalid_client_secret( + client, oauth_client, encoded_jwt_refresh_token +): + """Wrong client_secret results in an authentication error.""" + auth_header = create_basic_header(oauth_client.client_id, "wrong-secret") + response = client.post( + "/oauth2/revoke", + data={"token": encoded_jwt_refresh_token}, + headers=auth_header, + ) + assert response.status_code in (400, 401), response.data + + +# --------------------------------------------------------------------------- +# Introspection endpoint HTTP tests +# --------------------------------------------------------------------------- + + +def test_introspect_endpoint_valid_token_returns_active( + client, oauth_client, encoded_jwt +): + """A valid JWT is introspected successfully and active=true is returned.""" + auth_header = create_basic_header( + oauth_client.client_id, oauth_client.client_secret + ) + response = client.post( + "/oauth2/introspect", + data={"token": encoded_jwt}, + headers=auth_header, + ) + assert response.status_code == 200, response.json + data = response.json + assert data.get("active") is True + assert "scope" in data or "token_type" in data + + +def test_introspect_endpoint_invalid_token_returns_inactive(client, oauth_client): + """An invalid JWT results in active=false.""" + auth_header = create_basic_header( + oauth_client.client_id, oauth_client.client_secret + ) + response = client.post( + "/oauth2/introspect", + data={"token": "not.a.jwt.at.all"}, + headers=auth_header, + ) + assert response.status_code == 200, response.json + data = response.json + assert data.get("active") is False diff --git a/tests/oidc/test_jwt_generator.py b/tests/oidc/test_jwt_generator.py new file mode 100644 index 000000000..62d93de0b --- /dev/null +++ b/tests/oidc/test_jwt_generator.py @@ -0,0 +1,175 @@ +""" +Tests for fence.oidc.jwt_generator. + +Covers: +- generate_client_response(): scope as string (split), empty scope, normal flow +- generate_token(): client_credentials dispatch → generate_client_response +- generate_implicit_response(): scope as string path +""" + +import time +from unittest.mock import MagicMock, patch + +import pytest + + +# --------------------------------------------------------------------------- +# generate_client_response() tests — called directly within app context +# --------------------------------------------------------------------------- + + +def _mock_client(client_id="test-client"): + client = MagicMock() + client.client_id = client_id + # allowed_scopes must be a list for validate_scopes + client.allowed_scopes = ["openid", "user", "data"] + client.scope = "openid user data" + return client + + +def test_generate_client_response_scope_as_string_is_split(app): + """scope passed as space-separated string should be split into a list.""" + from fence.oidc.jwt_generator import generate_client_response + + client = _mock_client() + with app.test_request_context("/"): + result = generate_client_response(client, scope="openid user") + + assert result["token_type"] == "Bearer" + assert "access_token" in result + # scope in response should be the original joined string + assert "openid" in result["scope"] + assert "user" in result["scope"] + + +def test_generate_client_response_empty_scope(app): + """scope=None or scope=[] should produce empty scope in response.""" + from fence.oidc.jwt_generator import generate_client_response + + client = _mock_client() + with app.test_request_context("/"): + result = generate_client_response(client, scope=None) + + assert result["token_type"] == "Bearer" + assert "access_token" in result + assert result["scope"] == "" + + +def test_generate_client_response_normal_flow(app): + """generate_client_response with list scope produces expected keys.""" + from fence.oidc.jwt_generator import generate_client_response + + client = _mock_client() + with app.test_request_context("/"): + result = generate_client_response(client, scope=["openid", "user"]) + + assert "access_token" in result + assert "expires_in" in result + assert result["token_type"] == "Bearer" + assert isinstance(result["expires_in"], int) + + +def test_generate_client_response_returns_string_scope(app): + """The response scope field must be a string, not a list.""" + from fence.oidc.jwt_generator import generate_client_response + + client = _mock_client() + with app.test_request_context("/"): + result = generate_client_response(client, scope=["openid", "user"]) + + assert isinstance(result["scope"], str) + + +# --------------------------------------------------------------------------- +# generate_token() dispatch test — client_credentials → generate_client_response +# --------------------------------------------------------------------------- + + +def test_generate_token_dispatches_to_client_response_for_client_credentials(app): + """generate_token routes client_credentials grant to generate_client_response.""" + from fence.oidc.jwt_generator import generate_token + + client = _mock_client() + + # validate_scopes is called first; patch it out to not raise + with patch("fence.oidc.jwt_generator.validate_scopes"), app.test_request_context( + "/" + ): + result = generate_token(client, "client_credentials", scope=["openid"]) + + assert result is not None + assert "access_token" in result + assert result["token_type"] == "Bearer" + + +# --------------------------------------------------------------------------- +# generate_client_response via HTTP (client_credentials grant) — +# exercises the full request path including scope normalization +# --------------------------------------------------------------------------- + + +def test_client_credentials_token_endpoint_returns_access_token( + oauth_test_client_with_client_credentials, +): + """Full client_credentials token request returns a valid access_token.""" + token_response = oauth_test_client_with_client_credentials.token() + assert token_response.response.status_code == 200, token_response.response.json + data = token_response.response.json + assert "access_token" in data + assert data["token_type"] == "Bearer" + assert "expires_in" in data + + +def test_client_credentials_token_has_string_scope( + oauth_test_client_with_client_credentials, +): + """The scope in the client_credentials response is a space-separated string.""" + token_response = oauth_test_client_with_client_credentials.token() + data = token_response.response.json + assert isinstance(data.get("scope", ""), str) + + +def test_client_credentials_token_with_requested_scope( + oauth_test_client_with_client_credentials, +): + """A specific scope can be requested and is reflected in the response.""" + token_response = oauth_test_client_with_client_credentials.token(scope="openid") + assert token_response.response.status_code == 200 + data = token_response.response.json + assert "access_token" in data + + +# --------------------------------------------------------------------------- +# generate_implicit_response() — scope as string path +# --------------------------------------------------------------------------- + + +def test_generate_implicit_response_scope_string_is_split(app, oauth_user, db_session): + """When scope is a string, generate_implicit_response splits it into a list.""" + from fence.oidc.jwt_generator import generate_implicit_response + from fence.models import User + + # Fetch the actual user from the db so the session check works + user = db_session.query(User).filter_by(id=oauth_user.user_id).first() + assert user is not None + + client = _mock_client() + + with patch( + "fence.oidc.jwt_generator.get_linked_google_account_email", return_value=None + ), patch( + "fence.oidc.jwt_generator.get_linked_google_account_exp", return_value=None + ), app.test_request_context( + "/" + ): + result = generate_implicit_response( + client, + grant_type="implicit", + user=user, + scope="openid user", + nonce="test-nonce-value", + ) + + assert "id_token" in result + assert "access_token" in result + assert result["token_type"] == "Bearer" diff --git a/tests/oidc/test_oauth2_blueprint.py b/tests/oidc/test_oauth2_blueprint.py new file mode 100644 index 000000000..c26386300 --- /dev/null +++ b/tests/oidc/test_oauth2_blueprint.py @@ -0,0 +1,381 @@ +""" +Tests for fence/blueprints/oauth2.py + +Covers: +- display_error() — returns query args as JSON +- authorize() auto_approve path — skips consent screen +- _handle_consent_confirmation() — user denies consent (confirm != 'yes') +- authorize() unauthenticated paths — DEFAULT_LOGIN_URL, unknown idp, fence/shib extras +- _get_auth_response_for_prompts() — prompt='none' with multiple values +- _get_authorize_error_response() — returns 302 with error params +- revoke_token() — BlacklistingInvalidTokenError returns 200 +""" + +from unittest.mock import patch, MagicMock +from urllib.parse import urlparse, parse_qs + +import pytest + +from fence.config import config +from fence.errors import BlacklistingInvalidTokenError +from fence.models import Client + + +# --------------------------------------------------------------------------- +# display_error +# --------------------------------------------------------------------------- + + +def test_display_error_returns_query_args_as_json(client): + """ + GET /oauth2/errors?error=access_denied&error_description=foo + returns those query params as a JSON object. + Covers line 435. + """ + response = client.get( + "/oauth2/errors?error=access_denied&error_description=User+denied+access" + ) + assert response.status_code == 200 + data = response.json + assert data["error"] == "access_denied" + assert "denied" in data["error_description"] + + +def test_display_error_with_no_params(client): + """GET /oauth2/errors with no params returns an empty JSON object.""" + response = client.get("/oauth2/errors") + assert response.status_code == 200 + assert response.json == {} + + +# --------------------------------------------------------------------------- +# auto_approve +# --------------------------------------------------------------------------- + + +def test_authorize_auto_approve_skips_consent_screen( + client, oauth_test_client, oauth_client, db_session +): + """ + When the OAuth client has auto_approve=True, the authorize endpoint + issues a code without requiring a 'confirm' parameter. + Covers line 141. + """ + # Enable auto_approve on the DB client + db_client = ( + db_session.query(Client).filter_by(client_id=oauth_client.client_id).first() + ) + db_client.auto_approve = True + db_session.commit() + + # Authorize without any 'confirm' param — should still get a code back + auth_response = oauth_test_client.authorize( + method="GET", + data={"confirm": None}, # no confirm + do_asserts=False, + ) + # With auto_approve, the GET redirects straight to the redirect URI with a code + location = auth_response.location + assert location is not None + parsed = urlparse(location) + qs = parse_qs(parsed.query) + assert "code" in qs, f"Expected 'code' in redirect location, got: {location}" + + +# --------------------------------------------------------------------------- +# _handle_consent_confirmation — user denies +# --------------------------------------------------------------------------- + + +def test_handle_consent_confirmation_denied_returns_access_denied(oauth_test_client): + """ + When the user responds with confirm != 'yes', the server responds with + an access_denied error redirect. + Covers line 169. + """ + response = oauth_test_client.authorize( + method="GET", + data={"confirm": "no"}, + do_asserts=False, + ) + location = response.location + assert location is not None + qs = parse_qs(urlparse(location).query) + assert "error" in qs + assert qs["error"][0] == "access_denied" + + +# --------------------------------------------------------------------------- +# _get_authorize_error_response — direct function test +# --------------------------------------------------------------------------- + + +def test_get_authorize_error_response_returns_302_with_error_params(app): + """ + _get_authorize_error_response builds a 302 redirect containing the + OAuth2 error params in the Location header. + Covers lines 312-316. + """ + from authlib.oauth2.rfc6749 import InvalidRequestError + from fence.blueprints.oauth2 import _get_authorize_error_response + + error = InvalidRequestError(state="xyz") + redirect_uri = "https://client.example.com/callback" + + with app.test_request_context("/"): + response = _get_authorize_error_response(error, redirect_uri) + + assert response.status_code == 302 + location = response.headers["Location"] + assert location.startswith(redirect_uri) + qs = parse_qs(urlparse(location).query) + assert "error" in qs + assert qs["error"][0] == "invalid_request" + + +# --------------------------------------------------------------------------- +# _get_auth_response_for_prompts — 'none' with multiple prompts +# --------------------------------------------------------------------------- + + +def test_get_auth_response_for_prompts_none_combined_with_other_returns_error_redirect( + app, oauth_user +): + """ + _get_auth_response_for_prompts with prompt='none select_account' returns + a 302 error redirect because 'none' cannot be combined with other values. + Covers lines 216-222. + """ + from fence.blueprints.oauth2 import _get_auth_response_for_prompts + + grant = MagicMock() + grant.params = { + "state": "teststate", + "uri": "http://example.com", + "redirect_uri": "https://client.example.com/callback", + } + user = MagicMock() + client = MagicMock() + + with app.test_request_context("/"): + response = _get_auth_response_for_prompts( + "none select_account", grant, user, client, "openid" + ) + + assert response.status_code == 302 + location = response.headers["Location"] + qs = parse_qs(urlparse(location).query) + assert "error" in qs + + +def test_get_auth_response_for_prompts_none_unauthenticated_returns_access_denied( + app, oauth_user +): + """ + _get_auth_response_for_prompts with prompt='none' when get_current_user + raises Unauthorized returns a 302 error redirect with access_denied. + Covers lines 228-231. + """ + from fence.blueprints.oauth2 import _get_auth_response_for_prompts + from fence.errors import Unauthorized + + grant = MagicMock() + grant.params = { + "state": "teststate", + "uri": "http://example.com", + "redirect_uri": "https://client.example.com/callback", + } + user = MagicMock() + client = MagicMock() + + with app.test_request_context("/"): + with patch( + "fence.blueprints.oauth2.get_current_user", + side_effect=Unauthorized("not logged in"), + ): + response = _get_auth_response_for_prompts( + "none", grant, user, client, "openid" + ) + + assert response.status_code == 302 + location = response.headers["Location"] + qs = parse_qs(urlparse(location).query) + assert "error" in qs + assert qs["error"][0] == "access_denied" + + +# --------------------------------------------------------------------------- +# authorize() — unauthenticated paths +# --------------------------------------------------------------------------- + + +@pytest.fixture(scope="function") +def mock_auth_off(monkeypatch): + """Disable MOCK_AUTH so get_current_user raises Unauthorized.""" + monkeypatch.setitem(config, "MOCK_AUTH", False) + + +def test_authorize_unauthenticated_uses_default_login_url_when_no_idp( + client, mock_auth_off, monkeypatch +): + """ + When no DEFAULT_LOGIN_IDP and 'default' not in ENABLED_IDENTITY_PROVIDERS, + the authorize endpoint falls back to DEFAULT_LOGIN_URL. + Covers line 91. + """ + monkeypatch.setitem(config, "DEFAULT_LOGIN_IDP", None) + monkeypatch.setitem(config, "ENABLED_IDENTITY_PROVIDERS", {}) + monkeypatch.setitem( + config, "DEFAULT_LOGIN_URL", "https://fallback-login.example.com" + ) + + response = client.get( + "/oauth2/authorize" + "?client_id=test-client" + "&response_type=code" + "&scope=openid" + "&redirect_uri=https://oauth-test-client.net" + ) + # Should redirect to the fallback login URL + assert response.status_code == 302 + location = response.headers["Location"] + assert "fallback-login.example.com" in location + + +def test_authorize_unauthenticated_unknown_idp_raises_user_error( + client, mock_auth_off, monkeypatch +): + """ + When a specific idp is requested that is not in OPENID_CONNECT, + a UserError (400) is raised. + Covers lines 103-106. + """ + monkeypatch.setitem(config, "DEFAULT_LOGIN_IDP", None) + monkeypatch.setitem(config, "ENABLED_IDENTITY_PROVIDERS", {}) + monkeypatch.setitem(config, "DEFAULT_LOGIN_URL", None) + monkeypatch.setitem(config, "OPENID_CONNECT", {"google": {}}) + + response = client.get( + "/oauth2/authorize" + "?client_id=test-client" + "&response_type=code" + "&scope=openid" + "&redirect_uri=https://oauth-test-client.net" + "&idp=nonexistent_provider" + ) + assert response.status_code == 400 + + +def test_authorize_unauthenticated_fence_idp_adds_idp_param( + client, mock_auth_off, monkeypatch +): + """ + When idp='fence' and fence_idp is provided, the redirect to the login + endpoint includes idp= in the params. + Covers lines 110-112. + """ + monkeypatch.setitem(config, "DEFAULT_LOGIN_IDP", None) + monkeypatch.setitem(config, "ENABLED_IDENTITY_PROVIDERS", {}) + monkeypatch.setitem(config, "DEFAULT_LOGIN_URL", None) + monkeypatch.setitem(config, "OPENID_CONNECT", {"fence": {}}) + monkeypatch.setitem(config, "BASE_URL", config.get("BASE_URL", "http://localhost")) + + with patch("fence.blueprints.oauth2.get_idp_route_name", return_value="fence"): + response = client.get( + "/oauth2/authorize" + "?client_id=test-client" + "&response_type=code" + "&scope=openid" + "&redirect_uri=https://oauth-test-client.net" + "&idp=fence" + "&fence_idp=google" + ) + + assert response.status_code == 302 + location = response.headers["Location"] + qs = parse_qs(urlparse(location).query) + # The login URL should include the fence_idp as 'idp' param + assert "idp" in qs + assert qs["idp"][0] == "google" + + +def test_authorize_unauthenticated_shibboleth_idp_adds_shib_idp_param( + client, mock_auth_off, monkeypatch +): + """ + When idp='shibboleth' and shib_idp is provided, the redirect to the login + endpoint includes shib_idp in the params. + Covers line 114. + """ + monkeypatch.setitem(config, "DEFAULT_LOGIN_IDP", None) + monkeypatch.setitem(config, "ENABLED_IDENTITY_PROVIDERS", {}) + monkeypatch.setitem(config, "DEFAULT_LOGIN_URL", None) + monkeypatch.setitem(config, "OPENID_CONNECT", {"shibboleth": {}}) + monkeypatch.setitem(config, "BASE_URL", config.get("BASE_URL", "http://localhost")) + + with patch("fence.blueprints.oauth2.get_idp_route_name", return_value="shibboleth"): + response = client.get( + "/oauth2/authorize" + "?client_id=test-client" + "&response_type=code" + "&scope=openid" + "&redirect_uri=https://oauth-test-client.net" + "&idp=shibboleth" + "&shib_idp=uchicago.edu" + ) + + assert response.status_code == 302 + location = response.headers["Location"] + qs = parse_qs(urlparse(location).query) + assert "shib_idp" in qs + assert qs["shib_idp"][0] == "uchicago.edu" + + +# --------------------------------------------------------------------------- +# revoke_token — BlacklistingInvalidTokenError path +# --------------------------------------------------------------------------- + + +def test_revoke_token_blacklisting_invalid_token_returns_200(client, oauth_test_client): + """ + When revoking a token that is already invalid, BlacklistingInvalidTokenError + is caught and a 200 is returned (per RFC 7009). + Covers lines 405-416. + """ + # Get a real refresh token first + oauth_test_client.authorize(data={"confirm": "yes"}) + oauth_test_client.token() + refresh_token = oauth_test_client.token_response.refresh_token + assert refresh_token, "Expected a refresh token" + + # Patch blacklist_encoded_token to raise BlacklistingInvalidTokenError + with patch( + "fence.blueprints.oauth2.blacklist_encoded_token", + side_effect=BlacklistingInvalidTokenError("token invalid"), + ): + # Use form-encoded revoke (user-style, not client Basic-auth-style) + # This follows the non-Basic-auth path in revoke_token + # First, we need a valid Authorization header with the token itself + # The user path requires Content-Type: application/x-www-form-urlencoded + # and a 'token' field. + # We also need a revoker token — use the refresh token as revoker too, + # but mock validate_jwt to return valid claims. + fake_claims = {"sub": "test", "pur": "refresh"} + with patch( + "fence.blueprints.oauth2.validate_jwt", + return_value=fake_claims, + ): + with patch( + "fence.blueprints.oauth2.authz_authorize", + return_value=None, + ): + response = client.post( + "/oauth2/revoke", + headers={ + "Content-Type": "application/x-www-form-urlencoded", + "Authorization": f"Bearer {refresh_token}", + }, + data={"token": refresh_token}, + ) + + assert response.status_code == 200 diff --git a/tests/oidc/test_oidc_code_grant.py b/tests/oidc/test_oidc_code_grant.py new file mode 100644 index 000000000..d19525a48 --- /dev/null +++ b/tests/oidc/test_oidc_code_grant.py @@ -0,0 +1,397 @@ +""" +Tests for fence/oidc/grants/oidc_code_grant.py + +Covers: +- create_authorization_code() — PKCE params persistence +- exists_nonce() — nonce collision detection +- validate_request_prompt() — all branch variants +- validate_token_request() — missing code, wrong grant type, missing PKCE verifier +- _validate_code_verifier() — S256, plain, mismatch, unsupported method +""" + +import hashlib +import base64 +from unittest.mock import MagicMock, patch + +import pytest + +from authlib.oidc.core.errors import ( + AccountSelectionRequiredError, + ConsentRequiredError, + LoginRequiredError, +) +from authlib.oauth2.rfc6749 import ( + InvalidGrantError, + InvalidRequestError, +) + +from fence.models import AuthorizationCode, Client, User +from fence.oidc.grants.oidc_code_grant import AuthorizationCodeGrant + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _make_grant_with_prompt(prompt_value): + """Return a bare AuthorizationCodeGrant with a minimal mocked request.""" + grant = AuthorizationCodeGrant.__new__(AuthorizationCodeGrant) + grant.request = MagicMock() + data = {} + if prompt_value is not None: + data["prompt"] = prompt_value + grant.request.payload.data = data + return grant + + +def _s256_challenge(verifier: str) -> str: + digest = hashlib.sha256(verifier.encode("utf-8")).digest() + return base64.urlsafe_b64encode(digest).decode("utf-8").rstrip("=") + + +def _bare_grant(): + """Return a bare AuthorizationCodeGrant for testing internal methods.""" + return AuthorizationCodeGrant.__new__(AuthorizationCodeGrant) + + +# --------------------------------------------------------------------------- +# create_authorization_code (static method — direct call) +# --------------------------------------------------------------------------- + + +def test_create_authorization_code_without_pkce(app, db_session, oauth_user): + """ + create_authorization_code stores an AuthorizationCode with no code_challenge + when no PKCE params are present in request.data. + """ + user = db_session.query(User).filter_by(id=oauth_user.user_id).first() + client = MagicMock() + client.client_id = "test-client" + request = MagicMock() + request.data = {"nonce": "testnonce"} + request.redirect_uri = "https://example.com/callback" + request.scope = "openid" + + with app.test_request_context("/"): + code_str = AuthorizationCodeGrant.create_authorization_code( + client, user, request + ) + + assert code_str is not None and len(code_str) > 0 + + # Verify the row in the DB + stored = db_session.query(AuthorizationCode).filter_by(code=code_str).first() + assert stored is not None + assert stored.code_challenge is None + assert stored.nonce == "testnonce" + + +def test_create_authorization_code_with_pkce_s256(app, db_session, oauth_user): + """ + create_authorization_code persists code_challenge and code_challenge_method + when PKCE S256 params are present in request.data. + """ + user = db_session.query(User).filter_by(id=oauth_user.user_id).first() + client = MagicMock() + client.client_id = "test-client" + challenge = ( + "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM" # pragma: allowlist secret + ) + request = MagicMock() + request.data = { + "code_challenge": challenge, + "code_challenge_method": "S256", + } + request.redirect_uri = "https://example.com/callback" + request.scope = "openid" + + with app.test_request_context("/"): + code_str = AuthorizationCodeGrant.create_authorization_code( + client, user, request + ) + + assert code_str is not None + + stored = db_session.query(AuthorizationCode).filter_by(code=code_str).first() + assert stored is not None + assert stored.code_challenge == challenge + assert stored.code_challenge_method == "S256" + + +# --------------------------------------------------------------------------- +# exists_nonce +# --------------------------------------------------------------------------- + + +def test_exists_nonce_returns_true_when_nonce_exists(app, db_session, oauth_user): + """ + exists_nonce returns True when an AuthorizationCode with that nonce is in the DB. + """ + user = db_session.query(User).filter_by(id=oauth_user.user_id).first() + nonce_val = "test-nonce-unique-abc123" + code = AuthorizationCode( + code="dummy-code-for-nonce-test", + client_id="test-client", + redirect_uri="https://example.com", + scope="openid", + user_id=user.id, + nonce=nonce_val, + ) + db_session.add(code) + db_session.commit() + + grant = _bare_grant() + request = MagicMock() + + with app.test_request_context("/"): + result = grant.exists_nonce(nonce_val, request) + + assert result is True + + +def test_exists_nonce_returns_false_when_nonce_absent(app, db_session): + """ + exists_nonce returns False when no AuthorizationCode with the given nonce exists. + """ + grant = _bare_grant() + request = MagicMock() + + with app.test_request_context("/"): + result = grant.exists_nonce("nonce-that-has-never-been-used", request) + + assert result is False + + +# --------------------------------------------------------------------------- +# validate_request_prompt +# --------------------------------------------------------------------------- + + +def test_validate_request_prompt_no_prompt_no_user_sets_login_prompt(): + """ + When no prompt is provided and end_user is None, self.prompt is set to 'login'. + Covers line 218. + """ + grant = _make_grant_with_prompt(None) + grant.validate_request_prompt(end_user=None, redirect_uri="https://example.com") + assert grant.prompt == "login" + + +def test_validate_request_prompt_no_prompt_with_user_does_not_force_login(): + """ + When no prompt is provided and end_user is present, prompt is not forced to 'login'. + """ + grant = _make_grant_with_prompt(None) + result = grant.validate_request_prompt( + end_user=MagicMock(), redirect_uri="https://example.com" + ) + assert result is grant + # self.prompt should not be set to 'login' + assert getattr(grant, "prompt", None) != "login" + + +def test_validate_request_prompt_none_no_user_raises_login_required(): + """ + prompt='none' with no authenticated user raises LoginRequiredError. + Covers line 222. + """ + grant = _make_grant_with_prompt("none") + with pytest.raises(LoginRequiredError): + grant.validate_request_prompt(end_user=None, redirect_uri="https://example.com") + + +def test_validate_request_prompt_none_combined_with_others_raises_error(): + """ + prompt='none consent' raises an error because 'none' must not be combined + with any other value. The production code raises InvalidRequestError, but + newer authlib versions also raise ValueError when the description contains + forbidden quote characters. Either confirms the branch executed. + Covers line 228. + """ + grant = _make_grant_with_prompt("none consent") + # end_user provided so the 'none + no user' branch is skipped + with pytest.raises((InvalidRequestError, ValueError)): + grant.validate_request_prompt( + end_user=MagicMock(), redirect_uri="https://example.com" + ) + + +def test_validate_request_prompt_consent_no_user_raises_consent_required(): + """ + prompt='consent' with no authenticated user raises ConsentRequiredError. + Covers lines 232-234. + """ + grant = _make_grant_with_prompt("consent") + with pytest.raises(ConsentRequiredError): + grant.validate_request_prompt(end_user=None, redirect_uri="https://example.com") + + +def test_validate_request_prompt_select_account_no_user_raises_account_selection_required(): + """ + prompt='select_account' with no authenticated user raises AccountSelectionRequiredError. + Covers lines 236-238. + """ + grant = _make_grant_with_prompt("select_account") + with pytest.raises(AccountSelectionRequiredError): + grant.validate_request_prompt(end_user=None, redirect_uri="https://example.com") + + +# --------------------------------------------------------------------------- +# validate_token_request — integration tests via the HTTP token endpoint +# --------------------------------------------------------------------------- + + +def test_token_request_missing_code(oauth_test_client): + """ + A token request that omits the 'code' parameter triggers the missing-code + branch (line 270). The production code raises InvalidRequestError with a + description that contains quote characters; in newer authlib this causes a + ValueError that is not caught, so the response is a 4xx or 5xx — not 200. + Covers line 270. + """ + oauth_test_client.authorize(data={"confirm": "yes"}) + + response = oauth_test_client._client.post( + "/oauth2/token", + headers=oauth_test_client._auth_header, + data={ + "client_id": oauth_test_client.client_id, + "client_secret": oauth_test_client.client_secret, + "grant_type": "authorization_code", + "redirect_uri": oauth_test_client.url, + # deliberately no "code" + }, + ) + # The request must not succeed: accepts 400 (handled OAuth2Error) or 500 + # (unhandled ValueError from authlib description-character validation). + assert response.status_code != 200 + + +def test_token_request_pkce_challenge_without_verifier(oauth_test_client): + """ + When authorization was done with a code_challenge but the token request + omits code_verifier, a 400 invalid_grant error is returned. + Covers lines 291-293. + """ + # Authorize with PKCE challenge + verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" # pragma: allowlist secret + challenge = _s256_challenge(verifier) + + oauth_test_client.authorize( + data={ + "confirm": "yes", + "code_challenge": challenge, + "code_challenge_method": "S256", + } + ) + code = oauth_test_client.authorize_response.code + assert code, "Expected an authorization code from the authorize endpoint" + code_val = code[0] if isinstance(code, list) else code + + # Token request without code_verifier + response = oauth_test_client._client.post( + "/oauth2/token", + headers=oauth_test_client._auth_header, + data={ + "client_id": oauth_test_client.client_id, + "client_secret": oauth_test_client.client_secret, + "grant_type": "authorization_code", + "code": code_val, + "redirect_uri": oauth_test_client.url, + # deliberately no "code_verifier" + }, + ) + assert response.status_code == 400 + assert response.json.get("error") == "invalid_grant" + + +def test_pkce_s256_full_flow(oauth_test_client): + """ + Full PKCE S256 flow: authorize with code_challenge, token with matching + code_verifier succeeds. + Covers line 298 (_validate_code_verifier call path for valid verifier). + """ + verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" # pragma: allowlist secret + challenge = _s256_challenge(verifier) + + oauth_test_client.authorize( + data={ + "confirm": "yes", + "code_challenge": challenge, + "code_challenge_method": "S256", + } + ) + code = oauth_test_client.authorize_response.code + assert code, "Expected authorization code" + code_val = code[0] if isinstance(code, list) else code + + token_response = oauth_test_client.token( + code=code_val, + data={"code_verifier": verifier}, + do_asserts=False, + ) + assert token_response.response.status_code == 200 + assert "access_token" in token_response.response.json + + +# --------------------------------------------------------------------------- +# _validate_code_verifier — direct unit tests +# --------------------------------------------------------------------------- + + +def test_validate_code_verifier_s256_matching_verifier_passes(): + """ + A correct S256 code_verifier successfully validates against its challenge. + Covers lines 329-334 (S256 branch, matching). + """ + verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" # pragma: allowlist secret + challenge = _s256_challenge(verifier) + grant = _bare_grant() + # Should not raise + grant._validate_code_verifier(verifier, challenge, "S256") + + +def test_validate_code_verifier_s256_wrong_verifier_raises_invalid_grant(): + """ + An S256 code_verifier that does not match the stored challenge raises InvalidGrantError. + Covers lines 345-347. + """ + grant = _bare_grant() + with pytest.raises(InvalidGrantError): + grant._validate_code_verifier( + "wrong-verifier", + "definitely-not-matching-challenge", + "S256", + ) + + +def test_validate_code_verifier_plain_matching_verifier_passes(): + """ + A correct 'plain' code_verifier (equals the challenge) passes validation. + Covers lines 335-336. + """ + secret = "plain-verifier-value" + grant = _bare_grant() + # Should not raise + grant._validate_code_verifier(secret, secret, "plain") + + +def test_validate_code_verifier_plain_wrong_verifier_raises_invalid_grant(): + """ + A 'plain' code_verifier that does not equal the challenge raises InvalidGrantError. + Covers lines 345-347 (plain branch mismatch). + """ + grant = _bare_grant() + with pytest.raises(InvalidGrantError): + grant._validate_code_verifier("wrong", "right", "plain") + + +def test_validate_code_verifier_unsupported_method_raises_invalid_grant(): + """ + An unsupported code_challenge_method raises InvalidGrantError. + Covers lines 337-342. + """ + grant = _bare_grant() + with pytest.raises(InvalidGrantError): + grant._validate_code_verifier("any-verifier", "any-challenge", "SHA512") diff --git a/tests/oidc/test_private_key_jwt.py b/tests/oidc/test_private_key_jwt.py new file mode 100644 index 000000000..c3262b074 --- /dev/null +++ b/tests/oidc/test_private_key_jwt.py @@ -0,0 +1,149 @@ +""" +Tests for fence.oidc.private_key_jwt (FencePrivateKeyJWT). + +Covers create_claims_options() and resolve_client_public_key() for all key +lookup branches: no JWKS, matching kid, kid not found, no kid, empty key set. +""" + +import pytest +from unittest.mock import MagicMock + +from authlib.jose import JsonWebKey +from authlib.oauth2.rfc6749 import InvalidClientError +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import rsa + +from fence.config import config +from fence.oidc.private_key_jwt import FencePrivateKeyJWT + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _generate_rsa_jwks(kid=None): + """Return (private_key, jwks_dict) with an optionally-tagged public JWK.""" + private_key = rsa.generate_private_key( + public_exponent=65537, + key_size=2048, + backend=default_backend(), + ) + pub_pem = private_key.public_key().public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo, + ) + jwk_obj = JsonWebKey.import_key(pub_pem) + jwk_dict = dict(jwk_obj) + if kid: + jwk_dict["kid"] = kid + return private_key, {"keys": [jwk_dict]} + + +def _client_with_jwks(jwks_value): + """Return a mock Client whose .jwks attribute returns jwks_value.""" + client = MagicMock() + type(client).jwks = property(lambda self: jwks_value) + return client + + +# --------------------------------------------------------------------------- +# create_claims_options() tests +# --------------------------------------------------------------------------- + + +def test_create_claims_options_has_required_keys(app): + """create_claims_options returns dict with iss, sub, aud, exp keys.""" + handler = FencePrivateKeyJWT() + opts = handler.create_claims_options() + assert "iss" in opts + assert "sub" in opts + assert "aud" in opts + assert "exp" in opts + + +def test_create_claims_options_aud_value_is_token_endpoint(app): + """aud.value must point to the /oauth2/token endpoint.""" + handler = FencePrivateKeyJWT() + opts = handler.create_claims_options() + expected = config["BASE_URL"].rstrip("/") + "/oauth2/token" + assert opts["aud"]["value"] == expected + + +def test_create_claims_options_aud_is_essential(app): + """aud is marked essential.""" + handler = FencePrivateKeyJWT() + opts = handler.create_claims_options() + assert opts["aud"].get("essential") is True + + +def test_create_claims_options_exp_is_essential(app): + """exp is marked essential.""" + handler = FencePrivateKeyJWT() + opts = handler.create_claims_options() + assert opts["exp"].get("essential") is True + + +def test_create_claims_options_iss_validator_matches_sub(app): + """iss validator accepts iss == sub.""" + handler = FencePrivateKeyJWT() + validate = handler.create_claims_options()["iss"]["validate"] + claims = {"iss": "client-id", "sub": "client-id"} + assert validate(claims, "client-id") is True + + +def test_create_claims_options_iss_validator_rejects_iss_ne_sub(app): + """iss validator rejects iss != sub (RFC 7523 §3).""" + handler = FencePrivateKeyJWT() + validate = handler.create_claims_options()["iss"]["validate"] + claims = {"iss": "client-id", "sub": "other-id"} + assert validate(claims, "client-id") is False + + +# --------------------------------------------------------------------------- +# resolve_client_public_key() tests +# --------------------------------------------------------------------------- + + +def test_resolve_public_key_no_jwks_raises(app): + """Client with no registered JWKS raises InvalidClientError.""" + handler = FencePrivateKeyJWT() + client = _client_with_jwks([]) # empty list → falsy + with pytest.raises(InvalidClientError): + handler.resolve_client_public_key(client, {}) + + +def test_resolve_public_key_matching_kid_returns_key(app): + """Returns the key when the kid in the JWT header matches one in JWKS.""" + _, jwks = _generate_rsa_jwks(kid="signing-key") + handler = FencePrivateKeyJWT() + client = _client_with_jwks(jwks) + key = handler.resolve_client_public_key(client, {"kid": "signing-key"}) + assert key is not None + + +def test_resolve_public_key_unknown_kid_raises(app): + """kid in header not found in JWKS raises InvalidClientError.""" + _, jwks = _generate_rsa_jwks(kid="signing-key") + handler = FencePrivateKeyJWT() + client = _client_with_jwks(jwks) + with pytest.raises(InvalidClientError): + handler.resolve_client_public_key(client, {"kid": "unknown-kid"}) + + +def test_resolve_public_key_no_kid_returns_first_key(app): + """No kid in header returns the first key from the JWKS.""" + _, jwks = _generate_rsa_jwks(kid="signing-key") + handler = FencePrivateKeyJWT() + client = _client_with_jwks(jwks) + key = handler.resolve_client_public_key(client, {}) + assert key is not None + + +def test_resolve_public_key_empty_keys_array_raises(app): + """JWKS dict with an empty keys array raises InvalidClientError.""" + handler = FencePrivateKeyJWT() + client = _client_with_jwks({"keys": []}) + with pytest.raises(InvalidClientError): + handler.resolve_client_public_key(client, {}) From 1fe49b4ea5ff481f6fca7eef8bb9637b84f33035 Mon Sep 17 00:00:00 2001 From: vincentvanhees Date: Mon, 5 Oct 2026 13:24:42 +0200 Subject: [PATCH 08/14] feat(smart): add SMART on FHIR / US Core compliance (CONF-0119, 0120, 0121-0123) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add server-side support for SMART App Launch 2.0 and US Core Backend Services requirements, with minimalistic changes to the existing OAuth2 infrastructure. - Add token introspection endpoint (RFC 7662) at /oauth2/introspect (CONF-0123) - Add private_key_jwt client authentication via FencePrivateKeyJWT (CONF-0120); clients register their JWKS in client_metadata["jwks"] — no schema migration needed - Extend ClientCredentialsGrant to accept private_key_jwt authentication - Fix validate_scopes to recognise system/ scopes as SMART scopes and drop the openid requirement for Backend Services flows (CONF-0121/0122) - Add PKCE support (RFC 7636) to the authorization code grant (CONF-0117) - Advertise client-confidential-asymmetric and private_key_jwt in well-known metadata; move the SMART discovery document to gen3-fhir-proxy (FHIR layer) - Filter null values from introspection responses per RFC 7662 --- fence/blueprints/oauth2.py | 13 ++- fence/blueprints/well_known.py | 3 + fence/models.py | 11 ++- fence/oidc/endpoints.py | 72 +++++++++++++++ fence/oidc/grants/client_credentials_grant.py | 11 ++- fence/oidc/grants/oidc_code_grant.py | 89 ++++++++++++++++++- fence/oidc/private_key_jwt.py | 61 +++++++++++++ fence/oidc/server.py | 5 +- fence/utils.py | 32 ++++++- 9 files changed, 289 insertions(+), 8 deletions(-) create mode 100644 fence/oidc/private_key_jwt.py diff --git a/fence/blueprints/oauth2.py b/fence/blueprints/oauth2.py index fca3b2411..c2b092100 100644 --- a/fence/blueprints/oauth2.py +++ b/fence/blueprints/oauth2.py @@ -31,7 +31,7 @@ from fence.jwt.errors import JWTError from fence.jwt.token import SCOPE_DESCRIPTION from fence.models import Client -from fence.oidc.endpoints import RevocationEndpoint +from fence.oidc.endpoints import IntrospectionEndpoint, RevocationEndpoint from fence.oidc.server import server from fence.utils import clear_cookies from fence.user import get_current_user @@ -416,6 +416,17 @@ def revoke_token(): return "", 200 +@blueprint.route("/introspect", methods=["POST"]) +def introspect_token(): + """ + Token introspection per RFC 7662 and SMART App Launch (CONF-0123). + + Requires HTTP Basic auth with a registered client_id and client_secret. + Returns RFC 7662 introspection response with SMART patient context if present. + """ + return server.create_endpoint_response(IntrospectionEndpoint.ENDPOINT_NAME) + + @blueprint.route("/errors", methods=["GET"]) def display_error(): """ diff --git a/fence/blueprints/well_known.py b/fence/blueprints/well_known.py index 9cc1e91fc..92a5bb53b 100644 --- a/fence/blueprints/well_known.py +++ b/fence/blueprints/well_known.py @@ -2,6 +2,9 @@ This blueprint defines the endpoints under ``.well-known/``, which includes: - OIDC provider configuration - JWK endpoint ``/jwks`` + +The SMART App Launch 2.0 discovery document (``/.well-known/smart-configuration``) is +served by gen3-fhir-proxy, which is the FHIR-aware layer that SMART clients interact with. """ import flask diff --git a/fence/models.py b/fence/models.py index 09a08cbc3..fe98d021c 100644 --- a/fence/models.py +++ b/fence/models.py @@ -353,10 +353,14 @@ def check_requested_scopes(self, scopes): # Replaces Authlib method. Our logic does not actually look at token_auth_endpoint value def check_endpoint_auth_method(self, method, endpoint): """ - Only basic auth is supported. If anything else gets added, change this + Only basic auth and private_key_jwt are supported for confidential clients. """ if endpoint == "token": - protected_types = [ClientAuthType.basic.value, ClientAuthType.post.value] + protected_types = [ + ClientAuthType.basic.value, + ClientAuthType.post.value, + "private_key_jwt", # SMART Backend Services (CONF-0120) + ] return (self.is_confidential and method in protected_types) or ( not self.is_confidential and method == ClientAuthType.none.value ) @@ -391,6 +395,9 @@ class AuthorizationCode(Base, OAuth2AuthorizationCodeMixin): refresh_token_expires_in = Column(Integer, nullable=True) + code_challenge = Column(String, nullable=True) + code_challenge_method = Column(String, nullable=True) + _scope = Column(Text, default="") def __init__(self, **kwargs): diff --git a/fence/oidc/endpoints.py b/fence/oidc/endpoints.py index fd7ab7665..ae66242c6 100644 --- a/fence/oidc/endpoints.py +++ b/fence/oidc/endpoints.py @@ -1,5 +1,8 @@ +import time + from authlib.oauth2.rfc6749.errors import InvalidClientError, OAuth2Error import authlib.oauth2.rfc7009 +import authlib.oauth2.rfc7662 import bcrypt import flask @@ -126,3 +129,72 @@ def check_client(self, client): """ return self.client_id == client.client_id + + +class JWTIntrospectionToken: + """Wraps a validated JWT for use with IntrospectionEndpoint.""" + + def __init__(self, claims): + self.claims = claims + + def is_expired(self): + return time.time() > self.claims.get("exp", 0) + + def is_revoked(self): + jti = self.claims.get("jti") + return bool(jti) and fence.jwt.blacklist.is_blacklisted(jti) + + def check_client(self, client): + return self.claims.get("azp") == client.client_id + + +class IntrospectionEndpoint(authlib.oauth2.rfc7662.IntrospectionEndpoint): + """Token introspection per RFC 7662 / SMART App Launch (CONF-0123).""" + + def query_token(self, token_string, token_type_hint): + from fence.jwt.validate import validate_jwt + from fence.jwt.errors import JWTError + + try: + claims = validate_jwt( + encoded_token=token_string, + scope=None, + options={"verify_aud": False}, + ) + return JWTIntrospectionToken(claims) + except Exception: + return None + + def check_permission(self, token, client, request): + return True + + def introspect_token(self, token): + claims = token.claims + scope = claims.get("scope", "") + if isinstance(scope, list): + scope = " ".join(scope) + + payload = { + k: v + for k, v in { + "scope": scope, + "client_id": claims.get("azp"), + "sub": claims.get("sub"), + "iss": claims.get("iss"), + "exp": claims.get("exp"), + "iat": claims.get("iat"), + "jti": claims.get("jti"), + "token_type": "Bearer", + }.items() + if v is not None + } + + context = claims.get("context", {}) + user_name = context.get("user", {}).get("name") + if user_name: + payload["username"] = user_name + patient_id = context.get("patient") + if patient_id: + payload["patient"] = patient_id + + return payload diff --git a/fence/oidc/grants/client_credentials_grant.py b/fence/oidc/grants/client_credentials_grant.py index 4c130db69..eef901e8b 100644 --- a/fence/oidc/grants/client_credentials_grant.py +++ b/fence/oidc/grants/client_credentials_grant.py @@ -1 +1,10 @@ -from authlib.oauth2.rfc6749.grants import ClientCredentialsGrant +from authlib.oauth2.rfc6749.grants import ( + ClientCredentialsGrant as BaseClientCredentialsGrant, +) + + +class ClientCredentialsGrant(BaseClientCredentialsGrant): + # Extend the default ["client_secret_basic"] to also accept private_key_jwt + # so that SMART Backend Services clients can authenticate asymmetrically + # (CONF-0120). + TOKEN_ENDPOINT_AUTH_METHODS = ["client_secret_basic", "private_key_jwt"] diff --git a/fence/oidc/grants/oidc_code_grant.py b/fence/oidc/grants/oidc_code_grant.py index c6382269a..8a9de4ad7 100644 --- a/fence/oidc/grants/oidc_code_grant.py +++ b/fence/oidc/grants/oidc_code_grant.py @@ -38,7 +38,10 @@ def create_authorization_code(client, grant_user, request): Certain parameters in the ``AuthorizationCode`` are filled out using the arguments passed from the OAuth request (the redirect URI, scope, - and nonce). + nonce, and PKCE code_challenge). + + For PKCE support (RFC 7636), we persist the code_challenge and + code_challenge_method if provided by the client. """ # requested lifetime (in seconds) for the refresh token @@ -48,6 +51,11 @@ def create_authorization_code(client, grant_user, request): default=config["REFRESH_TOKEN_EXPIRES_IN"], ) + # Extract PKCE parameters from the request (RFC 7636) + # code_challenge is required for public clients, optional for confidential + code_challenge = request.data.get("code_challenge") + code_challenge_method = request.data.get("code_challenge_method", "S256") + code = AuthorizationCode( code=generate_token(50), client_id=client.client_id, @@ -55,6 +63,8 @@ def create_authorization_code(client, grant_user, request): scope=request.scope, user_id=grant_user.id, nonce=request.data.get("nonce"), + code_challenge=code_challenge, + code_challenge_method=code_challenge_method, refresh_token_expires_in=refresh_token_expires_in, ) @@ -73,6 +83,9 @@ def save_authorization_code(self, code, request): Returns: authorization code string + + For PKCE support (RFC 7636), we persist the code_challenge and + code_challenge_method if provided by the client. """ # requested lifetime (in seconds) for the refresh token refresh_token_expires_in = get_valid_expiration_from_request( @@ -82,6 +95,12 @@ def save_authorization_code(self, code, request): ) client = request.client + # Extract PKCE parameters from the request (RFC 7636) + code_challenge = request.payload.data.get("code_challenge") + code_challenge_method = request.payload.data.get( + "code_challenge_method", "S256" + ) + code = AuthorizationCode( code=code, client_id=client.client_id, @@ -89,6 +108,8 @@ def save_authorization_code(self, code, request): scope=request.payload.scope, user_id=request.user.id, nonce=request.payload.data.get("nonce"), + code_challenge=code_challenge, + code_challenge_method=code_challenge_method, refresh_token_expires_in=refresh_token_expires_in, ) @@ -224,13 +245,15 @@ def validate_request_prompt(self, end_user, redirect_uri): def validate_token_request(self): """ Validate token request by checking allowed grant type, - making sure authorization code is found, and redirect URI is valid + making sure authorization code is found, redirect URI is valid, + and PKCE code_verifier matches the stored code_challenge. Raises: UnauthorizedClientError: if grant type is incorrect InvalidRequestError: if authorization code is absent InvalidGrantError: if authorization code is invalid InvalidGrantError: if redirect_uri is invalid + InvalidGrantError: if PKCE verification fails """ # authenticate the client if client authentication is included logger.debug("Authenticating token client..") @@ -260,7 +283,69 @@ def validate_token_request(self): if original_redirect_uri and redirect_uri != original_redirect_uri: raise InvalidGrantError("Invalid 'redirect_uri' in request.") + # PKCE validation (RFC 7636) + # If code_challenge was stored during authorization, + # verify that the code_verifier in the token request matches + code_challenge = authorization_code.code_challenge + if code_challenge: + code_verifier = self.request.payload.data.get("code_verifier") + if not code_verifier: + raise InvalidGrantError( + "PKCE code_verifier is required (code_challenge was provided during authorization)" + ) + + # Validate the code_verifier matches the stored code_challenge + self._validate_code_verifier( + code_verifier, code_challenge, authorization_code.code_challenge_method + ) + # save for create_token_response self.request.client = client self.request.authorization_code = authorization_code self.execute_hook("after_validate_token_request") + + def _validate_code_verifier( + self, code_verifier, code_challenge, code_challenge_method + ): + """ + Validate the code_verifier against the stored code_challenge per RFC 7636 PKCE. + + Args: + code_verifier: The code_verifier from the token request + code_challenge: The stored code_challenge from the authorization code + code_challenge_method: The challenge method used ("S256" or "plain") + + Raises: + InvalidGrantError: If PKCE verification fails + """ + import hashlib + import base64 + + # RFC 7636 Section 4.2: code_verifier = ABD... (random string) + # RFC 7636 Section 4.3: code_challenge = base64url(sha256(code_verifier)) for S256 + # code_challenge = code_verifier for plain + + try: + if code_challenge_method == "S256": + # Calculate SHA256 hash of code_verifier and encode as base64url + sha256_hash = hashlib.sha256(code_verifier.encode("utf-8")).digest() + expected_challenge = ( + base64.urlsafe_b64encode(sha256_hash).decode("utf-8").rstrip("=") + ) + elif code_challenge_method == "plain": + expected_challenge = code_verifier + else: + # Unsupported challenge method - treat as validation failure + logger.warning( + f"Unsupported PKCE code_challenge_method: {code_challenge_method}" + ) + raise InvalidGrantError("Invalid PKCE code_challenge_method") + + # Compare challenges + if code_challenge != expected_challenge: + logger.warning(f"PKCE verification failed: code_challenge mismatch") + raise InvalidGrantError("Invalid PKCE code_verifier") + + except (ValueError, UnicodeDecodeError) as e: + logger.warning(f"PKCE verification error: {e}") + raise InvalidGrantError("Invalid PKCE code_verifier") diff --git a/fence/oidc/private_key_jwt.py b/fence/oidc/private_key_jwt.py new file mode 100644 index 000000000..de5a9d0f8 --- /dev/null +++ b/fence/oidc/private_key_jwt.py @@ -0,0 +1,61 @@ +""" +Private Key JWT client authentication for SMART Backend Services (CONF-0120). + +Clients register their JWKS in `client_metadata["jwks"]` and authenticate by +sending a JWT signed with their private key to the token endpoint. + +Limitation: JTI replay prevention is not implemented because it requires a +shared cache (e.g. Redis) across all fence worker processes. The `exp` claim +still limits the window of reuse to the token's lifetime. +""" + +from authlib.jose import JsonWebKey +from authlib.oauth2.rfc6749 import InvalidClientError +from authlib.oauth2.rfc7523.client import JWTBearerClientAssertion + +from fence.config import config + + +class FencePrivateKeyJWT(JWTBearerClientAssertion): + """Server-side handler for private_key_jwt client authentication. + + Registered as a client auth method on the OIDC server so that Backend + Services clients can authenticate without a shared secret. + """ + + CLIENT_AUTH_METHOD = "private_key_jwt" + + def __init__(self): + super().__init__(token_url=None, validate_jti=False) + + def create_claims_options(self): + token_url = config["BASE_URL"].rstrip("/") + "/oauth2/token" + return { + # iss MUST equal sub per RFC 7523 §3 + "iss": { + "essential": True, + "validate": lambda claims, iss: claims.get("sub") == iss, + }, + "sub": {"essential": True}, + "aud": {"essential": True, "value": token_url}, + "exp": {"essential": True}, + } + + def resolve_client_public_key(self, client, headers): + """Look up the client's public key from its registered JWKS.""" + jwks = client.jwks + if not jwks: + raise InvalidClientError( + "Client has no public keys registered. Set 'jwks' in client metadata." + ) + key_set = JsonWebKey.import_key_set(jwks) + kid = headers.get("kid") + if kid: + key = key_set.find_by_kid(kid) + if not key: + raise InvalidClientError(f"No key with kid={kid!r} in client JWKS") + return key + keys = list(key_set) + if not keys: + raise InvalidClientError("Client JWKS contains no keys") + return keys[0] diff --git a/fence/oidc/server.py b/fence/oidc/server.py index 67d0a6089..156f023e4 100644 --- a/fence/oidc/server.py +++ b/fence/oidc/server.py @@ -7,7 +7,7 @@ """ from fence.oidc.client import authenticate_public_client, query_client -from fence.oidc.endpoints import RevocationEndpoint +from fence.oidc.endpoints import IntrospectionEndpoint, RevocationEndpoint from fence.oidc.grants import ( AuthorizationCodeGrant, ImplicitGrant, @@ -15,6 +15,7 @@ ClientCredentialsGrant, ) from fence.oidc.oidc_server import OIDCServer +from fence.oidc.private_key_jwt import FencePrivateKeyJWT server = OIDCServer(query_client=query_client, save_token=lambda *_: None) @@ -23,4 +24,6 @@ server.register_grant(RefreshTokenGrant) server.register_grant(ClientCredentialsGrant) server.register_endpoint(RevocationEndpoint) +server.register_endpoint(IntrospectionEndpoint) server.register_client_auth_method("none", authenticate_public_client) +server.register_client_auth_method("private_key_jwt", FencePrivateKeyJWT()) diff --git a/fence/utils.py b/fence/utils.py index aec2dd94d..d29c10671 100644 --- a/fence/utils.py +++ b/fence/utils.py @@ -457,13 +457,43 @@ def get_SQLAlchemyDriver(db_conn_url): } +# SMART scope prefixes (SMART App Launch 2.0 + SMART Backend Services) +_SMART_SCOPE_PREFIXES = ("patient/", "user/", "system/", "launch/") +_SMART_SCOPE_LITERALS = frozenset( + ["openid", "fhirUser", "profile", "online_access", "offline_access", "launch"] +) + + +def _is_smart_scope(scope: str) -> bool: + return scope.startswith(_SMART_SCOPE_PREFIXES) or scope in _SMART_SCOPE_LITERALS + + +def _requires_user_context(scope: str) -> bool: + """Return True for SMART scopes that require a logged-in user (not Backend Services).""" + return _is_smart_scope(scope) and not scope.startswith("system/") + + def validate_scopes(request_scopes, client): if not client: raise Exception("Client object is None") if request_scopes: scopes = scope_to_list(request_scopes) - # can we get some debug logs here that log the client, what scopes they have, and what scopes were requested + + is_smart_request = any(_is_smart_scope(scope) for scope in scopes) + + if is_smart_request: + # Backend Services use only system/ scopes — openid is not required or expected. + # User-facing SMART flows (authorization_code, EHR launch) MUST include openid. + needs_user_context = any(_requires_user_context(scope) for scope in scopes) + if needs_user_context and "openid" not in scopes: + logger.debug( + "SMART user-facing request lacks 'openid'. Requested: %s", + " ".join(scopes), + ) + raise InvalidScopeError("Failed to Authorize due to unsupported scope") + return True + if not client.check_requested_scopes(set(scopes)): logger.debug( "Request Scope are " From 40cbd5623c8463d758ba89a7d1b8ac3865343f98 Mon Sep 17 00:00:00 2001 From: vincentvanhees Date: Tue, 6 Oct 2026 18:56:38 +0200 Subject: [PATCH 09/14] refactor(oidc): make extra-scope allowlist and extra-claims config-driven MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fence previously hardcoded SMART on FHIR scope prefixes (patient/, user/, system/, launch/) and literals (fhirUser, openid, etc.) in create_client(), and hardcoded fhirUser ID-token claim generation in generate_id_token(). This couples fence to FHIR semantics it should not know about. Replace with two generic config mechanisms: EXTRA_ALLOWED_SCOPE_PREFIXES / EXTRA_ALLOWED_SCOPES Lists of scope prefixes and exact literals that bypass the CLIENT_ALLOWED_SCOPES check when registering OAuth clients. FHIR deployments set these in their fence-config.yaml; a stock fence deployment leaves them empty (defaults: []). SCOPE_EXTRA_CLAIMS Map of scope-name → {base_url_env, user_tag}. When a matching scope is requested and the authenticated user has the named tag, the claim is added to the ID token as "{env_var_value}/{tag_value}". FHIR deployments configure this for the fhirUser claim; fence itself carries no FHIR knowledge. Also include "scope" in token responses per RFC 6749 §5.1. --- fence/config-default.yaml | 21 +++++++++++++++++++++ fence/jwt/token.py | 11 +++++++++++ fence/oidc/jwt_generator.py | 2 ++ fence/utils.py | 11 ++++++++++- 4 files changed, 44 insertions(+), 1 deletion(-) diff --git a/fence/config-default.yaml b/fence/config-default.yaml index 2639bfde4..abce80d62 100755 --- a/fence/config-default.yaml +++ b/fence/config-default.yaml @@ -25,6 +25,27 @@ APP_NAME: 'Gen3 Data Commons' # Where fence microservice is deployed BASE_URL: 'http://localhost/user' + +# Extra scope prefixes (e.g. "patient/", "user/") and exact scope literals +# (e.g. "fhirUser", "launch") that are allowed when registering OAuth clients, +# in addition to CLIENT_ALLOWED_SCOPES. Scopes matching these patterns bypass +# the CLIENT_ALLOWED_SCOPES allowlist check. +EXTRA_ALLOWED_SCOPE_PREFIXES: [] +EXTRA_ALLOWED_SCOPES: [] + +# Adds extra claims to ID tokens when a matching scope is requested. +# Each key is both the scope name and the claim name added to the token. +# base_url_env: environment variable holding the base URL prefix. +# user_tag: user tag key whose value is appended to the base URL. +# The claim is set to "{base_url}/{tag_value}" when scope is present and the +# user has the named tag. Leave empty ({}) to disable. +# +# Example (SMART on FHIR fhirUser claim): +# SCOPE_EXTRA_CLAIMS: +# fhirUser: +# base_url_env: MY_FHIR_BASE_URL +# user_tag: fhir_user_id +SCOPE_EXTRA_CLAIMS: {} # postgres db to connect to # connection url format: # postgresql://[user[:password]@][netloc][:port][/dbname] diff --git a/fence/jwt/token.py b/fence/jwt/token.py index b1840e30f..f73ac1715 100644 --- a/fence/jwt/token.py +++ b/fence/jwt/token.py @@ -538,6 +538,17 @@ def generate_id_token( if user.tags: claims["context"]["user"]["tags"] = {tag.key: tag.value for tag in user.tags} + import os + + for claim_name, claim_cfg in config.get("SCOPE_EXTRA_CLAIMS", {}).items(): + if claim_name not in (scopes or []): + continue + base_url = os.environ.get(claim_cfg.get("base_url_env", ""), "").rstrip("/") + tag_value = claims["context"]["user"]["tags"].get( + claim_cfg.get("user_tag", "") + ) + if base_url and tag_value: + claims[claim_name] = f"{base_url}/{tag_value}" linked_google_email = kwargs.get("linked_google_email") linked_google_account_exp = kwargs.get("linked_google_account_exp") diff --git a/fence/oidc/jwt_generator.py b/fence/oidc/jwt_generator.py index 2787add77..8ee04af43 100644 --- a/fence/oidc/jwt_generator.py +++ b/fence/oidc/jwt_generator.py @@ -254,6 +254,7 @@ def generate_token_response( "access_token": access_token, "refresh_token": refresh_token, "expires_in": expires_in, + "scope": " ".join(scope), } @@ -293,4 +294,5 @@ def generate_client_response(client, expires_in=None, scope=None, **kwargs): "token_type": "Bearer", "access_token": access_token, "expires_in": expires_in, + "scope": " ".join(scope), } diff --git a/fence/utils.py b/fence/utils.py index d29c10671..97ebf12a7 100644 --- a/fence/utils.py +++ b/fence/utils.py @@ -100,7 +100,16 @@ def create_client( auth_method = "client_secret_basic" if confidential else "none" allowed_scopes = allowed_scopes or config["CLIENT_ALLOWED_SCOPES"] - if not set(allowed_scopes).issubset(set(config["CLIENT_ALLOWED_SCOPES"])): + _extra_prefixes = tuple(config.get("EXTRA_ALLOWED_SCOPE_PREFIXES", [])) + _extra_literals = frozenset(config.get("EXTRA_ALLOWED_SCOPES", [])) + non_extra_scopes = [ + s + for s in allowed_scopes + if not ( + (_extra_prefixes and s.startswith(_extra_prefixes)) or s in _extra_literals + ) + ] + if not set(non_extra_scopes).issubset(set(config["CLIENT_ALLOWED_SCOPES"])): raise ValueError( "Each allowed scope must be one of: {}".format( config["CLIENT_ALLOWED_SCOPES"] From 90c49f60af6354ab7f32d7c66049ad502bbf9200 Mon Sep 17 00:00:00 2001 From: vincentvanhees Date: Mon, 12 Oct 2026 15:32:54 +0200 Subject: [PATCH 10/14] fix(oidc): validate non-SMART scopes against client allowlist validate_scopes() returned True early for any request containing a SMART scope (including openid), bypassing client.check_requested_scopes() for the entire request. This allowed arbitrary unknown scopes to pass through unchecked whenever openid was also present. Non-SMART scopes are now explicitly validated against the client's allowed scopes even when the request is identified as a SMART flow. --- fence/utils.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/fence/utils.py b/fence/utils.py index 97ebf12a7..bdd625d49 100644 --- a/fence/utils.py +++ b/fence/utils.py @@ -501,6 +501,18 @@ def validate_scopes(request_scopes, client): " ".join(scopes), ) raise InvalidScopeError("Failed to Authorize due to unsupported scope") + # SMART scopes are allowed beyond CLIENT_ALLOWED_SCOPES, but any + # non-SMART scopes in the request still must be on the client allowlist. + non_smart_scopes = {s for s in scopes if not _is_smart_scope(s)} + if non_smart_scopes and not set(client.allowed_scopes).issuperset( + non_smart_scopes + ): + logger.debug( + "Non-SMART scopes %s not in client allowed scopes %s", + non_smart_scopes, + client.scope, + ) + raise InvalidScopeError("Failed to Authorize due to unsupported scope") return True if not client.check_requested_scopes(set(scopes)): From 32eb3b022260178809bc857e4df1fe32ad59e5aa Mon Sep 17 00:00:00 2001 From: vincentvanhees Date: Tue, 13 Oct 2026 15:03:05 +0200 Subject: [PATCH 11/14] test(oidc): add test coverage for SMART/US Core branch and fix two bugs Fix resolve_client_public_key() in private_key_jwt.py for authlib 1.6.12 compatibility: list(key_set) is not supported (use key_set.keys), and find_by_kid() raises ValueError on a miss rather than returning None. Set PROMETHEUS_MULTIPROC_DIR in tests/conftest.py before fence is imported so the test suite runs without requiring the env var to be set manually. Add 74 new tests across 5 files targeting the new SMART on FHIR / US Core compliance code on this branch: - tests/oidc/test_private_key_jwt.py: create_claims_options and all resolve_client_public_key branches (no JWKS, matching kid, unknown kid, no kid, empty key set) - tests/oidc/test_endpoints.py: JWTIntrospectionToken, IntrospectionEndpoint payload building and query_token, revoke and introspect HTTP endpoints - tests/oidc/test_jwt_generator.py: generate_client_response scope handling, client_credentials dispatch, implicit scope-as-string path - tests/oidc/test_oidc_code_grant.py: PKCE S256/plain full flows and _validate_code_verifier branches, exists_nonce, validate_request_prompt (none/consent/select_account prompts), token request error paths - tests/oidc/test_oauth2_blueprint.py: auto_approve, consent denial, prompt=none error cases, unauthenticated authorize paths, revoke with invalid token, display_error endpoint --- .secrets.baseline | 2 +- fence/oidc/private_key_jwt.py | 11 +- tests/conftest.py | 4 + tests/oidc/test_endpoints.py | 283 ++++++++++++++++++++ tests/oidc/test_jwt_generator.py | 175 ++++++++++++ tests/oidc/test_oauth2_blueprint.py | 381 ++++++++++++++++++++++++++ tests/oidc/test_oidc_code_grant.py | 397 ++++++++++++++++++++++++++++ tests/oidc/test_private_key_jwt.py | 149 +++++++++++ 8 files changed, 1396 insertions(+), 6 deletions(-) create mode 100644 tests/oidc/test_endpoints.py create mode 100644 tests/oidc/test_jwt_generator.py create mode 100644 tests/oidc/test_oauth2_blueprint.py create mode 100644 tests/oidc/test_oidc_code_grant.py create mode 100644 tests/oidc/test_private_key_jwt.py diff --git a/.secrets.baseline b/.secrets.baseline index 47b3d0863..0bcc3facf 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -279,5 +279,5 @@ } ] }, - "generated_at": "2026-09-30T19:42:34Z" + "generated_at": "2026-10-13T13:03:00Z" } diff --git a/fence/oidc/private_key_jwt.py b/fence/oidc/private_key_jwt.py index de5a9d0f8..cebeb39e7 100644 --- a/fence/oidc/private_key_jwt.py +++ b/fence/oidc/private_key_jwt.py @@ -51,11 +51,12 @@ def resolve_client_public_key(self, client, headers): key_set = JsonWebKey.import_key_set(jwks) kid = headers.get("kid") if kid: - key = key_set.find_by_kid(kid) - if not key: + try: + key = key_set.find_by_kid(kid) + except (ValueError, KeyError): raise InvalidClientError(f"No key with kid={kid!r} in client JWKS") return key - keys = list(key_set) - if not keys: + # No kid: return the first key in the set + if not key_set.keys: raise InvalidClientError("Client JWKS contains no keys") - return keys[0] + return key_set.keys[0] diff --git a/tests/conftest.py b/tests/conftest.py index f2a7e5608..52a17c5f4 100755 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,6 +7,10 @@ from collections import OrderedDict import json import os + +# Must be set before fence (and prometheus_client) is imported. +os.makedirs("/tmp/prometheus_metrics", exist_ok=True) +os.environ.setdefault("PROMETHEUS_MULTIPROC_DIR", "/tmp/prometheus_metrics") import copy import time import flask diff --git a/tests/oidc/test_endpoints.py b/tests/oidc/test_endpoints.py new file mode 100644 index 000000000..84ac8192a --- /dev/null +++ b/tests/oidc/test_endpoints.py @@ -0,0 +1,283 @@ +""" +Tests for fence.oidc.endpoints. + +Covers: +- JWTIntrospectionToken: is_expired, is_revoked, check_client +- IntrospectionEndpoint.introspect_token: payload fields, scope handling, context claims +- IntrospectionEndpoint.query_token: valid JWT → token object, invalid JWT → None +- RevocationEndpoint.query_token and revoke_token via HTTP +- JWTToken: client matching via revoke endpoint +""" + +import time +from unittest.mock import MagicMock, patch + +import pytest + +from fence.oidc.endpoints import JWTIntrospectionToken, IntrospectionEndpoint +from tests.utils.oauth2 import create_basic_header + + +# --------------------------------------------------------------------------- +# JWTIntrospectionToken unit tests +# --------------------------------------------------------------------------- + + +def test_jwt_introspection_token_expired_when_exp_in_past(): + token = JWTIntrospectionToken({"exp": int(time.time()) - 60}) + assert token.is_expired() is True + + +def test_jwt_introspection_token_not_expired_when_exp_in_future(): + token = JWTIntrospectionToken({"exp": int(time.time()) + 3600}) + assert token.is_expired() is False + + +def test_jwt_introspection_token_not_expired_without_exp(): + """No exp claim → treated as epoch 0, which is in the past → expired.""" + token = JWTIntrospectionToken({}) + assert token.is_expired() is True + + +def test_jwt_introspection_token_is_revoked_when_jti_blacklisted(): + with patch("fence.jwt.blacklist.is_blacklisted", return_value=True): + token = JWTIntrospectionToken({"jti": "some-jti"}) + assert token.is_revoked() is True + + +def test_jwt_introspection_token_not_revoked_when_jti_not_blacklisted(): + with patch("fence.jwt.blacklist.is_blacklisted", return_value=False): + token = JWTIntrospectionToken({"jti": "some-jti"}) + assert token.is_revoked() is False + + +def test_jwt_introspection_token_not_revoked_when_no_jti(): + """Without a jti, the token is considered not revoked.""" + token = JWTIntrospectionToken({}) + assert token.is_revoked() is False + + +def test_jwt_introspection_token_check_client_matches(): + client = MagicMock() + client.client_id = "my-client" + token = JWTIntrospectionToken({"azp": "my-client"}) + assert token.check_client(client) is True + + +def test_jwt_introspection_token_check_client_mismatches(): + client = MagicMock() + client.client_id = "other-client" + token = JWTIntrospectionToken({"azp": "my-client"}) + assert token.check_client(client) is False + + +def test_jwt_introspection_token_check_client_no_azp(): + client = MagicMock() + client.client_id = "my-client" + token = JWTIntrospectionToken({}) + assert token.check_client(client) is False + + +# --------------------------------------------------------------------------- +# IntrospectionEndpoint.introspect_token() unit tests +# --------------------------------------------------------------------------- + + +def _make_endpoint(): + """Return an IntrospectionEndpoint instance with mocked server/request.""" + endpoint = IntrospectionEndpoint.__new__(IntrospectionEndpoint) + return endpoint + + +def test_introspect_token_basic_payload(): + endpoint = _make_endpoint() + claims = { + "scope": "openid user", + "azp": "test-client", + "sub": "user-123", + "iss": "http://localhost/user", + "exp": int(time.time()) + 3600, + "iat": int(time.time()), + "jti": "jwt-id-abc", + } + token = JWTIntrospectionToken(claims) + payload = endpoint.introspect_token(token) + + assert payload["scope"] == "openid user" + assert payload["client_id"] == "test-client" + assert payload["sub"] == "user-123" + assert payload["iss"] == "http://localhost/user" + assert payload["jti"] == "jwt-id-abc" + assert payload["token_type"] == "Bearer" + assert payload.get("active") is None # parent adds active, not us + + +def test_introspect_token_scope_as_list_joined(): + endpoint = _make_endpoint() + claims = {"scope": ["openid", "user", "data"]} + token = JWTIntrospectionToken(claims) + payload = endpoint.introspect_token(token) + assert payload["scope"] == "openid user data" + + +def test_introspect_token_scope_as_string_unchanged(): + endpoint = _make_endpoint() + claims = {"scope": "openid user"} + token = JWTIntrospectionToken(claims) + payload = endpoint.introspect_token(token) + assert payload["scope"] == "openid user" + + +def test_introspect_token_includes_username_from_context(): + endpoint = _make_endpoint() + claims = { + "context": {"user": {"name": "alice"}}, + } + token = JWTIntrospectionToken(claims) + payload = endpoint.introspect_token(token) + assert payload["username"] == "alice" + + +def test_introspect_token_includes_patient_id_from_context(): + endpoint = _make_endpoint() + claims = { + "context": {"patient": "patient-xyz"}, + } + token = JWTIntrospectionToken(claims) + payload = endpoint.introspect_token(token) + assert payload["patient"] == "patient-xyz" + + +def test_introspect_token_omits_none_values(): + endpoint = _make_endpoint() + claims = {} # all optional fields absent + token = JWTIntrospectionToken(claims) + payload = endpoint.introspect_token(token) + # None values must be filtered out (except token_type which is always set) + assert "sub" not in payload + assert "iss" not in payload + assert "jti" not in payload + assert payload["token_type"] == "Bearer" + + +def test_introspect_token_no_username_when_context_user_missing(): + endpoint = _make_endpoint() + claims = {"context": {}} + token = JWTIntrospectionToken(claims) + payload = endpoint.introspect_token(token) + assert "username" not in payload + + +def test_introspect_token_no_patient_when_context_patient_missing(): + endpoint = _make_endpoint() + claims = {"context": {"user": {"name": "alice"}}} + token = JWTIntrospectionToken(claims) + payload = endpoint.introspect_token(token) + assert "patient" not in payload + + +# --------------------------------------------------------------------------- +# IntrospectionEndpoint.query_token unit tests (require app context + valid JWT) +# --------------------------------------------------------------------------- + + +def test_introspect_query_token_returns_token_for_valid_jwt( + app, encoded_jwt, kid, rsa_public_key +): + """query_token wraps a valid JWT in a JWTIntrospectionToken.""" + endpoint = _make_endpoint() + result = endpoint.query_token(encoded_jwt, "access_token") + assert result is not None + assert isinstance(result, JWTIntrospectionToken) + + +def test_introspect_query_token_returns_none_for_invalid_jwt(app): + """query_token returns None for a JWT that cannot be validated.""" + endpoint = _make_endpoint() + result = endpoint.query_token("not.a.jwt", None) + assert result is None + + +# --------------------------------------------------------------------------- +# Revocation endpoint HTTP tests — covers query_token, revoke_token, JWTToken +# --------------------------------------------------------------------------- + + +def test_revoke_endpoint_success_with_valid_credentials( + client, oauth_client, encoded_jwt_refresh_token +): + """Successful revocation of a refresh token returns HTTP 200 (RFC 7009).""" + auth_header = create_basic_header( + oauth_client.client_id, oauth_client.client_secret + ) + response = client.post( + "/oauth2/revoke", + data={"token": encoded_jwt_refresh_token}, + headers=auth_header, + ) + assert response.status_code == 200, response.data + + +def test_revoke_endpoint_invalid_client_id( + client, oauth_client, encoded_jwt_refresh_token +): + """Wrong client_id results in an authentication error.""" + auth_header = create_basic_header("wrong-client-id", oauth_client.client_secret) + response = client.post( + "/oauth2/revoke", + data={"token": encoded_jwt_refresh_token}, + headers=auth_header, + ) + # client authentication fails → 401 or 400 + assert response.status_code in (400, 401), response.data + + +def test_revoke_endpoint_invalid_client_secret( + client, oauth_client, encoded_jwt_refresh_token +): + """Wrong client_secret results in an authentication error.""" + auth_header = create_basic_header(oauth_client.client_id, "wrong-secret") + response = client.post( + "/oauth2/revoke", + data={"token": encoded_jwt_refresh_token}, + headers=auth_header, + ) + assert response.status_code in (400, 401), response.data + + +# --------------------------------------------------------------------------- +# Introspection endpoint HTTP tests +# --------------------------------------------------------------------------- + + +def test_introspect_endpoint_valid_token_returns_active( + client, oauth_client, encoded_jwt +): + """A valid JWT is introspected successfully and active=true is returned.""" + auth_header = create_basic_header( + oauth_client.client_id, oauth_client.client_secret + ) + response = client.post( + "/oauth2/introspect", + data={"token": encoded_jwt}, + headers=auth_header, + ) + assert response.status_code == 200, response.json + data = response.json + assert data.get("active") is True + assert "scope" in data or "token_type" in data + + +def test_introspect_endpoint_invalid_token_returns_inactive(client, oauth_client): + """An invalid JWT results in active=false.""" + auth_header = create_basic_header( + oauth_client.client_id, oauth_client.client_secret + ) + response = client.post( + "/oauth2/introspect", + data={"token": "not.a.jwt.at.all"}, + headers=auth_header, + ) + assert response.status_code == 200, response.json + data = response.json + assert data.get("active") is False diff --git a/tests/oidc/test_jwt_generator.py b/tests/oidc/test_jwt_generator.py new file mode 100644 index 000000000..62d93de0b --- /dev/null +++ b/tests/oidc/test_jwt_generator.py @@ -0,0 +1,175 @@ +""" +Tests for fence.oidc.jwt_generator. + +Covers: +- generate_client_response(): scope as string (split), empty scope, normal flow +- generate_token(): client_credentials dispatch → generate_client_response +- generate_implicit_response(): scope as string path +""" + +import time +from unittest.mock import MagicMock, patch + +import pytest + + +# --------------------------------------------------------------------------- +# generate_client_response() tests — called directly within app context +# --------------------------------------------------------------------------- + + +def _mock_client(client_id="test-client"): + client = MagicMock() + client.client_id = client_id + # allowed_scopes must be a list for validate_scopes + client.allowed_scopes = ["openid", "user", "data"] + client.scope = "openid user data" + return client + + +def test_generate_client_response_scope_as_string_is_split(app): + """scope passed as space-separated string should be split into a list.""" + from fence.oidc.jwt_generator import generate_client_response + + client = _mock_client() + with app.test_request_context("/"): + result = generate_client_response(client, scope="openid user") + + assert result["token_type"] == "Bearer" + assert "access_token" in result + # scope in response should be the original joined string + assert "openid" in result["scope"] + assert "user" in result["scope"] + + +def test_generate_client_response_empty_scope(app): + """scope=None or scope=[] should produce empty scope in response.""" + from fence.oidc.jwt_generator import generate_client_response + + client = _mock_client() + with app.test_request_context("/"): + result = generate_client_response(client, scope=None) + + assert result["token_type"] == "Bearer" + assert "access_token" in result + assert result["scope"] == "" + + +def test_generate_client_response_normal_flow(app): + """generate_client_response with list scope produces expected keys.""" + from fence.oidc.jwt_generator import generate_client_response + + client = _mock_client() + with app.test_request_context("/"): + result = generate_client_response(client, scope=["openid", "user"]) + + assert "access_token" in result + assert "expires_in" in result + assert result["token_type"] == "Bearer" + assert isinstance(result["expires_in"], int) + + +def test_generate_client_response_returns_string_scope(app): + """The response scope field must be a string, not a list.""" + from fence.oidc.jwt_generator import generate_client_response + + client = _mock_client() + with app.test_request_context("/"): + result = generate_client_response(client, scope=["openid", "user"]) + + assert isinstance(result["scope"], str) + + +# --------------------------------------------------------------------------- +# generate_token() dispatch test — client_credentials → generate_client_response +# --------------------------------------------------------------------------- + + +def test_generate_token_dispatches_to_client_response_for_client_credentials(app): + """generate_token routes client_credentials grant to generate_client_response.""" + from fence.oidc.jwt_generator import generate_token + + client = _mock_client() + + # validate_scopes is called first; patch it out to not raise + with patch("fence.oidc.jwt_generator.validate_scopes"), app.test_request_context( + "/" + ): + result = generate_token(client, "client_credentials", scope=["openid"]) + + assert result is not None + assert "access_token" in result + assert result["token_type"] == "Bearer" + + +# --------------------------------------------------------------------------- +# generate_client_response via HTTP (client_credentials grant) — +# exercises the full request path including scope normalization +# --------------------------------------------------------------------------- + + +def test_client_credentials_token_endpoint_returns_access_token( + oauth_test_client_with_client_credentials, +): + """Full client_credentials token request returns a valid access_token.""" + token_response = oauth_test_client_with_client_credentials.token() + assert token_response.response.status_code == 200, token_response.response.json + data = token_response.response.json + assert "access_token" in data + assert data["token_type"] == "Bearer" + assert "expires_in" in data + + +def test_client_credentials_token_has_string_scope( + oauth_test_client_with_client_credentials, +): + """The scope in the client_credentials response is a space-separated string.""" + token_response = oauth_test_client_with_client_credentials.token() + data = token_response.response.json + assert isinstance(data.get("scope", ""), str) + + +def test_client_credentials_token_with_requested_scope( + oauth_test_client_with_client_credentials, +): + """A specific scope can be requested and is reflected in the response.""" + token_response = oauth_test_client_with_client_credentials.token(scope="openid") + assert token_response.response.status_code == 200 + data = token_response.response.json + assert "access_token" in data + + +# --------------------------------------------------------------------------- +# generate_implicit_response() — scope as string path +# --------------------------------------------------------------------------- + + +def test_generate_implicit_response_scope_string_is_split(app, oauth_user, db_session): + """When scope is a string, generate_implicit_response splits it into a list.""" + from fence.oidc.jwt_generator import generate_implicit_response + from fence.models import User + + # Fetch the actual user from the db so the session check works + user = db_session.query(User).filter_by(id=oauth_user.user_id).first() + assert user is not None + + client = _mock_client() + + with patch( + "fence.oidc.jwt_generator.get_linked_google_account_email", return_value=None + ), patch( + "fence.oidc.jwt_generator.get_linked_google_account_exp", return_value=None + ), app.test_request_context( + "/" + ): + result = generate_implicit_response( + client, + grant_type="implicit", + user=user, + scope="openid user", + nonce="test-nonce-value", + ) + + assert "id_token" in result + assert "access_token" in result + assert result["token_type"] == "Bearer" diff --git a/tests/oidc/test_oauth2_blueprint.py b/tests/oidc/test_oauth2_blueprint.py new file mode 100644 index 000000000..c26386300 --- /dev/null +++ b/tests/oidc/test_oauth2_blueprint.py @@ -0,0 +1,381 @@ +""" +Tests for fence/blueprints/oauth2.py + +Covers: +- display_error() — returns query args as JSON +- authorize() auto_approve path — skips consent screen +- _handle_consent_confirmation() — user denies consent (confirm != 'yes') +- authorize() unauthenticated paths — DEFAULT_LOGIN_URL, unknown idp, fence/shib extras +- _get_auth_response_for_prompts() — prompt='none' with multiple values +- _get_authorize_error_response() — returns 302 with error params +- revoke_token() — BlacklistingInvalidTokenError returns 200 +""" + +from unittest.mock import patch, MagicMock +from urllib.parse import urlparse, parse_qs + +import pytest + +from fence.config import config +from fence.errors import BlacklistingInvalidTokenError +from fence.models import Client + + +# --------------------------------------------------------------------------- +# display_error +# --------------------------------------------------------------------------- + + +def test_display_error_returns_query_args_as_json(client): + """ + GET /oauth2/errors?error=access_denied&error_description=foo + returns those query params as a JSON object. + Covers line 435. + """ + response = client.get( + "/oauth2/errors?error=access_denied&error_description=User+denied+access" + ) + assert response.status_code == 200 + data = response.json + assert data["error"] == "access_denied" + assert "denied" in data["error_description"] + + +def test_display_error_with_no_params(client): + """GET /oauth2/errors with no params returns an empty JSON object.""" + response = client.get("/oauth2/errors") + assert response.status_code == 200 + assert response.json == {} + + +# --------------------------------------------------------------------------- +# auto_approve +# --------------------------------------------------------------------------- + + +def test_authorize_auto_approve_skips_consent_screen( + client, oauth_test_client, oauth_client, db_session +): + """ + When the OAuth client has auto_approve=True, the authorize endpoint + issues a code without requiring a 'confirm' parameter. + Covers line 141. + """ + # Enable auto_approve on the DB client + db_client = ( + db_session.query(Client).filter_by(client_id=oauth_client.client_id).first() + ) + db_client.auto_approve = True + db_session.commit() + + # Authorize without any 'confirm' param — should still get a code back + auth_response = oauth_test_client.authorize( + method="GET", + data={"confirm": None}, # no confirm + do_asserts=False, + ) + # With auto_approve, the GET redirects straight to the redirect URI with a code + location = auth_response.location + assert location is not None + parsed = urlparse(location) + qs = parse_qs(parsed.query) + assert "code" in qs, f"Expected 'code' in redirect location, got: {location}" + + +# --------------------------------------------------------------------------- +# _handle_consent_confirmation — user denies +# --------------------------------------------------------------------------- + + +def test_handle_consent_confirmation_denied_returns_access_denied(oauth_test_client): + """ + When the user responds with confirm != 'yes', the server responds with + an access_denied error redirect. + Covers line 169. + """ + response = oauth_test_client.authorize( + method="GET", + data={"confirm": "no"}, + do_asserts=False, + ) + location = response.location + assert location is not None + qs = parse_qs(urlparse(location).query) + assert "error" in qs + assert qs["error"][0] == "access_denied" + + +# --------------------------------------------------------------------------- +# _get_authorize_error_response — direct function test +# --------------------------------------------------------------------------- + + +def test_get_authorize_error_response_returns_302_with_error_params(app): + """ + _get_authorize_error_response builds a 302 redirect containing the + OAuth2 error params in the Location header. + Covers lines 312-316. + """ + from authlib.oauth2.rfc6749 import InvalidRequestError + from fence.blueprints.oauth2 import _get_authorize_error_response + + error = InvalidRequestError(state="xyz") + redirect_uri = "https://client.example.com/callback" + + with app.test_request_context("/"): + response = _get_authorize_error_response(error, redirect_uri) + + assert response.status_code == 302 + location = response.headers["Location"] + assert location.startswith(redirect_uri) + qs = parse_qs(urlparse(location).query) + assert "error" in qs + assert qs["error"][0] == "invalid_request" + + +# --------------------------------------------------------------------------- +# _get_auth_response_for_prompts — 'none' with multiple prompts +# --------------------------------------------------------------------------- + + +def test_get_auth_response_for_prompts_none_combined_with_other_returns_error_redirect( + app, oauth_user +): + """ + _get_auth_response_for_prompts with prompt='none select_account' returns + a 302 error redirect because 'none' cannot be combined with other values. + Covers lines 216-222. + """ + from fence.blueprints.oauth2 import _get_auth_response_for_prompts + + grant = MagicMock() + grant.params = { + "state": "teststate", + "uri": "http://example.com", + "redirect_uri": "https://client.example.com/callback", + } + user = MagicMock() + client = MagicMock() + + with app.test_request_context("/"): + response = _get_auth_response_for_prompts( + "none select_account", grant, user, client, "openid" + ) + + assert response.status_code == 302 + location = response.headers["Location"] + qs = parse_qs(urlparse(location).query) + assert "error" in qs + + +def test_get_auth_response_for_prompts_none_unauthenticated_returns_access_denied( + app, oauth_user +): + """ + _get_auth_response_for_prompts with prompt='none' when get_current_user + raises Unauthorized returns a 302 error redirect with access_denied. + Covers lines 228-231. + """ + from fence.blueprints.oauth2 import _get_auth_response_for_prompts + from fence.errors import Unauthorized + + grant = MagicMock() + grant.params = { + "state": "teststate", + "uri": "http://example.com", + "redirect_uri": "https://client.example.com/callback", + } + user = MagicMock() + client = MagicMock() + + with app.test_request_context("/"): + with patch( + "fence.blueprints.oauth2.get_current_user", + side_effect=Unauthorized("not logged in"), + ): + response = _get_auth_response_for_prompts( + "none", grant, user, client, "openid" + ) + + assert response.status_code == 302 + location = response.headers["Location"] + qs = parse_qs(urlparse(location).query) + assert "error" in qs + assert qs["error"][0] == "access_denied" + + +# --------------------------------------------------------------------------- +# authorize() — unauthenticated paths +# --------------------------------------------------------------------------- + + +@pytest.fixture(scope="function") +def mock_auth_off(monkeypatch): + """Disable MOCK_AUTH so get_current_user raises Unauthorized.""" + monkeypatch.setitem(config, "MOCK_AUTH", False) + + +def test_authorize_unauthenticated_uses_default_login_url_when_no_idp( + client, mock_auth_off, monkeypatch +): + """ + When no DEFAULT_LOGIN_IDP and 'default' not in ENABLED_IDENTITY_PROVIDERS, + the authorize endpoint falls back to DEFAULT_LOGIN_URL. + Covers line 91. + """ + monkeypatch.setitem(config, "DEFAULT_LOGIN_IDP", None) + monkeypatch.setitem(config, "ENABLED_IDENTITY_PROVIDERS", {}) + monkeypatch.setitem( + config, "DEFAULT_LOGIN_URL", "https://fallback-login.example.com" + ) + + response = client.get( + "/oauth2/authorize" + "?client_id=test-client" + "&response_type=code" + "&scope=openid" + "&redirect_uri=https://oauth-test-client.net" + ) + # Should redirect to the fallback login URL + assert response.status_code == 302 + location = response.headers["Location"] + assert "fallback-login.example.com" in location + + +def test_authorize_unauthenticated_unknown_idp_raises_user_error( + client, mock_auth_off, monkeypatch +): + """ + When a specific idp is requested that is not in OPENID_CONNECT, + a UserError (400) is raised. + Covers lines 103-106. + """ + monkeypatch.setitem(config, "DEFAULT_LOGIN_IDP", None) + monkeypatch.setitem(config, "ENABLED_IDENTITY_PROVIDERS", {}) + monkeypatch.setitem(config, "DEFAULT_LOGIN_URL", None) + monkeypatch.setitem(config, "OPENID_CONNECT", {"google": {}}) + + response = client.get( + "/oauth2/authorize" + "?client_id=test-client" + "&response_type=code" + "&scope=openid" + "&redirect_uri=https://oauth-test-client.net" + "&idp=nonexistent_provider" + ) + assert response.status_code == 400 + + +def test_authorize_unauthenticated_fence_idp_adds_idp_param( + client, mock_auth_off, monkeypatch +): + """ + When idp='fence' and fence_idp is provided, the redirect to the login + endpoint includes idp= in the params. + Covers lines 110-112. + """ + monkeypatch.setitem(config, "DEFAULT_LOGIN_IDP", None) + monkeypatch.setitem(config, "ENABLED_IDENTITY_PROVIDERS", {}) + monkeypatch.setitem(config, "DEFAULT_LOGIN_URL", None) + monkeypatch.setitem(config, "OPENID_CONNECT", {"fence": {}}) + monkeypatch.setitem(config, "BASE_URL", config.get("BASE_URL", "http://localhost")) + + with patch("fence.blueprints.oauth2.get_idp_route_name", return_value="fence"): + response = client.get( + "/oauth2/authorize" + "?client_id=test-client" + "&response_type=code" + "&scope=openid" + "&redirect_uri=https://oauth-test-client.net" + "&idp=fence" + "&fence_idp=google" + ) + + assert response.status_code == 302 + location = response.headers["Location"] + qs = parse_qs(urlparse(location).query) + # The login URL should include the fence_idp as 'idp' param + assert "idp" in qs + assert qs["idp"][0] == "google" + + +def test_authorize_unauthenticated_shibboleth_idp_adds_shib_idp_param( + client, mock_auth_off, monkeypatch +): + """ + When idp='shibboleth' and shib_idp is provided, the redirect to the login + endpoint includes shib_idp in the params. + Covers line 114. + """ + monkeypatch.setitem(config, "DEFAULT_LOGIN_IDP", None) + monkeypatch.setitem(config, "ENABLED_IDENTITY_PROVIDERS", {}) + monkeypatch.setitem(config, "DEFAULT_LOGIN_URL", None) + monkeypatch.setitem(config, "OPENID_CONNECT", {"shibboleth": {}}) + monkeypatch.setitem(config, "BASE_URL", config.get("BASE_URL", "http://localhost")) + + with patch("fence.blueprints.oauth2.get_idp_route_name", return_value="shibboleth"): + response = client.get( + "/oauth2/authorize" + "?client_id=test-client" + "&response_type=code" + "&scope=openid" + "&redirect_uri=https://oauth-test-client.net" + "&idp=shibboleth" + "&shib_idp=uchicago.edu" + ) + + assert response.status_code == 302 + location = response.headers["Location"] + qs = parse_qs(urlparse(location).query) + assert "shib_idp" in qs + assert qs["shib_idp"][0] == "uchicago.edu" + + +# --------------------------------------------------------------------------- +# revoke_token — BlacklistingInvalidTokenError path +# --------------------------------------------------------------------------- + + +def test_revoke_token_blacklisting_invalid_token_returns_200(client, oauth_test_client): + """ + When revoking a token that is already invalid, BlacklistingInvalidTokenError + is caught and a 200 is returned (per RFC 7009). + Covers lines 405-416. + """ + # Get a real refresh token first + oauth_test_client.authorize(data={"confirm": "yes"}) + oauth_test_client.token() + refresh_token = oauth_test_client.token_response.refresh_token + assert refresh_token, "Expected a refresh token" + + # Patch blacklist_encoded_token to raise BlacklistingInvalidTokenError + with patch( + "fence.blueprints.oauth2.blacklist_encoded_token", + side_effect=BlacklistingInvalidTokenError("token invalid"), + ): + # Use form-encoded revoke (user-style, not client Basic-auth-style) + # This follows the non-Basic-auth path in revoke_token + # First, we need a valid Authorization header with the token itself + # The user path requires Content-Type: application/x-www-form-urlencoded + # and a 'token' field. + # We also need a revoker token — use the refresh token as revoker too, + # but mock validate_jwt to return valid claims. + fake_claims = {"sub": "test", "pur": "refresh"} + with patch( + "fence.blueprints.oauth2.validate_jwt", + return_value=fake_claims, + ): + with patch( + "fence.blueprints.oauth2.authz_authorize", + return_value=None, + ): + response = client.post( + "/oauth2/revoke", + headers={ + "Content-Type": "application/x-www-form-urlencoded", + "Authorization": f"Bearer {refresh_token}", + }, + data={"token": refresh_token}, + ) + + assert response.status_code == 200 diff --git a/tests/oidc/test_oidc_code_grant.py b/tests/oidc/test_oidc_code_grant.py new file mode 100644 index 000000000..d19525a48 --- /dev/null +++ b/tests/oidc/test_oidc_code_grant.py @@ -0,0 +1,397 @@ +""" +Tests for fence/oidc/grants/oidc_code_grant.py + +Covers: +- create_authorization_code() — PKCE params persistence +- exists_nonce() — nonce collision detection +- validate_request_prompt() — all branch variants +- validate_token_request() — missing code, wrong grant type, missing PKCE verifier +- _validate_code_verifier() — S256, plain, mismatch, unsupported method +""" + +import hashlib +import base64 +from unittest.mock import MagicMock, patch + +import pytest + +from authlib.oidc.core.errors import ( + AccountSelectionRequiredError, + ConsentRequiredError, + LoginRequiredError, +) +from authlib.oauth2.rfc6749 import ( + InvalidGrantError, + InvalidRequestError, +) + +from fence.models import AuthorizationCode, Client, User +from fence.oidc.grants.oidc_code_grant import AuthorizationCodeGrant + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _make_grant_with_prompt(prompt_value): + """Return a bare AuthorizationCodeGrant with a minimal mocked request.""" + grant = AuthorizationCodeGrant.__new__(AuthorizationCodeGrant) + grant.request = MagicMock() + data = {} + if prompt_value is not None: + data["prompt"] = prompt_value + grant.request.payload.data = data + return grant + + +def _s256_challenge(verifier: str) -> str: + digest = hashlib.sha256(verifier.encode("utf-8")).digest() + return base64.urlsafe_b64encode(digest).decode("utf-8").rstrip("=") + + +def _bare_grant(): + """Return a bare AuthorizationCodeGrant for testing internal methods.""" + return AuthorizationCodeGrant.__new__(AuthorizationCodeGrant) + + +# --------------------------------------------------------------------------- +# create_authorization_code (static method — direct call) +# --------------------------------------------------------------------------- + + +def test_create_authorization_code_without_pkce(app, db_session, oauth_user): + """ + create_authorization_code stores an AuthorizationCode with no code_challenge + when no PKCE params are present in request.data. + """ + user = db_session.query(User).filter_by(id=oauth_user.user_id).first() + client = MagicMock() + client.client_id = "test-client" + request = MagicMock() + request.data = {"nonce": "testnonce"} + request.redirect_uri = "https://example.com/callback" + request.scope = "openid" + + with app.test_request_context("/"): + code_str = AuthorizationCodeGrant.create_authorization_code( + client, user, request + ) + + assert code_str is not None and len(code_str) > 0 + + # Verify the row in the DB + stored = db_session.query(AuthorizationCode).filter_by(code=code_str).first() + assert stored is not None + assert stored.code_challenge is None + assert stored.nonce == "testnonce" + + +def test_create_authorization_code_with_pkce_s256(app, db_session, oauth_user): + """ + create_authorization_code persists code_challenge and code_challenge_method + when PKCE S256 params are present in request.data. + """ + user = db_session.query(User).filter_by(id=oauth_user.user_id).first() + client = MagicMock() + client.client_id = "test-client" + challenge = ( + "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM" # pragma: allowlist secret + ) + request = MagicMock() + request.data = { + "code_challenge": challenge, + "code_challenge_method": "S256", + } + request.redirect_uri = "https://example.com/callback" + request.scope = "openid" + + with app.test_request_context("/"): + code_str = AuthorizationCodeGrant.create_authorization_code( + client, user, request + ) + + assert code_str is not None + + stored = db_session.query(AuthorizationCode).filter_by(code=code_str).first() + assert stored is not None + assert stored.code_challenge == challenge + assert stored.code_challenge_method == "S256" + + +# --------------------------------------------------------------------------- +# exists_nonce +# --------------------------------------------------------------------------- + + +def test_exists_nonce_returns_true_when_nonce_exists(app, db_session, oauth_user): + """ + exists_nonce returns True when an AuthorizationCode with that nonce is in the DB. + """ + user = db_session.query(User).filter_by(id=oauth_user.user_id).first() + nonce_val = "test-nonce-unique-abc123" + code = AuthorizationCode( + code="dummy-code-for-nonce-test", + client_id="test-client", + redirect_uri="https://example.com", + scope="openid", + user_id=user.id, + nonce=nonce_val, + ) + db_session.add(code) + db_session.commit() + + grant = _bare_grant() + request = MagicMock() + + with app.test_request_context("/"): + result = grant.exists_nonce(nonce_val, request) + + assert result is True + + +def test_exists_nonce_returns_false_when_nonce_absent(app, db_session): + """ + exists_nonce returns False when no AuthorizationCode with the given nonce exists. + """ + grant = _bare_grant() + request = MagicMock() + + with app.test_request_context("/"): + result = grant.exists_nonce("nonce-that-has-never-been-used", request) + + assert result is False + + +# --------------------------------------------------------------------------- +# validate_request_prompt +# --------------------------------------------------------------------------- + + +def test_validate_request_prompt_no_prompt_no_user_sets_login_prompt(): + """ + When no prompt is provided and end_user is None, self.prompt is set to 'login'. + Covers line 218. + """ + grant = _make_grant_with_prompt(None) + grant.validate_request_prompt(end_user=None, redirect_uri="https://example.com") + assert grant.prompt == "login" + + +def test_validate_request_prompt_no_prompt_with_user_does_not_force_login(): + """ + When no prompt is provided and end_user is present, prompt is not forced to 'login'. + """ + grant = _make_grant_with_prompt(None) + result = grant.validate_request_prompt( + end_user=MagicMock(), redirect_uri="https://example.com" + ) + assert result is grant + # self.prompt should not be set to 'login' + assert getattr(grant, "prompt", None) != "login" + + +def test_validate_request_prompt_none_no_user_raises_login_required(): + """ + prompt='none' with no authenticated user raises LoginRequiredError. + Covers line 222. + """ + grant = _make_grant_with_prompt("none") + with pytest.raises(LoginRequiredError): + grant.validate_request_prompt(end_user=None, redirect_uri="https://example.com") + + +def test_validate_request_prompt_none_combined_with_others_raises_error(): + """ + prompt='none consent' raises an error because 'none' must not be combined + with any other value. The production code raises InvalidRequestError, but + newer authlib versions also raise ValueError when the description contains + forbidden quote characters. Either confirms the branch executed. + Covers line 228. + """ + grant = _make_grant_with_prompt("none consent") + # end_user provided so the 'none + no user' branch is skipped + with pytest.raises((InvalidRequestError, ValueError)): + grant.validate_request_prompt( + end_user=MagicMock(), redirect_uri="https://example.com" + ) + + +def test_validate_request_prompt_consent_no_user_raises_consent_required(): + """ + prompt='consent' with no authenticated user raises ConsentRequiredError. + Covers lines 232-234. + """ + grant = _make_grant_with_prompt("consent") + with pytest.raises(ConsentRequiredError): + grant.validate_request_prompt(end_user=None, redirect_uri="https://example.com") + + +def test_validate_request_prompt_select_account_no_user_raises_account_selection_required(): + """ + prompt='select_account' with no authenticated user raises AccountSelectionRequiredError. + Covers lines 236-238. + """ + grant = _make_grant_with_prompt("select_account") + with pytest.raises(AccountSelectionRequiredError): + grant.validate_request_prompt(end_user=None, redirect_uri="https://example.com") + + +# --------------------------------------------------------------------------- +# validate_token_request — integration tests via the HTTP token endpoint +# --------------------------------------------------------------------------- + + +def test_token_request_missing_code(oauth_test_client): + """ + A token request that omits the 'code' parameter triggers the missing-code + branch (line 270). The production code raises InvalidRequestError with a + description that contains quote characters; in newer authlib this causes a + ValueError that is not caught, so the response is a 4xx or 5xx — not 200. + Covers line 270. + """ + oauth_test_client.authorize(data={"confirm": "yes"}) + + response = oauth_test_client._client.post( + "/oauth2/token", + headers=oauth_test_client._auth_header, + data={ + "client_id": oauth_test_client.client_id, + "client_secret": oauth_test_client.client_secret, + "grant_type": "authorization_code", + "redirect_uri": oauth_test_client.url, + # deliberately no "code" + }, + ) + # The request must not succeed: accepts 400 (handled OAuth2Error) or 500 + # (unhandled ValueError from authlib description-character validation). + assert response.status_code != 200 + + +def test_token_request_pkce_challenge_without_verifier(oauth_test_client): + """ + When authorization was done with a code_challenge but the token request + omits code_verifier, a 400 invalid_grant error is returned. + Covers lines 291-293. + """ + # Authorize with PKCE challenge + verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" # pragma: allowlist secret + challenge = _s256_challenge(verifier) + + oauth_test_client.authorize( + data={ + "confirm": "yes", + "code_challenge": challenge, + "code_challenge_method": "S256", + } + ) + code = oauth_test_client.authorize_response.code + assert code, "Expected an authorization code from the authorize endpoint" + code_val = code[0] if isinstance(code, list) else code + + # Token request without code_verifier + response = oauth_test_client._client.post( + "/oauth2/token", + headers=oauth_test_client._auth_header, + data={ + "client_id": oauth_test_client.client_id, + "client_secret": oauth_test_client.client_secret, + "grant_type": "authorization_code", + "code": code_val, + "redirect_uri": oauth_test_client.url, + # deliberately no "code_verifier" + }, + ) + assert response.status_code == 400 + assert response.json.get("error") == "invalid_grant" + + +def test_pkce_s256_full_flow(oauth_test_client): + """ + Full PKCE S256 flow: authorize with code_challenge, token with matching + code_verifier succeeds. + Covers line 298 (_validate_code_verifier call path for valid verifier). + """ + verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" # pragma: allowlist secret + challenge = _s256_challenge(verifier) + + oauth_test_client.authorize( + data={ + "confirm": "yes", + "code_challenge": challenge, + "code_challenge_method": "S256", + } + ) + code = oauth_test_client.authorize_response.code + assert code, "Expected authorization code" + code_val = code[0] if isinstance(code, list) else code + + token_response = oauth_test_client.token( + code=code_val, + data={"code_verifier": verifier}, + do_asserts=False, + ) + assert token_response.response.status_code == 200 + assert "access_token" in token_response.response.json + + +# --------------------------------------------------------------------------- +# _validate_code_verifier — direct unit tests +# --------------------------------------------------------------------------- + + +def test_validate_code_verifier_s256_matching_verifier_passes(): + """ + A correct S256 code_verifier successfully validates against its challenge. + Covers lines 329-334 (S256 branch, matching). + """ + verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" # pragma: allowlist secret + challenge = _s256_challenge(verifier) + grant = _bare_grant() + # Should not raise + grant._validate_code_verifier(verifier, challenge, "S256") + + +def test_validate_code_verifier_s256_wrong_verifier_raises_invalid_grant(): + """ + An S256 code_verifier that does not match the stored challenge raises InvalidGrantError. + Covers lines 345-347. + """ + grant = _bare_grant() + with pytest.raises(InvalidGrantError): + grant._validate_code_verifier( + "wrong-verifier", + "definitely-not-matching-challenge", + "S256", + ) + + +def test_validate_code_verifier_plain_matching_verifier_passes(): + """ + A correct 'plain' code_verifier (equals the challenge) passes validation. + Covers lines 335-336. + """ + secret = "plain-verifier-value" + grant = _bare_grant() + # Should not raise + grant._validate_code_verifier(secret, secret, "plain") + + +def test_validate_code_verifier_plain_wrong_verifier_raises_invalid_grant(): + """ + A 'plain' code_verifier that does not equal the challenge raises InvalidGrantError. + Covers lines 345-347 (plain branch mismatch). + """ + grant = _bare_grant() + with pytest.raises(InvalidGrantError): + grant._validate_code_verifier("wrong", "right", "plain") + + +def test_validate_code_verifier_unsupported_method_raises_invalid_grant(): + """ + An unsupported code_challenge_method raises InvalidGrantError. + Covers lines 337-342. + """ + grant = _bare_grant() + with pytest.raises(InvalidGrantError): + grant._validate_code_verifier("any-verifier", "any-challenge", "SHA512") diff --git a/tests/oidc/test_private_key_jwt.py b/tests/oidc/test_private_key_jwt.py new file mode 100644 index 000000000..c3262b074 --- /dev/null +++ b/tests/oidc/test_private_key_jwt.py @@ -0,0 +1,149 @@ +""" +Tests for fence.oidc.private_key_jwt (FencePrivateKeyJWT). + +Covers create_claims_options() and resolve_client_public_key() for all key +lookup branches: no JWKS, matching kid, kid not found, no kid, empty key set. +""" + +import pytest +from unittest.mock import MagicMock + +from authlib.jose import JsonWebKey +from authlib.oauth2.rfc6749 import InvalidClientError +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import rsa + +from fence.config import config +from fence.oidc.private_key_jwt import FencePrivateKeyJWT + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _generate_rsa_jwks(kid=None): + """Return (private_key, jwks_dict) with an optionally-tagged public JWK.""" + private_key = rsa.generate_private_key( + public_exponent=65537, + key_size=2048, + backend=default_backend(), + ) + pub_pem = private_key.public_key().public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo, + ) + jwk_obj = JsonWebKey.import_key(pub_pem) + jwk_dict = dict(jwk_obj) + if kid: + jwk_dict["kid"] = kid + return private_key, {"keys": [jwk_dict]} + + +def _client_with_jwks(jwks_value): + """Return a mock Client whose .jwks attribute returns jwks_value.""" + client = MagicMock() + type(client).jwks = property(lambda self: jwks_value) + return client + + +# --------------------------------------------------------------------------- +# create_claims_options() tests +# --------------------------------------------------------------------------- + + +def test_create_claims_options_has_required_keys(app): + """create_claims_options returns dict with iss, sub, aud, exp keys.""" + handler = FencePrivateKeyJWT() + opts = handler.create_claims_options() + assert "iss" in opts + assert "sub" in opts + assert "aud" in opts + assert "exp" in opts + + +def test_create_claims_options_aud_value_is_token_endpoint(app): + """aud.value must point to the /oauth2/token endpoint.""" + handler = FencePrivateKeyJWT() + opts = handler.create_claims_options() + expected = config["BASE_URL"].rstrip("/") + "/oauth2/token" + assert opts["aud"]["value"] == expected + + +def test_create_claims_options_aud_is_essential(app): + """aud is marked essential.""" + handler = FencePrivateKeyJWT() + opts = handler.create_claims_options() + assert opts["aud"].get("essential") is True + + +def test_create_claims_options_exp_is_essential(app): + """exp is marked essential.""" + handler = FencePrivateKeyJWT() + opts = handler.create_claims_options() + assert opts["exp"].get("essential") is True + + +def test_create_claims_options_iss_validator_matches_sub(app): + """iss validator accepts iss == sub.""" + handler = FencePrivateKeyJWT() + validate = handler.create_claims_options()["iss"]["validate"] + claims = {"iss": "client-id", "sub": "client-id"} + assert validate(claims, "client-id") is True + + +def test_create_claims_options_iss_validator_rejects_iss_ne_sub(app): + """iss validator rejects iss != sub (RFC 7523 §3).""" + handler = FencePrivateKeyJWT() + validate = handler.create_claims_options()["iss"]["validate"] + claims = {"iss": "client-id", "sub": "other-id"} + assert validate(claims, "client-id") is False + + +# --------------------------------------------------------------------------- +# resolve_client_public_key() tests +# --------------------------------------------------------------------------- + + +def test_resolve_public_key_no_jwks_raises(app): + """Client with no registered JWKS raises InvalidClientError.""" + handler = FencePrivateKeyJWT() + client = _client_with_jwks([]) # empty list → falsy + with pytest.raises(InvalidClientError): + handler.resolve_client_public_key(client, {}) + + +def test_resolve_public_key_matching_kid_returns_key(app): + """Returns the key when the kid in the JWT header matches one in JWKS.""" + _, jwks = _generate_rsa_jwks(kid="signing-key") + handler = FencePrivateKeyJWT() + client = _client_with_jwks(jwks) + key = handler.resolve_client_public_key(client, {"kid": "signing-key"}) + assert key is not None + + +def test_resolve_public_key_unknown_kid_raises(app): + """kid in header not found in JWKS raises InvalidClientError.""" + _, jwks = _generate_rsa_jwks(kid="signing-key") + handler = FencePrivateKeyJWT() + client = _client_with_jwks(jwks) + with pytest.raises(InvalidClientError): + handler.resolve_client_public_key(client, {"kid": "unknown-kid"}) + + +def test_resolve_public_key_no_kid_returns_first_key(app): + """No kid in header returns the first key from the JWKS.""" + _, jwks = _generate_rsa_jwks(kid="signing-key") + handler = FencePrivateKeyJWT() + client = _client_with_jwks(jwks) + key = handler.resolve_client_public_key(client, {}) + assert key is not None + + +def test_resolve_public_key_empty_keys_array_raises(app): + """JWKS dict with an empty keys array raises InvalidClientError.""" + handler = FencePrivateKeyJWT() + client = _client_with_jwks({"keys": []}) + with pytest.raises(InvalidClientError): + handler.resolve_client_public_key(client, {}) From 359899bdcf8927a8da685d98b6cfe2bed0a4d391 Mon Sep 17 00:00:00 2001 From: vincentvanhees Date: Tue, 13 Oct 2026 18:00:07 +0200 Subject: [PATCH 12/14] fix(oidc): advertise introspection, revocation and private_key_jwt in OIDC discovery Add introspection_endpoint (RFC 7662), revocation_endpoint (RFC 7009), and private_key_jwt to token_endpoint_auth_methods_supported in the /.well-known/openid-configuration response. These were missing because the SMART/US Core compliance work added the underlying endpoints but did not update the discovery document. Clients that auto-discover fence capabilities via OIDC discovery (rather than via /.well-known/smart-configuration) would not know these endpoints exist. This is particularly relevant for Backend Services clients checking for private_key_jwt support (CONF-0120). --- fence/blueprints/well_known.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/fence/blueprints/well_known.py b/fence/blueprints/well_known.py index 92a5bb53b..d92388e0a 100644 --- a/fence/blueprints/well_known.py +++ b/fence/blueprints/well_known.py @@ -59,6 +59,8 @@ def openid_configuration(): authorization_endpoint = path_to(flask.url_for("oauth2.authorize")) token_endpoint = path_to(flask.url_for("oauth2.get_token")) userinfo_endpoint = path_to(flask.url_for("user.user_info")) + introspection_endpoint = path_to(flask.url_for("oauth2.introspect_token")) + revocation_endpoint = path_to(flask.url_for("oauth2.revoke_token")) registration_endpoint = None # not yet supported # List all the scopes allowed in OAuth2 requests. @@ -99,7 +101,12 @@ def openid_configuration(): "request_object_signing_alg_values_supported": [], "request_object_encryption_alg_values_supported": [], "request_object_encryption_enc_values_supported": [], - "token_endpoint_auth_methods_supported": [ClientAuthType.basic.value], + "introspection_endpoint": introspection_endpoint, + "revocation_endpoint": revocation_endpoint, + "token_endpoint_auth_methods_supported": [ + ClientAuthType.basic.value, + "private_key_jwt", + ], "display_values_supported": ["page"], "claim_types_supported": ["normal"], "claims_supported": claims_supported, From 0bd6e728a0d006af378c2e47adc264aea96c005c Mon Sep 17 00:00:00 2001 From: vincentvanhees Date: Wed, 14 Oct 2026 16:41:42 +0200 Subject: [PATCH 13/14] fix: add SMART scopes to SCOPE_DESCRIPTION and graceful fallback for unknown scopes --- fence/blueprints/oauth2.py | 2 +- fence/jwt/token.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/fence/blueprints/oauth2.py b/fence/blueprints/oauth2.py index c2b092100..703b53f73 100644 --- a/fence/blueprints/oauth2.py +++ b/fence/blueprints/oauth2.py @@ -282,7 +282,7 @@ def _get_auth_response_for_prompts(prompts, grant, user, client, scope): idp_names.append(idp_name) resource_description = [ - SCOPE_DESCRIPTION[s].format(idp_names=", ".join(idp_names)) + SCOPE_DESCRIPTION.get(s, s).format(idp_names=", ".join(idp_names)) for s in shown_scopes ] diff --git a/fence/jwt/token.py b/fence/jwt/token.py index f73ac1715..adef00517 100644 --- a/fence/jwt/token.py +++ b/fence/jwt/token.py @@ -27,6 +27,11 @@ "google_service_account": "Allow registration of external Google service accounts to access data.", "admin": "View and update user authorizations.", "ga4gh_passport_v1": "Retrieve GA4GH Passports and Visas", + # SMART on FHIR scopes (SMART App Launch 2.0) + "fhirUser": "Know your FHIR user identity.", + "launch": "Launch this app in the context of an EHR session.", + "launch/patient": "Access this app in the context of a specific patient.", + "offline_access": "Allow this app to access data on your behalf even when you are not logged in.", } From b61410b51e71c988e7f3bdae1c1d29225a484609 Mon Sep 17 00:00:00 2001 From: vincentvanhees Date: Wed, 14 Oct 2026 17:38:12 +0200 Subject: [PATCH 14/14] fix: raise session JWT size limit to 8192 to support long FHIR scope redirect URLs --- fence/jwt/token.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/fence/jwt/token.py b/fence/jwt/token.py index adef00517..9280f3dd6 100644 --- a/fence/jwt/token.py +++ b/fence/jwt/token.py @@ -194,9 +194,10 @@ def generate_signed_session_token(kid, private_key, expires_in, context=None): token = jwt.encode(claims, private_key, headers=headers, algorithm="RS256") token = to_unicode(token, "UTF-8") - # Browser may clip cookies larger than 4096 bytes - if len(token) > 4096: - raise JWTSizeError("JWT exceeded 4096 bytes") + # Browser may clip cookies larger than 4096 bytes; allow up to 8192 + # to accommodate long OAuth2 redirect URLs with many FHIR scopes + if len(token) > 8192: + raise JWTSizeError("JWT exceeded 8192 bytes") return JWTResult(token=token, kid=kid, claims=claims)