From e38d6b0b1efe27ad2d78fa62f8e9bd505cee3fea Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Fri, 12 Dec 2025 12:21:52 +1300 Subject: [PATCH] Replace supplementary record/manager classes with mixins Records in Odoo can have some additional fields that are shared across many different record models using model inheritance. To support adding these kinds of shared fields (and related methods) to record/manager classes in the OpenStack Odoo Client library in a more modular way, add support for the use of **mixins** to take advantage of Python's multiple inheritance to add such fields and methods to custom record/manager classes. **Protocol** classes were created for `RecordBase` (called `RecordProtocol`) and `RecordManagerBase` (called `RecordManagerProtocol`) which contain common attribute/field type hints and method stubs. The mixin classes subclass these protocol classes to provide type hints within mixin classes as if they subclass the record/manager base classes (they can't do this directly for complicated reasons). The implementations for the record and record manager base classes have been refactored to reduce duplication as part of this work. The `NamedRecordManagerBase` and `CodedRecordManagerBase` classes have been reimplemented using mixins to not only utilise this paradigm inside the library itself, but also demonstrate its usage in a simple and practical way for anyone looking to write their own mixins. The `get_by_unique_field` method provided by `RecordManagerWithUniqueFieldBase` has been incorporated into `RecordManagerBase` to make it available for use in any custom manager class. Finally, a basic unit testing pipeline using `pytest` has been added. Initially a single test to make sure the package can be imported correctly has been added, but the plan is to expand coverage over time. --- .github/workflows/main.yml | 2 +- .github/workflows/tag.yml | 6 +- .github/workflows/test.yml | 30 +- .gitignore | 1 + .pre-commit-config.yaml | 8 +- changelog.d/13.changed.md | 1 + docs/managers/custom.md | 226 ++++++- docs/managers/index.md | 2 - docs/managers/partner-category.md | 2 + docs/managers/product-category.md | 4 +- docs/managers/voucher-code.md | 2 +- openstack_odooclient/__init__.py | 25 +- openstack_odooclient/base/client.py | 4 +- openstack_odooclient/base/record/__init__.py | 0 .../base/{record.py => record/base.py} | 242 ++++--- openstack_odooclient/base/record/types.py | 104 +++ .../base/record_manager/__init__.py | 0 .../base.py} | 449 ++++++------ .../base/record_manager/protocol.py | 638 ++++++++++++++++++ .../base/record_manager/types.py | 53 ++ .../base/record_manager_with_unique_field.py | 259 ------- openstack_odooclient/managers/account_move.py | 19 +- .../managers/account_move_line.py | 5 +- openstack_odooclient/managers/company.py | 19 +- openstack_odooclient/managers/credit.py | 5 +- .../managers/credit_transaction.py | 5 +- openstack_odooclient/managers/credit_type.py | 19 +- openstack_odooclient/managers/currency.py | 18 +- .../managers/customer_group.py | 19 +- openstack_odooclient/managers/grant.py | 5 +- openstack_odooclient/managers/grant_type.py | 19 +- openstack_odooclient/managers/partner.py | 5 +- .../managers/partner_category.py | 12 +- openstack_odooclient/managers/pricelist.py | 19 +- openstack_odooclient/managers/product.py | 11 +- .../managers/product_category.py | 12 +- openstack_odooclient/managers/project.py | 11 +- .../managers/project_contact.py | 5 +- .../managers/referral_code.py | 19 +- openstack_odooclient/managers/reseller.py | 5 +- .../managers/reseller_tier.py | 19 +- openstack_odooclient/managers/sale_order.py | 19 +- .../managers/sale_order_line.py | 5 +- .../managers/support_subscription.py | 5 +- .../managers/support_subscription_type.py | 17 +- openstack_odooclient/managers/tax.py | 13 +- openstack_odooclient/managers/tax_group.py | 18 +- .../managers/term_discount.py | 5 +- openstack_odooclient/managers/trial.py | 5 +- openstack_odooclient/managers/uom.py | 5 +- openstack_odooclient/managers/uom_category.py | 4 +- openstack_odooclient/managers/user.py | 5 +- .../managers/volume_discount_range.py | 5 +- openstack_odooclient/managers/voucher_code.py | 21 +- openstack_odooclient/mixins/__init__.py | 0 .../coded_record.py} | 53 +- .../named_record.py} | 53 +- pyproject.toml | 33 +- tests/__init__.py | 0 tests/unit/__init__.py | 0 tests/unit/test_import.py | 33 + uv.lock | 591 +++++++++++----- 62 files changed, 2172 insertions(+), 1027 deletions(-) create mode 100644 changelog.d/13.changed.md create mode 100644 openstack_odooclient/base/record/__init__.py rename openstack_odooclient/base/{record.py => record/base.py} (82%) create mode 100644 openstack_odooclient/base/record/types.py create mode 100644 openstack_odooclient/base/record_manager/__init__.py rename openstack_odooclient/base/{record_manager.py => record_manager/base.py} (72%) create mode 100644 openstack_odooclient/base/record_manager/protocol.py create mode 100644 openstack_odooclient/base/record_manager/types.py delete mode 100644 openstack_odooclient/base/record_manager_with_unique_field.py create mode 100644 openstack_odooclient/mixins/__init__.py rename openstack_odooclient/{base/record_manager_coded.py => mixins/coded_record.py} (79%) rename openstack_odooclient/{base/record_manager_named.py => mixins/named_record.py} (80%) create mode 100644 tests/__init__.py create mode 100644 tests/unit/__init__.py create mode 100644 tests/unit/test_import.py diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e572448..e812c82 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -29,7 +29,7 @@ jobs: - name: Setup uv uses: astral-sh/setup-uv@v7 with: - version: "0.9.2" + version: "0.9.17" - name: Create virtual environment run: uv sync --only-dev - name: Publish the docs to GitHub Pages diff --git a/.github/workflows/tag.yml b/.github/workflows/tag.yml index f7417df..20535ce 100644 --- a/.github/workflows/tag.yml +++ b/.github/workflows/tag.yml @@ -23,7 +23,7 @@ jobs: - name: Setup uv uses: astral-sh/setup-uv@v7 with: - version: "0.9.2" + version: "0.9.17" - name: Build source dist and wheels run: uv build - name: Upload source dist and wheels to artifacts @@ -58,7 +58,7 @@ jobs: - name: Setup uv uses: astral-sh/setup-uv@v7 with: - version: "0.9.2" + version: "0.9.17" - name: Publish source dist and wheels to PyPI run: uv publish @@ -109,7 +109,7 @@ jobs: - name: Setup uv uses: astral-sh/setup-uv@v7 with: - version: "0.9.2" + version: "0.9.17" - name: Create virtual environment run: uv sync --only-dev - name: Publish the docs to GitHub Pages diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7b9151a..cb78565 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -30,6 +30,34 @@ jobs: - name: Run pre-commit hooks uses: pre-commit/action@v3.0.1 + test: + needs: pre-commit + runs-on: ubuntu-24.04 + strategy: + matrix: + python_version: + - "3.10" + - "3.11" + - "3.12" + - "3.13" + - "3.14" + steps: + - name: Clone full tree, and checkout branch + uses: actions/checkout@v5 + with: + fetch-depth: 0 + - name: Setup Python + uses: actions/setup-python@v6 + with: + python-version: "${{ matrix.python_version }}" + cache: "pip" + - name: Setup uv + uses: astral-sh/setup-uv@v7 + with: + version: "0.9.17" + - name: Run tests + run: uv run poe test + build: needs: pre-commit runs-on: ubuntu-24.04 @@ -46,7 +74,7 @@ jobs: - name: Setup uv uses: astral-sh/setup-uv@v7 with: - version: "0.9.2" + version: "0.9.17" - name: Build source dist and wheels run: uv build - name: Upload source dist and wheels to artifacts diff --git a/.gitignore b/.gitignore index 01b9b8c..d6c42b1 100644 --- a/.gitignore +++ b/.gitignore @@ -50,6 +50,7 @@ coverage.xml .hypothesis/ .pytest_cache/ cover/ +rspec.xml # Translations *.mo diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 780e794..b9e837a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,20 +15,20 @@ repos: - id: check-added-large-files - id: check-merge-conflict - repo: https://github.com/crate-ci/typos - rev: "v1.38.1" + rev: "v1.40.0" hooks: - id: typos - repo: https://github.com/astral-sh/uv-pre-commit - rev: "0.9.2" + rev: "0.9.17" hooks: - id: uv-lock - repo: https://github.com/astral-sh/ruff-pre-commit - rev: "v0.14.0" + rev: "v0.14.9" hooks: - id: ruff-check - id: ruff-format - repo: https://github.com/pre-commit/mirrors-mypy - rev: "v1.18.2" + rev: "v1.19.0" hooks: - id: mypy additional_dependencies: diff --git a/changelog.d/13.changed.md b/changelog.d/13.changed.md new file mode 100644 index 0000000..c600769 --- /dev/null +++ b/changelog.d/13.changed.md @@ -0,0 +1 @@ +Replace supplementary record/manager classes with mixins diff --git a/docs/managers/custom.md b/docs/managers/custom.md index 546e7d1..0c5fd6a 100644 --- a/docs/managers/custom.md +++ b/docs/managers/custom.md @@ -138,7 +138,7 @@ from datetime import datetime from openstack_odooclient import RecordBase class CustomRecord(RecordBase["CustomRecordManager"]): - custom_field: date + custom_field: datetime """Description of the field.""" ``` @@ -791,6 +791,230 @@ The following internal attributes are also available for use in methods: * `_odoo` (`odoorpc.ODOO`) - The OdooRPC connection object * `_env` (`odoorpc.env.Environment`) - The OdooRPC environment object for the model +## Mixins + +Python supports [multiple inheritance](https://docs.python.org/3/tutorial/classes.html#multiple-inheritance) +when creating new classes. A common use case for multiple inheritance is to extend +functionality of a class through the use of *mixin classes*, which are minimal +classes that only consist of supplementary attributes and methods, that get added +to other classes through subclassing. + +The OpenStack Odoo Client library for Python supports the use of mixin classes +to add functionality to custom record and manager classes in a modular way. +Multiple mixins can be added to record and manager classes to allow mixing and +matching additional functionality as required. + +### Using Mixins + +To extend the functionality of your custom record and manager classes, +append the mixins for the record class and/or record manager class +**AFTER** the inheritance for `RecordBase` and `RecordManagerBase`. +You also need to specify the **same** type arguments to the mixins as +is already being done for `RecordBase` and `RecordManagerBase`. + +```python +from __future__ import annotations + +from openstack_odooclient import ( + NamedRecordManagerMixin, + NamedRecordMixin, + RecordBase, + RecordManagerBase, +) + +class CustomRecord( + RecordBase["CustomRecordManager"], + NamedRecordMixin["CustomRecordManager"], +): + custom_field: str + """Description of the field.""" + +class CustomRecordManager( + RecordManagerBase[CustomRecord], + NamedRecordManagerMixin[CustomRecord], +): + env_name = "custom.record" + record_class = CustomRecord +``` + +That's all that needs to be done. The additional attributes and/or methods +should now be available on your record and manager objects. + +The following mixins are provided with the Odoo Client library. + +#### Named Records + +If your record model has a unique `name` field on it (of `str` type), +you can use the `NamedRecordMixin` and `NamedRecordManagerMixin` mixins +to define the `name` field on the record class, and add the +`get_by_name` method to your custom record manager class. + +```python +from __future__ import annotations + +from openstack_odooclient import ( + NamedRecordManagerMixin, + NamedRecordMixin, + RecordBase, + RecordManagerBase, +) + +class CustomRecord( + RecordBase["CustomRecordManager"], + NamedRecordMixin["CustomRecordManager"], +): + custom_field: str + """Description of the field.""" + + # Added by NamedRecordMixin: + # + # name: str + # """The unique name of the record.""" + +class CustomRecordManager( + RecordManagerBase[CustomRecord], + NamedRecordManagerMixin[CustomRecord], +): + env_name = "custom.record" + record_class = CustomRecord + + # Added by NamedRecordManagerMixin: + # + # def get_by_name(...): + # ... +``` + +For more information on using record managers with unique `name` fields, +see [Named Record Managers](index.md#named-record-managers). + +#### Coded Records + +If your record model has a unique `code` field on it (of `str` type), +you can use the `CodedRecordMixin` and `CodedRecordManagerMixin` mixins +to define the `code` field on the record class, and add the +`get_by_code` method to your custom record manager class. + +```python +from __future__ import annotations + +from openstack_odooclient import ( + CodedRecordManagerMixin, + CodedRecordMixin, + RecordBase, + RecordManagerBase, +) + +class CustomRecord( + RecordBase["CustomRecordManager"], + CodedRecordMixin["CustomRecordManager"], +): + custom_field: str + """Description of the field.""" + + # Added by CodedRecordMixin: + # + # code: str + # """The unique name for this record.""" + +class CustomRecordManager( + RecordManagerBase[CustomRecord], + CodedRecordManagerMixin[CustomRecord], +): + env_name = "custom.record" + record_class = CustomRecord + + # Added by CodedRecordManagerMixin: + # + # def get_by_code(...): + # ... +``` + +For more information on using record managers with unique `code` fields, +see [Coded Record Managers](index.md#coded-record-managers). + +### Creating Mixins + +It is possible to create your own custom mixins to incorporate into +custom record and manager classes. + +There are two mixin types: **record mixins** and **record manager mixins**. + +#### Record Mixins + +Record mixins are used to add custom fields and methods to record classes. + +Here is the full implementation of `NamedRecordMixin` as an example +of a mixin for a record class, that simply adds the `name` field: + +```python +from __future__ import annotations + +from typing import Generic + +from openstack_odooclient import RM, RecordProtocol + +class NamedRecordMixin(RecordProtocol[RM], Generic[RM]): + name: str + """The unique name of the record.""" +``` + +A record mixin consists of a class that subclasses `RecordProtocol[RM]` +(where `RM` is the type variable for a record manager class) to get the type +hints for a record class' common fields and methods. `Generic[RM]` is also +subclassed to make the mixin itself a generic class, to allow `RM` to be +passed when creating a record class with the mixin. + +Once you have the class, simply define any fields and methods you'd like +to add. + +You can then use the mixin as shown in [Using Mixins](#using-mixins). + +When defining custom methods, in addition to accessing fields/methods +defined within the mixin, fields/methods from the `RecordBase` class +are also available: + +```python +from __future__ import annotations + +from typing import Generic + +from openstack_odooclient import RM, RecordProtocol + +class NamedRecordMixin(RecordProtocol[RM], Generic[RM]): + name: str + """The unique name of the record.""" + + def custom_method(self) -> None: + self.name # str + self._env.custom_method(self.id) +``` + +#### Record Manager Mixins + +Record manager mixins are expected to be mainly used to add custom methods +to a record manager class. + +```python +from __future__ import annotations + +from typing import Generic + +from openstack_odooclient import R, RecordManagerProtocol + +class NamedRecordManagerMixin(RecordManagerProtocol[R], Generic[R]): + def custom_method(self, record: int | R) -> None: + self._env.custom_method( # self._env available from RecordManagerBase + record if isinstance(record, int) else record.id, + ) +``` + +A record manager mixin consists of a class that subclasses +`RecordManagerProtocol[R]` (where `R` is the type variable for a record class) +to get the type hints for a record manager class' common attributes and +methods. `Generic[R]` is also subclassed to make the mixin itself a generic +class, to allow `R` to be passed when creating a record manager class +with the mixin. + ## Extending Existing Record Types The Odoo Client library provides *limited* support for extending the built-in record types. diff --git a/docs/managers/index.md b/docs/managers/index.md index f0fb759..4fccb4b 100644 --- a/docs/managers/index.md +++ b/docs/managers/index.md @@ -873,9 +873,7 @@ The managers for these record types have additional methods for querying records * [Currencies](currency.md) * [OpenStack Customer Groups](customer-group.md) * [OpenStack Grant Types](grant-type.md) -* [Partner Categories](partner-category.md) * [Pricelists](pricelist.md) -* [Product Categories](product-category.md) * [OpenStack Reseller Tiers](reseller-tier.md) * [Sale Orders](sale-order.md) * [OpenStack Support Subscription Types](support-subscription-type.md) diff --git a/docs/managers/partner-category.md b/docs/managers/partner-category.md index 7780a4a..6125e41 100644 --- a/docs/managers/partner-category.md +++ b/docs/managers/partner-category.md @@ -99,6 +99,8 @@ name: str The name of the partner category. +Not guaranteed to be unique, even under the same parent category. + ### `parent_id` ```python diff --git a/docs/managers/product-category.md b/docs/managers/product-category.md index e2476f6..78f14d1 100644 --- a/docs/managers/product-category.md +++ b/docs/managers/product-category.md @@ -89,7 +89,9 @@ The complete product category tree. name: str ``` -Name of the product category. +The name of the product category. + +Not guaranteed to be unique, even under the same parent category. ### `parent_id` diff --git a/docs/managers/voucher-code.md b/docs/managers/voucher-code.md index 2709000..1db62b7 100644 --- a/docs/managers/voucher-code.md +++ b/docs/managers/voucher-code.md @@ -212,7 +212,7 @@ until it expires. name: str ``` -The unique name of this voucher code. +The automatically generated name of this voucher code. This uses the code specified in the record as-is. diff --git a/openstack_odooclient/__init__.py b/openstack_odooclient/__init__.py index 97cd8aa..22603bd 100644 --- a/openstack_odooclient/__init__.py +++ b/openstack_odooclient/__init__.py @@ -16,13 +16,10 @@ from __future__ import annotations from .base.client import ClientBase -from .base.record import FieldAlias, ModelRef, RecordBase -from .base.record_manager import RecordManagerBase -from .base.record_manager_coded import CodedRecordManagerBase -from .base.record_manager_named import NamedRecordManagerBase -from .base.record_manager_with_unique_field import ( - RecordManagerWithUniqueFieldBase, -) +from .base.record.base import RM, RecordBase, RecordProtocol, RM_co +from .base.record.types import FieldAlias, ModelRef +from .base.record_manager.base import R, RecordManagerBase +from .base.record_manager.protocol import RecordManagerProtocol from .client import Client from .exceptions import ( ClientError, @@ -77,8 +74,11 @@ VolumeDiscountRangeManager, ) from .managers.voucher_code import VoucherCode, VoucherCodeManager +from .mixins.coded_record import CodedRecordManagerMixin, CodedRecordMixin +from .mixins.named_record import NamedRecordManagerMixin, NamedRecordMixin __all__ = [ + "RM", "AccountMove", "AccountMoveLine", "AccountMoveLineManager", @@ -86,7 +86,8 @@ "Client", "ClientBase", "ClientError", - "CodedRecordManagerBase", + "CodedRecordManagerMixin", + "CodedRecordMixin", "Company", "CompanyManager", "Credit", @@ -106,7 +107,8 @@ "GrantTypeManager", "ModelRef", "MultipleRecordsFoundError", - "NamedRecordManagerBase", + "NamedRecordManagerMixin", + "NamedRecordMixin", "Partner", "PartnerCategory", "PartnerCategoryManager", @@ -121,10 +123,13 @@ "ProjectContact", "ProjectContactManager", "ProjectManager", + "R", + "RM_co", "RecordBase", "RecordManagerBase", - "RecordManagerWithUniqueFieldBase", + "RecordManagerProtocol", "RecordNotFoundError", + "RecordProtocol", "ReferralCode", "ReferralCodeManager", "Reseller", diff --git a/openstack_odooclient/base/client.py b/openstack_odooclient/base/client.py index 04bd4c2..db5fe6c 100644 --- a/openstack_odooclient/base/client.py +++ b/openstack_odooclient/base/client.py @@ -26,8 +26,8 @@ from typing_extensions import get_type_hints # 3.11 and later from ..util import is_subclass -from .record import RecordBase -from .record_manager import RecordManagerBase +from .record.base import RecordBase +from .record_manager.base import RecordManagerBase if TYPE_CHECKING: from odoorpc.db import DB # type: ignore[import] diff --git a/openstack_odooclient/base/record/__init__.py b/openstack_odooclient/base/record/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/openstack_odooclient/base/record.py b/openstack_odooclient/base/record/base.py similarity index 82% rename from openstack_odooclient/base/record.py rename to openstack_odooclient/base/record/base.py index 503a413..34236a7 100644 --- a/openstack_odooclient/base/record.py +++ b/openstack_odooclient/base/record/base.py @@ -1,4 +1,4 @@ -# Copyright (C) 2024 Catalyst Cloud Limited +# Copyright (C) 2025 Catalyst Cloud Limited # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,7 +17,6 @@ import copy -from dataclasses import dataclass from datetime import date, datetime from types import MappingProxyType, UnionType from typing import ( @@ -26,6 +25,7 @@ Any, Generic, Literal, + Protocol, Type, TypeVar, Union, @@ -37,7 +37,8 @@ get_origin as get_type_origin, ) -from ..util import is_subclass +from ...util import is_subclass +from .types import FieldAlias, ModelRef if TYPE_CHECKING: from collections.abc import Mapping, Sequence @@ -45,96 +46,37 @@ from odoorpc import ODOO # type: ignore[import] from odoorpc.env import Environment # type: ignore[import] - from .client import ClientBase + from ..client import ClientBase -RecordManager = TypeVar("RecordManager", bound="RecordManagerBase") +RM = TypeVar("RM", bound="RecordManagerBase") +"""An invariant type variable for a record manager class. +To be used when defining record mixins, +or generic base classes operating on record managers. +""" -class AnnotationBase: - @classmethod - def get(cls, type_hint: Any) -> Self | None: - """Return the annotation applied to the given type hint, - if the type hint is annotated with this type of annotation. - - If multiple matching annotations are found, the last occurrence - is returned. - - :param type_hint: The type hint to parse - :type type_hint: Any - :return: Applied annotation, or ``None`` if no annotation was found - :rtype: Self | None - """ - if get_type_origin(type_hint) is not Annotated: - return None - matching_annotation: Self | None = None - for annotation in get_type_args(type_hint)[1:]: - if isinstance(annotation, cls): - matching_annotation = annotation - return matching_annotation - - @classmethod - def is_annotated(cls, type_hint: Any) -> bool: - """Checks whether or not the given type hint is annotated - with an annotation of this type. - - :param type_hint: The type hint to parse - :type type_hint: Any - :return: ``True`` if annotated, otherwise ``False`` - :rtype: bool - """ - return bool(cls.get(type_hint)) - - -@dataclass(frozen=True) -class FieldAlias(AnnotationBase): - """An annotation for defining field aliases - (fields that point to other fields). - - Aliases are automatically resolved to the target field - when searching or creating records, or referencing field values - on record objects. - - >>> from typing import Annotated - >>> from openstack_odooclient import FieldAlias, RecordBase - >>> class CustomRecord(RecordBase["CustomRecordManager"]): - ... name: str - ... name_alias: Annotated[str, FieldAlias("name")] - """ - - field: str - - -@dataclass(frozen=True) -class ModelRef(AnnotationBase): - """An annotation for defining model refs - (fields that provide an interface to a model reference on a record). - - Model refs are used to express relationships between record types. - The first argument is the name of the relationship field in Odoo, - the second argument is the record class that type is represented by - in the OpenStack Odoo Client library. +RM_co = TypeVar("RM_co", bound="RecordManagerBase", covariant=True) +"""A covariant type variable for a record manager class. - >>> from typing import Annotated - >>> from openstack_odooclient import ModelRef, RecordBase, User - >>> class CustomRecord(RecordBase["CustomRecordManager"]): - ... user_id: Annotated[int, ModelRef("user_id", User)] - ... user_name: Annotated[str, ModelRef("user_id", User)] - ... user: Annotated[User, ModelRef("user_id", User)] +To be used when defining generic protocols, or parameters/return values +that allow ``Record`` objects using a ``RecordManager`` class that is +a subclass of the ``Record`` type within the given content. +""" - For more information, check the OpenStack Odoo Client - library documentation. - """ - field: str - record_class: Any +class RecordProtocol(Protocol[RM]): + """The protocol for a record class. + This defines all common attributes and methods available for + implementations of records to use. -class RecordBase(Generic[RecordManager]): - """The generic base class for records. + The primary use of this class is to be subclassed by mixins, to provide + type hinting for common attributes and methods available on the + ``RecordBase`` class. - Subclass this class to implement the record class for custom record types, - specifying the name of the manager class (string), as available in the - Python source file, as the generic type argument. + ``RecordBase`` is the base class that provides the core functionality + of a record object, and that is what should be subclassed to make a new + record class. """ id: int @@ -185,43 +127,28 @@ class RecordBase(Generic[RecordManager]): to their Odoo equivalent. """ - def __init__( - self, - client: ClientBase, - record: Mapping[str, Any], - fields: Sequence[str] | None, - ) -> None: - self._client = client + @property + def _client(self) -> ClientBase: """The Odoo client that created this record object.""" - self._record = MappingProxyType(record) - """The raw record fields from OdooRPC.""" - self._fields = tuple(fields) if fields else None - """The fields selected in the query that created this record object.""" - self._values: dict[str, Any] = {} - """The cache for the processed record field values.""" + ... @property - def _manager(self) -> RecordManager: + def _manager(self) -> RM: """The manager object responsible for this record.""" - mapping = self._client._record_manager_mapping - return mapping[type(self)] # type: ignore[return-value] + ... @property def _odoo(self) -> ODOO: """The OdooRPC connection object this record was created from.""" - return self._client._odoo + ... @property def _env(self) -> Environment: """The OdooRPC environment object this record was created from.""" - return self._manager._env - - @property - def _type_hints(self) -> MappingProxyType[str, Any]: - return self._manager._record_type_hints + ... @classmethod - def from_record_obj(cls, record_obj: RecordBase) -> Self: + def from_record_obj(cls, record_obj: RecordBase[RM]) -> Self: """Create a record object of this class's type from another record object. @@ -230,15 +157,11 @@ def from_record_obj(cls, record_obj: RecordBase) -> Self: of a model class). :param record_obj: Record to use to create the new object - :type record_obj: RecordBase + :type record_obj: RecordBase[RM] :return: Record object of the implementing class's type :rtype: Self """ - return cls( - client=record_obj._client, - record=record_obj._record, - fields=record_obj._fields, - ) + ... def as_dict(self, raw: bool = False) -> dict[str, Any]: """Convert this record object to a dictionary. @@ -257,14 +180,7 @@ def as_dict(self, raw: bool = False) -> dict[str, Any]: :return: Record dictionary :rtype: dict[str, Any] """ - return ( - copy.deepcopy(dict(self._record)) - if raw - else { - self._manager._get_local_field(field): copy.deepcopy(value) - for field, value in self._record.items() - } - ) + ... def update(self, **fields: Any) -> None: """Update one or more fields on this record in place. @@ -281,7 +197,7 @@ def update(self, **fields: Any) -> None: *Added in version 0.2.0.* """ - self._manager.update(self.id, **fields) + ... def refresh(self) -> Self: """Fetch the latest version of this record from Odoo. @@ -292,6 +208,82 @@ def refresh(self) -> Self: :return: Latest version of the record object :rtype: Self """ + ... + + def unlink(self) -> None: + """Delete this record from Odoo.""" + ... + + def delete(self) -> None: + """Delete this record from Odoo.""" + ... + + +class RecordBase(RecordProtocol[RM], Generic[RM]): + """The generic base class for records. + + Subclass this class to implement the record class for custom record types, + specifying the name of the manager class (string), as available in the + Python source file, as the generic type argument. + """ + + def __init__( + self, + client: ClientBase, + record: Mapping[str, Any], + fields: Sequence[str] | None, + ) -> None: + self._client_ = client + self._record = MappingProxyType(record) + """The raw record fields from OdooRPC.""" + self._fields = tuple(fields) if fields else None + """The fields selected in the query that created this record object.""" + self._values: dict[str, Any] = {} + """The cache for the processed record field values.""" + + @property + def _client(self) -> ClientBase: + return self._client_ + + @property + def _manager(self) -> RM: + mapping = self._client._record_manager_mapping + return mapping[type(self)] # type: ignore[return-value] + + @property + def _odoo(self) -> ODOO: + return self._client._odoo + + @property + def _env(self) -> Environment: + return self._manager._env + + @property + def _type_hints(self) -> MappingProxyType[str, Any]: + return self._manager._record_type_hints + + @classmethod + def from_record_obj(cls, record_obj: RecordBase[RM_co]) -> Self: + return cls( + client=record_obj._client, + record=record_obj._record, + fields=record_obj._fields, + ) + + def as_dict(self, raw: bool = False) -> dict[str, Any]: + return ( + copy.deepcopy(dict(self._record)) + if raw + else { + self._manager._get_local_field(field): copy.deepcopy(value) + for field, value in self._record.items() + } + ) + + def update(self, **fields: Any) -> None: + self._manager.update(self.id, **fields) + + def refresh(self) -> Self: return type(self)( client=self._client, record=self._env.read( @@ -302,11 +294,9 @@ def refresh(self) -> Self: ) def unlink(self) -> None: - """Delete this record from Odoo.""" self._manager.unlink(self) def delete(self) -> None: - """Delete this record from Odoo.""" self._manager.delete(self) def _get_remote_field(self, field: str) -> str: @@ -507,5 +497,5 @@ def __repr__(self) -> str: # NOTE(callumdickinson): Import here to avoid circular imports. -from ..managers.user import User # noqa: E402 -from .record_manager import RecordManagerBase # noqa: E402 +from ...managers.user import User # noqa: E402 +from ..record_manager.base import RecordManagerBase # noqa: E402 diff --git a/openstack_odooclient/base/record/types.py b/openstack_odooclient/base/record/types.py new file mode 100644 index 0000000..f7a9c45 --- /dev/null +++ b/openstack_odooclient/base/record/types.py @@ -0,0 +1,104 @@ +# Copyright (C) 2025 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Annotated, Any + +from typing_extensions import ( + Self, + get_args as get_type_args, + get_origin as get_type_origin, +) + + +class AnnotationBase: + @classmethod + def get(cls, type_hint: Any) -> Self | None: + """Return the annotation applied to the given type hint, + if the type hint is annotated with this type of annotation. + + If multiple matching annotations are found, the last occurrence + is returned. + + :param type_hint: The type hint to parse + :type type_hint: Any + :return: Applied annotation, or ``None`` if no annotation was found + :rtype: Self | None + """ + if get_type_origin(type_hint) is not Annotated: + return None + matching_annotation: Self | None = None + for annotation in get_type_args(type_hint)[1:]: + if isinstance(annotation, cls): + matching_annotation = annotation + return matching_annotation + + @classmethod + def is_annotated(cls, type_hint: Any) -> bool: + """Checks whether or not the given type hint is annotated + with an annotation of this type. + + :param type_hint: The type hint to parse + :type type_hint: Any + :return: ``True`` if annotated, otherwise ``False`` + :rtype: bool + """ + return bool(cls.get(type_hint)) + + +@dataclass(frozen=True) +class FieldAlias(AnnotationBase): + """An annotation for defining field aliases + (fields that point to other fields). + + Aliases are automatically resolved to the target field + when searching or creating records, or referencing field values + on record objects. + + >>> from typing import Annotated + >>> from openstack_odooclient import FieldAlias, RecordBase + >>> class CustomRecord(RecordBase["CustomRecordManager"]): + ... name: str + ... name_alias: Annotated[str, FieldAlias("name")] + """ + + field: str + + +@dataclass(frozen=True) +class ModelRef(AnnotationBase): + """An annotation for defining model refs + (fields that provide an interface to a model reference on a record). + + Model refs are used to express relationships between record types. + The first argument is the name of the relationship field in Odoo, + the second argument is the record class that type is represented by + in the OpenStack Odoo Client library. + + >>> from typing import Annotated + >>> from openstack_odooclient import ModelRef, RecordBase, User + >>> class CustomRecord(RecordBase["CustomRecordManager"]): + ... user_id: Annotated[int, ModelRef("user_id", User)] + ... user_name: Annotated[str, ModelRef("user_id", User)] + ... user: Annotated[User, ModelRef("user_id", User)] + + For more information, check the OpenStack Odoo Client + library documentation. + """ + + field: str + record_class: Any diff --git a/openstack_odooclient/base/record_manager/__init__.py b/openstack_odooclient/base/record_manager/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/openstack_odooclient/base/record_manager.py b/openstack_odooclient/base/record_manager/base.py similarity index 72% rename from openstack_odooclient/base/record_manager.py rename to openstack_odooclient/base/record_manager/base.py index c23e9b6..1b80f0c 100644 --- a/openstack_odooclient/base/record_manager.py +++ b/openstack_odooclient/base/record_manager/base.py @@ -1,4 +1,4 @@ -# Copyright (C) 2024 Catalyst Cloud Limited +# Copyright (C) 2025 Catalyst Cloud Limited # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,7 +16,9 @@ from __future__ import annotations import builtins +import itertools +from collections.abc import Iterable, Mapping, Sequence from datetime import date, datetime from types import MappingProxyType, UnionType from typing import ( @@ -26,7 +28,6 @@ Generic, Literal, Type, - TypeVar, Union, overload, ) @@ -38,28 +39,25 @@ get_type_hints, ) -from ..exceptions import RecordNotFoundError -from ..util import ( +from ...exceptions import MultipleRecordsFoundError, RecordNotFoundError +from ...util import ( DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT, get_mapped_field, ) -from .record import FieldAlias, ModelRef, RecordBase +from ..record.base import RecordBase +from ..record.types import FieldAlias, ModelRef +from .protocol import R, RecordManagerProtocol +from .types import FilterCriterion if TYPE_CHECKING: - from collections.abc import Iterable, Mapping, Sequence - from odoorpc import ODOO # type: ignore[import] from odoorpc.env import Environment # type: ignore[import] - from .client import ClientBase - - FilterCriterion = tuple[str, str, Any] | Sequence[Any] | str - -Record = TypeVar("Record", bound=RecordBase) + from ..client import ClientBase -class RecordManagerBase(Generic[Record]): +class RecordManagerBase(RecordManagerProtocol[R], Generic[R]): """A generic record manager base class. This is the class that is subclassed create a record manager @@ -78,7 +76,7 @@ class RecordManagerBase(Generic[Record]): >>> from openstack_odooclient import Client, RecordBase, RecordManagerBase >>> class CustomRecord(RecordBase["CustomRecordManager"]): ... name: str - >>> class CustomRecordManager(RecordManager[CustomRecord]): + >>> class CustomRecordManager(RecordManagerBase[CustomRecord]): ... env_name = "custom.record" ... record_class = CustomRecord @@ -91,22 +89,8 @@ class and add a type hint for your custom record manager. ... custom_records: CustomRecordManager """ - env_name: str - """The Odoo environment (model) name to manage.""" - - record_class: Type[Record] - """The record object type to instantiate using this manager.""" - - default_fields: tuple[str, ...] | None = None - """List of fields to fetch by default if a field list is not supplied - in queries. - - By default, all fields on the model will be fetched. - """ - def __init__(self, client: ClientBase) -> None: - self._client = client - """The Odoo client object the manager uses.""" + self._client_ = client # Assign this record manager object as the manager # responsible for the configured record class in the client. self._client._record_manager_mapping[self.record_class] = self @@ -153,6 +137,11 @@ def __init__(self, client: ClientBase) -> None: except IndexError: pass + @property + def _client(self) -> ClientBase: + """The Odoo client object the manager uses.""" + return self._client_ + @property def _odoo(self) -> ODOO: """The OdooRPC connection object this record manager uses.""" @@ -171,7 +160,7 @@ def list( fields: Iterable[str] | None = ..., as_dict: Literal[False] = ..., optional: bool = ..., - ) -> builtins.list[Record]: ... + ) -> builtins.list[R]: ... @overload def list( @@ -191,7 +180,7 @@ def list( fields: Iterable[str] | None = ..., as_dict: bool = ..., optional: bool = ..., - ) -> builtins.list[Record] | builtins.list[dict[str, Any]]: ... + ) -> builtins.list[R] | builtins.list[dict[str, Any]]: ... def list( self, @@ -199,37 +188,7 @@ def list( fields: Iterable[str] | None = None, as_dict: bool = False, optional: bool = False, - ) -> builtins.list[Record] | builtins.list[dict[str, Any]]: - """Get one or more specific records by ID. - - By default all fields available on the record model - will be selected, but this can be filtered using the - ``fields`` parameter. - - Use the ``as_dict`` parameter to return records as ``dict`` - objects, instead of record objects. - - By default, the method checks that all provided IDs - were found and returned (and will raise an error if any are missing), - at the cost of a small performance hit. - To instead return the list of records that were found - without raising an error, set ``optional`` to ``True``. - - If ``ids`` is given an empty iterator, this method - returns an empty list. - - :param ids: Record ID, or list of record IDs - :type ids: int | Iterable[int] - :param fields: Fields to select, defaults to ``None`` (select all) - :type fields: Iterable[str] | None, optional - :param as_dict: Return records as dictionaries, defaults to ``False`` - :type as_dict: bool, optional - :param optional: Disable missing record errors, defaults to ``False`` - :type optional: bool, optional - :raises RecordNotFoundError: If IDs are required but some are missing - :return: List of records - :rtype: list[Record] | list[dict[str, Any]] - """ + ) -> builtins.list[R] | builtins.list[dict[str, Any]]: if isinstance(ids, int): _ids: int | list[int] = ids else: @@ -293,7 +252,7 @@ def get( fields: Iterable[str] | None = ..., as_dict: Literal[False] = ..., optional: Literal[False] = ..., - ) -> Record: ... + ) -> R: ... @overload def get( @@ -313,7 +272,7 @@ def get( fields: Iterable[str] | None = ..., as_dict: Literal[False] = ..., optional: Literal[True], - ) -> Record | None: ... + ) -> R | None: ... @overload def get( @@ -333,7 +292,7 @@ def get( fields: Iterable[str] | None = ..., as_dict: bool = ..., optional: bool = ..., - ) -> Record | dict[str, Any] | None: ... + ) -> R | dict[str, Any] | None: ... def get( self, @@ -341,27 +300,7 @@ def get( fields: Iterable[str] | None = None, as_dict: bool = False, optional: bool = False, - ) -> Record | dict[str, Any] | None: - """Get a single record by ID. - - By default all fields available on the record model - will be selected, but this can be filtered using the - ``fields`` parameter. - - Use the ``as_dict`` parameter to return the record as - a ``dict`` object, instead of a record object. - - :param ids: Record ID - :type ids: int - :param fields: Fields to select, defaults to ``None`` (select all) - :type fields: Iterable[str] or None, optional - :param as_dict: Return record as a dictionary, defaults to ``False`` - :type as_dict: bool, optional - :param optional: Return ``None`` if not found, defaults to ``False`` - :raises RecordNotFoundError: Record with the given ID not found - :return: List of records - :rtype: Record | dict[str, Any] - """ + ) -> R | dict[str, Any] | None: try: return self.list( id, @@ -380,6 +319,166 @@ def get( ), ) from None + @overload + def get_by_unique_field( + self, + field: str, + value: Any, + *, + filters: Iterable[FilterCriterion] | None = ..., + fields: Iterable[str] | None = ..., + as_id: Literal[True], + as_dict: Literal[True], + optional: Literal[True], + ) -> int: ... + + @overload + def get_by_unique_field( + self, + field: str, + value: Any, + *, + filters: Iterable[FilterCriterion] | None = ..., + fields: Iterable[str] | None = ..., + as_id: Literal[True], + as_dict: Literal[False] = ..., + optional: Literal[True], + ) -> int | None: ... + + @overload + def get_by_unique_field( + self, + field: str, + value: Any, + *, + filters: Iterable[FilterCriterion] | None = ..., + fields: Iterable[str] | None = ..., + as_id: Literal[True], + as_dict: Literal[True], + optional: Literal[False] = ..., + ) -> int: ... + + @overload + def get_by_unique_field( + self, + field: str, + value: Any, + *, + filters: Iterable[FilterCriterion] | None = ..., + fields: Iterable[str] | None = ..., + as_id: Literal[True], + as_dict: Literal[False] = ..., + optional: Literal[False] = ..., + ) -> int: ... + + @overload + def get_by_unique_field( + self, + field: str, + value: Any, + *, + filters: Iterable[FilterCriterion] | None = ..., + fields: Iterable[str] | None = ..., + as_id: Literal[False] = ..., + as_dict: Literal[True], + optional: Literal[True], + ) -> dict[str, Any] | None: ... + + @overload + def get_by_unique_field( + self, + field: str, + value: Any, + *, + filters: Iterable[FilterCriterion] | None = ..., + fields: Iterable[str] | None = ..., + as_id: Literal[False] = ..., + as_dict: Literal[True], + optional: Literal[False] = ..., + ) -> dict[str, Any]: ... + + @overload + def get_by_unique_field( + self, + field: str, + value: Any, + *, + filters: Iterable[FilterCriterion] | None = ..., + fields: Iterable[str] | None = ..., + as_id: Literal[False] = ..., + as_dict: Literal[False] = ..., + optional: Literal[True], + ) -> R | None: ... + + @overload + def get_by_unique_field( + self, + field: str, + value: Any, + *, + filters: Iterable[FilterCriterion] | None = ..., + fields: Iterable[str] | None = ..., + as_id: Literal[False] = ..., + as_dict: Literal[False] = ..., + optional: Literal[False] = ..., + ) -> R: ... + + @overload + def get_by_unique_field( + self, + field: str, + value: Any, + *, + filters: Iterable[FilterCriterion] | None = ..., + fields: Iterable[str] | None = ..., + as_id: bool = ..., + as_dict: bool = ..., + optional: bool = ..., + ) -> R | int | dict[str, Any] | None: ... + + def get_by_unique_field( + self, + field: str, + value: Any, + filters: Iterable[FilterCriterion] | None = None, + fields: Iterable[str] | None = None, + as_id: bool = False, + as_dict: bool = False, + optional: bool = False, + ) -> R | int | dict[str, Any] | None: + field_filter = [(field, "=", value)] + try: + records = self.search( + filters=( + list(itertools.chain(field_filter, filters)) + if filters + else field_filter + ), + fields=fields, + as_id=as_id, + as_dict=as_dict, + ) + if len(records) > 1: + raise MultipleRecordsFoundError( + ( + f"Multiple {self.record_class.__name__} records " + f"found with {field!r} value {value!r} " + "when only one was expected: " + f"{', '.join(str(r) for r in records)}" + ), + ) + return records[0] + except IndexError: + if optional: + return None + else: + raise RecordNotFoundError( + ( + f"{self.record_class.__name__} record not found " + f"with {field!r} value: {value}" + ), + ) from None + @overload def search( self, @@ -388,7 +487,7 @@ def search( order: str | None = ..., as_id: Literal[False] = ..., as_dict: Literal[False] = ..., - ) -> builtins.list[Record]: ... + ) -> builtins.list[R]: ... @overload def search( @@ -432,9 +531,7 @@ def search( as_id: bool = ..., as_dict: bool = ..., ) -> ( - builtins.list[Record] - | builtins.list[int] - | builtins.list[dict[str, Any]] + builtins.list[R] | builtins.list[int] | builtins.list[dict[str, Any]] ): ... def search( @@ -444,89 +541,7 @@ def search( order: str | None = None, as_id: bool = False, as_dict: bool = False, - ) -> ( - builtins.list[Record] - | builtins.list[int] - | builtins.list[dict[str, Any]] - ): - """Query the ERP for records, optionally defining - filters to constrain the search and other parameters, - and return the results. - - Query filters should be defined using the ORM API search domain - format. For more information on the ORM API search domain format: - - https://www.odoo.com/documentation/14.0/developer/reference/addons/orm.html#search-domains - - Filters are a sequence of criteria, where each criterion - is one of the following types of values: - - * A 3-tuple or 3-element sequence in ``(field_name, operator, value)`` - format, where: - - * ``field_name`` (``str``) is the the name of the field to filter by. - * ``operator`` (`str`) is the comparison operator to use (for more - information on the available operators, check the ORM API - search domain documentation). - * ``value`` (`Any`) is the value to compare records against. - - * A logical operator which prefixes the following filter criteria - to form a **criteria combination**: - - * ``&`` is a logical AND. Records only match if **both** of the - following **two** criteria match. - * ``|`` is a logical OR. Records match if **either** of the - following **two** criteria match. - * ``!`` is a logical NOT (negation). Records match if the - following **one** criterion does **NOT** match. - - Every criteria combination is implicitly combined using a logical AND - to form the overall filter to use to query records. - - For the field value, this method accepts the same types as defined - on the record objects. - - In addition to the native Odoo field names, field aliases - and model ref field names can be specified as the field name - in the search filter. Record objects can also be directly - passed as the value on a filter, not just record IDs. - - When specifying a range of possible values, lists, tuples - and sets are supported. - - Search criteria using nested field references can be defined - by using the dot-notation (``.``) to specify what field on what - record reference to check. - Field names and values for nested field references are - validated and encoded just like criteria for standard - field references. - - To search *all* records, leave ``filters`` unset - (or set it to ``None``). - - By default all fields available on the record model - will be selected, but this can be filtered using the - ``fields`` parameter. - - Use the ``as_id`` parameter to return the record as - a list of IDs, instead of record objects. - - Use the ``as_dict`` parameter to return the record as - a list of ``dict`` objects, instead of record objects. - - :param filters: Filters to query by, defaults to ``None`` (no filters) - :type filters: tuple[str, str, Any] | Sequence[Any] | str | None - :param fields: Fields to select, defaults to ``None`` (select all) - :type fields: Iterable[str] or None, optional - :param order: Order results by field name, defaults to ``None`` - :type order: str or None, optional - :param as_id: Return the record IDs only, defaults to ``False`` - :type as_id: bool, optional - :param as_dict: Return records as dictionaries, defaults to ``False`` - :type as_dict: bool, optional - :return: List of records - :rtype: list[Record] | list[int] | list[dict[str, Any]] - """ + ) -> builtins.list[R] | builtins.list[int] | builtins.list[dict[str, Any]]: ids: list[int] = self._env.search( (self._encode_filters(filters) if filters else []), order=order, @@ -630,52 +645,9 @@ def _encode_filter_field(self, field: str) -> tuple[Any, str]: return (type_hint, remote_field) def create(self, **fields: Any) -> int: - """Create a new record, using the specified keyword arguments - as input fields. - - This method allows a lot of flexibility in how input fields - should be defined. - - The fields passed to this method should use the same field names - and value types that are defined on the record classes. - The Odoo Client library will convert the values to the formats - that the Odoo API expects. - - For example, when defining references to another record, - you can either pass the record ID, or the record object. - The field name can also either be for the ID or the object. - - Field aliases are also resolved to their target field names. - - When creating a record with a list of references to another record - (a ``One2many`` or ``Many2many`` relation), it is possible to nest - record mappings where an ID or object would normally go. - New records will be created for those mappings, and linked - to the parent record. Nested record mappings are recursively validated - and processed in the same way as the parent record. - - To fetch the newly created record object, - pass the returned ID to the ``get`` method. - - :return: The ID of the newly created record - :rtype: int - """ return self._env.create(self._encode_create_fields(fields)) def create_multi(self, *records: Mapping[str, Any]) -> builtins.list[int]: - """Create one or more new records in a single request, - passing in the mappings containing the record's input fields - as positional arguments. - - The record mappings should be in the same format as with - the ``create`` method. - - To fetch the newly created record objects, - pass the returned IDs to the ``list`` method. - - :return: The IDs of the newly created records - :rtype: list[int] - """ res: int | list[int] = self._env.create( [self._encode_create_fields(record) for record in records], ) @@ -808,39 +780,13 @@ def _encode_create_field( self._encode_value(type_hint=type_hint, value=value), ) - def update(self, record: int | Record, **fields: Any) -> None: - """Update one or more fields on the given record in place. - - Field names are passed as keyword arguments. - This method has the same flexibility with regards to what - field names are used as when creating records; for example, - when updating a model ref, either its ID (e.g. ``user_id``) - or object (e.g. ``user``) field names can be used. - - *Added in version 0.2.0.* - - :param record: The record to update (object or ID) - :type record: int | Record - """ + def update(self, record: int | R, **fields: Any) -> None: self._env.update( record.id if isinstance(record, RecordBase) else record, self._encode_create_fields(fields), ) - def unlink( - self, - *records: int | Record | Iterable[int | Record], - ) -> None: - """Delete one or more records from Odoo. - - This method accepts either a record object or ID, or an iterable of - either of those types. Multiple positional arguments are allowed. - - All specified records will be deleted in a single request. - - :param records: The records to delete (object, ID, or record/ID list) - :type records: Record | int | Iterable[Record | int] - """ + def unlink(self, *records: int | R | Iterable[int | R]) -> None: _ids: list[int] = [] for ids in records: if isinstance(ids, int): @@ -853,20 +799,7 @@ def unlink( ) self._env.unlink(_ids) - def delete( - self, - *records: Record | int | Iterable[Record | int], - ) -> None: - """Delete one or more records from Odoo. - - This method accepts either a record object or ID, or an iterable of - either of those types. Multiple positional arguments are allowed. - - All specified records will be deleted in a single request. - - :param records: The records to delete (object, ID, or record/ID list) - :type records: Record | int | Iterable[Record | int] - """ + def delete(self, *records: R | int | Iterable[R | int]) -> None: self.unlink(*records) def _get_remote_field(self, field: str) -> str: diff --git a/openstack_odooclient/base/record_manager/protocol.py b/openstack_odooclient/base/record_manager/protocol.py new file mode 100644 index 0000000..1bbf088 --- /dev/null +++ b/openstack_odooclient/base/record_manager/protocol.py @@ -0,0 +1,638 @@ +# Copyright (C) 2025 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import builtins + +from typing import TYPE_CHECKING, Protocol, TypeVar, overload + +from ..record.base import RecordBase +from .types import FilterCriterion + +if TYPE_CHECKING: + from collections.abc import Iterable, Mapping, Sequence + from typing import Any, Literal, Type + + from odoorpc import ODOO # type: ignore[import] + from odoorpc.env import Environment # type: ignore[import] + + from ..client import ClientBase + +R = TypeVar("R", bound=RecordBase) +"""An invariant type variable for a record class. + +To be used when defining record manager mixins, +or generic base classes operating on records. +""" + + +class RecordManagerProtocol(Protocol[R]): + """The protocol for a record manager class. + + This defines all common attributes and stubs for methods available for + implementations of record managers to use. + + The primary use of this class is to be subclassed by mixins, to provide + type hinting for common attributes and methods available on the + ``RecordManagerBase`` class. + + ``RecordManagerBase`` is the base class that provides the core + functionality of a record manager, and that class should be subclassed + to make a new record manager class. + """ + + env_name: str + """The Odoo environment (model) name to manage.""" + + record_class: Type[R] + """The record object type to instantiate using this manager.""" + + default_fields: tuple[str, ...] | None = None + """List of fields to fetch by default if a field list is not supplied + in queries. + + By default, all fields on the model will be fetched. + """ + + @property + def _client(self) -> ClientBase: + """The Odoo client object the manager uses.""" + ... + + @property + def _odoo(self) -> ODOO: + """The OdooRPC connection object this record manager uses.""" + ... + + @property + def _env(self) -> Environment: + """The OdooRPC environment object this record manager uses.""" + ... + + @overload + def list( + self, + ids: int | Iterable[int], + *, + fields: Iterable[str] | None = ..., + as_dict: Literal[False] = ..., + optional: bool = ..., + ) -> builtins.list[R]: ... + + @overload + def list( + self, + ids: int | Iterable[int], + *, + fields: Iterable[str] | None = ..., + as_dict: Literal[True], + optional: bool = ..., + ) -> builtins.list[dict[str, Any]]: ... + + @overload + def list( + self, + ids: int | Iterable[int], + *, + fields: Iterable[str] | None = ..., + as_dict: bool = ..., + optional: bool = ..., + ) -> builtins.list[R] | builtins.list[dict[str, Any]]: ... + + def list( + self, + ids: int | Iterable[int], + fields: Iterable[str] | None = None, + as_dict: bool = False, + optional: bool = False, + ) -> builtins.list[R] | builtins.list[dict[str, Any]]: + """Get one or more specific records by ID. + + By default all fields available on the record model + will be selected, but this can be filtered using the + ``fields`` parameter. + + Use the ``as_dict`` parameter to return records as ``dict`` + objects, instead of record objects. + + By default, the method checks that all provided IDs + were found and returned (and will raise an error if any are missing), + at the cost of a small performance hit. + To instead return the list of records that were found + without raising an error, set ``optional`` to ``True``. + + If ``ids`` is given an empty iterator, this method + returns an empty list. + + :param ids: Record ID, or list of record IDs + :type ids: int | Iterable[int] + :param fields: Fields to select, defaults to ``None`` (select all) + :type fields: Iterable[str] | None, optional + :param as_dict: Return records as dictionaries, defaults to ``False`` + :type as_dict: bool, optional + :param optional: Disable missing record errors, defaults to ``False`` + :type optional: bool, optional + :raises RecordNotFoundError: If IDs are required but some are missing + :return: List of records + :rtype: list[R] | list[dict[str, Any]] + """ + ... + + @overload + def get( + self, + id: int, + *, + fields: Iterable[str] | None = ..., + as_dict: Literal[False] = ..., + optional: Literal[False] = ..., + ) -> R: ... + + @overload + def get( + self, + id: int, + *, + fields: Iterable[str] | None = ..., + as_dict: Literal[True], + optional: Literal[False] = ..., + ) -> dict[str, Any]: ... + + @overload + def get( + self, + id: int, + *, + fields: Iterable[str] | None = ..., + as_dict: Literal[False] = ..., + optional: Literal[True], + ) -> R | None: ... + + @overload + def get( + self, + id: int, + *, + fields: Iterable[str] | None = ..., + as_dict: Literal[True], + optional: Literal[True], + ) -> dict[str, Any] | None: ... + + @overload + def get( + self, + id: int, + *, + fields: Iterable[str] | None = ..., + as_dict: bool = ..., + optional: bool = ..., + ) -> R | dict[str, Any] | None: ... + + def get( + self, + id: int, # noqa: A002 + fields: Iterable[str] | None = None, + as_dict: bool = False, + optional: bool = False, + ) -> R | dict[str, Any] | None: + """Get a single record by ID. + + By default all fields available on the record model + will be selected, but this can be filtered using the + ``fields`` parameter. + + Use the ``as_dict`` parameter to return the record as + a ``dict`` object, instead of a record object. + + :param ids: Record ID + :type ids: int + :param fields: Fields to select, defaults to ``None`` (select all) + :type fields: Iterable[str] or None, optional + :param as_dict: Return record as a dictionary, defaults to ``False`` + :type as_dict: bool, optional + :param optional: Return ``None`` if not found, defaults to ``False`` + :raises RecordNotFoundError: Record with the given ID not found + :return: List of records + :rtype: R | dict[str, Any] | None + """ + ... + + @overload + def get_by_unique_field( + self, + field: str, + value: Any, + *, + filters: Iterable[FilterCriterion] | None = ..., + fields: Iterable[str] | None = ..., + as_id: Literal[True], + as_dict: Literal[True], + optional: Literal[True], + ) -> int: ... + + @overload + def get_by_unique_field( + self, + field: str, + value: Any, + *, + filters: Iterable[FilterCriterion] | None = ..., + fields: Iterable[str] | None = ..., + as_id: Literal[True], + as_dict: Literal[False] = ..., + optional: Literal[True], + ) -> int | None: ... + + @overload + def get_by_unique_field( + self, + field: str, + value: Any, + *, + filters: Iterable[FilterCriterion] | None = ..., + fields: Iterable[str] | None = ..., + as_id: Literal[True], + as_dict: Literal[True], + optional: Literal[False] = ..., + ) -> int: ... + + @overload + def get_by_unique_field( + self, + field: str, + value: Any, + *, + filters: Iterable[FilterCriterion] | None = ..., + fields: Iterable[str] | None = ..., + as_id: Literal[True], + as_dict: Literal[False] = ..., + optional: Literal[False] = ..., + ) -> int: ... + + @overload + def get_by_unique_field( + self, + field: str, + value: Any, + *, + filters: Iterable[FilterCriterion] | None = ..., + fields: Iterable[str] | None = ..., + as_id: Literal[False] = ..., + as_dict: Literal[True], + optional: Literal[True], + ) -> dict[str, Any] | None: ... + + @overload + def get_by_unique_field( + self, + field: str, + value: Any, + *, + filters: Iterable[FilterCriterion] | None = ..., + fields: Iterable[str] | None = ..., + as_id: Literal[False] = ..., + as_dict: Literal[True], + optional: Literal[False] = ..., + ) -> dict[str, Any]: ... + + @overload + def get_by_unique_field( + self, + field: str, + value: Any, + *, + filters: Iterable[FilterCriterion] | None = ..., + fields: Iterable[str] | None = ..., + as_id: Literal[False] = ..., + as_dict: Literal[False] = ..., + optional: Literal[True], + ) -> R | None: ... + + @overload + def get_by_unique_field( + self, + field: str, + value: Any, + *, + filters: Iterable[FilterCriterion] | None = ..., + fields: Iterable[str] | None = ..., + as_id: Literal[False] = ..., + as_dict: Literal[False] = ..., + optional: Literal[False] = ..., + ) -> R: ... + + @overload + def get_by_unique_field( + self, + field: str, + value: Any, + *, + filters: Iterable[FilterCriterion] | None = ..., + fields: Iterable[str] | None = ..., + as_id: bool = ..., + as_dict: bool = ..., + optional: bool = ..., + ) -> R | int | dict[str, Any] | None: ... + + def get_by_unique_field( + self, + field: str, + value: Any, + filters: Iterable[FilterCriterion] | None = None, + fields: Iterable[str] | None = None, + as_id: bool = False, + as_dict: bool = False, + optional: bool = False, + ) -> R | int | dict[str, Any] | None: + """Query a unique record by a specific field. + + A number of parameters are available to configure the return type, + and what happens when a result is not found. + + Additional filters can be added to the search query using the + ``filters`` parameter. If defined, these filters will be appended + to the unique field search filter. Filters should be defined + using the same format that ``search`` uses. + + By default all fields available on the record model + will be selected, but this can be filtered using the + ``fields`` parameter. + + Use the ``as_id`` parameter to return the ID of the record, + instead of the record object. + + Use the ``as_dict`` parameter to return the record as + a ``dict`` object, instead of a record object. + + When ``optional`` is ``True``, ``None`` is returned if a record + with the given name does not exist, instead of raising an error. + + *Added in version 0.2.0.* + + :param field: The unique field name to query by + :type field: str + :param value: The unique field value + :type value: Any + :param filters: Optional additional filters to apply, defaults to None + :type filters: Iterable[FilterCriterion] | None, optional + :param fields: Fields to select, defaults to ``None`` (select all) + :type fields: Iterable[str] | None, optional + :param as_id: Return a record ID, defaults to False + :type as_id: bool, optional + :param as_dict: Return the record as a dictionary, defaults to False + :type as_dict: bool, optional + :param optional: Return ``None`` if not found, defaults to False + :type optional: bool, optional + :raises MultipleRecordsFoundError: Multiple records with the same name + :raises RecordNotFoundError: Record with the given name not found + :return: Query result (or ``None`` if record not found and optional) + :rtype: R | int | dict[str, Any] | None + """ + ... + + @overload + def search( + self, + filters: Sequence[FilterCriterion] | None = ..., + fields: Iterable[str] | None = ..., + order: str | None = ..., + as_id: Literal[False] = ..., + as_dict: Literal[False] = ..., + ) -> builtins.list[R]: ... + + @overload + def search( + self, + filters: Sequence[FilterCriterion] | None = ..., + fields: Iterable[str] | None = ..., + order: str | None = ..., + *, + as_id: Literal[True], + as_dict: Literal[False] = ..., + ) -> builtins.list[int]: ... + + @overload + def search( + self, + filters: Sequence[FilterCriterion] | None = ..., + fields: Iterable[str] | None = ..., + order: str | None = ..., + as_id: Literal[False] = ..., + *, + as_dict: Literal[True], + ) -> builtins.list[dict[str, Any]]: ... + + @overload + def search( + self, + filters: Sequence[FilterCriterion] | None = ..., + fields: Iterable[str] | None = ..., + order: str | None = ..., + *, + as_id: Literal[True], + as_dict: Literal[True], + ) -> builtins.list[int]: ... + + @overload + def search( + self, + filters: Sequence[FilterCriterion] | None = ..., + fields: Iterable[str] | None = ..., + order: str | None = ..., + as_id: bool = ..., + as_dict: bool = ..., + ) -> ( + builtins.list[R] | builtins.list[int] | builtins.list[dict[str, Any]] + ): ... + + def search( + self, + filters: Sequence[FilterCriterion] | None = None, + fields: Iterable[str] | None = None, + order: str | None = None, + as_id: bool = False, + as_dict: bool = False, + ) -> builtins.list[R] | builtins.list[int] | builtins.list[dict[str, Any]]: + """Query the ERP for records, optionally defining + filters to constrain the search and other parameters, + and return the results. + + Query filters should be defined using the ORM API search domain + format. For more information on the ORM API search domain format: + + https://www.odoo.com/documentation/14.0/developer/reference/addons/orm.html#search-domains + + Filters are a sequence of criteria, where each criterion + is one of the following types of values: + + * A 3-tuple or 3-element sequence in ``(field_name, operator, value)`` + format, where: + + * ``field_name`` (``str``) is the the name of the field to filter by. + * ``operator`` (`str`) is the comparison operator to use (for more + information on the available operators, check the ORM API + search domain documentation). + * ``value`` (`Any`) is the value to compare records against. + + * A logical operator which prefixes the following filter criteria + to form a **criteria combination**: + + * ``&`` is a logical AND. Records only match if **both** of the + following **two** criteria match. + * ``|`` is a logical OR. Records match if **either** of the + following **two** criteria match. + * ``!`` is a logical NOT (negation). Records match if the + following **one** criterion does **NOT** match. + + Every criteria combination is implicitly combined using a logical AND + to form the overall filter to use to query records. + + For the field value, this method accepts the same types as defined + on the record objects. + + In addition to the native Odoo field names, field aliases + and model ref field names can be specified as the field name + in the search filter. Record objects can also be directly + passed as the value on a filter, not just record IDs. + + When specifying a range of possible values, lists, tuples + and sets are supported. + + Search criteria using nested field references can be defined + by using the dot-notation (``.``) to specify what field on what + record reference to check. + Field names and values for nested field references are + validated and encoded just like criteria for standard + field references. + + To search *all* records, leave ``filters`` unset + (or set it to ``None``). + + By default all fields available on the record model + will be selected, but this can be filtered using the + ``fields`` parameter. + + Use the ``as_id`` parameter to return the record as + a list of IDs, instead of record objects. + + Use the ``as_dict`` parameter to return the record as + a list of ``dict`` objects, instead of record objects. + + :param filters: Filters to query by, defaults to ``None`` (no filters) + :type filters: tuple[str, str, Any] | Sequence[Any] | str | None + :param fields: Fields to select, defaults to ``None`` (select all) + :type fields: Iterable[str] or None, optional + :param order: Order results by field name, defaults to ``None`` + :type order: str or None, optional + :param as_id: Return the record IDs only, defaults to ``False`` + :type as_id: bool, optional + :param as_dict: Return records as dictionaries, defaults to ``False`` + :type as_dict: bool, optional + :return: List of records + :rtype: list[R] | list[int] | list[dict[str, Any]] + """ + ... + + def create(self, **fields: Any) -> int: + """Create a new record, using the specified keyword arguments + as input fields. + + This method allows a lot of flexibility in how input fields + should be defined. + + The fields passed to this method should use the same field names + and value types that are defined on the record classes. + The Odoo Client library will convert the values to the formats + that the Odoo API expects. + + For example, when defining references to another record, + you can either pass the record ID, or the record object. + The field name can also either be for the ID or the object. + + Field aliases are also resolved to their target field names. + + When creating a record with a list of references to another record + (a ``One2many`` or ``Many2many`` relation), it is possible to nest + record mappings where an ID or object would normally go. + New records will be created for those mappings, and linked + to the parent record. Nested record mappings are recursively validated + and processed in the same way as the parent record. + + To fetch the newly created record object, + pass the returned ID to the ``get`` method. + + :return: The ID of the newly created record + :rtype: int + """ + ... + + def create_multi(self, *records: Mapping[str, Any]) -> builtins.list[int]: + """Create one or more new records in a single request, + passing in the mappings containing the record's input fields + as positional arguments. + + The record mappings should be in the same format as with + the ``create`` method. + + To fetch the newly created record objects, + pass the returned IDs to the ``list`` method. + + :return: The IDs of the newly created records + :rtype: list[int] + """ + ... + + def update(self, record: int | R, **fields: Any) -> None: + """Update one or more fields on the given record in place. + + Field names are passed as keyword arguments. + This method has the same flexibility with regards to what + field names are used as when creating records; for example, + when updating a model ref, either its ID (e.g. ``user_id``) + or object (e.g. ``user``) field names can be used. + + *Added in version 0.2.0.* + + :param record: The record to update (object or ID) + :type record: int | R + """ + ... + + def unlink(self, *records: int | R | Iterable[int | R]) -> None: + """Delete one or more records from Odoo. + + This method accepts either a record object or ID, or an iterable of + either of those types. Multiple positional arguments are allowed. + + All specified records will be deleted in a single request. + + :param records: The records to delete (object, ID, or record/ID list) + :type records: Record | int | Iterable[Record | int] + """ + ... + + def delete(self, *records: R | int | Iterable[R | int]) -> None: + """Delete one or more records from Odoo. + + This method accepts either a record object or ID, or an iterable of + either of those types. Multiple positional arguments are allowed. + + All specified records will be deleted in a single request. + + :param records: The records to delete (object, ID, or record/ID list) + :type records: Record | int | Iterable[Record | int] + """ + ... diff --git a/openstack_odooclient/base/record_manager/types.py b/openstack_odooclient/base/record_manager/types.py new file mode 100644 index 0000000..e71673c --- /dev/null +++ b/openstack_odooclient/base/record_manager/types.py @@ -0,0 +1,53 @@ +# Copyright (C) 2025 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from collections.abc import Sequence +from typing import Any + +FilterCriterion = tuple[str, str, Any] | Sequence[Any] | str +"""An individual criterion used for searching records in Odoo. + +Query filters should be defined using the ORM API search domain +format. For more information on the ORM API search domain format: + +https://www.odoo.com/documentation/14.0/developer/reference/addons/orm.html#search-domains + +Filters are a sequence of criteria, where each criterion +is one of the following types of values: + +* A 3-tuple or 3-element sequence in ``(field_name, operator, value)`` + format, where: + + * ``field_name`` (``str``) is the the name of the field to filter by. + * ``operator`` (`str`) is the comparison operator to use (for more + information on the available operators, check the ORM API + search domain documentation). + * ``value`` (`Any`) is the value to compare records against. + +* A logical operator which prefixes the following filter criteria + to form a **criteria combination**: + + * ``&`` is a logical AND. Records only match if **both** of the + following **two** criteria match. + * ``|`` is a logical OR. Records match if **either** of the + following **two** criteria match. + * ``!`` is a logical NOT (negation). Records match if the + following **one** criterion does **NOT** match. + +Every criteria combination is implicitly combined using a logical AND +to form the overall filter to use to query records. +""" diff --git a/openstack_odooclient/base/record_manager_with_unique_field.py b/openstack_odooclient/base/record_manager_with_unique_field.py deleted file mode 100644 index f263b3d..0000000 --- a/openstack_odooclient/base/record_manager_with_unique_field.py +++ /dev/null @@ -1,259 +0,0 @@ -# Copyright (C) 2024 Catalyst Cloud Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -# implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -import itertools - -from typing import TYPE_CHECKING, Any, Generic, Literal, TypeVar, overload - -from ..exceptions import MultipleRecordsFoundError, RecordNotFoundError -from .record_manager import Record, RecordManagerBase - -if TYPE_CHECKING: - from collections.abc import Iterable - -T = TypeVar("T") - - -class RecordManagerWithUniqueFieldBase( - RecordManagerBase[Record], - Generic[Record, T], -): - """A generic record manager base class for defining a record class - with a searchable unique field. - - In addition to the usual generic type arg, a second type arg - should be provided when subclassing ``RecordManagerWithUniqueFieldBase``. - This becomes the expected type of the searchable unique field. - - >>> from openstack_odooclient import ( - ... RecordBase, - ... RecordManagerWithUniqueFieldBase, - ... ) - >>> class CustomRecord(RecordBase["CustomRecordManager"]): - ... name: str - >>> class CustomRecordManager( - ... RecordManagerWithUniqueFieldBase[CustomRecord, str], - ... ): - ... env_name = "custom.record" - ... record_class = CustomRecord - - Once you have your manager class, you can define methods - that use the provided ``_get_by_unique_field`` method to implement - custom search functionality according to your needs. - """ - - @overload - def _get_by_unique_field( - self, - field: str, - value: T, - *, - filters: Iterable[Any] | None = ..., - fields: Iterable[str] | None = ..., - as_id: Literal[True], - as_dict: Literal[True], - optional: Literal[True], - ) -> int: ... - - @overload - def _get_by_unique_field( - self, - field: str, - value: T, - *, - filters: Iterable[Any] | None = ..., - fields: Iterable[str] | None = ..., - as_id: Literal[True], - as_dict: Literal[False] = ..., - optional: Literal[True], - ) -> int | None: ... - - @overload - def _get_by_unique_field( - self, - field: str, - value: T, - *, - filters: Iterable[Any] | None = ..., - fields: Iterable[str] | None = ..., - as_id: Literal[True], - as_dict: Literal[True], - optional: Literal[False] = ..., - ) -> int: ... - - @overload - def _get_by_unique_field( - self, - field: str, - value: T, - *, - filters: Iterable[Any] | None = ..., - fields: Iterable[str] | None = ..., - as_id: Literal[True], - as_dict: Literal[False] = ..., - optional: Literal[False] = ..., - ) -> int: ... - - @overload - def _get_by_unique_field( - self, - field: str, - value: T, - *, - filters: Iterable[Any] | None = ..., - fields: Iterable[str] | None = ..., - as_id: Literal[False] = ..., - as_dict: Literal[True], - optional: Literal[True], - ) -> dict[str, Any] | None: ... - - @overload - def _get_by_unique_field( - self, - field: str, - value: T, - *, - filters: Iterable[Any] | None = ..., - fields: Iterable[str] | None = ..., - as_id: Literal[False] = ..., - as_dict: Literal[True], - optional: Literal[False] = ..., - ) -> dict[str, Any]: ... - - @overload - def _get_by_unique_field( - self, - field: str, - value: T, - *, - filters: Iterable[Any] | None = ..., - fields: Iterable[str] | None = ..., - as_id: Literal[False] = ..., - as_dict: Literal[False] = ..., - optional: Literal[True], - ) -> Record | None: ... - - @overload - def _get_by_unique_field( - self, - field: str, - value: T, - *, - filters: Iterable[Any] | None = ..., - fields: Iterable[str] | None = ..., - as_id: Literal[False] = ..., - as_dict: Literal[False] = ..., - optional: Literal[False] = ..., - ) -> Record: ... - - @overload - def _get_by_unique_field( - self, - field: str, - value: T, - *, - filters: Iterable[Any] | None = ..., - fields: Iterable[str] | None = ..., - as_id: bool = ..., - as_dict: bool = ..., - optional: bool = ..., - ) -> Record | int | dict[str, Any] | None: ... - - def _get_by_unique_field( - self, - field: str, - value: T, - filters: Iterable[Any] | None = None, - fields: Iterable[str] | None = None, - as_id: bool = False, - as_dict: bool = False, - optional: bool = False, - ) -> Record | int | dict[str, Any] | None: - """Query a unique record by a specific field. - - A number of parameters are available to configure the return type, - and what happens when a result is not found. - - Additional filters can be added to the search query using the - ``filters`` parameter. If defined, these filters will be appended - to the unique field search filter. Filters should be defined - using the same format that ``search`` uses. - - By default all fields available on the record model - will be selected, but this can be filtered using the - ``fields`` parameter. - - Use the ``as_id`` parameter to return the ID of the record, - instead of the record object. - - Use the ``as_dict`` parameter to return the record as - a ``dict`` object, instead of a record object. - - When ``optional`` is ``True``, ``None`` is returned if a record - with the given name does not exist, instead of raising an error. - - :param field: The unique field name to query by - :type field: str - :param value: The unique field value - :type value: T - :param filters: Optional additional filters to apply, defaults to None - :type filters: Iterable[Any] | None, optional - :param fields: Fields to select, defaults to ``None`` (select all) - :type fields: Iterable[str] | None, optional - :param as_id: Return a record ID, defaults to False - :type as_id: bool, optional - :param as_dict: Return the record as a dictionary, defaults to False - :type as_dict: bool, optional - :param optional: Return ``None`` if not found, defaults to False - :type optional: bool, optional - :raises MultipleRecordsFoundError: Multiple records with the same name - :raises RecordNotFoundError: Record with the given name not found - :return: Query result (or ``None`` if record not found and optional) - :rtype: Record | int | dict[str, Any] | None - """ - field_filter = [(field, "=", value)] - try: - records = self.search( - filters=( - list(itertools.chain(field_filter, filters)) - if filters - else field_filter - ), - fields=fields, - as_id=as_id, - as_dict=as_dict, - ) - if len(records) > 1: - raise MultipleRecordsFoundError( - ( - f"Multiple {self.record_class.__name__} records " - f"found with {field!r} value {value!r} " - "when only one was expected: " - f"{', '.join(str(r) for r in records)}" - ), - ) - return records[0] - except IndexError: - if optional: - return None - else: - raise RecordNotFoundError( - ( - f"{self.record_class.__name__} record not found " - f"with {field!r} value: {value}" - ), - ) from None diff --git a/openstack_odooclient/managers/account_move.py b/openstack_odooclient/managers/account_move.py index c401485..28dc955 100644 --- a/openstack_odooclient/managers/account_move.py +++ b/openstack_odooclient/managers/account_move.py @@ -18,14 +18,19 @@ from datetime import date from typing import TYPE_CHECKING, Annotated, Any, Literal -from ..base.record import ModelRef, RecordBase -from ..base.record_manager_named import NamedRecordManagerBase +from ..base.record.base import RecordBase +from ..base.record.types import ModelRef +from ..base.record_manager.base import RecordManagerBase +from ..mixins.named_record import NamedRecordManagerMixin, NamedRecordMixin if TYPE_CHECKING: from collections.abc import Iterable, Mapping -class AccountMove(RecordBase["AccountMoveManager"]): +class AccountMove( + RecordBase["AccountMoveManager"], + NamedRecordMixin["AccountMoveManager"], +): amount_total: float """Total (taxed) amount charged on the account move (invoice).""" @@ -95,9 +100,6 @@ class AccountMove(RecordBase["AccountMoveManager"]): * ``in_receipt`` - Purchase Receipt """ - name: str | Literal[False] - """Name assigned to the account move (invoice), if posted.""" - os_project_id: Annotated[int | None, ModelRef("os_project", Project)] """The ID of the OpenStack project this account move (invoice) was generated for, if this is an invoice for OpenStack project usage. @@ -176,7 +178,10 @@ def send_openstack_invoice_email( ) -class AccountMoveManager(NamedRecordManagerBase[AccountMove]): +class AccountMoveManager( + RecordManagerBase[AccountMove], + NamedRecordManagerMixin[AccountMove], +): env_name = "account.move" record_class = AccountMove diff --git a/openstack_odooclient/managers/account_move_line.py b/openstack_odooclient/managers/account_move_line.py index c738389..251ccdb 100644 --- a/openstack_odooclient/managers/account_move_line.py +++ b/openstack_odooclient/managers/account_move_line.py @@ -17,8 +17,9 @@ from typing import Annotated, Literal -from ..base.record import ModelRef, RecordBase -from ..base.record_manager import RecordManagerBase +from ..base.record.base import RecordBase +from ..base.record.types import ModelRef +from ..base.record_manager.base import RecordManagerBase class AccountMoveLine(RecordBase["AccountMoveLineManager"]): diff --git a/openstack_odooclient/managers/company.py b/openstack_odooclient/managers/company.py index f36a352..e6648ef 100644 --- a/openstack_odooclient/managers/company.py +++ b/openstack_odooclient/managers/company.py @@ -19,11 +19,16 @@ from typing_extensions import Self -from ..base.record import ModelRef, RecordBase -from ..base.record_manager_named import NamedRecordManagerBase +from ..base.record.base import RecordBase +from ..base.record.types import ModelRef +from ..base.record_manager.base import RecordManagerBase +from ..mixins.named_record import NamedRecordManagerMixin, NamedRecordMixin -class Company(RecordBase["CompanyManager"]): +class Company( + RecordBase["CompanyManager"], + NamedRecordMixin["CompanyManager"], +): active: bool """Whether or not this company is active (enabled).""" @@ -37,9 +42,6 @@ class Company(RecordBase["CompanyManager"]): and caches them for subsequent accesses. """ - name: str - """Company name, set from the partner name.""" - parent_id: Annotated[int | None, ModelRef("parent_id", Self)] """The ID for the parent company, if this company is the child of another company. @@ -75,7 +77,10 @@ class Company(RecordBase["CompanyManager"]): """ -class CompanyManager(NamedRecordManagerBase[Company]): +class CompanyManager( + RecordManagerBase[Company], + NamedRecordManagerMixin[Company], +): env_name = "res.company" record_class = Company diff --git a/openstack_odooclient/managers/credit.py b/openstack_odooclient/managers/credit.py index ea55c7e..fe47a18 100644 --- a/openstack_odooclient/managers/credit.py +++ b/openstack_odooclient/managers/credit.py @@ -18,8 +18,9 @@ from datetime import date from typing import Annotated -from ..base.record import ModelRef, RecordBase -from ..base.record_manager import RecordManagerBase +from ..base.record.base import RecordBase +from ..base.record.types import ModelRef +from ..base.record_manager.base import RecordManagerBase class Credit(RecordBase["CreditManager"]): diff --git a/openstack_odooclient/managers/credit_transaction.py b/openstack_odooclient/managers/credit_transaction.py index 7f60b51..939e81d 100644 --- a/openstack_odooclient/managers/credit_transaction.py +++ b/openstack_odooclient/managers/credit_transaction.py @@ -17,8 +17,9 @@ from typing import Annotated -from ..base.record import ModelRef, RecordBase -from ..base.record_manager import RecordManagerBase +from ..base.record.base import RecordBase +from ..base.record.types import ModelRef +from ..base.record_manager.base import RecordManagerBase class CreditTransaction(RecordBase["CreditTransactionManager"]): diff --git a/openstack_odooclient/managers/credit_type.py b/openstack_odooclient/managers/credit_type.py index e9af61a..c904584 100644 --- a/openstack_odooclient/managers/credit_type.py +++ b/openstack_odooclient/managers/credit_type.py @@ -17,11 +17,16 @@ from typing import Annotated -from ..base.record import ModelRef, RecordBase -from ..base.record_manager_named import NamedRecordManagerBase +from ..base.record.base import RecordBase +from ..base.record.types import ModelRef +from ..base.record_manager.base import RecordManagerBase +from ..mixins.named_record import NamedRecordManagerMixin, NamedRecordMixin -class CreditType(RecordBase["CreditTypeManager"]): +class CreditType( + RecordBase["CreditTypeManager"], + NamedRecordMixin["CreditTypeManager"], +): credit_ids: Annotated[list[int], ModelRef("credits", Credit)] """A list of IDs for the credits which are of this credit type.""" @@ -32,9 +37,6 @@ class CreditType(RecordBase["CreditTypeManager"]): and caches them for subsequent accesses. """ - name: str - """Name of the Credit Type.""" - only_for_product_ids: Annotated[ list[int], ModelRef("only_for_products", Product), @@ -104,7 +106,10 @@ class CreditType(RecordBase["CreditTypeManager"]): """Whether or not the credit is refundable.""" -class CreditTypeManager(NamedRecordManagerBase[CreditType]): +class CreditTypeManager( + RecordManagerBase[CreditType], + NamedRecordManagerMixin[CreditType], +): env_name = "openstack.credit.type" record_class = CreditType diff --git a/openstack_odooclient/managers/currency.py b/openstack_odooclient/managers/currency.py index b8d4d2e..2221252 100644 --- a/openstack_odooclient/managers/currency.py +++ b/openstack_odooclient/managers/currency.py @@ -18,11 +18,15 @@ from datetime import date as datetime_date from typing import Literal -from ..base.record import RecordBase -from ..base.record_manager_named import NamedRecordManagerBase +from ..base.record.base import RecordBase +from ..base.record_manager.base import RecordManagerBase +from ..mixins.named_record import NamedRecordManagerMixin, NamedRecordMixin -class Currency(RecordBase["CurrencyManager"]): +class Currency( + RecordBase["CurrencyManager"], + NamedRecordMixin["CurrencyManager"], +): active: bool """Whether or not this currency is active (enabled).""" @@ -42,9 +46,6 @@ class Currency(RecordBase["CurrencyManager"]): It is determined by the rounding factor (``rounding`` field). """ - name: str - """The ISO-4217 currency code for the currency.""" - position: Literal["before", "after"] """The position of the currency unit relative to the amount. @@ -64,6 +65,9 @@ class Currency(RecordBase["CurrencyManager"]): """The currency sign to be used when printing amounts.""" -class CurrencyManager(NamedRecordManagerBase[Currency]): +class CurrencyManager( + RecordManagerBase[Currency], + NamedRecordManagerMixin[Currency], +): env_name = "res.currency" record_class = Currency diff --git a/openstack_odooclient/managers/customer_group.py b/openstack_odooclient/managers/customer_group.py index 76e4aae..88f0cfb 100644 --- a/openstack_odooclient/managers/customer_group.py +++ b/openstack_odooclient/managers/customer_group.py @@ -17,14 +17,16 @@ from typing import Annotated -from ..base.record import ModelRef, RecordBase -from ..base.record_manager_named import NamedRecordManagerBase +from ..base.record.base import RecordBase +from ..base.record.types import ModelRef +from ..base.record_manager.base import RecordManagerBase +from ..mixins.named_record import NamedRecordManagerMixin, NamedRecordMixin -class CustomerGroup(RecordBase["CustomerGroupManager"]): - name: str - """The name of the customer group.""" - +class CustomerGroup( + RecordBase["CustomerGroupManager"], + NamedRecordMixin["CustomerGroupManager"], +): partner_ids: Annotated[list[int], ModelRef("partners", Partner)] """A list of IDs for the partners that are part of this customer group. @@ -55,7 +57,10 @@ class CustomerGroup(RecordBase["CustomerGroupManager"]): """ -class CustomerGroupManager(NamedRecordManagerBase[CustomerGroup]): +class CustomerGroupManager( + RecordManagerBase[CustomerGroup], + NamedRecordManagerMixin[CustomerGroup], +): env_name = "openstack.customer_group" record_class = CustomerGroup diff --git a/openstack_odooclient/managers/grant.py b/openstack_odooclient/managers/grant.py index 5b4d33c..aaca55c 100644 --- a/openstack_odooclient/managers/grant.py +++ b/openstack_odooclient/managers/grant.py @@ -18,8 +18,9 @@ from datetime import date from typing import Annotated -from ..base.record import ModelRef, RecordBase -from ..base.record_manager import RecordManagerBase +from ..base.record.base import RecordBase +from ..base.record.types import ModelRef +from ..base.record_manager.base import RecordManagerBase class Grant(RecordBase["GrantManager"]): diff --git a/openstack_odooclient/managers/grant_type.py b/openstack_odooclient/managers/grant_type.py index 641d922..805a985 100644 --- a/openstack_odooclient/managers/grant_type.py +++ b/openstack_odooclient/managers/grant_type.py @@ -17,11 +17,16 @@ from typing import Annotated -from ..base.record import ModelRef, RecordBase -from ..base.record_manager_named import NamedRecordManagerBase +from ..base.record.base import RecordBase +from ..base.record.types import ModelRef +from ..base.record_manager.base import RecordManagerBase +from ..mixins.named_record import NamedRecordManagerMixin, NamedRecordMixin -class GrantType(RecordBase["GrantTypeManager"]): +class GrantType( + RecordBase["GrantTypeManager"], + NamedRecordMixin["GrantTypeManager"], +): grant_ids: Annotated[list[int], ModelRef("grants", Grant)] """A list of IDs for the grants which are of this grant type.""" @@ -32,9 +37,6 @@ class GrantType(RecordBase["GrantTypeManager"]): and caches them for subsequent accesses. """ - name: str - """Name of the Grant Type.""" - only_for_product_ids: Annotated[ list[int], ModelRef("only_for_products", Product), @@ -112,7 +114,10 @@ class GrantType(RecordBase["GrantTypeManager"]): """ -class GrantTypeManager(NamedRecordManagerBase[GrantType]): +class GrantTypeManager( + RecordManagerBase[GrantType], + NamedRecordManagerMixin[GrantType], +): env_name = "openstack.grant.type" record_class = GrantType diff --git a/openstack_odooclient/managers/partner.py b/openstack_odooclient/managers/partner.py index 1fdd814..a8a76c8 100644 --- a/openstack_odooclient/managers/partner.py +++ b/openstack_odooclient/managers/partner.py @@ -19,8 +19,9 @@ from typing_extensions import Self -from ..base.record import ModelRef, RecordBase -from ..base.record_manager import RecordManagerBase +from ..base.record.base import RecordBase +from ..base.record.types import ModelRef +from ..base.record_manager.base import RecordManagerBase class Partner(RecordBase["PartnerManager"]): diff --git a/openstack_odooclient/managers/partner_category.py b/openstack_odooclient/managers/partner_category.py index 849a7bf..8c313b6 100644 --- a/openstack_odooclient/managers/partner_category.py +++ b/openstack_odooclient/managers/partner_category.py @@ -19,8 +19,9 @@ from typing_extensions import Self -from ..base.record import FieldAlias, ModelRef, RecordBase -from ..base.record_manager_named import NamedRecordManagerBase +from ..base.record.base import RecordBase +from ..base.record.types import FieldAlias, ModelRef +from ..base.record_manager.base import RecordManagerBase class PartnerCategory(RecordBase["PartnerCategoryManager"]): @@ -44,7 +45,10 @@ class PartnerCategory(RecordBase["PartnerCategoryManager"]): """Alias for ``color``.""" name: str - """The name of the partner category.""" + """The name of the partner category. + + Not guaranteed to be unique, even under the same parent category. + """ parent_id: Annotated[int | None, ModelRef("parent_id", Self)] """The ID for the parent partner category, if this category @@ -78,7 +82,7 @@ class PartnerCategory(RecordBase["PartnerCategoryManager"]): """ -class PartnerCategoryManager(NamedRecordManagerBase[PartnerCategory]): +class PartnerCategoryManager(RecordManagerBase[PartnerCategory]): env_name = "res.partner.category" record_class = PartnerCategory diff --git a/openstack_odooclient/managers/pricelist.py b/openstack_odooclient/managers/pricelist.py index bb8d2b1..d5c81b3 100644 --- a/openstack_odooclient/managers/pricelist.py +++ b/openstack_odooclient/managers/pricelist.py @@ -17,11 +17,16 @@ from typing import Annotated, Literal -from ..base.record import ModelRef, RecordBase -from ..base.record_manager_named import NamedRecordManagerBase +from ..base.record.base import RecordBase +from ..base.record.types import ModelRef +from ..base.record_manager.base import RecordManagerBase +from ..mixins.named_record import NamedRecordManagerMixin, NamedRecordMixin -class Pricelist(RecordBase["PricelistManager"]): +class Pricelist( + RecordBase["PricelistManager"], + NamedRecordMixin["PricelistManager"], +): active: bool """Whether or not the pricelist is active.""" @@ -60,9 +65,6 @@ class Pricelist(RecordBase["PricelistManager"]): * ``without_discount`` - Show public price & discount to the customer """ - name: str - """The name of this pricelist.""" - def get_price(self, product: int | Product, qty: float) -> float: """Get the price to charge for a given product and quantity. @@ -81,7 +83,10 @@ def get_price(self, product: int | Product, qty: float) -> float: ) -class PricelistManager(NamedRecordManagerBase[Pricelist]): +class PricelistManager( + RecordManagerBase[Pricelist], + NamedRecordManagerMixin[Pricelist], +): env_name = "product.pricelist" record_class = Pricelist diff --git a/openstack_odooclient/managers/product.py b/openstack_odooclient/managers/product.py index 032c6ed..c786b80 100644 --- a/openstack_odooclient/managers/product.py +++ b/openstack_odooclient/managers/product.py @@ -17,10 +17,9 @@ from typing import TYPE_CHECKING, Annotated, Any, Literal, overload -from ..base.record import ModelRef, RecordBase -from ..base.record_manager_with_unique_field import ( - RecordManagerWithUniqueFieldBase, -) +from ..base.record.base import RecordBase +from ..base.record.types import ModelRef +from ..base.record_manager.base import RecordManagerBase if TYPE_CHECKING: from collections.abc import Iterable @@ -103,7 +102,7 @@ class Product(RecordBase["ProductManager"]): """ -class ProductManager(RecordManagerWithUniqueFieldBase[Product, str]): +class ProductManager(RecordManagerBase[Product]): env_name = "product.product" record_class = Product @@ -349,7 +348,7 @@ def get_sellable_company_product_by_name( :return: Product (or ``None`` if record not found and optional) :rtype: Record | int | dict[str, Any] | None """ - return self._get_by_unique_field( + return self.get_by_unique_field( field="name", value=name, filters=[ diff --git a/openstack_odooclient/managers/product_category.py b/openstack_odooclient/managers/product_category.py index 029e990..d50db1f 100644 --- a/openstack_odooclient/managers/product_category.py +++ b/openstack_odooclient/managers/product_category.py @@ -19,8 +19,9 @@ from typing_extensions import Self -from ..base.record import FieldAlias, ModelRef, RecordBase -from ..base.record_manager_named import NamedRecordManagerBase +from ..base.record.base import RecordBase +from ..base.record.types import FieldAlias, ModelRef +from ..base.record_manager.base import RecordManagerBase class ProductCategory(RecordBase["ProductCategoryManager"]): @@ -41,7 +42,10 @@ class ProductCategory(RecordBase["ProductCategoryManager"]): """The complete product category tree.""" name: str - """Name of the product category.""" + """The name of the product category. + + Not guaranteed to be unique, even under the same parent category. + """ parent_id: Annotated[int | None, ModelRef("parent_id", Self)] """The ID for the parent product category, if this category @@ -68,6 +72,6 @@ class ProductCategory(RecordBase["ProductCategoryManager"]): """The number of products under this category.""" -class ProductCategoryManager(NamedRecordManagerBase[ProductCategory]): +class ProductCategoryManager(RecordManagerBase[ProductCategory]): env_name = "product.category" record_class = ProductCategory diff --git a/openstack_odooclient/managers/project.py b/openstack_odooclient/managers/project.py index 8a322fb..c4687d0 100644 --- a/openstack_odooclient/managers/project.py +++ b/openstack_odooclient/managers/project.py @@ -19,10 +19,9 @@ from typing_extensions import Self -from ..base.record import ModelRef, RecordBase -from ..base.record_manager_with_unique_field import ( - RecordManagerWithUniqueFieldBase, -) +from ..base.record.base import RecordBase +from ..base.record.types import ModelRef +from ..base.record_manager.base import RecordManagerBase if TYPE_CHECKING: from collections.abc import Iterable @@ -195,7 +194,7 @@ class Project(RecordBase["ProjectManager"]): """ -class ProjectManager(RecordManagerWithUniqueFieldBase[Project, str]): +class ProjectManager(RecordManagerBase[Project]): env_name = "openstack.project" record_class = Project @@ -339,7 +338,7 @@ def get_by_os_id( :return: Query result (or ``None`` if record not found and optional) :rtype: Project | int | dict[str, Any] | None """ - return self._get_by_unique_field( + return self.get_by_unique_field( field="os_id", value=os_id, fields=fields, diff --git a/openstack_odooclient/managers/project_contact.py b/openstack_odooclient/managers/project_contact.py index 056ece2..92397f1 100644 --- a/openstack_odooclient/managers/project_contact.py +++ b/openstack_odooclient/managers/project_contact.py @@ -17,8 +17,9 @@ from typing import Annotated, Literal -from ..base.record import ModelRef, RecordBase -from ..base.record_manager import RecordManagerBase +from ..base.record.base import RecordBase +from ..base.record.types import ModelRef +from ..base.record_manager.base import RecordManagerBase class ProjectContact(RecordBase["ProjectContactManager"]): diff --git a/openstack_odooclient/managers/referral_code.py b/openstack_odooclient/managers/referral_code.py index ea17dad..dc166e4 100644 --- a/openstack_odooclient/managers/referral_code.py +++ b/openstack_odooclient/managers/referral_code.py @@ -17,11 +17,16 @@ from typing import Annotated -from ..base.record import ModelRef, RecordBase -from ..base.record_manager_coded import CodedRecordManagerBase +from ..base.record.base import RecordBase +from ..base.record.types import ModelRef +from ..base.record_manager.base import RecordManagerBase +from ..mixins.coded_record import CodedRecordManagerMixin, CodedRecordMixin -class ReferralCode(RecordBase["ReferralCodeManager"]): +class ReferralCode( + RecordBase["ReferralCodeManager"], + CodedRecordMixin["ReferralCodeManager"], +): allowed_uses: int """The number of allowed uses of this referral code. @@ -33,9 +38,6 @@ class ReferralCode(RecordBase["ReferralCodeManager"]): before the reward credit is awarded to the referrer. """ - code: str - """The unique referral code.""" - name: str """Automatically generated name for the referral code.""" @@ -108,7 +110,10 @@ class ReferralCode(RecordBase["ReferralCodeManager"]): """ -class ReferralCodeManager(CodedRecordManagerBase[ReferralCode]): +class ReferralCodeManager( + RecordManagerBase[ReferralCode], + CodedRecordManagerMixin[ReferralCode], +): env_name = "openstack.referral_code" record_class = ReferralCode diff --git a/openstack_odooclient/managers/reseller.py b/openstack_odooclient/managers/reseller.py index 5eef66f..a067631 100644 --- a/openstack_odooclient/managers/reseller.py +++ b/openstack_odooclient/managers/reseller.py @@ -17,8 +17,9 @@ from typing import Annotated -from ..base.record import ModelRef, RecordBase -from ..base.record_manager import RecordManagerBase +from ..base.record.base import RecordBase +from ..base.record.types import ModelRef +from ..base.record_manager.base import RecordManagerBase class Reseller(RecordBase["ResellerManager"]): diff --git a/openstack_odooclient/managers/reseller_tier.py b/openstack_odooclient/managers/reseller_tier.py index 41f54f0..2c80a79 100644 --- a/openstack_odooclient/managers/reseller_tier.py +++ b/openstack_odooclient/managers/reseller_tier.py @@ -17,11 +17,16 @@ from typing import Annotated -from ..base.record import ModelRef, RecordBase -from ..base.record_manager_named import NamedRecordManagerBase +from ..base.record.base import RecordBase +from ..base.record.types import ModelRef +from ..base.record_manager.base import RecordManagerBase +from ..mixins.named_record import NamedRecordManagerMixin, NamedRecordMixin -class ResellerTier(RecordBase["ResellerTierManager"]): +class ResellerTier( + RecordBase["ResellerTierManager"], + NamedRecordMixin["ResellerTierManager"], +): discount_percent: float """The maximum discount percentage for this reseller tier (0-100).""" @@ -82,14 +87,14 @@ class ResellerTier(RecordBase["ResellerTierManager"]): under this tier. """ - name: str - """Reseller tier name.""" - min_usage_threshold: float """The minimum required usage amount for the reseller tier.""" -class ResellerTierManager(NamedRecordManagerBase[ResellerTier]): +class ResellerTierManager( + RecordManagerBase[ResellerTier], + NamedRecordManagerMixin[ResellerTier], +): env_name = "openstack.reseller.tier" record_class = ResellerTier diff --git a/openstack_odooclient/managers/sale_order.py b/openstack_odooclient/managers/sale_order.py index 15f1830..4838e09 100644 --- a/openstack_odooclient/managers/sale_order.py +++ b/openstack_odooclient/managers/sale_order.py @@ -18,11 +18,16 @@ from datetime import date, datetime from typing import Annotated, Literal -from ..base.record import FieldAlias, ModelRef, RecordBase -from ..base.record_manager_named import NamedRecordManagerBase +from ..base.record.base import RecordBase +from ..base.record.types import FieldAlias, ModelRef +from ..base.record_manager.base import RecordManagerBase +from ..mixins.named_record import NamedRecordManagerMixin, NamedRecordMixin -class SaleOrder(RecordBase["SaleOrderManager"]): +class SaleOrder( + RecordBase["SaleOrderManager"], + NamedRecordMixin["SaleOrderManager"], +): amount_untaxed: float """The untaxed total cost of the sale order.""" @@ -89,9 +94,6 @@ class SaleOrder(RecordBase["SaleOrderManager"]): * ``upselling`` - Upselling opportunity """ - name: str - """The name assigned to the sale order.""" - note: str """A note attached to the sale order. @@ -185,7 +187,10 @@ def create_invoices(self) -> None: self._env.create_invoices(self.id) -class SaleOrderManager(NamedRecordManagerBase[SaleOrder]): +class SaleOrderManager( + RecordManagerBase[SaleOrder], + NamedRecordManagerMixin[SaleOrder], +): env_name = "sale.order" record_class = SaleOrder diff --git a/openstack_odooclient/managers/sale_order_line.py b/openstack_odooclient/managers/sale_order_line.py index ade8a5a..57b7585 100644 --- a/openstack_odooclient/managers/sale_order_line.py +++ b/openstack_odooclient/managers/sale_order_line.py @@ -17,8 +17,9 @@ from typing import Annotated, Literal -from ..base.record import ModelRef, RecordBase -from ..base.record_manager import RecordManagerBase +from ..base.record.base import RecordBase +from ..base.record.types import ModelRef +from ..base.record_manager.base import RecordManagerBase class SaleOrderLine(RecordBase["SaleOrderLineManager"]): diff --git a/openstack_odooclient/managers/support_subscription.py b/openstack_odooclient/managers/support_subscription.py index dd58050..d7b488b 100644 --- a/openstack_odooclient/managers/support_subscription.py +++ b/openstack_odooclient/managers/support_subscription.py @@ -18,8 +18,9 @@ from datetime import date from typing import Annotated, Literal -from ..base.record import ModelRef, RecordBase -from ..base.record_manager import RecordManagerBase +from ..base.record.base import RecordBase +from ..base.record.types import ModelRef +from ..base.record_manager.base import RecordManagerBase class SupportSubscription(RecordBase["SupportSubscriptionManager"]): diff --git a/openstack_odooclient/managers/support_subscription_type.py b/openstack_odooclient/managers/support_subscription_type.py index d05b1a0..3ba2a6b 100644 --- a/openstack_odooclient/managers/support_subscription_type.py +++ b/openstack_odooclient/managers/support_subscription_type.py @@ -17,17 +17,19 @@ from typing import Annotated, Literal -from ..base.record import ModelRef, RecordBase -from ..base.record_manager_named import NamedRecordManagerBase +from ..base.record.base import RecordBase +from ..base.record.types import ModelRef +from ..base.record_manager.base import RecordManagerBase +from ..mixins.named_record import NamedRecordManagerMixin, NamedRecordMixin -class SupportSubscriptionType(RecordBase["SupportSubscriptionTypeManager"]): +class SupportSubscriptionType( + RecordBase["SupportSubscriptionTypeManager"], + NamedRecordMixin["SupportSubscriptionTypeManager"], +): billing_type: Literal["paid", "complimentary"] """The type of support subscription.""" - name: str - """The name of the support subscription type.""" - product_id: Annotated[int, ModelRef("product", Product)] """The ID for the product to use to invoice the support subscription. @@ -73,7 +75,8 @@ class SupportSubscriptionType(RecordBase["SupportSubscriptionTypeManager"]): class SupportSubscriptionTypeManager( - NamedRecordManagerBase[SupportSubscriptionType], + RecordManagerBase[SupportSubscriptionType], + NamedRecordManagerMixin[SupportSubscriptionType], ): env_name = "openstack.support_subscription.type" record_class = SupportSubscriptionType diff --git a/openstack_odooclient/managers/tax.py b/openstack_odooclient/managers/tax.py index 03c5fdc..1991e28 100644 --- a/openstack_odooclient/managers/tax.py +++ b/openstack_odooclient/managers/tax.py @@ -17,11 +17,13 @@ from typing import Annotated, Literal -from ..base.record import ModelRef, RecordBase -from ..base.record_manager_named import NamedRecordManagerBase +from ..base.record.base import RecordBase +from ..base.record.types import ModelRef +from ..base.record_manager.base import RecordManagerBase +from ..mixins.named_record import NamedRecordManagerMixin, NamedRecordMixin -class Tax(RecordBase["TaxManager"]): +class Tax(RecordBase["TaxManager"], NamedRecordMixin["TaxManager"]): active: bool """Whether or not this tax is active (enabled).""" @@ -68,9 +70,6 @@ class Tax(RecordBase["TaxManager"]): based on the price with this tax included. """ - name: str - """Name of the tax.""" - price_include: bool """Whether or not prices included in invoices should include this tax.""" @@ -97,7 +96,7 @@ class Tax(RecordBase["TaxManager"]): """ -class TaxManager(NamedRecordManagerBase[Tax]): +class TaxManager(RecordManagerBase[Tax], NamedRecordManagerMixin[Tax]): env_name = "account.tax" record_class = Tax diff --git a/openstack_odooclient/managers/tax_group.py b/openstack_odooclient/managers/tax_group.py index 1691fe0..25ea726 100644 --- a/openstack_odooclient/managers/tax_group.py +++ b/openstack_odooclient/managers/tax_group.py @@ -15,15 +15,21 @@ from __future__ import annotations -from ..base.record import RecordBase -from ..base.record_manager_named import NamedRecordManagerBase +from ..base.record.base import RecordBase +from ..base.record_manager.base import RecordManagerBase +from ..mixins.named_record import NamedRecordManagerMixin, NamedRecordMixin -class TaxGroup(RecordBase["TaxGroupManager"]): - name: str - """Name of the tax group.""" +class TaxGroup( + RecordBase["TaxGroupManager"], + NamedRecordMixin["TaxGroupManager"], +): + pass -class TaxGroupManager(NamedRecordManagerBase[TaxGroup]): +class TaxGroupManager( + RecordManagerBase[TaxGroup], + NamedRecordManagerMixin[TaxGroup], +): env_name = "account.tax.group" record_class = TaxGroup diff --git a/openstack_odooclient/managers/term_discount.py b/openstack_odooclient/managers/term_discount.py index 6117925..a7119f3 100644 --- a/openstack_odooclient/managers/term_discount.py +++ b/openstack_odooclient/managers/term_discount.py @@ -20,8 +20,9 @@ from typing_extensions import Self -from ..base.record import ModelRef, RecordBase -from ..base.record_manager import RecordManagerBase +from ..base.record.base import RecordBase +from ..base.record.types import ModelRef +from ..base.record_manager.base import RecordManagerBase class TermDiscount(RecordBase["TermDiscountManager"]): diff --git a/openstack_odooclient/managers/trial.py b/openstack_odooclient/managers/trial.py index 3cfb1b6..08d36de 100644 --- a/openstack_odooclient/managers/trial.py +++ b/openstack_odooclient/managers/trial.py @@ -18,8 +18,9 @@ from datetime import date from typing import Annotated, Literal -from ..base.record import ModelRef, RecordBase -from ..base.record_manager import RecordManagerBase +from ..base.record.base import RecordBase +from ..base.record.types import ModelRef +from ..base.record_manager.base import RecordManagerBase class Trial(RecordBase["TrialManager"]): diff --git a/openstack_odooclient/managers/uom.py b/openstack_odooclient/managers/uom.py index 8e42a7e..05fb258 100644 --- a/openstack_odooclient/managers/uom.py +++ b/openstack_odooclient/managers/uom.py @@ -17,8 +17,9 @@ from typing import Annotated, Literal -from ..base.record import ModelRef, RecordBase -from ..base.record_manager import RecordManagerBase +from ..base.record.base import RecordBase +from ..base.record.types import ModelRef +from ..base.record_manager.base import RecordManagerBase class Uom(RecordBase["UomManager"]): diff --git a/openstack_odooclient/managers/uom_category.py b/openstack_odooclient/managers/uom_category.py index 963327c..2b72747 100644 --- a/openstack_odooclient/managers/uom_category.py +++ b/openstack_odooclient/managers/uom_category.py @@ -17,8 +17,8 @@ from typing import Literal -from ..base.record import RecordBase -from ..base.record_manager import RecordManagerBase +from ..base.record.base import RecordBase +from ..base.record_manager.base import RecordManagerBase class UomCategory(RecordBase["UomCategoryManager"]): diff --git a/openstack_odooclient/managers/user.py b/openstack_odooclient/managers/user.py index 934562c..a0927d5 100644 --- a/openstack_odooclient/managers/user.py +++ b/openstack_odooclient/managers/user.py @@ -17,8 +17,9 @@ from typing import Annotated -from ..base.record import ModelRef, RecordBase -from ..base.record_manager import RecordManagerBase +from ..base.record.base import RecordBase +from ..base.record.types import ModelRef +from ..base.record_manager.base import RecordManagerBase class User(RecordBase["UserManager"]): diff --git a/openstack_odooclient/managers/volume_discount_range.py b/openstack_odooclient/managers/volume_discount_range.py index f6d68b6..baae6e4 100644 --- a/openstack_odooclient/managers/volume_discount_range.py +++ b/openstack_odooclient/managers/volume_discount_range.py @@ -17,8 +17,9 @@ from typing import Annotated -from ..base.record import ModelRef, RecordBase -from ..base.record_manager import RecordManagerBase +from ..base.record.base import RecordBase +from ..base.record.types import ModelRef +from ..base.record_manager.base import RecordManagerBase class VolumeDiscountRange(RecordBase["VolumeDiscountRangeManager"]): diff --git a/openstack_odooclient/managers/voucher_code.py b/openstack_odooclient/managers/voucher_code.py index 8db78f4..8f78a8e 100644 --- a/openstack_odooclient/managers/voucher_code.py +++ b/openstack_odooclient/managers/voucher_code.py @@ -18,17 +18,19 @@ from datetime import date from typing import Annotated, Literal -from ..base.record import ModelRef, RecordBase -from ..base.record_manager_named import NamedRecordManagerBase +from ..base.record.base import RecordBase +from ..base.record.types import ModelRef +from ..base.record_manager.base import RecordManagerBase +from ..mixins.coded_record import CodedRecordManagerMixin, CodedRecordMixin -class VoucherCode(RecordBase["VoucherCodeManager"]): +class VoucherCode( + RecordBase["VoucherCodeManager"], + CodedRecordMixin["VoucherCodeManager"], +): claimed: bool """Whether or not this voucher code has been claimed.""" - code: str - """The code string for this voucher code.""" - credit_amount: float | Literal[False] """The initial credit balance for the voucher code, if a credit is to be created by the voucher code. @@ -130,7 +132,7 @@ class VoucherCode(RecordBase["VoucherCodeManager"]): """ name: str - """The unique name of this voucher code. + """The automatically generated name of this voucher code. This uses the code specified in the record as-is. """ @@ -183,7 +185,10 @@ class VoucherCode(RecordBase["VoucherCodeManager"]): """ -class VoucherCodeManager(NamedRecordManagerBase[VoucherCode]): +class VoucherCodeManager( + RecordManagerBase[VoucherCode], + CodedRecordManagerMixin[VoucherCode], +): env_name = "openstack.voucher_code" record_class = VoucherCode diff --git a/openstack_odooclient/mixins/__init__.py b/openstack_odooclient/mixins/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/openstack_odooclient/base/record_manager_coded.py b/openstack_odooclient/mixins/coded_record.py similarity index 79% rename from openstack_odooclient/base/record_manager_coded.py rename to openstack_odooclient/mixins/coded_record.py index 91e3420..ca95c6a 100644 --- a/openstack_odooclient/base/record_manager_coded.py +++ b/openstack_odooclient/mixins/coded_record.py @@ -1,4 +1,4 @@ -# Copyright (C) 2024 Catalyst Cloud Limited +# Copyright (C) 2025 Catalyst Cloud Limited # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,32 +15,35 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Literal, overload +from typing import TYPE_CHECKING, Any, Generic, Literal, overload -from .record_manager_with_unique_field import ( - Record, - RecordManagerWithUniqueFieldBase, -) +from ..base.record.base import RM, RecordProtocol +from ..base.record_manager.base import R, RecordManagerProtocol if TYPE_CHECKING: from collections.abc import Iterable -class CodedRecordManagerBase(RecordManagerWithUniqueFieldBase[Record, str]): - """A record manager base class for record types with a code field. +class CodedRecordMixin(RecordProtocol[RM], Generic[RM]): + """A record mixin for record types with a ``code`` field. - This code field is reasonably expected to be unique, which allows - methods for getting records by code to be defined. - - The record class should be type hinted with the field to use as the code, - just like any other field. - Configure the name of the code field on the manager class by defining - the ``code_field`` attribute (set to ``code`` by default). + Include this mixin to add the ``code`` field, with ``str`` type, + to your custom record class. This allows the additional methods + provided by ``CodedRecordManagerMixin`` to be used. """ - code_field: str = "code" - """The field code to use when querying by code in - the ``get_by_code`` method. + code: str + """The unique code for this record.""" + + +class CodedRecordManagerMixin(RecordManagerProtocol[R], Generic[R]): + """A record manager mixin for record types with a ``code`` field. + + The ``code`` field is expected to be unique, which allows + methods for getting records by code to be defined. + + The record class must either include the ``CodedRecordMixin`` mixin, + or have its own ``code`` field defined with the ``str`` type. """ @overload @@ -118,7 +121,7 @@ def get_by_code( as_id: Literal[False] = ..., as_dict: Literal[False] = ..., optional: Literal[True], - ) -> Record | None: ... + ) -> R | None: ... @overload def get_by_code( @@ -129,7 +132,7 @@ def get_by_code( as_id: Literal[False] = ..., as_dict: Literal[False] = ..., optional: Literal[False] = ..., - ) -> Record: ... + ) -> R: ... @overload def get_by_code( @@ -140,7 +143,7 @@ def get_by_code( as_id: bool = ..., as_dict: bool = ..., optional: bool = ..., - ) -> Record | int | dict[str, Any] | None: ... + ) -> R | int | dict[str, Any] | None: ... def get_by_code( self, @@ -149,7 +152,7 @@ def get_by_code( as_id: bool = False, as_dict: bool = False, optional: bool = False, - ) -> Record | int | dict[str, Any] | None: + ) -> R | int | dict[str, Any] | None: """Query a unique record by code. A number of parameters are available to configure the return type, @@ -181,10 +184,10 @@ def get_by_code( :raises MultipleRecordsFoundError: Multiple records with the same code :raises RecordNotFoundError: Record with the given code not found :return: Query result (or ``None`` if record not found and optional) - :rtype: Record | int | dict[str, Any] | None + :rtype: R | int | dict[str, Any] | None """ - return self._get_by_unique_field( - field=self.code_field, + return self.get_by_unique_field( + field="code", value=code, fields=fields, as_id=as_id, diff --git a/openstack_odooclient/base/record_manager_named.py b/openstack_odooclient/mixins/named_record.py similarity index 80% rename from openstack_odooclient/base/record_manager_named.py rename to openstack_odooclient/mixins/named_record.py index d8d1379..c2faafa 100644 --- a/openstack_odooclient/base/record_manager_named.py +++ b/openstack_odooclient/mixins/named_record.py @@ -1,4 +1,4 @@ -# Copyright (C) 2024 Catalyst Cloud Limited +# Copyright (C) 2025 Catalyst Cloud Limited # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,32 +15,35 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Literal, overload +from typing import TYPE_CHECKING, Any, Generic, Literal, overload -from .record_manager_with_unique_field import ( - Record, - RecordManagerWithUniqueFieldBase, -) +from ..base.record.base import RM, RecordProtocol +from ..base.record_manager.base import R, RecordManagerProtocol if TYPE_CHECKING: from collections.abc import Iterable -class NamedRecordManagerBase(RecordManagerWithUniqueFieldBase[Record, str]): - """A record manager base class for record types with a name field. +class NamedRecordMixin(RecordProtocol[RM], Generic[RM]): + """A record mixin for record types with a ``name`` field. - This name field is reasonably expected to be unique, which allows - methods for getting records by name to be defined. - - The record class should be type hinted with the field to use as the name, - just like any other field. - Configure the name of the name field on the manager class by defining - the ``name_field`` attribute (set to ``name`` by default). + Include this mixin to add the ``name`` field, with ``str`` type, + to your custom record class. This allows the additional methods + provided by ``NamedRecordManagerMixin`` to be used. """ - name_field: str = "name" - """The field name to use when querying by name in - the ``get_by_name`` method. + name: str + """The unique name of the record.""" + + +class NamedRecordManagerMixin(RecordManagerProtocol[R], Generic[R]): + """A record manager mixin for record types with a ``name`` field. + + The ``name`` field is expected to be unique, which allows + methods for getting records by name to be defined. + + The record class must either include the ``NamedRecordMixin`` mixin, + or have its own ``name`` field defined with the ``str`` type. """ @overload @@ -118,7 +121,7 @@ def get_by_name( as_id: Literal[False] = ..., as_dict: Literal[False] = ..., optional: Literal[True], - ) -> Record | None: ... + ) -> R | None: ... @overload def get_by_name( @@ -129,7 +132,7 @@ def get_by_name( as_id: Literal[False] = ..., as_dict: Literal[False] = ..., optional: Literal[False] = ..., - ) -> Record: ... + ) -> R: ... @overload def get_by_name( @@ -140,7 +143,7 @@ def get_by_name( as_id: bool = ..., as_dict: bool = ..., optional: bool = ..., - ) -> Record | int | dict[str, Any] | None: ... + ) -> R | int | dict[str, Any] | None: ... def get_by_name( self, @@ -149,7 +152,7 @@ def get_by_name( as_id: bool = False, as_dict: bool = False, optional: bool = False, - ) -> Record | int | dict[str, Any] | None: + ) -> R | int | dict[str, Any] | None: """Query a unique record by name. A number of parameters are available to configure the return type, @@ -181,10 +184,10 @@ def get_by_name( :raises MultipleRecordsFoundError: Multiple records with the same name :raises RecordNotFoundError: Record with the given name not found :return: Query result (or ``None`` if record not found and optional) - :rtype: Record | int | dict[str, Any] | None + :rtype: R | int | dict[str, Any] | None """ - return self._get_by_unique_field( - field=self.name_field, + return self.get_by_unique_field( + field="name", value=name, fields=fields, as_id=as_id, diff --git a/pyproject.toml b/pyproject.toml index d8a9394..2bff68d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,12 +40,15 @@ dynamic = ["version"] [dependency-groups] dev = [ - "mike>=2.1.2", - "mkdocs-material>=9.5.27", - "mypy==1.18.2", - "poethepoet>=0.37.0", - "ruff==0.14.0", - "towncrier>=23.11.0", + "mike>=2.1.3", + "mkdocs-material>=9.7.0", + "mypy==1.19.0", + "poethepoet>=0.38.0", + "pytest>=9.0.2", + "pytest-cov>=7.0.0", + "pytest-mock>=3.15.1", + "ruff==0.14.9", + "towncrier>=25.8.0", ] [project.urls] @@ -64,6 +67,8 @@ packages = ["openstack_odooclient"] lint = "ruff check" format = "ruff format" type-check = "mypy openstack_odooclient" +test = "pytest" +unit-test = "pytest tests/unit" [tool.ruff] fix = true @@ -132,6 +137,22 @@ required-imports = [ [tool.mypy] pretty = true +[tool.pytest.ini_options] +minversion = "6.0" +addopts = "--color=yes --cov=openstack_odooclient --cov-config=pyproject.toml --cov-report=term --cov-report=html:cover --cov-report=xml:cover/coverage.xml --junit-xml=rspec.xml --log-level=DEBUG -r A --showlocals --verbosity=3" +testpaths = [ + "tests/unit", +] + +[tool.coverage.run] +branch = true + +[tool.coverage.report] +exclude_also = [ + "if TYPE_CHECKING:", +] +skip_empty = true + [tool.towncrier] directory = "changelog.d" filename = "docs/changelog.md" diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/test_import.py b/tests/unit/test_import.py new file mode 100644 index 0000000..2d5257a --- /dev/null +++ b/tests/unit/test_import.py @@ -0,0 +1,33 @@ +# Copyright (C) 2025 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + + +def test_import(): + """Test that the openstack_odooclient package can be imported. + + Believe it or not, this is an actual test of things. + The ``openstack_odooclient`` package has circular imports + in many places, to add type annotations for classes that + reference each other that must be available at runtime + so the library can parse the annotations. + + Importing the package as an individual test is a sanity check + to make sure that mistakes made in the dependency chain + do not result in the package being unable to be imported. + """ + + import openstack_odooclient # noqa: F401, PLC0415 diff --git a/uv.lock b/uv.lock index 607fc0b..126ae6b 100644 --- a/uv.lock +++ b/uv.lock @@ -13,101 +13,126 @@ wheels = [ [[package]] name = "backrefs" -version = "5.9" +version = "6.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/eb/a7/312f673df6a79003279e1f55619abbe7daebbb87c17c976ddc0345c04c7b/backrefs-5.9.tar.gz", hash = "sha256:808548cb708d66b82ee231f962cb36faaf4f2baab032f2fbb783e9c2fdddaa59", size = 5765857, upload-time = "2025-06-22T19:34:13.97Z" } +sdist = { url = "https://files.pythonhosted.org/packages/86/e3/bb3a439d5cb255c4774724810ad8073830fac9c9dee123555820c1bcc806/backrefs-6.1.tar.gz", hash = "sha256:3bba1749aafe1db9b915f00e0dd166cba613b6f788ffd63060ac3485dc9be231", size = 7011962, upload-time = "2025-11-15T14:52:08.323Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/19/4d/798dc1f30468134906575156c089c492cf79b5a5fd373f07fe26c4d046bf/backrefs-5.9-py310-none-any.whl", hash = "sha256:db8e8ba0e9de81fcd635f440deab5ae5f2591b54ac1ebe0550a2ca063488cd9f", size = 380267, upload-time = "2025-06-22T19:34:05.252Z" }, - { url = "https://files.pythonhosted.org/packages/55/07/f0b3375bf0d06014e9787797e6b7cc02b38ac9ff9726ccfe834d94e9991e/backrefs-5.9-py311-none-any.whl", hash = "sha256:6907635edebbe9b2dc3de3a2befff44d74f30a4562adbb8b36f21252ea19c5cf", size = 392072, upload-time = "2025-06-22T19:34:06.743Z" }, - { url = "https://files.pythonhosted.org/packages/9d/12/4f345407259dd60a0997107758ba3f221cf89a9b5a0f8ed5b961aef97253/backrefs-5.9-py312-none-any.whl", hash = "sha256:7fdf9771f63e6028d7fee7e0c497c81abda597ea45d6b8f89e8ad76994f5befa", size = 397947, upload-time = "2025-06-22T19:34:08.172Z" }, - { url = "https://files.pythonhosted.org/packages/10/bf/fa31834dc27a7f05e5290eae47c82690edc3a7b37d58f7fb35a1bdbf355b/backrefs-5.9-py313-none-any.whl", hash = "sha256:cc37b19fa219e93ff825ed1fed8879e47b4d89aa7a1884860e2db64ccd7c676b", size = 399843, upload-time = "2025-06-22T19:34:09.68Z" }, - { url = "https://files.pythonhosted.org/packages/fc/24/b29af34b2c9c41645a9f4ff117bae860291780d73880f449e0b5d948c070/backrefs-5.9-py314-none-any.whl", hash = "sha256:df5e169836cc8acb5e440ebae9aad4bf9d15e226d3bad049cf3f6a5c20cc8dc9", size = 411762, upload-time = "2025-06-22T19:34:11.037Z" }, - { url = "https://files.pythonhosted.org/packages/41/ff/392bff89415399a979be4a65357a41d92729ae8580a66073d8ec8d810f98/backrefs-5.9-py39-none-any.whl", hash = "sha256:f48ee18f6252b8f5777a22a00a09a85de0ca931658f1dd96d4406a34f3748c60", size = 380265, upload-time = "2025-06-22T19:34:12.405Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ee/c216d52f58ea75b5e1841022bbae24438b19834a29b163cb32aa3a2a7c6e/backrefs-6.1-py310-none-any.whl", hash = "sha256:2a2ccb96302337ce61ee4717ceacfbf26ba4efb1d55af86564b8bbaeda39cac1", size = 381059, upload-time = "2025-11-15T14:51:59.758Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9a/8da246d988ded941da96c7ed945d63e94a445637eaad985a0ed88787cb89/backrefs-6.1-py311-none-any.whl", hash = "sha256:e82bba3875ee4430f4de4b6db19429a27275d95a5f3773c57e9e18abc23fd2b7", size = 392854, upload-time = "2025-11-15T14:52:01.194Z" }, + { url = "https://files.pythonhosted.org/packages/37/c9/fd117a6f9300c62bbc33bc337fd2b3c6bfe28b6e9701de336b52d7a797ad/backrefs-6.1-py312-none-any.whl", hash = "sha256:c64698c8d2269343d88947c0735cb4b78745bd3ba590e10313fbf3f78c34da5a", size = 398770, upload-time = "2025-11-15T14:52:02.584Z" }, + { url = "https://files.pythonhosted.org/packages/eb/95/7118e935b0b0bd3f94dfec2d852fd4e4f4f9757bdb49850519acd245cd3a/backrefs-6.1-py313-none-any.whl", hash = "sha256:4c9d3dc1e2e558965202c012304f33d4e0e477e1c103663fd2c3cc9bb18b0d05", size = 400726, upload-time = "2025-11-15T14:52:04.093Z" }, + { url = "https://files.pythonhosted.org/packages/1d/72/6296bad135bfafd3254ae3648cd152980a424bd6fed64a101af00cc7ba31/backrefs-6.1-py314-none-any.whl", hash = "sha256:13eafbc9ccd5222e9c1f0bec563e6d2a6d21514962f11e7fc79872fd56cbc853", size = 412584, upload-time = "2025-11-15T14:52:05.233Z" }, + { url = "https://files.pythonhosted.org/packages/02/e3/a4fa1946722c4c7b063cc25043a12d9ce9b4323777f89643be74cef2993c/backrefs-6.1-py39-none-any.whl", hash = "sha256:a9e99b8a4867852cad177a6430e31b0f6e495d65f8c6c134b68c14c3c95bf4b0", size = 381058, upload-time = "2025-11-15T14:52:06.698Z" }, ] [[package]] name = "certifi" -version = "2025.10.5" +version = "2025.11.12" 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" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" }, ] [[package]] name = "charset-normalizer" -version = "3.4.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d6/98/f3b8013223728a99b908c9344da3aa04ee6e3fa235f19409033eda92fb78/charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72", size = 207695, upload-time = "2025-08-09T07:55:36.452Z" }, - { url = "https://files.pythonhosted.org/packages/21/40/5188be1e3118c82dcb7c2a5ba101b783822cfb413a0268ed3be0468532de/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe", size = 147153, upload-time = "2025-08-09T07:55:38.467Z" }, - { url = "https://files.pythonhosted.org/packages/37/60/5d0d74bc1e1380f0b72c327948d9c2aca14b46a9efd87604e724260f384c/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601", size = 160428, upload-time = "2025-08-09T07:55:40.072Z" }, - { url = "https://files.pythonhosted.org/packages/85/9a/d891f63722d9158688de58d050c59dc3da560ea7f04f4c53e769de5140f5/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c", size = 157627, upload-time = "2025-08-09T07:55:41.706Z" }, - { url = "https://files.pythonhosted.org/packages/65/1a/7425c952944a6521a9cfa7e675343f83fd82085b8af2b1373a2409c683dc/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2", size = 152388, upload-time = "2025-08-09T07:55:43.262Z" }, - { url = "https://files.pythonhosted.org/packages/f0/c9/a2c9c2a355a8594ce2446085e2ec97fd44d323c684ff32042e2a6b718e1d/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0", size = 150077, upload-time = "2025-08-09T07:55:44.903Z" }, - { url = "https://files.pythonhosted.org/packages/3b/38/20a1f44e4851aa1c9105d6e7110c9d020e093dfa5836d712a5f074a12bf7/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0", size = 161631, upload-time = "2025-08-09T07:55:46.346Z" }, - { url = "https://files.pythonhosted.org/packages/a4/fa/384d2c0f57edad03d7bec3ebefb462090d8905b4ff5a2d2525f3bb711fac/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0", size = 159210, upload-time = "2025-08-09T07:55:47.539Z" }, - { url = "https://files.pythonhosted.org/packages/33/9e/eca49d35867ca2db336b6ca27617deed4653b97ebf45dfc21311ce473c37/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a", size = 153739, upload-time = "2025-08-09T07:55:48.744Z" }, - { url = "https://files.pythonhosted.org/packages/2a/91/26c3036e62dfe8de8061182d33be5025e2424002125c9500faff74a6735e/charset_normalizer-3.4.3-cp310-cp310-win32.whl", hash = "sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f", size = 99825, upload-time = "2025-08-09T07:55:50.305Z" }, - { url = "https://files.pythonhosted.org/packages/e2/c6/f05db471f81af1fa01839d44ae2a8bfeec8d2a8b4590f16c4e7393afd323/charset_normalizer-3.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669", size = 107452, upload-time = "2025-08-09T07:55:51.461Z" }, - { url = "https://files.pythonhosted.org/packages/7f/b5/991245018615474a60965a7c9cd2b4efbaabd16d582a5547c47ee1c7730b/charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b", size = 204483, upload-time = "2025-08-09T07:55:53.12Z" }, - { url = "https://files.pythonhosted.org/packages/c7/2a/ae245c41c06299ec18262825c1569c5d3298fc920e4ddf56ab011b417efd/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64", size = 145520, upload-time = "2025-08-09T07:55:54.712Z" }, - { url = "https://files.pythonhosted.org/packages/3a/a4/b3b6c76e7a635748c4421d2b92c7b8f90a432f98bda5082049af37ffc8e3/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91", size = 158876, upload-time = "2025-08-09T07:55:56.024Z" }, - { url = "https://files.pythonhosted.org/packages/e2/e6/63bb0e10f90a8243c5def74b5b105b3bbbfb3e7bb753915fe333fb0c11ea/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f", size = 156083, upload-time = "2025-08-09T07:55:57.582Z" }, - { url = "https://files.pythonhosted.org/packages/87/df/b7737ff046c974b183ea9aa111b74185ac8c3a326c6262d413bd5a1b8c69/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07", size = 150295, upload-time = "2025-08-09T07:55:59.147Z" }, - { url = "https://files.pythonhosted.org/packages/61/f1/190d9977e0084d3f1dc169acd060d479bbbc71b90bf3e7bf7b9927dec3eb/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30", size = 148379, upload-time = "2025-08-09T07:56:00.364Z" }, - { url = "https://files.pythonhosted.org/packages/4c/92/27dbe365d34c68cfe0ca76f1edd70e8705d82b378cb54ebbaeabc2e3029d/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14", size = 160018, upload-time = "2025-08-09T07:56:01.678Z" }, - { url = "https://files.pythonhosted.org/packages/99/04/baae2a1ea1893a01635d475b9261c889a18fd48393634b6270827869fa34/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c", size = 157430, upload-time = "2025-08-09T07:56:02.87Z" }, - { url = "https://files.pythonhosted.org/packages/2f/36/77da9c6a328c54d17b960c89eccacfab8271fdaaa228305330915b88afa9/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae", size = 151600, upload-time = "2025-08-09T07:56:04.089Z" }, - { url = "https://files.pythonhosted.org/packages/64/d4/9eb4ff2c167edbbf08cdd28e19078bf195762e9bd63371689cab5ecd3d0d/charset_normalizer-3.4.3-cp311-cp311-win32.whl", hash = "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849", size = 99616, upload-time = "2025-08-09T07:56:05.658Z" }, - { url = "https://files.pythonhosted.org/packages/f4/9c/996a4a028222e7761a96634d1820de8a744ff4327a00ada9c8942033089b/charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c", size = 107108, upload-time = "2025-08-09T07:56:07.176Z" }, - { url = "https://files.pythonhosted.org/packages/e9/5e/14c94999e418d9b87682734589404a25854d5f5d0408df68bc15b6ff54bb/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", size = 205655, upload-time = "2025-08-09T07:56:08.475Z" }, - { url = "https://files.pythonhosted.org/packages/7d/a8/c6ec5d389672521f644505a257f50544c074cf5fc292d5390331cd6fc9c3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", size = 146223, upload-time = "2025-08-09T07:56:09.708Z" }, - { url = "https://files.pythonhosted.org/packages/fc/eb/a2ffb08547f4e1e5415fb69eb7db25932c52a52bed371429648db4d84fb1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018", size = 159366, upload-time = "2025-08-09T07:56:11.326Z" }, - { url = "https://files.pythonhosted.org/packages/82/10/0fd19f20c624b278dddaf83b8464dcddc2456cb4b02bb902a6da126b87a1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392", size = 157104, upload-time = "2025-08-09T07:56:13.014Z" }, - { url = "https://files.pythonhosted.org/packages/16/ab/0233c3231af734f5dfcf0844aa9582d5a1466c985bbed6cedab85af9bfe3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f", size = 151830, upload-time = "2025-08-09T07:56:14.428Z" }, - { url = "https://files.pythonhosted.org/packages/ae/02/e29e22b4e02839a0e4a06557b1999d0a47db3567e82989b5bb21f3fbbd9f/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154", size = 148854, upload-time = "2025-08-09T07:56:16.051Z" }, - { url = "https://files.pythonhosted.org/packages/05/6b/e2539a0a4be302b481e8cafb5af8792da8093b486885a1ae4d15d452bcec/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491", size = 160670, upload-time = "2025-08-09T07:56:17.314Z" }, - { url = "https://files.pythonhosted.org/packages/31/e7/883ee5676a2ef217a40ce0bffcc3d0dfbf9e64cbcfbdf822c52981c3304b/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93", size = 158501, upload-time = "2025-08-09T07:56:18.641Z" }, - { url = "https://files.pythonhosted.org/packages/c1/35/6525b21aa0db614cf8b5792d232021dca3df7f90a1944db934efa5d20bb1/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f", size = 153173, upload-time = "2025-08-09T07:56:20.289Z" }, - { url = "https://files.pythonhosted.org/packages/50/ee/f4704bad8201de513fdc8aac1cabc87e38c5818c93857140e06e772b5892/charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37", size = 99822, upload-time = "2025-08-09T07:56:21.551Z" }, - { url = "https://files.pythonhosted.org/packages/39/f5/3b3836ca6064d0992c58c7561c6b6eee1b3892e9665d650c803bd5614522/charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc", size = 107543, upload-time = "2025-08-09T07:56:23.115Z" }, - { url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326, upload-time = "2025-08-09T07:56:24.721Z" }, - { url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008, upload-time = "2025-08-09T07:56:26.004Z" }, - { url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196, upload-time = "2025-08-09T07:56:27.25Z" }, - { url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", size = 156819, upload-time = "2025-08-09T07:56:28.515Z" }, - { url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", size = 151350, upload-time = "2025-08-09T07:56:29.716Z" }, - { url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", size = 148644, upload-time = "2025-08-09T07:56:30.984Z" }, - { url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", size = 160468, upload-time = "2025-08-09T07:56:32.252Z" }, - { url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", size = 158187, upload-time = "2025-08-09T07:56:33.481Z" }, - { url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699, upload-time = "2025-08-09T07:56:34.739Z" }, - { url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580, upload-time = "2025-08-09T07:56:35.981Z" }, - { url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366, upload-time = "2025-08-09T07:56:37.339Z" }, - { url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342, upload-time = "2025-08-09T07:56:38.687Z" }, - { url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995, upload-time = "2025-08-09T07:56:40.048Z" }, - { url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640, upload-time = "2025-08-09T07:56:41.311Z" }, - { url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636, upload-time = "2025-08-09T07:56:43.195Z" }, - { url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939, upload-time = "2025-08-09T07:56:44.819Z" }, - { url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580, upload-time = "2025-08-09T07:56:46.684Z" }, - { url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870, upload-time = "2025-08-09T07:56:47.941Z" }, - { url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797, upload-time = "2025-08-09T07:56:49.756Z" }, - { url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224, upload-time = "2025-08-09T07:56:51.369Z" }, - { url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086, upload-time = "2025-08-09T07:56:52.722Z" }, - { url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400, upload-time = "2025-08-09T07:56:55.172Z" }, - { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" }, +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 = "click" -version = "8.3.0" +version = "8.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943, upload-time = "2025-09-18T17:32:23.696Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" }, + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, ] [[package]] @@ -119,6 +144,122 @@ 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.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/45/2c665ca77ec32ad67e25c77daf1cee28ee4558f3bc571cdbaf88a00b9f23/coverage-7.13.0.tar.gz", hash = "sha256:a394aa27f2d7ff9bc04cf703817773a59ad6dfbd577032e690f961d2460ee936", size = 820905, upload-time = "2025-12-08T13:14:38.055Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/08/bdd7ccca14096f7eb01412b87ac11e5d16e4cb54b6e328afc9dee8bdaec1/coverage-7.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:02d9fb9eccd48f6843c98a37bd6817462f130b86da8660461e8f5e54d4c06070", size = 217979, upload-time = "2025-12-08T13:12:14.505Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f0/d1302e3416298a28b5663ae1117546a745d9d19fde7e28402b2c5c3e2109/coverage-7.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:367449cf07d33dc216c083f2036bb7d976c6e4903ab31be400ad74ad9f85ce98", size = 218496, upload-time = "2025-12-08T13:12:16.237Z" }, + { url = "https://files.pythonhosted.org/packages/07/26/d36c354c8b2a320819afcea6bffe72839efd004b98d1d166b90801d49d57/coverage-7.13.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cdb3c9f8fef0a954c632f64328a3935988d33a6604ce4bf67ec3e39670f12ae5", size = 245237, upload-time = "2025-12-08T13:12:17.858Z" }, + { url = "https://files.pythonhosted.org/packages/91/52/be5e85631e0eec547873d8b08dd67a5f6b111ecfe89a86e40b89b0c1c61c/coverage-7.13.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d10fd186aac2316f9bbb46ef91977f9d394ded67050ad6d84d94ed6ea2e8e54e", size = 247061, upload-time = "2025-12-08T13:12:19.132Z" }, + { url = "https://files.pythonhosted.org/packages/0f/45/a5e8fa0caf05fbd8fa0402470377bff09cc1f026d21c05c71e01295e55ab/coverage-7.13.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f88ae3e69df2ab62fb0bc5219a597cb890ba5c438190ffa87490b315190bb33", size = 248928, upload-time = "2025-12-08T13:12:20.702Z" }, + { url = "https://files.pythonhosted.org/packages/f5/42/ffb5069b6fd1b95fae482e02f3fecf380d437dd5a39bae09f16d2e2e7e01/coverage-7.13.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c4be718e51e86f553bcf515305a158a1cd180d23b72f07ae76d6017c3cc5d791", size = 245931, upload-time = "2025-12-08T13:12:22.243Z" }, + { url = "https://files.pythonhosted.org/packages/95/6e/73e809b882c2858f13e55c0c36e94e09ce07e6165d5644588f9517efe333/coverage-7.13.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a00d3a393207ae12f7c49bb1c113190883b500f48979abb118d8b72b8c95c032", size = 246968, upload-time = "2025-12-08T13:12:23.52Z" }, + { url = "https://files.pythonhosted.org/packages/87/08/64ebd9e64b6adb8b4a4662133d706fbaccecab972e0b3ccc23f64e2678ad/coverage-7.13.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a7b1cd820e1b6116f92c6128f1188e7afe421c7e1b35fa9836b11444e53ebd9", size = 244972, upload-time = "2025-12-08T13:12:24.781Z" }, + { url = "https://files.pythonhosted.org/packages/12/97/f4d27c6fe0cb375a5eced4aabcaef22de74766fb80a3d5d2015139e54b22/coverage-7.13.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:37eee4e552a65866f15dedd917d5e5f3d59805994260720821e2c1b51ac3248f", size = 245241, upload-time = "2025-12-08T13:12:28.041Z" }, + { url = "https://files.pythonhosted.org/packages/0c/94/42f8ae7f633bf4c118bf1038d80472f9dade88961a466f290b81250f7ab7/coverage-7.13.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:62d7c4f13102148c78d7353c6052af6d899a7f6df66a32bddcc0c0eb7c5326f8", size = 245847, upload-time = "2025-12-08T13:12:29.337Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2f/6369ca22b6b6d933f4f4d27765d313d8914cc4cce84f82a16436b1a233db/coverage-7.13.0-cp310-cp310-win32.whl", hash = "sha256:24e4e56304fdb56f96f80eabf840eab043b3afea9348b88be680ec5986780a0f", size = 220573, upload-time = "2025-12-08T13:12:30.905Z" }, + { url = "https://files.pythonhosted.org/packages/f1/dc/a6a741e519acceaeccc70a7f4cfe5d030efc4b222595f0677e101af6f1f3/coverage-7.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:74c136e4093627cf04b26a35dab8cbfc9b37c647f0502fc313376e11726ba303", size = 221509, upload-time = "2025-12-08T13:12:32.09Z" }, + { url = "https://files.pythonhosted.org/packages/f1/dc/888bf90d8b1c3d0b4020a40e52b9f80957d75785931ec66c7dfaccc11c7d/coverage-7.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0dfa3855031070058add1a59fdfda0192fd3e8f97e7c81de0596c145dea51820", size = 218104, upload-time = "2025-12-08T13:12:33.333Z" }, + { url = "https://files.pythonhosted.org/packages/8d/ea/069d51372ad9c380214e86717e40d1a743713a2af191cfba30a0911b0a4a/coverage-7.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4fdb6f54f38e334db97f72fa0c701e66d8479af0bc3f9bfb5b90f1c30f54500f", size = 218606, upload-time = "2025-12-08T13:12:34.498Z" }, + { url = "https://files.pythonhosted.org/packages/68/09/77b1c3a66c2aa91141b6c4471af98e5b1ed9b9e6d17255da5eb7992299e3/coverage-7.13.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7e442c013447d1d8d195be62852270b78b6e255b79b8675bad8479641e21fd96", size = 248999, upload-time = "2025-12-08T13:12:36.02Z" }, + { url = "https://files.pythonhosted.org/packages/0a/32/2e2f96e9d5691eaf1181d9040f850b8b7ce165ea10810fd8e2afa534cef7/coverage-7.13.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ed5630d946859de835a85e9a43b721123a8a44ec26e2830b296d478c7fd4259", size = 250925, upload-time = "2025-12-08T13:12:37.221Z" }, + { url = "https://files.pythonhosted.org/packages/7b/45/b88ddac1d7978859b9a39a8a50ab323186148f1d64bc068f86fc77706321/coverage-7.13.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f15a931a668e58087bc39d05d2b4bf4b14ff2875b49c994bbdb1c2217a8daeb", size = 253032, upload-time = "2025-12-08T13:12:38.763Z" }, + { url = "https://files.pythonhosted.org/packages/71/cb/e15513f94c69d4820a34b6bf3d2b1f9f8755fa6021be97c7065442d7d653/coverage-7.13.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:30a3a201a127ea57f7e14ba43c93c9c4be8b7d17a26e03bb49e6966d019eede9", size = 249134, upload-time = "2025-12-08T13:12:40.382Z" }, + { url = "https://files.pythonhosted.org/packages/09/61/d960ff7dc9e902af3310ce632a875aaa7860f36d2bc8fc8b37ee7c1b82a5/coverage-7.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7a485ff48fbd231efa32d58f479befce52dcb6bfb2a88bb7bf9a0b89b1bc8030", size = 250731, upload-time = "2025-12-08T13:12:41.992Z" }, + { url = "https://files.pythonhosted.org/packages/98/34/c7c72821794afc7c7c2da1db8f00c2c98353078aa7fb6b5ff36aac834b52/coverage-7.13.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:22486cdafba4f9e471c816a2a5745337742a617fef68e890d8baf9f3036d7833", size = 248795, upload-time = "2025-12-08T13:12:43.331Z" }, + { url = "https://files.pythonhosted.org/packages/0a/5b/e0f07107987a43b2def9aa041c614ddb38064cbf294a71ef8c67d43a0cdd/coverage-7.13.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:263c3dbccc78e2e331e59e90115941b5f53e85cfcc6b3b2fbff1fd4e3d2c6ea8", size = 248514, upload-time = "2025-12-08T13:12:44.546Z" }, + { url = "https://files.pythonhosted.org/packages/71/c2/c949c5d3b5e9fc6dd79e1b73cdb86a59ef14f3709b1d72bf7668ae12e000/coverage-7.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e5330fa0cc1f5c3c4c3bb8e101b742025933e7848989370a1d4c8c5e401ea753", size = 249424, upload-time = "2025-12-08T13:12:45.759Z" }, + { url = "https://files.pythonhosted.org/packages/11/f1/bbc009abd6537cec0dffb2cc08c17a7f03de74c970e6302db4342a6e05af/coverage-7.13.0-cp311-cp311-win32.whl", hash = "sha256:0f4872f5d6c54419c94c25dd6ae1d015deeb337d06e448cd890a1e89a8ee7f3b", size = 220597, upload-time = "2025-12-08T13:12:47.378Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f6/d9977f2fb51c10fbaed0718ce3d0a8541185290b981f73b1d27276c12d91/coverage-7.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51a202e0f80f241ccb68e3e26e19ab5b3bf0f813314f2c967642f13ebcf1ddfe", size = 221536, upload-time = "2025-12-08T13:12:48.7Z" }, + { url = "https://files.pythonhosted.org/packages/be/ad/3fcf43fd96fb43e337a3073dea63ff148dcc5c41ba7a14d4c7d34efb2216/coverage-7.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:d2a9d7f1c11487b1c69367ab3ac2d81b9b3721f097aa409a3191c3e90f8f3dd7", size = 220206, upload-time = "2025-12-08T13:12:50.365Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f1/2619559f17f31ba00fc40908efd1fbf1d0a5536eb75dc8341e7d660a08de/coverage-7.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0b3d67d31383c4c68e19a88e28fc4c2e29517580f1b0ebec4a069d502ce1e0bf", size = 218274, upload-time = "2025-12-08T13:12:52.095Z" }, + { url = "https://files.pythonhosted.org/packages/2b/11/30d71ae5d6e949ff93b2a79a2c1b4822e00423116c5c6edfaeef37301396/coverage-7.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:581f086833d24a22c89ae0fe2142cfaa1c92c930adf637ddf122d55083fb5a0f", size = 218638, upload-time = "2025-12-08T13:12:53.418Z" }, + { url = "https://files.pythonhosted.org/packages/79/c2/fce80fc6ded8d77e53207489d6065d0fed75db8951457f9213776615e0f5/coverage-7.13.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0a3a30f0e257df382f5f9534d4ce3d4cf06eafaf5192beb1a7bd066cb10e78fb", size = 250129, upload-time = "2025-12-08T13:12:54.744Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b6/51b5d1eb6fcbb9a1d5d6984e26cbe09018475c2922d554fd724dd0f056ee/coverage-7.13.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:583221913fbc8f53b88c42e8dbb8fca1d0f2e597cb190ce45916662b8b9d9621", size = 252885, upload-time = "2025-12-08T13:12:56.401Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f8/972a5affea41de798691ab15d023d3530f9f56a72e12e243f35031846ff7/coverage-7.13.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f5d9bd30756fff3e7216491a0d6d520c448d5124d3d8e8f56446d6412499e74", size = 253974, upload-time = "2025-12-08T13:12:57.718Z" }, + { url = "https://files.pythonhosted.org/packages/8a/56/116513aee860b2c7968aa3506b0f59b22a959261d1dbf3aea7b4450a7520/coverage-7.13.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a23e5a1f8b982d56fa64f8e442e037f6ce29322f1f9e6c2344cd9e9f4407ee57", size = 250538, upload-time = "2025-12-08T13:12:59.254Z" }, + { url = "https://files.pythonhosted.org/packages/d6/75/074476d64248fbadf16dfafbf93fdcede389ec821f74ca858d7c87d2a98c/coverage-7.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9b01c22bc74a7fb44066aaf765224c0d933ddf1f5047d6cdfe4795504a4493f8", size = 251912, upload-time = "2025-12-08T13:13:00.604Z" }, + { url = "https://files.pythonhosted.org/packages/f2/d2/aa4f8acd1f7c06024705c12609d8698c51b27e4d635d717cd1934c9668e2/coverage-7.13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:898cce66d0836973f48dda4e3514d863d70142bdf6dfab932b9b6a90ea5b222d", size = 250054, upload-time = "2025-12-08T13:13:01.892Z" }, + { url = "https://files.pythonhosted.org/packages/19/98/8df9e1af6a493b03694a1e8070e024e7d2cdc77adedc225a35e616d505de/coverage-7.13.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:3ab483ea0e251b5790c2aac03acde31bff0c736bf8a86829b89382b407cd1c3b", size = 249619, upload-time = "2025-12-08T13:13:03.236Z" }, + { url = "https://files.pythonhosted.org/packages/d8/71/f8679231f3353018ca66ef647fa6fe7b77e6bff7845be54ab84f86233363/coverage-7.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1d84e91521c5e4cb6602fe11ece3e1de03b2760e14ae4fcf1a4b56fa3c801fcd", size = 251496, upload-time = "2025-12-08T13:13:04.511Z" }, + { url = "https://files.pythonhosted.org/packages/04/86/9cb406388034eaf3c606c22094edbbb82eea1fa9d20c0e9efadff20d0733/coverage-7.13.0-cp312-cp312-win32.whl", hash = "sha256:193c3887285eec1dbdb3f2bd7fbc351d570ca9c02ca756c3afbc71b3c98af6ef", size = 220808, upload-time = "2025-12-08T13:13:06.422Z" }, + { url = "https://files.pythonhosted.org/packages/1c/59/af483673df6455795daf5f447c2f81a3d2fcfc893a22b8ace983791f6f34/coverage-7.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:4f3e223b2b2db5e0db0c2b97286aba0036ca000f06aca9b12112eaa9af3d92ae", size = 221616, upload-time = "2025-12-08T13:13:07.95Z" }, + { url = "https://files.pythonhosted.org/packages/64/b0/959d582572b30a6830398c60dd419c1965ca4b5fb38ac6b7093a0d50ca8d/coverage-7.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:086cede306d96202e15a4b77ace8472e39d9f4e5f9fd92dd4fecdfb2313b2080", size = 220261, upload-time = "2025-12-08T13:13:09.581Z" }, + { url = "https://files.pythonhosted.org/packages/7c/cc/bce226595eb3bf7d13ccffe154c3c487a22222d87ff018525ab4dd2e9542/coverage-7.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:28ee1c96109974af104028a8ef57cec21447d42d0e937c0275329272e370ebcf", size = 218297, upload-time = "2025-12-08T13:13:10.977Z" }, + { url = "https://files.pythonhosted.org/packages/3b/9f/73c4d34600aae03447dff3d7ad1d0ac649856bfb87d1ca7d681cfc913f9e/coverage-7.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d1e97353dcc5587b85986cda4ff3ec98081d7e84dd95e8b2a6d59820f0545f8a", size = 218673, upload-time = "2025-12-08T13:13:12.562Z" }, + { url = "https://files.pythonhosted.org/packages/63/ab/8fa097db361a1e8586535ae5073559e6229596b3489ec3ef2f5b38df8cb2/coverage-7.13.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:99acd4dfdfeb58e1937629eb1ab6ab0899b131f183ee5f23e0b5da5cba2fec74", size = 249652, upload-time = "2025-12-08T13:13:13.909Z" }, + { url = "https://files.pythonhosted.org/packages/90/3a/9bfd4de2ff191feb37ef9465855ca56a6f2f30a3bca172e474130731ac3d/coverage-7.13.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ff45e0cd8451e293b63ced93161e189780baf444119391b3e7d25315060368a6", size = 252251, upload-time = "2025-12-08T13:13:15.553Z" }, + { url = "https://files.pythonhosted.org/packages/df/61/b5d8105f016e1b5874af0d7c67542da780ccd4a5f2244a433d3e20ceb1ad/coverage-7.13.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f4f72a85316d8e13234cafe0a9f81b40418ad7a082792fa4165bd7d45d96066b", size = 253492, upload-time = "2025-12-08T13:13:16.849Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b8/0fad449981803cc47a4694768b99823fb23632150743f9c83af329bb6090/coverage-7.13.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:11c21557d0e0a5a38632cbbaca5f008723b26a89d70db6315523df6df77d6232", size = 249850, upload-time = "2025-12-08T13:13:18.142Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e9/8d68337c3125014d918cf4327d5257553a710a2995a6a6de2ac77e5aa429/coverage-7.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76541dc8d53715fb4f7a3a06b34b0dc6846e3c69bc6204c55653a85dd6220971", size = 251633, upload-time = "2025-12-08T13:13:19.56Z" }, + { url = "https://files.pythonhosted.org/packages/55/14/d4112ab26b3a1bc4b3c1295d8452dcf399ed25be4cf649002fb3e64b2d93/coverage-7.13.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6e9e451dee940a86789134b6b0ffbe31c454ade3b849bb8a9d2cca2541a8e91d", size = 249586, upload-time = "2025-12-08T13:13:20.883Z" }, + { url = "https://files.pythonhosted.org/packages/2c/a9/22b0000186db663b0d82f86c2f1028099ae9ac202491685051e2a11a5218/coverage-7.13.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:5c67dace46f361125e6b9cace8fe0b729ed8479f47e70c89b838d319375c8137", size = 249412, upload-time = "2025-12-08T13:13:22.22Z" }, + { url = "https://files.pythonhosted.org/packages/a1/2e/42d8e0d9e7527fba439acdc6ed24a2b97613b1dc85849b1dd935c2cffef0/coverage-7.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f59883c643cb19630500f57016f76cfdcd6845ca8c5b5ea1f6e17f74c8e5f511", size = 251191, upload-time = "2025-12-08T13:13:23.899Z" }, + { url = "https://files.pythonhosted.org/packages/a4/af/8c7af92b1377fd8860536aadd58745119252aaaa71a5213e5a8e8007a9f5/coverage-7.13.0-cp313-cp313-win32.whl", hash = "sha256:58632b187be6f0be500f553be41e277712baa278147ecb7559983c6d9faf7ae1", size = 220829, upload-time = "2025-12-08T13:13:25.182Z" }, + { url = "https://files.pythonhosted.org/packages/58/f9/725e8bf16f343d33cbe076c75dc8370262e194ff10072c0608b8e5cf33a3/coverage-7.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:73419b89f812f498aca53f757dd834919b48ce4799f9d5cad33ca0ae442bdb1a", size = 221640, upload-time = "2025-12-08T13:13:26.836Z" }, + { url = "https://files.pythonhosted.org/packages/8a/ff/e98311000aa6933cc79274e2b6b94a2fe0fe3434fca778eba82003675496/coverage-7.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:eb76670874fdd6091eedcc856128ee48c41a9bbbb9c3f1c7c3cf169290e3ffd6", size = 220269, upload-time = "2025-12-08T13:13:28.116Z" }, + { url = "https://files.pythonhosted.org/packages/cf/cf/bbaa2e1275b300343ea865f7d424cc0a2e2a1df6925a070b2b2d5d765330/coverage-7.13.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6e63ccc6e0ad8986386461c3c4b737540f20426e7ec932f42e030320896c311a", size = 218990, upload-time = "2025-12-08T13:13:29.463Z" }, + { url = "https://files.pythonhosted.org/packages/21/1d/82f0b3323b3d149d7672e7744c116e9c170f4957e0c42572f0366dbb4477/coverage-7.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:494f5459ffa1bd45e18558cd98710c36c0b8fbfa82a5eabcbe671d80ecffbfe8", size = 219340, upload-time = "2025-12-08T13:13:31.524Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e3/fe3fd4702a3832a255f4d43013eacb0ef5fc155a5960ea9269d8696db28b/coverage-7.13.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:06cac81bf10f74034e055e903f5f946e3e26fc51c09fc9f584e4a1605d977053", size = 260638, upload-time = "2025-12-08T13:13:32.965Z" }, + { url = "https://files.pythonhosted.org/packages/ad/01/63186cb000307f2b4da463f72af9b85d380236965574c78e7e27680a2593/coverage-7.13.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f2ffc92b46ed6e6760f1d47a71e56b5664781bc68986dbd1836b2b70c0ce2071", size = 262705, upload-time = "2025-12-08T13:13:34.378Z" }, + { url = "https://files.pythonhosted.org/packages/7c/a1/c0dacef0cc865f2455d59eed3548573ce47ed603205ffd0735d1d78b5906/coverage-7.13.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0602f701057c6823e5db1b74530ce85f17c3c5be5c85fc042ac939cbd909426e", size = 265125, upload-time = "2025-12-08T13:13:35.73Z" }, + { url = "https://files.pythonhosted.org/packages/ef/92/82b99223628b61300bd382c205795533bed021505eab6dd86e11fb5d7925/coverage-7.13.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:25dc33618d45456ccb1d37bce44bc78cf269909aa14c4db2e03d63146a8a1493", size = 259844, upload-time = "2025-12-08T13:13:37.69Z" }, + { url = "https://files.pythonhosted.org/packages/cf/2c/89b0291ae4e6cd59ef042708e1c438e2290f8c31959a20055d8768349ee2/coverage-7.13.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:71936a8b3b977ddd0b694c28c6a34f4fff2e9dd201969a4ff5d5fc7742d614b0", size = 262700, upload-time = "2025-12-08T13:13:39.525Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f9/a5f992efae1996245e796bae34ceb942b05db275e4b34222a9a40b9fbd3b/coverage-7.13.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:936bc20503ce24770c71938d1369461f0c5320830800933bc3956e2a4ded930e", size = 260321, upload-time = "2025-12-08T13:13:41.172Z" }, + { url = "https://files.pythonhosted.org/packages/4c/89/a29f5d98c64fedbe32e2ac3c227fbf78edc01cc7572eee17d61024d89889/coverage-7.13.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:af0a583efaacc52ae2521f8d7910aff65cdb093091d76291ac5820d5e947fc1c", size = 259222, upload-time = "2025-12-08T13:13:43.282Z" }, + { url = "https://files.pythonhosted.org/packages/b3/c3/940fe447aae302a6701ee51e53af7e08b86ff6eed7631e5740c157ee22b9/coverage-7.13.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f1c23e24a7000da892a312fb17e33c5f94f8b001de44b7cf8ba2e36fbd15859e", size = 261411, upload-time = "2025-12-08T13:13:44.72Z" }, + { url = "https://files.pythonhosted.org/packages/eb/31/12a4aec689cb942a89129587860ed4d0fd522d5fda81237147fde554b8ae/coverage-7.13.0-cp313-cp313t-win32.whl", hash = "sha256:5f8a0297355e652001015e93be345ee54393e45dc3050af4a0475c5a2b767d46", size = 221505, upload-time = "2025-12-08T13:13:46.332Z" }, + { url = "https://files.pythonhosted.org/packages/65/8c/3b5fe3259d863572d2b0827642c50c3855d26b3aefe80bdc9eba1f0af3b0/coverage-7.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6abb3a4c52f05e08460bd9acf04fec027f8718ecaa0d09c40ffbc3fbd70ecc39", size = 222569, upload-time = "2025-12-08T13:13:47.79Z" }, + { url = "https://files.pythonhosted.org/packages/b0/39/f71fa8316a96ac72fc3908839df651e8eccee650001a17f2c78cdb355624/coverage-7.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:3ad968d1e3aa6ce5be295ab5fe3ae1bf5bb4769d0f98a80a0252d543a2ef2e9e", size = 220841, upload-time = "2025-12-08T13:13:49.243Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4b/9b54bedda55421449811dcd5263a2798a63f48896c24dfb92b0f1b0845bd/coverage-7.13.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:453b7ec753cf5e4356e14fe858064e5520c460d3bbbcb9c35e55c0d21155c256", size = 218343, upload-time = "2025-12-08T13:13:50.811Z" }, + { url = "https://files.pythonhosted.org/packages/59/df/c3a1f34d4bba2e592c8979f924da4d3d4598b0df2392fbddb7761258e3dc/coverage-7.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:af827b7cbb303e1befa6c4f94fd2bf72f108089cfa0f8abab8f4ca553cf5ca5a", size = 218672, upload-time = "2025-12-08T13:13:52.284Z" }, + { url = "https://files.pythonhosted.org/packages/07/62/eec0659e47857698645ff4e6ad02e30186eb8afd65214fd43f02a76537cb/coverage-7.13.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9987a9e4f8197a1000280f7cc089e3ea2c8b3c0a64d750537809879a7b4ceaf9", size = 249715, upload-time = "2025-12-08T13:13:53.791Z" }, + { url = "https://files.pythonhosted.org/packages/23/2d/3c7ff8b2e0e634c1f58d095f071f52ed3c23ff25be524b0ccae8b71f99f8/coverage-7.13.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3188936845cd0cb114fa6a51842a304cdbac2958145d03be2377ec41eb285d19", size = 252225, upload-time = "2025-12-08T13:13:55.274Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ac/fb03b469d20e9c9a81093575003f959cf91a4a517b783aab090e4538764b/coverage-7.13.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2bdb3babb74079f021696cb46b8bb5f5661165c385d3a238712b031a12355be", size = 253559, upload-time = "2025-12-08T13:13:57.161Z" }, + { url = "https://files.pythonhosted.org/packages/29/62/14afa9e792383c66cc0a3b872a06ded6e4ed1079c7d35de274f11d27064e/coverage-7.13.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7464663eaca6adba4175f6c19354feea61ebbdd735563a03d1e472c7072d27bb", size = 249724, upload-time = "2025-12-08T13:13:58.692Z" }, + { url = "https://files.pythonhosted.org/packages/31/b7/333f3dab2939070613696ab3ee91738950f0467778c6e5a5052e840646b7/coverage-7.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8069e831f205d2ff1f3d355e82f511eb7c5522d7d413f5db5756b772ec8697f8", size = 251582, upload-time = "2025-12-08T13:14:00.642Z" }, + { url = "https://files.pythonhosted.org/packages/81/cb/69162bda9381f39b2287265d7e29ee770f7c27c19f470164350a38318764/coverage-7.13.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6fb2d5d272341565f08e962cce14cdf843a08ac43bd621783527adb06b089c4b", size = 249538, upload-time = "2025-12-08T13:14:02.556Z" }, + { url = "https://files.pythonhosted.org/packages/e0/76/350387b56a30f4970abe32b90b2a434f87d29f8b7d4ae40d2e8a85aacfb3/coverage-7.13.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5e70f92ef89bac1ac8a99b3324923b4749f008fdbd7aa9cb35e01d7a284a04f9", size = 249349, upload-time = "2025-12-08T13:14:04.015Z" }, + { url = "https://files.pythonhosted.org/packages/86/0d/7f6c42b8d59f4c7e43ea3059f573c0dcfed98ba46eb43c68c69e52ae095c/coverage-7.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4b5de7d4583e60d5fd246dd57fcd3a8aa23c6e118a8c72b38adf666ba8e7e927", size = 251011, upload-time = "2025-12-08T13:14:05.505Z" }, + { url = "https://files.pythonhosted.org/packages/d7/f1/4bb2dff379721bb0b5c649d5c5eaf438462cad824acf32eb1b7ca0c7078e/coverage-7.13.0-cp314-cp314-win32.whl", hash = "sha256:a6c6e16b663be828a8f0b6c5027d36471d4a9f90d28444aa4ced4d48d7d6ae8f", size = 221091, upload-time = "2025-12-08T13:14:07.127Z" }, + { url = "https://files.pythonhosted.org/packages/ba/44/c239da52f373ce379c194b0ee3bcc121020e397242b85f99e0afc8615066/coverage-7.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:0900872f2fdb3ee5646b557918d02279dc3af3dfb39029ac4e945458b13f73bc", size = 221904, upload-time = "2025-12-08T13:14:08.542Z" }, + { url = "https://files.pythonhosted.org/packages/89/1f/b9f04016d2a29c2e4a0307baefefad1a4ec5724946a2b3e482690486cade/coverage-7.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:3a10260e6a152e5f03f26db4a407c4c62d3830b9af9b7c0450b183615f05d43b", size = 220480, upload-time = "2025-12-08T13:14:10.958Z" }, + { url = "https://files.pythonhosted.org/packages/16/d4/364a1439766c8e8647860584171c36010ca3226e6e45b1753b1b249c5161/coverage-7.13.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9097818b6cc1cfb5f174e3263eba4a62a17683bcfe5c4b5d07f4c97fa51fbf28", size = 219074, upload-time = "2025-12-08T13:14:13.345Z" }, + { url = "https://files.pythonhosted.org/packages/ce/f4/71ba8be63351e099911051b2089662c03d5671437a0ec2171823c8e03bec/coverage-7.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0018f73dfb4301a89292c73be6ba5f58722ff79f51593352759c1790ded1cabe", size = 219342, upload-time = "2025-12-08T13:14:15.02Z" }, + { url = "https://files.pythonhosted.org/packages/5e/25/127d8ed03d7711a387d96f132589057213e3aef7475afdaa303412463f22/coverage-7.13.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:166ad2a22ee770f5656e1257703139d3533b4a0b6909af67c6b4a3adc1c98657", size = 260713, upload-time = "2025-12-08T13:14:16.907Z" }, + { url = "https://files.pythonhosted.org/packages/fd/db/559fbb6def07d25b2243663b46ba9eb5a3c6586c0c6f4e62980a68f0ee1c/coverage-7.13.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f6aaef16d65d1787280943f1c8718dc32e9cf141014e4634d64446702d26e0ff", size = 262825, upload-time = "2025-12-08T13:14:18.68Z" }, + { url = "https://files.pythonhosted.org/packages/37/99/6ee5bf7eff884766edb43bd8736b5e1c5144d0fe47498c3779326fe75a35/coverage-7.13.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e999e2dcc094002d6e2c7bbc1fb85b58ba4f465a760a8014d97619330cdbbbf3", size = 265233, upload-time = "2025-12-08T13:14:20.55Z" }, + { url = "https://files.pythonhosted.org/packages/d8/90/92f18fe0356ea69e1f98f688ed80cec39f44e9f09a1f26a1bbf017cc67f2/coverage-7.13.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:00c3d22cf6fb1cf3bf662aaaa4e563be8243a5ed2630339069799835a9cc7f9b", size = 259779, upload-time = "2025-12-08T13:14:22.367Z" }, + { url = "https://files.pythonhosted.org/packages/90/5d/b312a8b45b37a42ea7d27d7d3ff98ade3a6c892dd48d1d503e773503373f/coverage-7.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22ccfe8d9bb0d6134892cbe1262493a8c70d736b9df930f3f3afae0fe3ac924d", size = 262700, upload-time = "2025-12-08T13:14:24.309Z" }, + { url = "https://files.pythonhosted.org/packages/63/f8/b1d0de5c39351eb71c366f872376d09386640840a2e09b0d03973d791e20/coverage-7.13.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:9372dff5ea15930fea0445eaf37bbbafbc771a49e70c0aeed8b4e2c2614cc00e", size = 260302, upload-time = "2025-12-08T13:14:26.068Z" }, + { url = "https://files.pythonhosted.org/packages/aa/7c/d42f4435bc40c55558b3109a39e2d456cddcec37434f62a1f1230991667a/coverage-7.13.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:69ac2c492918c2461bc6ace42d0479638e60719f2a4ef3f0815fa2df88e9f940", size = 259136, upload-time = "2025-12-08T13:14:27.604Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d3/23413241dc04d47cfe19b9a65b32a2edd67ecd0b817400c2843ebc58c847/coverage-7.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:739c6c051a7540608d097b8e13c76cfa85263ced467168dc6b477bae3df7d0e2", size = 261467, upload-time = "2025-12-08T13:14:29.09Z" }, + { url = "https://files.pythonhosted.org/packages/13/e6/6e063174500eee216b96272c0d1847bf215926786f85c2bd024cf4d02d2f/coverage-7.13.0-cp314-cp314t-win32.whl", hash = "sha256:fe81055d8c6c9de76d60c94ddea73c290b416e061d40d542b24a5871bad498b7", size = 221875, upload-time = "2025-12-08T13:14:31.106Z" }, + { url = "https://files.pythonhosted.org/packages/3b/46/f4fb293e4cbe3620e3ac2a3e8fd566ed33affb5861a9b20e3dd6c1896cbc/coverage-7.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:445badb539005283825959ac9fa4a28f712c214b65af3a2c464f1adc90f5fcbc", size = 222982, upload-time = "2025-12-08T13:14:33.1Z" }, + { url = "https://files.pythonhosted.org/packages/68/62/5b3b9018215ed9733fbd1ae3b2ed75c5de62c3b55377a52cae732e1b7805/coverage-7.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:de7f6748b890708578fc4b7bb967d810aeb6fcc9bff4bb77dbca77dab2f9df6a", size = 221016, upload-time = "2025-12-08T13:14:34.601Z" }, + { url = "https://files.pythonhosted.org/packages/8d/4c/1968f32fb9a2604645827e11ff84a31e59d532e01995f904723b4f5328b3/coverage-7.13.0-py3-none-any.whl", hash = "sha256:850d2998f380b1e266459ca5b47bc9e7daf9af1d070f66317972f382d46f1904", size = 210068, upload-time = "2025-12-08T13:14:36.236Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + [[package]] name = "ghp-import" version = "2.1.0" @@ -161,6 +302,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec", size = 37461, upload-time = "2025-01-03T18:51:54.306Z" }, ] +[[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" @@ -173,13 +323,86 @@ 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 = "librt" +version = "0.7.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/d9/6f3d3fcf5e5543ed8a60cc70fa7d50508ed60b8a10e9af6d2058159ab54e/librt-0.7.3.tar.gz", hash = "sha256:3ec50cf65235ff5c02c5b747748d9222e564ad48597122a361269dd3aa808798", size = 144549, upload-time = "2025-12-06T19:04:45.553Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/66/79a14e672256ef58144a24eb49adb338ec02de67ff4b45320af6504682ab/librt-0.7.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2682162855a708e3270eba4b92026b93f8257c3e65278b456c77631faf0f4f7a", size = 54707, upload-time = "2025-12-06T19:03:10.881Z" }, + { url = "https://files.pythonhosted.org/packages/58/fa/b709c65a9d5eab85f7bcfe0414504d9775aaad6e78727a0327e175474caa/librt-0.7.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:440c788f707c061d237c1e83edf6164ff19f5c0f823a3bf054e88804ebf971ec", size = 56670, upload-time = "2025-12-06T19:03:12.107Z" }, + { url = "https://files.pythonhosted.org/packages/3a/56/0685a0772ec89ddad4c00e6b584603274c3d818f9a68e2c43c4eb7b39ee9/librt-0.7.3-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:399938edbd3d78339f797d685142dd8a623dfaded023cf451033c85955e4838a", size = 161045, upload-time = "2025-12-06T19:03:13.444Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d9/863ada0c5ce48aefb89df1555e392b2209fcb6daee4c153c031339b9a89b/librt-0.7.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1975eda520957c6e0eb52d12968dd3609ffb7eef05d4223d097893d6daf1d8a7", size = 169532, upload-time = "2025-12-06T19:03:14.699Z" }, + { url = "https://files.pythonhosted.org/packages/68/a0/71da6c8724fd16c31749905ef1c9e11de206d9301b5be984bf2682b4efb3/librt-0.7.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f9da128d0edf990cf0d2ca011b02cd6f639e79286774bd5b0351245cbb5a6e51", size = 183277, upload-time = "2025-12-06T19:03:16.446Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/9c97bf2f8338ba1914de233ea312bba2bbd7c59f43f807b3e119796bab18/librt-0.7.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e19acfde38cb532a560b98f473adc741c941b7a9bc90f7294bc273d08becb58b", size = 179045, upload-time = "2025-12-06T19:03:17.838Z" }, + { url = "https://files.pythonhosted.org/packages/b3/b1/ceea067f489e904cb4ddcca3c9b06ba20229bc3fa7458711e24a5811f162/librt-0.7.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:7b4f57f7a0c65821c5441d98c47ff7c01d359b1e12328219709bdd97fdd37f90", size = 173521, upload-time = "2025-12-06T19:03:19.17Z" }, + { url = "https://files.pythonhosted.org/packages/7a/41/6cb18f5da9c89ed087417abb0127a445a50ad4eaf1282ba5b52588187f47/librt-0.7.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:256793988bff98040de23c57cf36e1f4c2f2dc3dcd17537cdac031d3b681db71", size = 193592, upload-time = "2025-12-06T19:03:20.637Z" }, + { url = "https://files.pythonhosted.org/packages/4c/3c/fcef208746584e7c78584b7aedc617130c4a4742cb8273361bbda8b183b5/librt-0.7.3-cp310-cp310-win32.whl", hash = "sha256:fcb72249ac4ea81a7baefcbff74df7029c3cb1cf01a711113fa052d563639c9c", size = 47201, upload-time = "2025-12-06T19:03:21.764Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bf/d8a6c35d1b2b789a4df9b3ddb1c8f535ea373fde2089698965a8f0d62138/librt-0.7.3-cp310-cp310-win_amd64.whl", hash = "sha256:4887c29cadbdc50640179e3861c276325ff2986791e6044f73136e6e798ff806", size = 54371, upload-time = "2025-12-06T19:03:23.231Z" }, + { url = "https://files.pythonhosted.org/packages/21/e6/f6391f5c6f158d31ed9af6bd1b1bcd3ffafdea1d816bc4219d0d90175a7f/librt-0.7.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:687403cced6a29590e6be6964463835315905221d797bc5c934a98750fe1a9af", size = 54711, upload-time = "2025-12-06T19:03:24.6Z" }, + { url = "https://files.pythonhosted.org/packages/ab/1b/53c208188c178987c081560a0fcf36f5ca500d5e21769596c845ef2f40d4/librt-0.7.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:24d70810f6e2ea853ff79338001533716b373cc0f63e2a0be5bc96129edb5fb5", size = 56664, upload-time = "2025-12-06T19:03:25.969Z" }, + { url = "https://files.pythonhosted.org/packages/cb/5c/d9da832b9a1e5f8366e8a044ec80217945385b26cb89fd6f94bfdc7d80b0/librt-0.7.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bf8c7735fbfc0754111f00edda35cf9e98a8d478de6c47b04eaa9cef4300eaa7", size = 161701, upload-time = "2025-12-06T19:03:27.035Z" }, + { url = "https://files.pythonhosted.org/packages/20/aa/1e0a7aba15e78529dd21f233076b876ee58c8b8711b1793315bdd3b263b0/librt-0.7.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32d43610dff472eab939f4d7fbdd240d1667794192690433672ae22d7af8445", size = 171040, upload-time = "2025-12-06T19:03:28.482Z" }, + { url = "https://files.pythonhosted.org/packages/69/46/3cfa325c1c2bc25775ec6ec1718cfbec9cff4ac767d37d2d3a2d1cc6f02c/librt-0.7.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:adeaa886d607fb02563c1f625cf2ee58778a2567c0c109378da8f17ec3076ad7", size = 184720, upload-time = "2025-12-06T19:03:29.599Z" }, + { url = "https://files.pythonhosted.org/packages/99/bb/e4553433d7ac47f4c75d0a7e59b13aee0e08e88ceadbee356527a9629b0a/librt-0.7.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:572a24fc5958c61431da456a0ef1eeea6b4989d81eeb18b8e5f1f3077592200b", size = 180731, upload-time = "2025-12-06T19:03:31.201Z" }, + { url = "https://files.pythonhosted.org/packages/35/89/51cd73006232981a3106d4081fbaa584ac4e27b49bc02266468d3919db03/librt-0.7.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6488e69d408b492e08bfb68f20c4a899a354b4386a446ecd490baff8d0862720", size = 174565, upload-time = "2025-12-06T19:03:32.818Z" }, + { url = "https://files.pythonhosted.org/packages/42/54/0578a78b587e5aa22486af34239a052c6366835b55fc307bc64380229e3f/librt-0.7.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ed028fc3d41adda916320712838aec289956c89b4f0a361ceadf83a53b4c047a", size = 195247, upload-time = "2025-12-06T19:03:34.434Z" }, + { url = "https://files.pythonhosted.org/packages/b5/0a/ee747cd999753dd9447e50b98fc36ee433b6c841a42dbf6d47b64b32a56e/librt-0.7.3-cp311-cp311-win32.whl", hash = "sha256:2cf9d73499486ce39eebbff5f42452518cc1f88d8b7ea4a711ab32962b176ee2", size = 47514, upload-time = "2025-12-06T19:03:35.959Z" }, + { url = "https://files.pythonhosted.org/packages/ec/af/8b13845178dec488e752878f8e290f8f89e7e34ae1528b70277aa1a6dd1e/librt-0.7.3-cp311-cp311-win_amd64.whl", hash = "sha256:35f1609e3484a649bb80431310ddbec81114cd86648f1d9482bc72a3b86ded2e", size = 54695, upload-time = "2025-12-06T19:03:36.956Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/ae59578501b1a25850266778f59279f4f3e726acc5c44255bfcb07b4bc57/librt-0.7.3-cp311-cp311-win_arm64.whl", hash = "sha256:550fdbfbf5bba6a2960b27376ca76d6aaa2bd4b1a06c4255edd8520c306fcfc0", size = 48142, upload-time = "2025-12-06T19:03:38.263Z" }, + { url = "https://files.pythonhosted.org/packages/29/90/ed8595fa4e35b6020317b5ea8d226a782dcbac7a997c19ae89fb07a41c66/librt-0.7.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0fa9ac2e49a6bee56e47573a6786cb635e128a7b12a0dc7851090037c0d397a3", size = 55687, upload-time = "2025-12-06T19:03:39.245Z" }, + { url = "https://files.pythonhosted.org/packages/dd/f6/6a20702a07b41006cb001a759440cb6b5362530920978f64a2b2ae2bf729/librt-0.7.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2e980cf1ed1a2420a6424e2ed884629cdead291686f1048810a817de07b5eb18", size = 57127, upload-time = "2025-12-06T19:03:40.3Z" }, + { url = "https://files.pythonhosted.org/packages/79/f3/b0c4703d5ffe9359b67bb2ccb86c42d4e930a363cfc72262ac3ba53cff3e/librt-0.7.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e094e445c37c57e9ec612847812c301840239d34ccc5d153a982fa9814478c60", size = 165336, upload-time = "2025-12-06T19:03:41.369Z" }, + { url = "https://files.pythonhosted.org/packages/02/69/3ba05b73ab29ccbe003856232cea4049769be5942d799e628d1470ed1694/librt-0.7.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aca73d70c3f553552ba9133d4a09e767dcfeee352d8d8d3eb3f77e38a3beb3ed", size = 174237, upload-time = "2025-12-06T19:03:42.44Z" }, + { url = "https://files.pythonhosted.org/packages/22/ad/d7c2671e7bf6c285ef408aa435e9cd3fdc06fd994601e1f2b242df12034f/librt-0.7.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c634a0a6db395fdaba0361aa78395597ee72c3aad651b9a307a3a7eaf5efd67e", size = 189017, upload-time = "2025-12-06T19:03:44.01Z" }, + { url = "https://files.pythonhosted.org/packages/f4/94/d13f57193148004592b618555f296b41d2d79b1dc814ff8b3273a0bf1546/librt-0.7.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a59a69deeb458c858b8fea6acf9e2acd5d755d76cd81a655256bc65c20dfff5b", size = 183983, upload-time = "2025-12-06T19:03:45.834Z" }, + { url = "https://files.pythonhosted.org/packages/02/10/b612a9944ebd39fa143c7e2e2d33f2cb790205e025ddd903fb509a3a3bb3/librt-0.7.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d91e60ac44bbe3a77a67af4a4c13114cbe9f6d540337ce22f2c9eaf7454ca71f", size = 177602, upload-time = "2025-12-06T19:03:46.944Z" }, + { url = "https://files.pythonhosted.org/packages/1f/48/77bc05c4cc232efae6c5592c0095034390992edbd5bae8d6cf1263bb7157/librt-0.7.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:703456146dc2bf430f7832fd1341adac5c893ec3c1430194fdcefba00012555c", size = 199282, upload-time = "2025-12-06T19:03:48.069Z" }, + { url = "https://files.pythonhosted.org/packages/12/aa/05916ccd864227db1ffec2a303ae34f385c6b22d4e7ce9f07054dbcf083c/librt-0.7.3-cp312-cp312-win32.whl", hash = "sha256:b7c1239b64b70be7759554ad1a86288220bbb04d68518b527783c4ad3fb4f80b", size = 47879, upload-time = "2025-12-06T19:03:49.289Z" }, + { url = "https://files.pythonhosted.org/packages/50/92/7f41c42d31ea818b3c4b9cc1562e9714bac3c676dd18f6d5dd3d0f2aa179/librt-0.7.3-cp312-cp312-win_amd64.whl", hash = "sha256:ef59c938f72bdbc6ab52dc50f81d0637fde0f194b02d636987cea2ab30f8f55a", size = 54972, upload-time = "2025-12-06T19:03:50.335Z" }, + { url = "https://files.pythonhosted.org/packages/3f/dc/53582bbfb422311afcbc92adb75711f04e989cec052f08ec0152fbc36c9c/librt-0.7.3-cp312-cp312-win_arm64.whl", hash = "sha256:ff21c554304e8226bf80c3a7754be27c6c3549a9fec563a03c06ee8f494da8fc", size = 48338, upload-time = "2025-12-06T19:03:51.431Z" }, + { url = "https://files.pythonhosted.org/packages/93/7d/e0ce1837dfb452427db556e6d4c5301ba3b22fe8de318379fbd0593759b9/librt-0.7.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56f2a47beda8409061bc1c865bef2d4bd9ff9255219402c0817e68ab5ad89aed", size = 55742, upload-time = "2025-12-06T19:03:52.459Z" }, + { url = "https://files.pythonhosted.org/packages/be/c0/3564262301e507e1d5cf31c7d84cb12addf0d35e05ba53312494a2eba9a4/librt-0.7.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:14569ac5dd38cfccf0a14597a88038fb16811a6fede25c67b79c6d50fc2c8fdc", size = 57163, upload-time = "2025-12-06T19:03:53.516Z" }, + { url = "https://files.pythonhosted.org/packages/be/ac/245e72b7e443d24a562f6047563c7f59833384053073ef9410476f68505b/librt-0.7.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6038ccbd5968325a5d6fd393cf6e00b622a8de545f0994b89dd0f748dcf3e19e", size = 165840, upload-time = "2025-12-06T19:03:54.918Z" }, + { url = "https://files.pythonhosted.org/packages/98/af/587e4491f40adba066ba39a450c66bad794c8d92094f936a201bfc7c2b5f/librt-0.7.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d39079379a9a28e74f4d57dc6357fa310a1977b51ff12239d7271ec7e71d67f5", size = 174827, upload-time = "2025-12-06T19:03:56.082Z" }, + { url = "https://files.pythonhosted.org/packages/78/21/5b8c60ea208bc83dd00421022a3874330685d7e856404128dc3728d5d1af/librt-0.7.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8837d5a52a2d7aa9f4c3220a8484013aed1d8ad75240d9a75ede63709ef89055", size = 189612, upload-time = "2025-12-06T19:03:57.507Z" }, + { url = "https://files.pythonhosted.org/packages/da/2f/8b819169ef696421fb81cd04c6cdf225f6e96f197366001e9d45180d7e9e/librt-0.7.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:399bbd7bcc1633c3e356ae274a1deb8781c7bf84d9c7962cc1ae0c6e87837292", size = 184584, upload-time = "2025-12-06T19:03:58.686Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fc/af9d225a9395b77bd7678362cb055d0b8139c2018c37665de110ca388022/librt-0.7.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8d8cf653e798ee4c4e654062b633db36984a1572f68c3aa25e364a0ddfbbb910", size = 178269, upload-time = "2025-12-06T19:03:59.769Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d8/7b4fa1683b772966749d5683aa3fd605813defffe157833a8fa69cc89207/librt-0.7.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2f03484b54bf4ae80ab2e504a8d99d20d551bfe64a7ec91e218010b467d77093", size = 199852, upload-time = "2025-12-06T19:04:00.901Z" }, + { url = "https://files.pythonhosted.org/packages/77/e8/4598413aece46ca38d9260ef6c51534bd5f34b5c21474fcf210ce3a02123/librt-0.7.3-cp313-cp313-win32.whl", hash = "sha256:44b3689b040df57f492e02cd4f0bacd1b42c5400e4b8048160c9d5e866de8abe", size = 47936, upload-time = "2025-12-06T19:04:02.054Z" }, + { url = "https://files.pythonhosted.org/packages/af/80/ac0e92d5ef8c6791b3e2c62373863827a279265e0935acdf807901353b0e/librt-0.7.3-cp313-cp313-win_amd64.whl", hash = "sha256:6b407c23f16ccc36614c136251d6b32bf30de7a57f8e782378f1107be008ddb0", size = 54965, upload-time = "2025-12-06T19:04:03.224Z" }, + { url = "https://files.pythonhosted.org/packages/f1/fd/042f823fcbff25c1449bb4203a29919891ca74141b68d3a5f6612c4ce283/librt-0.7.3-cp313-cp313-win_arm64.whl", hash = "sha256:abfc57cab3c53c4546aee31859ef06753bfc136c9d208129bad23e2eca39155a", size = 48350, upload-time = "2025-12-06T19:04:04.234Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ae/c6ecc7bb97134a71b5241e8855d39964c0e5f4d96558f0d60593892806d2/librt-0.7.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:120dd21d46ff875e849f1aae19346223cf15656be489242fe884036b23d39e93", size = 55175, upload-time = "2025-12-06T19:04:05.308Z" }, + { url = "https://files.pythonhosted.org/packages/cf/bc/2cc0cb0ab787b39aa5c7645cd792433c875982bdf12dccca558b89624594/librt-0.7.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1617bea5ab31266e152871208502ee943cb349c224846928a1173c864261375e", size = 56881, upload-time = "2025-12-06T19:04:06.674Z" }, + { url = "https://files.pythonhosted.org/packages/8e/87/397417a386190b70f5bf26fcedbaa1515f19dce33366e2684c6b7ee83086/librt-0.7.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:93b2a1f325fefa1482516ced160c8c7b4b8d53226763fa6c93d151fa25164207", size = 163710, upload-time = "2025-12-06T19:04:08.437Z" }, + { url = "https://files.pythonhosted.org/packages/c9/37/7338f85b80e8a17525d941211451199845093ca242b32efbf01df8531e72/librt-0.7.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d4801db8354436fd3936531e7f0e4feb411f62433a6b6cb32bb416e20b529f", size = 172471, upload-time = "2025-12-06T19:04:10.124Z" }, + { url = "https://files.pythonhosted.org/packages/3b/e0/741704edabbfae2c852fedc1b40d9ed5a783c70ed3ed8e4fe98f84b25d13/librt-0.7.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11ad45122bbed42cfc8b0597450660126ef28fd2d9ae1a219bc5af8406f95678", size = 186804, upload-time = "2025-12-06T19:04:11.586Z" }, + { url = "https://files.pythonhosted.org/packages/f4/d1/0a82129d6ba242f3be9af34815be089f35051bc79619f5c27d2c449ecef6/librt-0.7.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6b4e7bff1d76dd2b46443078519dc75df1b5e01562345f0bb740cea5266d8218", size = 181817, upload-time = "2025-12-06T19:04:12.802Z" }, + { url = "https://files.pythonhosted.org/packages/4f/32/704f80bcf9979c68d4357c46f2af788fbf9d5edda9e7de5786ed2255e911/librt-0.7.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:d86f94743a11873317094326456b23f8a5788bad9161fd2f0e52088c33564620", size = 175602, upload-time = "2025-12-06T19:04:14.004Z" }, + { url = "https://files.pythonhosted.org/packages/f7/6d/4355cfa0fae0c062ba72f541d13db5bc575770125a7ad3d4f46f4109d305/librt-0.7.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:754a0d09997095ad764ccef050dd5bf26cbf457aab9effcba5890dad081d879e", size = 196497, upload-time = "2025-12-06T19:04:15.487Z" }, + { url = "https://files.pythonhosted.org/packages/2e/eb/ac6d8517d44209e5a712fde46f26d0055e3e8969f24d715f70bd36056230/librt-0.7.3-cp314-cp314-win32.whl", hash = "sha256:fbd7351d43b80d9c64c3cfcb50008f786cc82cba0450e8599fdd64f264320bd3", size = 44678, upload-time = "2025-12-06T19:04:16.688Z" }, + { url = "https://files.pythonhosted.org/packages/e9/93/238f026d141faf9958da588c761a0812a1a21c98cc54a76f3608454e4e59/librt-0.7.3-cp314-cp314-win_amd64.whl", hash = "sha256:d376a35c6561e81d2590506804b428fc1075fcc6298fc5bb49b771534c0ba010", size = 51689, upload-time = "2025-12-06T19:04:17.726Z" }, + { url = "https://files.pythonhosted.org/packages/52/44/43f462ad9dcf9ed7d3172fe2e30d77b980956250bd90e9889a9cca93df2a/librt-0.7.3-cp314-cp314-win_arm64.whl", hash = "sha256:cbdb3f337c88b43c3b49ca377731912c101178be91cb5071aac48faa898e6f8e", size = 44662, upload-time = "2025-12-06T19:04:18.771Z" }, + { url = "https://files.pythonhosted.org/packages/1d/35/fed6348915f96b7323241de97f26e2af481e95183b34991df12fd5ce31b1/librt-0.7.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9f0e0927efe87cd42ad600628e595a1a0aa1c64f6d0b55f7e6059079a428641a", size = 57347, upload-time = "2025-12-06T19:04:19.812Z" }, + { url = "https://files.pythonhosted.org/packages/9a/f2/045383ccc83e3fea4fba1b761796584bc26817b6b2efb6b8a6731431d16f/librt-0.7.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:020c6db391268bcc8ce75105cb572df8cb659a43fd347366aaa407c366e5117a", size = 59223, upload-time = "2025-12-06T19:04:20.862Z" }, + { url = "https://files.pythonhosted.org/packages/77/3f/c081f8455ab1d7f4a10dbe58463ff97119272ff32494f21839c3b9029c2c/librt-0.7.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7af7785f5edd1f418da09a8cdb9ec84b0213e23d597413e06525340bcce1ea4f", size = 183861, upload-time = "2025-12-06T19:04:21.963Z" }, + { url = "https://files.pythonhosted.org/packages/1d/f5/73c5093c22c31fbeaebc25168837f05ebfd8bf26ce00855ef97a5308f36f/librt-0.7.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ccadf260bb46a61b9c7e89e2218f6efea9f3eeaaab4e3d1f58571890e54858e", size = 194594, upload-time = "2025-12-06T19:04:23.14Z" }, + { url = "https://files.pythonhosted.org/packages/78/b8/d5f17d4afe16612a4a94abfded94c16c5a033f183074fb130dfe56fc1a42/librt-0.7.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9883b2d819ce83f87ba82a746c81d14ada78784db431e57cc9719179847376e", size = 206759, upload-time = "2025-12-06T19:04:24.328Z" }, + { url = "https://files.pythonhosted.org/packages/36/2e/021765c1be85ee23ffd5b5b968bb4cba7526a4db2a0fc27dcafbdfc32da7/librt-0.7.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:59cb0470612d21fa1efddfa0dd710756b50d9c7fb6c1236bbf8ef8529331dc70", size = 203210, upload-time = "2025-12-06T19:04:25.544Z" }, + { url = "https://files.pythonhosted.org/packages/77/f0/9923656e42da4fd18c594bd08cf6d7e152d4158f8b808e210d967f0dcceb/librt-0.7.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:1fe603877e1865b5fd047a5e40379509a4a60204aa7aa0f72b16f7a41c3f0712", size = 196708, upload-time = "2025-12-06T19:04:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/fc/0b/0708b886ac760e64d6fbe7e16024e4be3ad1a3629d19489a97e9cf4c3431/librt-0.7.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5460d99ed30f043595bbdc888f542bad2caeb6226b01c33cda3ae444e8f82d42", size = 217212, upload-time = "2025-12-06T19:04:27.892Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7f/12a73ff17bca4351e73d585dd9ebf46723c4a8622c4af7fe11a2e2d011ff/librt-0.7.3-cp314-cp314t-win32.whl", hash = "sha256:d09f677693328503c9e492e33e9601464297c01f9ebd966ea8fc5308f3069bfd", size = 45586, upload-time = "2025-12-06T19:04:29.116Z" }, + { url = "https://files.pythonhosted.org/packages/e2/df/8decd032ac9b995e4f5606cde783711a71094128d88d97a52e397daf2c89/librt-0.7.3-cp314-cp314t-win_amd64.whl", hash = "sha256:25711f364c64cab2c910a0247e90b51421e45dbc8910ceeb4eac97a9e132fc6f", size = 53002, upload-time = "2025-12-06T19:04:30.173Z" }, + { url = "https://files.pythonhosted.org/packages/de/0c/6605b6199de8178afe7efc77ca1d8e6db00453bc1d3349d27605c0f42104/librt-0.7.3-cp314-cp314t-win_arm64.whl", hash = "sha256:a9f9b661f82693eb56beb0605156c7fca57f535704ab91837405913417d6990b", size = 45647, upload-time = "2025-12-06T19:04:31.302Z" }, +] + [[package]] name = "markdown" -version = "3.9" +version = "3.10" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8d/37/02347f6d6d8279247a5837082ebc26fc0d5aaeaf75aa013fcbb433c777ab/markdown-3.9.tar.gz", hash = "sha256:d2900fe1782bd33bdbbd56859defef70c2e78fc46668f8eb9df3128138f2cb6a", size = 364585, upload-time = "2025-09-04T20:25:22.885Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/7dd27d9d863b3376fcf23a5a13cb5d024aed1db46f963f1b5735ae43b3be/markdown-3.10.tar.gz", hash = "sha256:37062d4f2aa4b2b6b32aefb80faa300f82cc790cb949a35b8caede34f2b68c0e", size = 364931, upload-time = "2025-11-03T19:51:15.007Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/70/ae/44c4a6a4cbb496d93c6257954260fe3a6e91b7bed2240e5dad2a717f5111/markdown-3.9-py3-none-any.whl", hash = "sha256:9f4d91ed810864ea88a6f32c07ba8bee1346c0cc1f6b1f9f6c822f2a9667d280", size = 107441, upload-time = "2025-09-04T20:25:21.784Z" }, + { url = "https://files.pythonhosted.org/packages/70/81/54e3ce63502cd085a0c556652a4e1b919c45a446bd1e5300e10c44c8c521/markdown-3.10-py3-none-any.whl", hash = "sha256:b5b99d6951e2e4948d939255596523444c0e677c669700b1d17aa4a8a464cb7c", size = 107678, upload-time = "2025-11-03T19:51:13.887Z" }, ] [[package]] @@ -335,7 +558,7 @@ wheels = [ [[package]] name = "mkdocs-material" -version = "9.6.21" +version = "9.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "babel" }, @@ -350,9 +573,9 @@ dependencies = [ { name = "pymdown-extensions" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ff/d5/ab83ca9aa314954b0a9e8849780bdd01866a3cfcb15ffb7e3a61ca06ff0b/mkdocs_material-9.6.21.tar.gz", hash = "sha256:b01aa6d2731322438056f360f0e623d3faae981f8f2d8c68b1b973f4f2657870", size = 4043097, upload-time = "2025-09-30T19:11:27.517Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/3b/111b84cd6ff28d9e955b5f799ef217a17bc1684ac346af333e6100e413cb/mkdocs_material-9.7.0.tar.gz", hash = "sha256:602b359844e906ee402b7ed9640340cf8a474420d02d8891451733b6b02314ec", size = 4094546, upload-time = "2025-11-11T08:49:09.73Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cf/4f/98681c2030375fe9b057dbfb9008b68f46c07dddf583f4df09bf8075e37f/mkdocs_material-9.6.21-py3-none-any.whl", hash = "sha256:aa6a5ab6fb4f6d381588ac51da8782a4d3757cb3d1b174f81a2ec126e1f22c92", size = 9203097, upload-time = "2025-09-30T19:11:24.063Z" }, + { url = "https://files.pythonhosted.org/packages/04/87/eefe8d5e764f4cf50ed91b943f8e8f96b5efd65489d8303b7a36e2e79834/mkdocs_material-9.7.0-py3-none-any.whl", hash = "sha256:da2866ea53601125ff5baa8aa06404c6e07af3c5ce3d5de95e3b52b80b442887", size = 9283770, upload-time = "2025-11-11T08:49:06.26Z" }, ] [[package]] @@ -366,47 +589,48 @@ wheels = [ [[package]] name = "mypy" -version = "1.18.2" +version = "1.19.0" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "librt" }, { name = "mypy-extensions" }, { name = "pathspec" }, { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c0/77/8f0d0001ffad290cef2f7f216f96c814866248a0b92a722365ed54648e7e/mypy-1.18.2.tar.gz", hash = "sha256:06a398102a5f203d7477b2923dda3634c36727fa5c237d8f859ef90c42a9924b", size = 3448846, upload-time = "2025-09-19T00:11:10.519Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/03/6f/657961a0743cff32e6c0611b63ff1c1970a0b482ace35b069203bf705187/mypy-1.18.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c1eab0cf6294dafe397c261a75f96dc2c31bffe3b944faa24db5def4e2b0f77c", size = 12807973, upload-time = "2025-09-19T00:10:35.282Z" }, - { url = "https://files.pythonhosted.org/packages/10/e9/420822d4f661f13ca8900f5fa239b40ee3be8b62b32f3357df9a3045a08b/mypy-1.18.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7a780ca61fc239e4865968ebc5240bb3bf610ef59ac398de9a7421b54e4a207e", size = 11896527, upload-time = "2025-09-19T00:10:55.791Z" }, - { url = "https://files.pythonhosted.org/packages/aa/73/a05b2bbaa7005f4642fcfe40fb73f2b4fb6bb44229bd585b5878e9a87ef8/mypy-1.18.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:448acd386266989ef11662ce3c8011fd2a7b632e0ec7d61a98edd8e27472225b", size = 12507004, upload-time = "2025-09-19T00:11:05.411Z" }, - { url = "https://files.pythonhosted.org/packages/4f/01/f6e4b9f0d031c11ccbd6f17da26564f3a0f3c4155af344006434b0a05a9d/mypy-1.18.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f9e171c465ad3901dc652643ee4bffa8e9fef4d7d0eece23b428908c77a76a66", size = 13245947, upload-time = "2025-09-19T00:10:46.923Z" }, - { url = "https://files.pythonhosted.org/packages/d7/97/19727e7499bfa1ae0773d06afd30ac66a58ed7437d940c70548634b24185/mypy-1.18.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:592ec214750bc00741af1f80cbf96b5013d81486b7bb24cb052382c19e40b428", size = 13499217, upload-time = "2025-09-19T00:09:39.472Z" }, - { url = "https://files.pythonhosted.org/packages/9f/4f/90dc8c15c1441bf31cf0f9918bb077e452618708199e530f4cbd5cede6ff/mypy-1.18.2-cp310-cp310-win_amd64.whl", hash = "sha256:7fb95f97199ea11769ebe3638c29b550b5221e997c63b14ef93d2e971606ebed", size = 9766753, upload-time = "2025-09-19T00:10:49.161Z" }, - { url = "https://files.pythonhosted.org/packages/88/87/cafd3ae563f88f94eec33f35ff722d043e09832ea8530ef149ec1efbaf08/mypy-1.18.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:807d9315ab9d464125aa9fcf6d84fde6e1dc67da0b6f80e7405506b8ac72bc7f", size = 12731198, upload-time = "2025-09-19T00:09:44.857Z" }, - { url = "https://files.pythonhosted.org/packages/0f/e0/1e96c3d4266a06d4b0197ace5356d67d937d8358e2ee3ffac71faa843724/mypy-1.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:776bb00de1778caf4db739c6e83919c1d85a448f71979b6a0edd774ea8399341", size = 11817879, upload-time = "2025-09-19T00:09:47.131Z" }, - { url = "https://files.pythonhosted.org/packages/72/ef/0c9ba89eb03453e76bdac5a78b08260a848c7bfc5d6603634774d9cd9525/mypy-1.18.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1379451880512ffce14505493bd9fe469e0697543717298242574882cf8cdb8d", size = 12427292, upload-time = "2025-09-19T00:10:22.472Z" }, - { url = "https://files.pythonhosted.org/packages/1a/52/ec4a061dd599eb8179d5411d99775bec2a20542505988f40fc2fee781068/mypy-1.18.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1331eb7fd110d60c24999893320967594ff84c38ac6d19e0a76c5fd809a84c86", size = 13163750, upload-time = "2025-09-19T00:09:51.472Z" }, - { url = "https://files.pythonhosted.org/packages/c4/5f/2cf2ceb3b36372d51568f2208c021870fe7834cf3186b653ac6446511839/mypy-1.18.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ca30b50a51e7ba93b00422e486cbb124f1c56a535e20eff7b2d6ab72b3b2e37", size = 13351827, upload-time = "2025-09-19T00:09:58.311Z" }, - { url = "https://files.pythonhosted.org/packages/c8/7d/2697b930179e7277529eaaec1513f8de622818696857f689e4a5432e5e27/mypy-1.18.2-cp311-cp311-win_amd64.whl", hash = "sha256:664dc726e67fa54e14536f6e1224bcfce1d9e5ac02426d2326e2bb4e081d1ce8", size = 9757983, upload-time = "2025-09-19T00:10:09.071Z" }, - { url = "https://files.pythonhosted.org/packages/07/06/dfdd2bc60c66611dd8335f463818514733bc763e4760dee289dcc33df709/mypy-1.18.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:33eca32dd124b29400c31d7cf784e795b050ace0e1f91b8dc035672725617e34", size = 12908273, upload-time = "2025-09-19T00:10:58.321Z" }, - { url = "https://files.pythonhosted.org/packages/81/14/6a9de6d13a122d5608e1a04130724caf9170333ac5a924e10f670687d3eb/mypy-1.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a3c47adf30d65e89b2dcd2fa32f3aeb5e94ca970d2c15fcb25e297871c8e4764", size = 11920910, upload-time = "2025-09-19T00:10:20.043Z" }, - { url = "https://files.pythonhosted.org/packages/5f/a9/b29de53e42f18e8cc547e38daa9dfa132ffdc64f7250e353f5c8cdd44bee/mypy-1.18.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d6c838e831a062f5f29d11c9057c6009f60cb294fea33a98422688181fe2893", size = 12465585, upload-time = "2025-09-19T00:10:33.005Z" }, - { url = "https://files.pythonhosted.org/packages/77/ae/6c3d2c7c61ff21f2bee938c917616c92ebf852f015fb55917fd6e2811db2/mypy-1.18.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01199871b6110a2ce984bde85acd481232d17413868c9807e95c1b0739a58914", size = 13348562, upload-time = "2025-09-19T00:10:11.51Z" }, - { url = "https://files.pythonhosted.org/packages/4d/31/aec68ab3b4aebdf8f36d191b0685d99faa899ab990753ca0fee60fb99511/mypy-1.18.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a2afc0fa0b0e91b4599ddfe0f91e2c26c2b5a5ab263737e998d6817874c5f7c8", size = 13533296, upload-time = "2025-09-19T00:10:06.568Z" }, - { url = "https://files.pythonhosted.org/packages/9f/83/abcb3ad9478fca3ebeb6a5358bb0b22c95ea42b43b7789c7fb1297ca44f4/mypy-1.18.2-cp312-cp312-win_amd64.whl", hash = "sha256:d8068d0afe682c7c4897c0f7ce84ea77f6de953262b12d07038f4d296d547074", size = 9828828, upload-time = "2025-09-19T00:10:28.203Z" }, - { url = "https://files.pythonhosted.org/packages/5f/04/7f462e6fbba87a72bc8097b93f6842499c428a6ff0c81dd46948d175afe8/mypy-1.18.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:07b8b0f580ca6d289e69209ec9d3911b4a26e5abfde32228a288eb79df129fcc", size = 12898728, upload-time = "2025-09-19T00:10:01.33Z" }, - { url = "https://files.pythonhosted.org/packages/99/5b/61ed4efb64f1871b41fd0b82d29a64640f3516078f6c7905b68ab1ad8b13/mypy-1.18.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ed4482847168439651d3feee5833ccedbf6657e964572706a2adb1f7fa4dfe2e", size = 11910758, upload-time = "2025-09-19T00:10:42.607Z" }, - { url = "https://files.pythonhosted.org/packages/3c/46/d297d4b683cc89a6e4108c4250a6a6b717f5fa96e1a30a7944a6da44da35/mypy-1.18.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3ad2afadd1e9fea5cf99a45a822346971ede8685cc581ed9cd4d42eaf940986", size = 12475342, upload-time = "2025-09-19T00:11:00.371Z" }, - { url = "https://files.pythonhosted.org/packages/83/45/4798f4d00df13eae3bfdf726c9244bcb495ab5bd588c0eed93a2f2dd67f3/mypy-1.18.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a431a6f1ef14cf8c144c6b14793a23ec4eae3db28277c358136e79d7d062f62d", size = 13338709, upload-time = "2025-09-19T00:11:03.358Z" }, - { url = "https://files.pythonhosted.org/packages/d7/09/479f7358d9625172521a87a9271ddd2441e1dab16a09708f056e97007207/mypy-1.18.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7ab28cc197f1dd77a67e1c6f35cd1f8e8b73ed2217e4fc005f9e6a504e46e7ba", size = 13529806, upload-time = "2025-09-19T00:10:26.073Z" }, - { url = "https://files.pythonhosted.org/packages/71/cf/ac0f2c7e9d0ea3c75cd99dff7aec1c9df4a1376537cb90e4c882267ee7e9/mypy-1.18.2-cp313-cp313-win_amd64.whl", hash = "sha256:0e2785a84b34a72ba55fb5daf079a1003a34c05b22238da94fcae2bbe46f3544", size = 9833262, upload-time = "2025-09-19T00:10:40.035Z" }, - { url = "https://files.pythonhosted.org/packages/5a/0c/7d5300883da16f0063ae53996358758b2a2df2a09c72a5061fa79a1f5006/mypy-1.18.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:62f0e1e988ad41c2a110edde6c398383a889d95b36b3e60bcf155f5164c4fdce", size = 12893775, upload-time = "2025-09-19T00:10:03.814Z" }, - { url = "https://files.pythonhosted.org/packages/50/df/2cffbf25737bdb236f60c973edf62e3e7b4ee1c25b6878629e88e2cde967/mypy-1.18.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8795a039bab805ff0c1dfdb8cd3344642c2b99b8e439d057aba30850b8d3423d", size = 11936852, upload-time = "2025-09-19T00:10:51.631Z" }, - { url = "https://files.pythonhosted.org/packages/be/50/34059de13dd269227fb4a03be1faee6e2a4b04a2051c82ac0a0b5a773c9a/mypy-1.18.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ca1e64b24a700ab5ce10133f7ccd956a04715463d30498e64ea8715236f9c9c", size = 12480242, upload-time = "2025-09-19T00:11:07.955Z" }, - { url = "https://files.pythonhosted.org/packages/5b/11/040983fad5132d85914c874a2836252bbc57832065548885b5bb5b0d4359/mypy-1.18.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d924eef3795cc89fecf6bedc6ed32b33ac13e8321344f6ddbf8ee89f706c05cb", size = 13326683, upload-time = "2025-09-19T00:09:55.572Z" }, - { url = "https://files.pythonhosted.org/packages/e9/ba/89b2901dd77414dd7a8c8729985832a5735053be15b744c18e4586e506ef/mypy-1.18.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20c02215a080e3a2be3aa50506c67242df1c151eaba0dcbc1e4e557922a26075", size = 13514749, upload-time = "2025-09-19T00:10:44.827Z" }, - { url = "https://files.pythonhosted.org/packages/25/bc/cc98767cffd6b2928ba680f3e5bc969c4152bf7c2d83f92f5a504b92b0eb/mypy-1.18.2-cp314-cp314-win_amd64.whl", hash = "sha256:749b5f83198f1ca64345603118a6f01a4e99ad4bf9d103ddc5a3200cc4614adf", size = 9982959, upload-time = "2025-09-19T00:10:37.344Z" }, - { url = "https://files.pythonhosted.org/packages/87/e3/be76d87158ebafa0309946c4a73831974d4d6ab4f4ef40c3b53a385a66fd/mypy-1.18.2-py3-none-any.whl", hash = "sha256:22a1748707dd62b58d2ae53562ffc4d7f8bcc727e8ac7cbc69c053ddc874d47e", size = 2352367, upload-time = "2025-09-19T00:10:15.489Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/f9/b5/b58cdc25fadd424552804bf410855d52324183112aa004f0732c5f6324cf/mypy-1.19.0.tar.gz", hash = "sha256:f6b874ca77f733222641e5c46e4711648c4037ea13646fd0cdc814c2eaec2528", size = 3579025, upload-time = "2025-11-28T15:49:01.26Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/8f/55fb488c2b7dabd76e3f30c10f7ab0f6190c1fcbc3e97b1e588ec625bbe2/mypy-1.19.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6148ede033982a8c5ca1143de34c71836a09f105068aaa8b7d5edab2b053e6c8", size = 13093239, upload-time = "2025-11-28T15:45:11.342Z" }, + { url = "https://files.pythonhosted.org/packages/72/1b/278beea978456c56b3262266274f335c3ba5ff2c8108b3b31bec1ffa4c1d/mypy-1.19.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a9ac09e52bb0f7fb912f5d2a783345c72441a08ef56ce3e17c1752af36340a39", size = 12156128, upload-time = "2025-11-28T15:46:02.566Z" }, + { url = "https://files.pythonhosted.org/packages/21/f8/e06f951902e136ff74fd7a4dc4ef9d884faeb2f8eb9c49461235714f079f/mypy-1.19.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11f7254c15ab3f8ed68f8e8f5cbe88757848df793e31c36aaa4d4f9783fd08ab", size = 12753508, upload-time = "2025-11-28T15:44:47.538Z" }, + { url = "https://files.pythonhosted.org/packages/67/5a/d035c534ad86e09cee274d53cf0fd769c0b29ca6ed5b32e205be3c06878c/mypy-1.19.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318ba74f75899b0e78b847d8c50821e4c9637c79d9a59680fc1259f29338cb3e", size = 13507553, upload-time = "2025-11-28T15:44:39.26Z" }, + { url = "https://files.pythonhosted.org/packages/6a/17/c4a5498e00071ef29e483a01558b285d086825b61cf1fb2629fbdd019d94/mypy-1.19.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cf7d84f497f78b682edd407f14a7b6e1a2212b433eedb054e2081380b7395aa3", size = 13792898, upload-time = "2025-11-28T15:44:31.102Z" }, + { url = "https://files.pythonhosted.org/packages/67/f6/bb542422b3ee4399ae1cdc463300d2d91515ab834c6233f2fd1d52fa21e0/mypy-1.19.0-cp310-cp310-win_amd64.whl", hash = "sha256:c3385246593ac2b97f155a0e9639be906e73534630f663747c71908dfbf26134", size = 10048835, upload-time = "2025-11-28T15:48:15.744Z" }, + { url = "https://files.pythonhosted.org/packages/0f/d2/010fb171ae5ac4a01cc34fbacd7544531e5ace95c35ca166dd8fd1b901d0/mypy-1.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a31e4c28e8ddb042c84c5e977e28a21195d086aaffaf08b016b78e19c9ef8106", size = 13010563, upload-time = "2025-11-28T15:48:23.975Z" }, + { url = "https://files.pythonhosted.org/packages/41/6b/63f095c9f1ce584fdeb595d663d49e0980c735a1d2004720ccec252c5d47/mypy-1.19.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34ec1ac66d31644f194b7c163d7f8b8434f1b49719d403a5d26c87fff7e913f7", size = 12077037, upload-time = "2025-11-28T15:47:51.582Z" }, + { url = "https://files.pythonhosted.org/packages/d7/83/6cb93d289038d809023ec20eb0b48bbb1d80af40511fa077da78af6ff7c7/mypy-1.19.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cb64b0ba5980466a0f3f9990d1c582bcab8db12e29815ecb57f1408d99b4bff7", size = 12680255, upload-time = "2025-11-28T15:46:57.628Z" }, + { url = "https://files.pythonhosted.org/packages/99/db/d217815705987d2cbace2edd9100926196d6f85bcb9b5af05058d6e3c8ad/mypy-1.19.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:120cffe120cca5c23c03c77f84abc0c14c5d2e03736f6c312480020082f1994b", size = 13421472, upload-time = "2025-11-28T15:47:59.655Z" }, + { url = "https://files.pythonhosted.org/packages/4e/51/d2beaca7c497944b07594f3f8aad8d2f0e8fc53677059848ae5d6f4d193e/mypy-1.19.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7a500ab5c444268a70565e374fc803972bfd1f09545b13418a5174e29883dab7", size = 13651823, upload-time = "2025-11-28T15:45:29.318Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d1/7883dcf7644db3b69490f37b51029e0870aac4a7ad34d09ceae709a3df44/mypy-1.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:c14a98bc63fd867530e8ec82f217dae29d0550c86e70debc9667fff1ec83284e", size = 10049077, upload-time = "2025-11-28T15:45:39.818Z" }, + { url = "https://files.pythonhosted.org/packages/11/7e/1afa8fb188b876abeaa14460dc4983f909aaacaa4bf5718c00b2c7e0b3d5/mypy-1.19.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0fb3115cb8fa7c5f887c8a8d81ccdcb94cff334684980d847e5a62e926910e1d", size = 13207728, upload-time = "2025-11-28T15:46:26.463Z" }, + { url = "https://files.pythonhosted.org/packages/b2/13/f103d04962bcbefb1644f5ccb235998b32c337d6c13145ea390b9da47f3e/mypy-1.19.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3e19e3b897562276bb331074d64c076dbdd3e79213f36eed4e592272dabd760", size = 12202945, upload-time = "2025-11-28T15:48:49.143Z" }, + { url = "https://files.pythonhosted.org/packages/e4/93/a86a5608f74a22284a8ccea8592f6e270b61f95b8588951110ad797c2ddd/mypy-1.19.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9d491295825182fba01b6ffe2c6fe4e5a49dbf4e2bb4d1217b6ced3b4797bc6", size = 12718673, upload-time = "2025-11-28T15:47:37.193Z" }, + { url = "https://files.pythonhosted.org/packages/3d/58/cf08fff9ced0423b858f2a7495001fda28dc058136818ee9dffc31534ea9/mypy-1.19.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6016c52ab209919b46169651b362068f632efcd5eb8ef9d1735f6f86da7853b2", size = 13608336, upload-time = "2025-11-28T15:48:32.625Z" }, + { url = "https://files.pythonhosted.org/packages/64/ed/9c509105c5a6d4b73bb08733102a3ea62c25bc02c51bca85e3134bf912d3/mypy-1.19.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f188dcf16483b3e59f9278c4ed939ec0254aa8a60e8fc100648d9ab5ee95a431", size = 13833174, upload-time = "2025-11-28T15:45:48.091Z" }, + { url = "https://files.pythonhosted.org/packages/cd/71/01939b66e35c6f8cb3e6fdf0b657f0fd24de2f8ba5e523625c8e72328208/mypy-1.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:0e3c3d1e1d62e678c339e7ade72746a9e0325de42cd2cccc51616c7b2ed1a018", size = 10112208, upload-time = "2025-11-28T15:46:41.702Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0d/a1357e6bb49e37ce26fcf7e3cc55679ce9f4ebee0cd8b6ee3a0e301a9210/mypy-1.19.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7686ed65dbabd24d20066f3115018d2dce030d8fa9db01aa9f0a59b6813e9f9e", size = 13191993, upload-time = "2025-11-28T15:47:22.336Z" }, + { url = "https://files.pythonhosted.org/packages/5d/75/8e5d492a879ec4490e6ba664b5154e48c46c85b5ac9785792a5ec6a4d58f/mypy-1.19.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fd4a985b2e32f23bead72e2fb4bbe5d6aceee176be471243bd831d5b2644672d", size = 12174411, upload-time = "2025-11-28T15:44:55.492Z" }, + { url = "https://files.pythonhosted.org/packages/71/31/ad5dcee9bfe226e8eaba777e9d9d251c292650130f0450a280aec3485370/mypy-1.19.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fc51a5b864f73a3a182584b1ac75c404396a17eced54341629d8bdcb644a5bba", size = 12727751, upload-time = "2025-11-28T15:44:14.169Z" }, + { url = "https://files.pythonhosted.org/packages/77/06/b6b8994ce07405f6039701f4b66e9d23f499d0b41c6dd46ec28f96d57ec3/mypy-1.19.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:37af5166f9475872034b56c5efdcf65ee25394e9e1d172907b84577120714364", size = 13593323, upload-time = "2025-11-28T15:46:34.699Z" }, + { url = "https://files.pythonhosted.org/packages/68/b1/126e274484cccdf099a8e328d4fda1c7bdb98a5e888fa6010b00e1bbf330/mypy-1.19.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:510c014b722308c9bd377993bcbf9a07d7e0692e5fa8fc70e639c1eb19fc6bee", size = 13818032, upload-time = "2025-11-28T15:46:18.286Z" }, + { url = "https://files.pythonhosted.org/packages/f8/56/53a8f70f562dfc466c766469133a8a4909f6c0012d83993143f2a9d48d2d/mypy-1.19.0-cp313-cp313-win_amd64.whl", hash = "sha256:cabbee74f29aa9cd3b444ec2f1e4fa5a9d0d746ce7567a6a609e224429781f53", size = 10120644, upload-time = "2025-11-28T15:47:43.99Z" }, + { url = "https://files.pythonhosted.org/packages/b0/f4/7751f32f56916f7f8c229fe902cbdba3e4dd3f3ea9e8b872be97e7fc546d/mypy-1.19.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f2e36bed3c6d9b5f35d28b63ca4b727cb0228e480826ffc8953d1892ddc8999d", size = 13185236, upload-time = "2025-11-28T15:45:20.696Z" }, + { url = "https://files.pythonhosted.org/packages/35/31/871a9531f09e78e8d145032355890384f8a5b38c95a2c7732d226b93242e/mypy-1.19.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a18d8abdda14035c5718acb748faec09571432811af129bf0d9e7b2d6699bf18", size = 12213902, upload-time = "2025-11-28T15:46:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/58/b8/af221910dd40eeefa2077a59107e611550167b9994693fc5926a0b0f87c0/mypy-1.19.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f75e60aca3723a23511948539b0d7ed514dda194bc3755eae0bfc7a6b4887aa7", size = 12738600, upload-time = "2025-11-28T15:44:22.521Z" }, + { url = "https://files.pythonhosted.org/packages/11/9f/c39e89a3e319c1d9c734dedec1183b2cc3aefbab066ec611619002abb932/mypy-1.19.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f44f2ae3c58421ee05fe609160343c25f70e3967f6e32792b5a78006a9d850f", size = 13592639, upload-time = "2025-11-28T15:48:08.55Z" }, + { url = "https://files.pythonhosted.org/packages/97/6d/ffaf5f01f5e284d9033de1267e6c1b8f3783f2cf784465378a86122e884b/mypy-1.19.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:63ea6a00e4bd6822adbfc75b02ab3653a17c02c4347f5bb0cf1d5b9df3a05835", size = 13799132, upload-time = "2025-11-28T15:47:06.032Z" }, + { url = "https://files.pythonhosted.org/packages/fe/b0/c33921e73aaa0106224e5a34822411bea38046188eb781637f5a5b07e269/mypy-1.19.0-cp314-cp314-win_amd64.whl", hash = "sha256:3ad925b14a0bb99821ff6f734553294aa6a3440a8cb082fe1f5b84dfb662afb1", size = 10269832, upload-time = "2025-11-28T15:47:29.392Z" }, + { url = "https://files.pythonhosted.org/packages/09/0e/fe228ed5aeab470c6f4eb82481837fadb642a5aa95cc8215fd2214822c10/mypy-1.19.0-py3-none-any.whl", hash = "sha256:0c01c99d626380752e527d5ce8e69ffbba2046eb8a060db0329690849cf9b6f9", size = 2469714, upload-time = "2025-11-28T15:45:33.22Z" }, ] [[package]] @@ -442,6 +666,9 @@ dev = [ { name = "mkdocs-material" }, { name = "mypy" }, { name = "poethepoet" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-mock" }, { name = "ruff" }, { name = "towncrier" }, ] @@ -455,12 +682,15 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ - { name = "mike", specifier = ">=2.1.2" }, - { name = "mkdocs-material", specifier = ">=9.5.27" }, - { name = "mypy", specifier = "==1.18.2" }, - { name = "poethepoet", specifier = ">=0.37.0" }, - { name = "ruff", specifier = "==0.14.0" }, - { name = "towncrier", specifier = ">=23.11.0" }, + { name = "mike", specifier = ">=2.1.3" }, + { name = "mkdocs-material", specifier = ">=9.7.0" }, + { name = "mypy", specifier = "==1.19.0" }, + { name = "poethepoet", specifier = ">=0.38.0" }, + { name = "pytest", specifier = ">=9.0.2" }, + { name = "pytest-cov", specifier = ">=7.0.0" }, + { name = "pytest-mock", specifier = ">=3.15.1" }, + { name = "ruff", specifier = "==0.14.9" }, + { name = "towncrier", specifier = ">=25.8.0" }, ] [[package]] @@ -501,25 +731,34 @@ wheels = [ [[package]] name = "platformdirs" -version = "4.5.0" +version = "4.5.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/61/33/9611380c2bdb1225fdef633e2a9610622310fed35ab11dac9620972ee088/platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312", size = 21632, upload-time = "2025-10-08T17:44:48.791Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3", size = 18651, upload-time = "2025-10-08T17:44:47.223Z" }, + { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, +] + +[[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 = "poethepoet" -version = "0.37.0" +version = "0.38.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pastel" }, { name = "pyyaml" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a5/f2/273fe54a78dc5c6c8dd63db71f5a6ceb95e4648516b5aeaeff4bde804e44/poethepoet-0.37.0.tar.gz", hash = "sha256:73edf458707c674a079baa46802e21455bda3a7f82a408e58c31b9f4fe8e933d", size = 68570, upload-time = "2025-08-11T18:00:29.103Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4d/14/d1f795f314c4bf3ad6d64216e370bdfda73093ed76e979485778b655a7ac/poethepoet-0.38.0.tar.gz", hash = "sha256:aeeb2f0a2cf0d3afa833976eff3ac7b8f5e472ae64171824900d79d3c68163c7", size = 77339, upload-time = "2025-11-23T13:51:28.246Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/92/1b/5337af1a6a478d25a3e3c56b9b4b42b0a160314e02f4a0498d5322c8dac4/poethepoet-0.37.0-py3-none-any.whl", hash = "sha256:861790276315abcc8df1b4bd60e28c3d48a06db273edd3092f3c94e1a46e5e22", size = 90062, upload-time = "2025-08-11T18:00:27.595Z" }, + { url = "https://files.pythonhosted.org/packages/38/89/2bf7d43ef4b0d60f446933ae9d3649f95c2c45c47b6736d121b602c28361/poethepoet-0.38.0-py3-none-any.whl", hash = "sha256:214bd9fcb348ff3dfd1466579d67e0c02242451a7044aced1a79641adef9cad0", size = 101938, upload-time = "2025-11-23T13:51:26.518Z" }, ] [[package]] @@ -533,15 +772,15 @@ wheels = [ [[package]] name = "pymdown-extensions" -version = "10.16.1" +version = "10.19.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown" }, { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/55/b3/6d2b3f149bc5413b0a29761c2c5832d8ce904a1d7f621e86616d96f505cc/pymdown_extensions-10.16.1.tar.gz", hash = "sha256:aace82bcccba3efc03e25d584e6a22d27a8e17caa3f4dd9f207e49b787aa9a91", size = 853277, upload-time = "2025-07-28T16:19:34.167Z" } +sdist = { url = "https://files.pythonhosted.org/packages/72/2d/9f30cee56d4d6d222430d401e85b0a6a1ae229819362f5786943d1a8c03b/pymdown_extensions-10.19.1.tar.gz", hash = "sha256:4969c691009a389fb1f9712dd8e7bd70dcc418d15a0faf70acb5117d022f7de8", size = 847839, upload-time = "2025-12-14T17:25:24.42Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e4/06/43084e6cbd4b3bc0e80f6be743b2e79fbc6eed8de9ad8c629939fa55d972/pymdown_extensions-10.16.1-py3-none-any.whl", hash = "sha256:d6ba157a6c03146a7fb122b2b9a121300056384eafeec9c9f9e584adfdb2a32d", size = 266178, upload-time = "2025-07-28T16:19:31.401Z" }, + { url = "https://files.pythonhosted.org/packages/fb/35/b763e8fbcd51968329b9adc52d188fc97859f85f2ee15fe9f379987d99c5/pymdown_extensions-10.19.1-py3-none-any.whl", hash = "sha256:e8698a66055b1dc0dca2a7f2c9d0ea6f5faa7834a9c432e3535ab96c0c4e509b", size = 266693, upload-time = "2025-12-14T17:25:22.999Z" }, ] [[package]] @@ -553,6 +792,50 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/10/5e/1aa9a93198c6b64513c9d7752de7422c06402de6600a8767da1524f9570b/pyparsing-3.2.5-py3-none-any.whl", hash = "sha256:e38a4f02064cf41fe6593d328d0512495ad1f3d8a91c4f73fc401b3079a59a5e", size = 113890, upload-time = "2025-09-21T04:11:04.117Z" }, ] +[[package]] +name = "pytest" +version = "9.0.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/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[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 = "python-dateutil" version = "2.9.0.post0" @@ -658,28 +941,28 @@ wheels = [ [[package]] name = "ruff" -version = "0.14.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/41/b9/9bd84453ed6dd04688de9b3f3a4146a1698e8faae2ceeccce4e14c67ae17/ruff-0.14.0.tar.gz", hash = "sha256:62ec8969b7510f77945df916de15da55311fade8d6050995ff7f680afe582c57", size = 5452071, upload-time = "2025-10-07T18:21:55.763Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/4e/79d463a5f80654e93fa653ebfb98e0becc3f0e7cf6219c9ddedf1e197072/ruff-0.14.0-py3-none-linux_armv6l.whl", hash = "sha256:58e15bffa7054299becf4bab8a1187062c6f8cafbe9f6e39e0d5aface455d6b3", size = 12494532, upload-time = "2025-10-07T18:21:00.373Z" }, - { url = "https://files.pythonhosted.org/packages/ee/40/e2392f445ed8e02aa6105d49db4bfff01957379064c30f4811c3bf38aece/ruff-0.14.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:838d1b065f4df676b7c9957992f2304e41ead7a50a568185efd404297d5701e8", size = 13160768, upload-time = "2025-10-07T18:21:04.73Z" }, - { url = "https://files.pythonhosted.org/packages/75/da/2a656ea7c6b9bd14c7209918268dd40e1e6cea65f4bb9880eaaa43b055cd/ruff-0.14.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:703799d059ba50f745605b04638fa7e9682cc3da084b2092feee63500ff3d9b8", size = 12363376, upload-time = "2025-10-07T18:21:07.833Z" }, - { url = "https://files.pythonhosted.org/packages/42/e2/1ffef5a1875add82416ff388fcb7ea8b22a53be67a638487937aea81af27/ruff-0.14.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ba9a8925e90f861502f7d974cc60e18ca29c72bb0ee8bfeabb6ade35a3abde7", size = 12608055, upload-time = "2025-10-07T18:21:10.72Z" }, - { url = "https://files.pythonhosted.org/packages/4a/32/986725199d7cee510d9f1dfdf95bf1efc5fa9dd714d0d85c1fb1f6be3bc3/ruff-0.14.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e41f785498bd200ffc276eb9e1570c019c1d907b07cfb081092c8ad51975bbe7", size = 12318544, upload-time = "2025-10-07T18:21:13.741Z" }, - { url = "https://files.pythonhosted.org/packages/9a/ed/4969cefd53315164c94eaf4da7cfba1f267dc275b0abdd593d11c90829a3/ruff-0.14.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30a58c087aef4584c193aebf2700f0fbcfc1e77b89c7385e3139956fa90434e2", size = 14001280, upload-time = "2025-10-07T18:21:16.411Z" }, - { url = "https://files.pythonhosted.org/packages/ab/ad/96c1fc9f8854c37681c9613d825925c7f24ca1acfc62a4eb3896b50bacd2/ruff-0.14.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f8d07350bc7af0a5ce8812b7d5c1a7293cf02476752f23fdfc500d24b79b783c", size = 15027286, upload-time = "2025-10-07T18:21:19.577Z" }, - { url = "https://files.pythonhosted.org/packages/b3/00/1426978f97df4fe331074baf69615f579dc4e7c37bb4c6f57c2aad80c87f/ruff-0.14.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eec3bbbf3a7d5482b5c1f42d5fc972774d71d107d447919fca620b0be3e3b75e", size = 14451506, upload-time = "2025-10-07T18:21:22.779Z" }, - { url = "https://files.pythonhosted.org/packages/58/d5/9c1cea6e493c0cf0647674cca26b579ea9d2a213b74b5c195fbeb9678e15/ruff-0.14.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16b68e183a0e28e5c176d51004aaa40559e8f90065a10a559176713fcf435206", size = 13437384, upload-time = "2025-10-07T18:21:25.758Z" }, - { url = "https://files.pythonhosted.org/packages/29/b4/4cd6a4331e999fc05d9d77729c95503f99eae3ba1160469f2b64866964e3/ruff-0.14.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb732d17db2e945cfcbbc52af0143eda1da36ca8ae25083dd4f66f1542fdf82e", size = 13447976, upload-time = "2025-10-07T18:21:28.83Z" }, - { url = "https://files.pythonhosted.org/packages/3b/c0/ac42f546d07e4f49f62332576cb845d45c67cf5610d1851254e341d563b6/ruff-0.14.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:c958f66ab884b7873e72df38dcabee03d556a8f2ee1b8538ee1c2bbd619883dd", size = 13682850, upload-time = "2025-10-07T18:21:31.842Z" }, - { url = "https://files.pythonhosted.org/packages/5f/c4/4b0c9bcadd45b4c29fe1af9c5d1dc0ca87b4021665dfbe1c4688d407aa20/ruff-0.14.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7eb0499a2e01f6e0c285afc5bac43ab380cbfc17cd43a2e1dd10ec97d6f2c42d", size = 12449825, upload-time = "2025-10-07T18:21:35.074Z" }, - { url = "https://files.pythonhosted.org/packages/4b/a8/e2e76288e6c16540fa820d148d83e55f15e994d852485f221b9524514730/ruff-0.14.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4c63b2d99fafa05efca0ab198fd48fa6030d57e4423df3f18e03aa62518c565f", size = 12272599, upload-time = "2025-10-07T18:21:38.08Z" }, - { url = "https://files.pythonhosted.org/packages/18/14/e2815d8eff847391af632b22422b8207704222ff575dec8d044f9ab779b2/ruff-0.14.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:668fce701b7a222f3f5327f86909db2bbe99c30877c8001ff934c5413812ac02", size = 13193828, upload-time = "2025-10-07T18:21:41.216Z" }, - { url = "https://files.pythonhosted.org/packages/44/c6/61ccc2987cf0aecc588ff8f3212dea64840770e60d78f5606cd7dc34de32/ruff-0.14.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a86bf575e05cb68dcb34e4c7dfe1064d44d3f0c04bbc0491949092192b515296", size = 13628617, upload-time = "2025-10-07T18:21:44.04Z" }, - { url = "https://files.pythonhosted.org/packages/73/e6/03b882225a1b0627e75339b420883dc3c90707a8917d2284abef7a58d317/ruff-0.14.0-py3-none-win32.whl", hash = "sha256:7450a243d7125d1c032cb4b93d9625dea46c8c42b4f06c6b709baac168e10543", size = 12367872, upload-time = "2025-10-07T18:21:46.67Z" }, - { url = "https://files.pythonhosted.org/packages/41/77/56cf9cf01ea0bfcc662de72540812e5ba8e9563f33ef3d37ab2174892c47/ruff-0.14.0-py3-none-win_amd64.whl", hash = "sha256:ea95da28cd874c4d9c922b39381cbd69cb7e7b49c21b8152b014bd4f52acddc2", size = 13464628, upload-time = "2025-10-07T18:21:50.318Z" }, - { url = "https://files.pythonhosted.org/packages/c6/2a/65880dfd0e13f7f13a775998f34703674a4554906167dce02daf7865b954/ruff-0.14.0-py3-none-win_arm64.whl", hash = "sha256:f42c9495f5c13ff841b1da4cb3c2a42075409592825dada7c5885c2c844ac730", size = 12565142, upload-time = "2025-10-07T18:21:53.577Z" }, +version = "0.14.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/1b/ab712a9d5044435be8e9a2beb17cbfa4c241aa9b5e4413febac2a8b79ef2/ruff-0.14.9.tar.gz", hash = "sha256:35f85b25dd586381c0cc053f48826109384c81c00ad7ef1bd977bfcc28119d5b", size = 5809165, upload-time = "2025-12-11T21:39:47.381Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/1c/d1b1bba22cffec02351c78ab9ed4f7d7391876e12720298448b29b7229c1/ruff-0.14.9-py3-none-linux_armv6l.whl", hash = "sha256:f1ec5de1ce150ca6e43691f4a9ef5c04574ad9ca35c8b3b0e18877314aba7e75", size = 13576541, upload-time = "2025-12-11T21:39:14.806Z" }, + { url = "https://files.pythonhosted.org/packages/94/ab/ffe580e6ea1fca67f6337b0af59fc7e683344a43642d2d55d251ff83ceae/ruff-0.14.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ed9d7417a299fc6030b4f26333bf1117ed82a61ea91238558c0268c14e00d0c2", size = 13779363, upload-time = "2025-12-11T21:39:20.29Z" }, + { url = "https://files.pythonhosted.org/packages/7d/f8/2be49047f929d6965401855461e697ab185e1a6a683d914c5c19c7962d9e/ruff-0.14.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d5dc3473c3f0e4a1008d0ef1d75cee24a48e254c8bed3a7afdd2b4392657ed2c", size = 12925292, upload-time = "2025-12-11T21:39:38.757Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e9/08840ff5127916bb989c86f18924fd568938b06f58b60e206176f327c0fe/ruff-0.14.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84bf7c698fc8f3cb8278830fb6b5a47f9bcc1ed8cb4f689b9dd02698fa840697", size = 13362894, upload-time = "2025-12-11T21:39:02.524Z" }, + { url = "https://files.pythonhosted.org/packages/31/1c/5b4e8e7750613ef43390bb58658eaf1d862c0cc3352d139cd718a2cea164/ruff-0.14.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aa733093d1f9d88a5d98988d8834ef5d6f9828d03743bf5e338bf980a19fce27", size = 13311482, upload-time = "2025-12-11T21:39:17.51Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3a/459dce7a8cb35ba1ea3e9c88f19077667a7977234f3b5ab197fad240b404/ruff-0.14.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a1cfb04eda979b20c8c19550c8b5f498df64ff8da151283311ce3199e8b3648", size = 14016100, upload-time = "2025-12-11T21:39:41.948Z" }, + { url = "https://files.pythonhosted.org/packages/a6/31/f064f4ec32524f9956a0890fc6a944e5cf06c63c554e39957d208c0ffc45/ruff-0.14.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1e5cb521e5ccf0008bd74d5595a4580313844a42b9103b7388eca5a12c970743", size = 15477729, upload-time = "2025-12-11T21:39:23.279Z" }, + { url = "https://files.pythonhosted.org/packages/7a/6d/f364252aad36ccd443494bc5f02e41bf677f964b58902a17c0b16c53d890/ruff-0.14.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd429a8926be6bba4befa8cdcf3f4dd2591c413ea5066b1e99155ed245ae42bb", size = 15122386, upload-time = "2025-12-11T21:39:33.125Z" }, + { url = "https://files.pythonhosted.org/packages/20/02/e848787912d16209aba2799a4d5a1775660b6a3d0ab3944a4ccc13e64a02/ruff-0.14.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab208c1b7a492e37caeaf290b1378148f75e13c2225af5d44628b95fd7834273", size = 14497124, upload-time = "2025-12-11T21:38:59.33Z" }, + { url = "https://files.pythonhosted.org/packages/f3/51/0489a6a5595b7760b5dbac0dd82852b510326e7d88d51dbffcd2e07e3ff3/ruff-0.14.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72034534e5b11e8a593f517b2f2f2b273eb68a30978c6a2d40473ad0aaa4cb4a", size = 14195343, upload-time = "2025-12-11T21:39:44.866Z" }, + { url = "https://files.pythonhosted.org/packages/f6/53/3bb8d2fa73e4c2f80acc65213ee0830fa0c49c6479313f7a68a00f39e208/ruff-0.14.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:712ff04f44663f1b90a1195f51525836e3413c8a773574a7b7775554269c30ed", size = 14346425, upload-time = "2025-12-11T21:39:05.927Z" }, + { url = "https://files.pythonhosted.org/packages/ad/04/bdb1d0ab876372da3e983896481760867fc84f969c5c09d428e8f01b557f/ruff-0.14.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a111fee1db6f1d5d5810245295527cda1d367c5aa8f42e0fca9a78ede9b4498b", size = 13258768, upload-time = "2025-12-11T21:39:08.691Z" }, + { url = "https://files.pythonhosted.org/packages/40/d9/8bf8e1e41a311afd2abc8ad12be1b6c6c8b925506d9069b67bb5e9a04af3/ruff-0.14.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8769efc71558fecc25eb295ddec7d1030d41a51e9dcf127cbd63ec517f22d567", size = 13326939, upload-time = "2025-12-11T21:39:53.842Z" }, + { url = "https://files.pythonhosted.org/packages/f4/56/a213fa9edb6dd849f1cfbc236206ead10913693c72a67fb7ddc1833bf95d/ruff-0.14.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:347e3bf16197e8a2de17940cd75fd6491e25c0aa7edf7d61aa03f146a1aa885a", size = 13578888, upload-time = "2025-12-11T21:39:35.988Z" }, + { url = "https://files.pythonhosted.org/packages/33/09/6a4a67ffa4abae6bf44c972a4521337ffce9cbc7808faadede754ef7a79c/ruff-0.14.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7715d14e5bccf5b660f54516558aa94781d3eb0838f8e706fb60e3ff6eff03a8", size = 14314473, upload-time = "2025-12-11T21:39:50.78Z" }, + { url = "https://files.pythonhosted.org/packages/12/0d/15cc82da5d83f27a3c6b04f3a232d61bc8c50d38a6cd8da79228e5f8b8d6/ruff-0.14.9-py3-none-win32.whl", hash = "sha256:df0937f30aaabe83da172adaf8937003ff28172f59ca9f17883b4213783df197", size = 13202651, upload-time = "2025-12-11T21:39:26.628Z" }, + { url = "https://files.pythonhosted.org/packages/32/f7/c78b060388eefe0304d9d42e68fab8cffd049128ec466456cef9b8d4f06f/ruff-0.14.9-py3-none-win_amd64.whl", hash = "sha256:c0b53a10e61df15a42ed711ec0bda0c582039cf6c754c49c020084c55b5b0bc2", size = 14702079, upload-time = "2025-12-11T21:39:11.954Z" }, + { url = "https://files.pythonhosted.org/packages/26/09/7a9520315decd2334afa65ed258fed438f070e31f05a2e43dd480a5e5911/ruff-0.14.9-py3-none-win_arm64.whl", hash = "sha256:8e821c366517a074046d92f0e9213ed1c13dbc5b37a7fc20b07f79b64d62cc84", size = 13744730, upload-time = "2025-12-11T21:39:29.659Z" }, ] [[package]] @@ -765,11 +1048,11 @@ wheels = [ [[package]] name = "urllib3" -version = "2.5.0" +version = "2.6.2" 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" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/24/a2a2ed9addd907787d7aa0355ba36a6cadf1768b934c652ea78acbd59dcd/urllib3-2.6.2.tar.gz", hash = "sha256:016f9c98bb7e98085cb2b4b17b87d2c702975664e4f060c6532e64d1c1a5e797", size = 432930, upload-time = "2025-12-11T15:56:40.252Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl", hash = "sha256:ec21cddfe7724fc7cb4ba4bea7aa8e2ef36f607a4bab81aa6ce42a13dc3f03dd", size = 131182, upload-time = "2025-12-11T15:56:38.584Z" }, ] [[package]]