From 5f226c3662a38ff577fcc20078984eae6b7ff20f Mon Sep 17 00:00:00 2001 From: Gavin Chait Date: Thu, 20 Nov 2025 10:28:32 +0100 Subject: [PATCH 01/13] Updated HTTP structured field dependency `http-sfv` is deprecated in favour of `http-sf`. Replaced accordingly. --- poetry.lock | 42 ++++++++++++++------ pyproject.toml | 3 +- src/open_payments_sdk/gnap_utils/security.py | 3 +- 3 files changed, 34 insertions(+), 14 deletions(-) diff --git a/poetry.lock b/poetry.lock index f5c124b..8f53c38 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. [[package]] name = "annotated-types" @@ -32,7 +32,7 @@ typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} [package.extras] doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] -test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\" and python_version < \"3.14\""] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"] trio = ["trio (>=0.26.1)"] [[package]] @@ -285,10 +285,10 @@ files = [ cffi = {version = ">=1.14", markers = "platform_python_implementation != \"PyPy\""} [package.extras] -docs = ["sphinx (>=5.3.0)", "sphinx-inline-tabs ; python_full_version >= \"3.8.0\"", "sphinx-rtd-theme (>=3.0.0) ; python_full_version >= \"3.8.0\""] +docs = ["sphinx (>=5.3.0)", "sphinx-inline-tabs", "sphinx-rtd-theme (>=3.0.0)"] docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"] -nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2) ; python_full_version >= \"3.8.0\""] -pep8test = ["check-sdist ; python_full_version >= \"3.8.0\"", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"] +nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2)"] +pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"] sdist = ["build (>=1.0.0)"] ssh = ["bcrypt (>=3.1.5)"] test = ["certifi (>=2024)", "cryptography-vectors (==45.0.4)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] @@ -344,7 +344,7 @@ description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" groups = ["main", "dev"] -markers = "python_version == \"3.10\"" +markers = "python_version < \"3.11\"" files = [ {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, @@ -369,7 +369,7 @@ files = [ ] [package.extras] -tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich ; python_version >= \"3.11\""] +tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich"] [[package]] name = "genson" @@ -414,6 +414,24 @@ http-sfv = ">=0.9.3" [package.extras] tests = ["build", "coverage", "flake8", "mypy", "requests", "ruff", "wheel"] +[[package]] +name = "http-sf" +version = "1.0.4" +description = "Parse and serialise HTTP Structured Fields" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "http_sf-1.0.4-py3-none-any.whl", hash = "sha256:5f2d2feb70c383d4e3640181080f450857c1f3ddc07fb6f3d0ec22add12c04a9"}, + {file = "http_sf-1.0.4.tar.gz", hash = "sha256:1c3ff40dc12ea913a604d667180b2cf78c12bdaf348b7649a5129583643cc847"}, +] + +[package.dependencies] +typing_extensions = "*" + +[package.extras] +dev = ["black", "build", "mypy", "pylint", "pytest", "pytest-md", "validate-pyproject"] + [[package]] name = "http-sfv" version = "0.9.9" @@ -473,7 +491,7 @@ httpcore = "==1.*" idna = "*" [package.extras] -brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] +brotli = ["brotli", "brotlicffi"] cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] @@ -508,7 +526,7 @@ files = [ [package.extras] docs = ["jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx"] -testing = ["pygments", "pytest (>=6)", "pytest-black (>=0.3.7) ; platform_python_implementation != \"PyPy\"", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1) ; platform_python_implementation != \"PyPy\""] +testing = ["pygments", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] [[package]] name = "iniconfig" @@ -567,7 +585,7 @@ typing_extensions = {version = ">=4.6", markers = "python_version < \"3.12\""} [package.extras] all = ["ipython[black,doc,kernel,matplotlib,nbconvert,nbformat,notebook,parallel,qtconsole]", "ipython[test,test-extra]"] black = ["black"] -doc = ["docrepr", "exceptiongroup", "intersphinx_registry", "ipykernel", "ipython[test]", "matplotlib", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "sphinxcontrib-jquery", "tomli ; python_version < \"3.11\"", "typing_extensions"] +doc = ["docrepr", "exceptiongroup", "intersphinx_registry", "ipykernel", "ipython[test]", "matplotlib", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "sphinxcontrib-jquery", "tomli", "typing_extensions"] kernel = ["ipykernel"] matplotlib = ["matplotlib"] nbconvert = ["nbconvert"] @@ -894,7 +912,7 @@ typing-extensions = ">=4.12.2" [package.extras] email = ["email-validator (>=2.0.0)"] -timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] +timezone = ["tzdata"] [[package]] name = "pydantic-core" @@ -1240,4 +1258,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = ">=3.10" -content-hash = "9dde0f8e1090fb17c60fe4ae8de9749f3a21819dbaa747bba43daa10099db2c6" +content-hash = "568b0d6bd032d3b8b127fd04ba100d13a56e4745d12f75673cf41d1bf5228ce9" diff --git a/pyproject.toml b/pyproject.toml index 3a5fd16..4900c46 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,8 @@ dependencies = [ "httpx (>=0.28.1,<0.29.0)", "pydantic (>=2.10.6,<3.0.0)", "cryptography (>=45.0.4,<46.0.0)", - "http-message-signatures (>=0.6.1,<0.7.0)" + "http-message-signatures (>=0.6.1,<0.7.0)", + "http-sf (>=1.0.4,<1.1.0)" ] [build-system] diff --git a/src/open_payments_sdk/gnap_utils/security.py b/src/open_payments_sdk/gnap_utils/security.py index 445c062..ab648c8 100644 --- a/src/open_payments_sdk/gnap_utils/security.py +++ b/src/open_payments_sdk/gnap_utils/security.py @@ -7,6 +7,7 @@ from typing import Sequence from http_message_signatures import HTTPMessageSigner, algorithms import http_sfv +from http_sf import ser from httpx import Request from open_payments_sdk.gnap_utils.hash import HashManager from open_payments_sdk.gnap_utils.http_signatures import OPKeyResolver, PatchedHTTPSignatureComponentResolver @@ -49,6 +50,6 @@ def set_content_digest(self, request: Request) -> Request: """ Compute Digest """ - request.headers["Content-Digest"] = str(http_sfv.Dictionary({"sha-512": hashlib.sha512(request.content).digest()})) + request.headers["Content-Digest"] = ser({"sha-512": hashlib.sha512(request.content).digest()}) return request \ No newline at end of file From 840080f6ca5840eb7ee848f8f63efb9d0d67b305 Mon Sep 17 00:00:00 2001 From: Gavin Chait Date: Thu, 20 Nov 2025 10:30:03 +0100 Subject: [PATCH 02/13] Pydantic `classmethod` fix `classmethod` must return the value holding class data. --- src/open_payments_sdk/models/resource.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/open_payments_sdk/models/resource.py b/src/open_payments_sdk/models/resource.py index 278193f..c616e5f 100644 --- a/src/open_payments_sdk/models/resource.py +++ b/src/open_payments_sdk/models/resource.py @@ -333,6 +333,7 @@ class QuoteRequestBase(BaseModel): @classmethod def check_path(cls, v): assert "/incoming-payments/" in v.path + return v class QuoteFixedReceive(QuoteRequestBase): From d5fd01e2fb6439c6871b42439fc5427f67bec2bd Mon Sep 17 00:00:00 2001 From: Gavin Chait Date: Thu, 20 Nov 2025 10:33:11 +0100 Subject: [PATCH 03/13] Fixed outgoing response model validation --- src/open_payments_sdk/api/resource.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/open_payments_sdk/api/resource.py b/src/open_payments_sdk/api/resource.py index 732b6b8..55828cf 100644 --- a/src/open_payments_sdk/api/resource.py +++ b/src/open_payments_sdk/api/resource.py @@ -179,6 +179,10 @@ def get_outgoing_payments( ) response = request = self.sign_request(request,("authorization",*get_default_covered_components())) self.http_client.send(request=request) + req_headers = {**self.get_auth_header(access_token=access_token)} + request = self.http_client.build_request(method="GET", url=url, headers=req_headers, params=query_params) + request = self.sign_request(request, ("authorization", *get_default_covered_components())) + response = self.http_client.send(request=request) return PaginatedOutgoingPayments.model_validate(response.json()) def get_outgoing_payment( From 4e0c8a04dbb1b0808f03520b38542641b19df4ac Mon Sep 17 00:00:00 2001 From: Gavin Chait Date: Thu, 20 Nov 2025 10:38:07 +0100 Subject: [PATCH 04/13] Fixed Python 2 class format --- src/open_payments_sdk/gnap_utils/security.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/open_payments_sdk/gnap_utils/security.py b/src/open_payments_sdk/gnap_utils/security.py index ab648c8..eaf12c7 100644 --- a/src/open_payments_sdk/gnap_utils/security.py +++ b/src/open_payments_sdk/gnap_utils/security.py @@ -14,7 +14,7 @@ from open_payments_sdk.gnap_utils.keys import KeyManager -class SecurityBase(): +class SecurityBase: """ Base class to provide shared functionality for making authenticated requests """ From f6ba227fdc7566c27d8b8eda70f8782405de6e6e Mon Sep 17 00:00:00 2001 From: Gavin Chait Date: Thu, 20 Nov 2025 10:40:43 +0100 Subject: [PATCH 05/13] Fixed Pydantic field constraints --- src/open_payments_sdk/models/auth.py | 6 ++++-- src/open_payments_sdk/models/resource.py | 23 ++++++++++++++--------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/open_payments_sdk/models/auth.py b/src/open_payments_sdk/models/auth.py index e5da46a..67a7cd9 100644 --- a/src/open_payments_sdk/models/auth.py +++ b/src/open_payments_sdk/models/auth.py @@ -1,7 +1,7 @@ from enum import Enum from typing import Any, List, Optional, Union -from pydantic import AnyUrl, BaseModel, ConfigDict, Field, RootModel, conint, model_validator, root_validator +from pydantic import AnyUrl, BaseModel, ConfigDict, Field, RootModel, conint, model_validator class TypeIncoming(Enum): @@ -165,10 +165,12 @@ class AssetCode(RootModel[str]): class AssetScale(RootModel[conint(ge=0, le=255)]): - root: conint(ge=0, le=255) = Field( + root: int = Field( ..., description="The scale of amounts denoted in the corresponding asset code.", title="Asset scale", + ge=0, + le=255 ) diff --git a/src/open_payments_sdk/models/resource.py b/src/open_payments_sdk/models/resource.py index c616e5f..06aa306 100644 --- a/src/open_payments_sdk/models/resource.py +++ b/src/open_payments_sdk/models/resource.py @@ -1,6 +1,6 @@ from datetime import datetime from enum import Enum -from typing import Annotated, Any, Dict, List, Literal, Optional, Union +from typing import Any, Dict, List, Literal, Optional, Union from pydantic import (AnyUrl, BaseModel, ConfigDict, Field, HttpUrl, RootModel, StringConstraints, conint, constr, field_validator) @@ -15,10 +15,12 @@ class AssetCode(RootModel[str]): class AssetScale(RootModel[conint(ge=0, le=255)]): - root: conint(ge=0, le=255) = Field( + root: int = Field( ..., description="The scale of amounts denoted in the corresponding asset code.", title="Asset scale", + ge=0, + le=255 ) @@ -47,11 +49,11 @@ class PageInfo(BaseModel): model_config = ConfigDict( extra="forbid", ) - startCursor: Optional[constr(min_length=1)] = Field( + startCursor: Optional[str] = Field( None, description="Cursor corresponding to the first element in the result array.", ) - endCursor: Optional[constr(min_length=1)] = Field( + endCursor: Optional[str] = Field( None, description="Cursor corresponding to the last element in the result array.", ) @@ -72,19 +74,22 @@ class Type(Enum): class IlpPaymentMethod(BaseModel): - model_config = ConfigDict( - extra="forbid", - ) type: Type - ilpAddress: constr( + ilpAddress: str = Field( + ..., + description="The ILP address to use when establishing a STREAM connection.", pattern=r"^(g|private|example|peer|self|test[1-3]?|local)([.][a-zA-Z0-9_~-]+)+$", max_length=1023, ) = Field( ..., description="The ILP address to use when establishing a STREAM connection." ) - sharedSecret: constr(pattern=r"^[a-zA-Z0-9-_]+$") = Field( + sharedSecret: str = Field( ..., description="The base64 url-encoded shared secret to use when establishing a STREAM connection.", + pattern=r"^[a-zA-Z0-9-_]+$" + ) + model_config = ConfigDict( + extra="forbid", ) From 7138e17ea412e147c1da4b1a6ef4636ed3eb8dd2 Mon Sep 17 00:00:00 2001 From: Gavin Chait Date: Thu, 20 Nov 2025 10:44:14 +0100 Subject: [PATCH 06/13] Fixed Pydantic OpenPayments schema validation Optional fields are optional and not always returned. --- src/open_payments_sdk/models/auth.py | 2 +- src/open_payments_sdk/models/resource.py | 23 +++++++++-------------- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/src/open_payments_sdk/models/auth.py b/src/open_payments_sdk/models/auth.py index 67a7cd9..1f5f107 100644 --- a/src/open_payments_sdk/models/auth.py +++ b/src/open_payments_sdk/models/auth.py @@ -295,7 +295,7 @@ class GrantRequestAccessToken(BaseModel): class GrantRequest(BaseModel): access_token: GrantRequestAccessToken - client: Client + client: Optional[Client] = None interact: Optional[InteractRequest] = None class ReservedKeyMappingModel(BaseModel): diff --git a/src/open_payments_sdk/models/resource.py b/src/open_payments_sdk/models/resource.py index 06aa306..833c755 100644 --- a/src/open_payments_sdk/models/resource.py +++ b/src/open_payments_sdk/models/resource.py @@ -52,10 +52,12 @@ class PageInfo(BaseModel): startCursor: Optional[str] = Field( None, description="Cursor corresponding to the first element in the result array.", + min_length=1 ) endCursor: Optional[str] = Field( None, description="Cursor corresponding to the last element in the result array.", + min_length=1 ) hasNextPage: bool = Field( ..., description="Describes whether the data set has further entries." @@ -145,12 +147,9 @@ class OutgoingPayment(BaseModel): None, description="Additional metadata associated with the outgoing payment. (Optional)", ) - createdAt: datetime = Field( - ..., description="The date and time when the outgoing payment was created." - ) - updatedAt: datetime = Field( - ..., description="The date and time when the outgoing payment was updated." - ) + createdAt: datetime = Field(..., description="The date and time when the outgoing payment was created.") + updatedAt: Optional[datetime] = Field(None, description="The date and time when the outgoing payment was updated.") + model_config = ConfigDict() class OutgoingPaymentWithSpentAmounts(BaseModel): @@ -258,12 +257,8 @@ class IncomingPayment(BaseModel): None, description="Additional metadata associated with the incoming payment. (Optional)", ) - createdAt: datetime = Field( - ..., description="The date and time when the incoming payment was created." - ) - updatedAt: datetime = Field( - ..., description="The date and time when the incoming payment was updated." - ) + createdAt: datetime = Field(..., description="The date and time when the incoming payment was created.") + updatedAt: Optional[datetime] = Field(None, description="The date and time when the incoming payment was updated.") class IncomingPaymentWithMethods(IncomingPayment): @@ -277,8 +272,8 @@ class IncomingPaymentWithMethods(IncomingPayment): class IncomingPaymentRequest(BaseModel): walletAddress: WalletAddress incomingAmount: Optional[Amount] - expiresAt: Optional[datetime] - metadata: Optional[Dict[str, Any]] + expiresAt: Optional[datetime] = None + metadata: Optional[Dict[str, Any]] = None class IncomingPaymentResponse( From 371623a8d10c7feaba7408cfb949a85e029458a7 Mon Sep 17 00:00:00 2001 From: Gavin Chait Date: Thu, 20 Nov 2025 10:46:59 +0100 Subject: [PATCH 07/13] Python Black format conformance These take in a variety of fixes: - Row alignment and spacing - Correct dependency import path Note that this is only the most basic of fixes and much more needs to be done to fix duplicate functions and inefficient models. --- src/open_payments_sdk/api/auth.py | 118 +++------- src/open_payments_sdk/api/resource.py | 217 ++++++------------ src/open_payments_sdk/api/wallet.py | 11 +- src/open_payments_sdk/client/client.py | 40 ++-- .../gnap_utils/http_signatures.py | 12 +- src/open_payments_sdk/gnap_utils/keys.py | 35 ++- src/open_payments_sdk/gnap_utils/security.py | 27 ++- src/open_payments_sdk/models/resource.py | 67 ++---- src/open_payments_sdk/utils/utils.py | 13 +- 9 files changed, 188 insertions(+), 352 deletions(-) diff --git a/src/open_payments_sdk/api/auth.py b/src/open_payments_sdk/api/auth.py index e1c315a..23d2d1a 100644 --- a/src/open_payments_sdk/api/auth.py +++ b/src/open_payments_sdk/api/auth.py @@ -1,147 +1,105 @@ """ Grants Module """ + from logging import Logger from open_payments_sdk.gnap_utils.security import SecurityBase from open_payments_sdk.http import HttpClient from open_payments_sdk.models.auth import AccessToken, Grant -from open_payments_sdk.models.auth import (GrantContinueResponse, GrantRequest, - InteractRef) +from open_payments_sdk.models.auth import GrantContinueResponse, GrantRequest, InteractRef from open_payments_sdk.utils.utils import get_default_covered_components, get_default_headers - class Grants(SecurityBase): """ Class to handle Grants in the sdk """ - def __init__(self, keyid: str, private_key: str ,logger: Logger,http_client: HttpClient): - super().__init__(keyid=keyid, private_key=private_key,logger=logger) + + def __init__(self, keyid: str, private_key: str, logger: Logger, http_client: HttpClient): + super().__init__(keyid=keyid, private_key=private_key, logger=logger) self.logger = logger self.http_client = http_client def post_grant_request( - self, - grant_request: GrantRequest, - auth_server_endpoint: str, - ) -> Grant: + self, + grant_request: GrantRequest, + auth_server_endpoint: str, + ) -> Grant: """ Grant Request """ data = grant_request.model_dump(exclude_unset=True, mode="json") - req_headers = { - **get_default_headers() - } + req_headers = {**get_default_headers()} request = self.http_client.build_request( - method="POST", - url=auth_server_endpoint, - json=data, - headers=req_headers + method="POST", url=auth_server_endpoint, json=data, headers=req_headers ) request = self.set_content_digest(request=request) - request = self.sign_request(request,("content-type","content-digest","content-length",*get_default_covered_components())) + request = self.sign_request( + request, ("content-type", "content-digest", "content-length", *get_default_covered_components()) + ) response = self.http_client.send(request=request) return Grant.model_validate(response.json()) def post_grant_continuation_request( - self, - interact_ref: InteractRef, - continue_uri: str, - access_token: str - ) -> GrantContinueResponse: + self, interact_ref: InteractRef, continue_uri: str, access_token: str + ) -> GrantContinueResponse: """ Continue Grant Request """ data = interact_ref.model_dump(exclude_unset=True, mode="json") - req_headers = { - **get_default_headers(), - **self.get_auth_header(access_token=access_token) - } - request = self.http_client.build_request( - method="POST", - url=continue_uri, - json=data, - headers=req_headers - ) + req_headers = {**get_default_headers(), **self.get_auth_header(access_token=access_token)} + request = self.http_client.build_request(method="POST", url=continue_uri, json=data, headers=req_headers) request = self.set_content_digest(request=request) - request = self.sign_request(request,("content-type","content-digest","content-length","authorization",*get_default_covered_components())) + request = self.sign_request( + request, + ("content-type", "content-digest", "content-length", "authorization", *get_default_covered_components()), + ) response = self.http_client.send(request=request) return GrantContinueResponse.model_validate(response.json()) - def delete_grant( - self, - req_id: str, - auth_server_endpoint: str, - access_token: str - ) -> None: + def delete_grant(self, req_id: str, auth_server_endpoint: str, access_token: str) -> None: """ Delete Grant """ base_url = auth_server_endpoint.rstrip("/") url = f"{base_url}/continue/{req_id}" - req_headers = { - **self.get_auth_header(access_token=access_token) - } - request = self.http_client.build_request( - method="DELETE", - url=url, - headers=req_headers - ) - request = self.sign_request(request,("authorization",*get_default_covered_components())) + req_headers = {**self.get_auth_header(access_token=access_token)} + request = self.http_client.build_request(method="DELETE", url=url, headers=req_headers) + request = self.sign_request(request, ("authorization", *get_default_covered_components())) self.http_client.send(request=request) + class AccessTokens(SecurityBase): """ Access Token Class """ - def __init__(self, keyid: str , private_key: str,logger: Logger, http_client: HttpClient ): + + def __init__(self, keyid: str, private_key: str, logger: Logger, http_client: HttpClient): super().__init__(keyid=keyid, private_key=private_key, logger=logger) self.http_client = http_client - def post_rotate_access_token( - self, - token_id: str, - auth_server_endpoint: str, - access_token: str - ) -> AccessToken: + def post_rotate_access_token(self, token_id: str, auth_server_endpoint: str, access_token: str) -> AccessToken: """ Rotate Access Token """ base_url = auth_server_endpoint.rstrip("/") url = f"{base_url}/token/{token_id}" - req_headers = { - **self.get_auth_header(access_token=access_token) - } - request = self.http_client.build_request( - method="POST", - url=url, - headers=req_headers - ) - request = self.sign_request(request,("authorization",*get_default_covered_components())) + req_headers = {**self.get_auth_header(access_token=access_token)} + request = self.http_client.build_request(method="POST", url=url, headers=req_headers) + request = self.sign_request(request, ("authorization", *get_default_covered_components())) response = self.http_client.send(request=request) return AccessToken.model_validate(response.json()) - def delete_access_token( - self, - token_id: str, - auth_server_endpoint: str, - access_token: str - ) -> None: + def delete_access_token(self, token_id: str, auth_server_endpoint: str, access_token: str) -> None: """ Delete Access Token """ base_url = auth_server_endpoint.rstrip("/") url = f"{base_url}/token/{token_id}" - req_headers = { - **self.get_auth_header(access_token=access_token) - } + req_headers = {**self.get_auth_header(access_token=access_token)} - request = self.http_client.build_request( - method="DELETE", - url=url, - headers=req_headers - ) - request = self.sign_request(request,("authorization",*get_default_covered_components())) - self.http_client.send(request=request) \ No newline at end of file + request = self.http_client.build_request(method="DELETE", url=url, headers=req_headers) + request = self.sign_request(request, ("authorization", *get_default_covered_components())) + self.http_client.send(request=request) diff --git a/src/open_payments_sdk/api/resource.py b/src/open_payments_sdk/api/resource.py index 55828cf..abad064 100644 --- a/src/open_payments_sdk/api/resource.py +++ b/src/open_payments_sdk/api/resource.py @@ -1,18 +1,22 @@ """ Resource Server Module """ + from logging import Logger from open_payments_sdk.gnap_utils.security import SecurityBase from open_payments_sdk.http import HttpClient -from open_payments_sdk.models.resource import (IncomingPayment, - IncomingPaymentRequest, - IncomingPaymentResponse, - OutgoingPayment, - OutgoingPaymentRequest, - PaginatedIncomingPayments, - PaginatedOutgoingPayments, - PaymentListQuery, Quote, - QuoteRequest) +from open_payments_sdk.models.resource import ( + IncomingPayment, + IncomingPaymentRequest, + IncomingPaymentResponse, + OutgoingPayment, + OutgoingPaymentRequest, + PaginatedIncomingPayments, + PaginatedOutgoingPayments, + PaymentListQuery, + Quote, + QuoteRequest, +) from open_payments_sdk.utils.utils import get_default_covered_components, get_default_headers @@ -20,104 +24,70 @@ class IncomingPayments(SecurityBase): """ Class for handling incoming payments resources """ - def __init__(self, keyid: str, private_key: str,logger: Logger, http_client: HttpClient): - super().__init__(keyid=keyid,private_key=private_key,logger=logger) + + def __init__(self, keyid: str, private_key: str, logger: Logger, http_client: HttpClient): + super().__init__(keyid=keyid, private_key=private_key, logger=logger) self.http_client = http_client def post_create_payment( - self, - payment: IncomingPaymentRequest, - resource_server_endpoint: str, - access_token: str - ) -> IncomingPayment: + self, payment: IncomingPaymentRequest, resource_server_endpoint: str, access_token: str + ) -> IncomingPayment: """ Create Incoming Payment """ base_url = resource_server_endpoint.rstrip("/") url = f"{base_url}/incoming-payments" data = payment.model_dump(exclude_unset=True, mode="json") - req_headers = { - **get_default_headers(), - **self.get_auth_header(access_token=access_token) - } - request = self.http_client.build_request( - method="POST", - url=url, - json=data, - headers=req_headers - ) + req_headers = {**get_default_headers(), **self.get_auth_header(access_token=access_token)} + request = self.http_client.build_request(method="POST", url=url, json=data, headers=req_headers) request = self.set_content_digest(request=request) - request = self.sign_request(request,("content-type","content-digest","content-length","authorization",*get_default_covered_components())) + request = self.sign_request( + request, + ("content-type", "content-digest", "content-length", "authorization", *get_default_covered_components()), + ) response = self.http_client.send(request=request) return IncomingPayment.model_validate(response.json()) def get_incoming_payments( - self, query: PaymentListQuery, - resource_server_endpoint: str, - access_token: str - ) -> PaginatedIncomingPayments: + self, query: PaymentListQuery, resource_server_endpoint: str, access_token: str + ) -> PaginatedIncomingPayments: """ Get Incoming Payment """ base_url = resource_server_endpoint.rstrip("/") url = f"{base_url}/incoming-payments" query_params = query.model_dump(exclude_unset=True, mode="json") - req_headers = { - **self.get_auth_header(access_token=access_token) - } - request = self.http_client.build_request( - method="GET", - url=url, - headers=req_headers, - params=query_params - ) - request = self.sign_request(request,("authorization",*get_default_covered_components())) + req_headers = {**self.get_auth_header(access_token=access_token)} + request = self.http_client.build_request(method="GET", url=url, headers=req_headers, params=query_params) + request = self.sign_request(request, ("authorization", *get_default_covered_components())) response = self.http_client.send(request=request) return PaginatedIncomingPayments.model_validate(response.json()) def get_incoming_payment( - self, - payment_id: str, - resource_server_endpoint: str, - access_token: str - ) -> IncomingPayment: + self, payment_id: str, resource_server_endpoint: str, access_token: str + ) -> IncomingPaymentResponse: """ Get Incoming Payment """ base_url = resource_server_endpoint.rstrip("/") url = f"{base_url}/incoming-payments/{payment_id}" - req_headers = { - **self.get_auth_header(access_token=access_token) - } - request = self.http_client.build_request( - method="GET", - url=url, - headers=req_headers - ) - request = self.sign_request(request,("authorization",*get_default_covered_components())) + req_headers = {**self.get_auth_header(access_token=access_token)} + request = self.http_client.build_request(method="GET", url=url, headers=req_headers) + request = self.sign_request(request, ("authorization", *get_default_covered_components())) response = self.http_client.send(request=request) return IncomingPaymentResponse.model_validate(response.json()) def post_complete_incoming_payment( - self, - payment_id: str, - resource_server_endpoint: str, - access_token: str - ) -> IncomingPayment: + self, payment_id: str, resource_server_endpoint: str, access_token: str + ) -> IncomingPayment: """ Complete Incoming Payment """ base_url = resource_server_endpoint.rstrip("/") url = f"{base_url}/incoming-payments/{payment_id}/complete" - req_headers = { - **self.get_auth_header(access_token=access_token) - } - request = self.http_client.build_request( - method="POST", - url=url, - headers=req_headers - ) - request = self.sign_request(request,("authorization",*get_default_covered_components())) + req_headers = {**self.get_auth_header(access_token=access_token)} + request = self.http_client.build_request(method="POST", url=url, headers=req_headers) + request = self.sign_request(request, ("authorization", *get_default_covered_components())) response = self.http_client.send(request=request) return IncomingPayment.model_validate(response.json()) @@ -126,41 +96,32 @@ class OutgoingPayments(SecurityBase): """ Class for handling outgoing payments resources """ + def __init__(self, keyid: str, private_key: str, logger: Logger, http_client: HttpClient): - super().__init__(keyid=keyid,private_key=private_key,logger=logger) + super().__init__(keyid=keyid, private_key=private_key, logger=logger) self.http_client = http_client def post_create_payment( - self, payment: OutgoingPaymentRequest, - resource_server_endpoint: str, - access_token: str - ) -> OutgoingPayment: + self, payment: OutgoingPaymentRequest, resource_server_endpoint: str, access_token: str + ) -> OutgoingPayment: """ Create an Outgoing Payment Resource """ base_url = resource_server_endpoint.rstrip("/") url = f"{base_url}/outgoing-payments" data = payment.model_dump(exclude_unset=True, mode="json") - req_headers = { - **get_default_headers(), - **self.get_auth_header(access_token=access_token) - } - request = self.http_client.build_request( - method="POST", - url=url, - json=data, - headers=req_headers - ) + req_headers = {**get_default_headers(), **self.get_auth_header(access_token=access_token)} + request = self.http_client.build_request(method="POST", url=url, json=data, headers=req_headers) request = self.set_content_digest(request=request) - request = self.sign_request(request,("content-type","content-digest","content-length","authorization",*get_default_covered_components())) + request = self.sign_request( + request, + ("content-type", "content-digest", "content-length", "authorization", *get_default_covered_components()), + ) response = self.http_client.send(request=request) return OutgoingPayment.model_validate(response.json()) def get_outgoing_payments( - self, - query: PaymentListQuery, - resource_server_endpoint: str, - access_token: str + self, query: PaymentListQuery, resource_server_endpoint: str, access_token: str ) -> PaginatedOutgoingPayments: """ Get Outgoing Payments @@ -168,17 +129,6 @@ def get_outgoing_payments( base_url = resource_server_endpoint.rstrip("/") url = f"{base_url}/outgoing-payments" query_params = query.model_dump(exclude_unset=True, mode="json") - req_headers = { - **self.get_auth_header(access_token=access_token) - } - request = self.http_client.build_request( - method="GET", - url=url, - headers=req_headers, - params=query_params - ) - response = request = self.sign_request(request,("authorization",*get_default_covered_components())) - self.http_client.send(request=request) req_headers = {**self.get_auth_header(access_token=access_token)} request = self.http_client.build_request(method="GET", url=url, headers=req_headers, params=query_params) request = self.sign_request(request, ("authorization", *get_default_covered_components())) @@ -186,24 +136,16 @@ def get_outgoing_payments( return PaginatedOutgoingPayments.model_validate(response.json()) def get_outgoing_payment( - self, payment_id: str, - resource_server_endpoint: str, - access_token: str - ) -> OutgoingPayment: + self, payment_id: str, resource_server_endpoint: str, access_token: str + ) -> OutgoingPayment: """ Get Outgoing Payment """ base_url = resource_server_endpoint url = f"{base_url}/outgoing-payments/{payment_id}" - req_headers = { - **self.get_auth_header(access_token=access_token) - } - request = self.http_client.build_request( - method="GET", - url=url, - headers=req_headers - ) - request = self.sign_request(request,("authorization",*get_default_covered_components())) + req_headers = {**self.get_auth_header(access_token=access_token)} + request = self.http_client.build_request(method="GET", url=url, headers=req_headers) + request = self.sign_request(request, ("authorization", *get_default_covered_components())) response = self.http_client.send(request=request) return OutgoingPayment.model_validate(response.json()) @@ -212,55 +154,36 @@ class Quotes(SecurityBase): """ Class for handling Quote resources """ - def __init__(self, keyid: str, private_key: str,logger: Logger ,http_client: HttpClient): - super().__init__(keyid=keyid,private_key=private_key,logger=logger) + + def __init__(self, keyid: str, private_key: str, logger: Logger, http_client: HttpClient): + super().__init__(keyid=keyid, private_key=private_key, logger=logger) self.http_client = http_client - def post_create_quote( - self, quote: QuoteRequest, - resource_server_endpoint: str, - access_token: str - ) -> Quote: + def post_create_quote(self, quote: QuoteRequest, resource_server_endpoint: str, access_token: str) -> Quote: """ - Create a Quote + Create a Quote """ base_url = resource_server_endpoint.rstrip("/") url = f"{base_url}/quotes" data = quote.model_dump(exclude_unset=True, mode="json") - req_headers = { - **get_default_headers(), - **self.get_auth_header(access_token=access_token) - } - request = self.http_client.build_request( - method="POST", - url=url, - headers=req_headers, - json=data - ) + req_headers = {**get_default_headers(), **self.get_auth_header(access_token=access_token)} + request = self.http_client.build_request(method="POST", url=url, headers=req_headers, json=data) request = self.set_content_digest(request=request) - request = self.sign_request(request,("content-type","content-digest","content-length","authorization",*get_default_covered_components())) + request = self.sign_request( + request, + ("content-type", "content-digest", "content-length", "authorization", *get_default_covered_components()), + ) response = self.http_client.send(request=request) return Quote.model_validate(response.json()) - def get_quote( - self, - quote_id: str, - resource_server_endpoint: str, - access_token: str - ) -> Quote: + def get_quote(self, quote_id: str, resource_server_endpoint: str, access_token: str) -> Quote: """ Get a Quote """ base_url = resource_server_endpoint.strip("/") url = f"{base_url}/quotes/{quote_id}" - req_headers = { - **self.get_auth_header(access_token=access_token) - } - request = self.http_client.build_request( - method="GET", - url=url, - headers=req_headers - ) - request = self.sign_request(request,("authorization",*get_default_covered_components())) + req_headers = {**self.get_auth_header(access_token=access_token)} + request = self.http_client.build_request(method="GET", url=url, headers=req_headers) + request = self.sign_request(request, ("authorization", *get_default_covered_components())) response = self.http_client.send(request=request) return Quote.model_validate(response.json()) diff --git a/src/open_payments_sdk/api/wallet.py b/src/open_payments_sdk/api/wallet.py index 6f6577d..fe16fcc 100644 --- a/src/open_payments_sdk/api/wallet.py +++ b/src/open_payments_sdk/api/wallet.py @@ -6,15 +6,13 @@ class Wallet: """ Class for handling Wallet resource """ + def __init__(self, http_client: HttpClient): self.http_client = http_client def get_wallet_address(self, wallet_address_server_endpoint: str) -> WalletAddress: """Get wallet address from address server""" - request = self.http_client.build_request( - method="GET", - url=wallet_address_server_endpoint - ) + request = self.http_client.build_request(method="GET", url=wallet_address_server_endpoint) response = self.http_client.send(request=request) return WalletAddress.model_validate(response.json()) @@ -22,9 +20,6 @@ def get_keys(self, wallet_address_server_endpoint: str) -> JsonWebKeySet: """Get keys from address server""" base_url = wallet_address_server_endpoint.rstrip("/") url = f"{base_url}/jwks.json" - request = self.http_client.build_request( - method="GET", - url=url - ) + request = self.http_client.build_request(method="GET", url=url) response = self.http_client.send(request=request) return JsonWebKeySet.model_validate(response.json()) diff --git a/src/open_payments_sdk/client/client.py b/src/open_payments_sdk/client/client.py index 47ef48e..8f22475 100644 --- a/src/open_payments_sdk/client/client.py +++ b/src/open_payments_sdk/client/client.py @@ -14,11 +14,19 @@ class OpenPaymentsClient: """ Open Payments API Client """ - def __init__(self, keyid: str, private_key: str, client_wallet_address: str,cfg: configuration.Configuration = None, http_client: HttpClient = None): + + def __init__( + self, + keyid: str, + private_key: str, + client_wallet_address: str, + cfg: configuration.Configuration | None = None, + http_client: HttpClient | None = None, + ): if not cfg: cfg = configuration.Configuration() - if not http_client : - http_client = HttpClient(http_timeout=10.0) # TODO: get from cfg + if not http_client: + http_client = HttpClient(http_timeout=10.0) # TODO: get from cfg self.http_client = http_client self.logger = logging.getLogger(__name__) self.logger.addHandler(cfg.get_log_handler()) @@ -26,12 +34,7 @@ def __init__(self, keyid: str, private_key: str, client_wallet_address: str,cfg: self.client_wallet_address = client_wallet_address self.keyid = keyid self.private_key = private_key - self.grants = Grants( - keyid=keyid, - private_key=private_key, - logger=self.logger, - http_client=self.http_client - ) + self.grants = Grants(keyid=keyid, private_key=private_key, logger=self.logger, http_client=self.http_client) self.access_tokens = AccessTokens( keyid=keyid, private_key=private_key, @@ -40,22 +43,9 @@ def __init__(self, keyid: str, private_key: str, client_wallet_address: str,cfg: ) self.wallet = Wallet(self.http_client) self.incoming_payments = IncomingPayments( - keyid=keyid, - private_key=private_key, - logger=self.logger, - http_client=self.http_client + keyid=keyid, private_key=private_key, logger=self.logger, http_client=self.http_client ) self.outgoing_payments = OutgoingPayments( - keyid=keyid, - private_key=private_key, - logger=self.logger, - http_client=self.http_client - ) - self.quotes = Quotes( - keyid=keyid, - private_key=private_key, - logger=self.logger, - http_client=self.http_client + keyid=keyid, private_key=private_key, logger=self.logger, http_client=self.http_client ) - - + self.quotes = Quotes(keyid=keyid, private_key=private_key, logger=self.logger, http_client=self.http_client) diff --git a/src/open_payments_sdk/gnap_utils/http_signatures.py b/src/open_payments_sdk/gnap_utils/http_signatures.py index 120811f..41c1ff2 100644 --- a/src/open_payments_sdk/gnap_utils/http_signatures.py +++ b/src/open_payments_sdk/gnap_utils/http_signatures.py @@ -2,16 +2,17 @@ HTTP Signatures Helper functions """ -from http_message_signatures import HTTPSignatureKeyResolver -from http_message_signatures.resolvers import HTTPSignatureComponentResolver +from http_message_signatures.resolvers import HTTPSignatureComponentResolver, HTTPSignatureKeyResolver from http_message_signatures.structures import CaseInsensitiveDict from open_payments_sdk.gnap_utils.keys import KeyManager + class OPKeyResolver(HTTPSignatureKeyResolver): """ Key Resolver Class """ + def __init__(self, keyid: str, private_key: str): super().__init__() self.keys = {keyid: private_key.encode("utf-8")} @@ -30,14 +31,15 @@ def resolve_private_key(self, key_id: str): """ return self.keys[key_id] - + class PatchedHTTPSignatureComponentResolver(HTTPSignatureComponentResolver): """ Component Resolver to be used by http signing logic. The upstream resolver class has a bug which I fixed via a PR https://github.com/pyauth/http-message-signatures/pull/18 - + The new package is not yet deployed. In the meantime this class fixes the bug and it works in this package """ + def __init__(self, message): """ Do not call upstream class constructor because it is buggy @@ -48,7 +50,7 @@ def __init__(self, message): self.message_type = "response" self.url = str(message.url) self.headers = CaseInsensitiveDict(message.headers) - + def get_request_response(self, *, key: str): """ Required implementation from abstract class. Since it is not used in the lib. Just pass diff --git a/src/open_payments_sdk/gnap_utils/keys.py b/src/open_payments_sdk/gnap_utils/keys.py index 0e18960..e94c5e7 100644 --- a/src/open_payments_sdk/gnap_utils/keys.py +++ b/src/open_payments_sdk/gnap_utils/keys.py @@ -1,6 +1,7 @@ """ - Key Management module +Key Management module """ + import base64 from typing import Union import uuid @@ -9,9 +10,10 @@ from open_payments_sdk.models.keys import Key, KeyJwks, KeyPair + class KeyManager: """ - Key Management class + Key Management class """ def generate_key_pair(self) -> KeyPair: @@ -23,40 +25,29 @@ def generate_key_pair(self) -> KeyPair: private_pem = private_key.private_bytes( encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.PKCS8, - encryption_algorithm=serialization.NoEncryption() + encryption_algorithm=serialization.NoEncryption(), ) - private_key_pem = private_pem.decode('utf-8') + private_key_pem = private_pem.decode("utf-8") raw_public_bytes = public_key.public_bytes( - encoding=serialization.Encoding.Raw, - format=serialization.PublicFormat.Raw + encoding=serialization.Encoding.Raw, format=serialization.PublicFormat.Raw ) - x = base64.urlsafe_b64encode(raw_public_bytes).rstrip(b'=').decode() + x = base64.urlsafe_b64encode(raw_public_bytes).rstrip(b"=").decode() kid = str(uuid.uuid4()) - key = Key( - kid=kid, - x=x, - alg="EdDSA", - kty="OKP", - crv="Ed25519" - ) + key = Key(kid=kid, x=x, alg="EdDSA", kty="OKP", crv="Ed25519") key_jwks = KeyJwks(keys=[key]) keypair = KeyPair(jwks=key_jwks, private_key_pem=private_key_pem) return KeyPair.model_validate(keypair) - - def load_ed25519_private_key_from_pem(self,pem_bytes: Union[str, bytes]) -> Ed25519PrivateKey: + + def load_ed25519_private_key_from_pem(self, pem_bytes: Union[str, bytes]) -> Ed25519PrivateKey: """ Read private key from str or bytes string """ - if isinstance(pem_bytes,str): + if isinstance(pem_bytes, str): pem_bytes = pem_bytes.encode("utf-8") - private_key = serialization.load_pem_private_key( - data=pem_bytes, - password=None - ) + private_key = serialization.load_pem_private_key(data=pem_bytes, password=None) if not isinstance(private_key, Ed25519PrivateKey): raise ValueError("Loaded key is not an Ed25519PrivateKey") return private_key - diff --git a/src/open_payments_sdk/gnap_utils/security.py b/src/open_payments_sdk/gnap_utils/security.py index eaf12c7..4991c84 100644 --- a/src/open_payments_sdk/gnap_utils/security.py +++ b/src/open_payments_sdk/gnap_utils/security.py @@ -5,8 +5,8 @@ import hashlib from logging import Logger from typing import Sequence -from http_message_signatures import HTTPMessageSigner, algorithms -import http_sfv +from http_message_signatures import _algorithms as algorithms +from http_message_signatures.signatures import HTTPMessageSigner from http_sf import ser from httpx import Request from open_payments_sdk.gnap_utils.hash import HashManager @@ -18,10 +18,15 @@ class SecurityBase: """ Base class to provide shared functionality for making authenticated requests """ + def __init__(self, keyid: str, private_key: str, logger: Logger): self.key_manager = KeyManager() self.hash_manager = HashManager() - self.http_signatures = HTTPMessageSigner(signature_algorithm=algorithms.ED25519, key_resolver=OPKeyResolver(keyid=keyid,private_key=private_key),component_resolver_class=PatchedHTTPSignatureComponentResolver) + self.http_signatures = HTTPMessageSigner( + signature_algorithm=algorithms.ED25519, + key_resolver=OPKeyResolver(keyid=keyid, private_key=private_key), + component_resolver_class=PatchedHTTPSignatureComponentResolver, + ) self.keyid = keyid self.private_key = private_key self.logger = logger @@ -30,26 +35,20 @@ def get_auth_header(self, access_token: str) -> dict: """ Prepare Authorization GNAP header """ - return { - "Authorization": f"GNAP {access_token}" - } - - def sign_request(self, message: Request, covered_component_ids: Sequence[str] )-> Request: + return {"Authorization": f"GNAP {access_token}"} + + def sign_request(self, message: Request, covered_component_ids: Sequence[str]) -> Request: """ Prepare http signature headers """ self.http_signatures.sign( - message=message, - key_id=self.keyid, - covered_component_ids=covered_component_ids, - label="sig1" + message=message, key_id=self.keyid, covered_component_ids=covered_component_ids, label="sig1" ) return message - + def set_content_digest(self, request: Request) -> Request: """ Compute Digest """ request.headers["Content-Digest"] = ser({"sha-512": hashlib.sha512(request.content).digest()}) return request - \ No newline at end of file diff --git a/src/open_payments_sdk/models/resource.py b/src/open_payments_sdk/models/resource.py index 833c755..8edb708 100644 --- a/src/open_payments_sdk/models/resource.py +++ b/src/open_payments_sdk/models/resource.py @@ -2,8 +2,16 @@ from enum import Enum from typing import Any, Dict, List, Literal, Optional, Union -from pydantic import (AnyUrl, BaseModel, ConfigDict, Field, HttpUrl, RootModel, - StringConstraints, conint, constr, field_validator) +from pydantic import ( + AnyUrl, + BaseModel, + ConfigDict, + Field, + HttpUrl, + RootModel, + conint, + field_validator, +) class AssetCode(RootModel[str]): @@ -59,12 +67,8 @@ class PageInfo(BaseModel): description="Cursor corresponding to the last element in the result array.", min_length=1 ) - hasNextPage: bool = Field( - ..., description="Describes whether the data set has further entries." - ) - hasPreviousPage: bool = Field( - ..., description="Describes whether the data set has previous entries." - ) + hasNextPage: bool = Field(..., description="Describes whether the data set has further entries.") + hasPreviousPage: bool = Field(..., description="Describes whether the data set has previous entries.") class PaymentMethod(Enum): @@ -82,8 +86,6 @@ class IlpPaymentMethod(BaseModel): description="The ILP address to use when establishing a STREAM connection.", pattern=r"^(g|private|example|peer|self|test[1-3]?|local)([.][a-zA-Z0-9_~-]+)+$", max_length=1023, - ) = Field( - ..., description="The ILP address to use when establishing a STREAM connection." ) sharedSecret: str = Field( ..., @@ -113,24 +115,17 @@ class PublicIncomingPayment(BaseModel): class OutgoingPayment(BaseModel): - model_config = ConfigDict( - extra="forbid", - ) id: AnyUrl = Field(..., description="The URL identifying the outgoing payment.") walletAddress: AnyUrl = Field( ..., description="The URL of the wallet address from which this payment is sent.", ) - quoteId: Optional[AnyUrl] = Field( - None, description="The URL of the quote defining this payment's amounts." - ) + quoteId: Optional[AnyUrl] = Field(None, description="The URL of the quote defining this payment's amounts.") failed: Optional[bool] = Field( False, description="Describes whether the payment failed to send its full amount.", ) - receiver: Receiver = Field( - ..., description="The URL of the incoming payment that is being paid." - ) + receiver: Receiver = Field(..., description="The URL of the incoming payment that is being paid.") receiveAmount: Amount = Field( ..., description="The total amount that should be received by the receiver when this outgoing payment has been paid.", @@ -158,16 +153,12 @@ class OutgoingPaymentWithSpentAmounts(BaseModel): ..., description="The URL of the wallet address from which this payment is sent.", ) - quoteId: Optional[AnyUrl] = Field( - None, description="The URL of the quote defining this payment's amounts." - ) + quoteId: Optional[AnyUrl] = Field(None, description="The URL of the quote defining this payment's amounts.") failed: Optional[bool] = Field( False, description="Describes whether the payment failed to send its full amount.", ) - receiver: Receiver = Field( - ..., description="The URL of the incoming payment that is being paid." - ) + receiver: Receiver = Field(..., description="The URL of the incoming payment that is being paid.") receiveAmount: Amount = Field( ..., description="The total amount that should be received by the receiver when this outgoing payment has been paid.", @@ -192,12 +183,8 @@ class OutgoingPaymentWithSpentAmounts(BaseModel): None, description="Additional metadata associated with the outgoing payment. (Optional)", ) - createdAt: datetime = Field( - ..., description="The date and time when the outgoing payment was created." - ) - updatedAt: datetime = Field( - ..., description="The date and time when the outgoing payment was updated." - ) + createdAt: datetime = Field(..., description="The date and time when the outgoing payment was created.") + updatedAt: datetime = Field(..., description="The date and time when the outgoing payment was updated.") class Quote(BaseModel): @@ -226,9 +213,7 @@ class Quote(BaseModel): None, description="The date and time when the calculated `debitAmount` is no longer valid.", ) - createdAt: datetime = Field( - ..., description="The date and time when the quote was created." - ) + createdAt: datetime = Field(..., description="The date and time when the quote was created.") class IncomingPayment(BaseModel): @@ -276,11 +261,10 @@ class IncomingPaymentRequest(BaseModel): metadata: Optional[Dict[str, Any]] = None -class IncomingPaymentResponse( - RootModel[Union[PublicIncomingPayment, IncomingPaymentWithMethods]] -): +class IncomingPaymentResponse(RootModel[Union[PublicIncomingPayment, IncomingPaymentWithMethods]]): pass + class PaymentListQuery(BaseModel): walletAddress: WalletAddress cursor: Optional[str] = Field(min_length=1) @@ -313,9 +297,7 @@ class OutgoingPaymentRequestWithIncoming(BaseModel): metadata: Optional[Dict[str, Any]] -class OutgoingPaymentRequest( - RootModel[Union[OutgoingPaymentRequestWithIncoming, OutgoingPaymentRequestWithQuote]] -): +class OutgoingPaymentRequest(RootModel[Union[OutgoingPaymentRequestWithIncoming, OutgoingPaymentRequestWithQuote]]): pass @@ -344,8 +326,5 @@ class QuoteFixedSent(QuoteRequestBase): debitAmount: Amount - -class QuoteRequest( - RootModel[Union[QuoteRequestBase, QuoteFixedSent, QuoteFixedReceive]] -): +class QuoteRequest(RootModel[Union[QuoteRequestBase, QuoteFixedSent, QuoteFixedReceive]]): pass diff --git a/src/open_payments_sdk/utils/utils.py b/src/open_payments_sdk/utils/utils.py index 9268f96..bd98db9 100644 --- a/src/open_payments_sdk/utils/utils.py +++ b/src/open_payments_sdk/utils/utils.py @@ -4,15 +4,14 @@ def get_default_headers() -> dict: - """ - Get default headers - """ - return { - "Content-Type": "application/json" - } + """ + Get default headers + """ + return {"Content-Type": "application/json"} + def get_default_covered_components() -> tuple: """ Return default covered components """ - return ("@method","@target-uri") \ No newline at end of file + return ("@method", "@target-uri") From 3cb02708fd80d26509ebf3ea4207a0ee0b41d23f Mon Sep 17 00:00:00 2001 From: Gavin Chait Date: Tue, 2 Dec 2025 15:26:17 +0100 Subject: [PATCH 08/13] Refactor for Black standards --- tests/conftest.py | 54 +++++++++++++++++++++++------------------------ 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 49fd4b3..e24d137 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,7 +7,7 @@ def keyid_private_key() -> dict: """ Get Private Key and Key Id """ - with open("tests/integration/privkey.pem.example","r",encoding="utf_8") as privkey: + with open("tests/integration/privkey.pem.example", "r", encoding="utf_8") as privkey: private_key = privkey.read() return { "private_key": private_key, @@ -27,7 +27,7 @@ def op_client(keyid_private_key) -> OpenPaymentsClient: return client @pytest.fixture -def wallet_address_server()-> str: +def wallet_address_server() -> str: """ Get Wallet Address """ @@ -39,7 +39,7 @@ def grant() -> str: get access token """ wallet = op_client.wallet.get_wallet_address("https://ilp.interledger-test.dev/5c327379") - return op_client.grants.post_grant_request(grant_request=grant_req_dto,auth_server_endpoint=str(wallet.authServer)) + return op_client.grants.post_grant_request(grant_request=grant_req_dto, auth_server_endpoint=str(wallet.authServer)) @pytest.fixture def grant_req_dto() -> GrantRequest: @@ -47,37 +47,37 @@ def grant_req_dto() -> GrantRequest: Grant Request DTO """ grant_req = { - "access_token": { - "access": [ - { - "type": "incoming-payment", - "actions": [ - "create", - "read" + "access_token": { + "access": [ + { + "type": "incoming-payment", + "actions": [ + "create", + "read" + ], + "identifier": "https://ilp.interledger-test.dev/5c327379" + } + ] + }, + "client": "https://ilp.interledger-test.dev/elijahokellosalary", + "interact": { + "start": [ + "redirect" ], - "identifier": "https://ilp.interledger-test.dev/5c327379" + "finish": { + "method": "redirect", + "uri": "https://webmonize.com/return/876FGRD8VC", + "nonce": "4edb2194-dbdf-46bb-9397-d5fd57b7c8a7" + } } - ] - }, - "client": "https://ilp.interledger-test.dev/elijahokellosalary", - "interact": { - "start": [ - "redirect" - ], - "finish": { - "method": "redirect", - "uri": "https://webmonize.com/return/876FGRD8VC", - "nonce": "4edb2194-dbdf-46bb-9397-d5fd57b7c8a7" - } - } } return GrantRequest(**grant_req) @pytest.fixture -def interactive_grant_req_dto() -> GrantRequest: #TODO complete writing tests +def interactive_grant_req_dto() -> GrantRequest: # TODO complete writing tests """ - Create Interactive Grant + Create Interactive Grant """ grant_req = {} - return GrantRequest(**grant_req) \ No newline at end of file + return GrantRequest(**grant_req) From ec3d2bd5bd8167a2d589a5d8b1db604815d71a33 Mon Sep 17 00:00:00 2001 From: Gavin Chait Date: Tue, 2 Dec 2025 15:28:37 +0100 Subject: [PATCH 09/13] Updating tests with .env It is good practice to place all environment variables - even demo ones used in 'test' - into a `.env` file that is excluded in the `.gitignore`. This commit adds: - `pydantic-settings` to import environment variables from a `.env` - `.env.example` for others to add their own settings - `config.py` to import the settings and make them available in tests. --- poetry.lock | 56 +++++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 1 + tests/.env.example | 4 ++++ tests/config.py | 14 ++++++++++++ 4 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 tests/.env.example create mode 100644 tests/config.py diff --git a/poetry.lock b/poetry.lock index 8f53c38..3d6a8e7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1027,6 +1027,30 @@ files = [ [package.dependencies] typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" +[[package]] +name = "pydantic-settings" +version = "2.12.0" +description = "Settings management using Pydantic" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809"}, + {file = "pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0"}, +] + +[package.dependencies] +pydantic = ">=2.7.0" +python-dotenv = ">=0.21.0" +typing-inspection = ">=0.4.0" + +[package.extras] +aws-secrets-manager = ["boto3 (>=1.35.0)", "boto3-stubs[secretsmanager]"] +azure-key-vault = ["azure-identity (>=1.16.0)", "azure-keyvault-secrets (>=4.8.0)"] +gcp-secret-manager = ["google-cloud-secret-manager (>=2.23.1)"] +toml = ["tomli (>=2.0.1)"] +yaml = ["pyyaml (>=6.0.1)"] + [[package]] name = "pyflakes" version = "3.2.0" @@ -1077,6 +1101,21 @@ tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +[[package]] +name = "python-dotenv" +version = "1.2.1" +description = "Read key-value pairs from a .env file and set them as environment variables" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61"}, + {file = "python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6"}, +] + +[package.extras] +cli = ["click (>=5.0)"] + [[package]] name = "pyyaml" version = "6.0.2" @@ -1243,6 +1282,21 @@ files = [ {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] +[[package]] +name = "typing-inspection" +version = "0.4.2" +description = "Runtime typing introspection tools" +optional = false +python-versions = ">=3.9" +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"}, +] + +[package.dependencies] +typing-extensions = ">=4.12.0" + [[package]] name = "wcwidth" version = "0.2.13" @@ -1258,4 +1312,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = ">=3.10" -content-hash = "568b0d6bd032d3b8b127fd04ba100d13a56e4745d12f75673cf41d1bf5228ce9" +content-hash = "f0e2f84804d0edceca59e016f7fc75e454f1a8dad10526e7c62e6f2b8144f5d0" diff --git a/pyproject.toml b/pyproject.toml index 4900c46..fba768f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,6 +12,7 @@ requires-python = ">=3.10" dependencies = [ "httpx (>=0.28.1,<0.29.0)", "pydantic (>=2.10.6,<3.0.0)", + "pydantic-settings (>=2.10.6,<3.0.0)", "cryptography (>=45.0.4,<46.0.0)", "http-message-signatures (>=0.6.1,<0.7.0)", "http-sf (>=1.0.4,<1.1.0)" diff --git a/tests/.env.example b/tests/.env.example new file mode 100644 index 0000000..15e0b27 --- /dev/null +++ b/tests/.env.example @@ -0,0 +1,4 @@ +TEST_SELLER_WALLET= +TEST_SELLER_KEY= +TEST_SELLER_KEY_ID= +TEST_BUYER_WALLET= \ No newline at end of file diff --git a/tests/config.py b/tests/config.py new file mode 100644 index 0000000..28e4af6 --- /dev/null +++ b/tests/config.py @@ -0,0 +1,14 @@ +from pydantic import Field +from typing import Optional +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class Settings(BaseSettings): + TEST_SELLER_WALLET: Optional[str] = Field(default="") + TEST_SELLER_KEY: Optional[str] = Field(default="") + TEST_SELLER_KEY_ID: Optional[str] = Field(default="") + TEST_BUYER_WALLET: Optional[str] = Field(default="") + model_config = SettingsConfigDict(case_sensitive=True, env_file=".env", env_file_encoding="utf-8") + + +settings = Settings() From cc5a4d417feff6de9f4e0cce0f38ff379214153d Mon Sep 17 00:00:00 2001 From: Gavin Chait Date: Fri, 5 Dec 2025 16:40:44 +0100 Subject: [PATCH 10/13] Refactoring of generated Pydantic models There are numerous changes: - Removal of all unnecessary `RootModel` classes and refactoring for simple inheritence. - Removal of duplicated `WalletAddress` class from `resource.py` and `auth.py`. - Simplification of bizarre duplication of `LimitsOutgoing1`, `LimitsOutgoing2`, etc into a single class. And multiple other quality of life fixes and improves. I have no doubt this could be improved further, but this is significantly easier to navigate and read. --- src/open_payments_sdk/models/auth.py | 221 ++++------ .../models/http_signatures.py | 9 +- src/open_payments_sdk/models/keys.py | 2 +- src/open_payments_sdk/models/resource.py | 389 ++++++++---------- src/open_payments_sdk/models/wallet.py | 87 ++-- 5 files changed, 275 insertions(+), 433 deletions(-) diff --git a/src/open_payments_sdk/models/auth.py b/src/open_payments_sdk/models/auth.py index 1f5f107..7959a4e 100644 --- a/src/open_payments_sdk/models/auth.py +++ b/src/open_payments_sdk/models/auth.py @@ -1,8 +1,12 @@ from enum import Enum -from typing import Any, List, Optional, Union +from typing import Any, Optional, Union +from pydantic import AnyUrl, BaseModel, ConfigDict, Field, RootModel, model_validator -from pydantic import AnyUrl, BaseModel, ConfigDict, Field, RootModel, conint, model_validator +from .resource import Amount +################################################################################################### +# ENUMERATED TYPES +################################################################################################### class TypeIncoming(Enum): incoming_payment = "incoming-payment" @@ -17,21 +21,6 @@ class ActionIncoming(Enum): list_all = "list-all" -class AccessIncoming(BaseModel): - type: TypeIncoming = Field( - ..., - description="The type of resource request as a string. This field defines which other fields are allowed in the request object.", - ) - actions: List[ActionIncoming] = Field( - ..., - description="The types of actions the client instance will take at the RS as an array of strings.", - ) - identifier: Optional[AnyUrl] = Field( - None, - description="A string identifier indicating a specific resource at the RS.", - ) - - class TypeOutgoing(Enum): outgoing_payment = "outgoing-payment" @@ -54,24 +43,16 @@ class ActionQuote(Enum): read_all = "read-all" -class AccessQuote(BaseModel): - type: TypeQuote = Field( - ..., - description="The type of resource request as a string. This field defines which other fields are allowed in the request object.", - ) - actions: List[ActionQuote] = Field( - ..., - description="The types of actions the client instance will take at the RS as an array of strings.", - ) +class StartEnum(Enum): + redirect = "redirect" -class Client(RootModel[str]): - root: str = Field( - ..., - description="Wallet address of the client instance that is making this request.\n\nWhen sending a non-continuation request to the AS, the client instance MUST identify itself by including the client field of the request and by signing the request.\n\nA JSON Web Key Set document, including the public key that the client instance will use to protect this request and any continuation requests at the AS and any user-facing information about the client instance used in interactions, MUST be available at the wallet address + `/jwks.json` url.\n\nIf sending a grant initiation request that requires RO interaction, the wallet address MUST serve necessary client display information.", - title="client", - ) +class Method(Enum): + redirect = "redirect" +################################################################################################### +# MODEL UTILITIES +################################################################################################### class AccessTokenContinue(BaseModel): value: str @@ -92,14 +73,6 @@ class Continue(BaseModel): ) -class StartEnum(Enum): - redirect = "redirect" - - -class Method(Enum): - redirect = "redirect" - - class Finish(BaseModel): method: Method = Field( ..., @@ -116,7 +89,7 @@ class Finish(BaseModel): class InteractRequest(BaseModel): - start: List[StartEnum] = Field( + start: list[StartEnum] = Field( ..., description="Indicates how the client instance can start an interaction." ) finish: Optional[Finish] = Field( @@ -130,22 +103,9 @@ class InteractResponse(BaseModel): finish: str = Field(..., description="Unique key to secure the callback.") -class Interval(RootModel[str]): - root: str = Field( - ..., - description="[ISO8601 repeating interval](https://en.wikipedia.org/wiki/ISO_8601#Repeating_intervals)", - examples=[ - "R11/2022-08-24T14:15:22Z/P1M", - "R/2017-03-01T13:00:00Z/2018-05-11T15:30:00Z", - "R-1/P1Y2M10DT2H30M/2022-05-11T15:30:00Z", - ], - title="Interval", - ) - - -class Receiver(RootModel[AnyUrl]): - root: AnyUrl = Field( - ..., +class LimitsOutgoing(BaseModel): + receiver: Optional[AnyUrl] = Field( + None, description="The URL of the incoming payment that is being paid.", examples=[ "https://ilp.interledger-test.dev/incoming-payments/08394f02-7b7b-45e2-b645-51d04e7c330c", @@ -154,45 +114,6 @@ class Receiver(RootModel[AnyUrl]): ], title="Receiver", ) - - -class AssetCode(RootModel[str]): - root: str = Field( - ..., - description="The assetCode is a code that indicates the underlying asset. This SHOULD be an ISO4217 currency code.", - title="Asset code", - ) - - -class AssetScale(RootModel[conint(ge=0, le=255)]): - root: int = Field( - ..., - description="The scale of amounts denoted in the corresponding asset code.", - title="Asset scale", - ge=0, - le=255 - ) - - -class WalletAddress(RootModel[AnyUrl]): - root: AnyUrl = Field( - ..., - description="URL of a wallet address hosted by a Rafiki instance.", - title="Wallet Address", - ) - - -class Amount(BaseModel): - value: str = Field( - ..., - description="The value is an unsigned 64-bit integer amount, represented as a string.", - ) - assetCode: AssetCode - assetScale: AssetScale - - -class LimitsOutgoing1(BaseModel): - receiver: Optional[Receiver] = None debitAmount: Optional[Amount] = Field( None, description="All amounts are maxima, i.e. multiple payments can be created under a grant as long as the total amounts of these payments do not exceed the maximum amount per interval as specified in the grant.", @@ -201,42 +122,51 @@ class LimitsOutgoing1(BaseModel): None, description="All amounts are maxima, i.e. multiple payments can be created under a grant as long as the total amounts of these payments do not exceed the maximum amount per interval as specified in the grant.", ) - interval: Optional[Interval] = None + interval: Optional[str] = Field( + None, + description="[ISO8601 repeating interval](https://en.wikipedia.org/wiki/ISO_8601#Repeating_intervals)", + examples=[ + "R11/2022-08-24T14:15:22Z/P1M", + "R/2017-03-01T13:00:00Z/2018-05-11T15:30:00Z", + "R-1/P1Y2M10DT2H30M/2022-05-11T15:30:00Z", + ], + title="Interval", + ) -class LimitsOutgoing2(BaseModel): - receiver: Optional[Receiver] = None - debitAmount: Amount = Field( +class InteractRef(BaseModel): + interact_ref: str = Field( ..., - description="All amounts are maxima, i.e. multiple payments can be created under a grant as long as the total amounts of these payments do not exceed the maximum amount per interval as specified in the grant.", - ) - receiveAmount: Optional[Amount] = Field( - None, - description="All amounts are maxima, i.e. multiple payments can be created under a grant as long as the total amounts of these payments do not exceed the maximum amount per interval as specified in the grant.", + description="The interaction reference generated for this interaction by the AS." ) - interval: Optional[Interval] = None +################################################################################################### +# MODEL CORE DEFINITIONS +################################################################################################### -class LimitsOutgoing3(BaseModel): - receiver: Optional[Receiver] = None - debitAmount: Optional[Amount] = Field( - None, - description="All amounts are maxima, i.e. multiple payments can be created under a grant as long as the total amounts of these payments do not exceed the maximum amount per interval as specified in the grant.", +class AccessIncoming(BaseModel): + type: TypeIncoming = Field( + ..., + description="The type of resource request as a string. This field defines which other fields are allowed in the request object.", ) - receiveAmount: Amount = Field( + actions: list[ActionIncoming] = Field( ..., - description="All amounts are maxima, i.e. multiple payments can be created under a grant as long as the total amounts of these payments do not exceed the maximum amount per interval as specified in the grant.", + description="The types of actions the client instance will take at the RS as an array of strings.", + ) + identifier: Optional[AnyUrl] = Field( + None, + description="A string identifier indicating a specific resource at the RS.", ) - interval: Optional[Interval] = None -class LimitsOutgoing( - RootModel[Union[LimitsOutgoing1, LimitsOutgoing2, LimitsOutgoing3]] -): - root: Union[LimitsOutgoing1, LimitsOutgoing2, LimitsOutgoing3] = Field( +class AccessQuote(BaseModel): + type: TypeQuote = Field( ..., - description="Open Payments specific property that defines the limits under which outgoing payments can be created.", - title="limits-outgoing", + description="The type of resource request as a string. This field defines which other fields are allowed in the request object.", + ) + actions: list[ActionQuote] = Field( + ..., + description="The types of actions the client instance will take at the RS as an array of strings.", ) @@ -245,7 +175,7 @@ class AccessOutgoing(BaseModel): ..., description="The type of resource request as a string. This field defines which other fields are allowed in the request object.", ) - actions: List[ActionOutgoing] = Field( + actions: list[ActionOutgoing] = Field( ..., description="The types of actions the client instance will take at the RS as an array of strings.", ) @@ -255,25 +185,7 @@ class AccessOutgoing(BaseModel): limits: Optional[LimitsOutgoing] = None -class AccessItem(RootModel[Union[AccessIncoming, AccessOutgoing, AccessQuote]]): - root: Union[AccessIncoming, AccessOutgoing, AccessQuote] = Field( - ..., - description="The access associated with the access token is described using objects that each contain multiple dimensions of access.", - ) - - -class Access(RootModel[List[AccessItem]]): - root: List[AccessItem] = Field( - ..., - description="A description of the rights associated with this access token.", - max_length=3, - ) - - class AccessToken(BaseModel): - model_config = ConfigDict( - extra="forbid", - ) value: str = Field( ..., description="The value of the access token as a string. The value is opaque to the client instance. The value SHOULD be limited to ASCII characters to facilitate transmission over HTTP headers within other protocols without requiring additional encoding.", @@ -286,24 +198,32 @@ class AccessToken(BaseModel): None, description="The number of seconds in which the access will expire. The client instance MUST NOT use the access token past this time. An RS MUST NOT accept an access token past this time.", ) - access: Access + access: list[Union[AccessIncoming, AccessOutgoing, AccessQuote]] + model_config = ConfigDict( + extra="forbid", + ) class GrantRequestAccessToken(BaseModel): - access: Access + access: list[Union[AccessIncoming, AccessOutgoing, AccessQuote]] class GrantRequest(BaseModel): access_token: GrantRequestAccessToken - client: Optional[Client] = None + client: Optional[str] = Field( + None, + description="Wallet address of the client instance that is making this request.\n\nWhen sending a non-continuation request to the AS, the client instance MUST identify itself by including the client field of the request and by signing the request.\n\nA JSON Web Key Set document, including the public key that the client instance will use to protect this request and any continuation requests at the AS and any user-facing information about the client instance used in interactions, MUST be available at the wallet address + `/jwks.json` url.\n\nIf sending a grant initiation request that requires RO interaction, the wallet address MUST serve necessary client display information.", + title="client", + ) interact: Optional[InteractRequest] = None + class ReservedKeyMappingModel(BaseModel): """ Base class that maps 'continue' to 'cont' in incoming data. """ - @model_validator(mode='before') + @model_validator(mode="before") @classmethod def replace_continue_key(cls, values: Any) -> Any: """ @@ -313,6 +233,7 @@ def replace_continue_key(cls, values: Any) -> Any: values["cont"] = values.pop("continue") return values + class InteractionInstructionsResponse(ReservedKeyMappingModel): interact: InteractResponse cont: Continue @@ -324,18 +245,20 @@ class GrantResponse(ReservedKeyMappingModel): class Grant(RootModel[Union[InteractionInstructionsResponse, GrantResponse]]): + """ + Reference https://docs.pydantic.dev/latest/concepts/models/#rootmodel-and-custom-root-types + + NOTE: `__getattr__` allows us to bypass the irritating `.root` attribute and + get direct to the fields. + """ root: Union[InteractionInstructionsResponse, GrantResponse] = Field( ..., description="The grant object, either interaction instructions or grant response", title="grant", ) - -class InteractRef(BaseModel): - interact_ref: str = Field( - ..., - description="The interaction reference generated for this interaction by the AS." - ) + def __getattr__(self, item: str) -> Any: + return getattr(self.root, item) class GrantContinueResponse(BaseModel): diff --git a/src/open_payments_sdk/models/http_signatures.py b/src/open_payments_sdk/models/http_signatures.py index 1d24a7f..fed1a45 100644 --- a/src/open_payments_sdk/models/http_signatures.py +++ b/src/open_payments_sdk/models/http_signatures.py @@ -1,14 +1,13 @@ from pydantic import BaseModel, ConfigDict class SignatureBaseReturn(BaseModel): - signature_params: str - signature_base: str + signature_params: str + signature_base: str model_config = ConfigDict(extra='forbid') class SignatureHeaders(BaseModel): - signature_input: str - signature: str + signature_input: str + signature: str model_config = ConfigDict(extra="forbid") - \ No newline at end of file diff --git a/src/open_payments_sdk/models/keys.py b/src/open_payments_sdk/models/keys.py index 6c935b5..4f16ef7 100644 --- a/src/open_payments_sdk/models/keys.py +++ b/src/open_payments_sdk/models/keys.py @@ -19,4 +19,4 @@ class KeyJwks(BaseModel): class KeyPair(BaseModel): jwks: KeyJwks private_key_pem: str - + diff --git a/src/open_payments_sdk/models/resource.py b/src/open_payments_sdk/models/resource.py index 8edb708..eabfbae 100644 --- a/src/open_payments_sdk/models/resource.py +++ b/src/open_payments_sdk/models/resource.py @@ -1,6 +1,6 @@ from datetime import datetime from enum import Enum -from typing import Any, Dict, List, Literal, Optional, Union +from typing import Any, Optional from pydantic import ( AnyUrl, @@ -8,50 +8,19 @@ ConfigDict, Field, HttpUrl, - RootModel, - conint, field_validator, ) +################################################################################################### +# ENUMERATED TYPES +################################################################################################### -class AssetCode(RootModel[str]): - root: str = Field( - ..., - description="The assetCode is a code that indicates the underlying asset. This SHOULD be an ISO4217 currency code.", - title="Asset code", - ) - - -class AssetScale(RootModel[conint(ge=0, le=255)]): - root: int = Field( - ..., - description="The scale of amounts denoted in the corresponding asset code.", - title="Asset scale", - ge=0, - le=255 - ) - - -class Receiver(RootModel[AnyUrl]): - root: AnyUrl = Field( - ..., - description="The URL of the incoming payment that is being paid.", - examples=[ - "https://ilp.interledger-test.dev/incoming-payments/08394f02-7b7b-45e2-b645-51d04e7c330c", - "http://ilp.interledger-test.dev/incoming-payments/08394f02-7b7b-45e2-b645-51d04e7c330c", - "https://ilp.interledger-test.dev/incoming-payments/1", - ], - title="Receiver", - ) - - -class WalletAddress(RootModel[AnyUrl]): - root: AnyUrl = Field( - ..., - description="URL of a wallet address hosted by a Rafiki instance.", - title="Wallet Address", - ) +class PaymentMethod(Enum): + ilp = "ilp" +################################################################################################### +# MODEL UTILITIES +################################################################################################### class PageInfo(BaseModel): model_config = ConfigDict( @@ -71,16 +40,48 @@ class PageInfo(BaseModel): hasPreviousPage: bool = Field(..., description="Describes whether the data set has previous entries.") -class PaymentMethod(Enum): - ilp = "ilp" +class PaymentListQuery(BaseModel): + walletAddress: AnyUrl = Field( + ..., + description="URL of a wallet address hosted by a Rafiki instance.", + title="Wallet Address", + ) + cursor: Optional[str] = Field(min_length=1) + first: Optional[int] = Field(ge=1, le=100) + last: Optional[int] = Field(ge=1, le=100) -class Type(Enum): - ilp = "ilp" +class Pagination(BaseModel): + startCursor: str = Field(min_length=1) + endCursor: str = Field(min_length=1) + hasNextPage: Optional[bool] + hasPrevPage: Optional[bool] + +################################################################################################### +# MODEL CORE DEFINITIONS +################################################################################################### + +class Amount(BaseModel): + value: str = Field( + ..., + description="The value is an unsigned 64-bit integer amount, represented as a string.", + ) + assetCode: str = Field( + ..., + description="The assetCode is a code that indicates the underlying asset. This SHOULD be an ISO4217 currency code.", + title="Asset code", + ) + assetScale: int = Field( + ..., + description="The scale of amounts denoted in the corresponding asset code.", + title="Asset scale", + ge=0, + le=255 + ) class IlpPaymentMethod(BaseModel): - type: Type + type: PaymentMethod ilpAddress: str = Field( ..., description="The ILP address to use when establishing a STREAM connection.", @@ -96,235 +97,173 @@ class IlpPaymentMethod(BaseModel): extra="forbid", ) +################################################################################################### +# MODEL OPEN PAYMENT PROCESS DEFINITIONS +################################################################################################### -class Amount(BaseModel): - value: str = Field( +class IncomingPaymentRequest(BaseModel): + walletAddress: AnyUrl = Field( ..., - description="The value is an unsigned 64-bit integer amount, represented as a string.", + description="URL of a wallet address hosted by a Rafiki instance.", + title="Wallet Address", ) - assetCode: AssetCode - assetScale: AssetScale + incomingAmount: Optional[Amount] = Field( + None, + description="The maximum amount that should be paid into the wallet address under this incoming payment.", + ) + expiresAt: Optional[datetime] = Field( + None, + description="The date and time when payments under this incoming payment will no longer be accepted.", + ) + metadata: Optional[dict[str, Any]] = Field( + None, + description="Additional metadata associated with the incoming payment. (Optional)", + ) + +class IncomingPayment(IncomingPaymentRequest): + id: AnyUrl = Field(..., description="The URL identifying the incoming payment.") + completed: bool = Field( + ..., + description="Describes whether the incoming payment has completed receiving fund.", + ) + receivedAmount: Amount = Field( + ..., + description="The total amount that has been paid into the wallet address under this incoming payment.", + ) + createdAt: datetime = Field(..., description="The date and time when the incoming payment was created.") + updatedAt: Optional[datetime] = Field(None, description="The date and time when the incoming payment was updated.") -class PublicIncomingPayment(BaseModel): +class IncomingPaymentResponse(IncomingPayment): receivedAmount: Optional[Amount] = None authServer: AnyUrl = Field( ..., description="The URL of the authorization server endpoint for getting grants and access tokens for this wallet address.", ) + methods: list[IlpPaymentMethod] = Field( + ..., + description="The list of payment methods supported by this incoming payment.", + min_length=0, + ) -class OutgoingPayment(BaseModel): - id: AnyUrl = Field(..., description="The URL identifying the outgoing payment.") +class PaginatedIncomingPayments(BaseModel): + pagination: Pagination + result: list[IncomingPayment] + + +class QuoteRequest(BaseModel): walletAddress: AnyUrl = Field( ..., - description="The URL of the wallet address from which this payment is sent.", - ) - quoteId: Optional[AnyUrl] = Field(None, description="The URL of the quote defining this payment's amounts.") - failed: Optional[bool] = Field( - False, - description="Describes whether the payment failed to send its full amount.", + description="URL of a wallet address hosted by a Rafiki instance.", + title="Wallet Address", ) - receiver: Receiver = Field(..., description="The URL of the incoming payment that is being paid.") - receiveAmount: Amount = Field( - ..., - description="The total amount that should be received by the receiver when this outgoing payment has been paid.", + receiver: HttpUrl + method: PaymentMethod + receiveAmount: Optional[Amount] = Field( + None, + description="The total amount that should be received by the receiver when the corresponding outgoing payment has been paid.", ) - debitAmount: Amount = Field( - ..., - description="The total amount that should be deducted from the sender's account when this outgoing payment has been paid.", + debitAmount: Optional[Amount] = Field( + None, + description="The total amount that should be deducted from the sender's account when the corresponding outgoing payment has been paid. ", ) - sentAmount: Amount = Field( - ..., - description="The total amount that has been sent under this outgoing payment.", + + @field_validator("receiver") + @classmethod + def check_path(cls, v): + assert "/incoming-payments/" in v.path + return v + + +class Quote(QuoteRequest): + id: AnyUrl = Field(..., description="The URL identifying the quote.") + receiver: Optional[AnyUrl] = Field( + None, + description="The URL of the incoming payment that is being paid.", + examples=[ + "https://ilp.interledger-test.dev/incoming-payments/08394f02-7b7b-45e2-b645-51d04e7c330c", + "http://ilp.interledger-test.dev/incoming-payments/08394f02-7b7b-45e2-b645-51d04e7c330c", + "https://ilp.interledger-test.dev/incoming-payments/1", + ], + title="Receiver", ) - metadata: Optional[Dict[str, Any]] = Field( + expiresAt: Optional[str] = Field( None, - description="Additional metadata associated with the outgoing payment. (Optional)", + description="The date and time when the calculated `debitAmount` is no longer valid.", + ) + createdAt: datetime = Field(..., description="The date and time when the quote was created.") + model_config = ConfigDict( + extra="forbid", ) - createdAt: datetime = Field(..., description="The date and time when the outgoing payment was created.") - updatedAt: Optional[datetime] = Field(None, description="The date and time when the outgoing payment was updated.") - model_config = ConfigDict() -class OutgoingPaymentWithSpentAmounts(BaseModel): - id: AnyUrl = Field(..., description="The URL identifying the outgoing payment.") +class OutgoingPaymentBase(BaseModel): walletAddress: AnyUrl = Field( ..., description="The URL of the wallet address from which this payment is sent.", + title="Wallet Address", ) - quoteId: Optional[AnyUrl] = Field(None, description="The URL of the quote defining this payment's amounts.") - failed: Optional[bool] = Field( - False, - description="Describes whether the payment failed to send its full amount.", - ) - receiver: Receiver = Field(..., description="The URL of the incoming payment that is being paid.") - receiveAmount: Amount = Field( - ..., - description="The total amount that should be received by the receiver when this outgoing payment has been paid.", - ) - debitAmount: Amount = Field( - ..., - description="The total amount that should be deducted from the sender's account when this outgoing payment has been paid.", - ) - sentAmount: Amount = Field( - ..., - description="The total amount that has been sent under this outgoing payment.", - ) - grantSpentDebitAmount: Optional[Amount] = Field( - None, - description="The total amount successfully deducted from the sender's account using the current outgoing payment grant.", - ) - grantSpentReceiveAmount: Optional[Amount] = Field( + quoteId: AnyUrl = Field(..., description="The URL of the quote defining this payment's amounts.") + debitAmount: Optional[Amount] = Field( None, - description="The total amount successfully received (by all receivers) using the current outgoing payment grant.", + description="The total amount that should be deducted from the sender's account when this outgoing payment has been paid.", ) - metadata: Optional[Dict[str, Any]] = Field( + metadata: Optional[dict[str, Any]] = Field( None, description="Additional metadata associated with the outgoing payment. (Optional)", ) - createdAt: datetime = Field(..., description="The date and time when the outgoing payment was created.") - updatedAt: datetime = Field(..., description="The date and time when the outgoing payment was updated.") -class Quote(BaseModel): - model_config = ConfigDict( - extra="forbid", +class OutgoingPaymentRequest(OutgoingPaymentBase): + incomingPayment: Optional[AnyUrl] = Field(default=None) + + +class OutgoingPayment(OutgoingPaymentBase): + id: AnyUrl = Field(..., description="The URL identifying the outgoing payment.") + quoteId: Optional[AnyUrl] + failed: Optional[bool] = Field( + False, + description="Describes whether the payment failed to send its full amount.", ) - id: AnyUrl = Field(..., description="The URL identifying the quote.") - walletAddress: AnyUrl = Field( - ..., - description="The URL of the wallet address from which this quote's payment would be sent.", + receiver: Optional[AnyUrl] = Field( + None, + description="The URL of the incoming payment that is being paid.", + examples=[ + "https://ilp.interledger-test.dev/incoming-payments/08394f02-7b7b-45e2-b645-51d04e7c330c", + "http://ilp.interledger-test.dev/incoming-payments/08394f02-7b7b-45e2-b645-51d04e7c330c", + "https://ilp.interledger-test.dev/incoming-payments/1", + ], + title="Receiver", ) - receiver: Receiver = Field( + debitAmount: Amount = Field( ..., - description="The URL of the incoming payment that the quote is created for.", + description="The total amount that should be deducted from the sender's account when this outgoing payment has been paid.", ) receiveAmount: Amount = Field( ..., - description="The total amount that should be received by the receiver when the corresponding outgoing payment has been paid.", + description="The total amount that should be received by the receiver when this outgoing payment has been paid.", ) - debitAmount: Amount = Field( + sentAmount: Amount = Field( ..., - description="The total amount that should be deducted from the sender's account when the corresponding outgoing payment has been paid. ", - ) - method: PaymentMethod - expiresAt: Optional[str] = Field( - None, - description="The date and time when the calculated `debitAmount` is no longer valid.", + description="The total amount that has been sent under this outgoing payment.", ) - createdAt: datetime = Field(..., description="The date and time when the quote was created.") + createdAt: datetime = Field(..., description="The date and time when the outgoing payment was created.") + updatedAt: Optional[datetime] = Field(None, description="The date and time when the outgoing payment was updated.") + model_config = ConfigDict() -class IncomingPayment(BaseModel): - id: AnyUrl = Field(..., description="The URL identifying the incoming payment.") - walletAddress: AnyUrl = Field( - ..., - description="The URL of the wallet address this payment is being made into.", - ) - completed: bool = Field( - ..., - description="Describes whether the incoming payment has completed receiving fund.", - ) - incomingAmount: Optional[Amount] = Field( - None, - description="The maximum amount that should be paid into the wallet address under this incoming payment.", - ) - receivedAmount: Amount = Field( - ..., - description="The total amount that has been paid into the wallet address under this incoming payment.", - ) - expiresAt: Optional[datetime] = Field( +class OutgoingPaymentWithSpentAmounts(OutgoingPayment): + grantSpentDebitAmount: Optional[Amount] = Field( None, - description="The date and time when payments under this incoming payment will no longer be accepted.", + description="The total amount successfully deducted from the sender's account using the current outgoing payment grant.", ) - metadata: Optional[Dict[str, Any]] = Field( + grantSpentReceiveAmount: Optional[Amount] = Field( None, - description="Additional metadata associated with the incoming payment. (Optional)", - ) - createdAt: datetime = Field(..., description="The date and time when the incoming payment was created.") - updatedAt: Optional[datetime] = Field(None, description="The date and time when the incoming payment was updated.") - - -class IncomingPaymentWithMethods(IncomingPayment): - methods: List[IlpPaymentMethod] = Field( - ..., - description="The list of payment methods supported by this incoming payment.", - min_length=0, + description="The total amount successfully received (by all receivers) using the current outgoing payment grant.", ) -class IncomingPaymentRequest(BaseModel): - walletAddress: WalletAddress - incomingAmount: Optional[Amount] - expiresAt: Optional[datetime] = None - metadata: Optional[Dict[str, Any]] = None - - -class IncomingPaymentResponse(RootModel[Union[PublicIncomingPayment, IncomingPaymentWithMethods]]): - pass - - -class PaymentListQuery(BaseModel): - walletAddress: WalletAddress - cursor: Optional[str] = Field(min_length=1) - first: Optional[int] = Field(ge=1, le=100) - last: Optional[int] = Field(ge=1, le=100) - - -class Pagination(BaseModel): - startCursor: str = Field(min_length=1) - endCursor: str = Field(min_length=1) - hasNextPage: Optional[bool] - hasPrevPage: Optional[bool] - - -class PaginatedIncomingPayments(BaseModel): - pagination: Pagination - result: List[IncomingPayment] - - -class OutgoingPaymentRequestWithQuote(BaseModel): - walletAddress: WalletAddress - quoteId: AnyUrl - metadata: Optional[Dict[str, Any]] - - -class OutgoingPaymentRequestWithIncoming(BaseModel): - walletAddress: WalletAddress - incomingPayment: AnyUrl - debitAmount: Amount - metadata: Optional[Dict[str, Any]] - - -class OutgoingPaymentRequest(RootModel[Union[OutgoingPaymentRequestWithIncoming, OutgoingPaymentRequestWithQuote]]): - pass - - class PaginatedOutgoingPayments(BaseModel): pagination: Pagination - result: List[OutgoingPayment] - - -class QuoteRequestBase(BaseModel): - walletAddress: WalletAddress - receiver: HttpUrl - method: Literal["ilp"] - - @field_validator("receiver") - @classmethod - def check_path(cls, v): - assert "/incoming-payments/" in v.path - return v - - -class QuoteFixedReceive(QuoteRequestBase): - receiveAmount: Amount - - -class QuoteFixedSent(QuoteRequestBase): - debitAmount: Amount - - -class QuoteRequest(RootModel[Union[QuoteRequestBase, QuoteFixedSent, QuoteFixedReceive]]): - pass + result: list[OutgoingPayment] diff --git a/src/open_payments_sdk/models/wallet.py b/src/open_payments_sdk/models/wallet.py index 60aee07..087818e 100644 --- a/src/open_payments_sdk/models/wallet.py +++ b/src/open_payments_sdk/models/wallet.py @@ -1,38 +1,10 @@ from enum import Enum -from typing import List, Optional - -from pydantic import (AnyUrl, BaseModel, ConfigDict, Field, RootModel, conint, - constr) - - -class AssetCode(RootModel[str]): - root: str = Field( - ..., - description="The assetCode is a code that indicates the underlying asset. This SHOULD be an ISO4217 currency code.", - title="Asset code", - ) - - -class AssetScale(RootModel[conint(ge=0, le=255)]): - root: conint(ge=0, le=255) = Field( - ..., - description="The scale of amounts denoted in the corresponding asset code.", - title="Asset scale", - ) - - -class Receiver(RootModel[AnyUrl]): - root: AnyUrl = Field( - ..., - description="The URL of the incoming payment that is being paid.", - examples=[ - "https://ilp.interledger-test.dev/incoming-payments/08394f02-7b7b-45e2-b645-51d04e7c330c", - "http://ilp.interledger-test.dev/incoming-payments/08394f02-7b7b-45e2-b645-51d04e7c330c", - "https://ilp.interledger-test.dev/incoming-payments/1", - ], - title="Receiver", - ) +from typing import Optional +from pydantic import (AnyUrl, BaseModel, ConfigDict, Field) +################################################################################################### +# ENUMERATED TYPES +################################################################################################### class Alg(Enum): EdDSA = "EdDSA" @@ -49,6 +21,9 @@ class Kty(Enum): class Crv(Enum): Ed25519 = "Ed25519" +################################################################################################### +# MODEL UTILITIES +################################################################################################### class JsonWebKey(BaseModel): kid: str @@ -59,8 +34,10 @@ class JsonWebKey(BaseModel): use: Optional[Use] = None kty: Kty crv: Crv - x: constr(pattern=r"^[a-zA-Z0-9-_]+$") = Field( - ..., description="The base64 url-encoded public key." + x: str = Field( + ..., + description="The base64 url-encoded public key.", + pattern=r"^[a-zA-Z0-9-_]+$" ) @@ -68,17 +45,34 @@ class DidDocument(BaseModel): pass -class WalletAddress(BaseModel): +class JsonWebKeySet(BaseModel): + keys: list[JsonWebKey] model_config = ConfigDict( - extra="allow", + extra="forbid", ) + +################################################################################################### +# MODEL CORE DEFINITIONS +################################################################################################### + +class WalletAddress(BaseModel): id: AnyUrl = Field(..., description="The URL identifying the wallet address.") publicName: Optional[str] = Field( None, description="A public name for the account. This should be set by the account holder with their provider to provide a hint to counterparties as to the identity of the account holder.", ) - assetCode: AssetCode - assetScale: AssetScale + assetCode: str = Field( + ..., + description="The assetCode is a code that indicates the underlying asset. This SHOULD be an ISO4217 currency code.", + title="Asset code", + ) + assetScale: int = Field( + ..., + description="The scale of amounts denoted in the corresponding asset code.", + title="Asset scale", + ge=0, + le=255 + ) authServer: AnyUrl = Field( ..., description="The URL of the authorization server endpoint for getting grants and access tokens for this wallet address.", @@ -87,19 +81,6 @@ class WalletAddress(BaseModel): ..., description="The URL of the resource server endpoint for performing Open Payments with this wallet address.", ) - - -class Amount(BaseModel): - value: str = Field( - ..., - description="The value is an unsigned 64-bit integer amount, represented as a string.", - ) - assetCode: AssetCode - assetScale: AssetScale - - -class JsonWebKeySet(BaseModel): model_config = ConfigDict( - extra="forbid", + extra="allow", ) - keys: List[JsonWebKey] From 88588c72439c4784d712b3d832bb7f1b47cffdcc Mon Sep 17 00:00:00 2001 From: Gavin Chait Date: Fri, 5 Dec 2025 16:42:57 +0100 Subject: [PATCH 11/13] Convenience parser The parser is useful for: - Normalising wallet addresses from the form `$ilp.` to `https://ilp`. - Converting stored private keys back to PEM format. - Verifying payment response hash to validate buyer authentication. --- src/open_payments_sdk/utils/parser.py | 70 +++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 src/open_payments_sdk/utils/parser.py diff --git a/src/open_payments_sdk/utils/parser.py b/src/open_payments_sdk/utils/parser.py new file mode 100644 index 0000000..257e3d2 --- /dev/null +++ b/src/open_payments_sdk/utils/parser.py @@ -0,0 +1,70 @@ +from hashlib import sha256 +from base64 import b64encode, b64decode +from pydantic import AnyUrl + + +class PaymentsParser: + """ + Convenience class with functions to preprocess and parse Open Payments inputs and response data. + """ + + def normalise_wallet_address(self, *, wallet_address: str) -> str: + """ + Parse `$ilp.wallet.com/name` to `https://ilp.wallet.com/name`. + """ + if isinstance(wallet_address, AnyUrl): + return str(wallet_address) + wallet_address = wallet_address.strip("$").strip("/") + if wallet_address.startswith("https://"): + return wallet_address + return f"https://{wallet_address}" + + def isBase64(self, *, term: str | bytes) -> bool: + # https://stackoverflow.com/a/45928164 + try: + if isinstance(term, str): + # If there's any unicode here, an exception will be thrown and the function will return false + term_bytes = bytes(term, "ascii") + elif isinstance(term, bytes): + term_bytes = term + else: + raise ValueError("Argument must be string or bytes") + return b64encode(b64decode(term_bytes)) == term_bytes + except Exception: + return False + + def convert_private_key_to_PEM(self, *, private_key: str | bytes, format: str = "PRIVATE KEY") -> str: + """ + Private keys must be encapsulated as a PEM block, cf https://cryptography.io/en/latest/hazmat/primitives/asymmetric/serialization/#pem + PEM keys are recognizable because they all begin with `-----BEGIN {format}-----` and end with `-----END {format}-----` + """ + if self.isBase64(term=private_key): + private_key = b64decode(private_key) + if not isinstance(private_key, str): + private_key = private_key.decode("utf-8") + if private_key.startswith("-----BEGIN ") and "-----END " in private_key: + return private_key + return f"-----BEGIN {format}-----\n{private_key}\n-----END {format}-----\n" + + def verify_response_hash( + self, *, incoming_payment_id: str, finish_id: str, interact_ref: str, auth_server_url: str, received_hash: str + ) -> bool: + """ + After a resource owner allows a client to access their account, the client must verify that the resource owner provided their consent. + The client verifies consent by calculating the hash issued by the authorization server. + + https://openpayments.dev/identity/hash-verification/ + + NOTE: + - `incoming_payment_id` is the key sent in the `get_purchase_endpoint` step. + - `finish_id` is received as part of the redirect response; `response.interact.finish`. + - `interact_ref` is received after the buyer approves payment, and is sent to the endpoint allocated for the transaction. + - `auth_server_url` is the buyer's authentication server. + - `received` is the hash received, and is used for comparison. + """ + data = f"{incoming_payment_id}\n{finish_id}\n{interact_ref}\n{auth_server_url}".encode("utf-8") + calculated = b64encode(sha256(data).digest()) + return calculated.decode() == received_hash + + +paymentsparser = PaymentsParser() From a06e838260507a6136a735e9c6652436178a190a Mon Sep 17 00:00:00 2001 From: Gavin Chait Date: Fri, 5 Dec 2025 16:47:35 +0100 Subject: [PATCH 12/13] Convenience function for processing payments This is the simplest function for processing a one-off payment between a purchaser and seller. Includes: - Pydantic models defining a seller account, and a pending incoming payment transaction that can be used to store stateless interactions (i.e. generate approval link -> third-party approval -> redirect for transaction completion) - One-time payment process workflow. A README covers the context and future functionality for this feature. --- src/open_payments_sdk/process/README.md | 63 +++++ .../process/crud/__init__.py | 1 + src/open_payments_sdk/process/crud/process.py | 241 ++++++++++++++++++ .../process/models/__init__.py | 1 + .../process/models/process.py | 80 ++++++ 5 files changed, 386 insertions(+) create mode 100644 src/open_payments_sdk/process/README.md create mode 100644 src/open_payments_sdk/process/crud/__init__.py create mode 100644 src/open_payments_sdk/process/crud/process.py create mode 100644 src/open_payments_sdk/process/models/__init__.py create mode 100644 src/open_payments_sdk/process/models/process.py diff --git a/src/open_payments_sdk/process/README.md b/src/open_payments_sdk/process/README.md new file mode 100644 index 0000000..b815b4b --- /dev/null +++ b/src/open_payments_sdk/process/README.md @@ -0,0 +1,63 @@ +# Structured processes + +The functions in this module support commonly-used payments processes. For anything more complex, or where you wish for greater control, you can fall back to the standard API. + +## Standard payment processes + +1. **One-off**: a payment - usually of purchase and sale - between a buyer and seller which happens once. +2. **Recurring**: a payment - usually a subscription, or structured fee - between a buyer and seller. +3. **Conditional**: a payment which is conditional on some additional factor - e.g. the seller is required to perform some task, or a threshold of purchases are required - before funds are transfered. + +There are additional characteristics as well, such as **split payments** between multiple recipients, which may be required. + +Each of these has a common set of steps which are performed on behalf of the corresponding payments actors (cf. https://openpayments.dev/concepts/op-flow/): + +1. Get purchaser / buyer's wallet address information +2. Request an _Incoming Payment Grant_ +3. Create an _Incoming Payment_ +4. Request a _Quote Grant_ +5. Create a _Quote_ +6. Request an interactive _Outgoing Payment Grant_ +7. Start interaction with the purchaser +8. Finish interaction with the purchaser +9. Request a _Grant Continuation_ +10. Create an _Outgoing Payment_ + +**NOTE**: Steps 7 and 8 are discontinuous and stateless. This set of convenience functions does **NOT** store your transaction state. That is your responsibility. + +## Development requirements + +There are several "externalities" necessary to use these functions: + +1. You need to develop some mechanism for storing asynchronous / stateless information (e.g. text files or a database) received during the grant-making process. +2. You need to have the recipient / seller's information in advance, including their private key to permit independent grant requests on their behalf (`wallet_address`, `private_key`, `key_id`). +3. You must store the initial outgoing payment grant response to recover the purchase request. The merchant response will return a reference ID you provided, and which you can use to recover the specified transaction. +4. You need a web-based client which can receive the purchaser's merchant / wallet response and dispatch that response for further processing. The purchaser's merchant response will be directed to a specified URL endpoint. + +**NOTE**: there is nothing stopping you simply executing the grant continuation and outgoing payments on receipt of the merchant response, but you should also execute the following: + +1. Validate the `hash` received from the merchant which is derived from the nonce / key, a finish ID, interactive reference, and authentication server URL, all of which are stored in the outgoing payment grant. +2. After payment, validate that the amount - with any transaction fees factored in - has been transferred to the recipient. + +Only then should you consider the transaction successfully completed. + +## Architecture decisions + +This module is opinionated and implements the process in ways that may conflict with your way of working. If you find this challenging, the API is available to you. + +One specific example is that of using [ULIDs](https://github.com/ulid/spec) in preference to UUIDs. ULIDs are prefered as they are: + +- more memory-efficient than UUIDs, being a 26-character string rather than a 36-character string, +- lexicographically sortable as each string generated has a temporal sequence, +- case-insensitive and is URL safe. + +This is used in generating reference IDs. + +## Current state of this module + +Only one-off payments are currently developed. This note will be updated as progress is made. + +## Tests + +These modules are used to test the functionality of the API and general utilities in the SDK. + diff --git a/src/open_payments_sdk/process/crud/__init__.py b/src/open_payments_sdk/process/crud/__init__.py new file mode 100644 index 0000000..a05f332 --- /dev/null +++ b/src/open_payments_sdk/process/crud/__init__.py @@ -0,0 +1 @@ +from .process import OpenPaymentsProcessor # noqa: F401 diff --git a/src/open_payments_sdk/process/crud/process.py b/src/open_payments_sdk/process/crud/process.py new file mode 100644 index 0000000..84252ac --- /dev/null +++ b/src/open_payments_sdk/process/crud/process.py @@ -0,0 +1,241 @@ +from ulid import ULID +from pydantic import AnyUrl, HttpUrl + +from ...http import HttpClient +from ...client.client import OpenPaymentsClient +from ...api.auth import GrantRequest, Grant, InteractRef +from ...models.resource import ( + IncomingPaymentRequest, + OutgoingPaymentRequest, + OutgoingPayment, + Quote, + QuoteRequest, +) +from ...utils.parser import paymentsparser +from ..models.process import SellerOpenPaymentAccount, PendingIncomingPaymentTransaction + + +class OpenPaymentsProcessor: + """ + Core functions for processing open payments on behalf of an instance actor merchant account. + + Based on https://openpayments.dev/concepts/op-flow/ + + 1. Get recipient's wallet address information + 2. Request an Incoming Payment grant + 3. Create an Incoming Payment + 4. Request a Quote grant + 5. Create a Quote + 6. Request an interactive Outgoing Payment grant + 7. Start interaction with the user + 8. Finish interaction with the user + 9. Request a grant continuation + 10. Create an Outgoing Payment + + The key break is the interactive phase. It is necessary to save the buyer- + """ + + def __init__( + self, + *, + seller: SellerOpenPaymentAccount, + buyer: str, + http_client: HttpClient | None = None, + redirect_uri: str, + ) -> None: + if not http_client: + http_client = HttpClient(http_timeout=10.0) + self.http_client = http_client + self.seller = seller + self.buyer = paymentsparser.normalise_wallet_address(wallet_address=buyer) + self.client = OpenPaymentsClient( + keyid=self.seller.keyId, + private_key=self.seller.privateKey, + client_wallet_address=self.seller.walletAddressUrl, + http_client=self.http_client, + ) + self.seller_wallet = self.client.wallet.get_wallet_address(self.seller.walletAddressUrl) + self.buyer_wallet = self.client.wallet.get_wallet_address(self.buyer) + self.pending_payment = PendingIncomingPaymentTransaction( + **{"id": ULID(), "seller": self.seller_wallet, "buyer": self.buyer_wallet} + ) + self.redirect_uri = f"{redirect_uri}{self.pending_payment.id}" + + ################################################################################################### + # 1. GRANT-MAKING GENERAL UTILITY + ################################################################################################### + + def request_grant(self, *, grant: str, actions: list[str], endpoint: AnyUrl) -> Grant: + request = GrantRequest( + **{ + "access_token": { + "access": [ + { + "type": grant, + "actions": actions, + } + ] + }, + "client": str(self.seller_wallet.id), + } + ) + return self.client.grants.post_grant_request(grant_request=request, auth_server_endpoint=str(endpoint)) + + ################################################################################################### + # 2. SELLER INCOMING PAYMENT PROCESS + ################################################################################################### + + def request_incoming_payment(self, *, amount: int | str): + """TO THE SELLER""" + if isinstance(amount, int): + amount = str(amount) + # Request a grant + grant = self.request_grant( + grant="incoming-payment", + actions=["create", "read", "read-all", "complete", "list"], + endpoint=self.seller_wallet.authServer, + ) + grant = grant.model_dump(exclude_unset=True, mode="json") + access_token = grant.get("access_token", {}).get("value") + # Request an incoming payment + payment = IncomingPaymentRequest( + **{ + "walletAddress": str(self.seller_wallet.id), + "incomingAmount": { + "value": amount, + "assetCode": self.seller_wallet.assetCode, + "assetScale": self.seller_wallet.assetScale, + }, + } + ) + return self.client.incoming_payments.post_create_payment( + payment=payment, resource_server_endpoint=str(self.seller_wallet.resourceServer), access_token=access_token + ) + + ################################################################################################### + # 3. BUYER QUOTE REQUEST PROCESS + ################################################################################################### + + def request_quote(self, *, incoming_payment_id: str | HttpUrl | AnyUrl) -> Quote: + """TO THE BUYER""" + # Request a grant + grant = self.request_grant( + grant="quote", actions=["create", "read", "read-all"], endpoint=self.buyer_wallet.authServer + ) + grant = grant.model_dump(exclude_unset=True, mode="json") + access_token = grant.get("access_token", {}).get("value") + # Request a quote for the payment + quote = QuoteRequest( + **{ + "walletAddress": self.buyer_wallet.id, + "receiver": str(incoming_payment_id), + "method": "ilp", + } + ) + return self.client.quotes.post_create_quote( + quote=quote, resource_server_endpoint=str(self.buyer_wallet.resourceServer), access_token=access_token + ) + + ################################################################################################### + # 4. REQUEST BUYER INTERACTIVE GRANT FOR PURCHASE + ################################################################################################### + + def get_purchase_endpoint(self, *, amount: int | str) -> str | AnyUrl: + """ + Implements the first half of the purchase process, requesting 'incoming-payment' and 'quote' grants, + then requesting - and returning - an interactive payment grant for the buyer. + """ + if isinstance(amount, int): + amount = str(amount) + # 1. Request incoming payment grant for the seller + incoming_payment_response = self.request_incoming_payment(amount=amount) + self.pending_payment.incoming_payment_id = incoming_payment_response.id + # 2. Request quote grant for the buyer + quote_response = self.request_quote(incoming_payment_id=incoming_payment_response.id) + self.pending_payment.quote_id = quote_response.id + # 3. Request an interactive payment endpoint for the buyer + # Request a grant + grant_request = GrantRequest( + **{ + "access_token": { + "access": [ + { + "identifier": str(self.buyer_wallet.id), + "type": "outgoing-payment", + "actions": ["create", "read", "read-all", "list", "list-all"], + "limits": { + "debitAmount": { + "assetCode": quote_response.debitAmount.assetCode, + "assetScale": quote_response.debitAmount.assetScale, + "value": quote_response.debitAmount.value, + }, + }, + }, + ], + }, + "client": str(self.seller_wallet.id), + "interact": { + "start": ["redirect"], + "finish": { + "method": "redirect", + "uri": self.redirect_uri, + "nonce": str(self.pending_payment.id), + }, + }, + }, + ) + # Request the interactive endpoint + # TODO: db save of the quote and interactive responses to retrieve later to complete the purchase + interactive_response = self.client.grants.post_grant_request( + grant_request=grant_request, auth_server_endpoint=str(self.buyer_wallet.authServer) + ) + self.pending_payment.interactive_redirect = interactive_response.interact.redirect + self.pending_payment.finish_id = interactive_response.interact.finish + self.pending_payment.continue_id = interactive_response.cont.access_token.value + self.pending_payment.continue_url = interactive_response.cont.uri + return interactive_response.interact.redirect + + ################################################################################################### + # 5. COMPLETE OUTGOING PAYMENT + ################################################################################################### + + def complete_payment( + self, interact_ref: str, received_hash: str, pending_payment: PendingIncomingPaymentTransaction + ) -> OutgoingPayment: + """ + After purchaser approves interactive payment, webhook will receive confirmation permitting continuation. + + Use `key` to retrieve the original interactive grant request, and `interact_ref` to complete payment. + """ + if not pending_payment.finish_id or not pending_payment.continue_id: + e = "Payment completion impossible without both `finish_id` and `continue_id`." + raise ValueError(e) + # First validate the interactive response hash + if not paymentsparser.verify_response_hash( + incoming_payment_id=str(pending_payment.id), + finish_id=pending_payment.finish_id, + interact_ref=interact_ref, + auth_server_url=str(pending_payment.buyer.authServer), + received_hash=received_hash, + ): + raise ValueError(f"Hash invalid for pending payment `{pending_payment.incoming_payment_id}`") + # Request a grant continuation + grant_request = self.client.grants.post_grant_continuation_request( + interact_ref=InteractRef(**dict(interact_ref=interact_ref)), + continue_uri=str(pending_payment.continue_url), + access_token=pending_payment.continue_id, + ) + access_token = grant_request.access_token.value + # Create an outgoing payment from the `buyer` + outgoing_payment_request = OutgoingPaymentRequest( + **{ + "walletAddress": str(pending_payment.buyer.id), + "quoteId": pending_payment.quote_id, + "metadata": {} + } + ) + return self.client.outgoing_payments.post_create_payment( + payment=outgoing_payment_request, + resource_server_endpoint=str(pending_payment.buyer.resourceServer), + access_token=access_token, + ) diff --git a/src/open_payments_sdk/process/models/__init__.py b/src/open_payments_sdk/process/models/__init__.py new file mode 100644 index 0000000..a46af2b --- /dev/null +++ b/src/open_payments_sdk/process/models/__init__.py @@ -0,0 +1 @@ +from .process import PendingIncomingPaymentTransaction, SellerOpenPaymentAccount # noqa: F401 diff --git a/src/open_payments_sdk/process/models/process.py b/src/open_payments_sdk/process/models/process.py new file mode 100644 index 0000000..def1f49 --- /dev/null +++ b/src/open_payments_sdk/process/models/process.py @@ -0,0 +1,80 @@ +from typing import Optional +from pydantic import BaseModel, Field, field_validator, AnyUrl +from ulid import ULID + +from ...models.wallet import WalletAddress +from ...models.resource import Amount +from ...utils.parser import paymentsparser + + +class PendingIncomingPaymentTransaction(BaseModel): + """ + References to recover and continue a pending incoming payment. + """ + + id: ULID = Field(..., description="Tracking key needed to recover the transaction.") + buyer: WalletAddress = Field( + ..., + description="Data for the buyer's open payments wallet", + ) + seller: WalletAddress = Field( + ..., + description="Data for the seller's open payments wallet", + ) + incoming_payment_id: Optional[AnyUrl] = Field( + None, description="URL reference to incoming payment generated during initial grant to seller." + ) + incoming_amount: Optional[Amount] = Field(None, description="Amount requested, including currency code.") + quote_id: Optional[AnyUrl] = Field( + None, description="URL reference to quote generated during initial grant to buyer." + ) + quoted_amount: Optional[Amount] = Field(None, description="Amount quoted, including currency code.") + interactive_redirect: Optional[AnyUrl] = Field(None, description="URL redirect endpoint to send to the buyer.") + finish_id: Optional[str] = Field( + None, description="Random string response from interactive endpoint request, `response.interact.finish`." + ) + continue_id: Optional[str] = Field( + None, + description="Random string response from interactive endpoint request, `response.continue.access_token.value`.", + ) + continue_url: Optional[AnyUrl] = Field( + None, description="URL to request a new access key to complete the incoming payment, `response.continue.uri`." + ) + + @field_validator("buyer", "seller", mode="before") + @classmethod + def evaluate_wallet_address(cls, value): + if isinstance(value, BaseModel): + value = value.model_dump() + return value + + +class SellerOpenPaymentAccount(BaseModel): + """ + Convenience schema to normalise submitted seller open payments data. + """ + + walletAddressUrl: str = Field( + ..., + description="URL for the open payments wallet", + ) + privateKey: str = Field( + ..., + description="The types of actions the client instance will take at the RS as an array of strings.", + ) + keyId: str = Field( + ..., + description="The types of actions the client instance will take at the RS as an array of strings.", + ) + + @field_validator("walletAddressUrl", mode="before") + @classmethod + def evaluate_wallet_address(cls, walletAddressUrl): + walletAddressUrl = paymentsparser.normalise_wallet_address(wallet_address=walletAddressUrl) + return walletAddressUrl + + @field_validator("privateKey", mode="before") + @classmethod + def evaluate_private_key(cls, privateKey): + privateKey = paymentsparser.convert_private_key_to_PEM(private_key=privateKey) + return privateKey From f60c8f23069c2ed0cec80940c691dccd08e9c7d9 Mon Sep 17 00:00:00 2001 From: Gavin Chait Date: Fri, 5 Dec 2025 16:50:43 +0100 Subject: [PATCH 13/13] Updated tests for convenience processes New dev dependencies required: `selenium` for authenticating purchase transactions via web, and `jupyterlab` for dev testing of code. the `.env` requires a test wallet login and password to test the purchase approval workflow. --- .gitignore | 3 +- poetry.lock | 2073 +++++++++++++++++++++++++++--- pyproject.toml | 5 +- tests/.env.example | 4 +- tests/config.py | 7 +- tests/conftest.py | 21 +- tests/integration/test_grants.py | 14 +- tests/integration/test_wallet.py | 4 +- tests/process/test_process.py | 71 + 9 files changed, 1986 insertions(+), 216 deletions(-) create mode 100644 tests/process/test_process.py diff --git a/.gitignore b/.gitignore index 04ebe4d..7888bd3 100644 --- a/.gitignore +++ b/.gitignore @@ -81,6 +81,7 @@ target/ # Jupyter Notebook .ipynb_checkpoints +*.ipynb # IPython profile_default/ @@ -89,7 +90,7 @@ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: -# .python-version +.python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. diff --git a/poetry.lock b/poetry.lock index 3d6a8e7..27010cc 100644 --- a/poetry.lock +++ b/poetry.lock @@ -18,7 +18,7 @@ version = "4.8.0" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a"}, {file = "anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a"}, @@ -35,6 +35,19 @@ doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"] trio = ["trio (>=0.26.1)"] +[[package]] +name = "appnope" +version = "0.1.4" +description = "Disable App Nap on macOS >= 10.9" +optional = false +python-versions = ">=3.6" +groups = ["dev"] +markers = "platform_system == \"Darwin\"" +files = [ + {file = "appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c"}, + {file = "appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee"}, +] + [[package]] name = "argcomplete" version = "3.5.3" @@ -50,6 +63,83 @@ files = [ [package.extras] test = ["coverage", "mypy", "pexpect", "ruff", "wheel"] +[[package]] +name = "argon2-cffi" +version = "25.1.0" +description = "Argon2 for Python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "argon2_cffi-25.1.0-py3-none-any.whl", hash = "sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741"}, + {file = "argon2_cffi-25.1.0.tar.gz", hash = "sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1"}, +] + +[package.dependencies] +argon2-cffi-bindings = "*" + +[[package]] +name = "argon2-cffi-bindings" +version = "25.1.0" +description = "Low-level CFFI bindings for Argon2" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:3d3f05610594151994ca9ccb3c771115bdb4daef161976a266f0dd8aa9996b8f"}, + {file = "argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8b8efee945193e667a396cbc7b4fb7d357297d6234d30a489905d96caabde56b"}, + {file = "argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3c6702abc36bf3ccba3f802b799505def420a1b7039862014a65db3205967f5a"}, + {file = "argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1c70058c6ab1e352304ac7e3b52554daadacd8d453c1752e547c76e9c99ac44"}, + {file = "argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2fd3bfbff3c5d74fef31a722f729bf93500910db650c925c2d6ef879a7e51cb"}, + {file = "argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4f9665de60b1b0e99bcd6be4f17d90339698ce954cfd8d9cf4f91c995165a92"}, + {file = "argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ba92837e4a9aa6a508c8d2d7883ed5a8f6c308c89a4790e1e447a220deb79a85"}, + {file = "argon2_cffi_bindings-25.1.0-cp314-cp314t-win32.whl", hash = "sha256:84a461d4d84ae1295871329b346a97f68eade8c53b6ed9a7ca2d7467f3c8ff6f"}, + {file = "argon2_cffi_bindings-25.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b55aec3565b65f56455eebc9b9f34130440404f27fe21c3b375bf1ea4d8fbae6"}, + {file = "argon2_cffi_bindings-25.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:87c33a52407e4c41f3b70a9c2d3f6056d88b10dad7695be708c5021673f55623"}, + {file = "argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:aecba1723ae35330a008418a91ea6cfcedf6d31e5fbaa056a166462ff066d500"}, + {file = "argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2630b6240b495dfab90aebe159ff784d08ea999aa4b0d17efa734055a07d2f44"}, + {file = "argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:7aef0c91e2c0fbca6fc68e7555aa60ef7008a739cbe045541e438373bc54d2b0"}, + {file = "argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e021e87faa76ae0d413b619fe2b65ab9a037f24c60a1e6cc43457ae20de6dc6"}, + {file = "argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e924cfc503018a714f94a49a149fdc0b644eaead5d1f089330399134fa028a"}, + {file = "argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87b72589133f0346a1cb8d5ecca4b933e3c9b64656c9d175270a000e73b288d"}, + {file = "argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1db89609c06afa1a214a69a462ea741cf735b29a57530478c06eb81dd403de99"}, + {file = "argon2_cffi_bindings-25.1.0-cp39-abi3-win32.whl", hash = "sha256:473bcb5f82924b1becbb637b63303ec8d10e84c8d241119419897a26116515d2"}, + {file = "argon2_cffi_bindings-25.1.0-cp39-abi3-win_amd64.whl", hash = "sha256:a98cd7d17e9f7ce244c0803cad3c23a7d379c301ba618a5fa76a67d116618b98"}, + {file = "argon2_cffi_bindings-25.1.0-cp39-abi3-win_arm64.whl", hash = "sha256:b0fdbcf513833809c882823f98dc2f931cf659d9a1429616ac3adebb49f5db94"}, + {file = "argon2_cffi_bindings-25.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6dca33a9859abf613e22733131fc9194091c1fa7cb3e131c143056b4856aa47e"}, + {file = "argon2_cffi_bindings-25.1.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:21378b40e1b8d1655dd5310c84a40fc19a9aa5e6366e835ceb8576bf0fea716d"}, + {file = "argon2_cffi_bindings-25.1.0-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d588dec224e2a83edbdc785a5e6f3c6cd736f46bfd4b441bbb5aa1f5085e584"}, + {file = "argon2_cffi_bindings-25.1.0-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5acb4e41090d53f17ca1110c3427f0a130f944b896fc8c83973219c97f57b690"}, + {file = "argon2_cffi_bindings-25.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:da0c79c23a63723aa5d782250fbf51b768abca630285262fb5144ba5ae01e520"}, + {file = "argon2_cffi_bindings-25.1.0.tar.gz", hash = "sha256:b957f3e6ea4d55d820e40ff76f450952807013d361a65d7f28acc0acbf29229d"}, +] + +[package.dependencies] +cffi = [ + {version = ">=1.0.1", markers = "python_version < \"3.14\""}, + {version = ">=2.0.0b1", markers = "python_version >= \"3.14\""}, +] + +[[package]] +name = "arrow" +version = "1.4.0" +description = "Better dates & times for Python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "arrow-1.4.0-py3-none-any.whl", hash = "sha256:749f0769958ebdc79c173ff0b0670d59051a535fa26e8eba02953dc19eb43205"}, + {file = "arrow-1.4.0.tar.gz", hash = "sha256:ed0cc050e98001b8779e84d461b0098c4ac597e88704a655582b21d116e526d7"}, +] + +[package.dependencies] +python-dateutil = ">=2.7.0" +tzdata = {version = "*", markers = "python_version >= \"3.9\""} + +[package.extras] +doc = ["doc8", "sphinx (>=7.0.0)", "sphinx-autobuild", "sphinx-autodoc-typehints", "sphinx_rtd_theme (>=1.3.0)"] +test = ["dateparser (==1.*)", "pre-commit", "pytest", "pytest-cov", "pytest-mock", "pytz (==2025.2)", "simplejson (==3.*)"] + [[package]] name = "asttokens" version = "3.0.0" @@ -66,6 +156,71 @@ files = [ astroid = ["astroid (>=2,<4)"] test = ["astroid (>=2,<4)", "pytest", "pytest-cov", "pytest-xdist"] +[[package]] +name = "async-lru" +version = "2.0.5" +description = "Simple LRU cache for asyncio" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "async_lru-2.0.5-py3-none-any.whl", hash = "sha256:ab95404d8d2605310d345932697371a5f40def0487c03d6d0ad9138de52c9943"}, + {file = "async_lru-2.0.5.tar.gz", hash = "sha256:481d52ccdd27275f42c43a928b4a50c3bfb2d67af4e78b170e3e0bb39c66e5bb"}, +] + +[package.dependencies] +typing_extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} + +[[package]] +name = "attrs" +version = "25.4.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373"}, + {file = "attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11"}, +] + +[[package]] +name = "babel" +version = "2.17.0" +description = "Internationalization utilities" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2"}, + {file = "babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d"}, +] + +[package.extras] +dev = ["backports.zoneinfo", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata"] + +[[package]] +name = "beautifulsoup4" +version = "4.14.3" +description = "Screen-scraping library" +optional = false +python-versions = ">=3.7.0" +groups = ["dev"] +files = [ + {file = "beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb"}, + {file = "beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86"}, +] + +[package.dependencies] +soupsieve = ">=1.6.1" +typing-extensions = ">=4.0.0" + +[package.extras] +cchardet = ["cchardet"] +chardet = ["chardet"] +charset-normalizer = ["charset-normalizer"] +html5lib = ["html5lib"] +lxml = ["lxml"] + [[package]] name = "black" version = "25.1.0" @@ -113,98 +268,257 @@ d = ["aiohttp (>=3.10)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] +[[package]] +name = "bleach" +version = "6.3.0" +description = "An easy safelist-based HTML-sanitizing tool." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "bleach-6.3.0-py3-none-any.whl", hash = "sha256:fe10ec77c93ddf3d13a73b035abaac7a9f5e436513864ccdad516693213c65d6"}, + {file = "bleach-6.3.0.tar.gz", hash = "sha256:6f3b91b1c0a02bb9a78b5a454c92506aa0fdf197e1d5e114d2e00c6f64306d22"}, +] + +[package.dependencies] +tinycss2 = {version = ">=1.1.0,<1.5", optional = true, markers = "extra == \"css\""} +webencodings = "*" + +[package.extras] +css = ["tinycss2 (>=1.1.0,<1.5)"] + [[package]] name = "certifi" -version = "2025.1.31" +version = "2025.11.12" description = "Python package for providing Mozilla's CA Bundle." optional = false -python-versions = ">=3.6" -groups = ["main"] +python-versions = ">=3.7" +groups = ["main", "dev"] files = [ - {file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"}, - {file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"}, + {file = "certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b"}, + {file = "certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316"}, ] [[package]] name = "cffi" -version = "1.17.1" +version = "2.0.0" description = "Foreign Function Interface for Python calling C code." optional = false -python-versions = ">=3.8" -groups = ["main"] -markers = "platform_python_implementation != \"PyPy\"" -files = [ - {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, - {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, - {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, - {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, - {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, - {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, - {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, - {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, - {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, - {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, - {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, - {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, - {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, - {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, - {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, - {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, - {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, - {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, - {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, - {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, - {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, - {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, - {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, - {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, - {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, - {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, - {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, - {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, - {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, - {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, - {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, - {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, - {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, - {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, - {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, - {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, - {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"}, + {file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb"}, + {file = "cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a"}, + {file = "cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739"}, + {file = "cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe"}, + {file = "cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743"}, + {file = "cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5"}, + {file = "cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5"}, + {file = "cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d"}, + {file = "cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d"}, + {file = "cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba"}, + {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94"}, + {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187"}, + {file = "cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18"}, + {file = "cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5"}, + {file = "cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6"}, + {file = "cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb"}, + {file = "cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26"}, + {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c"}, + {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b"}, + {file = "cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27"}, + {file = "cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75"}, + {file = "cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91"}, + {file = "cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5"}, + {file = "cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775"}, + {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205"}, + {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1"}, + {file = "cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f"}, + {file = "cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25"}, + {file = "cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad"}, + {file = "cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9"}, + {file = "cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592"}, + {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512"}, + {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4"}, + {file = "cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e"}, + {file = "cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6"}, + {file = "cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9"}, + {file = "cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"}, + {file = "cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322"}, + {file = "cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a"}, + {file = "cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9"}, + {file = "cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529"}, ] +markers = {main = "platform_python_implementation != \"PyPy\""} [package.dependencies] -pycparser = "*" +pycparser = {version = "*", markers = "implementation_name != \"PyPy\""} + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-win32.whl", hash = "sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-win32.whl", hash = "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-win_arm64.whl", hash = "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50"}, + {file = "charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f"}, + {file = "charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a"}, +] [[package]] name = "click" @@ -234,6 +548,21 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "comm" +version = "0.2.3" +description = "Jupyter Python Comm implementation, for usage in ipykernel, xeus-python etc." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "comm-0.2.3-py3-none-any.whl", hash = "sha256:c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417"}, + {file = "comm-0.2.3.tar.gz", hash = "sha256:2dc8048c10962d55d7ad693be1e7045d891b7ce8d999c97963a5e3e99c055971"}, +] + +[package.extras] +test = ["pytest"] + [[package]] name = "cryptography" version = "45.0.4" @@ -325,6 +654,46 @@ graphql = ["graphql-core (>=3.2.3)"] http = ["httpx (>=0.24.1)"] validation = ["openapi-spec-validator (>=0.2.8,<0.7)", "prance (>=0.18.2)"] +[[package]] +name = "debugpy" +version = "1.8.17" +description = "An implementation of the Debug Adapter Protocol for Python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "debugpy-1.8.17-cp310-cp310-macosx_15_0_x86_64.whl", hash = "sha256:c41d2ce8bbaddcc0009cc73f65318eedfa3dbc88a8298081deb05389f1ab5542"}, + {file = "debugpy-1.8.17-cp310-cp310-manylinux_2_34_x86_64.whl", hash = "sha256:1440fd514e1b815edd5861ca394786f90eb24960eb26d6f7200994333b1d79e3"}, + {file = "debugpy-1.8.17-cp310-cp310-win32.whl", hash = "sha256:3a32c0af575749083d7492dc79f6ab69f21b2d2ad4cd977a958a07d5865316e4"}, + {file = "debugpy-1.8.17-cp310-cp310-win_amd64.whl", hash = "sha256:a3aad0537cf4d9c1996434be68c6c9a6d233ac6f76c2a482c7803295b4e4f99a"}, + {file = "debugpy-1.8.17-cp311-cp311-macosx_15_0_universal2.whl", hash = "sha256:d3fce3f0e3de262a3b67e69916d001f3e767661c6e1ee42553009d445d1cd840"}, + {file = "debugpy-1.8.17-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:c6bdf134457ae0cac6fb68205776be635d31174eeac9541e1d0c062165c6461f"}, + {file = "debugpy-1.8.17-cp311-cp311-win32.whl", hash = "sha256:e79a195f9e059edfe5d8bf6f3749b2599452d3e9380484cd261f6b7cd2c7c4da"}, + {file = "debugpy-1.8.17-cp311-cp311-win_amd64.whl", hash = "sha256:b532282ad4eca958b1b2d7dbcb2b7218e02cb934165859b918e3b6ba7772d3f4"}, + {file = "debugpy-1.8.17-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:f14467edef672195c6f6b8e27ce5005313cb5d03c9239059bc7182b60c176e2d"}, + {file = "debugpy-1.8.17-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:24693179ef9dfa20dca8605905a42b392be56d410c333af82f1c5dff807a64cc"}, + {file = "debugpy-1.8.17-cp312-cp312-win32.whl", hash = "sha256:6a4e9dacf2cbb60d2514ff7b04b4534b0139facbf2abdffe0639ddb6088e59cf"}, + {file = "debugpy-1.8.17-cp312-cp312-win_amd64.whl", hash = "sha256:e8f8f61c518952fb15f74a302e068b48d9c4691768ade433e4adeea961993464"}, + {file = "debugpy-1.8.17-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:857c1dd5d70042502aef1c6d1c2801211f3ea7e56f75e9c335f434afb403e464"}, + {file = "debugpy-1.8.17-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:3bea3b0b12f3946e098cce9b43c3c46e317b567f79570c3f43f0b96d00788088"}, + {file = "debugpy-1.8.17-cp313-cp313-win32.whl", hash = "sha256:e34ee844c2f17b18556b5bbe59e1e2ff4e86a00282d2a46edab73fd7f18f4a83"}, + {file = "debugpy-1.8.17-cp313-cp313-win_amd64.whl", hash = "sha256:6c5cd6f009ad4fca8e33e5238210dc1e5f42db07d4b6ab21ac7ffa904a196420"}, + {file = "debugpy-1.8.17-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:045290c010bcd2d82bc97aa2daf6837443cd52f6328592698809b4549babcee1"}, + {file = "debugpy-1.8.17-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:b69b6bd9dba6a03632534cdf67c760625760a215ae289f7489a452af1031fe1f"}, + {file = "debugpy-1.8.17-cp314-cp314-win32.whl", hash = "sha256:5c59b74aa5630f3a5194467100c3b3d1c77898f9ab27e3f7dc5d40fc2f122670"}, + {file = "debugpy-1.8.17-cp314-cp314-win_amd64.whl", hash = "sha256:893cba7bb0f55161de4365584b025f7064e1f88913551bcd23be3260b231429c"}, + {file = "debugpy-1.8.17-cp38-cp38-macosx_15_0_x86_64.whl", hash = "sha256:8deb4e31cd575c9f9370042876e078ca118117c1b5e1f22c32befcfbb6955f0c"}, + {file = "debugpy-1.8.17-cp38-cp38-manylinux_2_34_x86_64.whl", hash = "sha256:b75868b675949a96ab51abc114c7163f40ff0d8f7d6d5fd63f8932fd38e9c6d7"}, + {file = "debugpy-1.8.17-cp38-cp38-win32.whl", hash = "sha256:17e456da14848d618662354e1dccfd5e5fb75deec3d1d48dc0aa0baacda55860"}, + {file = "debugpy-1.8.17-cp38-cp38-win_amd64.whl", hash = "sha256:e851beb536a427b5df8aa7d0c7835b29a13812f41e46292ff80b2ef77327355a"}, + {file = "debugpy-1.8.17-cp39-cp39-macosx_15_0_x86_64.whl", hash = "sha256:f2ac8055a0c4a09b30b931100996ba49ef334c6947e7ae365cdd870416d7513e"}, + {file = "debugpy-1.8.17-cp39-cp39-manylinux_2_34_x86_64.whl", hash = "sha256:eaa85bce251feca8e4c87ce3b954aba84b8c645b90f0e6a515c00394a9f5c0e7"}, + {file = "debugpy-1.8.17-cp39-cp39-win32.whl", hash = "sha256:b13eea5587e44f27f6c48588b5ad56dcb74a4f3a5f89250443c94587f3eb2ea1"}, + {file = "debugpy-1.8.17-cp39-cp39-win_amd64.whl", hash = "sha256:bb1bbf92317e1f35afcf3ef0450219efb3afe00be79d8664b250ac0933b9015f"}, + {file = "debugpy-1.8.17-py2.py3-none-any.whl", hash = "sha256:60c7dca6571efe660ccb7a9508d73ca14b8796c4ed484c2002abba714226cfef"}, + {file = "debugpy-1.8.17.tar.gz", hash = "sha256:fd723b47a8c08892b1a16b2c6239a8b96637c62a59b94bb5dab4bac592a58a8e"}, +] + [[package]] name = "decorator" version = "5.1.1" @@ -337,6 +706,18 @@ files = [ {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, ] +[[package]] +name = "defusedxml" +version = "0.7.1" +description = "XML bomb protection for Python stdlib modules" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +groups = ["dev"] +files = [ + {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"}, + {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, +] + [[package]] name = "exceptiongroup" version = "1.3.0" @@ -371,6 +752,33 @@ files = [ [package.extras] tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich"] +[[package]] +name = "fastjsonschema" +version = "2.21.2" +description = "Fastest Python implementation of JSON schema" +optional = false +python-versions = "*" +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"}, +] + +[package.extras] +devel = ["colorama", "json-spec", "jsonschema", "pylint", "pytest", "pytest-benchmark", "pytest-cache", "validictory"] + +[[package]] +name = "fqdn" +version = "1.4.0" +description = "Validate fully-qualified domain names compliant to RFC 1035 and the preferred form in RFC 3686 s. 2." +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "fqdn-1.4.0-py3-none-any.whl", hash = "sha256:e935616ae81c9c60a22267593fe8e6af68cecc68549cc71bb9bfbcbbcb383386"}, + {file = "fqdn-1.4.0.tar.gz", hash = "sha256:30e8f2e685ce87cdace4712fd97c5d09f5e6fa519bbb66e8f188f6a7cb3a5c4e"}, +] + [[package]] name = "genson" version = "1.3.0" @@ -389,7 +797,7 @@ version = "0.14.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" optional = false python-versions = ">=3.7" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, @@ -456,7 +864,7 @@ version = "1.0.7" description = "A minimal low-level HTTP client." optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd"}, {file = "httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c"}, @@ -478,7 +886,7 @@ version = "0.28.1" description = "The next generation HTTP client." optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, @@ -503,7 +911,7 @@ version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.6" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, @@ -557,6 +965,40 @@ decorator = {version = "*", markers = "python_version > \"3.6\""} ipython = {version = ">=7.31.1", markers = "python_version > \"3.6\""} tomli = {version = "*", markers = "python_version > \"3.6\" and python_version < \"3.11\""} +[[package]] +name = "ipykernel" +version = "7.1.0" +description = "IPython Kernel for Jupyter" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "ipykernel-7.1.0-py3-none-any.whl", hash = "sha256:763b5ec6c5b7776f6a8d7ce09b267693b4e5ce75cb50ae696aaefb3c85e1ea4c"}, + {file = "ipykernel-7.1.0.tar.gz", hash = "sha256:58a3fc88533d5930c3546dc7eac66c6d288acde4f801e2001e65edc5dc9cf0db"}, +] + +[package.dependencies] +appnope = {version = ">=0.1.2", markers = "platform_system == \"Darwin\""} +comm = ">=0.1.1" +debugpy = ">=1.6.5" +ipython = ">=7.23.1" +jupyter-client = ">=8.0.0" +jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +matplotlib-inline = ">=0.1" +nest-asyncio = ">=1.4" +packaging = ">=22" +psutil = ">=5.7" +pyzmq = ">=25" +tornado = ">=6.2" +traitlets = ">=5.4.0" + +[package.extras] +cov = ["coverage[toml]", "matplotlib", "pytest-cov", "trio"] +docs = ["intersphinx-registry", "myst-parser", "pydata-sphinx-theme", "sphinx (<8.2.0)", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling", "trio"] +pyqt5 = ["pyqt5"] +pyside6 = ["pyside6"] +test = ["flaky", "ipyparallel", "pre-commit", "pytest (>=7.0,<9)", "pytest-asyncio (>=0.23.5)", "pytest-cov", "pytest-timeout"] + [[package]] name = "ipython" version = "8.32.0" @@ -596,6 +1038,21 @@ qtconsole = ["qtconsole"] test = ["packaging", "pickleshare", "pytest", "pytest-asyncio (<0.22)", "testpath"] test-extra = ["curio", "ipython[test]", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.23)", "pandas", "trio"] +[[package]] +name = "isoduration" +version = "20.11.0" +description = "Operations with ISO 8601 durations" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "isoduration-20.11.0-py3-none-any.whl", hash = "sha256:b2904c2a4228c3d44f409c8ae8e2370eb21a26f7ac2ec5446df141dde3452042"}, + {file = "isoduration-20.11.0.tar.gz", hash = "sha256:ac2f9015137935279eac671f94f89eb00584f940f5dc49462a0c4ee692ba1bd9"}, +] + +[package.dependencies] +arrow = ">=0.15.0" + [[package]] name = "isort" version = "6.0.0" @@ -651,103 +1108,564 @@ MarkupSafe = ">=2.0" i18n = ["Babel (>=2.7)"] [[package]] -name = "markupsafe" -version = "3.0.2" -description = "Safely add untrusted strings to HTML/XML markup." +name = "json5" +version = "0.12.1" +description = "A Python implementation of the JSON5 data format." optional = false -python-versions = ">=3.9" +python-versions = ">=3.8.0" groups = ["dev"] files = [ - {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, - {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, + {file = "json5-0.12.1-py3-none-any.whl", hash = "sha256:d9c9b3bc34a5f54d43c35e11ef7cb87d8bdd098c6ace87117a7b7e83e705c1d5"}, + {file = "json5-0.12.1.tar.gz", hash = "sha256:b2743e77b3242f8d03c143dd975a6ec7c52e2f2afe76ed934e53503dd4ad4990"}, ] +[package.extras] +dev = ["build (==1.2.2.post1)", "coverage (==7.5.4)", "coverage (==7.8.0)", "mypy (==1.14.1)", "mypy (==1.15.0)", "pip (==25.0.1)", "pylint (==3.2.7)", "pylint (==3.3.6)", "ruff (==0.11.2)", "twine (==6.1.0)", "uv (==0.6.11)"] + [[package]] -name = "matplotlib-inline" -version = "0.1.7" -description = "Inline Matplotlib backend for Jupyter" +name = "jsonpointer" +version = "3.0.0" +description = "Identify specific nodes in a JSON document (RFC 6901)" optional = false -python-versions = ">=3.8" +python-versions = ">=3.7" groups = ["dev"] files = [ - {file = "matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca"}, - {file = "matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90"}, + {file = "jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942"}, + {file = "jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef"}, ] -[package.dependencies] -traitlets = "*" - [[package]] -name = "mypy-extensions" -version = "1.0.0" -description = "Type system extensions for programs checked with the mypy type checker." +name = "jsonschema" +version = "4.25.1" +description = "An implementation of JSON Schema validation for Python" optional = false -python-versions = ">=3.5" +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63"}, + {file = "jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85"}, +] + +[package.dependencies] +attrs = ">=22.2.0" +fqdn = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} +idna = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} +isoduration = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} +jsonpointer = {version = ">1.13", optional = true, markers = "extra == \"format-nongpl\""} +jsonschema-specifications = ">=2023.03.6" +referencing = ">=0.28.4" +rfc3339-validator = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} +rfc3986-validator = {version = ">0.1.0", optional = true, markers = "extra == \"format-nongpl\""} +rfc3987-syntax = {version = ">=1.1.0", optional = true, markers = "extra == \"format-nongpl\""} +rpds-py = ">=0.7.1" +uri-template = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} +webcolors = {version = ">=24.6.0", optional = true, markers = "extra == \"format-nongpl\""} + +[package.extras] +format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] +format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "rfc3987-syntax (>=1.1.0)", "uri-template", "webcolors (>=24.6.0)"] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe"}, + {file = "jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d"}, +] + +[package.dependencies] +referencing = ">=0.31.0" + +[[package]] +name = "jupyter-client" +version = "8.6.3" +description = "Jupyter protocol implementation and client libraries" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "jupyter_client-8.6.3-py3-none-any.whl", hash = "sha256:e8a19cc986cc45905ac3362915f410f3af85424b4c0905e94fa5f2cb08e8f23f"}, + {file = "jupyter_client-8.6.3.tar.gz", hash = "sha256:35b3a0947c4a6e9d589eb97d7d4cd5e90f910ee73101611f01283732bd6d9419"}, +] + +[package.dependencies] +jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +python-dateutil = ">=2.8.2" +pyzmq = ">=23.0" +tornado = ">=6.2" +traitlets = ">=5.3" + +[package.extras] +docs = ["ipykernel", "myst-parser", "pydata-sphinx-theme", "sphinx (>=4)", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"] +test = ["coverage", "ipykernel (>=6.14)", "mypy", "paramiko", "pre-commit", "pytest (<8.2.0)", "pytest-cov", "pytest-jupyter[client] (>=0.4.1)", "pytest-timeout"] + +[[package]] +name = "jupyter-core" +version = "5.9.1" +description = "Jupyter core package. A base package on which Jupyter projects rely." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407"}, + {file = "jupyter_core-5.9.1.tar.gz", hash = "sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508"}, +] + +[package.dependencies] +platformdirs = ">=2.5" +traitlets = ">=5.3" + +[package.extras] +docs = ["intersphinx-registry", "myst-parser", "pydata-sphinx-theme", "sphinx-autodoc-typehints", "sphinxcontrib-spelling", "traitlets"] +test = ["ipykernel", "pre-commit", "pytest (<9)", "pytest-cov", "pytest-timeout"] + +[[package]] +name = "jupyter-events" +version = "0.12.0" +description = "Jupyter Event System library" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "jupyter_events-0.12.0-py3-none-any.whl", hash = "sha256:6464b2fa5ad10451c3d35fabc75eab39556ae1e2853ad0c0cc31b656731a97fb"}, + {file = "jupyter_events-0.12.0.tar.gz", hash = "sha256:fc3fce98865f6784c9cd0a56a20644fc6098f21c8c33834a8d9fe383c17e554b"}, +] + +[package.dependencies] +jsonschema = {version = ">=4.18.0", extras = ["format-nongpl"]} +packaging = "*" +python-json-logger = ">=2.0.4" +pyyaml = ">=5.3" +referencing = "*" +rfc3339-validator = "*" +rfc3986-validator = ">=0.1.1" +traitlets = ">=5.3" + +[package.extras] +cli = ["click", "rich"] +docs = ["jupyterlite-sphinx", "myst-parser", "pydata-sphinx-theme (>=0.16)", "sphinx (>=8)", "sphinxcontrib-spelling"] +test = ["click", "pre-commit", "pytest (>=7.0)", "pytest-asyncio (>=0.19.0)", "pytest-console-scripts", "rich"] + +[[package]] +name = "jupyter-lsp" +version = "2.3.0" +description = "Multi-Language Server WebSocket proxy for Jupyter Notebook/Lab server" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "jupyter_lsp-2.3.0-py3-none-any.whl", hash = "sha256:e914a3cb2addf48b1c7710914771aaf1819d46b2e5a79b0f917b5478ec93f34f"}, + {file = "jupyter_lsp-2.3.0.tar.gz", hash = "sha256:458aa59339dc868fb784d73364f17dbce8836e906cd75fd471a325cba02e0245"}, +] + +[package.dependencies] +jupyter_server = ">=1.1.2" + +[[package]] +name = "jupyter-server" +version = "2.17.0" +description = "The backend—i.e. core services, APIs, and REST endpoints—to Jupyter web applications." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "jupyter_server-2.17.0-py3-none-any.whl", hash = "sha256:e8cb9c7db4251f51ed307e329b81b72ccf2056ff82d50524debde1ee1870e13f"}, + {file = "jupyter_server-2.17.0.tar.gz", hash = "sha256:c38ea898566964c888b4772ae1ed58eca84592e88251d2cfc4d171f81f7e99d5"}, +] + +[package.dependencies] +anyio = ">=3.1.0" +argon2-cffi = ">=21.1" +jinja2 = ">=3.0.3" +jupyter-client = ">=7.4.4" +jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +jupyter-events = ">=0.11.0" +jupyter-server-terminals = ">=0.4.4" +nbconvert = ">=6.4.4" +nbformat = ">=5.3.0" +overrides = {version = ">=5.0", markers = "python_version < \"3.12\""} +packaging = ">=22.0" +prometheus-client = ">=0.9" +pywinpty = {version = ">=2.0.1", markers = "os_name == \"nt\""} +pyzmq = ">=24" +send2trash = ">=1.8.2" +terminado = ">=0.8.3" +tornado = ">=6.2.0" +traitlets = ">=5.6.0" +websocket-client = ">=1.7" + +[package.extras] +docs = ["ipykernel", "jinja2", "jupyter-client", "myst-parser", "nbformat", "prometheus-client", "pydata-sphinx-theme", "send2trash", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-openapi (>=0.8.0)", "sphinxcontrib-spelling", "sphinxemoji", "tornado", "typing-extensions"] +test = ["flaky", "ipykernel", "pre-commit", "pytest (>=7.0,<9)", "pytest-console-scripts", "pytest-jupyter[server] (>=0.7)", "pytest-timeout", "requests"] + +[[package]] +name = "jupyter-server-terminals" +version = "0.5.3" +description = "A Jupyter Server Extension Providing Terminals." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "jupyter_server_terminals-0.5.3-py3-none-any.whl", hash = "sha256:41ee0d7dc0ebf2809c668e0fc726dfaf258fcd3e769568996ca731b6194ae9aa"}, + {file = "jupyter_server_terminals-0.5.3.tar.gz", hash = "sha256:5ae0295167220e9ace0edcfdb212afd2b01ee8d179fe6f23c899590e9b8a5269"}, +] + +[package.dependencies] +pywinpty = {version = ">=2.0.3", markers = "os_name == \"nt\""} +terminado = ">=0.8.3" + +[package.extras] +docs = ["jinja2", "jupyter-server", "mistune (<4.0)", "myst-parser", "nbformat", "packaging", "pydata-sphinx-theme", "sphinxcontrib-github-alt", "sphinxcontrib-openapi", "sphinxcontrib-spelling", "sphinxemoji", "tornado"] +test = ["jupyter-server (>=2.0.0)", "pytest (>=7.0)", "pytest-jupyter[server] (>=0.5.3)", "pytest-timeout"] + +[[package]] +name = "jupyterlab" +version = "4.5.0" +description = "JupyterLab computational environment" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "jupyterlab-4.5.0-py3-none-any.whl", hash = "sha256:88e157c75c1afff64c7dc4b801ec471450b922a4eae4305211ddd40da8201c8a"}, + {file = "jupyterlab-4.5.0.tar.gz", hash = "sha256:aec33d6d8f1225b495ee2cf20f0514f45e6df8e360bdd7ac9bace0b7ac5177ea"}, +] + +[package.dependencies] +async-lru = ">=1.0.0" +httpx = ">=0.25.0,<1" +ipykernel = ">=6.5.0,<6.30.0 || >6.30.0" +jinja2 = ">=3.0.3" +jupyter-core = "*" +jupyter-lsp = ">=2.0.0" +jupyter-server = ">=2.4.0,<3" +jupyterlab-server = ">=2.28.0,<3" +notebook-shim = ">=0.2" +packaging = "*" +setuptools = ">=41.1.0" +tomli = {version = ">=1.2.2", markers = "python_version < \"3.11\""} +tornado = ">=6.2.0" +traitlets = "*" + +[package.extras] +dev = ["build", "bump2version", "coverage", "hatch", "pre-commit", "pytest-cov", "ruff (==0.11.12)"] +docs = ["jsx-lexer", "myst-parser", "pydata-sphinx-theme (>=0.13.0)", "pytest", "pytest-check-links", "pytest-jupyter", "sphinx (>=1.8,<8.2.0)", "sphinx-copybutton"] +docs-screenshots = ["altair (==6.0.0)", "ipython (==8.16.1)", "ipywidgets (==8.1.5)", "jupyterlab-geojson (==3.4.0)", "jupyterlab-language-pack-zh-cn (==4.3.post1)", "matplotlib (==3.10.0)", "nbconvert (>=7.0.0)", "pandas (==2.2.3)", "scipy (==1.15.1)"] +test = ["coverage", "pytest (>=7.0)", "pytest-check-links (>=0.7)", "pytest-console-scripts", "pytest-cov", "pytest-jupyter (>=0.5.3)", "pytest-timeout", "pytest-tornasync", "requests", "requests-cache", "virtualenv"] +upgrade-extension = ["copier (>=9,<10)", "jinja2-time (<0.3)", "pydantic (<3.0)", "pyyaml-include (<3.0)", "tomli-w (<2.0)"] + +[[package]] +name = "jupyterlab-pygments" +version = "0.3.0" +description = "Pygments theme using JupyterLab CSS variables" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "jupyterlab_pygments-0.3.0-py3-none-any.whl", hash = "sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780"}, + {file = "jupyterlab_pygments-0.3.0.tar.gz", hash = "sha256:721aca4d9029252b11cfa9d185e5b5af4d54772bb8072f9b7036f4170054d35d"}, +] + +[[package]] +name = "jupyterlab-server" +version = "2.28.0" +description = "A set of server components for JupyterLab and JupyterLab like applications." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "jupyterlab_server-2.28.0-py3-none-any.whl", hash = "sha256:e4355b148fdcf34d312bbbc80f22467d6d20460e8b8736bf235577dd18506968"}, + {file = "jupyterlab_server-2.28.0.tar.gz", hash = "sha256:35baa81898b15f93573e2deca50d11ac0ae407ebb688299d3a5213265033712c"}, +] + +[package.dependencies] +babel = ">=2.10" +jinja2 = ">=3.0.3" +json5 = ">=0.9.0" +jsonschema = ">=4.18.0" +jupyter-server = ">=1.21,<3" +packaging = ">=21.3" +requests = ">=2.31" + +[package.extras] +docs = ["autodoc-traits", "jinja2 (<3.2.0)", "mistune (<4)", "myst-parser", "pydata-sphinx-theme", "sphinx", "sphinx-copybutton", "sphinxcontrib-openapi (>0.8)"] +openapi = ["openapi-core (>=0.18.0,<0.19.0)", "ruamel-yaml"] +test = ["hatch", "ipykernel", "openapi-core (>=0.18.0,<0.19.0)", "openapi-spec-validator (>=0.6.0,<0.8.0)", "pytest (>=7.0,<8)", "pytest-console-scripts", "pytest-cov", "pytest-jupyter[server] (>=0.6.2)", "pytest-timeout", "requests-mock", "ruamel-yaml", "sphinxcontrib-spelling", "strict-rfc3339", "werkzeug"] + +[[package]] +name = "lark" +version = "1.3.1" +description = "a modern parsing library" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "lark-1.3.1-py3-none-any.whl", hash = "sha256:c629b661023a014c37da873b4ff58a817398d12635d3bbb2c5a03be7fe5d1e12"}, + {file = "lark-1.3.1.tar.gz", hash = "sha256:b426a7a6d6d53189d318f2b6236ab5d6429eaf09259f1ca33eb716eed10d2905"}, +] + +[package.extras] +atomic-cache = ["atomicwrites"] +interegular = ["interegular (>=0.3.1,<0.4.0)"] +nearley = ["js2py"] +regex = ["regex"] + +[[package]] +name = "markupsafe" +version = "3.0.2" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, + {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, +] + +[[package]] +name = "matplotlib-inline" +version = "0.1.7" +description = "Inline Matplotlib backend for Jupyter" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca"}, + {file = "matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90"}, +] + +[package.dependencies] +traitlets = "*" + +[[package]] +name = "mistune" +version = "3.1.4" +description = "A sane and fast Markdown parser with useful plugins and renderers" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "mistune-3.1.4-py3-none-any.whl", hash = "sha256:93691da911e5d9d2e23bc54472892aff676df27a75274962ff9edc210364266d"}, + {file = "mistune-3.1.4.tar.gz", hash = "sha256:b5a7f801d389f724ec702840c11d8fc48f2b33519102fc7ee739e8177b672164"}, +] + +[package.dependencies] +typing-extensions = {version = "*", markers = "python_version < \"3.11\""} + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" groups = ["dev"] files = [ {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, ] +[[package]] +name = "nbclient" +version = "0.10.2" +description = "A client library for executing notebooks. Formerly nbconvert's ExecutePreprocessor." +optional = false +python-versions = ">=3.9.0" +groups = ["dev"] +files = [ + {file = "nbclient-0.10.2-py3-none-any.whl", hash = "sha256:4ffee11e788b4a27fabeb7955547e4318a5298f34342a4bfd01f2e1faaeadc3d"}, + {file = "nbclient-0.10.2.tar.gz", hash = "sha256:90b7fc6b810630db87a6d0c2250b1f0ab4cf4d3c27a299b0cde78a4ed3fd9193"}, +] + +[package.dependencies] +jupyter-client = ">=6.1.12" +jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +nbformat = ">=5.1" +traitlets = ">=5.4" + +[package.extras] +dev = ["pre-commit"] +docs = ["autodoc-traits", "flaky", "ipykernel (>=6.19.3)", "ipython", "ipywidgets", "mock", "moto", "myst-parser", "nbconvert (>=7.1.0)", "pytest (>=7.0,<8)", "pytest-asyncio", "pytest-cov (>=4.0)", "sphinx (>=1.7)", "sphinx-book-theme", "sphinxcontrib-spelling", "testpath", "xmltodict"] +test = ["flaky", "ipykernel (>=6.19.3)", "ipython", "ipywidgets", "nbconvert (>=7.1.0)", "pytest (>=7.0,<8)", "pytest-asyncio", "pytest-cov (>=4.0)", "testpath", "xmltodict"] + +[[package]] +name = "nbconvert" +version = "7.16.6" +description = "Converting Jupyter Notebooks (.ipynb files) to other formats. Output formats include asciidoc, html, latex, markdown, pdf, py, rst, script. nbconvert can be used both as a Python library (`import nbconvert`) or as a command line tool (invoked as `jupyter nbconvert ...`)." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "nbconvert-7.16.6-py3-none-any.whl", hash = "sha256:1375a7b67e0c2883678c48e506dc320febb57685e5ee67faa51b18a90f3a712b"}, + {file = "nbconvert-7.16.6.tar.gz", hash = "sha256:576a7e37c6480da7b8465eefa66c17844243816ce1ccc372633c6b71c3c0f582"}, +] + +[package.dependencies] +beautifulsoup4 = "*" +bleach = {version = "!=5.0.0", extras = ["css"]} +defusedxml = "*" +jinja2 = ">=3.0" +jupyter-core = ">=4.7" +jupyterlab-pygments = "*" +markupsafe = ">=2.0" +mistune = ">=2.0.3,<4" +nbclient = ">=0.5.0" +nbformat = ">=5.7" +packaging = "*" +pandocfilters = ">=1.4.1" +pygments = ">=2.4.1" +traitlets = ">=5.1" + +[package.extras] +all = ["flaky", "ipykernel", "ipython", "ipywidgets (>=7.5)", "myst-parser", "nbsphinx (>=0.2.12)", "playwright", "pydata-sphinx-theme", "pyqtwebengine (>=5.15)", "pytest (>=7)", "sphinx (==5.0.2)", "sphinxcontrib-spelling", "tornado (>=6.1)"] +docs = ["ipykernel", "ipython", "myst-parser", "nbsphinx (>=0.2.12)", "pydata-sphinx-theme", "sphinx (==5.0.2)", "sphinxcontrib-spelling"] +qtpdf = ["pyqtwebengine (>=5.15)"] +qtpng = ["pyqtwebengine (>=5.15)"] +serve = ["tornado (>=6.1)"] +test = ["flaky", "ipykernel", "ipywidgets (>=7.5)", "pytest (>=7)"] +webpdf = ["playwright"] + +[[package]] +name = "nbformat" +version = "5.10.4" +description = "The Jupyter Notebook format" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "nbformat-5.10.4-py3-none-any.whl", hash = "sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b"}, + {file = "nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a"}, +] + +[package.dependencies] +fastjsonschema = ">=2.15" +jsonschema = ">=2.6" +jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +traitlets = ">=5.1" + +[package.extras] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"] +test = ["pep440", "pre-commit", "pytest", "testpath"] + +[[package]] +name = "nest-asyncio" +version = "1.6.0" +description = "Patch asyncio to allow nested event loops" +optional = false +python-versions = ">=3.5" +groups = ["dev"] +files = [ + {file = "nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c"}, + {file = "nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe"}, +] + +[[package]] +name = "notebook-shim" +version = "0.2.4" +description = "A shim layer for notebook traits and config" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "notebook_shim-0.2.4-py3-none-any.whl", hash = "sha256:411a5be4e9dc882a074ccbcae671eda64cceb068767e9a3419096986560e1cef"}, + {file = "notebook_shim-0.2.4.tar.gz", hash = "sha256:b4b2cfa1b65d98307ca24361f5b30fe785b53c3fd07b7a47e89acb5e6ac638cb"}, +] + +[package.dependencies] +jupyter-server = ">=1.8,<3" + +[package.extras] +test = ["pytest", "pytest-console-scripts", "pytest-jupyter", "pytest-tornasync"] + +[[package]] +name = "outcome" +version = "1.3.0.post0" +description = "Capture the outcome of Python function calls." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b"}, + {file = "outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8"}, +] + +[package.dependencies] +attrs = ">=19.2.0" + +[[package]] +name = "overrides" +version = "7.7.0" +description = "A decorator to automatically detect mismatch when overriding a method." +optional = false +python-versions = ">=3.6" +groups = ["dev"] +markers = "python_version < \"3.12\"" +files = [ + {file = "overrides-7.7.0-py3-none-any.whl", hash = "sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49"}, + {file = "overrides-7.7.0.tar.gz", hash = "sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a"}, +] + [[package]] name = "packaging" version = "24.2" @@ -760,6 +1678,18 @@ files = [ {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, ] +[[package]] +name = "pandocfilters" +version = "1.5.1" +description = "Utilities for writing pandoc filters in python" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["dev"] +files = [ + {file = "pandocfilters-1.5.1-py2.py3-none-any.whl", hash = "sha256:93be382804a9cdb0a7267585f157e5d1731bbe5545a85b268d6f5fe6232de2bc"}, + {file = "pandocfilters-1.5.1.tar.gz", hash = "sha256:002b4a555ee4ebc03f8b66307e287fa492e4a77b4ea14d3f934328297bb4939e"}, +] + [[package]] name = "parso" version = "0.8.4" @@ -837,6 +1767,21 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "prometheus-client" +version = "0.23.1" +description = "Python client for the Prometheus monitoring system." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "prometheus_client-0.23.1-py3-none-any.whl", hash = "sha256:dd1913e6e76b59cfe44e7a4b83e01afc9873c1bdfd2ed8739f1e76aeca115f99"}, + {file = "prometheus_client-0.23.1.tar.gz", hash = "sha256:6ae8f9081eaaaf153a2e959d2e6c4f4fb57b12ef76c8c7980202f1e57b48b2ce"}, +] + +[package.extras] +twisted = ["twisted"] + [[package]] name = "prompt-toolkit" version = "3.0.50" @@ -852,6 +1797,39 @@ files = [ [package.dependencies] wcwidth = "*" +[[package]] +name = "psutil" +version = "7.1.3" +description = "Cross-platform lib for process and system monitoring." +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "psutil-7.1.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0005da714eee687b4b8decd3d6cc7c6db36215c9e74e5ad2264b90c3df7d92dc"}, + {file = "psutil-7.1.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:19644c85dcb987e35eeeaefdc3915d059dac7bd1167cdcdbf27e0ce2df0c08c0"}, + {file = "psutil-7.1.3-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95ef04cf2e5ba0ab9eaafc4a11eaae91b44f4ef5541acd2ee91d9108d00d59a7"}, + {file = "psutil-7.1.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1068c303be3a72f8e18e412c5b2a8f6d31750fb152f9cb106b54090296c9d251"}, + {file = "psutil-7.1.3-cp313-cp313t-win_amd64.whl", hash = "sha256:18349c5c24b06ac5612c0428ec2a0331c26443d259e2a0144a9b24b4395b58fa"}, + {file = "psutil-7.1.3-cp313-cp313t-win_arm64.whl", hash = "sha256:c525ffa774fe4496282fb0b1187725793de3e7c6b29e41562733cae9ada151ee"}, + {file = "psutil-7.1.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b403da1df4d6d43973dc004d19cee3b848e998ae3154cc8097d139b77156c353"}, + {file = "psutil-7.1.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ad81425efc5e75da3f39b3e636293360ad8d0b49bed7df824c79764fb4ba9b8b"}, + {file = "psutil-7.1.3-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f33a3702e167783a9213db10ad29650ebf383946e91bc77f28a5eb083496bc9"}, + {file = "psutil-7.1.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fac9cd332c67f4422504297889da5ab7e05fd11e3c4392140f7370f4208ded1f"}, + {file = "psutil-7.1.3-cp314-cp314t-win_amd64.whl", hash = "sha256:3792983e23b69843aea49c8f5b8f115572c5ab64c153bada5270086a2123c7e7"}, + {file = "psutil-7.1.3-cp314-cp314t-win_arm64.whl", hash = "sha256:31d77fcedb7529f27bb3a0472bea9334349f9a04160e8e6e5020f22c59893264"}, + {file = "psutil-7.1.3-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2bdbcd0e58ca14996a42adf3621a6244f1bb2e2e528886959c72cf1e326677ab"}, + {file = "psutil-7.1.3-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:bc31fa00f1fbc3c3802141eede66f3a2d51d89716a194bf2cd6fc68310a19880"}, + {file = "psutil-7.1.3-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3bb428f9f05c1225a558f53e30ccbad9930b11c3fc206836242de1091d3e7dd3"}, + {file = "psutil-7.1.3-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56d974e02ca2c8eb4812c3f76c30e28836fffc311d55d979f1465c1feeb2b68b"}, + {file = "psutil-7.1.3-cp37-abi3-win_amd64.whl", hash = "sha256:f39c2c19fe824b47484b96f9692932248a54c43799a84282cfe58d05a6449efd"}, + {file = "psutil-7.1.3-cp37-abi3-win_arm64.whl", hash = "sha256:bd0d69cee829226a761e92f28140bec9a5ee9d5b4fb4b0cc589068dbfff559b1"}, + {file = "psutil-7.1.3.tar.gz", hash = "sha256:6c86281738d77335af7aec228328e944b30930899ea760ecf33a4dba66be5e74"}, +] + +[package.extras] +dev = ["abi3audit", "black", "check-manifest", "colorama", "coverage", "packaging", "pylint", "pyperf", "pypinfo", "pyreadline", "pytest", "pytest-cov", "pytest-instafail", "pytest-subtests", "pytest-xdist", "pywin32", "requests", "rstcheck", "ruff", "setuptools", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "validate-pyproject[all]", "virtualenv", "vulture", "wheel", "wheel", "wmi"] +test = ["pytest", "pytest-instafail", "pytest-subtests", "pytest-xdist", "pywin32", "setuptools", "wheel", "wmi"] + [[package]] name = "ptyprocess" version = "0.7.0" @@ -859,7 +1837,7 @@ description = "Run a subprocess in a pseudo terminal" optional = false python-versions = "*" groups = ["dev"] -markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\"" +markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\" or os_name != \"nt\"" files = [ {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, @@ -886,12 +1864,12 @@ version = "2.22" description = "C parser in Python" optional = false python-versions = ">=3.8" -groups = ["main"] -markers = "platform_python_implementation != \"PyPy\"" +groups = ["main", "dev"] files = [ {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, ] +markers = {main = "platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\"", dev = "implementation_name != \"PyPy\""} [[package]] name = "pydantic" @@ -1064,58 +2042,139 @@ files = [ ] [[package]] -name = "pygments" -version = "2.19.1" -description = "Pygments is a syntax highlighting package written in Python." +name = "pygments" +version = "2.19.1" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, + {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pysocks" +version = "1.7.1" +description = "A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["dev"] +files = [ + {file = "PySocks-1.7.1-py27-none-any.whl", hash = "sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299"}, + {file = "PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5"}, + {file = "PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"}, +] + +[[package]] +name = "pytest" +version = "8.3.4" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, + {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.5,<2" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["dev"] +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "python-dotenv" +version = "1.2.1" +description = "Read key-value pairs from a .env file and set them as environment variables" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61"}, + {file = "python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6"}, +] + +[package.extras] +cli = ["click (>=5.0)"] + +[[package]] +name = "python-json-logger" +version = "4.0.0" +description = "JSON Log Formatter for the Python Logging Package" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, - {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, + {file = "python_json_logger-4.0.0-py3-none-any.whl", hash = "sha256:af09c9daf6a813aa4cc7180395f50f2a9e5fa056034c9953aec92e381c5ba1e2"}, + {file = "python_json_logger-4.0.0.tar.gz", hash = "sha256:f58e68eb46e1faed27e0f574a55a0455eecd7b8a5b88b85a784519ba3cff047f"}, ] [package.extras] -windows-terminal = ["colorama (>=0.4.6)"] +dev = ["backports.zoneinfo", "black", "build", "freezegun", "mdx_truly_sane_lists", "mike", "mkdocs", "mkdocs-awesome-pages-plugin", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-material (>=8.5)", "mkdocstrings[python]", "msgspec", "mypy", "orjson", "pylint", "pytest", "tzdata", "validate-pyproject[all]"] [[package]] -name = "pytest" -version = "8.3.4" -description = "pytest: simple powerful testing with Python" +name = "python-ulid" +version = "3.1.0" +description = "Universally unique lexicographically sortable identifier" optional = false -python-versions = ">=3.8" -groups = ["dev"] +python-versions = ">=3.9" +groups = ["main"] files = [ - {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, - {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, + {file = "python_ulid-3.1.0-py3-none-any.whl", hash = "sha256:e2cdc979c8c877029b4b7a38a6fba3bc4578e4f109a308419ff4d3ccf0a46619"}, + {file = "python_ulid-3.1.0.tar.gz", hash = "sha256:ff0410a598bc5f6b01b602851a3296ede6f91389f913a5d5f8c496003836f636"}, ] [package.dependencies] -colorama = {version = "*", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} -iniconfig = "*" -packaging = "*" -pluggy = ">=1.5,<2" -tomli = {version = ">=1", markers = "python_version < \"3.11\""} +pydantic = {version = ">=2.0", optional = true, markers = "extra == \"pydantic\""} [package.extras] -dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +pydantic = ["pydantic (>=2.0)"] [[package]] -name = "python-dotenv" -version = "1.2.1" -description = "Read key-value pairs from a .env file and set them as environment variables" +name = "pywinpty" +version = "3.0.2" +description = "Pseudo terminal support for Windows from Python." optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["dev"] +markers = "os_name == \"nt\"" files = [ - {file = "python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61"}, - {file = "python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6"}, + {file = "pywinpty-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:65db57fd3387d71e8372b6a54269cbcd0f6dfa6d4616a29e0af749ec19f5c558"}, + {file = "pywinpty-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:327790d70e4c841ebd9d0f295a780177149aeb405bca44c7115a3de5c2054b23"}, + {file = "pywinpty-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:99fdd9b455f0ad6419aba6731a7a0d2f88ced83c3c94a80ff9533d95fa8d8a9e"}, + {file = "pywinpty-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:18f78b81e4cfee6aabe7ea8688441d30247b73e52cd9657138015c5f4ee13a51"}, + {file = "pywinpty-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:663383ecfab7fc382cc97ea5c4f7f0bb32c2f889259855df6ea34e5df42d305b"}, + {file = "pywinpty-3.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:28297cecc37bee9f24d8889e47231972d6e9e84f7b668909de54f36ca785029a"}, + {file = "pywinpty-3.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:34b55ae9a1b671fe3eae071d86618110538e8eaad18fcb1531c0830b91a82767"}, + {file = "pywinpty-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:3962daf801bc38dd4de872108c424b5338c9a46c6efca5761854cd66370a9022"}, + {file = "pywinpty-3.0.2.tar.gz", hash = "sha256:1505cc4cb248af42cb6285a65c9c2086ee9e7e574078ee60933d5d7fa86fb004"}, ] -[package.extras] -cli = ["click (>=5.0)"] - [[package]] name = "pyyaml" version = "6.0.2" @@ -1179,18 +2238,426 @@ files = [ {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, ] +[[package]] +name = "pyzmq" +version = "27.1.0" +description = "Python bindings for 0MQ" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pyzmq-27.1.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:508e23ec9bc44c0005c4946ea013d9317ae00ac67778bd47519fdf5a0e930ff4"}, + {file = "pyzmq-27.1.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:507b6f430bdcf0ee48c0d30e734ea89ce5567fd7b8a0f0044a369c176aa44556"}, + {file = "pyzmq-27.1.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf7b38f9fd7b81cb6d9391b2946382c8237fd814075c6aa9c3b746d53076023b"}, + {file = "pyzmq-27.1.0-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03ff0b279b40d687691a6217c12242ee71f0fba28bf8626ff50e3ef0f4410e1e"}, + {file = "pyzmq-27.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:677e744fee605753eac48198b15a2124016c009a11056f93807000ab11ce6526"}, + {file = "pyzmq-27.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dd2fec2b13137416a1c5648b7009499bcc8fea78154cd888855fa32514f3dad1"}, + {file = "pyzmq-27.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:08e90bb4b57603b84eab1d0ca05b3bbb10f60c1839dc471fc1c9e1507bef3386"}, + {file = "pyzmq-27.1.0-cp310-cp310-win32.whl", hash = "sha256:a5b42d7a0658b515319148875fcb782bbf118dd41c671b62dae33666c2213bda"}, + {file = "pyzmq-27.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:c0bb87227430ee3aefcc0ade2088100e528d5d3298a0a715a64f3d04c60ba02f"}, + {file = "pyzmq-27.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:9a916f76c2ab8d045b19f2286851a38e9ac94ea91faf65bd64735924522a8b32"}, + {file = "pyzmq-27.1.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:226b091818d461a3bef763805e75685e478ac17e9008f49fce2d3e52b3d58b86"}, + {file = "pyzmq-27.1.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0790a0161c281ca9723f804871b4027f2e8b5a528d357c8952d08cd1a9c15581"}, + {file = "pyzmq-27.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c895a6f35476b0c3a54e3eb6ccf41bf3018de937016e6e18748317f25d4e925f"}, + {file = "pyzmq-27.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bbf8d3630bf96550b3be8e1fc0fea5cbdc8d5466c1192887bd94869da17a63e"}, + {file = "pyzmq-27.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15c8bd0fe0dabf808e2d7a681398c4e5ded70a551ab47482067a572c054c8e2e"}, + {file = "pyzmq-27.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bafcb3dd171b4ae9f19ee6380dfc71ce0390fefaf26b504c0e5f628d7c8c54f2"}, + {file = "pyzmq-27.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e829529fcaa09937189178115c49c504e69289abd39967cd8a4c215761373394"}, + {file = "pyzmq-27.1.0-cp311-cp311-win32.whl", hash = "sha256:6df079c47d5902af6db298ec92151db82ecb557af663098b92f2508c398bb54f"}, + {file = "pyzmq-27.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:190cbf120fbc0fc4957b56866830def56628934a9d112aec0e2507aa6a032b97"}, + {file = "pyzmq-27.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:eca6b47df11a132d1745eb3b5b5e557a7dae2c303277aa0e69c6ba91b8736e07"}, + {file = "pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc"}, + {file = "pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113"}, + {file = "pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233"}, + {file = "pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31"}, + {file = "pyzmq-27.1.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28"}, + {file = "pyzmq-27.1.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856"}, + {file = "pyzmq-27.1.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496"}, + {file = "pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd"}, + {file = "pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf"}, + {file = "pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f"}, + {file = "pyzmq-27.1.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:93ad4b0855a664229559e45c8d23797ceac03183c7b6f5b4428152a6b06684a5"}, + {file = "pyzmq-27.1.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:fbb4f2400bfda24f12f009cba62ad5734148569ff4949b1b6ec3b519444342e6"}, + {file = "pyzmq-27.1.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:e343d067f7b151cfe4eb3bb796a7752c9d369eed007b91231e817071d2c2fec7"}, + {file = "pyzmq-27.1.0-cp313-cp313t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:08363b2011dec81c354d694bdecaef4770e0ae96b9afea70b3f47b973655cc05"}, + {file = "pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d54530c8c8b5b8ddb3318f481297441af102517602b569146185fa10b63f4fa9"}, + {file = "pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3afa12c392f0a44a2414056d730eebc33ec0926aae92b5ad5cf26ebb6cc128"}, + {file = "pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c65047adafe573ff023b3187bb93faa583151627bc9c51fc4fb2c561ed689d39"}, + {file = "pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:90e6e9441c946a8b0a667356f7078d96411391a3b8f80980315455574177ec97"}, + {file = "pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:add071b2d25f84e8189aaf0882d39a285b42fa3853016ebab234a5e78c7a43db"}, + {file = "pyzmq-27.1.0-cp313-cp313t-win32.whl", hash = "sha256:7ccc0700cfdf7bd487bea8d850ec38f204478681ea02a582a8da8171b7f90a1c"}, + {file = "pyzmq-27.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8085a9fba668216b9b4323be338ee5437a235fe275b9d1610e422ccc279733e2"}, + {file = "pyzmq-27.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6bb54ca21bcfe361e445256c15eedf083f153811c37be87e0514934d6913061e"}, + {file = "pyzmq-27.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ce980af330231615756acd5154f29813d553ea555485ae712c491cd483df6b7a"}, + {file = "pyzmq-27.1.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1779be8c549e54a1c38f805e56d2a2e5c009d26de10921d7d51cfd1c8d4632ea"}, + {file = "pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7200bb0f03345515df50d99d3db206a0a6bee1955fbb8c453c76f5bf0e08fb96"}, + {file = "pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01c0e07d558b06a60773744ea6251f769cd79a41a97d11b8bf4ab8f034b0424d"}, + {file = "pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:80d834abee71f65253c91540445d37c4c561e293ba6e741b992f20a105d69146"}, + {file = "pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:544b4e3b7198dde4a62b8ff6685e9802a9a1ebf47e77478a5eb88eca2a82f2fd"}, + {file = "pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cedc4c68178e59a4046f97eca31b148ddcf51e88677de1ef4e78cf06c5376c9a"}, + {file = "pyzmq-27.1.0-cp314-cp314t-win32.whl", hash = "sha256:1f0b2a577fd770aa6f053211a55d1c47901f4d537389a034c690291485e5fe92"}, + {file = "pyzmq-27.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:19c9468ae0437f8074af379e986c5d3d7d7bfe033506af442e8c879732bedbe0"}, + {file = "pyzmq-27.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dc5dbf68a7857b59473f7df42650c621d7e8923fb03fa74a526890f4d33cc4d7"}, + {file = "pyzmq-27.1.0-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:18339186c0ed0ce5835f2656cdfb32203125917711af64da64dbaa3d949e5a1b"}, + {file = "pyzmq-27.1.0-cp38-cp38-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:753d56fba8f70962cd8295fb3edb40b9b16deaa882dd2b5a3a2039f9ff7625aa"}, + {file = "pyzmq-27.1.0-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b721c05d932e5ad9ff9344f708c96b9e1a485418c6618d765fca95d4daacfbef"}, + {file = "pyzmq-27.1.0-cp38-cp38-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be883ff3d722e6085ee3f4afc057a50f7f2e0c72d289fd54df5706b4e3d3a50"}, + {file = "pyzmq-27.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:b2e592db3a93128daf567de9650a2f3859017b3f7a66bc4ed6e4779d6034976f"}, + {file = "pyzmq-27.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ad68808a61cbfbbae7ba26d6233f2a4aa3b221de379ce9ee468aa7a83b9c36b0"}, + {file = "pyzmq-27.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:e2687c2d230e8d8584fbea433c24382edfeda0c60627aca3446aa5e58d5d1831"}, + {file = "pyzmq-27.1.0-cp38-cp38-win32.whl", hash = "sha256:a1aa0ee920fb3825d6c825ae3f6c508403b905b698b6460408ebd5bb04bbb312"}, + {file = "pyzmq-27.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:df7cd397ece96cf20a76fae705d40efbab217d217897a5053267cd88a700c266"}, + {file = "pyzmq-27.1.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:96c71c32fff75957db6ae33cd961439f386505c6e6b377370af9b24a1ef9eafb"}, + {file = "pyzmq-27.1.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:49d3980544447f6bd2968b6ac913ab963a49dcaa2d4a2990041f16057b04c429"}, + {file = "pyzmq-27.1.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:849ca054d81aa1c175c49484afaaa5db0622092b5eccb2055f9f3bb8f703782d"}, + {file = "pyzmq-27.1.0-cp39-cp39-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3970778e74cb7f85934d2b926b9900e92bfe597e62267d7499acc39c9c28e345"}, + {file = "pyzmq-27.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:da96ecdcf7d3919c3be2de91a8c513c186f6762aa6cf7c01087ed74fad7f0968"}, + {file = "pyzmq-27.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:9541c444cfe1b1c0156c5c86ece2bb926c7079a18e7b47b0b1b3b1b875e5d098"}, + {file = "pyzmq-27.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e30a74a39b93e2e1591b58eb1acef4902be27c957a8720b0e368f579b82dc22f"}, + {file = "pyzmq-27.1.0-cp39-cp39-win32.whl", hash = "sha256:b1267823d72d1e40701dcba7edc45fd17f71be1285557b7fe668887150a14b78"}, + {file = "pyzmq-27.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:0c996ded912812a2fcd7ab6574f4ad3edc27cb6510349431e4930d4196ade7db"}, + {file = "pyzmq-27.1.0-cp39-cp39-win_arm64.whl", hash = "sha256:346e9ba4198177a07e7706050f35d733e08c1c1f8ceacd5eb6389d653579ffbc"}, + {file = "pyzmq-27.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c17e03cbc9312bee223864f1a2b13a99522e0dc9f7c5df0177cd45210ac286e6"}, + {file = "pyzmq-27.1.0-pp310-pypy310_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f328d01128373cb6763823b2b4e7f73bdf767834268c565151eacb3b7a392f90"}, + {file = "pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c1790386614232e1b3a40a958454bdd42c6d1811837b15ddbb052a032a43f62"}, + {file = "pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:448f9cb54eb0cee4732b46584f2710c8bc178b0e5371d9e4fc8125201e413a74"}, + {file = "pyzmq-27.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:05b12f2d32112bf8c95ef2e74ec4f1d4beb01f8b5e703b38537f8849f92cb9ba"}, + {file = "pyzmq-27.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:18770c8d3563715387139060d37859c02ce40718d1faf299abddcdcc6a649066"}, + {file = "pyzmq-27.1.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ac25465d42f92e990f8d8b0546b01c391ad431c3bf447683fdc40565941d0604"}, + {file = "pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53b40f8ae006f2734ee7608d59ed661419f087521edbfc2149c3932e9c14808c"}, + {file = "pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f605d884e7c8be8fe1aa94e0a783bf3f591b84c24e4bc4f3e7564c82ac25e271"}, + {file = "pyzmq-27.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c9f7f6e13dff2e44a6afeaf2cf54cee5929ad64afaf4d40b50f93c58fc687355"}, + {file = "pyzmq-27.1.0-pp38-pypy38_pp73-macosx_10_15_x86_64.whl", hash = "sha256:50081a4e98472ba9f5a02850014b4c9b629da6710f8f14f3b15897c666a28f1b"}, + {file = "pyzmq-27.1.0-pp38-pypy38_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:510869f9df36ab97f89f4cff9d002a89ac554c7ac9cadd87d444aa4cf66abd27"}, + {file = "pyzmq-27.1.0-pp38-pypy38_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1f8426a01b1c4098a750973c37131cf585f61c7911d735f729935a0c701b68d3"}, + {file = "pyzmq-27.1.0-pp38-pypy38_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:726b6a502f2e34c6d2ada5e702929586d3ac948a4dbbb7fed9854ec8c0466027"}, + {file = "pyzmq-27.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:bd67e7c8f4654bef471c0b1ca6614af0b5202a790723a58b79d9584dc8022a78"}, + {file = "pyzmq-27.1.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:722ea791aa233ac0a819fc2c475e1292c76930b31f1d828cb61073e2fe5e208f"}, + {file = "pyzmq-27.1.0-pp39-pypy39_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:01f9437501886d3a1dd4b02ef59fb8cc384fa718ce066d52f175ee49dd5b7ed8"}, + {file = "pyzmq-27.1.0-pp39-pypy39_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4a19387a3dddcc762bfd2f570d14e2395b2c9701329b266f83dd87a2b3cbd381"}, + {file = "pyzmq-27.1.0-pp39-pypy39_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c618fbcd069e3a29dcd221739cacde52edcc681f041907867e0f5cc7e85f172"}, + {file = "pyzmq-27.1.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ff8d114d14ac671d88c89b9224c63d6c4e5a613fe8acd5594ce53d752a3aafe9"}, + {file = "pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540"}, +] + +[package.dependencies] +cffi = {version = "*", markers = "implementation_name == \"pypy\""} + +[[package]] +name = "referencing" +version = "0.37.0" +description = "JSON Referencing + Python" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231"}, + {file = "referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8"}, +] + +[package.dependencies] +attrs = ">=22.2.0" +rpds-py = ">=0.7.0" +typing-extensions = {version = ">=4.4.0", markers = "python_version < \"3.13\""} + +[[package]] +name = "requests" +version = "2.32.5" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, + {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset_normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "rfc3339-validator" +version = "0.1.4" +description = "A pure python RFC3339 validator" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +groups = ["dev"] +files = [ + {file = "rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa"}, + {file = "rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b"}, +] + +[package.dependencies] +six = "*" + +[[package]] +name = "rfc3986-validator" +version = "0.1.1" +description = "Pure python rfc3986 validator" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +groups = ["dev"] +files = [ + {file = "rfc3986_validator-0.1.1-py2.py3-none-any.whl", hash = "sha256:2f235c432ef459970b4306369336b9d5dbdda31b510ca1e327636e01f528bfa9"}, + {file = "rfc3986_validator-0.1.1.tar.gz", hash = "sha256:3d44bde7921b3b9ec3ae4e3adca370438eccebc676456449b145d533b240d055"}, +] + +[[package]] +name = "rfc3987-syntax" +version = "1.1.0" +description = "Helper functions to syntactically validate strings according to RFC 3987." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "rfc3987_syntax-1.1.0-py3-none-any.whl", hash = "sha256:6c3d97604e4c5ce9f714898e05401a0445a641cfa276432b0a648c80856f6a3f"}, + {file = "rfc3987_syntax-1.1.0.tar.gz", hash = "sha256:717a62cbf33cffdd16dfa3a497d81ce48a660ea691b1ddd7be710c22f00b4a0d"}, +] + +[package.dependencies] +lark = ">=1.2.2" + +[package.extras] +testing = ["pytest (>=8.3.5)"] + +[[package]] +name = "rpds-py" +version = "0.30.0" +description = "Python bindings to Rust's persistent data structures (rpds)" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288"}, + {file = "rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7"}, + {file = "rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff"}, + {file = "rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7"}, + {file = "rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139"}, + {file = "rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464"}, + {file = "rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169"}, + {file = "rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425"}, + {file = "rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038"}, + {file = "rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7"}, + {file = "rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed"}, + {file = "rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85"}, + {file = "rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c"}, + {file = "rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825"}, + {file = "rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229"}, + {file = "rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad"}, + {file = "rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51"}, + {file = "rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5"}, + {file = "rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e"}, + {file = "rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394"}, + {file = "rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf"}, + {file = "rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b"}, + {file = "rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e"}, + {file = "rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2"}, + {file = "rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d"}, + {file = "rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7"}, + {file = "rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31"}, + {file = "rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95"}, + {file = "rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d"}, + {file = "rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15"}, + {file = "rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1"}, + {file = "rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a"}, + {file = "rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0"}, + {file = "rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94"}, + {file = "rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08"}, + {file = "rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27"}, + {file = "rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6"}, + {file = "rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d"}, + {file = "rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0"}, + {file = "rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f"}, + {file = "rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65"}, + {file = "rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f"}, + {file = "rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53"}, + {file = "rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed"}, + {file = "rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950"}, + {file = "rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6"}, + {file = "rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb"}, + {file = "rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5"}, + {file = "rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404"}, + {file = "rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856"}, + {file = "rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40"}, + {file = "rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0"}, + {file = "rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e"}, + {file = "rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84"}, +] + +[[package]] +name = "selenium" +version = "4.38.0" +description = "Official Python bindings for Selenium WebDriver" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "selenium-4.38.0-py3-none-any.whl", hash = "sha256:ed47563f188130a6fd486b327ca7ba48c5b11fb900e07d6457befdde320e35fd"}, + {file = "selenium-4.38.0.tar.gz", hash = "sha256:c117af6727859d50f622d6d0785b945c5db3e28a45ec12ad85cee2e7cc84fc4c"}, +] + +[package.dependencies] +certifi = ">=2025.10.5" +trio = ">=0.31.0,<1.0" +trio-websocket = ">=0.12.2,<1.0" +typing_extensions = ">=4.15.0,<5.0" +urllib3 = {version = ">=2.5.0,<3.0", extras = ["socks"]} +websocket-client = ">=1.8.0,<2.0" + +[[package]] +name = "send2trash" +version = "1.8.3" +description = "Send file to trash natively under Mac OS X, Windows and Linux" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +groups = ["dev"] +files = [ + {file = "Send2Trash-1.8.3-py3-none-any.whl", hash = "sha256:0c31227e0bd08961c7665474a3d1ef7193929fedda4233843689baa056be46c9"}, + {file = "Send2Trash-1.8.3.tar.gz", hash = "sha256:b18e7a3966d99871aefeb00cfbcfdced55ce4871194810fc71f4aa484b953abf"}, +] + +[package.extras] +nativelib = ["pyobjc-framework-Cocoa", "pywin32"] +objc = ["pyobjc-framework-Cocoa"] +win32 = ["pywin32"] + +[[package]] +name = "setuptools" +version = "80.9.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922"}, + {file = "setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.8.0)"] +core = ["importlib_metadata (>=6)", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib_metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.14.*)", "pytest-mypy"] + +[[package]] +name = "six" +version = "1.17.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["dev"] +files = [ + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, +] + [[package]] name = "sniffio" version = "1.3.1" description = "Sniff out which async library your code is running under" optional = false python-versions = ">=3.7" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, ] +[[package]] +name = "sortedcontainers" +version = "2.4.0" +description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, + {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, +] + +[[package]] +name = "soupsieve" +version = "2.8" +description = "A modern CSS selector implementation for Beautiful Soup." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "soupsieve-2.8-py3-none-any.whl", hash = "sha256:0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c"}, + {file = "soupsieve-2.8.tar.gz", hash = "sha256:e2dd4a40a628cb5f28f6d4b0db8800b8f581b65bb380b97de22ba5ca8d72572f"}, +] + [[package]] name = "stack-data" version = "0.6.3" @@ -1211,6 +2678,47 @@ pure-eval = "*" [package.extras] tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] +[[package]] +name = "terminado" +version = "0.18.1" +description = "Tornado websocket backend for the Xterm.js Javascript terminal emulator library." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "terminado-0.18.1-py3-none-any.whl", hash = "sha256:a4468e1b37bb318f8a86514f65814e1afc977cf29b3992a4500d9dd305dcceb0"}, + {file = "terminado-0.18.1.tar.gz", hash = "sha256:de09f2c4b85de4765f7714688fff57d3e75bad1f909b589fde880460c753fd2e"}, +] + +[package.dependencies] +ptyprocess = {version = "*", markers = "os_name != \"nt\""} +pywinpty = {version = ">=1.1.0", markers = "os_name == \"nt\""} +tornado = ">=6.1.0" + +[package.extras] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] +test = ["pre-commit", "pytest (>=7.0)", "pytest-timeout"] +typing = ["mypy (>=1.6,<2.0)", "traitlets (>=5.11.1)"] + +[[package]] +name = "tinycss2" +version = "1.4.0" +description = "A tiny CSS parser" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289"}, + {file = "tinycss2-1.4.0.tar.gz", hash = "sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7"}, +] + +[package.dependencies] +webencodings = ">=0.4" + +[package.extras] +doc = ["sphinx", "sphinx_rtd_theme"] +test = ["pytest", "ruff"] + [[package]] name = "tomli" version = "2.2.1" @@ -1254,6 +2762,28 @@ files = [ {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, ] +[[package]] +name = "tornado" +version = "6.5.2" +description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "tornado-6.5.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:2436822940d37cde62771cff8774f4f00b3c8024fe482e16ca8387b8a2724db6"}, + {file = "tornado-6.5.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:583a52c7aa94ee046854ba81d9ebb6c81ec0fd30386d96f7640c96dad45a03ef"}, + {file = "tornado-6.5.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0fe179f28d597deab2842b86ed4060deec7388f1fd9c1b4a41adf8af058907e"}, + {file = "tornado-6.5.2-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b186e85d1e3536d69583d2298423744740986018e393d0321df7340e71898882"}, + {file = "tornado-6.5.2-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e792706668c87709709c18b353da1f7662317b563ff69f00bab83595940c7108"}, + {file = "tornado-6.5.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:06ceb1300fd70cb20e43b1ad8aaee0266e69e7ced38fa910ad2e03285009ce7c"}, + {file = "tornado-6.5.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:74db443e0f5251be86cbf37929f84d8c20c27a355dd452a5cfa2aada0d001ec4"}, + {file = "tornado-6.5.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b5e735ab2889d7ed33b32a459cac490eda71a1ba6857b0118de476ab6c366c04"}, + {file = "tornado-6.5.2-cp39-abi3-win32.whl", hash = "sha256:c6f29e94d9b37a95013bb669616352ddb82e3bfe8326fccee50583caebc8a5f0"}, + {file = "tornado-6.5.2-cp39-abi3-win_amd64.whl", hash = "sha256:e56a5af51cc30dd2cae649429af65ca2f6571da29504a07995175df14c18f35f"}, + {file = "tornado-6.5.2-cp39-abi3-win_arm64.whl", hash = "sha256:d6c33dc3672e3a1f3618eb63b7ef4683a7688e7b9e6e8f0d9aa5726360a004af"}, + {file = "tornado-6.5.2.tar.gz", hash = "sha256:ab53c8f9a0fa351e2c0741284e06c7a45da86afb544133201c5cc8578eb076a0"}, +] + [[package]] name = "traitlets" version = "5.14.3" @@ -1271,15 +2801,54 @@ docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<8.2)", "pytest-mock", "pytest-mypy-testing"] [[package]] -name = "typing-extensions" -version = "4.12.2" -description = "Backported and Experimental Type Hints for Python 3.8+" +name = "trio" +version = "0.32.0" +description = "A friendly Python library for async concurrency and I/O" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "trio-0.32.0-py3-none-any.whl", hash = "sha256:4ab65984ef8370b79a76659ec87aa3a30c5c7c83ff250b4de88c29a8ab6123c5"}, + {file = "trio-0.32.0.tar.gz", hash = "sha256:150f29ec923bcd51231e1d4c71c7006e65247d68759dd1c19af4ea815a25806b"}, +] + +[package.dependencies] +attrs = ">=23.2.0" +cffi = {version = ">=1.14", markers = "os_name == \"nt\" and implementation_name != \"pypy\""} +exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} +idna = "*" +outcome = "*" +sniffio = ">=1.3.0" +sortedcontainers = "*" + +[[package]] +name = "trio-websocket" +version = "0.12.2" +description = "WebSocket library for Trio" optional = false python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "trio_websocket-0.12.2-py3-none-any.whl", hash = "sha256:df605665f1db533f4a386c94525870851096a223adcb97f72a07e8b4beba45b6"}, + {file = "trio_websocket-0.12.2.tar.gz", hash = "sha256:22c72c436f3d1e264d0910a3951934798dcc5b00ae56fc4ee079d46c7cf20fae"}, +] + +[package.dependencies] +exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} +outcome = ">=1.2.0" +trio = ">=0.11" +wsproto = ">=0.14" + +[[package]] +name = "typing-extensions" +version = "4.15.0" +description = "Backported and Experimental Type Hints for Python 3.9+" +optional = false +python-versions = ">=3.9" groups = ["main", "dev"] files = [ - {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, - {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, + {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, + {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, ] [[package]] @@ -1297,6 +2866,54 @@ files = [ [package.dependencies] typing-extensions = ">=4.12.0" +[[package]] +name = "tzdata" +version = "2025.2" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +groups = ["dev"] +files = [ + {file = "tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8"}, + {file = "tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9"}, +] + +[[package]] +name = "uri-template" +version = "1.3.0" +description = "RFC 6570 URI Template Processor" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "uri-template-1.3.0.tar.gz", hash = "sha256:0e00f8eb65e18c7de20d595a14336e9f337ead580c70934141624b6d1ffdacc7"}, + {file = "uri_template-1.3.0-py3-none-any.whl", hash = "sha256:a44a133ea12d44a0c0f06d7d42a52d71282e77e2f937d8abd5655b8d56fc1363"}, +] + +[package.extras] +dev = ["flake8", "flake8-annotations", "flake8-bandit", "flake8-bugbear", "flake8-commas", "flake8-comprehensions", "flake8-continuation", "flake8-datetimez", "flake8-docstrings", "flake8-import-order", "flake8-literal", "flake8-modern-annotations", "flake8-noqa", "flake8-pyproject", "flake8-requirements", "flake8-typechecking-import", "flake8-use-fstring", "mypy", "pep8-naming", "types-PyYAML"] + +[[package]] +name = "urllib3" +version = "2.5.0" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"}, + {file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"}, +] + +[package.dependencies] +pysocks = {version = ">=1.5.6,<1.5.7 || >1.5.7,<2.0", optional = true, markers = "extra == \"socks\""} + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + [[package]] name = "wcwidth" version = "0.2.13" @@ -1309,7 +2926,63 @@ files = [ {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, ] +[[package]] +name = "webcolors" +version = "25.10.0" +description = "A library for working with the color formats defined by HTML and CSS." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "webcolors-25.10.0-py3-none-any.whl", hash = "sha256:032c727334856fc0b968f63daa252a1ac93d33db2f5267756623c210e57a4f1d"}, + {file = "webcolors-25.10.0.tar.gz", hash = "sha256:62abae86504f66d0f6364c2a8520de4a0c47b80c03fc3a5f1815fedbef7c19bf"}, +] + +[[package]] +name = "webencodings" +version = "0.5.1" +description = "Character encoding aliases for legacy web content" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, + {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, +] + +[[package]] +name = "websocket-client" +version = "1.9.0" +description = "WebSocket client for Python with low level API options" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef"}, + {file = "websocket_client-1.9.0.tar.gz", hash = "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98"}, +] + +[package.extras] +docs = ["Sphinx (>=6.0)", "myst-parser (>=2.0.0)", "sphinx_rtd_theme (>=1.1.0)"] +optional = ["python-socks", "wsaccel"] +test = ["pytest", "websockets"] + +[[package]] +name = "wsproto" +version = "1.2.0" +description = "WebSockets state-machine based protocol implementation" +optional = false +python-versions = ">=3.7.0" +groups = ["dev"] +files = [ + {file = "wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736"}, + {file = "wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065"}, +] + +[package.dependencies] +h11 = ">=0.9.0,<1" + [metadata] lock-version = "2.1" python-versions = ">=3.10" -content-hash = "f0e2f84804d0edceca59e016f7fc75e454f1a8dad10526e7c62e6f2b8144f5d0" +content-hash = "36e5b914ba79a0a9f7d5f3554006dac388532e34ada61648f5ff097e905a6b17" diff --git a/pyproject.toml b/pyproject.toml index fba768f..6acedab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,8 @@ dependencies = [ "pydantic-settings (>=2.10.6,<3.0.0)", "cryptography (>=45.0.4,<46.0.0)", "http-message-signatures (>=0.6.1,<0.7.0)", - "http-sf (>=1.0.4,<1.1.0)" + "http-sf (>=1.0.4,<1.1.0)", + "python-ulid[pydantic] (>=3.1.0,<4.0.0)" ] [build-system] @@ -30,4 +31,6 @@ pyflakes = "^3.2.0" datamodel-code-generator = "^0.28.1" pytest = "^8.3.4" pyyaml = "^6.0.2" +jupyterlab = "^4.5.0" +selenium = "^4.38.0" diff --git a/tests/.env.example b/tests/.env.example index 15e0b27..59ee9ea 100644 --- a/tests/.env.example +++ b/tests/.env.example @@ -1,4 +1,6 @@ TEST_SELLER_WALLET= TEST_SELLER_KEY= TEST_SELLER_KEY_ID= -TEST_BUYER_WALLET= \ No newline at end of file +TEST_BUYER_WALLET= +TEST_BUYER_LOGIN= +TEST_BUYER_PASSWORD= \ No newline at end of file diff --git a/tests/config.py b/tests/config.py index 28e4af6..216d32c 100644 --- a/tests/config.py +++ b/tests/config.py @@ -8,7 +8,12 @@ class Settings(BaseSettings): TEST_SELLER_KEY: Optional[str] = Field(default="") TEST_SELLER_KEY_ID: Optional[str] = Field(default="") TEST_BUYER_WALLET: Optional[str] = Field(default="") - model_config = SettingsConfigDict(case_sensitive=True, env_file=".env", env_file_encoding="utf-8") + TEST_BUYER_LOGIN: Optional[str] = Field(default="") + TEST_BUYER_PASSWORD: Optional[str] = Field(default="") + TEST_PRODUCT_VALUE: str = Field(default="2500") + TEST_PRODUCT_VOLUME: int = Field(default=2) + TEST_REDIRECT_URI: str = Field(default="http://localhost/redirect/") + model_config = SettingsConfigDict(case_sensitive=True, env_file="./tests/.env", env_file_encoding="utf-8") settings = Settings() diff --git a/tests/conftest.py b/tests/conftest.py index e24d137..db7e37f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,9 @@ import pytest + from open_payments_sdk.client.client import OpenPaymentsClient +from open_payments_sdk.process.models.process import SellerOpenPaymentAccount from open_payments_sdk.models.auth import GrantRequest +from config import settings @pytest.fixture def keyid_private_key() -> dict: @@ -73,7 +76,6 @@ def grant_req_dto() -> GrantRequest: } return GrantRequest(**grant_req) - @pytest.fixture def interactive_grant_req_dto() -> GrantRequest: # TODO complete writing tests """ @@ -81,3 +83,20 @@ def interactive_grant_req_dto() -> GrantRequest: # TODO complete writing tests """ grant_req = {} return GrantRequest(**grant_req) + +################################################################################################### +# PROCESS TEST FIXTURES +################################################################################################### + +@pytest.fixture +def op_seller_account() -> SellerOpenPaymentAccount: + if not settings.TEST_SELLER_WALLET or not settings.TEST_SELLER_KEY or not settings.TEST_SELLER_KEY_ID: + raise ValueError("Test Seller settings are required.") + return SellerOpenPaymentAccount( + **{ + "walletAddressUrl": settings.TEST_SELLER_WALLET, + "privateKey": settings.TEST_SELLER_KEY, + "keyId": settings.TEST_SELLER_KEY_ID, + } + ) + diff --git a/tests/integration/test_grants.py b/tests/integration/test_grants.py index 2978cf4..e7deb41 100644 --- a/tests/integration/test_grants.py +++ b/tests/integration/test_grants.py @@ -1,4 +1,3 @@ -import json from open_payments_sdk.client.client import OpenPaymentsClient from open_payments_sdk.models.auth import GrantRequest @@ -8,16 +7,13 @@ def test_create_grant_request(op_client: OpenPaymentsClient, grant_req_dto: Gran Test create grant request and get back access token """ wallet = op_client.wallet.get_wallet_address("https://ilp.interledger-test.dev/5c327379") - grant_response = op_client.grants.post_grant_request(grant_request=grant_req_dto,auth_server_endpoint=str(wallet.authServer)) - assert grant_response.root.access_token.value is not None - assert grant_response.root.access_token.value != "" + grant_response = op_client.grants.post_grant_request(grant_request=grant_req_dto, auth_server_endpoint=str(wallet.authServer)) + assert grant_response.access_token.value is not None + assert grant_response.access_token.value != "" -def test_create_interactive_grant_request( - op_client: OpenPaymentsClient, - grant_req_dto: GrantRequest - ): +def test_create_interactive_grant_request(op_client: OpenPaymentsClient, grant_req_dto: GrantRequest): """ Test interactive grant request """ - \ No newline at end of file + pass diff --git a/tests/integration/test_wallet.py b/tests/integration/test_wallet.py index 0dcdb36..f6abe44 100644 --- a/tests/integration/test_wallet.py +++ b/tests/integration/test_wallet.py @@ -1,4 +1,4 @@ -def test_get_wallet_address(op_client,wallet_address_server): +def test_get_wallet_address(op_client, wallet_address_server): """ Test get wallet address """ @@ -7,7 +7,7 @@ def test_get_wallet_address(op_client,wallet_address_server): assert wallet.assetCode is not None -def test_get_wallet_address_keys(op_client,wallet_address_server): +def test_get_wallet_address_keys(op_client, wallet_address_server): """ Test get jwks.json """ diff --git a/tests/process/test_process.py b/tests/process/test_process.py new file mode 100644 index 0000000..2588b98 --- /dev/null +++ b/tests/process/test_process.py @@ -0,0 +1,71 @@ +import time +from selenium import webdriver +from selenium.webdriver.common.by import By +from urllib.parse import urlparse, parse_qs + +from config import settings +from open_payments_sdk.process.crud import OpenPaymentsProcessor + +def interactive_purchase_approval(purchase_endpoint, accept=True) -> str | None: + if not settings.TEST_BUYER_LOGIN or not settings.TEST_BUYER_PASSWORD: + raise ValueError("A test buyer login and password are required.") + response_url = None + if not isinstance(purchase_endpoint, str): + purchase_endpoint = str(purchase_endpoint) + driver = webdriver.Chrome() + driver.get(purchase_endpoint) + driver.implicitly_wait(10) + if "/auth/login" in driver.current_url: + driver.find_element(By.NAME, "email").send_keys(settings.TEST_BUYER_LOGIN) + driver.find_element(By.NAME, "password").send_keys(settings.TEST_BUYER_PASSWORD) + driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click() + time.sleep(2) + # Continue with the approval + if accept: + driver.find_element(By.CSS_SELECTOR, "button[aria-label='accept']").click() + else: + driver.find_element(By.CSS_SELECTOR, "button[aria-label='decline']").click() + time.sleep(2) + response_url = str(driver.current_url) + driver.quit() + return response_url + +def test_one_time_purchase(op_seller_account): + ################################################################################################### + # REQUEST PURCHASE ENDPOINT + ################################################################################################### + if not settings.TEST_BUYER_WALLET or not settings.TEST_REDIRECT_URI: + raise ValueError("A test buyer wallet and redirect URI are required.") + # 1. SET UP THE ORDER + order = OpenPaymentsProcessor(seller=op_seller_account, buyer=settings.TEST_BUYER_WALLET, redirect_uri=settings.TEST_REDIRECT_URI) + amount = str(int(settings.TEST_PRODUCT_VALUE) * settings.TEST_PRODUCT_VOLUME) + # 2. SELLER INCOMING PAYMENT PROCESS + incoming_payment_response = order.request_incoming_payment(amount=amount) + # 3. BUYER QUOTE REQUEST PROCESS + order.request_quote(incoming_payment_id=incoming_payment_response.id) + # This could be returned to the buyer for review, but we'll skip this for now... + # 4. REQUEST BUYER INTERACTIVE GRANT FOR PURCHASE + purchase_endpoint = order.get_purchase_endpoint(amount=amount) + # Could also test saving of `order.pending_payment` + ################################################################################################### + # HAND OFF FOR INTERACTIVE PURCHASE APPROVAL + ################################################################################################### + response_url = interactive_purchase_approval(purchase_endpoint) + if not response_url: + raise ValueError("Something went wrong during interactive purchase approval. Check the endpoints.") + response_url = urlparse(response_url) + received_hash = parse_qs(response_url.query)["hash"][0] + interact_ref = parse_qs(response_url.query)["interact_ref"][0] + # key = str(response_url.path).split("/")[-1] + # key unneccessary here, but would normally be used to recover the specific transaction being processed + ################################################################################################### + # COMPLETE OUTGOING PAYMENT + ################################################################################################### + # 1. RECOVER THE PENDING PAYMENT FROM THE KEY + pending_payment = order.pending_payment + # Ordinarily, use `pending_payment.seller` to recover stored seller data + payment = OpenPaymentsProcessor(seller=op_seller_account, buyer=str(pending_payment.buyer.id), redirect_uri=settings.TEST_REDIRECT_URI) + # 5. COMPLETE OUTGOING PAYMENT + # This depends on the app ... can divide up payments between collaborators, take platform fees, etc. + # Not fully tested here, yet + incoming_payment_response = payment.complete_payment(interact_ref=str(interact_ref), received_hash=str(received_hash), pending_payment=pending_payment)