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]]