diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index e1c6437..0000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: Build - -on: - push: - branches: - - main - -jobs: - build: - name: Build - runs-on: ubuntu-latest - permissions: read-all - steps: - - uses: actions/checkout@v5 - with: - fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis - - uses: sonarsource/sonarqube-scan-action@master - env: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 2557c3f..5e2ca94 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -1,11 +1,12 @@ name: "CodeQL" on: - push: - branches: [ main ] pull_request: - # The branches below must be a subset of the branches above - branches: [ main ] + branches: + - main + push: + branches: + - main schedule: - cron: '21 1 * * 2' @@ -17,24 +18,19 @@ jobs: actions: read contents: read security-events: write - strategy: fail-fast: false matrix: - language: [ 'python' ] - + language: + - 'python' steps: - name: Checkout repository - uses: actions/checkout@v5 - - # Initializes the CodeQL tools for scanning. + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@16df4fbc19aea13d921737861d6c622bf3cefe23 with: languages: ${{ matrix.language }} - - name: Autobuild - uses: github/codeql-action/autobuild@v3 - + uses: github/codeql-action/autobuild@192325c86100d080feab897ff886c34abd4c83a3 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + uses: github/codeql-action/analyze@192325c86100d080feab897ff886c34abd4c83a3 diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml deleted file mode 100644 index ed76feb..0000000 --- a/.github/workflows/pre-commit.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: pre-commit - -on: - pull_request: - branches: - - 'main' - -jobs: - pre-commit: - runs-on: ubuntu-latest - strategy: - matrix: - python-version: [ "3.13" ] - steps: - - uses: actions/checkout@v5 - - uses: actions/setup-python@v6 - with: - python-version: ${{ matrix.python-version }} - - uses: pre-commit/action@v3.0.1 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..6b00aaf --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,29 @@ +name: Publish to PyPI + +on: + push: + branches: + - main + +jobs: + pypi-publish: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: + - "3.10" + permissions: + id-token: write + steps: + - name: Checkout + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 + with: + fetch-depth: 0 + - name: Install uv + uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 + with: + python-version: ${{ matrix.python-version }} + - name: UV Build + run: uv build + - name: Publish package distributions to PyPI + uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml deleted file mode 100644 index 4b5fa80..0000000 --- a/.github/workflows/pypi.yml +++ /dev/null @@ -1,23 +0,0 @@ -on: push - -jobs: - build: - runs-on: ubuntu-latest - name: Publish package - steps: - - uses: actions/checkout@v5 - - uses: actions/setup-python@v6 - with: - python-version: 3.8 - - name: Install Dependencies - run: | - pip install -e .[build] - - name: Build Package - run: | - python -m build - - name: Publish - if: startsWith(github.ref, 'refs/tags') - uses: pypa/gh-action-pypi-publish@release/v1 - with: - user: __token__ - password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml deleted file mode 100644 index e6d81b6..0000000 --- a/.github/workflows/pytest.yml +++ /dev/null @@ -1,24 +0,0 @@ -on: - pull_request: - branches: - - 'main' - -jobs: - build: - runs-on: ubuntu-latest - name: pyTest - strategy: - matrix: - python-version: [ "3.8", "3.9", "3.10", "3.11", "3.12" , "3.13" ] - steps: - - uses: actions/checkout@v5 - - uses: actions/setup-python@v6 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install pytest pytest-mock - - name: Test with pytest - run: | - pytest diff --git a/.github/workflows/uv.yml b/.github/workflows/uv.yml new file mode 100644 index 0000000..ccf270d --- /dev/null +++ b/.github/workflows/uv.yml @@ -0,0 +1,45 @@ +name: UV + +on: + pull_request: + branches: + - main + push: + branches: + - main + +jobs: + uv: + name: python + runs-on: ubuntu-latest + permissions: read-all + strategy: + matrix: + python-version: + - "3.11" + steps: + - name: Checkout + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 + with: + fetch-depth: 0 + - name: Install uv + uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 + with: + python-version: ${{ matrix.python-version }} + - name: Set up Python + run: uv python install + - name: pyTest + run: uv run pytest tests + - name: Ruff (check) + run: uv run ruff check + - name: Ruff (format) + run: uv run ruff format --diff --check + - name: Ty + run: uv run ty check + #- name: Troml + # run: uv run troml check + - name: Run SonarQube + uses: sonarsource/sonarqube-scan-action@fd88b7d7ccbaefd23d8f36f73b59db7a3d246602 + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index b6b5103..0000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,34 +0,0 @@ -repos: - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 - hooks: - - id: trailing-whitespace - - id: end-of-file-fixer - - id: check-yaml - - id: check-added-large-files - - id: check-merge-conflict - - id: check-case-conflict - - - repo: https://github.com/pycqa/isort - rev: 6.0.1 - hooks: - - id: isort - name: isort - - - repo: https://github.com/pycqa/flake8 - rev: 7.2.0 - hooks: - - id: flake8 - name: flake8 - additional_dependencies: - - flake8-bugbear - - flake8-no-pep420 - - flake8-comprehensions - # - flake8-docstrings - Disabled as is now incompatible. - - - repo: https://github.com/pre-commit/mirrors-mypy - rev: 'v1.15.0' - hooks: - - id: mypy - name: mypy - exclude: ^tests/test_.* diff --git a/docs/source/conf.py b/docs/source/conf.py index e46a2c7..b818790 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -14,20 +14,20 @@ # -- Path setup -------------------------------------------------------------- -sys.path.insert(0, os.path.abspath('../')) +sys.path.insert(0, os.path.abspath("../")) config = ConfigParser() -config.read('../../setup.cfg') +config.read("../../setup.cfg") # -- Project information ----------------------------------------------------- copyright_year = datetime.datetime.now().year -author = config['metadata']['author'] -project = config['metadata']['name'] -copyright = f'{copyright_year}, {author}' +author = config["metadata"]["author"] +project = config["metadata"]["name"] +copyright = f"{copyright_year}, {author}" # The full version, including alpha/beta/rc tags -release = config['metadata']['version'] +release = config["metadata"]["version"] # -- General configuration --------------------------------------------------- @@ -36,13 +36,13 @@ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions: List[str] = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.coverage', - 'sphinx.ext.doctest', + "sphinx.ext.autodoc", + "sphinx.ext.coverage", + "sphinx.ext.doctest", ] # Add any paths that contain templates here, relative to this directory. -templates_path: List[str] = ['_templates'] +templates_path: List[str] = ["_templates"] # List of patterns, relative to the source directory, that match files and # directories to ignore when looking for source files. @@ -55,9 +55,9 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = 'sphinx_rtd_theme' +html_theme = "sphinx_rtd_theme" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] diff --git a/examples/auth_step_01.py b/examples/auth_step_01.py index 0f7a4a2..b4d11ef 100644 --- a/examples/auth_step_01.py +++ b/examples/auth_step_01.py @@ -1,9 +1,10 @@ """Example code to authenticate against the Monzo API.""" + from monzo.authentication import Authentication -client_id = '' # Client ID obtained when creating a Monzo client -client_secret = '' # Client secret obtained when creating a Monzo client -redirect_uri = 'http://127.0.0.1/monzo' # URL requests via Monzo will be redirected in a browser +client_id = "" # Client ID obtained when creating a Monzo client +client_secret = "" # Client secret obtained when creating a Monzo client +redirect_uri = "http://127.0.0.1/monzo" # URL requests via Monzo will be redirected in a browser monzo = Authentication(client_id=client_id, client_secret=client_secret, redirect_url=redirect_uri) diff --git a/examples/auth_step_02.py b/examples/auth_step_02.py index 37739e3..8b004fc 100644 --- a/examples/auth_step_02.py +++ b/examples/auth_step_02.py @@ -1,28 +1,29 @@ """Code to handle the second stage of authentication.""" + import sys from monzo.authentication import Authentication from monzo.exceptions import MonzoAuthenticationError, MonzoServerError -client_id = '' # Client ID obtained when creating a Monzo client -client_secret = '' # Client secret obtained when creating a Monzo client -redirect_uri = 'http://127.0.0.1/monzo' # URL requests via Monzo will be redirected in a browser -state = '' # State random string created when creating the Monzo URL (generated in step 1 and appended to the URL) -code = '' # Authorization code from Monzo (this will be in the redirected URL after clicking the link from step 1) +client_id = "" # Client ID obtained when creating a Monzo client +client_secret = "" # Client secret obtained when creating a Monzo client +redirect_uri = "http://127.0.0.1/monzo" # URL requests via Monzo will be redirected in a browser +state = "" # State random string created when creating the Monzo URL (generated in step 1 and appended to the URL) +code = "" # Authorization code from Monzo (this will be in the redirected URL after clicking the link from step 1) monzo = Authentication(client_id=client_id, client_secret=client_secret, redirect_url=redirect_uri) try: monzo.authenticate(authorization_token=code, state_token=state) except MonzoAuthenticationError: - print('State code does not match') + print("State code does not match") sys.exit(1) except MonzoServerError: - print('Monzo Server Error') + print("Monzo Server Error") sys.exit(1) # The following 3 items should be stored for future requests print(f"access_token = '{monzo.access_token}'") -print(f'expiry = {monzo.access_token_expiry}') +print(f"expiry = {monzo.access_token_expiry}") print(f"refresh_token = '{monzo.refresh_token}'") # Now authorize access from the alert in the Monzo app diff --git a/examples/create_feed_item.py b/examples/create_feed_item.py index 39442e1..fdaccf7 100644 --- a/examples/create_feed_item.py +++ b/examples/create_feed_item.py @@ -1,15 +1,16 @@ """Example code for creating a feed item.""" + from monzo.authentication import Authentication from monzo.endpoints.account import Account from monzo.endpoints.feed_item import FeedItem from monzo.exceptions import MonzoError -client_id = '' # Client ID obtained when creating a Monzo client -client_secret = '' # Client secret obtained when creating a Monzo client -redirect_uri = 'http://127.0.0.1/monzo' # URL requests via Monzo will be redirected in a browser -access_token = '' # access_token retrieved in step 2 +client_id = "" # Client ID obtained when creating a Monzo client +client_secret = "" # Client secret obtained when creating a Monzo client +redirect_uri = "http://127.0.0.1/monzo" # URL requests via Monzo will be redirected in a browser +access_token = "" # access_token retrieved in step 2 expiry = 0 # access_token_expiry retrieved in step 2 -refresh_token = '' # refresh_token retrieved in step 2 +refresh_token = "" # refresh_token retrieved in step 2 monzo = Authentication( client_id=client_id, @@ -17,17 +18,23 @@ redirect_url=redirect_uri, access_token=access_token, access_token_expiry=expiry, - refresh_token=refresh_token + refresh_token=refresh_token, ) accounts = Account.fetch(monzo) -url = 'https://monzo.com/' +url = "https://monzo.com/" params = { - 'title': 'Sending you to Monzo', - 'image_url': 'https://monzo.com/static/images/favicon.png', - 'body': 'Off to Monzo you go', + "title": "Sending you to Monzo", + "image_url": "https://monzo.com/static/images/favicon.png", + "body": "Off to Monzo you go", } try: - FeedItem.create(auth=monzo, account_id=accounts[0].account_id, feed_type='basic', params=params, url=url) + FeedItem.create( + auth=monzo, + account_id=accounts[0].account_id, + feed_type="basic", + params=params, + url=url, + ) except MonzoError: - print('Failed to create feed item') + print("Failed to create feed item") diff --git a/examples/get_accounts.py b/examples/get_accounts.py index 027efd9..69d11e7 100644 --- a/examples/get_accounts.py +++ b/examples/get_accounts.py @@ -1,14 +1,15 @@ """Example code to fetch accounts.""" + from monzo.authentication import Authentication from monzo.endpoints.account import Account from monzo.exceptions import MonzoError -client_id = '' # Client ID obtained when creating a Monzo client -client_secret = '' # Client secret obtained when creating a Monzo client -redirect_uri = 'http://127.0.0.1/monzo' # URL requests via Monzo will be redirected in a browser -access_token = '' # access_token retrieved in step 2 +client_id = "" # Client ID obtained when creating a Monzo client +client_secret = "" # Client secret obtained when creating a Monzo client +redirect_uri = "http://127.0.0.1/monzo" # URL requests via Monzo will be redirected in a browser +access_token = "" # access_token retrieved in step 2 expiry = 0 # access_token_expiry retrieved in step 2 -refresh_token = '' # refresh_token retrieved in step 2\ +refresh_token = "" # refresh_token retrieved in step 2\ monzo = Authentication( client_id=client_id, @@ -16,15 +17,14 @@ redirect_url=redirect_uri, access_token=access_token, access_token_expiry=expiry, - refresh_token=refresh_token + refresh_token=refresh_token, ) try: accounts = Account.fetch(monzo) for account in accounts: print( - f'Account ID: {account.account_type()} - ' - f'Balance: {account.balance.total_balance if account.balance else 0}' + f"Account ID: {account.account_type()} - Balance: {account.balance.total_balance if account.balance else 0}" ) except MonzoError: - print('Failed to retrieve accounts') + print("Failed to retrieve accounts") diff --git a/examples/get_whoami.py b/examples/get_whoami.py index 0a1b5a3..e22f637 100644 --- a/examples/get_whoami.py +++ b/examples/get_whoami.py @@ -1,14 +1,15 @@ """Example code for showing who is logged into the Monzo API.""" + from monzo.authentication import Authentication from monzo.endpoints.whoami import WhoAmI from monzo.exceptions import MonzoError -client_id = '' # Client ID obtained when creating a Monzo client -client_secret = '' # Client secret obtained when creating a Monzo client -redirect_uri = 'http://127.0.0.1/monzo' # URL requests via Monzo will be redirected in a browser -access_token = '' # access_token retrieved in step 2 +client_id = "" # Client ID obtained when creating a Monzo client +client_secret = "" # Client secret obtained when creating a Monzo client +redirect_uri = "http://127.0.0.1/monzo" # URL requests via Monzo will be redirected in a browser +access_token = "" # access_token retrieved in step 2 expiry = 0 # access_token_expiry retrieved in step 2 -refresh_token = '' # refresh_token retrieved in step 2 +refresh_token = "" # refresh_token retrieved in step 2 monzo = Authentication( client_id=client_id, @@ -16,7 +17,7 @@ redirect_url=redirect_uri, access_token=access_token, access_token_expiry=expiry, - refresh_token=refresh_token + refresh_token=refresh_token, ) try: @@ -25,4 +26,4 @@ print(who.client_id) print(who.authenticated) except MonzoError: - print('failed to fetch whoami endpoint') + print("failed to fetch whoami endpoint") diff --git a/monzo/authentication.py b/monzo/authentication.py index f69f87c..b03ed1c 100644 --- a/monzo/authentication.py +++ b/monzo/authentication.py @@ -1,4 +1,5 @@ """Class to allow authentication on the Monzo API.""" + import logging import os import secrets @@ -11,8 +12,8 @@ from monzo.handlers.storage import Storage from monzo.httpio import DEFAULT_TIMEOUT, REQUEST_RESPONSE_TYPE, HttpIO -MONZO_AUTH_URL = 'https://auth.monzo.com' -MONZO_API_URL = 'https://api.monzo.com' +MONZO_AUTH_URL = "https://auth.monzo.com" +MONZO_API_URL = "https://api.monzo.com" class Authentication(object): @@ -24,23 +25,23 @@ class is usually passed to each action """ __slots__ = [ - '_access_token', - '_access_token_expiry', - '_client_id', - '_client_secret', - '_handlers', - '_redirect_url', - '_refresh_token', + "_access_token", + "_access_token_expiry", + "_client_id", + "_client_secret", + "_handlers", + "_redirect_url", + "_refresh_token", ] def __init__( - self, - client_id: str, - client_secret: str, - redirect_url: str, - access_token: str = '', - access_token_expiry: int = 0, - refresh_token: str = '' + self, + client_id: str, + client_secret: str, + redirect_url: str, + access_token: str = "", + access_token_expiry: int = 0, + refresh_token: str = "", ): """ Initialize Authentication. @@ -72,31 +73,31 @@ def authenticate(self, authorization_token: str, state_token: str) -> None: Raises: MonzoAuthenticationError On missing authorization token or mismatching state tokens """ - logging.debug('Attempting authentication') + logging.debug("Attempting authentication") if not authorization_token: - logging.debug('Authentication - Missing token') - raise MonzoAuthenticationError('Code missing from response') + logging.debug("Authentication - Missing token") + raise MonzoAuthenticationError("Code missing from response") if state_token != self.state_token: - logging.debug('Authentication - state token mismatch') - raise MonzoAuthenticationError('State tokens do not match') - tmp_file_name = 'monzo' + logging.debug("Authentication - state token mismatch") + raise MonzoAuthenticationError("State tokens do not match") + tmp_file_name = "monzo" tmp_file_path = PurePath(gettempdir(), tmp_file_name) os.remove(tmp_file_path) self._exchange_token(authorization_token=authorization_token) def logout(self) -> None: """Invalidate the access token.""" - logging.debug('Invalidating token') - self.make_request(path='/oauth2/logout', method='post') + logging.debug("Invalidating token") + self.make_request(path="/oauth2/logout", method="post") def make_request( - self, - path: str, - authenticated: bool = True, - method: str = 'GET', - data=None, - headers=None, - timeout: int = DEFAULT_TIMEOUT + self, + path: str, + authenticated: bool = True, + method: str = "GET", + data=None, + headers=None, + timeout: int = DEFAULT_TIMEOUT, ) -> REQUEST_RESPONSE_TYPE: """ Make an API call to Monzo. @@ -122,13 +123,13 @@ def make_request( if headers is None: headers = {} if authenticated: - headers['Authorization'] = f'Bearer {self.access_token}' + headers["Authorization"] = f"Bearer {self.access_token}" conn = HttpIO(MONZO_API_URL) method = method.lower() try: connection = getattr(conn, method) except AttributeError as exc: - raise MonzoHTTPError('Specified HTTP method is not supported') from exc + raise MonzoHTTPError("Specified HTTP method is not supported") from exc return connection(path=path, data=data, headers=headers, timeout=timeout) def refresh_access(self) -> None: @@ -140,23 +141,23 @@ def refresh_access(self) -> None: Raises: MonzoAuthenticationError: On lack of refresh token or failure to refresh a token """ - logging.debug('Fetching new token') + logging.debug("Fetching new token") if not self.refresh_token: - logging.debug('Unable to fetch new token without a refresh token') - raise MonzoAuthenticationError('Unable to refresh without a refresh token') + logging.debug("Unable to fetch new token without a refresh token") + raise MonzoAuthenticationError("Unable to refresh without a refresh token") data = { - 'grant_type': 'refresh_token', - 'client_id': self._client_id, - 'client_secret': self._client_secret, - 'refresh_token': self.refresh_token, + "grant_type": "refresh_token", + "client_id": self._client_id, + "client_secret": self._client_secret, + "refresh_token": self.refresh_token, } conn = HttpIO(MONZO_API_URL) try: - res = conn.post(path='/oauth2/token', data=data) + res = conn.post(path="/oauth2/token", data=data) self._populate_tokens(res) except MonzoError as exc: - logging.debug('Failed to fetch new token') - raise MonzoAuthenticationError('Could not refresh the access token') from exc + logging.debug("Failed to fetch new token") + raise MonzoAuthenticationError("Could not refresh the access token") from exc @property def access_token(self) -> str: @@ -166,7 +167,7 @@ def access_token(self) -> str: Returns: Access token if one exists, otherwise an empty string """ - return self._access_token or '' + return self._access_token or "" @property def access_token_expiry(self) -> int: @@ -196,8 +197,10 @@ def authentication_url(self) -> str: Returns: URL for Monzo authentication """ - return f'{MONZO_AUTH_URL}?client_id={self._client_id}&redirect_uri={self._redirect_url}' \ - f'&response_type=code&state={self.state_token}' + return ( + f"{MONZO_AUTH_URL}?client_id={self._client_id}&redirect_uri={self._redirect_url}" + f"&response_type=code&state={self.state_token}" + ) @property def refresh_token(self) -> str: @@ -217,14 +220,14 @@ def state_token(self) -> str: Returns: A state token used for authentication requests. """ - tmp_file_name = 'monzo' + tmp_file_name = "monzo" tmp_file_path = PurePath(gettempdir(), tmp_file_name) if not Path(tmp_file_path).is_file(): - with open(tmp_file_path, 'w') as fh: + with open(tmp_file_path, "w") as fh: state_token = secrets.token_urlsafe(64) fh.write(state_token) fh.flush() - with open(tmp_file_path, 'r') as fh: + with open(tmp_file_path, "r") as fh: state_token = fh.read() return state_token @@ -238,20 +241,20 @@ def _exchange_token(self, authorization_token: str) -> None: Raises: MonzoAuthenticationError On failure to create a token """ - logging.debug('Authentication - swapping authorization token for an access token') + logging.debug("Authentication - swapping authorization token for an access token") data = { - 'grant_type': 'authorization_code', - 'client_id': self._client_id, - 'client_secret': self._client_secret, - 'redirect_uri': self._redirect_url, - 'code': authorization_token, + "grant_type": "authorization_code", + "client_id": self._client_id, + "client_secret": self._client_secret, + "redirect_uri": self._redirect_url, + "code": authorization_token, } try: - res = self.make_request('/oauth2/token', authenticated=False, method='post', data=data) + res = self.make_request("/oauth2/token", authenticated=False, method="post", data=data) self._populate_tokens(res) except MonzoError as exc: - logging.debug(f'Could not fetch access token {exc}') - raise MonzoAuthenticationError('Could not fetch a valid access token') from exc + logging.debug(f"Could not fetch access token {exc}") + raise MonzoAuthenticationError("Could not fetch a valid access token") from exc def _populate_tokens(self, response: REQUEST_RESPONSE_TYPE) -> None: """ @@ -260,12 +263,12 @@ def _populate_tokens(self, response: REQUEST_RESPONSE_TYPE) -> None: Args: response: Response from an auth request. """ - logging.debug('Populating tokens') - self._access_token = response['data']['access_token'] - self.access_token_expiry = response['data']['expires_in'] - self._refresh_token = '' - if 'refresh_token' in response['data']: - self._refresh_token = response['data']['refresh_token'] + logging.debug("Populating tokens") + self._access_token = response["data"]["access_token"] + self.access_token_expiry = response["data"]["expires_in"] + self._refresh_token = "" + if "refresh_token" in response["data"]: + self._refresh_token = response["data"]["refresh_token"] for handler in self._handlers: handler.store( @@ -273,7 +276,7 @@ def _populate_tokens(self, response: REQUEST_RESPONSE_TYPE) -> None: client_id=self._client_id, client_secret=self._client_secret, expiry=self._access_token_expiry, - refresh_token=self._refresh_token + refresh_token=self._refresh_token, ) def register_callback_handler(self, handler: Storage) -> None: @@ -283,5 +286,5 @@ def register_callback_handler(self, handler: Storage) -> None: Args: handler: Credential handler implementing Storage """ - logging.debug('Registered a new callback handler') + logging.debug("Registered a new callback handler") self._handlers.append(handler) diff --git a/monzo/endpoints/account.py b/monzo/endpoints/account.py index 59e9c79..cf3f499 100644 --- a/monzo/endpoints/account.py +++ b/monzo/endpoints/account.py @@ -1,4 +1,5 @@ """Class to manage Accounts.""" + from __future__ import annotations from datetime import datetime @@ -11,15 +12,15 @@ from monzo.helpers import create_date ACCOUNT_TYPES = [ - 'uk_retail', - 'uk_retail_joint', + "uk_retail", + "uk_retail_joint", ] MONZO_ACCOUNT_TYPES = { - 'user_': 'Current Account', - 'monzoflex_': 'Flex', - 'monzoflexbackingloan_': 'Loan (Flex)', - 'loan_': 'Loan', + "user_": "Current Account", + "monzoflex_": "Flex", + "monzoflexbackingloan_": "Loan (Flex)", + "loan_": "Loan", } @@ -31,9 +32,24 @@ class Account(Monzo): method should be used. """ - __slots__ = ['_account_id', '_auth', '_balance', '_created', '_description', '_has_balance', '_closed'] - - def __init__(self, auth: Authentication, account_id: str, description: str, created: datetime, closed: bool): + __slots__ = [ + "_account_id", + "_auth", + "_balance", + "_created", + "_description", + "_has_balance", + "_closed", + ] + + def __init__( + self, + auth: Authentication, + account_id: str, + description: str, + created: datetime, + closed: bool, + ): """ Initialize Account. @@ -76,7 +92,7 @@ def account_type(self) -> str: for account_type in MONZO_ACCOUNT_TYPES.keys() if self.description.lower().startswith(account_type) ), - 'UNKNOWN', + "UNKNOWN", ) def fetch_balance(self) -> Optional[Balance]: @@ -142,7 +158,7 @@ def closed(self) -> bool: return self._closed @classmethod - def fetch(cls, auth: Authentication, account_type: str = '') -> List[Account]: + def fetch(cls, auth: Authentication, account_type: str = "") -> List[Account]: """ Implement and instantiates an Account object. @@ -155,16 +171,16 @@ def fetch(cls, auth: Authentication, account_type: str = '') -> List[Account]: """ data = {} if account_type and account_type.lower() in ACCOUNT_TYPES: - data['account_type'] = account_type.lower() - res = auth.make_request(path='/accounts', data=data) + data["account_type"] = account_type.lower() + res = auth.make_request(path="/accounts", data=data) account_list = [] - for account_item in res['data']['accounts']: + for account_item in res["data"]["accounts"]: account = Account( auth=auth, - account_id=account_item['id'], - description=account_item['description'], - created=create_date(account_item['created']), - closed=account_item['closed'], + account_id=account_item["id"], + description=account_item["description"], + created=create_date(account_item["created"]), + closed=account_item["closed"], ) account_list.append(account) return account_list diff --git a/monzo/endpoints/attachment.py b/monzo/endpoints/attachment.py index ce10656..12b4f1a 100644 --- a/monzo/endpoints/attachment.py +++ b/monzo/endpoints/attachment.py @@ -1,4 +1,5 @@ """Class to manage Attachments.""" + from __future__ import annotations from datetime import datetime @@ -11,9 +12,9 @@ from monzo.helpers import create_date SUPPORTED_ATTACHMENT_EXTENSIONS = { - 'jpeg': 'image/jpeg', - 'jpg': 'image/jpg', - 'png': 'image/png', + "jpeg": "image/jpeg", + "jpg": "image/jpg", + "png": "image/png", } @@ -25,23 +26,23 @@ class Attachment(Monzo): """ __slots__ = [ - '_attachment_id', - '_user_id', - '_transaction_id', - '_url', - '_file_type', - '_created', + "_attachment_id", + "_user_id", + "_transaction_id", + "_url", + "_file_type", + "_created", ] def __init__( - self, - auth: Authentication, - attachment_id: str, - user_id: str, - transaction_id: str, - url: str, - file_type: str, - created: datetime + self, + auth: Authentication, + attachment_id: str, + user_id: str, + transaction_id: str, + url: str, + file_type: str, + created: datetime, ): """ Initialize Attachment. @@ -115,22 +116,15 @@ def created(self) -> datetime: def delete(self) -> None: """Delete the attachment.""" - data = { - 'id': self.attachment_id - } + data = {"id": self.attachment_id} self._monzo_auth.make_request( - path='/attachment/deregister', - method='POST', + path="/attachment/deregister", + method="POST", data=data, ) @classmethod - def create_attachment( - cls, - auth: Authentication, - transaction_id: str, - url: str - ) -> Attachment: + def create_attachment(cls, auth: Authentication, transaction_id: str, url: str) -> Attachment: """ Create a new image attachment. @@ -147,33 +141,29 @@ def create_attachment( file_url = urlparse(url) _, file_extension = splitext(url) if file_extension not in SUPPORTED_ATTACHMENT_EXTENSIONS: - raise MonzoGeneralError('Unsupported file type') + raise MonzoGeneralError("Unsupported file type") file_type = SUPPORTED_ATTACHMENT_EXTENSIONS[file_extension] if file_url.netloc: file_type = Attachment._upload_file(auth=auth, url=url, file_type=file_type) data = { - 'external_id': transaction_id, - 'file_type': file_type, - 'file_url': file_url, + "external_id": transaction_id, + "file_type": file_type, + "file_url": file_url, } - response = auth.make_request( - path='', - method='POST', - data=data - ) + response = auth.make_request(path="", method="POST", data=data) - if response['code'] != 200: - raise MonzoGeneralError('Failed to create attachment') + if response["code"] != 200: + raise MonzoGeneralError("Failed to create attachment") return Attachment( auth=auth, - attachment_id=response['data']['attachment']['id'], - user_id=response['data']['attachment']['user_id'], - transaction_id=response['data']['attachment']['external_id'], - url=response['data']['attachment']['file_url'], - file_type=response['data']['attachment']['file_type'], - created=create_date(response['data']['attachment']['created']), + attachment_id=response["data"]["attachment"]["id"], + user_id=response["data"]["attachment"]["user_id"], + transaction_id=response["data"]["attachment"]["external_id"], + url=response["data"]["attachment"]["file_url"], + file_type=response["data"]["attachment"]["file_type"], + created=create_date(response["data"]["attachment"]["created"]), ) @classmethod @@ -190,17 +180,17 @@ def _upload_file(cls, auth: Authentication, url: str, file_type: str) -> str: URL of the uploaded file """ if not isfile(url): - raise MonzoGeneralError('File does not exist') + raise MonzoGeneralError("File does not exist") content_length = getsize(url) data = { - 'file_name': None, - 'file_type': file_type, - 'content_length': content_length, + "file_name": None, + "file_type": file_type, + "content_length": content_length, } response = auth.make_request( - path='', - method='POST', + path="", + method="POST", data=data, ) # TODO upload file - return response['data']['file_url'] + return response["data"]["file_url"] diff --git a/monzo/endpoints/balance.py b/monzo/endpoints/balance.py index 656f8ec..a07b269 100644 --- a/monzo/endpoints/balance.py +++ b/monzo/endpoints/balance.py @@ -1,4 +1,5 @@ """Class to manage balance.""" + from __future__ import annotations from monzo.authentication import Authentication @@ -13,9 +14,16 @@ class Balance(Monzo): along with the balance property. Otherwise, you can fetch an account balance using the fetch class method. """ - __slots__ = ['_balance', '_total_balance', '_currency', '_spend_today'] + __slots__ = ["_balance", "_total_balance", "_currency", "_spend_today"] - def __init__(self, auth: Authentication, balance: int, total_balance: int, currency: str, spend_today: str): + def __init__( + self, + auth: Authentication, + balance: int, + total_balance: int, + currency: str, + spend_today: str, + ): """ Initialize Balance. @@ -84,12 +92,12 @@ def fetch(cls, auth: Authentication, account_id: str) -> Balance: Returns: Balance object for the account """ - data = {'account_id': account_id} - res = auth.make_request(path='/balance', data=data) + data = {"account_id": account_id} + res = auth.make_request(path="/balance", data=data) return Balance( auth=auth, - balance=res['data']['balance'], - total_balance=res['data']['total_balance'], - currency=res['data']['currency'], - spend_today=res['data']['spend_today'], + balance=res["data"]["balance"], + total_balance=res["data"]["total_balance"], + currency=res["data"]["currency"], + spend_today=res["data"]["spend_today"], ) diff --git a/monzo/endpoints/feed_item.py b/monzo/endpoints/feed_item.py index 7c34da8..3658a01 100644 --- a/monzo/endpoints/feed_item.py +++ b/monzo/endpoints/feed_item.py @@ -1,4 +1,5 @@ """Class to manage Feed Items.""" + from __future__ import annotations from typing import Dict, Optional @@ -7,22 +8,20 @@ from monzo.endpoints.monzo import Monzo from monzo.exceptions import MonzoArgumentError -FEED_ITEM_TYPES = [ - 'basic' -] +FEED_ITEM_TYPES = ["basic"] FEED_ITEM_PARAMS = { - 'basic': { - 'optional': [ - 'body', - 'background_color', - 'title_color', - 'body_color', + "basic": { + "optional": [ + "body", + "background_color", + "title_color", + "body_color", + ], + "required": [ + "title", + "image_url", ], - 'required': [ - 'title', - 'image_url', - ] } } @@ -36,19 +35,19 @@ class FeedItem(Monzo): """ __slots__ = [ - '_account_id', - '_feed_type', - '_params', - '_url', + "_account_id", + "_feed_type", + "_params", + "_url", ] def __init__( - self, - auth: Authentication, - account_id: str, - feed_type: str, - params: Dict[str, str], - url: str = '', + self, + auth: Authentication, + account_id: str, + feed_type: str, + params: Dict[str, str], + url: str = "", ): """ Initialize FeedItem. @@ -77,16 +76,16 @@ def _validate_feed_params(self) -> Dict[str, str]: Dictionary of parameters only containing valid keys """ if self._feed_type.lower() not in FEED_ITEM_TYPES: - raise MonzoArgumentError('Feed type appears invalid') + raise MonzoArgumentError("Feed type appears invalid") parameters_clean = {} - for parameter in FEED_ITEM_PARAMS[self._feed_type.lower()]['required']: + for parameter in FEED_ITEM_PARAMS[self._feed_type.lower()]["required"]: if parameter not in self._params: - raise MonzoArgumentError(f'{parameter} is a required parameter for self._feed_type.lower()') + raise MonzoArgumentError(f"{parameter} is a required parameter for self._feed_type.lower()") parameters_clean[parameter] = self._params[parameter] - for parameter in FEED_ITEM_PARAMS[self._feed_type.lower()]['optional']: + for parameter in FEED_ITEM_PARAMS[self._feed_type.lower()]["optional"]: if parameter in self._params: parameters_clean[parameter] = self._params[parameter] @@ -96,23 +95,23 @@ def _create(self) -> None: """Create the feed item record.""" parameters = self._validate_feed_params() data = { - 'account_id': self._account_id, - 'type': self._feed_type, + "account_id": self._account_id, + "type": self._feed_type, } if self._url: - data['url'] = self._url + data["url"] = self._url for parameter in parameters.keys(): - data[f'params[{parameter}]'] = parameters[parameter] - self._monzo_auth.make_request(path='/feed', method='POST', data=data) + data[f"params[{parameter}]"] = parameters[parameter] + self._monzo_auth.make_request(path="/feed", method="POST", data=data) @classmethod def create( - cls, - auth: Authentication, - account_id: str, - feed_type: str, - params: Dict[str, str], - url: str = '' + cls, + auth: Authentication, + account_id: str, + feed_type: str, + params: Dict[str, str], + url: str = "", ) -> FeedItem: """ Create a new feed item. @@ -124,6 +123,12 @@ def create( params: Parameters for the feed item. url: Optional URL for feed item """ - feed_item = FeedItem(auth=auth, account_id=account_id, feed_type=feed_type, params=params, url=url) + feed_item = FeedItem( + auth=auth, + account_id=account_id, + feed_type=feed_type, + params=params, + url=url, + ) feed_item._create() return feed_item diff --git a/monzo/endpoints/monzo.py b/monzo/endpoints/monzo.py index e62591b..e179520 100644 --- a/monzo/endpoints/monzo.py +++ b/monzo/endpoints/monzo.py @@ -1,4 +1,5 @@ """Base Monzo class.""" + from monzo.authentication import Authentication from monzo.exceptions import MonzoAuthenticationError @@ -10,7 +11,9 @@ class Monzo(object): Class to provide basic functionality to all endpoints """ - __slots__ = ['_monzo_auth', ] + __slots__ = [ + "_monzo_auth", + ] def __init__(self, auth: Authentication): """ @@ -23,5 +26,5 @@ def __init__(self, auth: Authentication): MonzoAuthenticationError: On failure to provide an object with an access token """ if not auth.access_token: - raise MonzoAuthenticationError('Endpoint cannot be instantiated without a valid access token') + raise MonzoAuthenticationError("Endpoint cannot be instantiated without a valid access token") self._monzo_auth = auth diff --git a/monzo/endpoints/pot.py b/monzo/endpoints/pot.py index 8b13bde..10532c8 100644 --- a/monzo/endpoints/pot.py +++ b/monzo/endpoints/pot.py @@ -1,4 +1,5 @@ """Class to manage pots.""" + from __future__ import annotations from datetime import datetime @@ -19,39 +20,39 @@ class Pot(Monzo): """ __slots__ = [ - '_pot_id', - '_name', - '_style', - '_balance', - '_currency', - '_created', - '_updated', - '_deleted', - '_goal_amount', - '_has_round_up', - '_round_up_multiplier', - '_pot_type', - '_locked', - '_locked_until', + "_pot_id", + "_name", + "_style", + "_balance", + "_currency", + "_created", + "_updated", + "_deleted", + "_goal_amount", + "_has_round_up", + "_round_up_multiplier", + "_pot_type", + "_locked", + "_locked_until", ] def __init__( - self, - auth: Authentication, - pot_id: str, - name: str, - style: str, - balance: int, - currency: str, - created: datetime, - updated: datetime, - deleted: bool, - goal_amount: int | None, - round_up_multiplier: int | None, - has_round_up: bool, - pot_type: str, - locked: bool, - locked_until: datetime | None, + self, + auth: Authentication, + pot_id: str, + name: str, + style: str, + balance: int, + currency: str, + created: datetime, + updated: datetime, + deleted: bool, + goal_amount: int | None, + round_up_multiplier: int | None, + has_round_up: bool, + pot_type: str, + locked: bool, + locked_until: datetime | None, ): """ Initialize Pot. @@ -230,7 +231,14 @@ def locked_until(self) -> datetime | None: return self._locked_until @classmethod - def deposit(cls, auth: Authentication, pot: Pot, account_id: str, amount: int, dedupe_id: str) -> Pot: + def deposit( + cls, + auth: Authentication, + pot: Pot, + account_id: str, + amount: int, + dedupe_id: str, + ) -> Pot: """ Deposit funds from an account into a pot. @@ -251,15 +259,15 @@ def deposit(cls, auth: Authentication, pot: Pot, account_id: str, amount: int, d account_balance = account_balance_obj.balance if account_balance < amount: - raise MonzoGeneralError('The account does not contain enough funds') - path = f'/pots/{pot.pot_id}/deposit' + raise MonzoGeneralError("The account does not contain enough funds") + path = f"/pots/{pot.pot_id}/deposit" data = { - 'source_account_id': account_id, - 'amount': amount, - 'dedupe_id': dedupe_id + "source_account_id": account_id, + "amount": amount, + "dedupe_id": dedupe_id, } - res = auth.make_request(path=path, method='PUT', data=data) - return cls._update_pot(pot=pot, data=res['data']) + res = auth.make_request(path=path, method="PUT", data=data) + return cls._update_pot(pot=pot, data=res["data"]) @classmethod def fetch(cls, auth: Authentication, account_id: str) -> List[Pot]: @@ -273,30 +281,28 @@ def fetch(cls, auth: Authentication, account_id: str) -> List[Pot]: Returns: List of pots """ - data = { - 'current_account_id': account_id - } - res = auth.make_request(path='/pots', data=data) + data = {"current_account_id": account_id} + res = auth.make_request(path="/pots", data=data) pot_list = [] - for pot_item in res['data']['pots']: - locked_until = pot_item.get('locked_until', None) + for pot_item in res["data"]["pots"]: + locked_until = pot_item.get("locked_until", None) if locked_until: locked_until = create_date(locked_until) pot = Pot( auth=auth, - pot_id=pot_item['id'], - name=pot_item['name'], - style=pot_item['style'], - balance=pot_item['balance'], - currency=pot_item['currency'], - created=create_date(pot_item['created']), - updated=create_date(pot_item['updated']), - deleted=pot_item['deleted'], - goal_amount=pot_item.get('goal_amount', None), - round_up_multiplier=pot_item['round_up_multiplier'], - has_round_up=pot_item['round_up'], - pot_type=pot_item['type'], - locked=pot_item['locked'], + pot_id=pot_item["id"], + name=pot_item["name"], + style=pot_item["style"], + balance=pot_item["balance"], + currency=pot_item["currency"], + created=create_date(pot_item["created"]), + updated=create_date(pot_item["updated"]), + deleted=pot_item["deleted"], + goal_amount=pot_item.get("goal_amount", None), + round_up_multiplier=pot_item["round_up_multiplier"], + has_round_up=pot_item["round_up"], + pot_type=pot_item["type"], + locked=pot_item["locked"], locked_until=locked_until, ) pot_list.append(pot) @@ -319,7 +325,14 @@ def fetch_single(cls, auth: Authentication, account_id: str, pot_id: str) -> Opt return next((pot for pot in pots if pot.pot_id == pot_id), None) @classmethod - def withdraw(cls, auth: Authentication, pot: Pot, account_id: str, amount: int, dedupe_id: str) -> Pot: + def withdraw( + cls, + auth: Authentication, + pot: Pot, + account_id: str, + amount: int, + dedupe_id: str, + ) -> Pot: """ Withdraw funds from a pot into an account. @@ -337,15 +350,15 @@ def withdraw(cls, auth: Authentication, pot: Pot, account_id: str, amount: int, Updated pot """ if amount > pot.balance: - raise MonzoGeneralError('The pot does not contain enough funds') - path = f'/pots/{pot.pot_id}/withdraw' + raise MonzoGeneralError("The pot does not contain enough funds") + path = f"/pots/{pot.pot_id}/withdraw" data = { - 'destination_account_id': account_id, - 'amount': amount, - 'dedupe_id': dedupe_id + "destination_account_id": account_id, + "amount": amount, + "dedupe_id": dedupe_id, } - res = auth.make_request(path=path, method='PUT', data=data) - return cls._update_pot(pot=pot, data=res['data']) + res = auth.make_request(path=path, method="PUT", data=data) + return cls._update_pot(pot=pot, data=res["data"]) @classmethod def _update_pot(cls, pot: Pot, data: Dict[str, Any]) -> Pot: @@ -359,8 +372,8 @@ def _update_pot(cls, pot: Pot, data: Dict[str, Any]) -> Pot: Returns: Updated pot """ - pot._balance = data['balance'] - pot._created = create_date(data['created']) - pot._updated = create_date(data['updated']) + pot._balance = data["balance"] + pot._created = create_date(data["created"]) + pot._updated = create_date(data["updated"]) return pot diff --git a/monzo/endpoints/receipt.py b/monzo/endpoints/receipt.py index 61518ff..ce86b46 100644 --- a/monzo/endpoints/receipt.py +++ b/monzo/endpoints/receipt.py @@ -1,4 +1,5 @@ """Class to manage receipts.""" + from __future__ import annotations from json import dumps @@ -7,9 +8,12 @@ from monzo.authentication import Authentication from monzo.endpoints.monzo import Monzo -RECEIPTS_PATH = '/transaction-receipts' +RECEIPTS_PATH = "/transaction-receipts" -ITEM_TYPE = Dict[str, Union[float, Optional[int], str, List[Dict[str, Union[float, Optional[int], str]]]]] +ITEM_TYPE = Dict[ + str, + Union[float, Optional[int], str, List[Dict[str, Union[float, Optional[int], str]]]], +] MERCHANT_TYPE = Dict[str, Union[bool, str]] PAYMENT_TYPE = Dict[str, Union[int, str]] TAX_TYPE = Dict[str, Union[int, str]] @@ -23,23 +27,23 @@ class ReceiptItem(object): """ __slots__ = ( - '_amount', - '_currency', - '_description', - '_quantity', - '_sub_items', - '_tax', - '_unit', + "_amount", + "_currency", + "_description", + "_quantity", + "_sub_items", + "_tax", + "_unit", ) def __init__( - self, - description: str, - amount: int, - currency: str, - quantity: float = 0, - unit: str = '', - tax: int = 0, + self, + description: str, + amount: int, + currency: str, + quantity: float = 0, + unit: str = "", + tax: int = 0, ): """ Initialise ReceiptItem. @@ -77,16 +81,16 @@ def as_dict(self) -> Any: Object as a dict """ item: Any = { - 'amount': self._amount, - 'currency': self._currency, - 'description': self._description, - 'quantity': self._quantity, - 'tax': self._tax, - 'unit': self._unit, + "amount": self._amount, + "currency": self._currency, + "description": self._description, + "quantity": self._quantity, + "tax": self._tax, + "unit": self._unit, } sub_item_list: List[Any] = [sub_item.as_dict() for sub_item in self._sub_items] - item['sub_items'] = sub_item_list + item["sub_items"] = sub_item_list return item @@ -99,24 +103,24 @@ class Receipt(Monzo): """ __slots__ = ( - '_external_id', - '_transaction_id', - '_total', - '_currency', - '_items', - '_taxes', - '_payments', - '_merchant', + "_external_id", + "_transaction_id", + "_total", + "_currency", + "_items", + "_taxes", + "_payments", + "_merchant", ) def __init__( - self, - auth: Authentication, - transaction_id: str, - external_id: str, - transaction_total: int, - transaction_currency: str, - items: List[ReceiptItem], + self, + auth: Authentication, + transaction_id: str, + external_id: str, + transaction_total: int, + transaction_currency: str, + items: List[ReceiptItem], ): """ Initialize Receipt. @@ -141,14 +145,14 @@ def __init__( super().__init__(auth=auth) def add_merchant( - self, - name: str, - online: bool, - phone: Optional[str] = None, - email: Optional[str] = None, - store_name: Optional[str] = None, - store_address: Optional[str] = None, - store_postcode: Optional[str] = None, + self, + name: str, + online: bool, + phone: Optional[str] = None, + email: Optional[str] = None, + store_name: Optional[str] = None, + store_address: Optional[str] = None, + store_postcode: Optional[str] = None, ): """ Set the merchant for the receipt. @@ -163,28 +167,28 @@ def add_merchant( store_postcode: Postcode of the store """ merchant: MERCHANT_TYPE = { - 'name': name, - 'online': online, + "name": name, + "online": online, } if phone: - merchant['phone'] = phone + merchant["phone"] = phone if email: - merchant['email'] = email + merchant["email"] = email if store_name: - merchant['store_name'] = store_name + merchant["store_name"] = store_name if store_address: - merchant['store_address'] = store_address + merchant["store_address"] = store_address if store_postcode: - merchant['store_postcode'] = store_postcode + merchant["store_postcode"] = store_postcode self._merchant = merchant def add_tax( - self, - description: str, - amount: int, - currency: str, - tax_number: Optional[str] = None, + self, + description: str, + amount: int, + currency: str, + tax_number: Optional[str] = None, ): """ Add receipt tax item. @@ -196,47 +200,45 @@ def add_tax( tax_number: The tax number from the receipt """ tax: TAX_TYPE = { - 'description': description, - 'amount': amount, - 'currency': currency, + "description": description, + "amount": amount, + "currency": currency, } if tax_number: - tax['tax_number'] = tax_number + tax["tax_number"] = tax_number self._taxes.append(tax) def _create(self) -> None: """Create the receipt.""" data = { - 'transaction_id': self._transaction_id, - 'external_id': self._external_id, - 'total': self._total, - 'currency': self._currency, - 'taxes': self._taxes, - 'payments': self._payments, - 'merchant': self._merchant + "transaction_id": self._transaction_id, + "external_id": self._external_id, + "total": self._total, + "currency": self._currency, + "taxes": self._taxes, + "payments": self._payments, + "merchant": self._merchant, } receipt_items: List[ITEM_TYPE] = [item.as_dict() for item in self._items] - data['items'] = receipt_items + data["items"] = receipt_items headers = { - 'Content-Type': 'application/json', + "Content-Type": "application/json", } self._monzo_auth.make_request( path=RECEIPTS_PATH, authenticated=True, - method='PUT', + method="PUT", data=dumps(data), headers=headers, ) def _delete(self): """Delete the current receipt.""" - data = { - 'external_id': self._external_id - } - self._monzo_auth.make_request(path=RECEIPTS_PATH, data=data, method='DELETE') + data = {"external_id": self._external_id} + self._monzo_auth.make_request(path=RECEIPTS_PATH, data=data, method="DELETE") @property def external_id(self) -> str: @@ -356,31 +358,31 @@ def fetch(cls, auth: Authentication, external_id: str) -> List[Receipt]: Returns: List of receipts objects for the external ID """ - data = {'external_id': external_id} + data = {"external_id": external_id} res = auth.make_request(path=RECEIPTS_PATH, data=data) - receipt_data = res['data']['receipt'] + receipt_data = res["data"]["receipt"] receipt_items: List[ReceiptItem] = [] - for item in receipt_data['items']: - quantity = float(item['quantity']) if 'quantity' in item else 0.0 - tax = item['tax'] if 'tax' in item else 0 - unit = item['unit'] if 'unit' in item else '' + for item in receipt_data["items"]: + quantity = float(item["quantity"]) if "quantity" in item else 0.0 + tax = item["tax"] if "tax" in item else 0 + unit = item["unit"] if "unit" in item else "" receipt_item: ReceiptItem = ReceiptItem( - description=item['description'], - amount=item['amount'], - currency=item['currency'], + description=item["description"], + amount=item["amount"], + currency=item["currency"], quantity=quantity, unit=unit, tax=tax, ) - for sub_item in item['sub_items']: - sub_item_quantity = sub_item['quantity'] if 'quantity' in sub_item else 0.0 - sub_item_tax = sub_item['tax'] if 'tax' in sub_item else 0 - sub_item_unit = sub_item['unit'] if 'unit' in sub_item else '' + for sub_item in item["sub_items"]: + sub_item_quantity = sub_item["quantity"] if "quantity" in sub_item else 0.0 + sub_item_tax = sub_item["tax"] if "tax" in sub_item else 0 + sub_item_unit = sub_item["unit"] if "unit" in sub_item else "" receipt_sub_item = ReceiptItem( - description=sub_item['description'], - amount=sub_item['amount'], - currency=sub_item['currency'], + description=sub_item["description"], + amount=sub_item["amount"], + currency=sub_item["currency"], quantity=sub_item_quantity, unit=sub_item_unit, tax=sub_item_tax, @@ -391,32 +393,32 @@ def fetch(cls, auth: Authentication, external_id: str) -> List[Receipt]: receipt: Receipt = Receipt( auth=auth, - transaction_id=receipt_data['transaction_id'], - external_id=receipt_data['external_id'], - transaction_total=receipt_data['total'], - transaction_currency=receipt_data['currency'], + transaction_id=receipt_data["transaction_id"], + external_id=receipt_data["external_id"], + transaction_total=receipt_data["total"], + transaction_currency=receipt_data["currency"], items=receipt_items, ) - if receipt_data['merchant']['name']: - merchant_data = receipt_data['merchant'] + if receipt_data["merchant"]["name"]: + merchant_data = receipt_data["merchant"] receipt.add_merchant( - name=merchant_data['name'], - online=merchant_data['online'], - phone=merchant_data['phone'], - email=merchant_data['email'], - store_name=merchant_data['store_name'], - store_address=merchant_data['store_address'], - store_postcode=merchant_data['store_postcode'], + name=merchant_data["name"], + online=merchant_data["online"], + phone=merchant_data["phone"], + email=merchant_data["email"], + store_name=merchant_data["store_name"], + store_address=merchant_data["store_address"], + store_postcode=merchant_data["store_postcode"], ) - if len(receipt_data['taxes']): - for tax_data in receipt_data['taxes']: + if len(receipt_data["taxes"]): + for tax_data in receipt_data["taxes"]: receipt.add_tax( - description=tax_data['description'], - amount=tax_data['amount'], - currency=tax_data['currency'], - tax_number=tax_data['tax_number'], + description=tax_data["description"], + amount=tax_data["amount"], + currency=tax_data["currency"], + tax_number=tax_data["tax_number"], ) return [receipt] diff --git a/monzo/endpoints/transaction.py b/monzo/endpoints/transaction.py index 8ae972b..b29e404 100644 --- a/monzo/endpoints/transaction.py +++ b/monzo/endpoints/transaction.py @@ -1,4 +1,5 @@ """Class to manage transactions.""" + from __future__ import annotations from datetime import datetime @@ -8,9 +9,7 @@ from monzo.endpoints.monzo import Monzo from monzo.helpers import create_date, format_date -EXPAND_VALID_VALUES = [ - 'merchant' -] +EXPAND_VALID_VALUES = ["merchant"] class Transaction(Monzo): @@ -21,47 +20,43 @@ class Transaction(Monzo): """ __slots__ = [ - '_account_id', - '_amount', - '_amount_is_pending', - '_atm_fees_detailed', - '_attachments', - '_can_add_to_tab', - '_can_be_excluded_from_breakdown', - '_can_be_made_subscription', - '_can_match_transactions_in_categorization', - '_can_split_the_bill', - '_categories', - '_category', - '_counterparty', - '_created', - '_currency', - '_decline_reason', - '_dedupe_id', - '_description', - '_fees', - '_include_in_spending', - '_international', - '_is_load', - '_labels', - '_local_amount', - '_local_currency', - '_merchant', - '_metadata', - '_notes', - '_originator', - '_scheme', - '_settled', - '_transaction_id', - '_updated', - '_user_id' + "_account_id", + "_amount", + "_amount_is_pending", + "_atm_fees_detailed", + "_attachments", + "_can_add_to_tab", + "_can_be_excluded_from_breakdown", + "_can_be_made_subscription", + "_can_match_transactions_in_categorization", + "_can_split_the_bill", + "_categories", + "_category", + "_counterparty", + "_created", + "_currency", + "_decline_reason", + "_dedupe_id", + "_description", + "_fees", + "_include_in_spending", + "_international", + "_is_load", + "_labels", + "_local_amount", + "_local_currency", + "_merchant", + "_metadata", + "_notes", + "_originator", + "_scheme", + "_settled", + "_transaction_id", + "_updated", + "_user_id", ] - def __init__( - self, - auth: Authentication, - transaction_data: Dict[str, Any] - ): + def __init__(self, auth: Authentication, transaction_data: Dict[str, Any]): """ Initialize Transaction. @@ -69,43 +64,44 @@ def __init__( auth: Monzo authentication object transaction_data: Data returned from an API call """ - self._account_id: str = transaction_data['account_id'] - self._amount: int = transaction_data['amount'] - self._amount_is_pending: bool = transaction_data['amount_is_pending'] - self._atm_fees_detailed: str = transaction_data['atm_fees_detailed'] - self._attachments: str = transaction_data['attachments'] - self._can_add_to_tab: bool = transaction_data['can_add_to_tab'] - self._can_be_excluded_from_breakdown: bool = transaction_data['can_be_excluded_from_breakdown'] - self._can_be_made_subscription: bool = transaction_data['can_be_made_subscription'] - self._can_match_transactions_in_categorization: bool = \ - transaction_data['can_match_transactions_in_categorization'] - self._can_split_the_bill: bool = transaction_data['can_split_the_bill'] - self._categories: Dict[str, Union[int, str]] = transaction_data['categories'] - self._category: str = transaction_data['category'] - self._counterparty = transaction_data['counterparty'] - self._created: datetime = create_date(transaction_data['created']) - self._currency: str = transaction_data['currency'] - self._dedupe_id: str = transaction_data['dedupe_id'] - self._decline_reason: str = transaction_data.get('decline_reason', '') - self._description: str = transaction_data['description'] - self._fees: Dict[str, Any] = transaction_data['fees'] - self._include_in_spending: bool = transaction_data['include_in_spending'] - self._international: Optional[str] = transaction_data['international'] - self._is_load: bool = transaction_data['is_load'] - self._labels: str = transaction_data['labels'] - self._local_amount: int = transaction_data['local_amount'] - self._local_currency: str = transaction_data['local_currency'] - self._merchant: str = transaction_data['merchant'] - self._metadata: Dict[str, str] = transaction_data['metadata'] - self._notes: str = transaction_data['notes'] - self._originator: bool = transaction_data['originator'] - self._scheme: str = transaction_data['scheme'] + self._account_id: str = transaction_data["account_id"] + self._amount: int = transaction_data["amount"] + self._amount_is_pending: bool = transaction_data["amount_is_pending"] + self._atm_fees_detailed: str = transaction_data["atm_fees_detailed"] + self._attachments: str = transaction_data["attachments"] + self._can_add_to_tab: bool = transaction_data["can_add_to_tab"] + self._can_be_excluded_from_breakdown: bool = transaction_data["can_be_excluded_from_breakdown"] + self._can_be_made_subscription: bool = transaction_data["can_be_made_subscription"] + self._can_match_transactions_in_categorization: bool = transaction_data[ + "can_match_transactions_in_categorization" + ] + self._can_split_the_bill: bool = transaction_data["can_split_the_bill"] + self._categories: Dict[str, Union[int, str]] = transaction_data["categories"] + self._category: str = transaction_data["category"] + self._counterparty = transaction_data["counterparty"] + self._created: datetime = create_date(transaction_data["created"]) + self._currency: str = transaction_data["currency"] + self._dedupe_id: str = transaction_data["dedupe_id"] + self._decline_reason: str = transaction_data.get("decline_reason", "") + self._description: str = transaction_data["description"] + self._fees: Dict[str, Any] = transaction_data["fees"] + self._include_in_spending: bool = transaction_data["include_in_spending"] + self._international: Optional[str] = transaction_data["international"] + self._is_load: bool = transaction_data["is_load"] + self._labels: str = transaction_data["labels"] + self._local_amount: int = transaction_data["local_amount"] + self._local_currency: str = transaction_data["local_currency"] + self._merchant: str = transaction_data["merchant"] + self._metadata: Dict[str, str] = transaction_data["metadata"] + self._notes: str = transaction_data["notes"] + self._originator: bool = transaction_data["originator"] + self._scheme: str = transaction_data["scheme"] self._settled: Optional[datetime] = None - if transaction_data['settled']: - self._settled = create_date(transaction_data['settled']) - self._transaction_id: str = transaction_data['id'] - self._updated: datetime = create_date(transaction_data['updated']) - self._user_id: str = transaction_data['user_id'] + if transaction_data["settled"]: + self._settled = create_date(transaction_data["settled"]) + self._transaction_id: str = transaction_data["id"] + self._updated: datetime = create_date(transaction_data["updated"]) + self._user_id: str = transaction_data["user_id"] super().__init__(auth=auth) @@ -458,9 +454,9 @@ def user_id(self) -> str: return self._user_id def annotate( - self, - key: str, - value: str = '', + self, + key: str, + value: str = "", ): """ Annotate the transaction. @@ -472,20 +468,17 @@ def annotate( key: Key for the annotation. value: Value for annotation, if left blank, it will remove the annotation. """ - path = f'/transactions/{self.transaction_id}' + path = f"/transactions/{self.transaction_id}" data = { - f'metadata[{key}]': value, + f"metadata[{key}]": value, } - res = self._monzo_auth.make_request(path=path, method='PATCH', data=data) - self._notes = res['data']['transaction']['notes'] - self._metadata = res['data']['transaction']['metadata'] + res = self._monzo_auth.make_request(path=path, method="PATCH", data=data) + self._notes = res["data"]["transaction"]["notes"] + self._metadata = res["data"]["transaction"]["metadata"] @classmethod def fetch_single( - cls, - auth: Authentication, - transaction_id: str, - expand_on: str = 'merchant' + cls, auth: Authentication, transaction_id: str, expand_on: str = "merchant" ) -> Optional[Transaction]: """ Fetch a transaction with a specific ID. @@ -501,23 +494,23 @@ def fetch_single( data = {} if expand_on and expand_on.lower() in EXPAND_VALID_VALUES: data = { - 'expand[]': expand_on, + "expand[]": expand_on, } - path = f'/transactions/{transaction_id}' + path = f"/transactions/{transaction_id}" res = auth.make_request(path=path, data=data) - if len(res['data'].get('transaction', {})) == 0: + if len(res["data"].get("transaction", {})) == 0: return None - return Transaction(auth=auth, transaction_data=res['data']['transaction']) + return Transaction(auth=auth, transaction_data=res["data"]["transaction"]) @classmethod def fetch( - cls, - auth: Authentication, - account_id: str, - since: Optional[datetime] = None, - before: Optional[datetime] = None, - expand=None, - limit=30, + cls, + auth: Authentication, + account_id: str, + since: Optional[datetime | str] = None, + before: Optional[datetime] = None, + expand=None, + limit=30, ) -> List[Transaction]: """ Fetch a list of transaction. @@ -536,21 +529,24 @@ def fetch( if expand is None: expand = [] data = { - 'account_id': account_id, + "account_id": account_id, } if expand: # TODO fix so that this works on the list - data['expand'] = expand[0] + data["expand"] = expand[0] if since: - data['since'] = format_date(since) + if isinstance(since, datetime): + data["since"] = format_date(since) + elif isinstance(since, str): + data["since"] = since if before: - data['before'] = format_date(before) + data["before"] = format_date(before) if limit: - data['limit'] = min(limit, 100) - path = '/transactions' + data["limit"] = min(limit, 100) + path = "/transactions" res = auth.make_request(path=path, data=data) transactions = [] - for transaction_data in res['data']['transactions']: + for transaction_data in res["data"]["transactions"]: transaction = Transaction(auth=auth, transaction_data=transaction_data) transactions.append(transaction) return transactions diff --git a/monzo/endpoints/webhooks.py b/monzo/endpoints/webhooks.py index 4417c2e..efbd4e9 100644 --- a/monzo/endpoints/webhooks.py +++ b/monzo/endpoints/webhooks.py @@ -1,4 +1,5 @@ """Class to manage webhooks.""" + from __future__ import annotations from typing import List @@ -14,7 +15,7 @@ class Webhook(Monzo): Class provides methods to create, fetch and delete webhooks. """ - __slots__ = ['_account_id', '_url', '_webhook_id'] + __slots__ = ["_account_id", "_url", "_webhook_id"] def __init__(self, auth: Authentication, account_id: str, url: str, webhook_id: str): """ @@ -34,16 +35,16 @@ def __init__(self, auth: Authentication, account_id: str, url: str, webhook_id: def _create(self) -> None: """Create the webhook.""" data = { - 'account_id': self._account_id, - 'url': self._url, + "account_id": self._account_id, + "url": self._url, } - res = self._monzo_auth.make_request(path='/webhooks', method='POST', data=data) - self._webhook_id = res['data']['webhook']['id'] + res = self._monzo_auth.make_request(path="/webhooks", method="POST", data=data) + self._webhook_id = res["data"]["webhook"]["id"] def _delete(self): """Delete the current webhook.""" - path = f'/webhooks/{self._webhook_id}' - self._monzo_auth.make_request(path=path, method='DELETE') + path = f"/webhooks/{self._webhook_id}" + self._monzo_auth.make_request(path=path, method="DELETE") @property def account_id(self) -> str: @@ -88,7 +89,7 @@ def create(cls, auth: Authentication, account_id: str, url: str) -> Webhook: Returns: Created webhook """ - webhook = Webhook(auth=auth, account_id=account_id, url=url, webhook_id='NEW') + webhook = Webhook(auth=auth, account_id=account_id, url=url, webhook_id="NEW") webhook._create() return webhook @@ -114,15 +115,15 @@ def fetch(cls, auth: Authentication, account_id: str) -> List[Webhook]: Returns: List of webhook objects for the account """ - data = {'account_id': account_id} - res = auth.make_request(path='/webhooks', data=data) + data = {"account_id": account_id} + res = auth.make_request(path="/webhooks", data=data) webhooks = [] - for webhook_item in res['data']['webhooks']: + for webhook_item in res["data"]["webhooks"]: webhook = Webhook( auth=auth, - account_id=webhook_item['account_id'], - url=webhook_item['url'], - webhook_id=webhook_item['id'], + account_id=webhook_item["account_id"], + url=webhook_item["url"], + webhook_id=webhook_item["id"], ) webhooks.append(webhook) return webhooks diff --git a/monzo/endpoints/whoami.py b/monzo/endpoints/whoami.py index a1c3291..b9761c4 100644 --- a/monzo/endpoints/whoami.py +++ b/monzo/endpoints/whoami.py @@ -1,4 +1,5 @@ """Class to manage the whoami request.""" + from __future__ import annotations from monzo.authentication import Authentication @@ -12,7 +13,7 @@ class WhoAmI(Monzo): Class provides access to the whoami endpoint. This is usually used to test connectivity to the API """ - __slots__ = ['_authenticated', '_client_id', '_user_id'] + __slots__ = ["_authenticated", "_client_id", "_user_id"] def __init__(self, auth: Authentication, authenticated: bool, client_id: str, user_id: str): """ @@ -70,10 +71,10 @@ def fetch(cls, auth: Authentication) -> WhoAmI: Returns: Instantiated WhoAmI object """ - res = auth.make_request(path='/ping/whoami') + res = auth.make_request(path="/ping/whoami") return cls( auth=auth, - authenticated=res['data']['authenticated'], - client_id=res['data']['client_id'], - user_id=res['data']['user_id'], + authenticated=res["data"]["authenticated"], + client_id=res["data"]["client_id"], + user_id=res["data"]["user_id"], ) diff --git a/monzo/exceptions.py b/monzo/exceptions.py index 316871d..d9f2a1a 100644 --- a/monzo/exceptions.py +++ b/monzo/exceptions.py @@ -8,8 +8,6 @@ class MonzoError(Exception): Exception all other Monzo exceptions inherit from """ - pass - class MonzoAuthenticationError(MonzoError): """ @@ -18,8 +16,6 @@ class MonzoAuthenticationError(MonzoError): Exception to be thrown when Authentication failure has occurred at any point in dealing with the API """ - pass - class MonzoHTTPError(MonzoError): """ @@ -28,8 +24,6 @@ class MonzoHTTPError(MonzoError): Exception to be thrown when an HTTP error occurs during an API call """ - pass - class MonzoArgumentError(MonzoError): """ @@ -38,8 +32,6 @@ class MonzoArgumentError(MonzoError): Exception to be thrown when an invalid value has been passed as an argument to an endpoint """ - pass - class MonzoServerError(MonzoError): """ @@ -48,8 +40,6 @@ class MonzoServerError(MonzoError): Exception usually caused by an issue on the Monzo servers """ - pass - class MonzoPermissionsError(MonzoError): """ @@ -58,8 +48,6 @@ class MonzoPermissionsError(MonzoError): The API is logged in but does not have enough permissions to perform the query """ - pass - class MonzoRateError(MonzoError): """ @@ -68,8 +56,6 @@ class MonzoRateError(MonzoError): Exception to be thrown when a Monzo advises you are exceeding the rate limit for the API """ - pass - class MonzoGeneralError(MonzoError): """ @@ -77,5 +63,3 @@ class MonzoGeneralError(MonzoError): Exception to be thrown when a general error occurs that does not fit into other exception types """ - - pass diff --git a/monzo/handlers/filesystem.py b/monzo/handlers/filesystem.py index e119d20..ab439c4 100644 --- a/monzo/handlers/filesystem.py +++ b/monzo/handlers/filesystem.py @@ -1,4 +1,5 @@ """Class to store credentials on the file system.""" + from json import dumps, loads from typing import Dict, Union @@ -8,7 +9,7 @@ class FileSystem(Storage): """Class that will store credentials on the file system.""" - __slots__ = ['_file'] + __slots__ = ["_file"] def __init__(self, file: str): """ @@ -27,7 +28,7 @@ def fetch(self) -> Dict[str, Union[int, str]]: Dictionary containing access token, expiry and refresh token """ try: - with open(self._file, 'r') as handler: + with open(self._file, "r") as handler: content = loads(handler.read()) except FileNotFoundError: content = {} @@ -35,12 +36,12 @@ def fetch(self) -> Dict[str, Union[int, str]]: return content def store( - self, - access_token: str, - client_id: str, - client_secret: str, - expiry: int, - refresh_token: str = '' + self, + access_token: str, + client_id: str, + client_secret: str, + expiry: int, + refresh_token: str = "", ) -> None: """ Store the Monzo credentials. @@ -53,11 +54,11 @@ def store( refresh_token: Refresh token that can be used to renew an access token """ content = { - 'access_token': access_token, - 'client_id': client_id, - 'client_secret': client_secret, - 'expiry': expiry, - 'refresh_token': refresh_token + "access_token": access_token, + "client_id": client_id, + "client_secret": client_secret, + "expiry": expiry, + "refresh_token": refresh_token, } - with open(self._file, 'w') as handler: + with open(self._file, "w") as handler: handler.write(dumps(content)) diff --git a/monzo/handlers/storage.py b/monzo/handlers/storage.py index 9c1dcc4..c8af999 100644 --- a/monzo/handlers/storage.py +++ b/monzo/handlers/storage.py @@ -1,4 +1,5 @@ """Abstract class for credential storage.""" + from abc import ABC, abstractmethod from typing import Dict, Union @@ -19,16 +20,15 @@ def fetch(self) -> Dict[str, Union[int, str]]: Returns: Dictionary containing access token, expiry and refresh token """ - pass @abstractmethod def store( - self, - access_token: str, - client_id: str, - client_secret: str, - expiry: int, - refresh_token: str = '' + self, + access_token: str, + client_id: str, + client_secret: str, + expiry: int, + refresh_token: str = "", ) -> None: """ Abstract method that needs to be implemented to store credentials from Monzo. @@ -40,4 +40,3 @@ def store( expiry: Access token expiry as a unix timestamp refresh_token: Refresh token that can be used to renew an access token """ - pass diff --git a/monzo/helpers.py b/monzo/helpers.py index 46029c1..3bec7ec 100644 --- a/monzo/helpers.py +++ b/monzo/helpers.py @@ -1,4 +1,5 @@ """Helper functions.""" + from datetime import datetime @@ -12,7 +13,7 @@ def create_date(date_str: str) -> datetime: Returns: Converted date and time """ - return datetime.strptime(date_str[:19], '%Y-%m-%dT%H:%M:%S') + return datetime.strptime(date_str[:19], "%Y-%m-%dT%H:%M:%S") def format_date(date: datetime) -> str: @@ -25,4 +26,4 @@ def format_date(date: datetime) -> str: Returns: Converted date and time """ - return date.strftime('%Y-%m-%dT%H:%M:%SZ') + return date.strftime("%Y-%m-%dT%H:%M:%SZ") diff --git a/monzo/httpio.py b/monzo/httpio.py index 6ccb039..a7ad7f1 100644 --- a/monzo/httpio.py +++ b/monzo/httpio.py @@ -1,12 +1,19 @@ -"""Class that handles HTTP requests.""" +"""Class that handles HTTP requests.""" + from json import loads from typing import Any, Dict, Optional from urllib.error import HTTPError from urllib.parse import urlencode from urllib.request import Request, urlopen -from monzo.exceptions import (MonzoAuthenticationError, MonzoGeneralError, MonzoHTTPError, MonzoPermissionsError, - MonzoRateError, MonzoServerError) +from monzo.exceptions import ( + MonzoAuthenticationError, + MonzoGeneralError, + MonzoHTTPError, + MonzoPermissionsError, + MonzoRateError, + MonzoServerError, +) DEFAULT_TIMEOUT = 10 @@ -60,7 +67,13 @@ def delete(self, path: str, data=None, headers=None, timeout: int = DEFAULT_TIME if data is None: data = {} parameters = urlencode(data).encode() if data else None - return self._perform_request(method='DELETE', path=path, data=parameters, headers=headers, timeout=timeout) + return self._perform_request( + method="DELETE", + path=path, + data=parameters, + headers=headers, + timeout=timeout, + ) def get(self, path: str, data=None, headers=None, timeout: int = DEFAULT_TIMEOUT) -> REQUEST_RESPONSE_TYPE: """ @@ -81,16 +94,10 @@ def get(self, path: str, data=None, headers=None, timeout: int = DEFAULT_TIMEOUT headers = {} parameters = urlencode(data) if data else None if parameters: - path += f'?{parameters}' - return self._perform_request(method='GET', path=path, data=None, headers=headers, timeout=timeout) - - def patch( - self, - path: str, - data=None, - headers=None, - timeout: int = DEFAULT_TIMEOUT - ) -> REQUEST_RESPONSE_TYPE: + path += f"?{parameters}" + return self._perform_request(method="GET", path=path, data=None, headers=headers, timeout=timeout) + + def patch(self, path: str, data=None, headers=None, timeout: int = DEFAULT_TIMEOUT) -> REQUEST_RESPONSE_TYPE: """ Perform a PATCH request. @@ -108,15 +115,9 @@ def patch( if data is None: data = {} parameters = urlencode(data).encode() if data else None - return self._perform_request(method='PATCH', path=path, data=parameters, headers=headers, timeout=timeout) - - def post( - self, - path: str, - data=None, - headers=None, - timeout: int = DEFAULT_TIMEOUT - ) -> REQUEST_RESPONSE_TYPE: + return self._perform_request(method="PATCH", path=path, data=parameters, headers=headers, timeout=timeout) + + def post(self, path: str, data=None, headers=None, timeout: int = DEFAULT_TIMEOUT) -> REQUEST_RESPONSE_TYPE: """ Perform a POST request. @@ -134,15 +135,9 @@ def post( if data is None: data = {} parameters = urlencode(data).encode() if data else None - return self._perform_request(method='POST', path=path, data=parameters, headers=headers, timeout=timeout) - - def put( - self, - path: str, - data=None, - headers=None, - timeout: int = DEFAULT_TIMEOUT - ) -> REQUEST_RESPONSE_TYPE: + return self._perform_request(method="POST", path=path, data=parameters, headers=headers, timeout=timeout) + + def put(self, path: str, data=None, headers=None, timeout: int = DEFAULT_TIMEOUT) -> REQUEST_RESPONSE_TYPE: """ Perform a PUT request. @@ -162,16 +157,16 @@ def put( if type(data) is dict: parameters = urlencode(data).encode() if data else None else: - parameters = data.encode('utf8') - return self._perform_request(method='PUT', path=path, data=parameters, headers=headers, timeout=timeout) + parameters = data.encode("utf8") # type: ignore + return self._perform_request(method="PUT", path=path, data=parameters, headers=headers, timeout=timeout) def _perform_request( - self, - method: str, - path: str, - data: Optional[bytes], - headers: Dict[str, Any], - timeout + self, + method: str, + path: str, + data: Optional[bytes], + headers: Dict[str, Any], + timeout, ) -> REQUEST_RESPONSE_TYPE: """ Perform a given request. @@ -186,19 +181,19 @@ def _perform_request( Returns: Dictionary containing the response code, headers and content """ - full_url = f'{self._url}{path}' + full_url = f"{self._url}{path}" try: request = Request(url=full_url, data=data, headers=headers) request.method = method - content: str = '' + content: str = "" response = urlopen(request, timeout=timeout) with response as fh: - content += fh.read().decode('utf-8') + content += fh.read().decode("utf-8") except HTTPError as error: exception_cls = MONZO_ERROR_MAP.get(error.code, MonzoGeneralError) raise exception_cls() from error return { - 'code': response.code, - 'headers': response.headers, - 'data': loads(content) if len(content) > 0 else '', + "code": response.code, + "headers": response.headers, + "data": loads(content) if len(content) > 0 else "", } diff --git a/py.typed b/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/pyproject.toml b/pyproject.toml index 9787c3b..070d636 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,45 @@ -[build-system] -requires = ["setuptools", "wheel"] -build-backend = "setuptools.build_meta" +[project] +name = "Monzo-API" +version = "1.2.2" +authors = [{ name = "Peter McDonald", email = "git@petermcdonald.co.uk"}] +description = "Package to interact with the API provided by Monzo bank" +keywords = ["monzo", "bank", "api"] +readme = "README.rst" +license = { file = "LICENSE" } +requires-python = ">=3.10" + +classifiers = [ + "Intended Audience :: Developers", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Topic :: Office/Business :: Financial", + "Typing :: Typed", +] + +[project.urls] +homepage = "https://github.com/petermcd/monzo-api" +repository = "https://github.com/petermcd/monzo-api.git" +issues = "https://github.com/petermcd/monzo-api/issues" +documentation = "https://monzo-api.readthedocs.io" + +[dependency-groups] +dev = [ + "pytest>=8.4.2", + "pytest-cov>=7.0.0", + "pytest-mock>=3.15.1", + "ruff>=0.14.3", + "sphinx>=8.1.3", + "sphinx-rtd-theme>=3.0.2", + "ty>=0.0.1a25", +] + +[tool.pytest.ini_options] +addopts = "--cov=monzo --cov-report term-missing --cov-report xml:coverage.xml" + +[tool.ruff] +line-length = 120 diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 09b9e10..0000000 --- a/setup.cfg +++ /dev/null @@ -1,36 +0,0 @@ -[metadata] -name = Monzo API -version = 1.2.1 -author = Peter McDonald -author_email = git@petermcdonald.co.uk -description = Package to interact with the API provided by Monzo bank -long_description = file: README.rst -long_description_content_type = text/x-rst -url = https://github.com/petermcd/monzo-api -project_urls = - Bug Tracker = https://github.com/petermcd/monzo-api/issues - Documentation = https://monzo-api.readthedocs.io - Source = https://github.com/petermcd/monzo-api -classifiers = - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 - Programming Language :: Python :: 3.11 - Programming Language :: Python :: 3.12 - Programming Language :: Python :: 3.13 - License :: OSI Approved :: MIT License - Operating System :: OS Independent - Intended Audience :: Developers - Topic :: Office/Business :: Financial - -[options] -packages = find: -python_requires = >=3.8 - -[options.extras_require] -test = flake8; isort; mypy; pytest; pytest-mock; -build = wheel; build; -docs = sphinx; sphinx_rtd_theme; - -[flake8] -max_line_length = 120 diff --git a/tests/helpers.py b/tests/helpers.py index 81eb9ba..8e86e30 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -1,4 +1,5 @@ """Helper functions and classes for testing.""" + import pathlib from datetime import datetime, timedelta from json import loads @@ -18,7 +19,7 @@ def load_data(path: str, filename: str): Returns: JSON Content """ - content = pathlib.Path(f'./tests/{path}/{filename}.json').read_text() + content = pathlib.Path(f"./tests/{path}/{filename}.json").read_text() return loads(content) @@ -26,22 +27,22 @@ class Handler(Storage): """Class to use as a handler for testing.""" __slots__ = ( - '_access_token', - '_client_id', - '_client_secret', - '_expiry', - '_refresh_token' + "_access_token", + "_client_id", + "_client_secret", + "_expiry", + "_refresh_token", ) def __init__(self): """Initialise Handler.""" expiry = int((datetime.now() + timedelta(days=1)).timestamp()) - self._access_token = 'abc123' - self._client_id = 'cde456' - self._client_secret = 'fgh789' + self._access_token = "abc123" + self._client_id = "cde456" + self._client_secret = "fgh789" self._expiry = expiry - self._refresh_token = 'ijk012' + self._refresh_token = "ijk012" def fetch(self) -> Dict[str, Union[int, str]]: """ @@ -51,20 +52,20 @@ def fetch(self) -> Dict[str, Union[int, str]]: Dictionary containing access token, expiry and refresh token """ return { - 'access_token': self._access_token, - 'client_id': self._client_id, - 'client_secret': self._client_secret, - 'expiry': self._expiry, - 'refresh_token': self._refresh_token + "access_token": self._access_token, + "client_id": self._client_id, + "client_secret": self._client_secret, + "expiry": self._expiry, + "refresh_token": self._refresh_token, } def store( - self, - access_token: str, - client_id: str, - client_secret: str, - expiry: int, - refresh_token: str = '' + self, + access_token: str, + client_id: str, + client_secret: str, + expiry: int, + refresh_token: str = "", ) -> None: """ Store the Monzo credentials. diff --git a/tests/test_authentication.py b/tests/test_authentication.py index 1f6831f..b2ed97f 100644 --- a/tests/test_authentication.py +++ b/tests/test_authentication.py @@ -1,4 +1,5 @@ """Tests for authentication.""" + import pytest from monzo import authentication @@ -18,8 +19,8 @@ def test_logout(self, mocker): """ httpio_capture = mocker.patch.object( authentication.HttpIO, - 'post', - return_value=load_data(path='mock_responses', filename='Logout') + "post", + return_value=load_data(path="mock_responses", filename="Logout"), ) handler = Handler() @@ -27,25 +28,25 @@ def test_logout(self, mocker): credentials = handler.fetch() auth = authentication.Authentication( - client_id=credentials['client_id'], - client_secret=credentials['client_secret'], - redirect_url='', - access_token=credentials['access_token'], - access_token_expiry=credentials['expiry'], - refresh_token=credentials['refresh_token'], + client_id=str(credentials["client_id"]), + client_secret=str(credentials["client_secret"]), + redirect_url="", + access_token=str(credentials["access_token"]), + access_token_expiry=int(credentials["expiry"]), + refresh_token=str(credentials["refresh_token"]), ) auth.register_callback_handler(handler) auth.logout() - expected_data = load_data(path='mock_payloads', filename='Logout') + expected_data = load_data(path="mock_payloads", filename="Logout") httpio_capture.assert_called_with( - data=expected_data['data'], - headers=expected_data['headers'], - path=expected_data['path'], - timeout=expected_data['timeout'] + data=expected_data["data"], + headers=expected_data["headers"], + path=expected_data["path"], + timeout=expected_data["timeout"], ) def test_authenticate_with_empty_token(self): @@ -53,13 +54,13 @@ def test_authenticate_with_empty_token(self): Test authenticate raises an error when the authorization token is empty. """ auth = authentication.Authentication( - client_id='client_id', - client_secret='client_secret', - redirect_url='', + client_id="client_id", + client_secret="client_secret", + redirect_url="", ) with pytest.raises(MonzoAuthenticationError): - auth.authenticate(authorization_token='', state_token='state_token') + auth.authenticate(authorization_token="", state_token="state_token") def test_authenticate_state_token_mismatch(self, tmp_path, mocker): """ @@ -69,16 +70,16 @@ def test_authenticate_state_token_mismatch(self, tmp_path, mocker): tmp_path: Pytest fixture for temporary directory. mocker: Pytest mocker fixture. """ - mocker.patch('monzo.authentication.gettempdir', return_value=str(tmp_path)) + mocker.patch("monzo.authentication.gettempdir", return_value=str(tmp_path)) auth = authentication.Authentication( - client_id='client_id', - client_secret='client_secret', - redirect_url='', + client_id="client_id", + client_secret="client_secret", + redirect_url="", ) state_token = auth.state_token with pytest.raises(MonzoAuthenticationError): auth.authenticate( - authorization_token='auth_token', - state_token=f'{state_token}invalid', + authorization_token="auth_token", + state_token=f"{state_token}invalid", ) diff --git a/tests/test_endpoints.py b/tests/test_endpoints.py index 7171e92..2a02b29 100644 --- a/tests/test_endpoints.py +++ b/tests/test_endpoints.py @@ -1,4 +1,5 @@ """Tests for endpoints.""" + from datetime import datetime from typing import Any, Dict, List, Optional, Union @@ -18,22 +19,27 @@ class TestEndPoints(object): """Tests for the endpoints.""" @pytest.mark.parametrize( - 'mock_file,expected_account_id,expected_account_type,expected_description', + "mock_file,expected_account_id,expected_account_type,expected_description", [ - ('Accounts', 'acc_123ABC', 'Current Account', 'user_123ABC'), - ('Accounts_Flex_Type', 'acc_123ABC', 'Flex', 'monzoflex_123ABC'), - ('Accounts_Flex_Loan_Type', 'acc_123ABC', 'Loan (Flex)', 'monzoflexbackingloan_123ABC'), - ('Accounts_Loan_Type', 'acc_123ABC', 'Loan', 'loan_123ABC'), - ('Accounts_Unknown_Type', 'acc_123ABC', 'UNKNOWN', 'random_123ABC'), + ("Accounts", "acc_123ABC", "Current Account", "user_123ABC"), + ("Accounts_Flex_Type", "acc_123ABC", "Flex", "monzoflex_123ABC"), + ( + "Accounts_Flex_Loan_Type", + "acc_123ABC", + "Loan (Flex)", + "monzoflexbackingloan_123ABC", + ), + ("Accounts_Loan_Type", "acc_123ABC", "Loan", "loan_123ABC"), + ("Accounts_Unknown_Type", "acc_123ABC", "UNKNOWN", "random_123ABC"), ], ) def test_account( - self, - mock_file: str, - expected_account_id: str, - expected_account_type: str, - expected_description: str, - mocker + self, + mock_file: str, + expected_account_id: str, + expected_account_type: str, + expected_description: str, + mocker, ): """ Test Account endpoint. @@ -47,8 +53,8 @@ def test_account( """ mocker.patch.object( authentication.HttpIO, - 'get', - return_value=load_data(path='mock_responses', filename=mock_file) + "get", + return_value=load_data(path="mock_responses", filename=mock_file), ) handler = Handler() @@ -56,12 +62,12 @@ def test_account( credentials = handler.fetch() auth = authentication.Authentication( - client_id=credentials['client_id'], - client_secret=credentials['client_secret'], - redirect_url='', - access_token=credentials['access_token'], - access_token_expiry=credentials['expiry'], - refresh_token=credentials['refresh_token'], + client_id=str(credentials["client_id"]), + client_secret=str(credentials["client_secret"]), + redirect_url="", + access_token=str(credentials["access_token"]), + access_token_expiry=int(credentials["expiry"]), + refresh_token=str(credentials["refresh_token"]), ) auth.register_callback_handler(handler) @@ -74,19 +80,19 @@ def test_account( assert accounts[0].description == expected_description @pytest.mark.parametrize( - 'mock_file,expected_balance,expected_currency,expected_spend_today,expected_total_balance', + "mock_file,expected_balance,expected_currency,expected_spend_today,expected_total_balance", [ - ('Balance', 40000, 'GBP', -3000, 60000), + ("Balance", 40000, "GBP", -3000, 60000), ], ) def test_balance( - self, - mock_file: str, - expected_balance: int, - expected_currency: str, - expected_spend_today: int, - expected_total_balance: int, - mocker + self, + mock_file: str, + expected_balance: int, + expected_currency: str, + expected_spend_today: int, + expected_total_balance: int, + mocker, ): """ Test Balance endpoint. @@ -101,8 +107,8 @@ def test_balance( """ mocker.patch.object( authentication.HttpIO, - 'get', - return_value=load_data(path='mock_responses', filename=mock_file) + "get", + return_value=load_data(path="mock_responses", filename=mock_file), ) handler = Handler() @@ -110,17 +116,17 @@ def test_balance( credentials = handler.fetch() auth = authentication.Authentication( - client_id=credentials['client_id'], - client_secret=credentials['client_secret'], - redirect_url='', - access_token=credentials['access_token'], - access_token_expiry=credentials['expiry'], - refresh_token=credentials['refresh_token'], + client_id=str(credentials["client_id"]), + client_secret=str(credentials["client_secret"]), + redirect_url="", + access_token=str(credentials["access_token"]), + access_token_expiry=int(credentials["expiry"]), + refresh_token=str(credentials["refresh_token"]), ) auth.register_callback_handler(handler) - balance = Balance.fetch(auth=auth, account_id='123ABC') + balance = Balance.fetch(auth=auth, account_id="123ABC") assert balance.balance == expected_balance assert balance.total_balance == expected_total_balance @@ -128,32 +134,23 @@ def test_balance( assert balance.spend_today == expected_spend_today @pytest.mark.parametrize( - 'mock_file,expected_external_id,expected_receipt_currency,expected_receipt_merchant,expected_receipt_payments,' - + 'expected_receipt_taxes,expected_receipt_total,expected_transaction_id,', + "mock_file,expected_external_id,expected_receipt_currency,expected_receipt_merchant,expected_receipt_payments," + + "expected_receipt_taxes,expected_receipt_total,expected_transaction_id,", [ - ( - 'Receipt', - '123ABC', - 'GBP', - {}, - [], - [], - 665, - 'tx_123ABC' - ), + ("Receipt", "123ABC", "GBP", {}, [], [], 665, "tx_123ABC"), ], ) def test_create_receipt( - self, - mock_file: str, - expected_external_id: str, - expected_receipt_currency: str, - expected_receipt_merchant: MERCHANT_TYPE, - expected_receipt_payments: List[PAYMENT_TYPE], - expected_receipt_taxes: List[TAX_TYPE], - expected_receipt_total: int, - expected_transaction_id: str, - mocker + self, + mock_file: str, + expected_external_id: str, + expected_receipt_currency: str, + expected_receipt_merchant: MERCHANT_TYPE, + expected_receipt_payments: List[PAYMENT_TYPE], + expected_receipt_taxes: List[TAX_TYPE], + expected_receipt_total: int, + expected_transaction_id: str, + mocker, ): """ Test Receipt endpoint. @@ -171,8 +168,8 @@ def test_create_receipt( """ mocker.patch.object( authentication.HttpIO, - 'get', - return_value=load_data(path='mock_responses', filename=mock_file) + "get", + return_value=load_data(path="mock_responses", filename=mock_file), ) handler = Handler() @@ -180,17 +177,17 @@ def test_create_receipt( credentials = handler.fetch() auth = authentication.Authentication( - client_id=credentials['client_id'], - client_secret=credentials['client_secret'], - redirect_url='', - access_token=credentials['access_token'], - access_token_expiry=credentials['expiry'], - refresh_token=credentials['refresh_token'], + client_id=str(credentials["client_id"]), + client_secret=str(credentials["client_secret"]), + redirect_url="", + access_token=str(credentials["access_token"]), + access_token_expiry=int(credentials["expiry"]), + refresh_token=str(credentials["refresh_token"]), ) auth.register_callback_handler(handler) - receipt = Receipt.fetch(auth=auth, external_id='123ABC') + receipt = Receipt.fetch(auth=auth, external_id="123ABC") assert len(receipt) == 1 assert len(receipt[0].receipt_items) == 1 @@ -204,101 +201,100 @@ def test_create_receipt( assert receipt[0].transaction_id == expected_transaction_id @pytest.mark.parametrize( - 'mock_file,expected_account_id,expected_amount,expected_amount_is_pending,expected_atm_fees_detailed,' - + 'expected_attachments,expected_can_add_to_tab,expected_can_be_excluded_from_breakdown,' - + 'expected_can_be_made_subscription,expected_can_match_transactions_in_categorization,' - + 'expected_can_split_the_bill,expected_categories,expected_category,expected_counterparty,expected_created,' - + 'expected_currency,expected_dedupe_id,expected_decline_reason,expected_description,expected_fees,' - + 'expected_include_in_spending,expected_international,expected_is_load,expected_labels,expected_local_amount,' - + 'expected_local_currency,expected_merchant,expected_metadata,expected_notes,expected_originator,' - + 'expected_scheme,expected_settled,expected_transaction_id,expected_updated,expected_user_id', + "mock_file,expected_account_id,expected_amount,expected_amount_is_pending,expected_atm_fees_detailed," + + "expected_attachments,expected_can_add_to_tab,expected_can_be_excluded_from_breakdown," + + "expected_can_be_made_subscription,expected_can_match_transactions_in_categorization," + + "expected_can_split_the_bill,expected_categories,expected_category,expected_counterparty,expected_created," + + "expected_currency,expected_dedupe_id,expected_decline_reason,expected_description,expected_fees," + + "expected_include_in_spending,expected_international,expected_is_load,expected_labels,expected_local_amount," + + "expected_local_currency,expected_merchant,expected_metadata,expected_notes,expected_originator," + + "expected_scheme,expected_settled,expected_transaction_id,expected_updated,expected_user_id", [ ( - 'Transaction', - 'acc_123ABC', - -2775, - False, - None, - None, - True, - True, - True, - True, - True, - { - 'bills': -2775 - }, - 'bills', - {}, - datetime(2022, 8, 9, 14, 15, 1), - 'GBP', - '123ABC', - '', - 'Company INTERNET GBR', - {}, - True, - None, - False, - None, - -2775, - 'GBP', - 'merch_123ABC', - { - "coin_jar_transaction": "tx_123ABC", - "ledger_insertion_id": "entryset_123ABC", - "mastercard_approval_type": "full", - "mastercard_auth_message_id": "mcauthmsg_123ABC", - "mastercard_card_id": "mccard_123ABC", - "mastercard_lifecycle_id": "mclifecycle_123ABC", "mcc": "1234" - }, - '', - False, - 'mastercard', - datetime(2022, 8, 10, 0, 30, 40), - 'tx_123ABC1', - datetime(2022, 8, 10, 0, 30, 40), - 'user_ABC123', + "Transaction", + "acc_123ABC", + -2775, + False, + None, + None, + True, + True, + True, + True, + True, + {"bills": -2775}, + "bills", + {}, + datetime(2022, 8, 9, 14, 15, 1), + "GBP", + "123ABC", + "", + "Company INTERNET GBR", + {}, + True, + None, + False, + None, + -2775, + "GBP", + "merch_123ABC", + { + "coin_jar_transaction": "tx_123ABC", + "ledger_insertion_id": "entryset_123ABC", + "mastercard_approval_type": "full", + "mastercard_auth_message_id": "mcauthmsg_123ABC", + "mastercard_card_id": "mccard_123ABC", + "mastercard_lifecycle_id": "mclifecycle_123ABC", + "mcc": "1234", + }, + "", + False, + "mastercard", + datetime(2022, 8, 10, 0, 30, 40), + "tx_123ABC1", + datetime(2022, 8, 10, 0, 30, 40), + "user_ABC123", ), ], ) def test_multiple_transaction( - self, - mock_file: str, - expected_account_id: str, - expected_amount: int, - expected_amount_is_pending: bool, - expected_atm_fees_detailed: str, - expected_attachments: str, - expected_can_add_to_tab: bool, - expected_can_be_excluded_from_breakdown: bool, - expected_can_be_made_subscription: bool, - expected_can_match_transactions_in_categorization: bool, - expected_can_split_the_bill: bool, - expected_categories: Dict[str, Union[int, str]], - expected_category: str, - expected_counterparty: str, - expected_created: datetime, - expected_currency: str, - expected_dedupe_id: str, - expected_decline_reason: str, - expected_description: str, - expected_fees: Dict[str, Any], - expected_include_in_spending: bool, - expected_international: Optional[str], - expected_is_load: bool, - expected_labels: str, - expected_local_amount: int, - expected_local_currency: str, - expected_merchant: str, - expected_metadata: Dict[str, str], - expected_notes: str, - expected_originator: bool, - expected_scheme: str, - expected_settled: Optional[datetime], - expected_transaction_id: str, - expected_updated: Optional[datetime], - expected_user_id: bool, - mocker + self, + mock_file: str, + expected_account_id: str, + expected_amount: int, + expected_amount_is_pending: bool, + expected_atm_fees_detailed: str, + expected_attachments: str, + expected_can_add_to_tab: bool, + expected_can_be_excluded_from_breakdown: bool, + expected_can_be_made_subscription: bool, + expected_can_match_transactions_in_categorization: bool, + expected_can_split_the_bill: bool, + expected_categories: Dict[str, Union[int, str]], + expected_category: str, + expected_counterparty: str, + expected_created: datetime, + expected_currency: str, + expected_dedupe_id: str, + expected_decline_reason: str, + expected_description: str, + expected_fees: Dict[str, Any], + expected_include_in_spending: bool, + expected_international: Optional[str], + expected_is_load: bool, + expected_labels: str, + expected_local_amount: int, + expected_local_currency: str, + expected_merchant: str, + expected_metadata: Dict[str, str], + expected_notes: str, + expected_originator: bool, + expected_scheme: str, + expected_settled: Optional[datetime], + expected_transaction_id: str, + expected_updated: Optional[datetime], + expected_user_id: bool, + mocker, ): """ Test Transaction endpoint. @@ -343,8 +339,8 @@ def test_multiple_transaction( """ mocker.patch.object( authentication.HttpIO, - 'get', - return_value=load_data(path='mock_responses', filename=mock_file) + "get", + return_value=load_data(path="mock_responses", filename=mock_file), ) handler = Handler() @@ -352,12 +348,12 @@ def test_multiple_transaction( credentials = handler.fetch() auth = authentication.Authentication( - client_id=credentials['client_id'], - client_secret=credentials['client_secret'], - redirect_url='', - access_token=credentials['access_token'], - access_token_expiry=credentials['expiry'], - refresh_token=credentials['refresh_token'], + client_id=str(credentials["client_id"]), + client_secret=str(credentials["client_secret"]), + redirect_url="", + access_token=str(credentials["access_token"]), + access_token_expiry=int(credentials["expiry"]), + refresh_token=str(credentials["refresh_token"]), ) auth.register_callback_handler(handler) @@ -402,20 +398,26 @@ def test_multiple_transaction( assert transaction.user_id == expected_user_id @pytest.mark.parametrize( - 'mock_file,expected_account_id,expected_url,expected_webhook_id,expected_count', + "mock_file,expected_account_id,expected_url,expected_webhook_id,expected_count", [ - ('WebhooksNone', 'acc_123ABC', None, None, 0), - ('WebhooksOne', 'acc_123ABC', 'https://some-url.co.uk', 'webhook_123ABC', 1), + ("WebhooksNone", "acc_123ABC", None, None, 0), + ( + "WebhooksOne", + "acc_123ABC", + "https://some-url.co.uk", + "webhook_123ABC", + 1, + ), ], ) def test_webhooks( - self, - mock_file: str, - expected_account_id: str, - expected_url: Optional[str], - expected_webhook_id: Optional[str], - expected_count: int, - mocker + self, + mock_file: str, + expected_account_id: str, + expected_url: Optional[str], + expected_webhook_id: Optional[str], + expected_count: int, + mocker, ): """ Test Webhook endpoint. @@ -430,8 +432,8 @@ def test_webhooks( """ mocker.patch.object( authentication.HttpIO, - 'get', - return_value=load_data(path='mock_responses', filename=mock_file) + "get", + return_value=load_data(path="mock_responses", filename=mock_file), ) handler = Handler() @@ -439,12 +441,12 @@ def test_webhooks( credentials = handler.fetch() auth = authentication.Authentication( - client_id=credentials['client_id'], - client_secret=credentials['client_secret'], - redirect_url='', - access_token=credentials['access_token'], - access_token_expiry=credentials['expiry'], - refresh_token=credentials['refresh_token'], + client_id=str(credentials["client_id"]), + client_secret=str(credentials["client_secret"]), + redirect_url="", + access_token=str(credentials["access_token"]), + access_token_expiry=int(credentials["expiry"]), + refresh_token=str(credentials["refresh_token"]), ) auth.register_callback_handler(handler) @@ -459,18 +461,16 @@ def test_webhooks( assert webhook[0].url == expected_url @pytest.mark.parametrize( - 'mock_file, expected_authenticated, expected_client_id, expected_user_id', - [ - ('WhoAmI', True, 'client123', 'user123') - ], + "mock_file, expected_authenticated, expected_client_id, expected_user_id", + [("WhoAmI", True, "client123", "user123")], ) def test_whoami( - self, - mock_file: str, - expected_authenticated: str, - expected_client_id: str, - expected_user_id: str, - mocker + self, + mock_file: str, + expected_authenticated: str, + expected_client_id: str, + expected_user_id: str, + mocker, ): """ Test WhoAmI endpoint. @@ -484,8 +484,8 @@ def test_whoami( """ mocker.patch.object( authentication.HttpIO, - 'get', - return_value=load_data(path='mock_responses', filename=mock_file) + "get", + return_value=load_data(path="mock_responses", filename=mock_file), ) handler = Handler() @@ -493,12 +493,12 @@ def test_whoami( credentials = handler.fetch() auth = authentication.Authentication( - client_id=credentials['client_id'], - client_secret=credentials['client_secret'], - redirect_url='', - access_token=credentials['access_token'], - access_token_expiry=credentials['expiry'], - refresh_token=credentials['refresh_token'], + client_id=str(credentials["client_id"]), + client_secret=str(credentials["client_secret"]), + redirect_url="", + access_token=str(credentials["access_token"]), + access_token_expiry=int(credentials["expiry"]), + refresh_token=str(credentials["refresh_token"]), ) auth.register_callback_handler(handler) diff --git a/tests/test_httpio_errors.py b/tests/test_httpio_errors.py index 3292221..ae2bfd7 100644 --- a/tests/test_httpio_errors.py +++ b/tests/test_httpio_errors.py @@ -5,11 +5,12 @@ from monzo.exceptions import MonzoGeneralError from monzo.httpio import HttpIO +from email.message import Message def test_unknown_status_code_raises_monzogeneralerror(): - http = HttpIO('https://example.com') - error = HTTPError(url='https://example.com/test', code=418, msg='teapot', hdrs=None, fp=None) - with patch('monzo.httpio.urlopen', side_effect=error): + http = HttpIO("https://example.com") + error = HTTPError(url="https://example.com/test", code=418, msg="teapot", hdrs=Message(), fp=None) + with patch("monzo.httpio.urlopen", side_effect=error): with pytest.raises(MonzoGeneralError): - http.get('/test') + http.get("/test") diff --git a/tests/test_payload.py b/tests/test_payload.py index d30fb21..ccb952c 100644 --- a/tests/test_payload.py +++ b/tests/test_payload.py @@ -1,4 +1,5 @@ """Tests for HttpIO.""" + from json import dumps from typing import Dict @@ -18,37 +19,43 @@ class TestHttpIO(object): """Tests for the HttpIO class.""" - @pytest.mark.parametrize("endpoint,http_method,data_filename,response_filename,extra_data", [ - ( + @pytest.mark.parametrize( + "endpoint,http_method,data_filename,response_filename,extra_data", + [ + ( FeedItem, - 'post', - 'FeedItem', - 'FeedItem', + "post", + "FeedItem", + "FeedItem", { - 'account_id': 'acc_123ABC', - 'feed_type': 'basic', - 'params': {'title': 'My Alert', 'image_url': 'https://some-url.co.uk/image.jpg'}, - } - ), - ( + "account_id": "acc_123ABC", + "feed_type": "basic", + "params": { + "title": "My Alert", + "image_url": "https://some-url.co.uk/image.jpg", + }, + }, + ), + ( Webhook, - 'post', - 'WebhooksCreate', - 'WebhooksCreated', + "post", + "WebhooksCreate", + "WebhooksCreated", { - 'account_id': 'acc_123ABC', - "url": 'https://some-url.co.uk', + "account_id": "acc_123ABC", + "url": "https://some-url.co.uk", }, - ) - ]) + ), + ], + ) def test_create_payload( - self, - endpoint, - http_method: str, - data_filename: str, - response_filename: str, - extra_data: Dict[str, str], - mocker, + self, + endpoint, + http_method: str, + data_filename: str, + response_filename: str, + extra_data: Dict[str, str], + mocker, ): """ Test the payload httpio would send. @@ -64,7 +71,7 @@ def test_create_payload( httpio_capture = mocker.patch.object( authentication.HttpIO, http_method, - return_value=load_data(path='mock_responses', filename=response_filename) + return_value=load_data(path="mock_responses", filename=response_filename), ) handler = Handler() @@ -72,44 +79,47 @@ def test_create_payload( credentials = handler.fetch() auth = authentication.Authentication( - client_id=credentials['client_id'], - client_secret=credentials['client_secret'], - redirect_url='', - access_token=credentials['access_token'], - access_token_expiry=credentials['expiry'], - refresh_token=credentials['refresh_token'], + client_id=str(credentials["client_id"]), + client_secret=str(credentials["client_secret"]), + redirect_url="", + access_token=str(credentials["access_token"]), + access_token_expiry=int(credentials["expiry"]), + refresh_token=str(credentials["refresh_token"]), ) auth.register_callback_handler(handler) endpoint.create(auth=auth, **extra_data) - expected_data = load_data(path='mock_payloads', filename=data_filename) + expected_data = load_data(path="mock_payloads", filename=data_filename) httpio_capture.assert_called_with( - data=expected_data['data'], - headers=expected_data['headers'], - path=expected_data['path'], - timeout=expected_data['timeout'] + data=expected_data["data"], + headers=expected_data["headers"], + path=expected_data["path"], + timeout=expected_data["timeout"], ) - @pytest.mark.parametrize("endpoint,http_method,data_filename,response_filename,webhook_id", [ - ( + @pytest.mark.parametrize( + "endpoint,http_method,data_filename,response_filename,webhook_id", + [ + ( Webhook, - 'delete', - 'WebhooksDelete', - 'WebhooksDeleted', - 'webhook_123ABC', - ) - ]) + "delete", + "WebhooksDelete", + "WebhooksDeleted", + "webhook_123ABC", + ) + ], + ) def test_delete_webhook_payload( - self, - endpoint, - http_method: str, - data_filename: str, - response_filename: str, - webhook_id: str, - mocker, + self, + endpoint, + http_method: str, + data_filename: str, + response_filename: str, + webhook_id: str, + mocker, ): """ Test the payload httpio would send. @@ -125,7 +135,7 @@ def test_delete_webhook_payload( httpio_capture = mocker.patch.object( authentication.HttpIO, http_method, - return_value=load_data(path='mock_responses', filename=response_filename) + return_value=load_data(path="mock_responses", filename=response_filename), ) handler = Handler() @@ -133,45 +143,59 @@ def test_delete_webhook_payload( credentials = handler.fetch() auth = authentication.Authentication( - client_id=credentials['client_id'], - client_secret=credentials['client_secret'], - redirect_url='', - access_token=credentials['access_token'], - access_token_expiry=credentials['expiry'], - refresh_token=credentials['refresh_token'], + client_id=str(credentials["client_id"]), + client_secret=str(credentials["client_secret"]), + redirect_url="", + access_token=str(credentials["access_token"]), + access_token_expiry=int(credentials["expiry"]), + refresh_token=str(credentials["refresh_token"]), ) auth.register_callback_handler(handler) - webhook = Webhook(auth=auth, account_id='acc_123ABC', url='https://some-url.co.uk', webhook_id=webhook_id) + webhook = Webhook( + auth=auth, + account_id="acc_123ABC", + url="https://some-url.co.uk", + webhook_id=webhook_id, + ) endpoint.delete(webhook=webhook) - expected_data = load_data(path='mock_payloads', filename=data_filename) + expected_data = load_data(path="mock_payloads", filename=data_filename) httpio_capture.assert_called_with( - data=expected_data['data'], - headers=expected_data['headers'], - path=expected_data['path'], - timeout=expected_data['timeout'] + data=expected_data["data"], + headers=expected_data["headers"], + path=expected_data["path"], + timeout=expected_data["timeout"], ) - @pytest.mark.parametrize("endpoint,http_method,data_filename,response_filename,extra_data", [ - (Account, 'get', 'Accounts', 'Accounts', {}), - (Balance, 'get', 'Balance', 'Balance', {'account_id': 'acc_123ABC'}), - (Transaction, 'get', 'Transaction', 'Transaction', {'account_id': 'acc_123ABC'}), - (Webhook, 'get', 'Webhooks', 'WebhooksNone', {'account_id': 'acc_123ABC'}), - (Webhook, 'get', 'Webhooks', 'WebhooksOne', {'account_id': 'acc_123ABC'}), - (WhoAmI, 'get', 'WhoAmI', 'WhoAmI', {}), - ]) + @pytest.mark.parametrize( + "endpoint,http_method,data_filename,response_filename,extra_data", + [ + (Account, "get", "Accounts", "Accounts", {}), + (Balance, "get", "Balance", "Balance", {"account_id": "acc_123ABC"}), + ( + Transaction, + "get", + "Transaction", + "Transaction", + {"account_id": "acc_123ABC"}, + ), + (Webhook, "get", "Webhooks", "WebhooksNone", {"account_id": "acc_123ABC"}), + (Webhook, "get", "Webhooks", "WebhooksOne", {"account_id": "acc_123ABC"}), + (WhoAmI, "get", "WhoAmI", "WhoAmI", {}), + ], + ) def test_fetch_payload( - self, - endpoint, - http_method: str, - data_filename: str, - response_filename: str, - extra_data: Dict[str, str], - mocker + self, + endpoint, + http_method: str, + data_filename: str, + response_filename: str, + extra_data: Dict[str, str], + mocker, ): """ Test the payload httpio would send. @@ -187,7 +211,7 @@ def test_fetch_payload( httpio_capture = mocker.patch.object( authentication.HttpIO, http_method, - return_value=load_data(path='mock_responses', filename=response_filename) + return_value=load_data(path="mock_responses", filename=response_filename), ) handler = Handler() @@ -195,38 +219,47 @@ def test_fetch_payload( credentials = handler.fetch() auth = authentication.Authentication( - client_id=credentials['client_id'], - client_secret=credentials['client_secret'], - redirect_url='', - access_token=credentials['access_token'], - access_token_expiry=credentials['expiry'], - refresh_token=credentials['refresh_token'], + client_id=str(credentials["client_id"]), + client_secret=str(credentials["client_secret"]), + redirect_url="", + access_token=str(credentials["access_token"]), + access_token_expiry=int(credentials["expiry"]), + refresh_token=str(credentials["refresh_token"]), ) auth.register_callback_handler(handler) endpoint.fetch(auth=auth, **extra_data) - expected_data = load_data(path='mock_payloads', filename=data_filename) + expected_data = load_data(path="mock_payloads", filename=data_filename) httpio_capture.assert_called_with( - data=expected_data['data'], - headers=expected_data['headers'], - path=expected_data['path'], - timeout=expected_data['timeout'] + data=expected_data["data"], + headers=expected_data["headers"], + path=expected_data["path"], + timeout=expected_data["timeout"], ) - @pytest.mark.parametrize("endpoint,http_method,data_filename,response_filename,extra_data", [ - (Transaction, 'patch', 'Annotate', 'Annotate', {'key': 'notes', 'value': 'Test Note'}), - ]) + @pytest.mark.parametrize( + "endpoint,http_method,data_filename,response_filename,extra_data", + [ + ( + Transaction, + "patch", + "Annotate", + "Annotate", + {"key": "notes", "value": "Test Note"}, + ), + ], + ) def test_patch_annotate_payload( - self, - endpoint, - http_method: str, - data_filename: str, - response_filename: str, - extra_data: Dict[str, str], - mocker + self, + endpoint, + http_method: str, + data_filename: str, + response_filename: str, + extra_data: Dict[str, str], + mocker, ): """ Test the payload HttpIO would send for patching annotate. @@ -242,7 +275,7 @@ def test_patch_annotate_payload( httpio_capture = mocker.patch.object( authentication.HttpIO, http_method, - return_value=load_data(path='mock_responses', filename=response_filename) + return_value=load_data(path="mock_responses", filename=response_filename), ) handler = Handler() @@ -250,63 +283,67 @@ def test_patch_annotate_payload( credentials = handler.fetch() auth = authentication.Authentication( - client_id=credentials['client_id'], - client_secret=credentials['client_secret'], - redirect_url='', - access_token=credentials['access_token'], - access_token_expiry=credentials['expiry'], - refresh_token=credentials['refresh_token'], + client_id=str(credentials["client_id"]), + client_secret=str(credentials["client_secret"]), + redirect_url="", + access_token=str(credentials["access_token"]), + access_token_expiry=int(credentials["expiry"]), + refresh_token=str(credentials["refresh_token"]), ) auth.register_callback_handler(handler) transaction = Transaction( auth=auth, - transaction_data=load_data(path='mock_responses', filename='Transaction')['data']['transactions'][0] + transaction_data=load_data(path="mock_responses", filename="Transaction")["data"]["transactions"][0], ) transaction.annotate(**extra_data) - expected_data = load_data(path='mock_payloads', filename=data_filename) + expected_data = load_data(path="mock_payloads", filename=data_filename) httpio_capture.assert_called_with( - data=expected_data['data'], - headers=expected_data['headers'], - path=expected_data['path'], - timeout=expected_data['timeout'] + data=expected_data["data"], + headers=expected_data["headers"], + path=expected_data["path"], + timeout=expected_data["timeout"], ) @pytest.mark.parametrize( - 'endpoint,http_method,data_filename,response_filename,transaction_id,external_id,transaction_total,' - + 'transaction_currency,extra_data', [ + "endpoint,http_method,data_filename,response_filename,transaction_id,external_id,transaction_total," + + "transaction_currency,items", + [ ( - Receipt, - 'put', - 'ReceiptCreate', - 'ReceiptCreated', - 'tx_123ABC', - '123ABC', - 665, - 'GBP', - { - 'items': [ - ReceiptItem(description='testing receipts', amount=665, currency='GBP', quantity=1), - ], - }, + Receipt, + "put", + "ReceiptCreate", + "ReceiptCreated", + "tx_123ABC", + "123ABC", + 665, + "GBP", + [ + ReceiptItem( + description="testing receipts", + amount=665, + currency="GBP", + quantity=1, + ), + ], ), - ] + ], ) def test_put_receipt_payload( - self, - endpoint, - http_method: str, - data_filename: str, - response_filename: str, - transaction_id: str, - external_id: str, - transaction_total: int, - transaction_currency: str, - extra_data: Dict[str, str], - mocker + self, + endpoint, + http_method: str, + data_filename: str, + response_filename: str, + transaction_id: str, + external_id: str, + transaction_total: int, + transaction_currency: str, + items: list[ReceiptItem], + mocker, ): """ Test the payload HttpIO would send for patching annotate. @@ -320,13 +357,13 @@ def test_put_receipt_payload( external_id: External ID for the receipt transaction_total: Total amount of the transaction for the receipt transaction_currency: Currency for the receipt - extra_data: Extra parameters for the fetch request + items: Receipt items mocker: Pytest mocker fixture """ httpio_capture = mocker.patch.object( authentication.HttpIO, http_method, - return_value=load_data(path='mock_responses', filename=response_filename) + return_value=load_data(path="mock_responses", filename=response_filename), ) handler = Handler() @@ -334,12 +371,12 @@ def test_put_receipt_payload( credentials = handler.fetch() auth = authentication.Authentication( - client_id=credentials['client_id'], - client_secret=credentials['client_secret'], - redirect_url='', - access_token=credentials['access_token'], - access_token_expiry=credentials['expiry'], - refresh_token=credentials['refresh_token'], + client_id=str(credentials["client_id"]), + client_secret=str(credentials["client_secret"]), + redirect_url="", + access_token=str(credentials["access_token"]), + access_token_expiry=int(credentials["expiry"]), + refresh_token=str(credentials["refresh_token"]), ) auth.register_callback_handler(handler) @@ -350,15 +387,15 @@ def test_put_receipt_payload( external_id=external_id, transaction_total=transaction_total, transaction_currency=transaction_currency, - **extra_data, + items=items, ) receipt.create(auth=auth, receipt=receipt) - expected_data = load_data(path='mock_payloads', filename=data_filename) + expected_data = load_data(path="mock_payloads", filename=data_filename) httpio_capture.assert_called_with( - data=dumps(expected_data['data']), - headers=expected_data['headers'], - path=expected_data['path'], - timeout=expected_data['timeout'] + data=dumps(expected_data["data"]), + headers=expected_data["headers"], + path=expected_data["path"], + timeout=expected_data["timeout"], ) diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..19a1050 --- /dev/null +++ b/uv.lock @@ -0,0 +1,777 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version < '3.11'", +] + +[[package]] +name = "alabaster" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210, upload-time = "2024-07-26T18:15:03.762Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" }, +] + +[[package]] +name = "babel" +version = "2.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" }, +] + +[[package]] +name = "certifi" +version = "2025.10.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/5b/b6ce21586237c77ce67d01dc5507039d444b630dd76611bbca2d8e5dcd91/certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43", size = 164519, upload-time = "2025-10-05T04:12:15.808Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", size = 163286, upload-time = "2025-10-05T04:12:14.03Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" }, + { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" }, + { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" }, + { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" }, + { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" }, + { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" }, + { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" }, + { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" }, + { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" }, + { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" }, + { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" }, + { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, + { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, + { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, + { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, + { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, + { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, + { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, + { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, + { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, + { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/38/ee22495420457259d2f3390309505ea98f98a5eed40901cf62196abad006/coverage-7.11.0.tar.gz", hash = "sha256:167bd504ac1ca2af7ff3b81d245dfea0292c5032ebef9d66cc08a7d28c1b8050", size = 811905, upload-time = "2025-10-15T15:15:08.542Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/95/c49df0aceb5507a80b9fe5172d3d39bf23f05be40c23c8d77d556df96cec/coverage-7.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eb53f1e8adeeb2e78962bade0c08bfdc461853c7969706ed901821e009b35e31", size = 215800, upload-time = "2025-10-15T15:12:19.824Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c6/7bb46ce01ed634fff1d7bb53a54049f539971862cc388b304ff3c51b4f66/coverage-7.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d9a03ec6cb9f40a5c360f138b88266fd8f58408d71e89f536b4f91d85721d075", size = 216198, upload-time = "2025-10-15T15:12:22.549Z" }, + { url = "https://files.pythonhosted.org/packages/94/b2/75d9d8fbf2900268aca5de29cd0a0fe671b0f69ef88be16767cc3c828b85/coverage-7.11.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0d7f0616c557cbc3d1c2090334eddcbb70e1ae3a40b07222d62b3aa47f608fab", size = 242953, upload-time = "2025-10-15T15:12:24.139Z" }, + { url = "https://files.pythonhosted.org/packages/65/ac/acaa984c18f440170525a8743eb4b6c960ace2dbad80dc22056a437fc3c6/coverage-7.11.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e44a86a47bbdf83b0a3ea4d7df5410d6b1a0de984fbd805fa5101f3624b9abe0", size = 244766, upload-time = "2025-10-15T15:12:25.974Z" }, + { url = "https://files.pythonhosted.org/packages/d8/0d/938d0bff76dfa4a6b228c3fc4b3e1c0e2ad4aa6200c141fcda2bd1170227/coverage-7.11.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:596763d2f9a0ee7eec6e643e29660def2eef297e1de0d334c78c08706f1cb785", size = 246625, upload-time = "2025-10-15T15:12:27.387Z" }, + { url = "https://files.pythonhosted.org/packages/38/54/8f5f5e84bfa268df98f46b2cb396b1009734cfb1e5d6adb663d284893b32/coverage-7.11.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ef55537ff511b5e0a43edb4c50a7bf7ba1c3eea20b4f49b1490f1e8e0e42c591", size = 243568, upload-time = "2025-10-15T15:12:28.799Z" }, + { url = "https://files.pythonhosted.org/packages/68/30/8ba337c2877fe3f2e1af0ed7ff4be0c0c4aca44d6f4007040f3ca2255e99/coverage-7.11.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9cbabd8f4d0d3dc571d77ae5bdbfa6afe5061e679a9d74b6797c48d143307088", size = 244665, upload-time = "2025-10-15T15:12:30.297Z" }, + { url = "https://files.pythonhosted.org/packages/cc/fb/c6f1d6d9a665536b7dde2333346f0cc41dc6a60bd1ffc10cd5c33e7eb000/coverage-7.11.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e24045453384e0ae2a587d562df2a04d852672eb63051d16096d3f08aa4c7c2f", size = 242681, upload-time = "2025-10-15T15:12:32.326Z" }, + { url = "https://files.pythonhosted.org/packages/be/38/1b532319af5f991fa153c20373291dc65c2bf532af7dbcffdeef745c8f79/coverage-7.11.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:7161edd3426c8d19bdccde7d49e6f27f748f3c31cc350c5de7c633fea445d866", size = 242912, upload-time = "2025-10-15T15:12:34.079Z" }, + { url = "https://files.pythonhosted.org/packages/67/3d/f39331c60ef6050d2a861dc1b514fa78f85f792820b68e8c04196ad733d6/coverage-7.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d4ed4de17e692ba6415b0587bc7f12bc80915031fc9db46a23ce70fc88c9841", size = 243559, upload-time = "2025-10-15T15:12:35.809Z" }, + { url = "https://files.pythonhosted.org/packages/4b/55/cb7c9df9d0495036ce582a8a2958d50c23cd73f84a23284bc23bd4711a6f/coverage-7.11.0-cp310-cp310-win32.whl", hash = "sha256:765c0bc8fe46f48e341ef737c91c715bd2a53a12792592296a095f0c237e09cf", size = 218266, upload-time = "2025-10-15T15:12:37.429Z" }, + { url = "https://files.pythonhosted.org/packages/68/a8/b79cb275fa7bd0208767f89d57a1b5f6ba830813875738599741b97c2e04/coverage-7.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:24d6f3128f1b2d20d84b24f4074475457faedc3d4613a7e66b5e769939c7d969", size = 219169, upload-time = "2025-10-15T15:12:39.25Z" }, + { url = "https://files.pythonhosted.org/packages/49/3a/ee1074c15c408ddddddb1db7dd904f6b81bc524e01f5a1c5920e13dbde23/coverage-7.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d58ecaa865c5b9fa56e35efc51d1014d4c0d22838815b9fce57a27dd9576847", size = 215912, upload-time = "2025-10-15T15:12:40.665Z" }, + { url = "https://files.pythonhosted.org/packages/70/c4/9f44bebe5cb15f31608597b037d78799cc5f450044465bcd1ae8cb222fe1/coverage-7.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b679e171f1c104a5668550ada700e3c4937110dbdd153b7ef9055c4f1a1ee3cc", size = 216310, upload-time = "2025-10-15T15:12:42.461Z" }, + { url = "https://files.pythonhosted.org/packages/42/01/5e06077cfef92d8af926bdd86b84fb28bf9bc6ad27343d68be9b501d89f2/coverage-7.11.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ca61691ba8c5b6797deb221a0d09d7470364733ea9c69425a640f1f01b7c5bf0", size = 246706, upload-time = "2025-10-15T15:12:44.001Z" }, + { url = "https://files.pythonhosted.org/packages/40/b8/7a3f1f33b35cc4a6c37e759137533119560d06c0cc14753d1a803be0cd4a/coverage-7.11.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:aef1747ede4bd8ca9cfc04cc3011516500c6891f1b33a94add3253f6f876b7b7", size = 248634, upload-time = "2025-10-15T15:12:45.768Z" }, + { url = "https://files.pythonhosted.org/packages/7a/41/7f987eb33de386bc4c665ab0bf98d15fcf203369d6aacae74f5dd8ec489a/coverage-7.11.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1839d08406e4cba2953dcc0ffb312252f14d7c4c96919f70167611f4dee2623", size = 250741, upload-time = "2025-10-15T15:12:47.222Z" }, + { url = "https://files.pythonhosted.org/packages/23/c1/a4e0ca6a4e83069fb8216b49b30a7352061ca0cb38654bd2dc96b7b3b7da/coverage-7.11.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e0eb0a2dcc62478eb5b4cbb80b97bdee852d7e280b90e81f11b407d0b81c4287", size = 246837, upload-time = "2025-10-15T15:12:48.904Z" }, + { url = "https://files.pythonhosted.org/packages/5d/03/ced062a17f7c38b4728ff76c3acb40d8465634b20b4833cdb3cc3a74e115/coverage-7.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bc1fbea96343b53f65d5351d8fd3b34fd415a2670d7c300b06d3e14a5af4f552", size = 248429, upload-time = "2025-10-15T15:12:50.73Z" }, + { url = "https://files.pythonhosted.org/packages/97/af/a7c6f194bb8c5a2705ae019036b8fe7f49ea818d638eedb15fdb7bed227c/coverage-7.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:214b622259dd0cf435f10241f1333d32caa64dbc27f8790ab693428a141723de", size = 246490, upload-time = "2025-10-15T15:12:52.646Z" }, + { url = "https://files.pythonhosted.org/packages/ab/c3/aab4df02b04a8fde79068c3c41ad7a622b0ef2b12e1ed154da986a727c3f/coverage-7.11.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:258d9967520cca899695d4eb7ea38be03f06951d6ca2f21fb48b1235f791e601", size = 246208, upload-time = "2025-10-15T15:12:54.586Z" }, + { url = "https://files.pythonhosted.org/packages/30/d8/e282ec19cd658238d60ed404f99ef2e45eed52e81b866ab1518c0d4163cf/coverage-7.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cf9e6ff4ca908ca15c157c409d608da77a56a09877b97c889b98fb2c32b6465e", size = 247126, upload-time = "2025-10-15T15:12:56.485Z" }, + { url = "https://files.pythonhosted.org/packages/d1/17/a635fa07fac23adb1a5451ec756216768c2767efaed2e4331710342a3399/coverage-7.11.0-cp311-cp311-win32.whl", hash = "sha256:fcc15fc462707b0680cff6242c48625da7f9a16a28a41bb8fd7a4280920e676c", size = 218314, upload-time = "2025-10-15T15:12:58.365Z" }, + { url = "https://files.pythonhosted.org/packages/2a/29/2ac1dfcdd4ab9a70026edc8d715ece9b4be9a1653075c658ee6f271f394d/coverage-7.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:865965bf955d92790f1facd64fe7ff73551bd2c1e7e6b26443934e9701ba30b9", size = 219203, upload-time = "2025-10-15T15:12:59.902Z" }, + { url = "https://files.pythonhosted.org/packages/03/21/5ce8b3a0133179115af4c041abf2ee652395837cb896614beb8ce8ddcfd9/coverage-7.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:5693e57a065760dcbeb292d60cc4d0231a6d4b6b6f6a3191561e1d5e8820b745", size = 217879, upload-time = "2025-10-15T15:13:01.35Z" }, + { url = "https://files.pythonhosted.org/packages/c4/db/86f6906a7c7edc1a52b2c6682d6dd9be775d73c0dfe2b84f8923dfea5784/coverage-7.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9c49e77811cf9d024b95faf86c3f059b11c0c9be0b0d61bc598f453703bd6fd1", size = 216098, upload-time = "2025-10-15T15:13:02.916Z" }, + { url = "https://files.pythonhosted.org/packages/21/54/e7b26157048c7ba555596aad8569ff903d6cd67867d41b75287323678ede/coverage-7.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a61e37a403a778e2cda2a6a39abcc895f1d984071942a41074b5c7ee31642007", size = 216331, upload-time = "2025-10-15T15:13:04.403Z" }, + { url = "https://files.pythonhosted.org/packages/b9/19/1ce6bf444f858b83a733171306134a0544eaddf1ca8851ede6540a55b2ad/coverage-7.11.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c79cae102bb3b1801e2ef1511fb50e91ec83a1ce466b2c7c25010d884336de46", size = 247825, upload-time = "2025-10-15T15:13:05.92Z" }, + { url = "https://files.pythonhosted.org/packages/71/0b/d3bcbbc259fcced5fb67c5d78f6e7ee965f49760c14afd931e9e663a83b2/coverage-7.11.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:16ce17ceb5d211f320b62df002fa7016b7442ea0fd260c11cec8ce7730954893", size = 250573, upload-time = "2025-10-15T15:13:07.471Z" }, + { url = "https://files.pythonhosted.org/packages/58/8d/b0ff3641a320abb047258d36ed1c21d16be33beed4152628331a1baf3365/coverage-7.11.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:80027673e9d0bd6aef86134b0771845e2da85755cf686e7c7c59566cf5a89115", size = 251706, upload-time = "2025-10-15T15:13:09.4Z" }, + { url = "https://files.pythonhosted.org/packages/59/c8/5a586fe8c7b0458053d9c687f5cff515a74b66c85931f7fe17a1c958b4ac/coverage-7.11.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d3ffa07a08657306cd2215b0da53761c4d73cb54d9143b9303a6481ec0cd415", size = 248221, upload-time = "2025-10-15T15:13:10.964Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ff/3a25e3132804ba44cfa9a778cdf2b73dbbe63ef4b0945e39602fc896ba52/coverage-7.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a3b6a5f8b2524fd6c1066bc85bfd97e78709bb5e37b5b94911a6506b65f47186", size = 249624, upload-time = "2025-10-15T15:13:12.5Z" }, + { url = "https://files.pythonhosted.org/packages/c5/12/ff10c8ce3895e1b17a73485ea79ebc1896a9e466a9d0f4aef63e0d17b718/coverage-7.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:fcc0a4aa589de34bc56e1a80a740ee0f8c47611bdfb28cd1849de60660f3799d", size = 247744, upload-time = "2025-10-15T15:13:14.554Z" }, + { url = "https://files.pythonhosted.org/packages/16/02/d500b91f5471b2975947e0629b8980e5e90786fe316b6d7299852c1d793d/coverage-7.11.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dba82204769d78c3fd31b35c3d5f46e06511936c5019c39f98320e05b08f794d", size = 247325, upload-time = "2025-10-15T15:13:16.438Z" }, + { url = "https://files.pythonhosted.org/packages/77/11/dee0284fbbd9cd64cfce806b827452c6df3f100d9e66188e82dfe771d4af/coverage-7.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:81b335f03ba67309a95210caf3eb43bd6fe75a4e22ba653ef97b4696c56c7ec2", size = 249180, upload-time = "2025-10-15T15:13:17.959Z" }, + { url = "https://files.pythonhosted.org/packages/59/1b/cdf1def928f0a150a057cab03286774e73e29c2395f0d30ce3d9e9f8e697/coverage-7.11.0-cp312-cp312-win32.whl", hash = "sha256:037b2d064c2f8cc8716fe4d39cb705779af3fbf1ba318dc96a1af858888c7bb5", size = 218479, upload-time = "2025-10-15T15:13:19.608Z" }, + { url = "https://files.pythonhosted.org/packages/ff/55/e5884d55e031da9c15b94b90a23beccc9d6beee65e9835cd6da0a79e4f3a/coverage-7.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:d66c0104aec3b75e5fd897e7940188ea1892ca1d0235316bf89286d6a22568c0", size = 219290, upload-time = "2025-10-15T15:13:21.593Z" }, + { url = "https://files.pythonhosted.org/packages/23/a8/faa930cfc71c1d16bc78f9a19bb73700464f9c331d9e547bfbc1dbd3a108/coverage-7.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:d91ebeac603812a09cf6a886ba6e464f3bbb367411904ae3790dfe28311b15ad", size = 217924, upload-time = "2025-10-15T15:13:23.39Z" }, + { url = "https://files.pythonhosted.org/packages/60/7f/85e4dfe65e400645464b25c036a26ac226cf3a69d4a50c3934c532491cdd/coverage-7.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cc3f49e65ea6e0d5d9bd60368684fe52a704d46f9e7fc413918f18d046ec40e1", size = 216129, upload-time = "2025-10-15T15:13:25.371Z" }, + { url = "https://files.pythonhosted.org/packages/96/5d/dc5fa98fea3c175caf9d360649cb1aa3715e391ab00dc78c4c66fabd7356/coverage-7.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f39ae2f63f37472c17b4990f794035c9890418b1b8cca75c01193f3c8d3e01be", size = 216380, upload-time = "2025-10-15T15:13:26.976Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f5/3da9cc9596708273385189289c0e4d8197d37a386bdf17619013554b3447/coverage-7.11.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7db53b5cdd2917b6eaadd0b1251cf4e7d96f4a8d24e174bdbdf2f65b5ea7994d", size = 247375, upload-time = "2025-10-15T15:13:28.923Z" }, + { url = "https://files.pythonhosted.org/packages/65/6c/f7f59c342359a235559d2bc76b0c73cfc4bac7d61bb0df210965cb1ecffd/coverage-7.11.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10ad04ac3a122048688387828b4537bc9cf60c0bf4869c1e9989c46e45690b82", size = 249978, upload-time = "2025-10-15T15:13:30.525Z" }, + { url = "https://files.pythonhosted.org/packages/e7/8c/042dede2e23525e863bf1ccd2b92689692a148d8b5fd37c37899ba882645/coverage-7.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4036cc9c7983a2b1f2556d574d2eb2154ac6ed55114761685657e38782b23f52", size = 251253, upload-time = "2025-10-15T15:13:32.174Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a9/3c58df67bfa809a7bddd786356d9c5283e45d693edb5f3f55d0986dd905a/coverage-7.11.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7ab934dd13b1c5e94b692b1e01bd87e4488cb746e3a50f798cb9464fd128374b", size = 247591, upload-time = "2025-10-15T15:13:34.147Z" }, + { url = "https://files.pythonhosted.org/packages/26/5b/c7f32efd862ee0477a18c41e4761305de6ddd2d49cdeda0c1116227570fd/coverage-7.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59a6e5a265f7cfc05f76e3bb53eca2e0dfe90f05e07e849930fecd6abb8f40b4", size = 249411, upload-time = "2025-10-15T15:13:38.425Z" }, + { url = "https://files.pythonhosted.org/packages/76/b5/78cb4f1e86c1611431c990423ec0768122905b03837e1b4c6a6f388a858b/coverage-7.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:df01d6c4c81e15a7c88337b795bb7595a8596e92310266b5072c7e301168efbd", size = 247303, upload-time = "2025-10-15T15:13:40.464Z" }, + { url = "https://files.pythonhosted.org/packages/87/c9/23c753a8641a330f45f221286e707c427e46d0ffd1719b080cedc984ec40/coverage-7.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8c934bd088eed6174210942761e38ee81d28c46de0132ebb1801dbe36a390dcc", size = 247157, upload-time = "2025-10-15T15:13:42.087Z" }, + { url = "https://files.pythonhosted.org/packages/c5/42/6e0cc71dc8a464486e944a4fa0d85bdec031cc2969e98ed41532a98336b9/coverage-7.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5a03eaf7ec24078ad64a07f02e30060aaf22b91dedf31a6b24d0d98d2bba7f48", size = 248921, upload-time = "2025-10-15T15:13:43.715Z" }, + { url = "https://files.pythonhosted.org/packages/e8/1c/743c2ef665e6858cccb0f84377dfe3a4c25add51e8c7ef19249be92465b6/coverage-7.11.0-cp313-cp313-win32.whl", hash = "sha256:695340f698a5f56f795b2836abe6fb576e7c53d48cd155ad2f80fd24bc63a040", size = 218526, upload-time = "2025-10-15T15:13:45.336Z" }, + { url = "https://files.pythonhosted.org/packages/ff/d5/226daadfd1bf8ddbccefbd3aa3547d7b960fb48e1bdac124e2dd13a2b71a/coverage-7.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:2727d47fce3ee2bac648528e41455d1b0c46395a087a229deac75e9f88ba5a05", size = 219317, upload-time = "2025-10-15T15:13:47.401Z" }, + { url = "https://files.pythonhosted.org/packages/97/54/47db81dcbe571a48a298f206183ba8a7ba79200a37cd0d9f4788fcd2af4a/coverage-7.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:0efa742f431529699712b92ecdf22de8ff198df41e43aeaaadf69973eb93f17a", size = 217948, upload-time = "2025-10-15T15:13:49.096Z" }, + { url = "https://files.pythonhosted.org/packages/e5/8b/cb68425420154e7e2a82fd779a8cc01549b6fa83c2ad3679cd6c088ebd07/coverage-7.11.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:587c38849b853b157706407e9ebdca8fd12f45869edb56defbef2daa5fb0812b", size = 216837, upload-time = "2025-10-15T15:13:51.09Z" }, + { url = "https://files.pythonhosted.org/packages/33/55/9d61b5765a025685e14659c8d07037247de6383c0385757544ffe4606475/coverage-7.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b971bdefdd75096163dd4261c74be813c4508477e39ff7b92191dea19f24cd37", size = 217061, upload-time = "2025-10-15T15:13:52.747Z" }, + { url = "https://files.pythonhosted.org/packages/52/85/292459c9186d70dcec6538f06ea251bc968046922497377bf4a1dc9a71de/coverage-7.11.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:269bfe913b7d5be12ab13a95f3a76da23cf147be7fa043933320ba5625f0a8de", size = 258398, upload-time = "2025-10-15T15:13:54.45Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e2/46edd73fb8bf51446c41148d81944c54ed224854812b6ca549be25113ee0/coverage-7.11.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:dadbcce51a10c07b7c72b0ce4a25e4b6dcb0c0372846afb8e5b6307a121eb99f", size = 260574, upload-time = "2025-10-15T15:13:56.145Z" }, + { url = "https://files.pythonhosted.org/packages/07/5e/1df469a19007ff82e2ca8fe509822820a31e251f80ee7344c34f6cd2ec43/coverage-7.11.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ed43fa22c6436f7957df036331f8fe4efa7af132054e1844918866cd228af6c", size = 262797, upload-time = "2025-10-15T15:13:58.635Z" }, + { url = "https://files.pythonhosted.org/packages/f9/50/de216b31a1434b94d9b34a964c09943c6be45069ec704bfc379d8d89a649/coverage-7.11.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9516add7256b6713ec08359b7b05aeff8850c98d357784c7205b2e60aa2513fa", size = 257361, upload-time = "2025-10-15T15:14:00.409Z" }, + { url = "https://files.pythonhosted.org/packages/82/1e/3f9f8344a48111e152e0fd495b6fff13cc743e771a6050abf1627a7ba918/coverage-7.11.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb92e47c92fcbcdc692f428da67db33337fa213756f7adb6a011f7b5a7a20740", size = 260349, upload-time = "2025-10-15T15:14:02.188Z" }, + { url = "https://files.pythonhosted.org/packages/65/9b/3f52741f9e7d82124272f3070bbe316006a7de1bad1093f88d59bfc6c548/coverage-7.11.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d06f4fc7acf3cabd6d74941d53329e06bab00a8fe10e4df2714f0b134bfc64ef", size = 258114, upload-time = "2025-10-15T15:14:03.907Z" }, + { url = "https://files.pythonhosted.org/packages/0b/8b/918f0e15f0365d50d3986bbd3338ca01178717ac5678301f3f547b6619e6/coverage-7.11.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:6fbcee1a8f056af07ecd344482f711f563a9eb1c2cad192e87df00338ec3cdb0", size = 256723, upload-time = "2025-10-15T15:14:06.324Z" }, + { url = "https://files.pythonhosted.org/packages/44/9e/7776829f82d3cf630878a7965a7d70cc6ca94f22c7d20ec4944f7148cb46/coverage-7.11.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dbbf012be5f32533a490709ad597ad8a8ff80c582a95adc8d62af664e532f9ca", size = 259238, upload-time = "2025-10-15T15:14:08.002Z" }, + { url = "https://files.pythonhosted.org/packages/9a/b8/49cf253e1e7a3bedb85199b201862dd7ca4859f75b6cf25ffa7298aa0760/coverage-7.11.0-cp313-cp313t-win32.whl", hash = "sha256:cee6291bb4fed184f1c2b663606a115c743df98a537c969c3c64b49989da96c2", size = 219180, upload-time = "2025-10-15T15:14:09.786Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e1/1a541703826be7ae2125a0fb7f821af5729d56bb71e946e7b933cc7a89a4/coverage-7.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:a386c1061bf98e7ea4758e4313c0ab5ecf57af341ef0f43a0bf26c2477b5c268", size = 220241, upload-time = "2025-10-15T15:14:11.471Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d1/5ee0e0a08621140fd418ec4020f595b4d52d7eb429ae6a0c6542b4ba6f14/coverage-7.11.0-cp313-cp313t-win_arm64.whl", hash = "sha256:f9ea02ef40bb83823b2b04964459d281688fe173e20643870bb5d2edf68bc836", size = 218510, upload-time = "2025-10-15T15:14:13.46Z" }, + { url = "https://files.pythonhosted.org/packages/f4/06/e923830c1985ce808e40a3fa3eb46c13350b3224b7da59757d37b6ce12b8/coverage-7.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c770885b28fb399aaf2a65bbd1c12bf6f307ffd112d6a76c5231a94276f0c497", size = 216110, upload-time = "2025-10-15T15:14:15.157Z" }, + { url = "https://files.pythonhosted.org/packages/42/82/cdeed03bfead45203fb651ed756dfb5266028f5f939e7f06efac4041dad5/coverage-7.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a3d0e2087dba64c86a6b254f43e12d264b636a39e88c5cc0a01a7c71bcfdab7e", size = 216395, upload-time = "2025-10-15T15:14:16.863Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ba/e1c80caffc3199aa699813f73ff097bc2df7b31642bdbc7493600a8f1de5/coverage-7.11.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:73feb83bb41c32811973b8565f3705caf01d928d972b72042b44e97c71fd70d1", size = 247433, upload-time = "2025-10-15T15:14:18.589Z" }, + { url = "https://files.pythonhosted.org/packages/80/c0/5b259b029694ce0a5bbc1548834c7ba3db41d3efd3474489d7efce4ceb18/coverage-7.11.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c6f31f281012235ad08f9a560976cc2fc9c95c17604ff3ab20120fe480169bca", size = 249970, upload-time = "2025-10-15T15:14:20.307Z" }, + { url = "https://files.pythonhosted.org/packages/8c/86/171b2b5e1aac7e2fd9b43f7158b987dbeb95f06d1fbecad54ad8163ae3e8/coverage-7.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9570ad567f880ef675673992222746a124b9595506826b210fbe0ce3f0499cd", size = 251324, upload-time = "2025-10-15T15:14:22.419Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/7e10414d343385b92024af3932a27a1caf75c6e27ee88ba211221ff1a145/coverage-7.11.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8badf70446042553a773547a61fecaa734b55dc738cacf20c56ab04b77425e43", size = 247445, upload-time = "2025-10-15T15:14:24.205Z" }, + { url = "https://files.pythonhosted.org/packages/c4/3b/e4f966b21f5be8c4bf86ad75ae94efa0de4c99c7bbb8114476323102e345/coverage-7.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a09c1211959903a479e389685b7feb8a17f59ec5a4ef9afde7650bd5eabc2777", size = 249324, upload-time = "2025-10-15T15:14:26.234Z" }, + { url = "https://files.pythonhosted.org/packages/00/a2/8479325576dfcd909244d0df215f077f47437ab852ab778cfa2f8bf4d954/coverage-7.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:5ef83b107f50db3f9ae40f69e34b3bd9337456c5a7fe3461c7abf8b75dd666a2", size = 247261, upload-time = "2025-10-15T15:14:28.42Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d8/3a9e2db19d94d65771d0f2e21a9ea587d11b831332a73622f901157cc24b/coverage-7.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f91f927a3215b8907e214af77200250bb6aae36eca3f760f89780d13e495388d", size = 247092, upload-time = "2025-10-15T15:14:30.784Z" }, + { url = "https://files.pythonhosted.org/packages/b3/b1/bbca3c472544f9e2ad2d5116b2379732957048be4b93a9c543fcd0207e5f/coverage-7.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cdbcd376716d6b7fbfeedd687a6c4be019c5a5671b35f804ba76a4c0a778cba4", size = 248755, upload-time = "2025-10-15T15:14:32.585Z" }, + { url = "https://files.pythonhosted.org/packages/89/49/638d5a45a6a0f00af53d6b637c87007eb2297042186334e9923a61aa8854/coverage-7.11.0-cp314-cp314-win32.whl", hash = "sha256:bab7ec4bb501743edc63609320aaec8cd9188b396354f482f4de4d40a9d10721", size = 218793, upload-time = "2025-10-15T15:14:34.972Z" }, + { url = "https://files.pythonhosted.org/packages/30/cc/b675a51f2d068adb3cdf3799212c662239b0ca27f4691d1fff81b92ea850/coverage-7.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:3d4ba9a449e9364a936a27322b20d32d8b166553bfe63059bd21527e681e2fad", size = 219587, upload-time = "2025-10-15T15:14:37.047Z" }, + { url = "https://files.pythonhosted.org/packages/93/98/5ac886876026de04f00820e5094fe22166b98dcb8b426bf6827aaf67048c/coverage-7.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:ce37f215223af94ef0f75ac68ea096f9f8e8c8ec7d6e8c346ee45c0d363f0479", size = 218168, upload-time = "2025-10-15T15:14:38.861Z" }, + { url = "https://files.pythonhosted.org/packages/14/d1/b4145d35b3e3ecf4d917e97fc8895bcf027d854879ba401d9ff0f533f997/coverage-7.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:f413ce6e07e0d0dc9c433228727b619871532674b45165abafe201f200cc215f", size = 216850, upload-time = "2025-10-15T15:14:40.651Z" }, + { url = "https://files.pythonhosted.org/packages/ca/d1/7f645fc2eccd318369a8a9948acc447bb7c1ade2911e31d3c5620544c22b/coverage-7.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:05791e528a18f7072bf5998ba772fe29db4da1234c45c2087866b5ba4dea710e", size = 217071, upload-time = "2025-10-15T15:14:42.755Z" }, + { url = "https://files.pythonhosted.org/packages/54/7d/64d124649db2737ceced1dfcbdcb79898d5868d311730f622f8ecae84250/coverage-7.11.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cacb29f420cfeb9283b803263c3b9a068924474ff19ca126ba9103e1278dfa44", size = 258570, upload-time = "2025-10-15T15:14:44.542Z" }, + { url = "https://files.pythonhosted.org/packages/6c/3f/6f5922f80dc6f2d8b2c6f974835c43f53eb4257a7797727e6ca5b7b2ec1f/coverage-7.11.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314c24e700d7027ae3ab0d95fbf8d53544fca1f20345fd30cd219b737c6e58d3", size = 260738, upload-time = "2025-10-15T15:14:46.436Z" }, + { url = "https://files.pythonhosted.org/packages/0e/5f/9e883523c4647c860b3812b417a2017e361eca5b635ee658387dc11b13c1/coverage-7.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:630d0bd7a293ad2fc8b4b94e5758c8b2536fdf36c05f1681270203e463cbfa9b", size = 262994, upload-time = "2025-10-15T15:14:48.3Z" }, + { url = "https://files.pythonhosted.org/packages/07/bb/43b5a8e94c09c8bf51743ffc65c4c841a4ca5d3ed191d0a6919c379a1b83/coverage-7.11.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e89641f5175d65e2dbb44db15fe4ea48fade5d5bbb9868fdc2b4fce22f4a469d", size = 257282, upload-time = "2025-10-15T15:14:50.236Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e5/0ead8af411411330b928733e1d201384b39251a5f043c1612970310e8283/coverage-7.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c9f08ea03114a637dab06cedb2e914da9dc67fa52c6015c018ff43fdde25b9c2", size = 260430, upload-time = "2025-10-15T15:14:52.413Z" }, + { url = "https://files.pythonhosted.org/packages/ae/66/03dd8bb0ba5b971620dcaac145461950f6d8204953e535d2b20c6b65d729/coverage-7.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce9f3bde4e9b031eaf1eb61df95c1401427029ea1bfddb8621c1161dcb0fa02e", size = 258190, upload-time = "2025-10-15T15:14:54.268Z" }, + { url = "https://files.pythonhosted.org/packages/45/ae/28a9cce40bf3174426cb2f7e71ee172d98e7f6446dff936a7ccecee34b14/coverage-7.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:e4dc07e95495923d6fd4d6c27bf70769425b71c89053083843fd78f378558996", size = 256658, upload-time = "2025-10-15T15:14:56.436Z" }, + { url = "https://files.pythonhosted.org/packages/5c/7c/3a44234a8599513684bfc8684878fd7b126c2760f79712bb78c56f19efc4/coverage-7.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:424538266794db2861db4922b05d729ade0940ee69dcf0591ce8f69784db0e11", size = 259342, upload-time = "2025-10-15T15:14:58.538Z" }, + { url = "https://files.pythonhosted.org/packages/e1/e6/0108519cba871af0351725ebdb8660fd7a0fe2ba3850d56d32490c7d9b4b/coverage-7.11.0-cp314-cp314t-win32.whl", hash = "sha256:4c1eeb3fb8eb9e0190bebafd0462936f75717687117339f708f395fe455acc73", size = 219568, upload-time = "2025-10-15T15:15:00.382Z" }, + { url = "https://files.pythonhosted.org/packages/c9/76/44ba876e0942b4e62fdde23ccb029ddb16d19ba1bef081edd00857ba0b16/coverage-7.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b56efee146c98dbf2cf5cffc61b9829d1e94442df4d7398b26892a53992d3547", size = 220687, upload-time = "2025-10-15T15:15:02.322Z" }, + { url = "https://files.pythonhosted.org/packages/b9/0c/0df55ecb20d0d0ed5c322e10a441775e1a3a5d78c60f0c4e1abfe6fcf949/coverage-7.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b5c2705afa83f49bd91962a4094b6b082f94aef7626365ab3f8f4bd159c5acf3", size = 218711, upload-time = "2025-10-15T15:15:04.575Z" }, + { url = "https://files.pythonhosted.org/packages/5f/04/642c1d8a448ae5ea1369eac8495740a79eb4e581a9fb0cbdce56bbf56da1/coverage-7.11.0-py3-none-any.whl", hash = "sha256:4b7589765348d78fb4e5fb6ea35d07564e387da2fc5efff62e0222971f155f68", size = 207761, upload-time = "2025-10-15T15:15:06.439Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "docutils" +version = "0.21.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444, upload-time = "2024-04-23T18:57:18.24Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408, upload-time = "2024-04-23T18:57:14.835Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "imagesize" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/84/62473fb57d61e31fef6e36d64a179c8781605429fd927b5dd608c997be31/imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a", size = 1280026, upload-time = "2022-07-01T12:21:05.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769, upload-time = "2022-07-01T12:21:02.467Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "monzo-api" +version = "1.2.2" +source = { virtual = "." } + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-mock" }, + { name = "ruff" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinx-rtd-theme" }, + { name = "ty" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +dev = [ + { name = "pytest", specifier = ">=8.4.2" }, + { name = "pytest-cov", specifier = ">=7.0.0" }, + { name = "pytest-mock", specifier = ">=3.15.1" }, + { name = "ruff", specifier = ">=0.14.3" }, + { name = "sphinx", specifier = ">=8.1.3" }, + { name = "sphinx-rtd-theme", specifier = ">=3.0.2" }, + { name = "ty", specifier = ">=0.0.1a25" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +] + +[[package]] +name = "pytest-mock" +version = "3.15.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "roman-numerals-py" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/76/48fd56d17c5bdbdf65609abbc67288728a98ed4c02919428d4f52d23b24b/roman_numerals_py-3.1.0.tar.gz", hash = "sha256:be4bf804f083a4ce001b5eb7e3c0862479d10f94c936f6c4e5f250aa5ff5bd2d", size = 9017, upload-time = "2025-02-22T07:34:54.333Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/97/d2cbbaa10c9b826af0e10fdf836e1bf344d9f0abb873ebc34d1f49642d3f/roman_numerals_py-3.1.0-py3-none-any.whl", hash = "sha256:9da2ad2fb670bcf24e81070ceb3be72f6c11c440d73bd579fbeca1e9f330954c", size = 7742, upload-time = "2025-02-22T07:34:52.422Z" }, +] + +[[package]] +name = "ruff" +version = "0.14.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/75/62/50b7727004dfe361104dfbf898c45a9a2fdfad8c72c04ae62900224d6ecf/ruff-0.14.3.tar.gz", hash = "sha256:4ff876d2ab2b161b6de0aa1f5bd714e8e9b4033dc122ee006925fbacc4f62153", size = 5558687, upload-time = "2025-10-31T00:26:26.878Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/8e/0c10ff1ea5d4360ab8bfca4cb2c9d979101a391f3e79d2616c9bf348cd26/ruff-0.14.3-py3-none-linux_armv6l.whl", hash = "sha256:876b21e6c824f519446715c1342b8e60f97f93264012de9d8d10314f8a79c371", size = 12535613, upload-time = "2025-10-31T00:25:44.302Z" }, + { url = "https://files.pythonhosted.org/packages/d3/c8/6724f4634c1daf52409fbf13fefda64aa9c8f81e44727a378b7b73dc590b/ruff-0.14.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b6fd8c79b457bedd2abf2702b9b472147cd860ed7855c73a5247fa55c9117654", size = 12855812, upload-time = "2025-10-31T00:25:47.793Z" }, + { url = "https://files.pythonhosted.org/packages/de/03/db1bce591d55fd5f8a08bb02517fa0b5097b2ccabd4ea1ee29aa72b67d96/ruff-0.14.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:71ff6edca490c308f083156938c0c1a66907151263c4abdcb588602c6e696a14", size = 11944026, upload-time = "2025-10-31T00:25:49.657Z" }, + { url = "https://files.pythonhosted.org/packages/0b/75/4f8dbd48e03272715d12c87dc4fcaaf21b913f0affa5f12a4e9c6f8a0582/ruff-0.14.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:786ee3ce6139772ff9272aaf43296d975c0217ee1b97538a98171bf0d21f87ed", size = 12356818, upload-time = "2025-10-31T00:25:51.949Z" }, + { url = "https://files.pythonhosted.org/packages/ec/9b/506ec5b140c11d44a9a4f284ea7c14ebf6f8b01e6e8917734a3325bff787/ruff-0.14.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cd6291d0061811c52b8e392f946889916757610d45d004e41140d81fb6cd5ddc", size = 12336745, upload-time = "2025-10-31T00:25:54.248Z" }, + { url = "https://files.pythonhosted.org/packages/c7/e1/c560d254048c147f35e7f8131d30bc1f63a008ac61595cf3078a3e93533d/ruff-0.14.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a497ec0c3d2c88561b6d90f9c29f5ae68221ac00d471f306fa21fa4264ce5fcd", size = 13101684, upload-time = "2025-10-31T00:25:56.253Z" }, + { url = "https://files.pythonhosted.org/packages/a5/32/e310133f8af5cd11f8cc30f52522a3ebccc5ea5bff4b492f94faceaca7a8/ruff-0.14.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e231e1be58fc568950a04fbe6887c8e4b85310e7889727e2b81db205c45059eb", size = 14535000, upload-time = "2025-10-31T00:25:58.397Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a1/7b0470a22158c6d8501eabc5e9b6043c99bede40fa1994cadf6b5c2a61c7/ruff-0.14.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:469e35872a09c0e45fecf48dd960bfbce056b5db2d5e6b50eca329b4f853ae20", size = 14156450, upload-time = "2025-10-31T00:26:00.889Z" }, + { url = "https://files.pythonhosted.org/packages/0a/96/24bfd9d1a7f532b560dcee1a87096332e461354d3882124219bcaff65c09/ruff-0.14.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d6bc90307c469cb9d28b7cfad90aaa600b10d67c6e22026869f585e1e8a2db0", size = 13568414, upload-time = "2025-10-31T00:26:03.291Z" }, + { url = "https://files.pythonhosted.org/packages/a7/e7/138b883f0dfe4ad5b76b58bf4ae675f4d2176ac2b24bdd81b4d966b28c61/ruff-0.14.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2f8a0bbcffcfd895df39c9a4ecd59bb80dca03dc43f7fb63e647ed176b741e", size = 13315293, upload-time = "2025-10-31T00:26:05.708Z" }, + { url = "https://files.pythonhosted.org/packages/33/f4/c09bb898be97b2eb18476b7c950df8815ef14cf956074177e9fbd40b7719/ruff-0.14.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:678fdd7c7d2d94851597c23ee6336d25f9930b460b55f8598e011b57c74fd8c5", size = 13539444, upload-time = "2025-10-31T00:26:08.09Z" }, + { url = "https://files.pythonhosted.org/packages/9c/aa/b30a1db25fc6128b1dd6ff0741fa4abf969ded161599d07ca7edd0739cc0/ruff-0.14.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1ec1ac071e7e37e0221d2f2dbaf90897a988c531a8592a6a5959f0603a1ecf5e", size = 12252581, upload-time = "2025-10-31T00:26:10.297Z" }, + { url = "https://files.pythonhosted.org/packages/da/13/21096308f384d796ffe3f2960b17054110a9c3828d223ca540c2b7cc670b/ruff-0.14.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:afcdc4b5335ef440d19e7df9e8ae2ad9f749352190e96d481dc501b753f0733e", size = 12307503, upload-time = "2025-10-31T00:26:12.646Z" }, + { url = "https://files.pythonhosted.org/packages/cb/cc/a350bac23f03b7dbcde3c81b154706e80c6f16b06ff1ce28ed07dc7b07b0/ruff-0.14.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:7bfc42f81862749a7136267a343990f865e71fe2f99cf8d2958f684d23ce3dfa", size = 12675457, upload-time = "2025-10-31T00:26:15.044Z" }, + { url = "https://files.pythonhosted.org/packages/cb/76/46346029fa2f2078826bc88ef7167e8c198e58fe3126636e52f77488cbba/ruff-0.14.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a65e448cfd7e9c59fae8cf37f9221585d3354febaad9a07f29158af1528e165f", size = 13403980, upload-time = "2025-10-31T00:26:17.81Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a4/35f1ef68c4e7b236d4a5204e3669efdeefaef21f0ff6a456792b3d8be438/ruff-0.14.3-py3-none-win32.whl", hash = "sha256:f3d91857d023ba93e14ed2d462ab62c3428f9bbf2b4fbac50a03ca66d31991f7", size = 12500045, upload-time = "2025-10-31T00:26:20.503Z" }, + { url = "https://files.pythonhosted.org/packages/03/15/51960ae340823c9859fb60c63301d977308735403e2134e17d1d2858c7fb/ruff-0.14.3-py3-none-win_amd64.whl", hash = "sha256:d7b7006ac0756306db212fd37116cce2bd307e1e109375e1c6c106002df0ae5f", size = 13594005, upload-time = "2025-10-31T00:26:22.533Z" }, + { url = "https://files.pythonhosted.org/packages/b7/73/4de6579bac8e979fca0a77e54dec1f1e011a0d268165eb8a9bc0982a6564/ruff-0.14.3-py3-none-win_arm64.whl", hash = "sha256:26eb477ede6d399d898791d01961e16b86f02bc2486d0d1a7a9bb2379d055dc1", size = 12590017, upload-time = "2025-10-31T00:26:24.52Z" }, +] + +[[package]] +name = "snowballstemmer" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/75/a7/9810d872919697c9d01295633f5d574fb416d47e535f258272ca1f01f447/snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895", size = 105575, upload-time = "2025-05-09T16:34:51.843Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274, upload-time = "2025-05-09T16:34:50.371Z" }, +] + +[[package]] +name = "sphinx" +version = "8.1.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "alabaster", marker = "python_full_version < '3.11'" }, + { name = "babel", marker = "python_full_version < '3.11'" }, + { name = "colorama", marker = "python_full_version < '3.11' and sys_platform == 'win32'" }, + { name = "docutils", marker = "python_full_version < '3.11'" }, + { name = "imagesize", marker = "python_full_version < '3.11'" }, + { name = "jinja2", marker = "python_full_version < '3.11'" }, + { name = "packaging", marker = "python_full_version < '3.11'" }, + { name = "pygments", marker = "python_full_version < '3.11'" }, + { name = "requests", marker = "python_full_version < '3.11'" }, + { name = "snowballstemmer", marker = "python_full_version < '3.11'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version < '3.11'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version < '3.11'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version < '3.11'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version < '3.11'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version < '3.11'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version < '3.11'" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/be0b61178fe2cdcb67e2a92fc9ebb488e3c51c4f74a36a7824c0adf23425/sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927", size = 8184611, upload-time = "2024-10-13T20:27:13.93Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/60/1ddff83a56d33aaf6f10ec8ce84b4c007d9368b21008876fceda7e7381ef/sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2", size = 3487125, upload-time = "2024-10-13T20:27:10.448Z" }, +] + +[[package]] +name = "sphinx" +version = "8.2.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", +] +dependencies = [ + { name = "alabaster", marker = "python_full_version >= '3.11'" }, + { name = "babel", marker = "python_full_version >= '3.11'" }, + { name = "colorama", marker = "python_full_version >= '3.11' and sys_platform == 'win32'" }, + { name = "docutils", marker = "python_full_version >= '3.11'" }, + { name = "imagesize", marker = "python_full_version >= '3.11'" }, + { name = "jinja2", marker = "python_full_version >= '3.11'" }, + { name = "packaging", marker = "python_full_version >= '3.11'" }, + { name = "pygments", marker = "python_full_version >= '3.11'" }, + { name = "requests", marker = "python_full_version >= '3.11'" }, + { name = "roman-numerals-py", marker = "python_full_version >= '3.11'" }, + { name = "snowballstemmer", marker = "python_full_version >= '3.11'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.11'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.11'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version >= '3.11'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.11'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.11'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/ad/4360e50ed56cb483667b8e6dadf2d3fda62359593faabbe749a27c4eaca6/sphinx-8.2.3.tar.gz", hash = "sha256:398ad29dee7f63a75888314e9424d40f52ce5a6a87ae88e7071e80af296ec348", size = 8321876, upload-time = "2025-03-02T22:31:59.658Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/53/136e9eca6e0b9dc0e1962e2c908fbea2e5ac000c2a2fbd9a35797958c48b/sphinx-8.2.3-py3-none-any.whl", hash = "sha256:4405915165f13521d875a8c29c8970800a0141c14cc5416a38feca4ea5d9b9c3", size = 3589741, upload-time = "2025-03-02T22:31:56.836Z" }, +] + +[[package]] +name = "sphinx-rtd-theme" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinxcontrib-jquery" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/44/c97faec644d29a5ceddd3020ae2edffa69e7d00054a8c7a6021e82f20335/sphinx_rtd_theme-3.0.2.tar.gz", hash = "sha256:b7457bc25dda723b20b086a670b9953c859eab60a2a03ee8eb2bb23e176e5f85", size = 7620463, upload-time = "2024-11-13T11:06:04.545Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/77/46e3bac77b82b4df5bb5b61f2de98637724f246b4966cfc34bc5895d852a/sphinx_rtd_theme-3.0.2-py2.py3-none-any.whl", hash = "sha256:422ccc750c3a3a311de4ae327e82affdaf59eb695ba4936538552f3b00f4ee13", size = 7655561, upload-time = "2024-11-13T11:06:02.094Z" }, +] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053, upload-time = "2024-07-29T01:09:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" }, +] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967, upload-time = "2024-07-29T01:09:23.417Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" }, +] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617, upload-time = "2024-07-29T01:09:37.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" }, +] + +[[package]] +name = "sphinxcontrib-jquery" +version = "4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/f3/aa67467e051df70a6330fe7770894b3e4f09436dea6881ae0b4f3d87cad8/sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a", size = 122331, upload-time = "2023-03-14T15:01:01.944Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/85/749bd22d1a68db7291c89e2ebca53f4306c3f205853cf31e9de279034c3c/sphinxcontrib_jquery-4.1-py2.py3-none-any.whl", hash = "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae", size = 121104, upload-time = "2023-03-14T15:01:00.356Z" }, +] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787, upload-time = "2019-01-21T16:10:16.347Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" }, +] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165, upload-time = "2024-07-29T01:09:56.435Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" }, +] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080, upload-time = "2024-07-29T01:10:09.332Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, +] + +[[package]] +name = "tomli" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" }, + { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" }, + { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" }, + { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" }, + { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" }, + { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" }, + { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" }, + { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" }, + { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" }, + { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" }, + { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" }, + { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" }, + { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" }, + { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586, upload-time = "2025-10-08T22:01:21.164Z" }, + { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792, upload-time = "2025-10-08T22:01:22.417Z" }, + { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909, upload-time = "2025-10-08T22:01:23.859Z" }, + { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" }, + { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244, upload-time = "2025-10-08T22:01:27.06Z" }, + { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637, upload-time = "2025-10-08T22:01:28.059Z" }, + { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925, upload-time = "2025-10-08T22:01:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045, upload-time = "2025-10-08T22:01:31.98Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835, upload-time = "2025-10-08T22:01:32.989Z" }, + { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109, upload-time = "2025-10-08T22:01:34.052Z" }, + { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930, upload-time = "2025-10-08T22:01:35.082Z" }, + { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964, upload-time = "2025-10-08T22:01:36.057Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065, upload-time = "2025-10-08T22:01:37.27Z" }, + { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088, upload-time = "2025-10-08T22:01:38.235Z" }, + { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193, upload-time = "2025-10-08T22:01:39.712Z" }, + { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488, upload-time = "2025-10-08T22:01:40.773Z" }, + { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669, upload-time = "2025-10-08T22:01:41.824Z" }, + { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709, upload-time = "2025-10-08T22:01:43.177Z" }, + { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563, upload-time = "2025-10-08T22:01:44.233Z" }, + { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756, upload-time = "2025-10-08T22:01:45.234Z" }, + { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, +] + +[[package]] +name = "ty" +version = "0.0.1a25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/6b/e73bc3c1039ea72936158a08313155a49e5aa5e7db5205a149fe516a4660/ty-0.0.1a25.tar.gz", hash = "sha256:5550b24b9dd0e0f8b4b2c1f0fcc608a55d0421dd67b6c364bc7bf25762334511", size = 4403670, upload-time = "2025-10-29T19:40:23.647Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/3b/4457231238a2eeb04cba4ba7cc33d735be68ee46ca40a98ae30e187de864/ty-0.0.1a25-py3-none-linux_armv6l.whl", hash = "sha256:d35b2c1f94a014a22875d2745aa0432761d2a9a8eb7212630d5caf547daeef6d", size = 8878803, upload-time = "2025-10-29T19:39:42.243Z" }, + { url = "https://files.pythonhosted.org/packages/8a/fa/a328713dd310018fc7a381693d8588185baa2fdae913e01a6839187215df/ty-0.0.1a25-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:192edac94675a468bac7f6e04687a77a64698e4e1fe01f6a048bf9b6dde5b703", size = 8695667, upload-time = "2025-10-29T19:39:45.179Z" }, + { url = "https://files.pythonhosted.org/packages/22/e8/5707939118992ced2bf5385adc3ede7723c1b717b07ad14c495eea1e47b4/ty-0.0.1a25-py3-none-macosx_11_0_arm64.whl", hash = "sha256:949523621f336e01bc7d687b7bd08fe838edadbdb6563c2c057ed1d264e820cf", size = 8159012, upload-time = "2025-10-29T19:39:47.011Z" }, + { url = "https://files.pythonhosted.org/packages/eb/fb/ff313aa71602225cd78f1bce3017713d6d1b1c1e0fa8101ead4594a60d95/ty-0.0.1a25-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94f78f621458c05e59e890061021198197f29a7b51a33eda82bbb036e7ed73d7", size = 8433675, upload-time = "2025-10-29T19:39:48.443Z" }, + { url = "https://files.pythonhosted.org/packages/c0/8d/cc7e7fb57215a15b575a43ed042bdd92971871e0decec1b26d2e7d969465/ty-0.0.1a25-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d9656fca8062a2c6709c30d76d662c96d2e7dbfee8f70e55ec6b6afd67b5d447", size = 8668456, upload-time = "2025-10-29T19:39:50.412Z" }, + { url = "https://files.pythonhosted.org/packages/b8/6d/d7bf5909ed2dcdcbc1e2ca7eea80929893e2d188d9c36b3fcb2b36532ff6/ty-0.0.1a25-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9f3bbf523b49935bbd76e230408d858dce0d614f44f5807bbbd0954f64e0f01", size = 9023543, upload-time = "2025-10-29T19:39:52.292Z" }, + { url = "https://files.pythonhosted.org/packages/b4/b8/72bcefb4be32e5a84f0b21de2552f16cdb4cae3eb271ac891c8199c26b1a/ty-0.0.1a25-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f13ea9815f4a54a0a303ca7bf411b0650e3c2a24fc6c7889ffba2c94f5e97a6a", size = 9700013, upload-time = "2025-10-29T19:39:57.283Z" }, + { url = "https://files.pythonhosted.org/packages/90/0d/cf7e794b840cf6b0bbecb022e593c543f85abad27a582241cf2095048cb1/ty-0.0.1a25-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eab6e33ebe202a71a50c3d5a5580e3bc1a85cda3ffcdc48cec3f1c693b7a873b", size = 9372574, upload-time = "2025-10-29T19:40:04.532Z" }, + { url = "https://files.pythonhosted.org/packages/1e/71/2d35e7d51b48eabd330e2f7b7e0bce541cbd95950c4d2f780e85f3366af1/ty-0.0.1a25-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6b9a31da43424cdab483703a54a561b93aabba84630788505329fc5294a9c62", size = 9535726, upload-time = "2025-10-29T19:40:06.548Z" }, + { url = "https://files.pythonhosted.org/packages/57/d3/01ecc23bbd8f3e0dfbcf9172d06d84e88155c5f416f1491137e8066fd859/ty-0.0.1a25-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a90d897a7c1a5ae9b41a4c7b0a42262a06361476ad88d783dbedd7913edadbc", size = 9003380, upload-time = "2025-10-29T19:40:08.683Z" }, + { url = "https://files.pythonhosted.org/packages/de/f9/cde9380d8a1a6ca61baeb9aecb12cbec90d489aa929be55cd78ad5c2ccd9/ty-0.0.1a25-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:93c7e7ab2859af0f866d34d27f4ae70dd4fb95b847387f082de1197f9f34e068", size = 8401833, upload-time = "2025-10-29T19:40:10.627Z" }, + { url = "https://files.pythonhosted.org/packages/0b/39/0acf3625b0c495011795a391016b572f97a812aca1d67f7a76621fdb9ebf/ty-0.0.1a25-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4a247061bd32bae3865a236d7f8b6c9916c80995db30ae1600999010f90623a9", size = 8706761, upload-time = "2025-10-29T19:40:12.575Z" }, + { url = "https://files.pythonhosted.org/packages/25/73/7de1648f3563dd9d416d36ab5f1649bfd7b47a179135027f31d44b89a246/ty-0.0.1a25-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1711dd587eccf04fd50c494dc39babe38f4cb345bc3901bf1d8149cac570e979", size = 8792426, upload-time = "2025-10-29T19:40:14.553Z" }, + { url = "https://files.pythonhosted.org/packages/7d/8a/b6e761a65eac7acd10b2e452f49b2d8ae0ea163ca36bb6b18b2dadae251b/ty-0.0.1a25-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5f4c9b0cf7995e2e3de9bab4d066063dea92019f2f62673b7574e3612643dd35", size = 9103991, upload-time = "2025-10-29T19:40:16.332Z" }, + { url = "https://files.pythonhosted.org/packages/e4/25/9324ae947fcc4322470326cf8276a3fc2f08dc82adec1de79d963fdf7af5/ty-0.0.1a25-py3-none-win32.whl", hash = "sha256:168fc8aee396d617451acc44cd28baffa47359777342836060c27aa6f37e2445", size = 8387095, upload-time = "2025-10-29T19:40:18.368Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2b/cb12cbc7db1ba310aa7b1de9b4e018576f653105993736c086ee67d2ec02/ty-0.0.1a25-py3-none-win_amd64.whl", hash = "sha256:a2fad3d8e92bb4d57a8872a6f56b1aef54539d36f23ebb01abe88ac4338efafb", size = 9059225, upload-time = "2025-10-29T19:40:20.278Z" }, + { url = "https://files.pythonhosted.org/packages/2f/c1/f6be8cdd0bf387c1d8ee9d14bb299b7b5d2c0532f550a6693216a32ec0c5/ty-0.0.1a25-py3-none-win_arm64.whl", hash = "sha256:dde2962d448ed87c48736e9a4bb13715a4cced705525e732b1c0dac1d4c66e3d", size = 8536832, upload-time = "2025-10-29T19:40:22.014Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, +]