From 188aa3e0fa27f5ff0ec35c7cd9b9fe8140ca7422 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Zar=C4=99bski?= Date: Thu, 4 Sep 2025 23:39:39 +0100 Subject: [PATCH 01/23] Started advanced linting --- poetry.lock | 68 ++++++- pyproject.toml | 19 +- simvue/__init__.py | 12 +- simvue/api/objects/artifact/fetch.py | 21 +-- simvue/api/objects/base.py | 48 ++--- simvue/api/objects/folder.py | 13 +- simvue/api/objects/run.py | 30 ++-- simvue/api/request.py | 11 +- simvue/client.py | 257 +++++++++++++++------------ simvue/converters.py | 129 +++++++------- simvue/exception.py | 15 +- 11 files changed, 374 insertions(+), 249 deletions(-) diff --git a/poetry.lock b/poetry.lock index 38084522..633e7feb 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand. [[package]] name = "annotated-types" @@ -32,6 +32,21 @@ docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphi tests = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\""] +[[package]] +name = "basedpyright" +version = "1.31.4" +description = "static type checking for Python (but based)" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "basedpyright-1.31.4-py3-none-any.whl", hash = "sha256:055e4a38024bd653be12d6216c1cfdbee49a1096d342b4d5f5b4560f7714b6fc"}, + {file = "basedpyright-1.31.4.tar.gz", hash = "sha256:2450deb16530f7c88c1a7da04530a079f9b0b18ae1c71cb6f812825b3b82d0b1"}, +] + +[package.dependencies] +nodejs-wheel-binaries = ">=20.13.1" + [[package]] name = "certifi" version = "2025.7.14" @@ -1008,13 +1023,30 @@ pyspark = ["pyspark (>=3.5.0)"] pyspark-connect = ["pyspark[connect] (>=3.5.0)"] sqlframe = ["sqlframe (>=3.22.0)"] +[[package]] +name = "nodejs-wheel-binaries" +version = "22.18.0" +description = "unoffical Node.js package" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "nodejs_wheel_binaries-22.18.0-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:53b04495857755c5d5658f7ac969d84f25898fe0b0c1bdc41172e5e0ac6105ca"}, + {file = "nodejs_wheel_binaries-22.18.0-py2.py3-none-macosx_11_0_x86_64.whl", hash = "sha256:bd4d016257d4dfe604ed526c19bd4695fdc4f4cc32e8afc4738111447aa96d03"}, + {file = "nodejs_wheel_binaries-22.18.0-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3b125f94f3f5e8ab9560d3bd637497f02e45470aeea74cf6fe60afe751cfa5f"}, + {file = "nodejs_wheel_binaries-22.18.0-py2.py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78bbb81b6e67c15f04e2a9c6c220d7615fb46ae8f1ad388df0d66abac6bed5f8"}, + {file = "nodejs_wheel_binaries-22.18.0-py2.py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f5d3ea8b7f957ae16b73241451f6ce831d6478156f363cce75c7ea71cbe6c6f7"}, + {file = "nodejs_wheel_binaries-22.18.0-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:bcda35b07677039670102a6f9b78c2313fd526111d407cb7ffc2a4c243a48ef9"}, + {file = "nodejs_wheel_binaries-22.18.0-py2.py3-none-win_amd64.whl", hash = "sha256:0f55e72733f1df2f542dce07f35145ac2e125408b5e2051cac08e5320e41b4d1"}, +] + [[package]] name = "numpy" version = "2.2.6" description = "Fundamental package for array computing in Python" optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["main", "dev"] markers = "python_version == \"3.10\"" files = [ {file = "numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb"}, @@ -1080,7 +1112,7 @@ version = "2.3.1" description = "Fundamental package for array computing in Python" optional = false python-versions = ">=3.11" -groups = ["main"] +groups = ["main", "dev"] markers = "python_version >= \"3.11\"" files = [ {file = "numpy-2.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6ea9e48336a402551f52cd8f593343699003d2353daa4b72ce8d34f66b722070"}, @@ -1236,6 +1268,22 @@ sql-other = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-d test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"] xml = ["lxml (>=4.9.2)"] +[[package]] +name = "pandas-stubs" +version = "2.3.2.250827" +description = "Type annotations for pandas" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "pandas_stubs-2.3.2.250827-py3-none-any.whl", hash = "sha256:3d613013b4189147a9a6bb18d8bec1e5b137de091496e9b9ff9f137ec3e223a9"}, + {file = "pandas_stubs-2.3.2.250827.tar.gz", hash = "sha256:bcc2d49a2766325e4a1a492c3eeda879e9521bb5e26e69e2bbf13e80e7ef569a"}, +] + +[package.dependencies] +numpy = ">=1.23.5" +types-pytz = ">=2022.1.1" + [[package]] name = "pillow" version = "11.3.0" @@ -2129,6 +2177,18 @@ files = [ {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, ] +[[package]] +name = "types-pytz" +version = "2025.2.0.20250809" +description = "Typing stubs for pytz" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "types_pytz-2025.2.0.20250809-py3-none-any.whl", hash = "sha256:4f55ed1b43e925cf851a756fe1707e0f5deeb1976e15bf844bcaa025e8fbd0db"}, + {file = "types_pytz-2025.2.0.20250809.tar.gz", hash = "sha256:222e32e6a29bb28871f8834e8785e3801f2dc4441c715cd2082b271eecbe21e5"}, +] + [[package]] name = "types-requests" version = "2.32.4.20250611" @@ -2208,4 +2268,4 @@ plot = ["matplotlib", "plotly"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<3.14" -content-hash = "5a006e36fde3605b7ef53b51c55d4b4faa639d3b4617611650728effda1701d6" +content-hash = "59ea3cfb087445307fd430371d134b797fc49b48fd9ea9730c232ec65bb78982" diff --git a/pyproject.toml b/pyproject.toml index 1cd4eb13..40d4298b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -81,15 +81,23 @@ jinja2 = "^3.1.6" types-requests = "^2.32.0.20241016" interrogate = "^1.7.0" pytest-timeout = "^2.3.1" +pandas-stubs = "^2.3.2.250827" +basedpyright = "^1.31.4" [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" [tool.ruff] -lint.extend-select = ["C901", "T201"] -lint.mccabe.max-complexity = 11 extend-exclude = ["tests", "examples", "notebooks"] +target-version = "py313" +line-length = 88 + +[tool.ruff.lint] +mccabe.max-complexity = 11 +select = ["ALL"] +ignore = ["COM812", "D105", "TRY003", "EM101", "FIX002"] +pydocstyle.convention = "numpy" [tool.pytest.ini_options] addopts = "-p no:warnings --no-cov -n 0" @@ -121,5 +129,12 @@ fail-under = 95 verbose = 1 exclude = ["docs", "tests"] +[tool.basedpyright] +reportImplicitStringConcatenation = "none" +reportUnreachable = "none" +reportUnknownMemberType = "none" +reportUnknownVariableType = "none" +reportMissingTypeStubs = "none" + [tool.mypy] ignore_missing_imports = true diff --git a/simvue/__init__.py b/simvue/__init__.py index 267add6e..c277940b 100644 --- a/simvue/__init__.py +++ b/simvue/__init__.py @@ -1,4 +1,8 @@ -from simvue.client import Client as Client -from simvue.handler import Handler as Handler -from simvue.models import RunInput as RunInput -from simvue.run import Run as Run +"""Simvue Python API.""" + +from simvue.client import Client +from simvue.handler import Handler +from simvue.models import RunInput +from simvue.run import Run + +__all__ = ["Client", "Handler", "Run", "RunInput"] diff --git a/simvue/api/objects/artifact/fetch.py b/simvue/api/objects/artifact/fetch.py index 37f5a7f6..58c1011a 100644 --- a/simvue/api/objects/artifact/fetch.py +++ b/simvue/api/objects/artifact/fetch.py @@ -1,16 +1,19 @@ import http +import json import typing +from collections.abc import Generator + import pydantic -import json from simvue.api.objects.artifact.base import ArtifactBase -from simvue.api.objects.base import Sort -from .file import FileArtifact from simvue.api.objects.artifact.object import ObjectArtifact -from simvue.api.request import get_json_from_response, get as sv_get +from simvue.api.objects.base import Sort +from simvue.api.request import get as sv_get +from simvue.api.request import get_json_from_response from simvue.api.url import URL from simvue.exception import ObjectNotFoundError +from .file import FileArtifact __all__ = ["Artifact"] @@ -44,8 +47,7 @@ def __new__(cls, identifier: str | None = None, **kwargs): _artifact_pre = ArtifactBase(identifier=identifier, **kwargs) if _artifact_pre.original_path: return FileArtifact(identifier=identifier, **kwargs) - else: - return ObjectArtifact(identifier=identifier, **kwargs) + return ObjectArtifact(identifier=identifier, **kwargs) @classmethod def from_run( @@ -53,7 +55,7 @@ def from_run( run_id: str, category: typing.Literal["input", "output", "code"] | None = None, **kwargs, - ) -> typing.Generator[tuple[str, FileArtifact | ObjectArtifact], None, None]: + ) -> typing.Generator[tuple[str, FileArtifact | ObjectArtifact]]: """Return artifacts associated with a given run. Parameters @@ -109,7 +111,7 @@ def from_run( @classmethod def from_name( cls, run_id: str, name: str, **kwargs - ) -> typing.Union[FileArtifact | ObjectArtifact, None]: + ) -> FileArtifact | ObjectArtifact | None: """Retrieve an artifact by name. Parameters @@ -162,7 +164,7 @@ def get( offset: int | None = None, sorting: list[ArtifactSort] | None = None, **kwargs, - ) -> typing.Generator[tuple[str, FileArtifact | ObjectArtifact], None, None]: + ) -> Generator[tuple[str, FileArtifact | ObjectArtifact]]: """Returns artifacts associated with the current user. Parameters @@ -180,7 +182,6 @@ def get( identifier for artifact the artifact itself as a class instance """ - _class_instance = ArtifactBase(_local=True, _read_only=True) _url = f"{_class_instance._base_url}" _params = {"start": offset, "count": count} diff --git a/simvue/api/objects/base.py b/simvue/api/objects/base.py index e26c6481..91008094 100644 --- a/simvue/api/objects/base.py +++ b/simvue/api/objects/base.py @@ -6,38 +6,47 @@ """ import abc +import http +import inspect +import json +import logging import pathlib import types import typing -import inspect import uuid -import http -import json -import logging +from collections.abc import Generator import msgpack import pydantic -from simvue.utilities import staging_merger -from simvue.config.user import SimvueConfiguration -from simvue.exception import ObjectNotFoundError -from simvue.version import __version__ +from simvue.api.request import ( + delete as sv_delete, +) from simvue.api.request import ( get as sv_get, +) +from simvue.api.request import ( + get_json_from_response, get_paginated, +) +from simvue.api.request import ( post as sv_post, +) +from simvue.api.request import ( put as sv_put, - delete as sv_delete, - get_json_from_response, ) from simvue.api.url import URL +from simvue.config.user import SimvueConfiguration +from simvue.exception import ObjectNotFoundError +from simvue.utilities import staging_merger +from simvue.version import __version__ logging.basicConfig(level=logging.INFO) try: from typing import Self except ImportError: - from typing_extensions import Self + from typing import Self # Need to use this inside of Generator typing to fix bug present in Python 3.10 - see issue #745 T = typing.TypeVar("T", bound="SimvueObject") @@ -367,7 +376,7 @@ def new(cls, **_) -> Self: @classmethod def ids( cls, count: int | None = None, offset: int | None = None, **kwargs - ) -> typing.Generator[str, None, None]: + ) -> typing.Generator[str]: """Retrieve a list of all object identifiers. Parameters @@ -378,7 +387,7 @@ def ids( set start index for objects list Yields - ------- + ------ str identifiers for all objects of this type. """ @@ -402,7 +411,7 @@ def get( count: pydantic.PositiveInt | None = None, offset: pydantic.NonNegativeInt | None = None, **kwargs, - ) -> typing.Generator[tuple[str, T | None], None, None]: + ) -> Generator[tuple[str, Self]]: """Retrieve items of this object type from the server. Parameters @@ -419,7 +428,7 @@ def get( Returns ------- - Generator[tuple[str, SimvueObject | None], None, None] + Generator[tuple[str, SimvueObject]] """ _class_instance = cls(_read_only=True, _local=True) _count: int = 0 @@ -462,13 +471,12 @@ def count(cls, **kwargs) -> int: @classmethod def _get_all_objects( cls, offset: int | None, count: int | None, **kwargs - ) -> typing.Generator[dict, None, None]: + ) -> typing.Generator[dict]: _class_instance = cls(_read_only=True) _url = f"{_class_instance._base_url}" _label = _class_instance.__class__.__name__.lower() - if _label.endswith("s"): - _label = _label[:-1] + _label = _label.removesuffix("s") for response in get_paginated( _url, headers=_class_instance._headers, offset=offset, count=count, **kwargs @@ -506,7 +514,7 @@ def commit(self) -> dict | None: f"Writing updates to staging file for {self._label} '{self.id}': {self._staging}" ) self._cache() - return + return None _response: dict | None = None @@ -620,7 +628,6 @@ def delete(self, **kwargs) -> dict[str, typing.Any]: dict[str, Any] response from server on deletion. """ - if self._get_local_staged(): self._local_staging_file.unlink(missing_ok=True) @@ -710,7 +717,6 @@ def on_reconnect(self, id_mapping: dict[str, str]) -> None: In this case no action is taken. """ - pass @property def staged(self) -> dict[str, typing.Any] | None: diff --git a/simvue/api/objects/folder.py b/simvue/api/objects/folder.py index 6a8fc9aa..ccba6010 100644 --- a/simvue/api/objects/folder.py +++ b/simvue/api/objects/folder.py @@ -7,23 +7,24 @@ """ -import pathlib -import typing import datetime import json +import pathlib +import typing +from collections.abc import Generator import pydantic from simvue.exception import ObjectNotFoundError +from simvue.models import DATETIME_FORMAT, FOLDER_REGEX -from .base import SimvueObject, staging_check, write_only, Sort -from simvue.models import FOLDER_REGEX, DATETIME_FORMAT +from .base import SimvueObject, Sort, staging_check, write_only # Need to use this inside of Generator typing to fix bug present in Python 3.10 - see issue #745 try: from typing import Self except ImportError: - from typing_extensions import Self + from typing import Self __all__ = ["Folder"] @@ -93,7 +94,7 @@ def get( offset: pydantic.NonNegativeInt | None = None, sorting: list[FolderSort] | None = None, **kwargs, - ) -> typing.Generator[tuple[str, T | None], None, None]: + ) -> Generator[tuple[str, Self]]: _params: dict[str, str] = kwargs if sorting: diff --git a/simvue/api/objects/run.py b/simvue/api/objects/run.py index c00b3f9e..2962c03f 100644 --- a/simvue/api/objects/run.py +++ b/simvue/api/objects/run.py @@ -7,26 +7,33 @@ """ +import datetime import http +import json +import time import typing +from collections.abc import Generator + import pydantic -import datetime -import time -import json try: from typing import Self except ImportError: - from typing_extensions import Self + from typing import Self -from .base import SimvueObject, Sort, staging_check, Visibility, write_only from simvue.api.request import ( get as sv_get, - put as sv_put, +) +from simvue.api.request import ( get_json_from_response, ) +from simvue.api.request import ( + put as sv_put, +) from simvue.api.url import URL -from simvue.models import FOLDER_REGEX, NAME_REGEX, DATETIME_FORMAT +from simvue.models import DATETIME_FORMAT, FOLDER_REGEX, NAME_REGEX + +from .base import SimvueObject, Sort, Visibility, staging_check, write_only Status = typing.Literal[ "lost", "failed", "completed", "terminated", "running", "created" @@ -103,7 +110,6 @@ def new( Examples -------- - ```python run = Run.new( folder="/", @@ -325,7 +331,7 @@ def get( offset: pydantic.NonNegativeInt | None = None, sorting: list[RunSort] | None = None, **kwargs, - ) -> typing.Generator[tuple[str, T | None], None, None]: + ) -> Generator[tuple[str, Self]] | None: """Get runs from the server. Parameters @@ -356,7 +362,7 @@ def get( def alerts(self, alerts: list[str]) -> None: self._staging["alerts"] = list(set(self._staging.get("alerts", []) + alerts)) - def get_alert_details(self) -> typing.Generator[dict[str, typing.Any], None, None]: + def get_alert_details(self) -> typing.Generator[dict[str, typing.Any]]: """Retrieve the full details of alerts for this run. Yields @@ -445,7 +451,7 @@ def endtime(self, endtime: datetime.datetime) -> None: @property def metrics( self, - ) -> typing.Generator[tuple[str, dict[str, int | float | bool]], None, None]: + ) -> typing.Generator[tuple[str, dict[str, int | float | bool]]]: """Retrieve metrics for this run from the server. Yields @@ -462,7 +468,7 @@ def metrics( @property def events( self, - ) -> typing.Generator[tuple[str, dict[str, typing.Any]], None, None]: + ) -> typing.Generator[tuple[str, dict[str, typing.Any]]]: """Returns events information for this run from the server. Yields diff --git a/simvue/api/request.py b/simvue/api/request.py index fff056ed..5e0c34f2 100644 --- a/simvue/api/request.py +++ b/simvue/api/request.py @@ -8,10 +8,10 @@ """ import copy +import http import json as json_module -import typing import logging -import http +import typing import requests from tenacity import ( @@ -20,6 +20,7 @@ stop_after_attempt, wait_exponential, ) + from simvue.utilities import parse_validation_response DEFAULT_API_TIMEOUT = 10 @@ -239,8 +240,8 @@ def get_json_from_response( scenario: str, response: requests.Response, allow_parse_failure: bool = False, - expected_type: typing.Type[dict | list] = dict, -) -> dict | list: + expected_type: type[dict[str, object] | list[object]] = dict, +) -> dict[str, object] | list[object]: try: json_response = response.json() json_response = json_response or ({} if expected_type is dict else []) @@ -284,7 +285,7 @@ def get_paginated( offset: int | None = None, count: int | None = None, **params, -) -> typing.Generator[requests.Response, None, None]: +) -> typing.Generator[requests.Response]: """Paginate results of a server query. Parameters diff --git a/simvue/client.py b/simvue/client.py index 88ee074e..c7705a67 100644 --- a/simvue/client.py +++ b/simvue/client.py @@ -1,66 +1,70 @@ -""" -Simvue Client -============= +"""Simvue Client. Contains a Simvue client class for interacting with existing objects on the server including deletion and retrieval. """ import contextlib +import http import json import logging import pathlib import typing -import http -import pydantic +from collections.abc import Generator from concurrent.futures import ThreadPoolExecutor, as_completed -from pandas import DataFrame +import pydantic import requests +from pandas import DataFrame from simvue.api.objects.alert.base import AlertBase +from simvue.api.objects.artifact.fetch import ArtifactSort +from simvue.api.objects.folder import FolderSort +from simvue.api.objects.run import RunSort from simvue.exception import ObjectNotFoundError -from .converters import ( - aggregated_metrics_to_dataframe, - to_dataframe, - parse_run_set_metrics, -) -from .serialization import deserialize_data -from .simvue_types import DeserializedContent -from .utilities import check_extra, prettify_pydantic -from .models import FOLDER_REGEX, NAME_REGEX -from .config.user import SimvueConfiguration -from .api.request import get_json_from_response from .api.objects import ( - Run, - Folder, - Tag, - Artifact, Alert, + Artifact, FileArtifact, + Folder, ObjectArtifact, + Run, + Tag, get_folder_from_path, ) +from .api.request import DEFAULT_API_TIMEOUT, get_json_from_response +from .config.user import SimvueConfiguration +from .converters import ( + aggregated_metrics_to_dataframe, + parse_run_set_metrics, + to_dataframe, +) +from .models import FOLDER_REGEX, NAME_REGEX +from .serialization import deserialize_data +from .utilities import check_extra, prettify_pydantic +if typing.TYPE_CHECKING: + from .simvue_types import DeserializedContent CONCURRENT_DOWNLOADS = 10 DOWNLOAD_CHUNK_SIZE = 8192 -logger = logging.getLogger(__file__) +logger = logging.getLogger(__name__) def _download_artifact_to_file( artifact: FileArtifact | ObjectArtifact, output_dir: pathlib.Path | None ) -> None: if not artifact.name: - raise RuntimeError(f"Expected artifact '{artifact.id}' to have a name") + _out_msg: str = f"Expected artifact '{artifact.id}' to have a name" + raise RuntimeError(_out_msg) _output_file = (output_dir or pathlib.Path.cwd()).joinpath(artifact.name) # If this is a hierarchical structure being downloaded, need to create directories _output_file.parent.mkdir(parents=True, exist_ok=True) with _output_file.open("wb") as out_f: for content in artifact.download_content(): - out_f.write(content) + _ = out_f.write(content) class Client: @@ -72,7 +76,7 @@ def __init__( server_token: pydantic.SecretStr | None = None, server_url: str | None = None, ) -> None: - """Initialise an instance of the Simvue client + """Initialise an instance of the Simvue client. Parameters ---------- @@ -88,12 +92,15 @@ def __init__( for label, value in zip( ("URL", "API token"), (self._user_config.server.url, self._user_config.server.url), + strict=False, ): if not value: - logger.warning(f"No {label} specified") + logger.warning("No %s specified", label) self._headers: dict[str, str] = { - "Authorization": f"Bearer {self._user_config.server.token.get_secret_value()}", + "Authorization": ( + f"Bearer {self._user_config.server.token.get_secret_value()}" + ), "Accept-Encoding": "gzip", } @@ -102,7 +109,7 @@ def __init__( def get_run_id_from_name( self, name: typing.Annotated[str, pydantic.Field(pattern=NAME_REGEX)] ) -> str: - """Get Run ID from the server matching the specified name + """Get Run ID from the server matching the specified name. Assumes a unique name for this run. If multiple results are found this method will fail. @@ -133,7 +140,7 @@ def get_run_id_from_name( ) from e with contextlib.suppress(StopIteration): - next(_runs) + _ = next(_runs) raise RuntimeError( "Could not collect ID - more than one run exists with this name." ) @@ -143,7 +150,7 @@ def get_run_id_from_name( @prettify_pydantic @pydantic.validate_call def get_run(self, run_id: str) -> Run | None: - """Retrieve a single run + """Retrieve a single run. Parameters ---------- @@ -165,7 +172,7 @@ def get_run(self, run_id: str) -> Run | None: @prettify_pydantic @pydantic.validate_call def get_run_name_from_id(self, run_id: str) -> str: - """Retrieve the name of a run from its identifier + """Retrieve the name of a run from its identifier. Parameters ---------- @@ -185,7 +192,6 @@ def get_runs( self, filters: list[str] | None, *, - system: bool = False, attributes: list[str] | None = None, metadata: bool = False, metrics: bool = False, @@ -197,7 +203,7 @@ def get_runs( start_index: pydantic.NonNegativeInt = 0, show_shared: bool = True, sort_by_columns: list[tuple[str, bool]] | None = None, - ) -> DataFrame | typing.Generator[tuple[str, Run], None, None] | None: + ) -> DataFrame | list[dict[str, object]] | Generator[tuple[str, Run]] | None: """Retrieve all runs matching filters. Parameters @@ -272,7 +278,10 @@ def get_runs( return_metrics=metrics, return_alerts=alerts, return_metadata=metadata, - sorting=[dict(zip(("column", "descending"), a)) for a in sort_by_columns] + sorting=[ + RunSort(column=column, descending=descending) + for column, descending in sort_by_columns + ] if sort_by_columns else None, ) @@ -280,7 +289,7 @@ def get_runs( if output_format == "objects": return _runs - run_objs: list[Run] = [run for _, run in _runs] + run_objs: list[Run] = [run for _, run in _runs or []] response_data = [run.to_dict() for run in run_objs] if output_format == "dict": @@ -290,8 +299,8 @@ def get_runs( @prettify_pydantic @pydantic.validate_call - def delete_run(self, run_id: str) -> dict | None: - """Delete run by identifier + def delete_run(self, run_id: str) -> dict[str, object] | None: + """Delete run by identifier. Parameters ---------- @@ -311,7 +320,7 @@ def delete_run(self, run_id: str) -> dict | None: return Run(identifier=run_id).delete() or None def _get_folder_from_path(self, path: str) -> Folder | None: - """Retrieve folder for the specified path if found + """Retrieve folder for the specified path if found. Parameters ---------- @@ -327,12 +336,13 @@ def _get_folder_from_path(self, path: str) -> Folder | None: try: _, _folder = next(_folders) - return _folder # type: ignore except StopIteration: return None + return _folder + def _get_folder_id_from_path(self, path: str) -> str | None: - """Retrieve folder identifier for the specified path if found + """Retrieve folder identifier for the specified path if found. Parameters ---------- @@ -352,10 +362,11 @@ def _get_folder_id_from_path(self, path: str) -> str | None: return None with contextlib.suppress(StopIteration): - next(_ids) - raise RuntimeError( + _ = next(_ids) + _out_msg: str = ( f"Expected single folder match for '{path}', but found duplicate." ) + raise RuntimeError(_out_msg) return _id @@ -363,8 +374,8 @@ def _get_folder_id_from_path(self, path: str) -> str | None: @pydantic.validate_call def delete_runs( self, folder_path: typing.Annotated[str, pydantic.Field(pattern=FOLDER_REGEX)] - ) -> list | None: - """Delete runs in a named folder + ) -> list[dict[str, object]] | None: + """Delete runs in a named folder. Parameters ---------- @@ -383,20 +394,22 @@ def delete_runs( if deletion fails due to server request error """ if not (_folder := self._get_folder_from_path(folder_path)): - raise ValueError(f"Could not find a folder matching '{folder_path}'") + _out_msg: str = f"Could not find a folder matching '{folder_path}'" + raise ValueError(_out_msg) _delete = _folder.delete(runs_only=True, delete_runs=True, recursive=False) - return _delete.get("runs", []) + return typing.cast("list[dict[str, object]]", _delete.get("runs", [])) @prettify_pydantic @pydantic.validate_call def delete_folder( self, folder_path: typing.Annotated[str, pydantic.Field(pattern=FOLDER_REGEX)], + *, recursive: bool = False, remove_runs: bool = False, allow_missing: bool = False, - ) -> list | None: - """Delete a folder by name + ) -> list[dict[str, object]] | None: + """Delete a folder by name. Parameters ---------- @@ -426,11 +439,10 @@ def delete_folder( if not folder_id: if allow_missing: return None - else: - raise ObjectNotFoundError( - name=folder_path, - obj_type="folder", - ) + raise ObjectNotFoundError( + name=folder_path, + obj_type="folder", + ) _response = Folder(identifier=folder_id).delete( delete_runs=remove_runs, recursive=recursive, runs_only=False ) @@ -438,26 +450,26 @@ def delete_folder( if folder_id not in _response.get("folders", []): raise RuntimeError("Deletion of folder failed, server returned mismatch.") - return _response.get("runs", []) + return typing.cast("list[dict[str, object]]", _response.get("runs", [])) @prettify_pydantic @pydantic.validate_call def delete_alert(self, alert_id: str) -> None: - """Delete an alert from the server by ID + """Delete an alert from the server by ID. Parameters ---------- alert_id : str the unique identifier for the alert """ - Alert(identifier=alert_id).delete() # type: ignore + _ = Alert(identifier=alert_id).delete() @prettify_pydantic @pydantic.validate_call def list_artifacts( self, run_id: str, sort_by_columns: list[tuple[str, bool]] | None = None - ) -> typing.Generator[Artifact, None, None]: - """Retrieve artifacts for a given run + ) -> Generator[tuple[str, FileArtifact | ObjectArtifact]]: + """Retrieve artifacts for a given run. Parameters ---------- @@ -470,7 +482,7 @@ def list_artifacts( Yields ------ - str, Artifact + str, FileArtifact | ObjectArtifact ID and artifact entry for relevant artifacts Raises @@ -480,10 +492,13 @@ def list_artifacts( """ return Artifact.get( runs=json.dumps([run_id]), - sorting=[dict(zip(("column", "descending"), a)) for a in sort_by_columns] + sorting=[ + ArtifactSort(column=column, descending=descending) + for column, descending in sort_by_columns + ] if sort_by_columns else None, - ) # type: ignore + ) def _retrieve_artifacts_from_server( self, run_id: str, name: str @@ -497,8 +512,8 @@ def _retrieve_artifacts_from_server( @prettify_pydantic @pydantic.validate_call - def abort_run(self, run_id: str, reason: str) -> dict | list: - """Abort a currently active run on the server + def abort_run(self, run_id: str, reason: str) -> dict[str, object]: + """Abort a currently active run on the server. Parameters ---------- @@ -509,7 +524,7 @@ def abort_run(self, run_id: str, reason: str) -> dict | list: Returns ------- - dict | list + dict response from server """ return Run(identifier=run_id).abort(reason=reason) @@ -517,9 +532,9 @@ def abort_run(self, run_id: str, reason: str) -> dict | list: @prettify_pydantic @pydantic.validate_call def get_artifact( - self, run_id: str, name: str, allow_pickle: bool = False - ) -> typing.Any: - """Return the contents of a specified artifact + self, run_id: str, name: str, *, allow_pickle: bool = False + ) -> object: + """Return the contents of a specified artifact. Parameters ---------- @@ -568,7 +583,7 @@ def get_artifact_as_file( name: str, output_dir: pydantic.DirectoryPath | None = None, ) -> None: - """Retrieve the specified artifact in the form of a file + """Retrieve the specified artifact in the form of a file. Information is saved to a file as opposed to deserialized @@ -607,7 +622,7 @@ def get_artifacts_as_files( category: typing.Literal["input", "output", "code"] | None = None, output_dir: pydantic.DirectoryPath | None = None, ) -> None: - """Retrieve artifacts from the given run as a set of files + """Retrieve artifacts from the given run as a set of files. Parameters ---------- @@ -627,7 +642,7 @@ def get_artifacts_as_files( RuntimeError if there was a failure retrieving artifacts from the server """ - _artifacts: typing.Generator[tuple[str, Artifact], None, None] = ( + _artifacts: Generator[tuple[str, FileArtifact | ObjectArtifact]] = ( Artifact.from_run(run_id=run_id, category=category) ) @@ -638,23 +653,27 @@ def get_artifacts_as_files( executor.submit(_download_artifact_to_file, artifact, output_dir) for _, artifact in _artifacts ] - for future, (_, artifact) in zip(as_completed(futures), _artifacts): + for future, (_, artifact) in zip( + as_completed(futures), _artifacts, strict=False + ): try: future.result() except Exception as e: - raise RuntimeError( + _out_msg: str = ( f"Download of file {artifact.storage_url} " f"failed with exception: {e}" - ) from e + ) + raise RuntimeError(_out_msg) from e @prettify_pydantic @pydantic.validate_call def get_folder( self, folder_path: typing.Annotated[str, pydantic.Field(pattern=FOLDER_REGEX)], + *, read_only: bool = True, ) -> Folder | None: - """Retrieve a folder by identifier + """Retrieve a folder by identifier. Parameters ---------- @@ -691,8 +710,8 @@ def get_folders( count: pydantic.PositiveInt = 100, start_index: pydantic.NonNegativeInt = 0, sort_by_columns: list[tuple[str, bool]] | None = None, - ) -> typing.Generator[tuple[str, Folder], None, None]: - """Retrieve folders from the server + ) -> Generator[tuple[str, Folder]]: + """Retrieve folders from the server. Parameters ---------- @@ -721,15 +740,18 @@ def get_folders( filters=json.dumps(filters or []), count=count, offset=start_index, - sorting=[dict(zip(("column", "descending"), a)) for a in sort_by_columns] + sorting=[ + FolderSort(column=column, descending=descending) + for column, descending in sort_by_columns + ] if sort_by_columns else None, - ) # type: ignore + ) @prettify_pydantic @pydantic.validate_call - def get_metrics_names(self, run_id: str) -> typing.Generator[str, None, None]: - """Return information on all metrics within a run + def get_metrics_names(self, run_id: str) -> Generator[str]: + """Return information on all metrics within a run. Parameters ---------- @@ -748,17 +770,18 @@ def get_metrics_names(self, run_id: str) -> typing.Generator[str, None, None]: """ _run = Run(identifier=run_id) - for id, _ in _run.metrics: - yield id + for _id, _ in _run.metrics: + yield _id def _get_run_metrics_from_server( self, metric_names: list[str], run_ids: list[str], xaxis: str, + *, aggregate: bool, max_points: int | None = None, - ) -> dict[str, typing.Any]: + ) -> dict[str, object]: params: dict[str, str | int | None] = { "runs": json.dumps(run_ids), "aggregate": aggregate, @@ -771,6 +794,7 @@ def _get_run_metrics_from_server( f"{self._user_config.server.url}/metrics", headers=self._headers, params=params, + timeout=DEFAULT_API_TIMEOUT, ) return get_json_from_response( @@ -792,8 +816,8 @@ def get_metric_values( use_run_names: bool = False, aggregate: bool = False, max_points: pydantic.PositiveInt | None = None, - ) -> dict | DataFrame | None: - """Retrieve the values for a given metric across multiple runs + ) -> dict[str, object] | DataFrame | None: + """Retrieve the values for a given metric across multiple runs. Uses filters to specify which runs should be retrieved. @@ -847,20 +871,24 @@ def get_metric_values( "'xaxis=timestamp'" ) - _args = {"filters": json.dumps(run_filters)} if run_filters else {} + _args: dict[str, str | list[object]] = ( + {"filters": json.dumps(run_filters)} if run_filters else {} + ) + + _run_data: dict[str, object] = {} if not run_ids: _run_data = dict(Run.get(**_args)) - if not ( - _run_metrics := self._get_run_metrics_from_server( - metric_names=metric_names, - run_ids=run_ids or list(_run_data.keys()), - xaxis=xaxis, - aggregate=aggregate, - max_points=max_points, - ) - ): + _run_metrics: dict[str, object] = self._get_run_metrics_from_server( + metric_names=metric_names, + run_ids=run_ids or list(_run_data.keys()), + xaxis=xaxis, + aggregate=aggregate, + max_points=max_points, + ) + + if not _run_metrics: return None if aggregate: return aggregated_metrics_to_dataframe( @@ -868,8 +896,7 @@ def get_metric_values( ) if use_run_names: _run_metrics = { - Run(identifier=key).name: _run_metrics[key] - for key in _run_metrics.keys() + Run(identifier=key).name: _run_metrics[key] for key in _run_metrics } return parse_run_set_metrics( _run_metrics, @@ -884,12 +911,14 @@ def get_metric_values( def plot_metrics( self, run_ids: list[str], - metric_names: list[str], + metric_names: list[ + typing.Annotated[str, pydantic.StringConstraints(pattern=NAME_REGEX)] + ], xaxis: typing.Literal["step", "time"], *, max_points: int | None = None, - ) -> typing.Any: - """Plt the time series values for multiple metrics/runs + ) -> object: + """Plot the time series values for multiple metrics/runs. Parameters ---------- @@ -912,13 +941,7 @@ def plot_metrics( ValueError if invalid arguments are provided """ - if not isinstance(run_ids, list): - raise ValueError("Invalid runs specified, must be a list of run names.") - - if not isinstance(metric_names, list): - raise ValueError("Invalid names specified, must be a list of metric names.") - - data: DataFrame = self.get_metric_values( # type: ignore + _data: DataFrame = self.get_metric_values( run_ids=run_ids, metric_names=metric_names, xaxis=xaxis, @@ -927,13 +950,14 @@ def plot_metrics( aggregate=False, ) - if data is None: - raise RuntimeError( + if _data is None: + _out_msg = ( f"Cannot plot metrics {metric_names}, no data found for runs {run_ids}." ) + raise RuntimeError(_out_msg) # Undo multi-indexing - flattened_df = data.reset_index() + flattened_df = _data.reset_index() import matplotlib.pyplot as plt @@ -973,7 +997,7 @@ def get_events( start_index: pydantic.NonNegativeInt | None = None, count_limit: pydantic.PositiveInt | None = None, ) -> list[dict[str, str]]: - """Return events for a specified run + """Return events for a specified run. Parameters ---------- @@ -996,7 +1020,6 @@ def get_events( RuntimeError if there was a failure retrieving information from the server """ - msg_filter: str = ( json.dumps([f"event.message contains {message_contains}"]) if message_contains @@ -1074,7 +1097,8 @@ def get_alerts( alert.name if names_only else alert for _, alert in Alert.get( sorting=[ - dict(zip(("column", "descending"), a)) for a in sort_by_columns + dict(zip(("column", "descending"), a, strict=False)) + for a in sort_by_columns ] if sort_by_columns else None, @@ -1108,7 +1132,7 @@ def get_tags( start_index: pydantic.NonNegativeInt | None = None, count_limit: pydantic.PositiveInt | None = None, sort_by_columns: list[tuple[str, bool]] | None = None, - ) -> typing.Generator[Tag, None, None]: + ) -> typing.Generator[Tag]: """Retrieve tags Parameters @@ -1136,7 +1160,10 @@ def get_tags( return Tag.get( count=count_limit, offset=start_index, - sorting=[dict(zip(("column", "descending"), a)) for a in sort_by_columns] + sorting=[ + dict(zip(("column", "descending"), a, strict=False)) + for a in sort_by_columns + ] if sort_by_columns else None, ) diff --git a/simvue/converters.py b/simvue/converters.py index 944599d1..3c6bfa17 100644 --- a/simvue/converters.py +++ b/simvue/converters.py @@ -1,17 +1,19 @@ -""" -Converter Functions -=================== +"""Converter Functions. Contains functions for converting objects retrieved from the server between data types including creation of DataFrames for metrics """ +from __future__ import annotations + import typing -import pandas -import flatdict +import flatdict +import pandas as pd if typing.TYPE_CHECKING: + from collections.abc import Mapping + from pandas import DataFrame @@ -19,8 +21,8 @@ def aggregated_metrics_to_dataframe( request_response_data: dict[str, list[dict[str, float]]], xaxis: str, parse_to: typing.Literal["dict", "dataframe"] = "dict", -) -> typing.Union["DataFrame", dict[str, dict[tuple[float, str], float]] | None]: - """Create data frame for an aggregate of metrics +) -> DataFrame | dict[str, dict[tuple[float, str], float | None]]: + """Create data frame for an aggregate of metrics. Returns a dataframe with columns being metrics and sub-columns being the minimum, average etc. @@ -34,14 +36,13 @@ def aggregated_metrics_to_dataframe( parse_to : Literal["dict", "dataframe"], optional form of output * dict - dictionary of values. - * dataframe - dataframe (Pandas must be installed). + * dataframe - dataframe (pd must be installed). Returns ------- DataFrame | dict - a Pandas dataframe of the metric set or the data as a dictionary + a pd dataframe of the metric set or the data as a dictionary """ - _all_steps: list[float] = sorted( { d[xaxis] @@ -57,7 +58,7 @@ def aggregated_metrics_to_dataframe( _value_types = list(_value_types) _value_types.remove(xaxis) - result_dict: dict[str, dict[tuple[float, str], float]] | None = { + result_dict: dict[str, dict[tuple[float, str], float | None]] = { metric_name: {} for metric_name in request_response_data } @@ -76,13 +77,13 @@ def aggregated_metrics_to_dataframe( ) if parse_to == "dataframe": - _data_frame = pandas.DataFrame(result_dict) + _data_frame = pd.DataFrame(result_dict) _data_frame.index.name = xaxis return _data_frame - elif parse_to == "dict": + if parse_to == "dict": return result_dict - else: - raise ValueError(f"Unrecognised parse format '{parse_to}'") + _out_msg: str = "Unrecognised parse format '{parse_to}'" + raise ValueError(_out_msg) def parse_run_set_metrics( @@ -90,10 +91,10 @@ def parse_run_set_metrics( xaxis: str, run_labels: list[str], parse_to: typing.Literal["dict", "dataframe"] = "dict", -) -> typing.Union[dict[str, dict[tuple[float, str], float]] | None, "DataFrame"]: - """Parse JSON response metric data from the server into the specified form +) -> dict[str, dict[tuple[float, str], float | None]] | None | DataFrame: + """Parse JSON response metric data from the server into the specified form. - Creates either a dictionary or a pandas dataframe of the data collected + Creates either a dictionary or a pd dataframe of the data collected from multiple runs and metrics Parameters @@ -107,12 +108,12 @@ def parse_run_set_metrics( parse_to : Literal["dict", "dataframe"], optional form in which to parse data * dict - return a values dictionary (default). - * dataframe - assembled into dataframe (requires Pandas). + * dataframe - assembled into dataframe (requires pd). Returns ------- dict[str, dict[tuple[float, str], float]] | None | DataFrame - either a dictionary or Pandas DataFrame containing the results + either a dictionary or pd DataFrame containing the results Raises ------ @@ -120,7 +121,7 @@ def parse_run_set_metrics( if an unrecognised parse format is specified """ if not request_response_data: - return pandas.DataFrame({}) if parse_to == "dataframe" else {} + return pd.DataFrame({}) if parse_to == "dataframe" else {} _all_steps: list[float] = sorted( { @@ -133,7 +134,7 @@ def parse_run_set_metrics( ) _all_metrics: list[str] = sorted( - {key for run_data in request_response_data.values() for key in run_data.keys()} + {key for run_data in request_response_data.values() for key in run_data} ) # Get the keys from the aggregate which are not the xaxis label @@ -143,47 +144,49 @@ def parse_run_set_metrics( _value_types = list(_value_types) _value_types.remove(xaxis) - result_dict: dict[str, dict[tuple[float, str], float]] | None = { + _result_dict: dict[str, dict[tuple[float, str], float | None]] = { metric_name: {} for metric_name in _all_metrics } - for run_label, run_data in zip(run_labels, request_response_data.values()): + for run_label, run_data in zip( + run_labels, request_response_data.values(), strict=False + ): for metric_name in _all_metrics: if metric_name not in run_data: for step in _all_steps: - result_dict[metric_name][step, run_label] = None + _result_dict[metric_name][step, run_label] = None continue metrics = run_data[metric_name] metrics_iterator = iter(metrics) _metric_steps = (d[xaxis] for d in metrics) for step in _all_steps: if step not in _metric_steps: - result_dict[metric_name][step, run_label] = None + _result_dict[metric_name][step, run_label] = None else: next_item = next(metrics_iterator) - result_dict[metric_name][step, run_label] = next_item.get("value") + _result_dict[metric_name][step, run_label] = next_item.get("value") if parse_to == "dataframe": - return pandas.DataFrame( - result_dict, - index=pandas.MultiIndex.from_product( + return pd.DataFrame( + _result_dict, + index=pd.MultiIndex.from_product( [_all_steps, run_labels], names=(xaxis, "run") ), ) - elif parse_to == "dict": - return result_dict - else: - raise ValueError(f"Unrecognised parse format '{parse_to}'") + if parse_to == "dict": + return _result_dict + _out_msg: str = f"Unrecognised parse format '{parse_to}'" + raise ValueError(_out_msg) -def to_dataframe(data) -> pandas.DataFrame: - """ - Convert runs to dataframe - """ - metadata = [] - system_columns = [] - columns = { +def to_dataframe( + data: list[dict[str, list[str] | dict[str, object]]], +) -> pd.DataFrame: + """Convert runs to dataframe.""" + _metadata: list[str] = [] + _system_columns: list[str] = [] + _columns: dict[str, list[str | float | None]] = { "name": [], "status": [], "folder": [], @@ -193,39 +196,44 @@ def to_dataframe(data) -> pandas.DataFrame: } for run in data: - for item in run.get("metadata", []): - if item not in metadata: - metadata.append(item) - for item, value in (run.get("system", {}) or {}).items(): + _meta: list[str] = typing.cast("list[str]", run.get("metadata", [])) + _system: dict[str, dict[str, str] | str] = typing.cast( + "dict[str, dict[str, str] | str]", run.get("system", {}) or {} + ) + for item in _meta: + if item not in _metadata: + _metadata.append(item) + for item, value in _system.items(): if isinstance(value, dict): - system_columns += [ + _system_columns += [ col_name - for sub_item in value.keys() - if (col_name := f"system.{item}.{sub_item}") not in system_columns + for sub_item in value + if (col_name := f"system.{item}.{sub_item}") not in _system_columns ] - elif f"system.{item}" not in system_columns: - system_columns.append(f"system.{item}") + elif f"system.{item}" not in _system_columns: + _system_columns.append(f"system.{item}") - columns |= {f"metadata.{column}": [] for column in metadata} | { - column: [] for column in system_columns + _columns |= {f"metadata.{column}": [] for column in _metadata} | { + column: [] for column in _system_columns } for run in data: - run_info = flatdict.FlatDict(run, delimiter=".") - for column, value_ in columns.items(): + _run_info: Mapping[str, str] = flatdict.FlatDict(run, delimiter=".") + for column, value_ in _columns.items(): try: - value_.append(run_info.get(column)) + _column_val = typing.cast("str", _run_info.get(column)) + value_.append(_column_val) except TypeError: value_.append(None) - return pandas.DataFrame(data=columns) + return pd.DataFrame(data=_columns) def metric_time_series_to_dataframe( data: list[dict[str, float]], xaxis: typing.Literal["step", "time", "timestamp"], name: str | None = None, -) -> "DataFrame": - """Convert a single metric value set from a run into a dataframe +) -> DataFrame: + """Convert a single metric value set from a run into a dataframe. Parameters ---------- @@ -242,12 +250,11 @@ def metric_time_series_to_dataframe( Returns ------- DataFrame - a Pandas DataFrame containing values for the metric and run at each + a pd DataFrame containing values for the metric and run at each """ - _df_dict: dict[str, list[float]] = { xaxis: [v[xaxis] for v in data], name or "value": [v["value"] for v in data], } - return pandas.DataFrame(_df_dict) + return pd.DataFrame(_df_dict) diff --git a/simvue/exception.py b/simvue/exception.py index 3dc5e65e..a6ac77ab 100644 --- a/simvue/exception.py +++ b/simvue/exception.py @@ -1,24 +1,21 @@ -""" -Simvue Exception Types -====================== +"""Simvue Exception Types. -Custom exceptions for handling of Simvue request scenarions. +Custom exceptions for handling of Simvue request scenarios. """ class ObjectNotFoundError(Exception): - """For failure retrieving Simvue object from server""" + """For failure retrieving Simvue object from server.""" def __init__(self, obj_type: str, name: str, extra: str | None = None) -> None: - super().__init__( + _msg: str = ( f"Failed to retrieve '{name}' of type '{obj_type}' " f"{f'{extra}, ' if extra else ''}" "no such object" ) + super().__init__(_msg) class SimvueRunError(RuntimeError): - """A special sub-class of runtime error specifically for Simvue run errors""" - - pass + """A special sub-class of runtime error specifically for Simvue run errors.""" From b6fc695e1479e440312f869e8ac8df48f361f362 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Zar=C4=99bski?= Date: Fri, 5 Sep 2025 08:15:45 +0100 Subject: [PATCH 02/23] Continued linting fixes --- pyproject.toml | 3 +- simvue/api/objects/alert/fetch.py | 29 +++--- simvue/api/objects/base.py | 103 +++++++++++++------- simvue/api/objects/run.py | 4 +- simvue/api/objects/tag.py | 12 ++- simvue/client.py | 153 +++++++++++++++++------------- simvue/config/user.py | 35 ++++--- simvue/converters.py | 4 +- simvue/utilities.py | 27 +++--- 9 files changed, 219 insertions(+), 151 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 40d4298b..790c48da 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -96,7 +96,7 @@ line-length = 88 [tool.ruff.lint] mccabe.max-complexity = 11 select = ["ALL"] -ignore = ["COM812", "D105", "TRY003", "EM101", "FIX002"] +ignore = ["COM812", "D105", "TRY003", "EM101", "FIX002", "PLR0913"] pydocstyle.convention = "numpy" [tool.pytest.ini_options] @@ -135,6 +135,7 @@ reportUnreachable = "none" reportUnknownMemberType = "none" reportUnknownVariableType = "none" reportMissingTypeStubs = "none" +reportPrivateUsage = "none" # ruff already does this [tool.mypy] ignore_missing_imports = true diff --git a/simvue/api/objects/alert/fetch.py b/simvue/api/objects/alert/fetch.py index e2cce7af..4c8b6ec0 100644 --- a/simvue/api/objects/alert/fetch.py +++ b/simvue/api/objects/alert/fetch.py @@ -6,19 +6,20 @@ with an identifier, use a generic alert object. """ -import typing import http import json +import typing import pydantic from simvue.api.objects.alert.user import UserAlert from simvue.api.objects.base import Sort -from simvue.api.request import get_json_from_response from simvue.api.request import get as sv_get -from .events import EventsAlert -from .metrics import MetricsThresholdAlert, MetricsRangeAlert +from simvue.api.request import get_json_from_response + from .base import AlertBase +from .events import EventsAlert +from .metrics import MetricsRangeAlert, MetricsThresholdAlert AlertType = EventsAlert | UserAlert | MetricsThresholdAlert | MetricsRangeAlert @@ -36,7 +37,9 @@ class Alert: """Generic Simvue alert retrieval class""" @pydantic.validate_call() - def __new__(cls, identifier: str, **kwargs) -> AlertType: + def __new__( + cls, identifier: str, _local: bool = False, **kwargs: object + ) -> AlertType: """Retrieve an object representing an alert either locally or on the server by id""" _alert_pre = AlertBase(identifier=identifier, **kwargs) if ( @@ -50,13 +53,13 @@ def __new__(cls, identifier: str, **kwargs) -> AlertType: "exact Alert type instead (eg MetricThresholdAlert, MetricRangeAlert etc)." ) if _alert_pre.source == "events": - return EventsAlert(identifier=identifier, **kwargs) - elif _alert_pre.source == "metrics" and _alert_pre.get_alert().get("threshold"): - return MetricsThresholdAlert(identifier=identifier, **kwargs) - elif _alert_pre.source == "metrics": - return MetricsRangeAlert(identifier=identifier, **kwargs) - elif _alert_pre.source == "user": - return UserAlert(identifier=identifier, **kwargs) + return EventsAlert(identifier=identifier, _local=_local, **kwargs) + if _alert_pre.source == "metrics" and _alert_pre.get_alert().get("threshold"): + return MetricsThresholdAlert(identifier=identifier, _local=_local, **kwargs) + if _alert_pre.source == "metrics": + return MetricsRangeAlert(identifier=identifier, _local=_local, **kwargs) + if _alert_pre.source == "user": + return UserAlert(identifier=identifier, _local=_local, **kwargs) raise RuntimeError(f"Unknown source type '{_alert_pre.source}'") @@ -69,7 +72,7 @@ def get( offset: int | None = None, sorting: list[AlertSort] | None = None, **kwargs, - ) -> typing.Generator[tuple[str, AlertType], None, None]: + ) -> typing.Generator[tuple[str, AlertType]]: """Fetch all alerts from the server for the current user. Parameters diff --git a/simvue/api/objects/base.py b/simvue/api/objects/base.py index 91008094..4ea3532e 100644 --- a/simvue/api/objects/base.py +++ b/simvue/api/objects/base.py @@ -1,16 +1,15 @@ -""" -Simvue RestAPI Objects -====================== +"""Simvue RestAPI Objects. Contains base class for interacting with objects on the Simvue server """ +from __future__ import annotations + import abc import http import inspect import json import logging -import pathlib import types import typing import uuid @@ -19,6 +18,9 @@ import msgpack import pydantic +if typing.TYPE_CHECKING: + import pathlib + from simvue.api.request import ( delete as sv_delete, ) @@ -48,27 +50,29 @@ except ImportError: from typing import Self -# Need to use this inside of Generator typing to fix bug present in Python 3.10 - see issue #745 +# Need to use this inside of Generator typing to fix bug +# present in Python 3.10 - see issue #745 T = typing.TypeVar("T", bound="SimvueObject") +U = typing.TypeVar("U", bound="SimvueObject | Visibility") -def staging_check(member_func: typing.Callable) -> typing.Callable: - """Decorator for checking if requested attribute has uncommitted changes""" +def staging_check( + member_func: typing.Callable[[U], typing.Any | None], +) -> typing.Callable[[U], typing.Any | None]: + """Checki if requested attribute has uncommitted changes via decorator.""" - def _wrapper(self) -> typing.Any: - if isinstance(self, SimvueObject): - _sv_obj = self - elif hasattr(self, "_sv_obj"): - _sv_obj = self._sv_obj - else: - raise RuntimeError( + def _wrapper(self: U) -> object: + _sv_obj = typing.cast("SimvueObject", getattr(self, "_sv_obj", self)) + if not hasattr(_sv_obj, "_offline"): + _out_msg: str = ( f"Cannot use 'staging_check' decorator on type '{type(self).__name__}'" ) - if _sv_obj._offline: + raise RuntimeError(_out_msg) + if _sv_obj._offline: # noqa: SLF001 return member_func(self) - if not _sv_obj._read_only and member_func.__name__ in _sv_obj._staging: - _sv_obj._logger.warning( - f"Uncommitted change found for attribute '{member_func.__name__}'" + if not _sv_obj._read_only and member_func.__name__ in _sv_obj._staging: # noqa: SLF001 + _sv_obj._logger.warning( # noqa: SLF001 + "Uncommitted change found for attribute '%s'", member_func.__name__ ) return member_func(self) @@ -81,14 +85,21 @@ def _wrapper(self) -> typing.Any: return _wrapper -def write_only(attribute_func: typing.Callable) -> typing.Callable: - def _wrapper(self: "SimvueObject", *args, **kwargs) -> typing.Any: - _sv_obj = getattr(self, "_sv_obj", self) - if _sv_obj._read_only: - raise AssertionError( +def write_only( + attribute_func: typing.Callable[[U, typing.Any], typing.Any | None], +) -> typing.Callable[[U, typing.Any], typing.Any | None]: + """Check if function only available in write mode.""" + + def _wrapper(self: U, *args: object, **kwargs: object) -> object | None: + _sv_obj: SimvueObject = typing.cast( + "SimvueObject", getattr(self, "_sv_obj", self) + ) + if _sv_obj._read_only: # noqa: SLF001 + _out_msg: str = ( f"Cannot set property '{attribute_func.__name__}' " - f"on read-only object of type '{self._label}'" + f"on read-only object of type '{_sv_obj._label}'" # noqa: SLF001 ) + raise AssertionError(_out_msg) return attribute_func(self, *args, **kwargs) _wrapper.__name__ = attribute_func.__name__ @@ -101,16 +112,16 @@ def _wrapper(self: "SimvueObject", *args, **kwargs) -> typing.Any: class Visibility: - """Interface for object visibility definition""" + """Interface for object visibility definition.""" - def __init__(self, sv_obj: "SimvueObject") -> None: - """Initialise visibility with target object""" - self._sv_obj = sv_obj + def __init__(self, sv_obj: SimvueObject) -> None: + """Initialise visibility with target object.""" + self._sv_obj: SimvueObject = sv_obj - def _update_visibility(self, key: str, value: typing.Any) -> None: - """Update the visibility configuration for this object""" - _visibility = self._sv_obj._get_visibility() | {key: value} - self._sv_obj._staging["visibility"] = _visibility + def _update_visibility(self, key: str, value: object) -> None: + """Update the visibility configuration for this object.""" + _visibility = self._sv_obj._get_visibility() | {key: value} # noqa: SLF001 + self._sv_obj._staging["visibility"] = _visibility # noqa: SLF001 @property @staging_check @@ -125,7 +136,11 @@ def users(self) -> list[str]: ------- list[str] """ - return self._sv_obj._get_visibility().get("users", []) + _users: list[str] = typing.cast( + "list[str]", + self._sv_obj._get_visibility().get("users", []), # noqa: SLF001 + ) + return _users @users.setter @write_only @@ -145,7 +160,11 @@ def public(self) -> bool: ------- bool """ - return self._sv_obj._get_visibility().get("public", False) # type: ignore + _public: bool = typing.cast( + "bool", + self._sv_obj._get_visibility().get("public", False), # noqa: SLF001 + ) + return _public @public.setter @write_only @@ -165,7 +184,11 @@ def tenant(self) -> bool: ------- bool """ - return self._sv_obj._get_visibility().get("tenant", False) # type: ignore + _tenant: bool = typing.cast( + "bool", + self._sv_obj._get_visibility().get("tenant", False), # noqa: SLF001 + ) + return _tenant @tenant.setter @write_only @@ -193,7 +216,12 @@ def __init__( ) -> None: self._logger = logging.getLogger(f"simvue.{self.__class__.__name__}") self._label: str = getattr(self, "_label", self.__class__.__name__.lower()) + + # Local blocks any remote connection completely, this prevents multiple server + # calls when information cannot be found locally which is important + # especially if the user opted to veto information self._local: bool = _local + self._read_only: bool = _read_only self._is_set: bool = False self._endpoint: str = getattr(self, "_endpoint", f"{self._label}s") @@ -536,6 +564,11 @@ def commit(self) -> dict | None: return _response + @property + def offline(self) -> bool: + """Return of object in offline mode.""" + return self._offline + @property def id(self) -> str | None: """The identifier for this object if applicable. diff --git a/simvue/api/objects/run.py b/simvue/api/objects/run.py index 2962c03f..ad2cbda9 100644 --- a/simvue/api/objects/run.py +++ b/simvue/api/objects/run.py @@ -331,7 +331,7 @@ def get( offset: pydantic.NonNegativeInt | None = None, sorting: list[RunSort] | None = None, **kwargs, - ) -> Generator[tuple[str, Self]] | None: + ) -> Generator[tuple[str, Self]]: """Get runs from the server. Parameters @@ -362,7 +362,7 @@ def get( def alerts(self, alerts: list[str]) -> None: self._staging["alerts"] = list(set(self._staging.get("alerts", []) + alerts)) - def get_alert_details(self) -> typing.Generator[dict[str, typing.Any]]: + def get_alert_details(self) -> Generator[dict[str, object]]: """Retrieve the full details of alerts for this run. Yields diff --git a/simvue/api/objects/tag.py b/simvue/api/objects/tag.py index 464dc874..8dc1fa7b 100644 --- a/simvue/api/objects/tag.py +++ b/simvue/api/objects/tag.py @@ -7,9 +7,11 @@ """ -import typing -import json import datetime +import json +import typing +from collections.abc import Generator + import pydantic import pydantic_extra_types.color as pyd_color @@ -19,7 +21,7 @@ try: from typing import Self except ImportError: - from typing_extensions import Self + from typing import Self __all__ = ["Tag"] @@ -111,8 +113,8 @@ def get( count: int | None = None, offset: int | None = None, sorting: list[TagSort] | None = None, - **kwargs, - ) -> typing.Generator[tuple[str, "SimvueObject"], None, None]: + **kwargs: object, + ) -> Generator[tuple[str, Self]]: """Get tags from the server. Parameters diff --git a/simvue/client.py b/simvue/client.py index c7705a67..b2aa72b9 100644 --- a/simvue/client.py +++ b/simvue/client.py @@ -6,6 +6,7 @@ import contextlib import http +import itertools import json import logging import pathlib @@ -17,10 +18,11 @@ import requests from pandas import DataFrame -from simvue.api.objects.alert.base import AlertBase +from simvue.api.objects.alert.fetch import AlertSort, AlertType from simvue.api.objects.artifact.fetch import ArtifactSort from simvue.api.objects.folder import FolderSort from simvue.api.objects.run import RunSort +from simvue.api.objects.tag import TagSort from simvue.exception import ObjectNotFoundError from .api.objects import ( @@ -85,17 +87,14 @@ def __init__( server_url : str, optional specify URL, if unset this is read from the config file """ - self._user_config = SimvueConfiguration.fetch( + self._user_config: SimvueConfiguration = SimvueConfiguration.fetch( server_token=server_token, server_url=server_url, mode="online" ) - for label, value in zip( - ("URL", "API token"), - (self._user_config.server.url, self._user_config.server.url), - strict=False, - ): - if not value: - logger.warning("No %s specified", label) + if not self._user_config.server.url: + raise ValueError("No URL specified") + if not self._user_config.server.token: + raise ValueError("No API token specified") self._headers: dict[str, str] = { "Authorization": ( @@ -797,12 +796,14 @@ def _get_run_metrics_from_server( timeout=DEFAULT_API_TIMEOUT, ) - return get_json_from_response( + _json_response = get_json_from_response( expected_status=[http.HTTPStatus.OK], scenario=f"Retrieval of metrics '{metric_names}' in runs '{run_ids}'", response=metrics_response, ) + return typing.cast("dict[str, object]", _json_response) + @prettify_pydantic @pydantic.validate_call def get_metric_values( @@ -816,7 +817,7 @@ def get_metric_values( use_run_names: bool = False, aggregate: bool = False, max_points: pydantic.PositiveInt | None = None, - ) -> dict[str, object] | DataFrame | None: + ) -> dict[str, dict[tuple[float, str], float | None]] | DataFrame | None: """Retrieve the values for a given metric across multiple runs. Uses filters to specify which runs should be retrieved. @@ -878,7 +879,7 @@ def get_metric_values( _run_data: dict[str, object] = {} if not run_ids: - _run_data = dict(Run.get(**_args)) + _run_data = dict(Run.get(**_args)) # pyright: ignore[reportArgumentType] _run_metrics: dict[str, object] = self._get_run_metrics_from_server( metric_names=metric_names, @@ -914,7 +915,7 @@ def plot_metrics( metric_names: list[ typing.Annotated[str, pydantic.StringConstraints(pattern=NAME_REGEX)] ], - xaxis: typing.Literal["step", "time"], + xaxis: typing.Literal["step", "time", "timestamp"], *, max_points: int | None = None, ) -> object: @@ -961,29 +962,28 @@ def plot_metrics( import matplotlib.pyplot as plt - for run in run_ids: - for name in metric_names: - label = None - if len(run_ids) > 1 and len(metric_names) > 1: - label = f"{run}: {name}" - elif len(run_ids) > 1 and len(metric_names) == 1: - label = run - elif len(run_ids) == 1 and len(metric_names) > 1: - label = name + for run, name in itertools.product(run_ids, metric_names): + label = None + if len(run_ids) > 1 and len(metric_names) > 1: + label = f"{run}: {name}" + elif len(run_ids) > 1 and len(metric_names) == 1: + label = run + elif len(run_ids) == 1 and len(metric_names) > 1: + label = name - flattened_df.plot(y=name, x=xaxis, label=label) + _ = flattened_df.plot(y=name, x=xaxis, label=label) if xaxis == "step": - plt.xlabel("Steps") + _ = plt.xlabel("Steps") elif xaxis == "time": - plt.xlabel("Relative Time") + _ = plt.xlabel("Relative Time") if xaxis == "step": - plt.xlabel("steps") + _ = plt.xlabel("steps") elif xaxis == "timestamp": - plt.xlabel("Time") + _ = plt.xlabel("Time") if len(metric_names) == 1: - plt.ylabel(metric_names[0]) + _ = plt.ylabel(metric_names[0]) return plt.figure() @@ -1020,32 +1020,35 @@ def get_events( RuntimeError if there was a failure retrieving information from the server """ - msg_filter: str = ( + _msg_filter: str = ( json.dumps([f"event.message contains {message_contains}"]) if message_contains else "" ) - params: dict[str, str | int] = { + _params: dict[str, str | int] = { "run": run_id, - "filters": msg_filter, + "filters": _msg_filter, "start": start_index or 0, "count": count_limit or 0, } - response = requests.get( + _response = requests.get( f"{self._user_config.server.url}/events", headers=self._headers, - params=params, + params=_params, + timeout=DEFAULT_API_TIMEOUT, ) - json_response = get_json_from_response( + _json_response = get_json_from_response( expected_status=[http.HTTPStatus.OK], scenario=f"Retrieval of events for run '{run_id}'", - response=response, + response=_response, ) - return json_response.get("data", []) + _json_response = typing.cast("dict[str, object]", _json_response) + + return typing.cast("list[dict[str, str]]", _json_response.get("data", [])) @prettify_pydantic @pydantic.validate_call @@ -1058,17 +1061,19 @@ def get_alerts( start_index: pydantic.NonNegativeInt | None = None, count_limit: pydantic.PositiveInt | None = None, sort_by_columns: list[tuple[str, bool]] | None = None, - ) -> list[AlertBase] | list[str | None]: - """Retrieve alerts for a given run + ) -> list[AlertType] | list[str | None]: + """Retrieve alerts for a given run. Parameters ---------- run_id : str | None The ID of the run to find alerts for critical_only : bool, optional - If a run is specified, whether to only return details about alerts which are currently critical, by default True + If a run is specified, whether to only return details about alerts + which are currently critical, by default True names_only: bool, optional - Whether to only return the names of the alerts (otherwise return the full details of the alerts), by default True + Whether to only return the names of the alerts (otherwise return the + full details of the alerts), by default True start_index : typing.int, optional slice results returning only those above this index, by default None count_limit : typing.int, optional @@ -1090,22 +1095,32 @@ def get_alerts( """ if not run_id: if critical_only: - raise RuntimeError( - "critical_only is ambiguous when returning alerts with no run ID specified." + _out_msg: str = ( + "critical_only is ambiguous when returning alerts " + "with no run ID specified." ) + raise RuntimeError(_out_msg) + _sorting = ( + [ + AlertSort(column=column, descending=descending) + for column, descending in sort_by_columns + ] + if sort_by_columns + else None + ) + if names_only: + return [ + alert.name + for _, alert in Alert.get( + sorting=_sorting, count=count_limit, offset=start_index + ) + ] return [ - alert.name if names_only else alert + alert for _, alert in Alert.get( - sorting=[ - dict(zip(("column", "descending"), a, strict=False)) - for a in sort_by_columns - ] - if sort_by_columns - else None, - count=count_limit, - offset=start_index, + sorting=_sorting, count=count_limit, offset=start_index ) - ] # type: ignore + ] if sort_by_columns: logger.warning( @@ -1113,13 +1128,23 @@ def get_alerts( " argument 'sort_by_columns' will be ignored" ) - _alerts = [ - Alert(identifier=alert.get("id"), **alert) - for alert in Run(identifier=run_id).get_alert_details() - ] + try: + _alerts = [ + Alert(identifier=typing.cast("str", alert["id"]), _local=True, **alert) + for alert in Run(identifier=run_id).get_alert_details() + ] + except KeyError as e: + _out_msg = "Expected 'id' in server response for alert." + raise RuntimeError(_out_msg) from e + if names_only: + return [ + alert.name + for alert in _alerts + if not critical_only or alert.get_status(run_id) == "critical" + ] return [ - alert.name if names_only else alert + alert for alert in _alerts if not critical_only or alert.get_status(run_id) == "critical" ] @@ -1132,8 +1157,8 @@ def get_tags( start_index: pydantic.NonNegativeInt | None = None, count_limit: pydantic.PositiveInt | None = None, sort_by_columns: list[tuple[str, bool]] | None = None, - ) -> typing.Generator[Tag]: - """Retrieve tags + ) -> Generator[tuple[str, Tag]]: + """Retrieve tags. Parameters ---------- @@ -1161,8 +1186,8 @@ def get_tags( count=count_limit, offset=start_index, sorting=[ - dict(zip(("column", "descending"), a, strict=False)) - for a in sort_by_columns + TagSort(column=column, descending=descending) + for column, descending in sort_by_columns ] if sort_by_columns else None, @@ -1171,7 +1196,7 @@ def get_tags( @prettify_pydantic @pydantic.validate_call def delete_tag(self, tag_id: str) -> None: - """Delete a tag by its identifier + """Delete a tag by its identifier. Parameters ---------- @@ -1184,12 +1209,12 @@ def delete_tag(self, tag_id: str) -> None: if the deletion failed due to a server request error """ with contextlib.suppress(ValueError): - Tag(identifier=tag_id).delete() + _ = Tag(identifier=tag_id).delete() @prettify_pydantic @pydantic.validate_call def get_tag(self, tag_id: str) -> Tag: - """Retrieve a single tag + """Retrieve a single tag. Parameters ---------- diff --git a/simvue/config/user.py b/simvue/config/user.py index f631fa01..7e81662e 100644 --- a/simvue/config/user.py +++ b/simvue/config/user.py @@ -7,33 +7,38 @@ """ import functools +import http import logging import os -import typing -import http import pathlib +import typing + import pydantic -import toml import semver +import toml import simvue.utilities as sv_util +from simvue.api.request import get as sv_get +from simvue.api.url import URL +from simvue.config.files import ( + CONFIG_FILE_NAMES, + CONFIG_INI_FILE_NAMES, + DEFAULT_OFFLINE_DIRECTORY, +) from simvue.config.parameters import ( ClientGeneralOptions, DefaultRunSpecifications, MetricsSpecifications, - ServerSpecifications, OfflineSpecifications, + ServerSpecifications, ) - -from simvue.config.files import ( - CONFIG_FILE_NAMES, - CONFIG_INI_FILE_NAMES, - DEFAULT_OFFLINE_DIRECTORY, -) -from simvue.version import __version__ -from simvue.api.request import get as sv_get -from simvue.api.url import URL from simvue.eco.config import EcoConfig +from simvue.version import __version__ + +try: + from typing import Self +except ImportError: + from typing import Self logger = logging.getLogger(__name__) @@ -113,7 +118,7 @@ def _check_server( except Exception as err: raise AssertionError( - f"Exception retrieving server version:\n {str(err)}" + f"Exception retrieving server version:\n {err!s}" ) from err _version = semver.Version.parse(_version_str) @@ -154,7 +159,7 @@ def fetch( server_url: str | None = None, server_token: str | None = None, mode: typing.Literal["offline", "online", "disabled"] | None = None, - ) -> "SimvueConfiguration": + ) -> Self: """Retrieve the Simvue configuration from this project Will retrieve the configuration options set for this project either using diff --git a/simvue/converters.py b/simvue/converters.py index 3c6bfa17..69fb4db0 100644 --- a/simvue/converters.py +++ b/simvue/converters.py @@ -18,7 +18,7 @@ def aggregated_metrics_to_dataframe( - request_response_data: dict[str, list[dict[str, float]]], + request_response_data: dict[str, object], xaxis: str, parse_to: typing.Literal["dict", "dataframe"] = "dict", ) -> DataFrame | dict[str, dict[tuple[float, str], float | None]]: @@ -87,7 +87,7 @@ def aggregated_metrics_to_dataframe( def parse_run_set_metrics( - request_response_data: dict[str, dict[str, list[dict[str, float]]]], + request_response_data: dict[str, object], xaxis: str, run_labels: list[str], parse_to: typing.Literal["dict", "dataframe"] = "dict", diff --git a/simvue/utilities.py b/simvue/utilities.py index 93cadfb6..5fd504d8 100644 --- a/simvue/utilities.py +++ b/simvue/utilities.py @@ -1,23 +1,22 @@ +import contextlib import datetime +import functools import hashlib -import logging +import importlib.util import json +import logging import mimetypes -import tabulate -import pydantic -import importlib.util -import functools -import contextlib import os import pathlib import typing + import jwt +import pydantic +import tabulate from deepmerge import Merger -from datetime import timezone from simvue.models import DATETIME_FORMAT - CHECKSUM_BLOCK_SIZE = 4096 EXTRAS: tuple[str, ...] = ("plot", "torch") @@ -135,10 +134,10 @@ def parse_validation_response( return str(_table) -def check_extra(extra_name: str) -> typing.Callable: +def check_extra(extra_name: str) -> typing.Callable[[object], object | None]: def decorator( - class_func: typing.Callable | None = None, - ) -> typing.Callable | None: + class_func: typing.Callable[[object], object | None] | None = None, + ) -> typing.Callable[[object], object | None] | None: @functools.wraps(class_func) def wrapper(self, *args, **kwargs) -> typing.Any: if extra_name == "plot" and not all( @@ -150,7 +149,7 @@ def wrapper(self, *args, **kwargs) -> typing.Any: raise RuntimeError( f"Plotting features require the '{extra_name}' extension to Simvue" ) - elif extra_name == "eco": + if extra_name == "eco": if not importlib.util.find_spec("geocoder"): raise RuntimeError( f"Eco features require the '{extra_name}' extenstion to Simvue" @@ -182,7 +181,7 @@ def parse_pydantic_error(error: pydantic.ValidationError) -> str: else: _input_str = ( _input_str - if len((_input_str := f"{_input}")) < 50 + if len(_input_str := f"{_input}") < 50 else f"{_input_str[:50]}..." ) _type: str = data["type"] @@ -397,7 +396,7 @@ def simvue_timestamp(date_time: datetime.datetime | None = None) -> str: Datetime string valid for the Simvue server """ if not date_time: - date_time = datetime.datetime.now(timezone.utc) + date_time = datetime.datetime.now(datetime.UTC) return date_time.strftime(DATETIME_FORMAT) From 3615b5f9bfc51ac555aa149042f4f2618ced5535 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Zar=C4=99bski?= Date: Mon, 15 Sep 2025 13:40:44 +0100 Subject: [PATCH 03/23] =?UTF-8?q?=F0=9F=9A=A7=20Continue=20linting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 + simvue/api/objects/__init__.py | 62 ++++--- simvue/api/objects/artifact/base.py | 45 +++-- simvue/api/objects/base.py | 270 ++++++++++++++++++---------- simvue/api/objects/events.py | 115 ++++++++---- simvue/api/objects/folder.py | 103 ++++++----- simvue/api/objects/metrics.py | 18 +- simvue/api/request.py | 2 +- simvue/config/parameters.py | 19 +- simvue/utilities.py | 4 +- 10 files changed, 406 insertions(+), 234 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 790c48da..c5b859f9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -134,8 +134,10 @@ reportImplicitStringConcatenation = "none" reportUnreachable = "none" reportUnknownMemberType = "none" reportUnknownVariableType = "none" +reportAny = "none" # ruff already does this reportMissingTypeStubs = "none" reportPrivateUsage = "none" # ruff already does this +reportImportCycles = "none" [tool.mypy] ignore_missing_imports = true diff --git a/simvue/api/objects/__init__.py b/simvue/api/objects/__init__.py index 36950de4..46c1850d 100644 --- a/simvue/api/objects/__init__.py +++ b/simvue/api/objects/__init__.py @@ -1,6 +1,4 @@ -""" -Simvue API Objects -================== +"""Simvue API Objects. The following module defines objects which provide exact representations of information accessible via the Simvue RestAPI, this provides a lower @@ -8,28 +6,42 @@ """ -from .administrator import Tenant as Tenant, User as User +from .administrator import Tenant, User from .alert import ( - Alert as Alert, - EventsAlert as EventsAlert, - MetricsThresholdAlert as MetricsThresholdAlert, - MetricsRangeAlert as MetricsRangeAlert, - UserAlert as UserAlert, -) -from .storage import ( - S3Storage as S3Storage, - FileStorage as FileStorage, - Storage as Storage, -) -from .artifact import ( - FileArtifact as FileArtifact, - ObjectArtifact as ObjectArtifact, - Artifact as Artifact, + Alert, + EventsAlert, + MetricsRangeAlert, + MetricsThresholdAlert, + UserAlert, ) +from .artifact import Artifact, FileArtifact, ObjectArtifact +from .events import Events +from .folder import Folder, get_folder_from_path +from .metrics import Metrics +from .run import Run +from .stats import Stats +from .storage import FileStorage, S3Storage, Storage +from .tag import Tag -from .stats import Stats as Stats -from .run import Run as Run -from .tag import Tag as Tag -from .folder import Folder as Folder, get_folder_from_path as get_folder_from_path -from .events import Events as Events -from .metrics import Metrics as Metrics +__all__ = [ + "Alert", + "Artifact", + "Events", + "EventsAlert", + "FileArtifact", + "FileStorage", + "Folder", + "Metrics", + "MetricsRangeAlert", + "MetricsThresholdAlert", + "ObjectArtifact", + "Run", + "S3Storage", + "Stats", + "Storage", + "Tag", + "Tenant", + "User", + "UserAlert", + "get_folder_from_path", +] diff --git a/simvue/api/objects/artifact/base.py b/simvue/api/objects/artifact/base.py index e8e4df92..cba0af19 100644 --- a/simvue/api/objects/artifact/base.py +++ b/simvue/api/objects/artifact/base.py @@ -10,24 +10,31 @@ import http import io import typing + import pydantic try: from typing import Self except ImportError: - from typing_extensions import Self # noqa: F401 + from typing import Self # noqa: F401 -from simvue.api.url import URL -from simvue.exception import ObjectNotFoundError -from simvue.models import DATETIME_FORMAT from simvue.api.objects.base import SimvueObject, staging_check, write_only from simvue.api.objects.run import Run from simvue.api.request import ( - put as sv_put, + get as sv_get, +) +from simvue.api.request import ( get_json_from_response, +) +from simvue.api.request import ( post as sv_post, - get as sv_get, ) +from simvue.api.request import ( + put as sv_put, +) +from simvue.api.url import URL +from simvue.exception import ObjectNotFoundError +from simvue.models import DATETIME_FORMAT Category = typing.Literal["code", "input", "output"] @@ -38,10 +45,14 @@ class ArtifactBase(SimvueObject): - """Connect to/create an artifact locally or on the server""" + """Connect to/create an artifact locally or on the server.""" def __init__( - self, identifier: str | None = None, _read_only: bool = True, **kwargs + self, + identifier: str | None = None, + *, + _read_only: bool = True, + **kwargs: object, ) -> None: """Initialise an artifact connection. @@ -50,14 +61,18 @@ def __init__( identifier : str, optional the identifier of this object on the server. """ - - self._label = "artifact" - self._endpoint = f"{self._label}s" - super().__init__(identifier=identifier, _read_only=_read_only, **kwargs) + self._label: str = "artifact" + self._endpoint: str = f"{self._label}s" + super().__init__(identifier=identifier, _read_only=_read_only, **kwargs) # pyright: ignore[reportArgumentType] # If the artifact is an online instance, need a place to store the response # from the initial creation - self._init_data: dict[str, dict] = {} + self._init_data: dict[str, dict[str, object]] = {} + + @classmethod + @typing.override + def new(cls, *_: object, **__: object) -> None: + raise NotImplementedError def commit(self) -> None: """Not applicable, cannot commit single write artifact.""" @@ -277,7 +292,7 @@ def download_url(self) -> URL | None: return self._get_attribute("url") @property - def runs(self) -> typing.Generator[str, None, None]: + def runs(self) -> typing.Generator[str]: """Retrieve all runs for which this artifact is related. Yields @@ -317,7 +332,7 @@ def get_category(self, run_id: str) -> Category: return _json_response["category"] @pydantic.validate_call - def download_content(self) -> typing.Generator[bytes, None, None]: + def download_content(self) -> typing.Generator[bytes]: """Stream artifact content. Yields diff --git a/simvue/api/objects/base.py b/simvue/api/objects/base.py index 4ea3532e..0ffe32e6 100644 --- a/simvue/api/objects/base.py +++ b/simvue/api/objects/base.py @@ -13,7 +13,7 @@ import types import typing import uuid -from collections.abc import Generator +from collections.abc import Generator # noqa: TC003 import msgpack import pydantic @@ -59,7 +59,7 @@ def staging_check( member_func: typing.Callable[[U], typing.Any | None], ) -> typing.Callable[[U], typing.Any | None]: - """Checki if requested attribute has uncommitted changes via decorator.""" + """Check if requested attribute has uncommitted changes via decorator.""" def _wrapper(self: U) -> object: _sv_obj = typing.cast("SimvueObject", getattr(self, "_sv_obj", self)) @@ -197,24 +197,32 @@ def tenant(self, tenant: bool) -> None: class Sort(pydantic.BaseModel): + """Base class for performing sort during retrieval.""" + column: str descending: bool = True - def to_params(self) -> dict[str, str]: + def to_params(self) -> dict[str, str | bool]: + """Convert to RestAPI parameters.""" return {"id": self.column, "desc": self.descending} class SimvueObject(abc.ABC): + """Base class for all RestAPI objects.""" + def __init__( self, identifier: str | None = None, + *, _read_only: bool = True, _local: bool = False, _user_agent: str | None = None, _offline: bool = False, - **kwargs, + **kwargs: object, ) -> None: - self._logger = logging.getLogger(f"simvue.{self.__class__.__name__}") + self._logger: logging.Logger = logging.getLogger( + f"simvue.{self.__class__.__name__}" + ) self._label: str = getattr(self, "_label", self.__class__.__name__.lower()) # Local blocks any remote connection completely, this prevents multiple server @@ -228,7 +236,7 @@ def __init__( self._identifier: str | None = ( identifier if identifier is not None else f"offline_{uuid.uuid1()}" ) - self._properties = [ + self._properties: list[str] = [ name for name, member in inspect.getmembers(self.__class__) if isinstance(member, property) @@ -243,7 +251,9 @@ def __init__( "mode": "offline" if self._offline else "online", } - self._user_config = SimvueConfiguration.fetch(**_config_args) + self._user_config: SimvueConfiguration = SimvueConfiguration.fetch( + **_config_args + ) # Use a single file for each object so we can have parallelism # e.g. multiple runs writing at the same time @@ -255,7 +265,9 @@ def __init__( self._headers: dict[str, str] = ( { - "Authorization": f"Bearer {self._user_config.server.token.get_secret_value()}", + "Authorization": ( + f"Bearer {self._user_config.server.token.get_secret_value()}" + ), "User-Agent": _user_agent or f"Simvue Python client {__version__}", "Accept-Encoding": "gzip", } @@ -282,20 +294,23 @@ def __init__( self._staging |= kwargs - def _get_local_staged(self, obj_label: str | None = None) -> dict[str, typing.Any]: - """Retrieve any locally staged data for this identifier""" + def _get_local_staged(self) -> dict[str, object]: + """Retrieve any locally staged data for this identifier.""" if not self._local_staging_file.exists() or not self._identifier: return {} with self._local_staging_file.open() as in_f: - _staged_data = json.load(in_f) - - return _staged_data + return typing.cast("dict[str, object]", json.load(in_f)) - def _stage_to_other(self, obj_label: str, key: str, value: typing.Any) -> None: - """Stage a change to another object type""" + def _stage_to_other( + self, + obj_label: str, + key: str, + value: typing.Any, # noqa: ANN401 + ) -> None: + """Stage a change to another object type.""" with self._local_staging_file.open() as in_f: - _staged_data = json.load(in_f) + _staged_data = typing.cast("dict[str, typing.Any]", json.load(in_f)) if key not in _staged_data[obj_label]: _staged_data[key] = value @@ -348,7 +363,7 @@ def _get_attribute( return self._staging[attribute] except KeyError as e: if self._local: - raise e + raise # If the key is not in staging, but the object is not in offline mode # retrieve from the server and update cache instead if not _offline_state and ( @@ -356,25 +371,32 @@ def _get_attribute( ): self._staging[attribute] = _attribute return _attribute - raise AttributeError( + _out_msg: str = ( f"Could not retrieve attribute '{attribute}' " f"for {self._label} '{self._identifier}' from cached data" - ) from e + ) + raise AttributeError(_out_msg) from e try: self._logger.debug( - f"Retrieving attribute '{attribute}' from {self._label} '{self._identifier}'" + "Retrieving attribute '%s' from %s '%s'", + attribute, + self._label, + self._identifier, ) return self._get(url=url)[attribute] except KeyError as e: if self._offline: - raise AttributeError( + _out_msg = ( f"A value for attribute '{attribute}' has " - f"not yet been committed for offline {self._label} '{self._identifier}'" - ) from e - raise RuntimeError( + f"not yet been committed for offline {self._label}" + f" {self._identifier}'" + ) + raise AttributeError(_out_msg) from e + _out_msg = ( f"Expected key '{attribute}' for {self._label} '{self._identifier}'" - ) from e + ) + raise RuntimeError(_out_msg) from e def _clear_staging(self) -> None: self._staging = {} @@ -393,18 +415,21 @@ def _clear_staging(self) -> None: def _get_visibility(self) -> dict[str, bool | list[str]]: try: - return self._get_attribute("visibility") + return typing.cast( + "dict[str, bool | list[str]]", self._get_attribute("visibility") + ) except AttributeError: return {} @classmethod - def new(cls, **_) -> Self: - pass + @abc.abstractmethod + def new(cls, **_: object) -> Self: + """Define new instance of this object.""" @classmethod def ids( - cls, count: int | None = None, offset: int | None = None, **kwargs - ) -> typing.Generator[str]: + cls, count: int | None = None, offset: int | None = None, **kwargs: object + ) -> Generator[str]: """Retrieve a list of all object identifiers. Parameters @@ -422,12 +447,16 @@ def ids( _class_instance = cls(_read_only=True, _local=True) _count: int = 0 for response in cls._get_all_objects(offset, count=count, **kwargs): - if (_data := response.get("data")) is None: - raise RuntimeError( - f"Expected key 'data' for retrieval of {_class_instance.__class__.__name__.lower()}s" + _data = typing.cast("list[dict[str, object]] | None", response.get("data")) + if _data is None: + _out_msg: str = ( + f"Expected key 'data' for retrieval of " + f"{_class_instance.__class__.__name__.lower()}s" ) + raise RuntimeError(_out_msg) for entry in _data: - yield entry["id"] + _id = typing.cast("str", entry["id"]) + yield _id _count += 1 if count and _count > count: return @@ -436,9 +465,10 @@ def ids( @pydantic.validate_call def get( cls, + *, count: pydantic.PositiveInt | None = None, offset: pydantic.NonNegativeInt | None = None, - **kwargs, + **kwargs: object, ) -> Generator[tuple[str, Self]]: """Retrieve items of this object type from the server. @@ -460,25 +490,30 @@ def get( """ _class_instance = cls(_read_only=True, _local=True) _count: int = 0 + for _response in cls._get_all_objects(offset, count=count, **kwargs): if count and _count > count: return - if (_data := _response.get("data")) is None: - raise RuntimeError( - f"Expected key 'data' for retrieval of {_class_instance.__class__.__name__.lower()}s" + + _data = typing.cast("list[dict[str, object]] | None", _response.get("data")) + if _data is None: + _out_msg: str = ( + f"Expected key 'data' for retrieval of " + f"{_class_instance.__class__.__name__.lower()}s" ) + raise RuntimeError(_out_msg) # If data is an empty list if not _data: return for entry in _data: - _id = entry["id"] - yield _id, cls(_read_only=True, identifier=_id, _local=True, **entry) + _id = typing.cast("str", entry["id"]) + yield _id, cls(_read_only=True, identifier=_id, _local=True, **entry) # pyright: ignore[reportArgumentType] _count += 1 @classmethod - def count(cls, **kwargs) -> int: + def count(cls, **kwargs: object) -> int: """Return the total number of entries for this object type from the server. Returns @@ -489,17 +524,20 @@ def count(cls, **kwargs) -> int: _class_instance = cls(_read_only=True) _count_total: int = 0 for _data in cls._get_all_objects(count=None, offset=None, **kwargs): - if not (_count := _data.get("count")): - raise RuntimeError( - f"Expected key 'count' for retrieval of {_class_instance.__class__.__name__.lower()}s" + _count = typing.cast("int | None", _data.get("count")) + if not _count: + _out_msg: str = ( + "Expected key 'count' for retrieval of" + f" {_class_instance.__class__.__name__.lower()}s" ) + raise RuntimeError(_out_msg) _count_total += _count return _count_total @classmethod def _get_all_objects( - cls, offset: int | None, count: int | None, **kwargs - ) -> typing.Generator[dict]: + cls, offset: int | None, count: int | None, **kwargs: object + ) -> Generator[dict[str, object]]: _class_instance = cls(_read_only=True) _url = f"{_class_instance._base_url}" @@ -507,15 +545,22 @@ def _get_all_objects( _label = _label.removesuffix("s") for response in get_paginated( - _url, headers=_class_instance._headers, offset=offset, count=count, **kwargs + _url, + headers=_class_instance._headers, + offset=offset, + count=count, + **kwargs, # pyright: ignore[reportArgumentType] ): - yield get_json_from_response( - response=response, - expected_status=[http.HTTPStatus.OK], - scenario=f"Retrieval of {_label}s", - ) # type: ignore + yield typing.cast( + "dict[str, object]", + get_json_from_response( + response=response, + expected_status=[http.HTTPStatus.OK], + scenario=f"Retrieval of {_label}s", + ), + ) - def read_only(self, is_read_only: bool) -> None: + def read_only(self, is_read_only: bool) -> None: # noqa: FBT001 """Set whether this object is in read only state. Parameters @@ -532,30 +577,39 @@ def read_only(self, is_read_only: bool) -> None: if not self._read_only: self._staging = self._get_local_staged() - def commit(self) -> dict | None: + def commit(self) -> dict[str, object] | None: """Send updates to the server, or if offline, store locally.""" if self._read_only: raise AttributeError("Cannot commit object in 'read-only' mode") if self._offline: self._logger.debug( - f"Writing updates to staging file for {self._label} '{self.id}': {self._staging}" + "Writing updates to staging file for %s '%s': %s", + self._label, + self.id, + self._staging, ) self._cache() return None - _response: dict | None = None + _response: dict[str, object] | None = None # Initial commit is creation of object # if staging is empty then we do not need to use PUT if not self._identifier or self._identifier.startswith("offline_"): self._logger.debug( - f"Posting from staged data for {self._label} '{self.id}': {self._staging}" + "Posting from staged data for %s '%s': %s", + self._label, + self.id, + self._staging, ) _response = self._post(**self._staging) elif self._staging: self._logger.debug( - f"Pushing updates from staged data for {self._label} '{self.id}': {self._staging}" + "Pushing updates from staged data for %s '%s': %s", + self._label, + self.id, + self._staging, ) _response = self._put(**self._staging) @@ -581,7 +635,7 @@ def id(self) -> str | None: @property def _base_url(self) -> URL: - return URL(self._user_config.server.url) / self._endpoint + return URL(f"{self._user_config.server.url}") / self._endpoint @property def url(self) -> URL | None: @@ -593,9 +647,14 @@ def url(self) -> URL | None: """ return None if self._identifier is None else self._base_url / self._identifier - def _post(self, is_json: bool = True, **kwargs) -> dict[str, typing.Any]: - if not is_json: - kwargs = msgpack.packb(kwargs, use_bin_type=True) + def _post(self, *, is_json: bool = True, **kwargs: object) -> dict[str, typing.Any]: + if not is_json and not ( + kwargs := (msgpack.packb(kwargs, use_bin_type=True) or {}) + ): + raise TypeError( + "Expected value for object data post after packing but" + " got empty dictionary." + ) _response = sv_post( url=f"{self._base_url}", headers=self._headers | {"Content-Type": "application/msgpack"}, @@ -605,9 +664,11 @@ def _post(self, is_json: bool = True, **kwargs) -> dict[str, typing.Any]: ) if _response.status_code == http.HTTPStatus.FORBIDDEN: - raise RuntimeError( - f"Forbidden: You do not have permission to create object of type '{self._label}'" + _out_msg: str = ( + "Forbidden: You do not have permission to create " + f"object of type '{self._label}'" ) + raise RuntimeError(_out_msg) _json_response = get_json_from_response( response=_response, @@ -616,11 +677,10 @@ def _post(self, is_json: bool = True, **kwargs) -> dict[str, typing.Any]: ) if isinstance(_json_response, list): - raise RuntimeError( - "Expected dictionary from JSON response but got type list" - ) + raise TypeError("Expected dictionary from JSON response but got type list") - if _id := _json_response.get("id"): + _id = typing.cast("str | None", _json_response.get("id")) + if _id: self._logger.debug("'%s' created successfully", _id) self._identifier = _id elif not self._is_set: @@ -629,31 +689,36 @@ def _post(self, is_json: bool = True, **kwargs) -> dict[str, typing.Any]: if not _detail: _detail = "No information in JSON response." - raise RuntimeError( - f"Expected new ID for {self._label} but none found: {_detail}." - ) + _out_msg = f"Expected new ID for {self._label} but none found: {_detail}." + raise RuntimeError(_out_msg) return _json_response - def _put(self, **kwargs) -> dict[str, typing.Any]: + def _put(self, **kwargs: object) -> dict[str, object]: if not self.url: - raise RuntimeError(f"Identifier for instance of {self._label} Unknown") + _out_msg: str = f"Identifier for instance of {self._label} Unknown" + raise RuntimeError(_out_msg) _response = sv_put( url=f"{self.url}", headers=self._headers, data=kwargs, is_json=True ) if _response.status_code == http.HTTPStatus.FORBIDDEN: - raise RuntimeError( - f"Forbidden: You do not have permission to create object of type '{self._label}'" + _out_msg = ( + "Forbidden: You do not have permission to " + f"create object of type '{self._label}'" ) + raise RuntimeError(_out_msg) - return get_json_from_response( - response=_response, - expected_status=[http.HTTPStatus.OK, http.HTTPStatus.CONFLICT], - scenario=f"Creation of {self._label} '{self._identifier}", + return typing.cast( + "dict[str, object]", + get_json_from_response( + response=_response, + expected_status=[http.HTTPStatus.OK, http.HTTPStatus.CONFLICT], + scenario=f"Creation of {self._label} '{self._identifier}", + ), ) - def delete(self, **kwargs) -> dict[str, typing.Any]: + def delete(self, **kwargs: object) -> dict[str, object]: """Delete the server entry for this object. Returns @@ -668,10 +733,12 @@ def delete(self, **kwargs) -> dict[str, typing.Any]: return {"id": self._identifier} if not self._identifier: - raise RuntimeError(f"Object of type '{self._label}' has no identifier.") + _out_msg: str = f"Object of type '{self._label}' has no identifier." + raise RuntimeError(_out_msg) if not self.url: - raise RuntimeError(f"Identifier for instance of {self._label} Unknown") + _out_msg = f"Identifier for instance of {self._label} Unknown" + raise RuntimeError(_out_msg) _response = sv_delete(url=f"{self.url}", headers=self._headers, params=kwargs) _json_response = get_json_from_response( response=_response, @@ -680,16 +747,21 @@ def delete(self, **kwargs) -> dict[str, typing.Any]: ) self._logger.debug("'%s' deleted successfully", self._identifier) - return _json_response + return typing.cast("dict[str, object]", _json_response) def _get( - self, url: str | None = None, allow_parse_failure: bool = False, **kwargs + self, + url: str | None = None, + *, + allow_parse_failure: bool = False, + **kwargs: str | float | None, ) -> dict[str, typing.Any]: - if self._identifier.startswith("offline_"): + if (self._identifier or "").startswith("offline_"): return self._get_local_staged() if not self.url: - raise RuntimeError(f"Identifier for instance of {self._label} Unknown") + _out_msg: str = f"Identifier for instance of {self._label} Unknown" + raise RuntimeError(_out_msg) _response = sv_get( url=f"{url or self.url}", headers=self._headers, params=kwargs @@ -709,10 +781,11 @@ def _get( self._logger.debug("'%s' retrieved successfully", self._identifier) if not isinstance(_json_response, dict): - raise RuntimeError( - f"Expected dictionary from JSON response during {self._label} retrieval " - f"but got '{type(_json_response)}'" + _out_msg = ( + f"Expected dictionary from JSON response during {self._label}" + f"retrieval but got '{type(_json_response)}'" ) + raise TypeError(_out_msg) return _json_response def refresh(self) -> None: @@ -730,7 +803,7 @@ def _cache(self) -> None: with self._local_staging_file.open() as in_f: _local_data = json.load(in_f) - staging_merger.merge(_local_data, self._staging) + _ = staging_merger.merge(_local_data, self._staging) with self._local_staging_file.open("w", encoding="utf-8") as out_f: json.dump(_local_data, out_f, indent=2) @@ -746,10 +819,8 @@ def to_dict(self) -> dict[str, typing.Any]: return self._get() | self._staging def on_reconnect(self, id_mapping: dict[str, str]) -> None: - """Executed when a run switches from offline to online mode. - - In this case no action is taken. - """ + """Perform action when switching from offline to online mode.""" + _ = id_mapping @property def staged(self) -> dict[str, typing.Any] | None: @@ -762,24 +833,27 @@ def staged(self) -> dict[str, typing.Any] | None: """ return self._staging or None + @typing.override def __str__(self) -> str: - """String representation of Simvue object.""" + """Represent Simvue object as string.""" return f"{self.__class__.__name__}({self.id=})" + @typing.override def __repr__(self) -> str: + """Represent Simvue object as Python repr format.""" _out_str = f"{self.__class__.__module__}.{self.__class__.__qualname__}(" _property_values: list[str] = [] - for property in self._properties: + for _property in self._properties: try: - _value = getattr(self, property) + _value = getattr(self, _property) except KeyError: continue if isinstance(_value, types.GeneratorType): continue - _property_values.append(f"{property}={_value!r}") + _property_values.append(f"{_property}={_value!r}") _out_str += ", ".join(_property_values) _out_str += ")" diff --git a/simvue/api/objects/events.py b/simvue/api/objects/events.py index f786c99b..bcb835ed 100644 --- a/simvue/api/objects/events.py +++ b/simvue/api/objects/events.py @@ -1,87 +1,107 @@ -""" -Simvue Server Events -==================== +"""Simvue Server Events. Contains a class for remotely connecting to Simvue events, or defining a new set of events given relevant arguments. """ +import datetime import http import typing -import datetime +from collections.abc import Generator import pydantic -from simvue.api.url import URL +from simvue.api.request import get as sv_get +from simvue.api.request import get_json_from_response +from simvue.models import DATETIME_FORMAT, EventSet from .base import SimvueObject -from simvue.models import DATETIME_FORMAT, EventSet -from simvue.api.request import get as sv_get, get_json_from_response try: from typing import Self except ImportError: - from typing_extensions import Self + from typing import Self + +if typing.TYPE_CHECKING: + from simvue.api.url import URL __all__ = ["Events"] class Events(SimvueObject): + """Object representing a set of run events on the Simvue Server.""" + def __init__( self, + *, _read_only: bool = True, _local: bool = False, - **kwargs, + **kwargs: object, ) -> None: - self._label = "event" - super().__init__(_read_only=_read_only, _local=_local, **kwargs) - self._run_id = self._staging.get("run") - self._is_set = True + self._label: str = "event" + super().__init__(_read_only=_read_only, _local=_local, **kwargs) # pyright: ignore[reportArgumentType] + self._run_id: str | None = self._staging.get("run") + self._is_set: bool = True @classmethod @pydantic.validate_call + @typing.override def get( cls, run_id: str, *, count: pydantic.PositiveInt | None = None, offset: pydantic.PositiveInt | None = None, - **kwargs, - ) -> typing.Generator[EventSet, None, None]: + **kwargs: object, + ) -> Generator[EventSet]: + """Retrieve objects from the Simvue server.""" _class_instance = cls(_read_only=True, _local=True) _count: int = 0 for response in cls._get_all_objects(offset, count=count, run=run_id, **kwargs): - if (_data := response.get("data")) is None: - raise RuntimeError( - f"Expected key 'data' for retrieval of {_class_instance.__class__.__name__.lower()}s" + _data = typing.cast( + "list[dict[str, str | float | None]] | None", response.get("data") + ) + if _data is None: + _out_msg: str = ( + "Expected key 'data' for retrieval " + f"of {_class_instance.__class__.__name__.lower()}s" ) + raise RuntimeError(_out_msg) for _entry in _data: - yield EventSet(**_entry) + yield EventSet(**_entry) # pyright: ignore[reportArgumentType] _count += 1 - if _count > count: + if count and _count > count: return @classmethod @pydantic.validate_call + @typing.override def new( - cls, *, run: str, offline: bool = False, events: list[EventSet], **kwargs + cls, + *, + run: str, + offline: bool = False, + events: list[EventSet], + **kwargs: object, ) -> Self: - """Create a new Events entry on the Simvue server""" - return Events( + """Create a new Events entry on the Simvue server.""" + return cls( run=run, events=[event.model_dump() for event in events], _read_only=False, _offline=offline, - **kwargs, + **kwargs, # pyright: ignore[reportArgumentType] ) - def _post(self, **kwargs) -> dict[str, typing.Any]: + @typing.override + def _post(self, **kwargs: object) -> dict[str, typing.Any]: return super()._post(is_json=False, **kwargs) - def _put(self, **kwargs) -> dict[str, typing.Any]: + @typing.override + def _put(self, **kwargs: object) -> dict[str, typing.Any]: raise NotImplementedError("Method 'put' is not available for type Events") @pydantic.validate_call @@ -92,6 +112,24 @@ def histogram( window: int, filters: list[str] | None, ) -> list[dict[str, str | int]]: + """Return binned data for events. + + Parameters + ---------- + timestamp_begin : datetime.datetime + time window start period + timestamp_end : datetime.datetime + time window end period + window : int + window for aggregation + filters : list[str] | None + filter results using filter expressions + + Returns + ------- + list[dict[str, str | int]] + histogram data from server + """ if timestamp_end - timestamp_begin <= datetime.timedelta(seconds=window): raise ValueError( "Invalid arguments for datetime range, " @@ -101,13 +139,13 @@ def histogram( _time_begin: str = timestamp_begin.strftime(DATETIME_FORMAT) _time_end: str = timestamp_end.strftime(DATETIME_FORMAT) _response = sv_get( - url=_url, + url=f"{_url}", headers=self._headers, - params={ + params={ # pyright: ignore[reportArgumentType] "run": self._run_id, "window": window, - "timestamp_begin": timestamp_begin, - "timestamp_end": timestamp_end, + "timestamp_begin": _time_begin, + "timestamp_end": _time_end, } | ({"filters": filters} if filters else {}), ) @@ -116,9 +154,19 @@ def histogram( scenario="Retrieval of events histogram", response=_response, ) - return _json_response.get("data") + _json_response = typing.cast("dict[str, object]", _json_response) + + _data = typing.cast( + "list[dict[str, str | int]] | None", _json_response.get("data") + ) + + if not _data: + raise RuntimeError("Expected key 'data' in response for histogram request.") + + return _data - def delete(self, **kwargs) -> dict[str, typing.Any]: + @typing.override + def delete(self, **kwargs: object) -> dict[str, typing.Any]: """Event set deletion not implemented. Raises @@ -128,6 +176,7 @@ def delete(self, **kwargs) -> dict[str, typing.Any]: """ raise NotImplementedError("Cannot delete event set") - def on_reconnect(self, id_mapping: dict[str, str]): + @typing.override + def on_reconnect(self, id_mapping: dict[str, str]) -> None: if online_run_id := id_mapping.get(self._staging["run"]): self._staging["run"] = online_run_id diff --git a/simvue/api/objects/folder.py b/simvue/api/objects/folder.py index ccba6010..c604bd15 100644 --- a/simvue/api/objects/folder.py +++ b/simvue/api/objects/folder.py @@ -1,6 +1,4 @@ -""" -Simvue Server Folder -==================== +"""Simvue Server Folder. Contains a class for remotely connecting to a Simvue folder, or defining a new folder given relevant arguments. @@ -9,7 +7,6 @@ import datetime import json -import pathlib import typing from collections.abc import Generator @@ -20,7 +17,8 @@ from .base import SimvueObject, Sort, staging_check, write_only -# Need to use this inside of Generator typing to fix bug present in Python 3.10 - see issue #745 +# Need to use this inside of Generator typing to fix bug +# present in Python 3.10 - see issue #745 try: from typing import Self except ImportError: @@ -42,22 +40,21 @@ def check_column(cls, column: str) -> str: and column not in ("created", "modified", "path") and not column.startswith("metadata.") ): - raise ValueError(f"Invalid sort column for folders '{column}") + _out_msg: str = f"Invalid sort column for folders '{column}" + raise ValueError(_out_msg) return column class Folder(SimvueObject): - """ - Simvue Folder - ============= + """Simvue Folder. This class is used to connect to/create folder objects on the Simvue server, any modification of instance attributes is mirrored on the remote object. """ - def __init__(self, identifier: str | None = None, **kwargs) -> None: - """Initialise a Folder + def __init__(self, identifier: str | None = None, **kwargs: object) -> None: + """Initialise a Folder. If an identifier is provided a connection will be made to the object matching the identifier on the target server. @@ -72,28 +69,31 @@ def __init__(self, identifier: str | None = None, **kwargs) -> None: **kwargs : dict any additional arguments to be passed to the object initialiser """ - super().__init__(identifier, **kwargs) + super().__init__(identifier, **kwargs) # pyright: ignore[reportArgumentType] @classmethod @pydantic.validate_call + @typing.override def new( cls, *, path: typing.Annotated[str, pydantic.Field(pattern=FOLDER_REGEX)], offline: bool = False, - **kwargs, + **kwargs: object, ) -> Self: - """Create a new Folder on the Simvue server with the given path""" - return Folder(path=path, _read_only=False, _offline=offline, **kwargs) + """Create a new Folder on the Simvue server with the given path.""" + return cls(path=path, _read_only=False, _offline=offline, **kwargs) # pyright: ignore[reportArgumentType] @classmethod @pydantic.validate_call + @typing.override def get( cls, + *, count: pydantic.PositiveInt | None = None, offset: pydantic.NonNegativeInt | None = None, sorting: list[FolderSort] | None = None, - **kwargs, + **kwargs: str, ) -> Generator[tuple[str, Self]]: _params: dict[str, str] = kwargs @@ -105,103 +105,126 @@ def get( @property @staging_check def tags(self) -> list[str]: - """Return list of tags assigned to this folder""" - return self._get_attribute("tags") + """Return list of tags assigned to this folder.""" + return typing.cast("list[str]", self._get_attribute("tags")) @tags.setter @write_only @pydantic.validate_call def tags(self, tags: list[str]) -> None: - """Set tags assigned to this folder""" + """Set tags assigned to this folder.""" self._staging["tags"] = tags @property - def path(self) -> pathlib.Path: - """Return the path of this folder""" - return self._get_attribute("path") + def path(self) -> str: + """Return the path of this folder.""" + return typing.cast("str", self._get_attribute("path")) @property @staging_check def description(self) -> str | None: - """Return the folder description""" + """Return the folder description.""" return self._get().get("description") @description.setter @write_only @pydantic.validate_call def description(self, description: str) -> None: - """Update the folder description""" + """Update the folder description.""" self._staging["description"] = description @property @staging_check def name(self) -> str | None: - """Return the folder name""" + """Return the folder name.""" return self._get().get("name") @name.setter @write_only @pydantic.validate_call def name(self, name: str) -> None: - """Update the folder name""" + """Update the folder name.""" self._staging["name"] = name @property @staging_check - def metadata(self) -> dict[str, int | str | None | float | dict] | None: - """Return the folder metadata""" + def metadata(self) -> dict[str, int | str | None | float | dict[str, str]] | None: + """Return the folder metadata.""" return self._get().get("metadata") @metadata.setter @write_only @pydantic.validate_call - def metadata(self, metadata: dict[str, int | str | None | float | dict]) -> None: - """Update the folder metadata""" + def metadata( + self, metadata: dict[str, int | str | None | float | dict[str, str]] + ) -> None: + """Update the folder metadata.""" self._staging["metadata"] = metadata @property @staging_check def star(self) -> bool: - """Return if this folder is starred""" + """Return if this folder is starred.""" return self._get().get("starred", False) @star.setter @write_only @pydantic.validate_call def star(self, is_true: bool = True) -> None: - """Star this folder as a favourite""" + """Star this folder as a favourite.""" self._staging["starred"] = is_true @property @staging_check def ttl(self) -> int: - """Return the retention period for this folder""" - return self._get_attribute("ttl") + """Return the retention period for this folder.""" + return typing.cast("int", self._get_attribute("ttl")) @ttl.setter @write_only @pydantic.validate_call def ttl(self, time_seconds: int) -> None: - """Update the retention period for this folder""" + """Update the retention period for this folder.""" self._staging["ttl"] = time_seconds - def delete( # should params to this be optional and default to False? + @typing.override + def delete( self, *, recursive: bool | None = False, delete_runs: bool | None = False, runs_only: bool | None = False, ) -> dict[str, typing.Any]: + """Delete a folder and its contents. + + Parameters + ---------- + recursive : bool, optional + whether to delete any sub-folders, default is False. + delete_runs : bool, optional + whether to delete any runs in this folder, default is False. + runs_only : bool, optional + whether to delete only runs, default is False. + + Returns + ------- + dict[str, Any] + information on the deleted content. + """ return super().delete( recursive=recursive, runs=delete_runs, runs_only=runs_only ) @property def created(self) -> datetime.datetime | None: - """Retrieve created datetime for the run""" - _created: str | None = self._get_attribute("created") + """Retrieve created datetime for the run.""" + _created = typing.cast("str | None", self._get_attribute("created")) return ( - datetime.datetime.strptime(_created, DATETIME_FORMAT) if _created else None + datetime.datetime.strptime(_created, DATETIME_FORMAT).replace( + tzinfo=datetime.UTC + ) + if _created + else None ) @@ -215,4 +238,4 @@ def get_folder_from_path( _, _folder = next(_folders) except StopIteration as e: raise ObjectNotFoundError(obj_type="folder", name=path) from e - return _folder # type: ignore + return _folder diff --git a/simvue/api/objects/metrics.py b/simvue/api/objects/metrics.py index fb9cea1e..49c82d8e 100644 --- a/simvue/api/objects/metrics.py +++ b/simvue/api/objects/metrics.py @@ -1,6 +1,4 @@ -""" -Simvue Server Metrics -===================== +"""Simvue Server Metrics. Contains a class for remotely connecting to Simvue metrics, or defining a new set of metrics given relevant arguments. @@ -8,20 +6,21 @@ """ import http -import typing import json +import typing import pydantic +from simvue.api.request import get as sv_get +from simvue.api.request import get_json_from_response +from simvue.models import MetricSet from .base import SimvueObject -from simvue.models import MetricSet -from simvue.api.request import get as sv_get, get_json_from_response try: from typing import Self except ImportError: - from typing_extensions import Self + from typing import Self __all__ = ["Metrics"] @@ -31,9 +30,10 @@ class Metrics(SimvueObject): def __init__( self, + *, _read_only: bool = True, _local: bool = False, - **kwargs, + **kwargs: object, ) -> None: """Initialise a Metrics object instance.""" self._label = "metric" @@ -80,7 +80,7 @@ def get( count: pydantic.PositiveInt | None = None, offset: pydantic.PositiveInt | None = None, **kwargs, - ) -> typing.Generator[dict[str, dict[str, list[dict[str, float]]]], None, None]: + ) -> typing.Generator[dict[str, dict[str, list[dict[str, float]]]]]: """Retrieve metrics from the server for a given set of runs. Parameters diff --git a/simvue/api/request.py b/simvue/api/request.py index 5e0c34f2..4d715e4b 100644 --- a/simvue/api/request.py +++ b/simvue/api/request.py @@ -175,7 +175,7 @@ def put( def get( url: str, headers: dict[str, str] | None = None, - params: dict[str, str | int | float | None] | None = None, + params: dict[str, str | int | float | None | list[str]] | None = None, timeout: int = DEFAULT_API_TIMEOUT, json: dict[str, typing.Any] | None = None, ) -> requests.Response: diff --git a/simvue/config/parameters.py b/simvue/config/parameters.py index be54de9e..f6afe4b6 100644 --- a/simvue/config/parameters.py +++ b/simvue/config/parameters.py @@ -8,28 +8,27 @@ import logging import os +import pathlib import time -import pydantic import typing -import pathlib + +import pydantic import simvue.models as sv_models -from simvue.utilities import get_expiry from simvue.api.url import URL - +from simvue.config.files import DEFAULT_OFFLINE_DIRECTORY +from simvue.utilities import get_expiry logger = logging.getLogger(__file__) class ServerSpecifications(pydantic.BaseModel): - url: pydantic.AnyHttpUrl | None - token: pydantic.SecretStr | None + url: pydantic.AnyHttpUrl + token: pydantic.SecretStr @pydantic.field_validator("url") @classmethod def url_to_api_url(cls, v: typing.Any) -> str | None: - if not v: - return if f"{v}".endswith("/api"): return f"{v}" _url = URL(f"{v}") / "api" @@ -37,8 +36,6 @@ def url_to_api_url(cls, v: typing.Any) -> str | None: @pydantic.field_validator("token") def check_token(cls, v: typing.Any) -> str | None: - if not v: - return value = v.get_secret_value() if not (expiry := get_expiry(value)): raise AssertionError("Failed to parse Simvue token - invalid token form") @@ -48,7 +45,7 @@ def check_token(cls, v: typing.Any) -> str | None: class OfflineSpecifications(pydantic.BaseModel): - cache: pathlib.Path | None = None + cache: pathlib.Path = pathlib.Path(DEFAULT_OFFLINE_DIRECTORY) @pydantic.field_validator("cache") @classmethod diff --git a/simvue/utilities.py b/simvue/utilities.py index 5fd504d8..89d76681 100644 --- a/simvue/utilities.py +++ b/simvue/utilities.py @@ -10,10 +10,10 @@ import pathlib import typing +import deepmerge import jwt import pydantic import tabulate -from deepmerge import Merger from simvue.models import DATETIME_FORMAT @@ -416,7 +416,7 @@ def get_mimetype_for_file(file_path: pathlib.Path) -> str: # Create a new Merge strategy for merging local file and staging attributes -staging_merger = Merger( +staging_merger = deepmerge.Merger( # pass in a list of tuple, with the # strategies you are looking to apply # to each type. From d947c90e0843ee84a2d808686c7ee0ebd86a4e84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Zar=C4=99bski?= Date: Wed, 17 Sep 2025 11:06:10 +0100 Subject: [PATCH 04/23] =?UTF-8?q?=F0=9F=9A=A8=20Added=20more=20linter=20fi?= =?UTF-8?q?xes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- simvue/api/objects/artifact/base.py | 136 +++++++++++++++++---------- simvue/api/objects/artifact/fetch.py | 98 ++++++++++++------- simvue/api/objects/artifact/file.py | 55 +++++++---- simvue/api/objects/base.py | 94 +++++++++++------- simvue/api/url.py | 35 ++++--- simvue/config/user.py | 8 ++ simvue/run.py | 112 ++++++++++------------ 7 files changed, 319 insertions(+), 219 deletions(-) diff --git a/simvue/api/objects/artifact/base.py b/simvue/api/objects/artifact/base.py index 871a3053..6b2cd600 100644 --- a/simvue/api/objects/artifact/base.py +++ b/simvue/api/objects/artifact/base.py @@ -1,6 +1,4 @@ -""" -Simvue Artifact -=============== +"""Simvue Artifact. Class for defining and interacting with artifact objects. @@ -10,13 +8,19 @@ import http import io import typing +from collections.abc import Generator import pydantic try: from typing import Self except ImportError: - from typing import Self + from typing_extensions import Self # noqa: UP035 + +try: + from typing import override +except ImportError: + from typing_extensions import override # noqa: UP035 from simvue.api.objects.base import SimvueObject, staging_check, write_only from simvue.api.objects.run import Run @@ -70,19 +74,18 @@ def __init__( self._init_data: dict[str, dict[str, object]] = {} @classmethod - @typing.override - def new(cls, *_: object, **__: object) -> None: - raise NotImplementedError - - @classmethod - def new(cls, *_, **__) -> Self: + @override + def new(cls, *_: object, **__: object) -> Self: raise NotImplementedError + @override def commit(self) -> None: """Not applicable, cannot commit single write artifact.""" self._logger.info("Cannot call method 'commit' on write-once type 'Artifact'") - def attach_to_run(self, run_id: str, category: Category) -> None: + def attach_to_run( + self, run_id: str, category: Category + ) -> dict[str, object] | None: """Attach this artifact to a given run. Parameters @@ -91,13 +94,18 @@ def attach_to_run(self, run_id: str, category: Category) -> None: identifier of run to associate this artifact with. category : Literal['input', 'output', 'code'] category of this artifact with respect to the run. + + Returns + ------- + dict[str, object] | None + response from server or None if offline """ self._init_data["runs"][run_id] = category if self._offline: self._staging["runs"] = self._init_data["runs"] - super().commit() - return + _ = super().commit() + return None _run_artifacts_url = ( URL(self._user_config.server.url) @@ -110,37 +118,47 @@ def attach_to_run(self, run_id: str, category: Category) -> None: json={"category": category}, ) - get_json_from_response( + _json_response = get_json_from_response( expected_status=[http.HTTPStatus.OK], scenario=f"adding artifact '{self.name}' to run '{run_id}'", response=_response, ) + return typing.cast("dict[str, object]", _json_response) + + @override def on_reconnect(self, id_mapping: dict[str, str]) -> None: - """Operations performed when this artifact is switched from offline to online mode. + """Perform operation when this artifact is switched from offline to online mode. Parameters ---------- id_mapping : dict[str, str] mapping from offline identifier to new online identifier. """ - _offline_staging = self._init_data["runs"].copy() - for id, category in _offline_staging.items(): - self.attach_to_run(run_id=id_mapping[id], category=category) + _offline_staging = typing.cast( + "dict[str, Category]", self._init_data["runs"].copy() + ) + for _id, category in _offline_staging.items(): + _ = self.attach_to_run(run_id=id_mapping[_id], category=category) - def _upload(self, file: io.BytesIO, timeout: int, file_size: int) -> None: + def _upload( + self, file: io.BytesIO | io.BufferedReader, timeout: int | None, file_size: int + ) -> None: if self._offline: - super().commit() + _ = super().commit() return if not (_url := self._staging.get("url")): return if not timeout: - timeout = BASE_TIMEOUT + UPLOAD_TIMEOUT_PER_MB * file_size / 1024 / 1024 + timeout = BASE_TIMEOUT + UPLOAD_TIMEOUT_PER_MB * file_size // 1024 // 1024 self._logger.debug( - f"Will wait for a period of {timeout:.0f}s for upload of file for {file_size}B file to complete." + "Will wait for a period of %.0fs for upload of file " + "for %dB file to complete.", + timeout, + file_size, ) _name = self._staging["name"] @@ -160,7 +178,7 @@ def _upload(self, file: io.BytesIO, timeout: int, file_size: int) -> None: _response.status_code, ) - get_json_from_response( + _ = get_json_from_response( expected_status=[http.HTTPStatus.OK, http.HTTPStatus.NO_CONTENT], allow_parse_failure=True, # JSON response from S3 not parsible scenario=f"uploading artifact '{_name}' to object storage", @@ -168,20 +186,21 @@ def _upload(self, file: io.BytesIO, timeout: int, file_size: int) -> None: ) # Temporarily remove read-only state - self.read_only(False) + self.read_only(is_read_only=False) # Update the server status to confirm file uploaded self.uploaded = True - super().commit() - self.read_only(True) + _ = super().commit() + self.read_only(is_read_only=True) + @override def _get( - self, storage: str | None = None, url: str | None = None, **kwargs + self, url: str | None = None, storage: str | None = None, **kwargs: object ) -> dict[str, typing.Any]: return super()._get( storage=storage or self._staging.get("server", {}).get("storage_id"), url=url, - **kwargs, + **kwargs, # pyright: ignore[reportArgumentType] ) @property @@ -192,7 +211,7 @@ def checksum(self) -> str: ------- str """ - return self._get_attribute("checksum") + return typing.cast("str", self._get_attribute("checksum")) @property def storage_url(self) -> URL | None: @@ -202,7 +221,8 @@ def storage_url(self) -> URL | None: ------- simvue.api.url.URL | None """ - return URL(_url) if (_url := self._init_data.get("url")) else None + _url = typing.cast("str | None", self._init_data.get("url")) + return URL(_url) if _url else None @property def original_path(self) -> str: @@ -212,7 +232,7 @@ def original_path(self) -> str: ------- str """ - return self._get_attribute("original_path") + return typing.cast("str", self._get_attribute("original_path")) @property def storage_id(self) -> str | None: @@ -222,7 +242,7 @@ def storage_id(self) -> str | None: ------- str | None """ - return self._get_attribute("storage_id") + return typing.cast("str | None", self._get_attribute("storage_id")) @property def mime_type(self) -> str: @@ -232,7 +252,7 @@ def mime_type(self) -> str: ------- str """ - return self._get_attribute("mime_type") + return typing.cast("str", self._get_attribute("mime_type")) @property def size(self) -> int: @@ -242,7 +262,7 @@ def size(self) -> int: ------- int """ - return self._get_attribute("size") + return typing.cast("int", self._get_attribute("size")) @property def name(self) -> str | None: @@ -252,7 +272,7 @@ def name(self) -> str | None: ------- str | None """ - return self._get_attribute("name") + return typing.cast("str | None", self._get_attribute("name")) @property def created(self) -> datetime.datetime | None: @@ -262,9 +282,13 @@ def created(self) -> datetime.datetime | None: ------- datetime.datetime | None """ - _created: str | None = self._get_attribute("created") + _created: str | None = typing.cast("str | None", self._get_attribute("created")) return ( - datetime.datetime.strptime(_created, DATETIME_FORMAT) if _created else None + datetime.datetime.strptime(_created, DATETIME_FORMAT).replace( + tzinfo=datetime.UTC + ) + if _created + else None ) @property @@ -276,7 +300,7 @@ def uploaded(self) -> bool: ------- bool """ - return self._get_attribute("uploaded") + return typing.cast("bool", self._get_attribute("uploaded")) @uploaded.setter @write_only @@ -287,16 +311,16 @@ def uploaded(self, is_uploaded: bool) -> None: @property def download_url(self) -> URL | None: - """Retrieve the URL for downloading this artifact + """Retrieve the URL for downloading this artifact. Returns ------- simvue.api.url.URL | None """ - return self._get_attribute("url") + return typing.cast("URL", self._get_attribute("url")) @property - def runs(self) -> typing.Generator[str]: + def runs(self) -> Generator[str]: """Retrieve all runs for which this artifact is related. Yields @@ -322,21 +346,25 @@ def get_category(self, run_id: str) -> Category: URL(self._user_config.server.url) / f"runs/{run_id}/artifacts/{self._identifier}" ) - _response = sv_get(url=_run_url, header=self._headers) + _response = sv_get(url=f"{_run_url}", headers=self._headers) _json_response = get_json_from_response( response=_response, expected_status=[http.HTTPStatus.OK, http.HTTPStatus.NOT_FOUND], - scenario=f"Retrieval of category for artifact '{self._identifier}' with respect to run '{run_id}'", + scenario=( + f"Retrieval of category for artifact '{self._identifier}' " + f"with respect to run '{run_id}'" + ), ) + _json_response = typing.cast("dict[str, object]", _json_response) if _response.status_code == http.HTTPStatus.NOT_FOUND: raise ObjectNotFoundError( - self._label, self._identifier, extra=f"for run '{run_id}'" + self._label, self._identifier or "unknown", extra=f"for run '{run_id}'" ) - return _json_response["category"] + return typing.cast("Category", _json_response["category"]) @pydantic.validate_call - def download_content(self) -> typing.Generator[bytes]: + def download_content(self) -> Generator[bytes]: """Stream artifact content. Yields @@ -349,14 +377,18 @@ def download_content(self) -> typing.Generator[bytes]: Generator[bytes, None, None] """ if not self.download_url: - raise ValueError( - f"Could not retrieve URL for artifact '{self._identifier}'" - ) + _out_msg: str = f"Could not retrieve URL for artifact '{self._identifier}'" + raise ValueError(_out_msg) - _timeout = BASE_TIMEOUT + DOWNLOAD_TIMEOUT_PER_MB * self.size / 1024 / 1024 + _timeout: int = ( + BASE_TIMEOUT + DOWNLOAD_TIMEOUT_PER_MB * self.size // 1024 // 1024 + ) self._logger.debug( - f"Will wait {_timeout:.0f}s for download of file {self.name} of size {self.size}B" + "Will wait %.0fs for download of file %s of size %dB", + _timeout, + self.name, + self.size, ) _response = sv_get( @@ -365,7 +397,7 @@ def download_content(self) -> typing.Generator[bytes]: headers=None, ) - get_json_from_response( + _ = get_json_from_response( response=_response, allow_parse_failure=True, expected_status=[http.HTTPStatus.OK], diff --git a/simvue/api/objects/artifact/fetch.py b/simvue/api/objects/artifact/fetch.py index 58c1011a..051a2b79 100644 --- a/simvue/api/objects/artifact/fetch.py +++ b/simvue/api/objects/artifact/fetch.py @@ -1,3 +1,8 @@ +"""Artifact retrieval. + +Defines methods for retrieving artifacts from the Simvue server. +""" + import http import json import typing @@ -11,6 +16,7 @@ from simvue.api.request import get as sv_get from simvue.api.request import get_json_from_response from simvue.api.url import URL +from simvue.config.user import SimvueConfiguration from simvue.exception import ObjectNotFoundError from .file import FileArtifact @@ -25,14 +31,15 @@ def check_column(cls, column: str) -> str: if column and ( column not in ("name", "created") and not column.startswith("metadata.") ): - raise ValueError(f"Invalid sort column for artifacts '{column}'") + _out_msg: str = f"Invalid sort column for artifacts '{column}'" + raise ValueError(_out_msg) return column class Artifact: - """Generic Simvue artifact retrieval class""" + """Generic Simvue artifact retrieval class.""" - def __init__(self, identifier: str | None = None, *args, **kwargs) -> None: + def __init__(self, identifier: str | None = None, **kwargs: object) -> None: """Initialise an instance of generic artifact retriever. Parameters @@ -40,28 +47,29 @@ def __init__(self, identifier: str | None = None, *args, **kwargs) -> None: identifier : str identifier of artifact object to retrieve """ - super().__init__(identifier=identifier, *args, **kwargs) - def __new__(cls, identifier: str | None = None, **kwargs): - """Retrieve an object representing an Artifact by id""" - _artifact_pre = ArtifactBase(identifier=identifier, **kwargs) + def __new__( + cls, identifier: str | None = None, **kwargs: object + ) -> FileArtifact | ObjectArtifact: + """Retrieve an object representing an Artifact by id.""" + _artifact_pre = ArtifactBase(identifier=identifier, **kwargs) # pyright: ignore[reportArgumentType] if _artifact_pre.original_path: - return FileArtifact(identifier=identifier, **kwargs) - return ObjectArtifact(identifier=identifier, **kwargs) + return FileArtifact(identifier=identifier, **kwargs) # pyright: ignore[reportArgumentType] + return ObjectArtifact(identifier=identifier, **kwargs) # pyright: ignore[reportArgumentType] @classmethod def from_run( cls, run_id: str, category: typing.Literal["input", "output", "code"] | None = None, - **kwargs, - ) -> typing.Generator[tuple[str, FileArtifact | ObjectArtifact]]: + **kwargs: object, + ) -> Generator[tuple[str | None, FileArtifact | ObjectArtifact]]: """Return artifacts associated with a given run. Parameters ---------- run_id : str - The ID of the run to retriece artifacts from + The ID of the run to retrieve artifacts from category : Literal['input', 'output', 'code'] | None category of artifacts to return, if None, do not filter * input - this file is an input file. @@ -75,7 +83,7 @@ def from_run( Yields ------ - Iterator[typing.Generator[tuple[str, FileArtifact | ObjectArtifact], None, None]] + Iterator[Generator[tuple[str, FileArtifact | ObjectArtifact], None, None]] identifier for artifact the artifact itself as a class instance @@ -84,10 +92,13 @@ def from_run( ObjectNotFoundError Raised if artifacts could not be found for that run """ - _temp = ArtifactBase(**kwargs) - _url = URL(_temp._user_config.server.url) / f"runs/{run_id}/artifacts" + _config: SimvueConfiguration = SimvueConfiguration.fetch() + _obj_label: str = ArtifactBase(_local=True, **kwargs).label # pyright: ignore[reportArgumentType] + _url = URL(f"{_config.server.url}") / f"runs/{run_id}/artifacts" _response = sv_get( - url=f"{_url}", params={"category": category}, headers=_temp._headers + url=f"{_url}", + params={"category": category}, + headers=_config.headers, # pyright: ignore[reportUnknownArgumentType] ) _json_response = get_json_from_response( expected_type=list, @@ -96,13 +107,17 @@ def from_run( scenario=f"Retrieval of artifacts for run '{run_id}'", ) + _json_response = typing.cast("list[dict[str, object]]", _json_response) + if _response.status_code == http.HTTPStatus.NOT_FOUND or not _json_response: raise ObjectNotFoundError( - _temp._label, category, extra=f"for run '{run_id}'" + _obj_label, + category or "unknown", + extra=f"for run '{run_id}'", ) for _entry in _json_response: - _id = _entry.pop("id") + _id = typing.cast("str | None", _entry.pop("id")) yield ( _id, Artifact(_local=True, _read_only=True, identifier=_id, **_entry), @@ -110,7 +125,7 @@ def from_run( @classmethod def from_name( - cls, run_id: str, name: str, **kwargs + cls, run_id: str, name: str, **kwargs: object ) -> FileArtifact | ObjectArtifact | None: """Retrieve an artifact by name. @@ -126,24 +141,35 @@ def from_name( FileArtifact | ObjectArtifact | None the artifact if found """ - _temp = ArtifactBase(**kwargs) - _url = URL(_temp._user_config.server.url) / f"runs/{run_id}/artifacts" - _response = sv_get(url=f"{_url}", params={"name": name}, headers=_temp._headers) + _config: SimvueConfiguration = SimvueConfiguration.fetch() + _obj_label: str = ArtifactBase(_local=True, **kwargs).label # pyright: ignore[reportArgumentType] + _url = URL(f"{_config.server.url}") / f"runs/{run_id}/artifacts" + _response = sv_get( + url=f"{_url}", + params={"name": name}, + headers=_config.headers, # pyright: ignore[reportUnknownArgumentType] + ) _json_response = get_json_from_response( expected_type=list, response=_response, expected_status=[http.HTTPStatus.OK, http.HTTPStatus.NOT_FOUND], scenario=f"Retrieval of artifact '{name}' for run '{run_id}'", ) + _json_response = typing.cast("list[dict[str, object]]", _json_response) if _response.status_code == http.HTTPStatus.NOT_FOUND or not _json_response: - raise ObjectNotFoundError(_temp._label, name, extra=f"for run '{run_id}'") + raise ObjectNotFoundError( + _obj_label, + name, + extra=f"for run '{run_id}'", + ) if (_n_res := len(_json_response)) > 1: - raise RuntimeError( + _out_msg: str = ( f"Expected single result for artifact '{name}' for run '{run_id}'" f" but got {_n_res}" ) + raise RuntimeError(_out_msg) _first_result: dict[str, typing.Any] = _json_response[0] _artifact_id: str = _first_result.pop("id") @@ -163,9 +189,9 @@ def get( count: int | None = None, offset: int | None = None, sorting: list[ArtifactSort] | None = None, - **kwargs, + **kwargs: object, ) -> Generator[tuple[str, FileArtifact | ObjectArtifact]]: - """Returns artifacts associated with the current user. + """Return artifacts associated with the current user. Parameters ---------- @@ -183,16 +209,17 @@ def get( the artifact itself as a class instance """ _class_instance = ArtifactBase(_local=True, _read_only=True) - _url = f"{_class_instance._base_url}" - _params = {"start": offset, "count": count} + _config: SimvueConfiguration = SimvueConfiguration.fetch() + _url = f"{_class_instance.base_url}" + _params: dict[str, int | None | str] = {"start": offset, "count": count} if sorting: _params["sorting"] = json.dumps([sort.to_params() for sort in sorting]) _response = sv_get( _url, - headers=_class_instance._headers, - params=_params | kwargs, + headers=_config.headers, # pyright: ignore[reportUnknownArgumentType] + params=_params | kwargs, # pyright: ignore[reportArgumentType] ) _label: str = _class_instance.__class__.__name__.lower() _label = _label.replace("base", "") @@ -201,12 +228,17 @@ def get( expected_status=[http.HTTPStatus.OK], scenario=f"Retrieval of {_label}s", ) + _json_response = typing.cast("dict[str, object]", _json_response) + _data = typing.cast( + "list[dict[str, object]] | None", _json_response.get("data") + ) - if (_data := _json_response.get("data")) is None: - raise RuntimeError(f"Expected key 'data' for retrieval of {_label}s") + if _data is None: + _out_msg: str = f"Expected key 'data' for retrieval of {_label}s" + raise RuntimeError(_out_msg) for _entry in _data: - _id = _entry.pop("id") + _id = typing.cast("str", _entry.pop("id")) yield ( _id, Artifact(_local=True, _read_only=True, identifier=_id, **_entry), diff --git a/simvue/api/objects/artifact/file.py b/simvue/api/objects/artifact/file.py index 664dadde..140d37ab 100644 --- a/simvue/api/objects/artifact/file.py +++ b/simvue/api/objects/artifact/file.py @@ -1,23 +1,36 @@ -from .base import ArtifactBase +"""File type artifact handling.""" -import typing -import pydantic import os import pathlib +import typing + +import pydantic + from simvue.models import NAME_REGEX -from simvue.utilities import get_mimetype_for_file, get_mimetypes, calculate_sha256 +from simvue.utilities import calculate_sha256, get_mimetype_for_file, get_mimetypes + +from .base import ArtifactBase try: from typing import Self except ImportError: - from typing_extensions import Self + from typing_extensions import Self # noqa: UP035 + +try: + from typing import override +except ImportError: + from typing_extensions import override # noqa: UP035 class FileArtifact(ArtifactBase): """File based artifact modification and creation class.""" def __init__( - self, identifier: str | None = None, _read_only: bool = True, **kwargs + self, + identifier: str | None = None, + *, + _read_only: bool = True, + **kwargs: object, ) -> None: """Initialise a new file artifact connection. @@ -29,6 +42,7 @@ def __init__( super().__init__(identifier=identifier, _read_only=_read_only, **kwargs) @classmethod + @override def new( cls, *, @@ -39,9 +53,9 @@ def new( metadata: dict[str, typing.Any] | None, upload_timeout: int | None = None, offline: bool = False, - **kwargs, + **kwargs: object, ) -> Self: - """Create a new artifact either locally or on the server + """Create a new artifact either locally or on the server. Note all arguments are keyword arguments @@ -66,18 +80,20 @@ def new( _mime_type = mime_type or get_mimetype_for_file(file_path) if _mime_type not in get_mimetypes(): - raise ValueError(f"Invalid MIME type '{mime_type}' specified") + _out_msg: str = f"Invalid MIME type '{mime_type}' specified" + raise ValueError(_out_msg) - if _file_orig_path := kwargs.pop("original_path", None): - _file_size = kwargs.pop("size") - _file_checksum = kwargs.pop("checksum") + _file_orig_path = typing.cast("str | None", kwargs.pop("original_path", None)) + if _file_orig_path: + _file_size = typing.cast("int", kwargs.pop("size")) + _file_checksum = typing.cast("str", kwargs.pop("checksum")) else: file_path = pathlib.Path(file_path) _file_size = file_path.stat().st_size _file_orig_path = file_path.expanduser().absolute() _file_checksum = calculate_sha256(f"{file_path}", is_file=True) - _artifact = FileArtifact( + _artifact = cls( name=name, storage=storage, original_path=os.path.expandvars(_file_orig_path), @@ -87,22 +103,27 @@ def new( _offline=offline, _read_only=False, metadata=metadata, - **kwargs, + **kwargs, # pyright: ignore[reportArgumentType] ) _artifact._staging["file_path"] = str(file_path) if offline: _artifact._init_data = {} else: - _artifact._init_data = _artifact._post_single(**_artifact._staging) + _artifact._init_data = typing.cast( + "dict[str, dict[str, object]]", + _artifact._post_single(**_artifact._staging), + ) _artifact._staging["url"] = _artifact._init_data["url"] - _artifact._init_data["runs"] = kwargs.get("runs") or {} + _artifact._init_data["runs"] = typing.cast( + "dict[str, object]", kwargs.get("runs", {}) + ) if offline: return _artifact - with open(_file_orig_path, "rb") as out_f: + with pathlib.Path(_file_orig_path).open("rb") as out_f: _artifact._upload(file=out_f, timeout=upload_timeout, file_size=_file_size) return _artifact diff --git a/simvue/api/objects/base.py b/simvue/api/objects/base.py index 2fb33414..7360be83 100644 --- a/simvue/api/objects/base.py +++ b/simvue/api/objects/base.py @@ -10,7 +10,6 @@ import inspect import json import logging -import pathlib import types import typing import uuid @@ -19,6 +18,7 @@ import msgpack import pydantic +from simvue.api.request import delete as sv_delete from simvue.api.request import ( get as sv_get, ) @@ -43,7 +43,15 @@ try: from typing import Self except ImportError: - from typing import Self + from typing_extensions import Self # noqa: UP035 + +try: + from typing import override +except ImportError: + from typing_extensions import override # noqa: UP035 + +if typing.TYPE_CHECKING: + import pathlib # Need to use this inside of Generator typing to fix bug # present in Python 3.10 - see issue #745 @@ -203,13 +211,15 @@ def to_params(self) -> dict[str, str | bool]: class VisibilityBatchArgs(pydantic.BaseModel): + """Class defining visibility arguments for batch upload.""" + tenant: bool | None = None user: list[str] | None = None public: bool | None = None class ObjectBatchArgs(pydantic.BaseModel): - pass + """Base class for object batch upload arguments.""" class SimvueObject(abc.ABC): @@ -244,7 +254,7 @@ def __init__( self._properties: list[str] = [ name for name, member in inspect.getmembers(self.__class__) - if isinstance(member, property) + if isinstance(member, property) and name not in ("label", "base_url") ] self._offline: bool = _offline or ( identifier is not None and identifier.startswith("offline_") @@ -428,13 +438,14 @@ def _get_visibility(self) -> dict[str, bool | list[str]]: @classmethod @abc.abstractmethod - def new(cls, **_) -> Self: + def new(cls, **_: object) -> Self: """Define new instance of this object.""" @classmethod def batch_create( cls, obj_args: ObjectBatchArgs, visibility: VisibilityBatchArgs ) -> Generator[str]: + """Upload a set of objects.""" _, __ = obj_args, visibility raise NotImplementedError @@ -458,7 +469,7 @@ def ids( """ _class_instance = cls(_read_only=True, _local=True) _count: int = 0 - for response in cls._get_all_objects(offset, count=count, **kwargs): + for response in cls._get_all_objects(offset, count=count, **kwargs): # pyright: ignore[reportArgumentType] _data = typing.cast("list[dict[str, object]] | None", response.get("data")) if _data is None: _out_msg: str = ( @@ -503,7 +514,7 @@ def get( _class_instance = cls(_read_only=True, _local=True) _count: int = 0 - for _response in cls._get_all_objects(offset, count=count, **kwargs): + for _response in cls._get_all_objects(offset, count=count, **kwargs): # pyright: ignore[reportArgumentType] if count and _count > count: return @@ -535,7 +546,7 @@ def count(cls, **kwargs: object) -> int: """ _class_instance = cls(_read_only=True) _count_total: int = 0 - for _data in cls._get_all_objects(count=None, offset=None, **kwargs): + for _data in cls._get_all_objects(count=None, offset=None, **kwargs): # pyright: ignore[reportArgumentType] _count = typing.cast("int | None", _data.get("count")) if not _count: _out_msg: str = ( @@ -553,7 +564,7 @@ def _get_all_objects( count: int | None, endpoint: str | None = None, expected_type: type = dict, - **kwargs, + **kwargs: object, ) -> Generator[dict[str, object]]: _class_instance = cls(_read_only=True) @@ -562,7 +573,7 @@ def _get_all_objects( _url = ( f"{_class_instance._user_config.server.url}/{endpoint}" if endpoint - else f"{_class_instance._base_url}" + else f"{_class_instance.base_url}" ) _label = _class_instance.__class__.__name__.lower() @@ -583,11 +594,11 @@ def _get_all_objects( ) if expected_type is dict: - yield _generator + yield typing.cast("dict[str, object]", _generator) else: - yield from _generator + yield from typing.cast("list[dict[str, object]]", _generator) - def read_only(self, is_read_only: bool) -> None: + def read_only(self, is_read_only: bool) -> None: # noqa: FBT001 """Set whether this object is in read only state. Parameters @@ -626,7 +637,7 @@ def commit(self) -> list[dict[str, str]] | dict[str, str] | None: if not self._identifier or self._identifier.startswith("offline_"): # If batch upload send as list, else send as dictionary of params _batch_commit = typing.cast( - "list[dict[str, object]] | None", self._staging.get("batch") + "list[ObjectBatchArgs] | None", self._staging.get("batch") ) if _batch_commit is not None: self._logger.debug( @@ -634,9 +645,7 @@ def commit(self) -> list[dict[str, str]] | dict[str, str] | None: len(_batch_commit), self._label, ) - _response = typing.cast( - "list[dict[str, str]]", self._post_batch(batch_data=_batch_commit) - ) + _response = self._post_batch(batch_data=_batch_commit) else: self._logger.debug( "Posting from staged data for %s '%s': %s", @@ -677,7 +686,8 @@ def id(self) -> str | None: return self._identifier @property - def _base_url(self) -> URL: + def base_url(self) -> URL: + """Retrieve the base URL for this object.""" return URL(f"{self._user_config.server.url}") / self._endpoint @property @@ -688,14 +698,14 @@ def url(self) -> URL | None: ------- simvue.api.url.URL | None """ - return None if self._identifier is None else self._base_url / self._identifier + return None if self._identifier is None else self.base_url / self._identifier def _post_batch( self, batch_data: list[ObjectBatchArgs], ) -> list[dict[str, str]]: _response = sv_post( - url=f"{self._base_url}", + url=f"{self.base_url}", headers=self._headers | {"Content-Type": "application/msgpack"}, params=self._params, data=batch_data, @@ -703,9 +713,11 @@ def _post_batch( ) if _response.status_code == http.HTTPStatus.FORBIDDEN: - raise RuntimeError( - f"Forbidden: You do not have permission to create object of type '{self._label}'" + _out_msg: str = ( + "Forbidden: You do not have permission to create object " + f"of type '{self._label}'" ) + raise RuntimeError(_out_msg) _json_response = get_json_from_response( response=_response, @@ -714,26 +726,31 @@ def _post_batch( expected_type=list, ) - if not len(batch_data) == (_n_created := len(_json_response)): - raise RuntimeError( - f"Expected {len(batch_data)} to be created, but only {_n_created} found." + if len(batch_data) != (_n_created := len(_json_response)): + _out_msg = ( + f"Expected {len(batch_data)} to be created, " + f"but only {_n_created} found." ) + raise RuntimeError(_out_msg) - self._logger.debug(f"successfully created {_n_created} {self._label}s") + self._logger.debug("Successfully created %s %ss", _n_created, self._label) - return _json_response + return typing.cast("list[dict[str, str]]", _json_response) def _post_single( - self, *, is_json: bool = True, data: list | dict | None = None, **kwargs + self, + *, + is_json: bool = True, + data: list[dict[str, object]] | dict[str, object] | None = None, + **kwargs: object, ) -> dict[str, typing.Any] | list[dict[str, typing.Any]]: - if not is_json: - kwargs = msgpack.packb(data or kwargs, use_bin_type=True) + _data = kwargs if is_json else msgpack.packb(data or kwargs, use_bin_type=True) _response = sv_post( - url=f"{self._base_url}", + url=f"{self.base_url}", headers=self._headers | {"Content-Type": "application/msgpack"}, params=self._params, - data=data or kwargs, + data=_data or kwargs, is_json=is_json, ) @@ -838,7 +855,9 @@ def _get( raise RuntimeError(_out_msg) _response = sv_get( - url=f"{url or self.url}", headers=self._headers, params=kwargs + url=f"{url or self.url}", + headers=self._headers, + params=kwargs, # pyright: ignore[reportArgumentType] ) if _response.status_code == http.HTTPStatus.NOT_FOUND: @@ -907,12 +926,17 @@ def staged(self) -> dict[str, typing.Any] | None: """ return self._staging or None - @typing.override + @override def __str__(self) -> str: """Represent Simvue object as string.""" return f"{self.__class__.__name__}({self.id=})" - @typing.override + @property + def label(self) -> str: + """Return label for this object type.""" + return self._label + + @override def __repr__(self) -> str: """Represent Simvue object as Python repr format.""" _out_str = f"{self.__class__.__module__}.{self.__class__.__qualname__}(" diff --git a/simvue/api/url.py b/simvue/api/url.py index 92d6a8fd..9c52f6c1 100644 --- a/simvue/api/url.py +++ b/simvue/api/url.py @@ -1,6 +1,4 @@ -""" -URL Library -=========== +"""URL Library. Module contains classes for easier handling of URLs. @@ -9,9 +7,9 @@ try: from typing import Self except ImportError: - from typing_extensions import Self -import urllib.parse + from typing import Self import copy +import urllib.parse import pydantic @@ -20,19 +18,20 @@ class URL: """URL class for ease of construction and use of server endpoints.""" @pydantic.validate_call - def __init__(self, url: str) -> None: - """Initialise a url from string form""" - url = url[:-1] if url.endswith("/") else url - - _url = urllib.parse.urlparse(url) - self._scheme: str = _url.scheme - self._path: str = _url.path - self._host: str | None = _url.hostname - self._port: int | None = _url.port - self._fragment: str = _url.fragment + def __init__(self, url: str | pydantic.AnyHttpUrl) -> None: + """Initialise a url from string form.""" + _url: str = f"{url}" + _url = _url.removesuffix("/") + + _parsed_url = urllib.parse.urlparse(_url) + self._scheme: str = _parsed_url.scheme + self._path: str = _parsed_url.path + self._host: str | None = _parsed_url.hostname + self._port: int | None = _parsed_url.port + self._fragment: str = _parsed_url.fragment def __truediv__(self, other: str) -> Self: - """Define URL extension through use of '/'""" + """Define URL extension through use of '/'.""" _new = copy.deepcopy(self) _new /= other return _new @@ -45,8 +44,8 @@ def __repr__(self) -> str: @pydantic.validate_call def __itruediv__(self, other: str) -> Self: """Define URL extension through use of '/'""" - other = other[1:] if other.startswith("/") else other - other = other[:-1] if other.endswith("/") else other + other = other.removeprefix("/") + other = other.removesuffix("/") self._path = f"{self._path}/{other}" if other else self._path return self diff --git a/simvue/config/user.py b/simvue/config/user.py index 7e81662e..89dab7d5 100644 --- a/simvue/config/user.py +++ b/simvue/config/user.py @@ -258,3 +258,11 @@ def config_file(cls) -> pathlib.Path: raise FileNotFoundError("Failed to find Simvue configuration file") return _config_file + + @property + def headers(self) -> dict[str, str]: + """Return online API call headers.""" + return { + "Authorization": f"Bearer {self.server.token.get_secret_value()}", + "Accept-Encoding": "gzip", + } diff --git a/simvue/run.py b/simvue/run.py index 6c855524..40ed7620 100644 --- a/simvue/run.py +++ b/simvue/run.py @@ -7,28 +7,29 @@ """ import contextlib +import functools import logging -import pathlib import mimetypes +import multiprocessing import multiprocessing.synchronize -import threading -import humanfriendly import os -import multiprocessing -import pydantic +import pathlib +import platform import re import sys -import traceback as tb +import threading import time -import functools -import platform +import traceback as tb import typing -import warnings import uuid -import numpy -import randomname +import warnings + import click +import humanfriendly +import numpy import psutil +import pydantic +import randomname from simvue.api.objects.alert.fetch import Alert from simvue.api.objects.folder import Folder @@ -36,38 +37,38 @@ from simvue.exception import ObjectNotFoundError, SimvueRunError from simvue.utilities import prettify_pydantic - +from .api.objects import ( + Events, + EventsAlert, + FileArtifact, + Grid, + Metrics, + MetricsRangeAlert, + MetricsThresholdAlert, + ObjectArtifact, + UserAlert, +) +from .api.objects import ( + Run as RunObject, +) from .config.user import SimvueConfiguration - -from .factory.dispatch import Dispatcher +from .eco import CO2Monitor from .executor import Executor, get_current_shell +from .factory.dispatch import Dispatcher +from .metadata import environment, git_info from .metrics import SystemResourceMeasurement from .models import FOLDER_REGEX, NAME_REGEX, MetricKeyString from .system import get_system -from .metadata import git_info, environment -from .eco import CO2Monitor from .utilities import ( + simvue_timestamp, skip_if_failed, validate_timestamp, - simvue_timestamp, -) -from .api.objects import ( - Run as RunObject, - FileArtifact, - ObjectArtifact, - MetricsThresholdAlert, - MetricsRangeAlert, - UserAlert, - EventsAlert, - Events, - Metrics, - Grid, ) try: from typing import Self except ImportError: - from typing_extensions import Self # noqa: F401 + from typing import Self if typing.TYPE_CHECKING: @@ -142,7 +143,6 @@ def __init__( Examples -------- - ```python with simvue.Run() as run: ... @@ -198,12 +198,7 @@ def __init__( else self._user_config.metrics.system_metrics_interval ) self._headers: dict[str, str] = ( - { - "Authorization": f"Bearer {self._user_config.server.token.get_secret_value()}", - "Accept-Encoding": "gzip", - } - if mode != "offline" - else {} + self._user_config.headers if mode != "offline" else {} ) self._sv_obj: RunObject | None = None self._pid: int | None = 0 @@ -222,9 +217,9 @@ def __enter__(self) -> Self: def _handle_exception_throw( self, - exc_type: typing.Type[BaseException] | None, + exc_type: type[BaseException] | None, value: BaseException, - traceback: typing.Type[BaseException] | BaseException | None, + traceback: type[BaseException] | BaseException | None, ) -> None: _exception_thrown: str | None = exc_type.__name__ if exc_type else None _is_running: bool = self._status == "running" @@ -260,9 +255,9 @@ def _handle_exception_throw( def __exit__( self, - exc_type: typing.Type[BaseException] | None, + exc_type: type[BaseException] | None, value: BaseException, - traceback: typing.Type[BaseException] | BaseException | None, + traceback: type[BaseException] | BaseException | None, ) -> None: logger.debug( "Automatically closing run '%s' in status %s", @@ -290,7 +285,6 @@ def time_stamp(self) -> str: @property def processes(self) -> list[psutil.Process]: """Create an array containing a list of processes""" - process_list = self._executor.processes if not self._parent_process: @@ -360,7 +354,6 @@ def _get_internal_metrics( new resource metric measure time new emissions metric measure time """ - # In order to get a resource metric reading at t=0 # because there is no previous CPU reading yet we cannot # use the default of None for the interval here, so we measure @@ -468,7 +461,6 @@ def _create_dispatch_callback( The generated callback is assigned to the dispatcher instance and is executed on metrics and events objects held in a buffer. """ - if self._user_config.run.mode == "online" and not self.id: raise RuntimeError("Expected identifier for run") @@ -488,20 +480,19 @@ def _dispatch_callback( events=buffer, ) return _events.commit() - elif category == "metrics_tensor": + if category == "metrics_tensor": _grid_metrics = GridMetrics.new( run=self.id, data=buffer, offline=self._user_config.run.mode == "offline", ) return _grid_metrics.commit() - else: - _metrics = Metrics.new( - run=self.id, - offline=self._user_config.run.mode == "offline", - metrics=buffer, - ) - return _metrics.commit() + _metrics = Metrics.new( + run=self.id, + offline=self._user_config.run.mode == "offline", + metrics=buffer, + ) + return _metrics.commit() return _dispatch_callback @@ -721,7 +712,7 @@ def init( if name and not re.match(r"^[a-zA-Z0-9\-\_\s\/\.:]+$", name): self._error("specified name is invalid") return False - elif not name and self._user_config.run.mode == "offline": + if not name and self._user_config.run.mode == "offline": name = randomname.get_name() self._status = "running" if running else "created" @@ -804,9 +795,7 @@ def add_process( executable: str | pathlib.Path | None = None, script: pydantic.FilePath | None = None, input_file: pydantic.FilePath | None = None, - completion_callback: typing.Optional[ - typing.Callable[[int, str, str], None] - ] = None, + completion_callback: typing.Callable[[int, str, str], None] | None = None, completion_trigger: multiprocessing.synchronize.Event | None = None, env: dict[str, str] | None = None, cwd: pathlib.Path | None = None, @@ -1095,7 +1084,6 @@ def config( bool if configuration was successful """ - with self._configuration_lock: if suppress_errors is not None: self._suppress_errors = suppress_errors @@ -1572,9 +1560,7 @@ def save_object( self, obj: typing.Any, category: typing.Literal["input", "output", "code"], - name: typing.Optional[ - typing.Annotated[str, pydantic.Field(pattern=NAME_REGEX)] - ] = None, + name: typing.Annotated[str, pydantic.Field(pattern=NAME_REGEX)] | None = None, allow_pickle: bool = False, metadata: dict[str, typing.Any] = None, ) -> bool: @@ -1632,9 +1618,7 @@ def save_file( category: typing.Literal["input", "output", "code"], file_type: str | None = None, preserve_path: bool = False, - name: typing.Optional[ - typing.Annotated[str, pydantic.Field(pattern=NAME_REGEX)] - ] = None, + name: typing.Annotated[str, pydantic.Field(pattern=NAME_REGEX)] | None = None, metadata: dict[str, typing.Any] = None, ) -> bool: """Upload file to the server @@ -1880,7 +1864,7 @@ def close(self) -> bool: """ if self._context_manager_called: self._error("Cannot call close method in context manager.") - return + return None self._executor.wait_for_completion() From 5830cb605bb88c7f03dd65687f09ee31eb49c009 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Zar=C4=99bski?= Date: Tue, 23 Sep 2025 15:06:27 +0100 Subject: [PATCH 05/23] =?UTF-8?q?=F0=9F=9A=A8=20Continue=20linting=20progr?= =?UTF-8?q?ess?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- simvue/api/objects/administrator/tenant.py | 1 + simvue/api/objects/administrator/user.py | 101 +++++++------ simvue/api/objects/alert/__init__.py | 10 +- simvue/api/objects/alert/base.py | 157 +++++++++++++-------- simvue/api/objects/alert/events.py | 104 +++++++++----- simvue/api/objects/base.py | 2 +- 6 files changed, 241 insertions(+), 134 deletions(-) diff --git a/simvue/api/objects/administrator/tenant.py b/simvue/api/objects/administrator/tenant.py index f841afa7..74047597 100644 --- a/simvue/api/objects/administrator/tenant.py +++ b/simvue/api/objects/administrator/tenant.py @@ -37,6 +37,7 @@ def new( max_runs: int = 0, max_data_volume: int = 0, offline: bool = False, + **_: typing.Any, ) -> Self: """Create a new tenant on the Simvue server. diff --git a/simvue/api/objects/administrator/user.py b/simvue/api/objects/administrator/user.py index 34136561..30d2750b 100644 --- a/simvue/api/objects/administrator/user.py +++ b/simvue/api/objects/administrator/user.py @@ -1,18 +1,25 @@ -""" -Simvue Users -============ +"""Simvue Users. Contains a class for remotely connecting to Simvue users, or defining a new user given relevant arguments. """ +import typing +from collections.abc import Generator + import pydantic try: from typing import Self except ImportError: - from typing_extensions import Self + from typing_extensions import Self # noqa: UP035 + +try: + from typing import override +except ImportError: + from typing_extensions import override # noqa: UP035 + from simvue.api.objects.base import SimvueObject, staging_check, write_only @@ -21,6 +28,7 @@ class User(SimvueObject): @classmethod @pydantic.validate_call + @override def new( cls, *, @@ -34,7 +42,7 @@ def new( tenant: str, enabled: bool = True, offline: bool = False, - **_, + **_: typing.Any, ) -> Self: """Create a new user on the Simvue server. @@ -79,7 +87,7 @@ def new( "is_admin": is_admin, "is_enabled": enabled, } - _user = User( + _user = cls( user=_user_info, tenant=tenant, offline=offline, @@ -90,9 +98,14 @@ def new( return _user @classmethod + @override def get( - cls, *, count: int | None = None, offset: int | None = None, **kwargs - ) -> dict[str, "User"]: + cls, + *, + count: pydantic.PositiveInt | None = None, + offset: pydantic.PositiveInt | None = None, + **kwargs: object, + ) -> Generator[tuple[str, Self]]: """Retrieve users from the Simvue server. Parameters @@ -108,117 +121,125 @@ def get( user instance representing user on server """ # Currently no user filters - kwargs.pop("filters", None) + _ = kwargs.pop("filters", None) return super().get(count=count, offset=offset, **kwargs) @property @staging_check def username(self) -> str: - """Retrieve the username for the user""" + """Retrieve the username for the user.""" if self.id and self.id.startswith("offline_"): - return self._get_attribute("user")["username"] - return self._get_attribute("username") + _user_dict = typing.cast("dict[str, str]", self._get_attribute("user")) + return _user_dict["username"] + return typing.cast("str", self._get_attribute("username")) @username.setter @write_only @pydantic.validate_call def username(self, username: str) -> None: - """Set the username for the user""" + """Set the username for the user.""" self._staging["username"] = username @property @staging_check def fullname(self) -> str: - """Retrieve the full name for the user""" + """Retrieve the full name for the user.""" if self.id and self.id.startswith("offline_"): - return self._get_attribute("user")["fullname"] - return self._get_attribute("fullname") + _user_dict = typing.cast("dict[str, str]", self._get_attribute("user")) + return _user_dict["fullname"] + return typing.cast("str", self._get_attribute("fullname")) @fullname.setter @write_only @pydantic.validate_call def fullname(self, fullname: str) -> None: - """Set the full name for the user""" + """Set the full name for the user.""" self._staging["fullname"] = fullname @property @staging_check def is_manager(self) -> bool: - """Retrieve if the user has manager privileges""" + """Retrieve if the user has manager privileges.""" if self.id and self.id.startswith("offline_"): - return self._get_attribute("user")["is_manager"] - return self._get_attribute("is_manager") + _user_dict = typing.cast("dict[str, bool]", self._get_attribute("user")) + return _user_dict["is_manager"] + return typing.cast("bool", self._get_attribute("is_manager")) @is_manager.setter @write_only @pydantic.validate_call def is_manager(self, is_manager: bool) -> None: - """Set if the user has manager privileges""" + """Set if the user has manager privileges.""" self._staging["is_manager"] = is_manager @property @staging_check def is_admin(self) -> bool: - """Retrieve if the user has admin privileges""" + """Retrieve if the user has admin privileges.""" if self.id and self.id.startswith("offline_"): - return self._get_attribute("user")["is_admin"] - return self._get_attribute("is_admin") + _user_dict = typing.cast("dict[str, bool]", self._get_attribute("user")) + return _user_dict["is_admin"] + return typing.cast("bool", self._get_attribute("is_admin")) @is_admin.setter @write_only @pydantic.validate_call def is_admin(self, is_admin: bool) -> None: - """Set if the user has admin privileges""" + """Set if the user has admin privileges.""" self._staging["is_admin"] = is_admin @property def deleted(self) -> bool: - """Retrieve if the user is pending deletion""" + """Retrieve if the user is pending deletion.""" if self.id and self.id.startswith("offline_"): - return self._get_attribute("user")["is_deleted"] - return self._get_attribute("is_deleted") + _user_dict = typing.cast("dict[str, bool]", self._get_attribute("user")) + return _user_dict["is_deleted"] + return typing.cast("bool", self._get_attribute("is_deleted")) @property @staging_check def is_readonly(self) -> bool: - """Retrieve if the user has read-only access""" + """Retrieve if the user has read-only access.""" if self.id and self.id.startswith("offline_"): - return self._get_attribute("user")["is_readonly"] - return self._get_attribute("is_readonly") + _user_dict = typing.cast("dict[str, bool]", self._get_attribute("user")) + return _user_dict["is_readonly"] + return typing.cast("bool", self._get_attribute("is_readonly")) @is_readonly.setter @write_only @pydantic.validate_call def is_readonly(self, is_readonly: bool) -> None: - """Set if the user has read-only access""" + """Set if the user has read-only access.""" self._staging["is_readonly"] = is_readonly @property @staging_check def enabled(self) -> bool: - """Retrieve if the user is enabled""" + """Retrieve if the user is enabled.""" if self.id and self.id.startswith("offline_"): - return self._get_attribute("user")["is_enabled"] - return self._get_attribute("is_enabled") + _user_dict = typing.cast("dict[str, bool]", self._get_attribute("user")) + return _user_dict["is_enabled"] + return typing.cast("bool", self._get_attribute("is_enabled")) @enabled.setter @write_only @pydantic.validate_call def enabled(self, is_enabled: bool) -> None: - """Set if the user is enabled""" + """Set if the user is enabled.""" self._staging["is_enabled"] = is_enabled @property @staging_check def email(self) -> str: - """Retrieve the user email""" + """Retrieve the user email.""" if self.id and self.id.startswith("offline_"): - return self._get_attribute("user")["email"] - return self._get_attribute("email") + _user_dict = typing.cast("dict[str, str]", self._get_attribute("user")) + return _user_dict["email"] + return typing.cast("str", self._get_attribute("email")) @email.setter @write_only @pydantic.validate_call def email(self, email: str) -> None: - """Set the user email""" + """Set the user email.""" self._staging["email"] = email diff --git a/simvue/api/objects/alert/__init__.py b/simvue/api/objects/alert/__init__.py index b9be7d66..27bb7b13 100644 --- a/simvue/api/objects/alert/__init__.py +++ b/simvue/api/objects/alert/__init__.py @@ -1,6 +1,4 @@ -""" -Simvue Alerts -============= +"""Simvue Alerts. Creation and management of Alerts on the Simvue server, the alerts are split into sub-categories to ensure correct arguments @@ -8,15 +6,15 @@ """ -from .fetch import Alert -from .metrics import MetricsThresholdAlert, MetricsRangeAlert from .events import EventsAlert +from .fetch import Alert +from .metrics import MetricsRangeAlert, MetricsThresholdAlert from .user import UserAlert __all__ = [ "Alert", + "EventsAlert", "MetricsRangeAlert", "MetricsThresholdAlert", - "EventsAlert", "UserAlert", ] diff --git a/simvue/api/objects/alert/base.py b/simvue/api/objects/alert/base.py index 4799cd1c..2a62a677 100644 --- a/simvue/api/objects/alert/base.py +++ b/simvue/api/objects/alert/base.py @@ -1,53 +1,84 @@ -""" -Alert Object Base -================= +"""Alert Object Base. Contains general definitions for Simvue Alert objects. """ -import http -import pydantic +import abc import datetime +import http import typing + +import pydantic + +try: + from typing import Self +except ImportError: + from typing_extensions import Self # noqa: UP035 + +try: + from typing import override +except ImportError: + from typing_extensions import override # noqa: UP035 + from simvue.api.objects.base import SimvueObject, staging_check, write_only -from simvue.api.request import get as sv_get, get_json_from_response -from simvue.api.url import URL -from simvue.models import NAME_REGEX, DATETIME_FORMAT +from simvue.api.request import get as sv_get +from simvue.api.request import get_json_from_response +from simvue.models import DATETIME_FORMAT, NAME_REGEX +if typing.TYPE_CHECKING: + from simvue.api.url import URL -class AlertBase(SimvueObject): - """Class for interfacing with Simvue alerts + +class AlertBase(SimvueObject, abc.ABC): + """Class for interfacing with Simvue alerts. Contains properties common to all alert types. """ @classmethod - def new(cls, **kwargs): - """Create a new alert""" - pass - - def __init__(self, identifier: str | None = None, **kwargs) -> None: - """Retrieve an alert from the Simvue server by identifier""" - self._label = "alert" - super().__init__(identifier=identifier, **kwargs) + @override + @abc.abstractmethod + def new(cls, **_: typing.Any) -> Self: + """Create a new alert.""" + + def __init__( + self, + identifier: str | None = None, + *, + _offline: bool = False, + _local: bool = False, + _user_agent: str | None = None, + _read_only: bool = True, + **kwargs: object, + ) -> None: + """Retrieve an alert from the Simvue server by identifier.""" + self._label: str = "alert" + super().__init__( + identifier=identifier, + _offline=_offline, + _local=_local, + _user_agent=_user_agent, + _read_only=_read_only, + **kwargs, + ) def compare(self, other: "AlertBase") -> bool: - """Compare this alert to another""" + """Compare this alert to another.""" return type(self) is type(other) and self.name == other.name @staging_check - def get_alert(self) -> dict[str, typing.Any]: - """Retrieve alert definition""" + def get_alert(self) -> dict[str, object]: + """Retrieve alert definition.""" try: - return self._get_attribute("alert") + return typing.cast("dict[str, object]", self._get_attribute("alert")) except AttributeError: return {} @property def name(self) -> str: - """Retrieve alert name""" - return self._get_attribute("name") + """Retrieve alert name.""" + return typing.cast("str", self._get_attribute("name")) @name.setter @write_only @@ -55,107 +86,119 @@ def name(self) -> str: def name( self, name: typing.Annotated[str, pydantic.Field(pattern=NAME_REGEX)] ) -> None: - """Set alert name""" + """Set alert name.""" self._staging["name"] = name @property @staging_check def description(self) -> str | None: - """Retrieve alert description""" - return self._get_attribute("description") + """Retrieve alert description.""" + return typing.cast("str", self._get_attribute("description")) @description.setter @write_only @pydantic.validate_call def description(self, description: str | None) -> None: - """Set alert description""" + """Set alert description.""" self._staging["description"] = description @property def run_tags(self) -> list[str]: - """Retrieve automatically assigned tags from runs""" - return self._get_attribute("run_tags") + """Retrieve automatically assigned tags from runs.""" + return typing.cast("list[str]", self._get_attribute("run_tags")) @property @staging_check def auto(self) -> bool: - """Retrieve if alert has run tag auto-assign""" - return self._get_attribute("auto") + """Retrieve if alert has run tag auto-assign.""" + return typing.cast("bool", self._get_attribute("auto")) @auto.setter @write_only @pydantic.validate_call def auto(self, auto: bool) -> None: - """Set alert to use run tag auto-assign""" + """Set alert to use run tag auto-assign.""" self._staging["auto"] = auto @property @staging_check def notification(self) -> typing.Literal["none", "email"]: - """Retrieve alert notification setting""" - return self._get_attribute("notification") + """Retrieve alert notification setting.""" + return typing.cast( + "typing.Literal['none', 'email']", self._get_attribute("notification") + ) @notification.setter @write_only @pydantic.validate_call def notification(self, notification: typing.Literal["none", "email"]) -> None: - """Configure alert notification setting""" + """Configure alert notification setting.""" self._staging["notification"] = notification @property def source(self) -> typing.Literal["events", "metrics", "user"]: - """Retrieve alert source""" - return self._get_attribute("source") + """Retrieve alert source.""" + return typing.cast( + "typing.Literal['events', 'metrics', 'user']", self._get_attribute("source") + ) @property @staging_check def enabled(self) -> bool: - """Retrieve if alert is enabled""" - return self._get_attribute("enabled") + """Retrieve if alert is enabled.""" + return typing.cast("bool", self._get_attribute("enabled")) @enabled.setter @write_only @pydantic.validate_call - def enabled(self, enabled: str) -> None: - """Enable/disable alert""" + def enabled(self, enabled: bool) -> None: + """Enable/disable alert.""" self._staging["enabled"] = enabled @property @staging_check def abort(self) -> bool: - """Retrieve if alert can abort simulations""" - return self._get_attribute("abort") + """Retrieve if alert can abort simulations.""" + return typing.cast("bool", self._get_attribute("abort")) @property @staging_check def delay(self) -> int: - """Retrieve delay value for this alert""" - return self._get_attribute("delay") + """Retrieve delay value for this alert.""" + return typing.cast("int", self._get_attribute("delay")) @property def created(self) -> datetime.datetime | None: - """Retrieve created datetime for the alert""" - _created: str | None = self._get_attribute("created") + """Retrieve created datetime for the alert.""" + _created = typing.cast("str | None", self._get_attribute("created")) return ( - datetime.datetime.strptime(_created, DATETIME_FORMAT) if _created else None + datetime.datetime.strptime(_created, DATETIME_FORMAT).replace( + tzinfo=datetime.UTC + ) + if _created + else None ) @abort.setter @write_only @pydantic.validate_call def abort(self, abort: bool) -> None: - """Configure alert to trigger aborts""" + """Configure alert to trigger aborts.""" self._staging["abort"] = abort @pydantic.validate_call - def set_status(self, run_id: str, status: typing.Literal["ok", "critical"]) -> None: - """Set the status of this alert for a given run""" - raise AttributeError( + def set_status(self, *args: object, **kwargs: object) -> None: + """Set the status of this alert for a given run.""" + _ = args, kwargs + _out_msg: str = ( f"Cannot update state for alert of type '{self.__class__.__name__}'" ) + raise AttributeError(_out_msg) def get_status(self, run_id: str) -> typing.Literal["ok", "critical"]: - """Retrieve the status of this alert for a given run""" + """Retrieve the status of this alert for a given run.""" + if not self.url: + raise RuntimeError("Cannot retrieve status, no object URL found.") _url: URL = self.url / f"status/{run_id}" _response = sv_get(url=f"{_url}", headers=self._headers) _json_response = get_json_from_response( @@ -164,4 +207,8 @@ def get_status(self, run_id: str) -> typing.Literal["ok", "critical"]: scenario=f"Retrieving status for alert '{self.id}' in run '{run_id}'", ) - return _json_response.get("status") + _json_response = typing.cast("dict[str, object]", _json_response) + + return typing.cast( + "typing.Literal['ok', 'critical']", _json_response.get("status") + ) diff --git a/simvue/api/objects/alert/events.py b/simvue/api/objects/alert/events.py index 00558ffb..3e9829a3 100644 --- a/simvue/api/objects/alert/events.py +++ b/simvue/api/objects/alert/events.py @@ -1,40 +1,69 @@ -""" -Simvue Event Alerts -=================== +"""Simvue Event Alerts. Interface to event-based Simvue alerts. """ import typing +from collections.abc import Generator + import pydantic try: from typing import Self except ImportError: - from typing_extensions import Self -from simvue.api.objects.base import write_only -from .base import AlertBase, staging_check + from typing_extensions import Self # noqa: UP035 + +try: + from typing import override +except ImportError: + from typing_extensions import override # noqa: UP035 + +from simvue.api.objects.base import staging_check, write_only from simvue.models import NAME_REGEX +from .base import AlertBase + class EventsAlert(AlertBase): - """Connect to an event-based alert either locally or on a server""" + """Connect to an event-based alert either locally or on a server.""" - def __init__(self, identifier: str | None = None, **kwargs) -> None: - """Initialise a connection to an event alert by identifier""" - self.alert = EventAlertDefinition(self) - super().__init__(identifier, **kwargs) + def __init__( + self, + identifier: str | None = None, + *, + _offline: bool = False, + _local: bool = False, + _user_agent: str | None = None, + _read_only: bool = True, + **kwargs: object, + ) -> None: + """Initialise a connection to an event alert by identifier.""" + self.alert: EventAlertDefinition = EventAlertDefinition(self) + super().__init__( + identifier=identifier, + _offline=_offline, + _local=_local, + _user_agent=_user_agent, + _read_only=_read_only, + **kwargs, + ) @classmethod + @override def get( - cls, count: int | None = None, offset: int | None = None - ) -> dict[str, typing.Any]: - """Retrieve only alerts of the event alert type""" + cls, + *, + count: pydantic.PositiveInt | None = None, + offset: pydantic.NonNegativeInt | None = None, + **kwargs: object, + ) -> Generator[tuple[str, Self]]: + """Retrieve only alerts of the event alert type.""" raise NotImplementedError("Retrieval of only event alerts is not yet supported") @classmethod @pydantic.validate_call + @override def new( cls, *, @@ -45,9 +74,9 @@ def new( frequency: pydantic.PositiveInt, enabled: bool = True, offline: bool = False, - **_, + **_: object, ) -> Self: - """Create a new event-based alert + """Create a new event-based alert. Note parameters are keyword arguments only. @@ -74,9 +103,8 @@ def new( a new event alert with changes staged """ - _alert_definition = {"pattern": pattern, "frequency": frequency} - _alert = EventsAlert( + _alert = cls( name=name, description=description, notification=notification, @@ -87,19 +115,19 @@ def new( _offline=offline, ) _alert._staging |= _alert_definition - _alert._params = {"deduplicate": True} + _alert._params = {"deduplicate": True} # pyright: ignore[reportAttributeAccessIssue] return _alert class EventAlertDefinition: - """Event alert definition sub-class""" + """Event alert definition sub-class.""" def __init__(self, alert: EventsAlert) -> None: - """Initialise an alert definition with its parent alert""" - self._sv_obj = alert + """Initialise an alert definition with its parent alert.""" + self._sv_obj: EventsAlert = alert - def compare(self, other: "EventAlertDefinition") -> bool: - """Compare this definition with that of another EventAlert""" + def compare(self, other: "EventAlertDefinition | object") -> bool: + """Compare this definition with that of another EventAlert.""" if not isinstance(other, EventAlertDefinition): return False @@ -111,10 +139,14 @@ def compare(self, other: "EventAlertDefinition") -> bool: ) @property - def pattern(self) -> str: - """Retrieve the event log pattern monitored by this alert""" + def pattern(self) -> str | None: + """Retrieve the event log pattern monitored by this alert.""" + _alert = self._sv_obj.get_alert() + if not _alert: + _out_msg: str = f"{self.__class__.__name__} object has no alert definition." + raise RuntimeError(_out_msg) try: - return self._sv_obj.get_alert()["pattern"] + return _alert["pattern"] except KeyError as e: raise RuntimeError( "Expected key 'pattern' in alert definition retrieval" @@ -123,9 +155,13 @@ def pattern(self) -> str: @property @staging_check def frequency(self) -> int: - """Retrieve the update frequency for this alert""" + """Retrieve the update frequency for this alert.""" + _alert = self._sv_obj.get_alert() + if not _alert: + _out_msg: str = f"{self.__class__.__name__} object has no alert definition." + raise RuntimeError(_out_msg) try: - return self._sv_obj.get_alert()["frequency"] + return _alert["frequency"] except KeyError as e: raise RuntimeError( "Expected key 'frequency' in alert definition retrieval" @@ -135,6 +171,10 @@ def frequency(self) -> int: @write_only @pydantic.validate_call def frequency(self, frequency: int) -> None: - """Set the update frequency for this alert""" - _alert = self._sv_obj.get_alert() | {"frequency": frequency} - self._sv_obj._staging["alert"] = _alert + """Set the update frequency for this alert.""" + _alert = self._sv_obj.get_alert() + if not _alert: + _out_msg: str = f"{self.__class__.__name__} object has no alert definition." + raise RuntimeError(_out_msg) + _alert |= {"frequency": frequency} + self._sv_obj._staging["alert"] = _alert # noqa: SLF001 diff --git a/simvue/api/objects/base.py b/simvue/api/objects/base.py index a91bfad0..204c8861 100644 --- a/simvue/api/objects/base.py +++ b/simvue/api/objects/base.py @@ -438,7 +438,7 @@ def _get_visibility(self) -> dict[str, bool | list[str]]: @classmethod @abc.abstractmethod - def new(cls, **_: object | str | float | bool) -> Self: + def new(cls, **_: typing.Any) -> Self: # noqa: ANN401 """Define new instance of this object.""" @classmethod From 8591f855d5a6174b13e29cf680e0ed83579e20b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Zar=C4=99bski?= Date: Thu, 2 Oct 2025 07:24:19 +0100 Subject: [PATCH 06/23] Continue linting on requests submodule --- simvue/api/request.py | 78 ++++++++++++++++++++++++++++--------------- 1 file changed, 52 insertions(+), 26 deletions(-) diff --git a/simvue/api/request.py b/simvue/api/request.py index 4d715e4b..07852f87 100644 --- a/simvue/api/request.py +++ b/simvue/api/request.py @@ -1,6 +1,4 @@ -""" -Simvue API Connection -===================== +"""Simvue API Connection. Provides methods for interacting with a Simvue server which include retry policies. In cases where JSON is the expected form the data is firstly converted @@ -23,6 +21,8 @@ from simvue.utilities import parse_validation_response +logger = logging.getLogger(__name__) + DEFAULT_API_TIMEOUT = 10 RETRY_MULTIPLIER = 1 RETRY_MIN = 4 @@ -40,17 +40,14 @@ def set_json_header(headers: dict[str, str]) -> dict[str, str]: - """ - Return a copy of the headers with Content-Type set to - application/json - """ + """Return a copy of the headers with Content-Type set to application/json.""" headers = copy.deepcopy(headers) headers["Content-Type"] = "application/json" return headers -def is_retryable_exception(exception: Exception) -> bool: - """Returns if the given exception should lead to a retry being called""" +def is_retryable_exception(exception: BaseException) -> bool: + """Return if the given exception should lead to a retry being called.""" if isinstance(exception, requests.HTTPError): return exception.response.status_code in RETRY_STATUS_CODES @@ -67,12 +64,13 @@ def post( url: str, headers: dict[str, str], params: dict[str, str], - data: typing.Any, + data: object, + *, is_json: bool = True, timeout: int | None = None, files: dict[str, typing.Any] | None = None, ) -> requests.Response: - """HTTP POST with retries + """HTTP POST with retries. Parameters ---------- @@ -94,7 +92,7 @@ def post( """ if is_json: - data_sent: str | dict[str, typing.Any] = json_module.dumps(data) + data_sent: str | dict[str, typing.Any] | object = json_module.dumps(data) headers = set_json_header(headers) else: data_sent = data @@ -110,9 +108,11 @@ def post( if response.status_code == http.HTTPStatus.UNPROCESSABLE_ENTITY: _parsed_response = parse_validation_response(response.json()) - raise ValueError( - f"Validation error for '{url}' [{response.status_code}]:\n{_parsed_response}" + _out_msg: str = ( + f"Validation error for '{url}' [{response.status_code}]:" + f"\n{_parsed_response}" ) + raise ValueError(_out_msg) return response @@ -128,10 +128,11 @@ def put( headers: dict[str, str], data: dict[str, typing.Any] | None = None, json: dict[str, typing.Any] | None = None, + *, is_json: bool = True, timeout: int = DEFAULT_API_TIMEOUT, ) -> requests.Response: - """HTTP PUT with retries + """HTTP PUT with retries. Parameters ---------- @@ -154,12 +155,12 @@ def put( response from executing PUT """ if is_json and data: - data_sent: str | dict[str, typing.Any] = json_module.dumps(data) + data_sent: str | dict[str, typing.Any] | object = json_module.dumps(data) headers = set_json_header(headers) else: data_sent = data - logging.debug(f"PUT: {url}\n\tdata={data_sent}\n\tjson={json}") + logger.debug("PUT: %s\n\tdata=%s\n\tjson=%s", url, data_sent, json) return requests.put( url, headers=headers, data=data_sent, timeout=timeout, json=json @@ -179,7 +180,7 @@ def get( timeout: int = DEFAULT_API_TIMEOUT, json: dict[str, typing.Any] | None = None, ) -> requests.Response: - """HTTP GET + """HTTP GET. Parameters ---------- @@ -197,7 +198,7 @@ def get( requests.Response response from executing GET """ - logging.debug(f"GET: {url}\n\tparams={params}") + logger.debug("GET: %s\n\tparams=%s", url, params) return requests.get(url, headers=headers, timeout=timeout, params=params, json=json) @@ -213,7 +214,7 @@ def delete( timeout: int = DEFAULT_API_TIMEOUT, params: dict[str, typing.Any] | None = None, ) -> requests.Response: - """HTTP DELETE + """HTTP DELETE. Parameters ---------- @@ -231,19 +232,43 @@ def delete( requests.Response response from executing DELETE """ - logging.debug(f"DELETE: {url}\n\tparams={params}") + logger.debug("DELETE: %s\n\tparams=%s", url, params) return requests.delete(url, headers=headers, timeout=timeout, params=params) def get_json_from_response( - expected_status: list[int], + expected_status: list[http.HTTPStatus], scenario: str, response: requests.Response, + *, allow_parse_failure: bool = False, expected_type: type[dict[str, object] | list[object]] = dict, ) -> dict[str, object] | list[object]: + """Retrieve the JSON data from a server response. + + Parameters + ---------- + expected_status : list[http.HTTPStatus] + list of valid response status codes. + scenario : str + description of the scenario. + response : requests.Response + the response object to process + allow_parse_failure : bool, optional + whether the attempt to parse failure response + is allowed to itself fail, default False. + expected_type : type[dict | list], optional + the expected response type, default dict. + + Returns + ------- + list | dict + the response information. + """ try: - json_response = response.json() + json_response: list[dict[str, object]] | dict[str, object] | None = ( + response.json() + ) json_response = json_response or ({} if expected_type is dict else []) decode_error = "" except json_module.JSONDecodeError as e: @@ -254,12 +279,13 @@ def get_json_from_response( details: str | None = None if (_status_code := response.status_code) in expected_status: - if not isinstance(json_response, expected_type): + if json_response is None: + details = f"could not request JSON response: {decode_error}" + elif not isinstance(json_response, expected_type): details = f"expected type '{expected_type.__name__}' but got '{type(json_response).__name__}'" - elif json_response is not None: + else: return json_response else: - details = f"could not request JSON response: {decode_error}" elif isinstance(json_response, dict): error_str += f" with status {_status_code}" details = (json_response or {}).get("detail") From 87cfa3bcc99125323c98d1d2b3143fc6525655ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Zar=C4=99bski?= Date: Thu, 9 Oct 2025 08:25:46 +0100 Subject: [PATCH 07/23] Continue linter fixes --- .pre-commit-config.yaml | 4 + simvue/api/objects/artifact/__init__.py | 8 +- simvue/api/objects/artifact/base.py | 2 +- simvue/api/objects/artifact/file.py | 18 +++- simvue/api/objects/artifact/object.py | 84 +++++++++++----- simvue/api/objects/storage/__init__.py | 12 +-- simvue/api/objects/storage/base.py | 91 +++++++++++------ simvue/api/objects/storage/fetch.py | 128 ++++++++++++++++++------ simvue/api/objects/storage/file.py | 20 ++-- simvue/api/objects/storage/s3.py | 67 +++++++++---- simvue/config/user.py | 5 +- 11 files changed, 307 insertions(+), 132 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0eb4fead..969bd3b1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -39,3 +39,7 @@ repos: hooks: - id: bandit args: [-lll, --recursive, clumper] + - repo: https://github.com/DetachHead/basedpyright-pre-commit-mirror + rev: 1.31.6 + hooks: + - id: basedpyright diff --git a/simvue/api/objects/artifact/__init__.py b/simvue/api/objects/artifact/__init__.py index 168fb3a8..194ae4c3 100644 --- a/simvue/api/objects/artifact/__init__.py +++ b/simvue/api/objects/artifact/__init__.py @@ -1,5 +1,7 @@ -from .fetch import Artifact as Artifact -from .file import FileArtifact as FileArtifact -from .object import ObjectArtifact as ObjectArtifact +"""Simvue Artifact Object types.""" + +from .fetch import Artifact +from .file import FileArtifact +from .object import ObjectArtifact __all__ = ["Artifact", "FileArtifact", "ObjectArtifact"] diff --git a/simvue/api/objects/artifact/base.py b/simvue/api/objects/artifact/base.py index 6b2cd600..92927ccf 100644 --- a/simvue/api/objects/artifact/base.py +++ b/simvue/api/objects/artifact/base.py @@ -75,7 +75,7 @@ def __init__( @classmethod @override - def new(cls, *_: object, **__: object) -> Self: + def new(cls, **kwargs: typing.Any) -> Self: raise NotImplementedError @override diff --git a/simvue/api/objects/artifact/file.py b/simvue/api/objects/artifact/file.py index 2a0216d3..98974a19 100644 --- a/simvue/api/objects/artifact/file.py +++ b/simvue/api/objects/artifact/file.py @@ -1,10 +1,10 @@ """File type artifact handling.""" +import datetime import os import pathlib import shutil import typing -from datetime import datetime import pydantic @@ -25,6 +25,12 @@ from typing_extensions import override # noqa: UP035 +try: + tz_utc = datetime.UTC +except AttributeError: + tz_utc = datetime.timezone.utc # noqa: UP017 + + class FileArtifact(ArtifactBase): """File based artifact modification and creation class.""" @@ -102,11 +108,15 @@ def new( _local_staging_dir: pathlib.Path = _user_config.offline.cache.joinpath( "artifacts" ) - _local_staging_dir.mkdir(parents=True, exist_ok=True) + _local_staging_dir.mkdir(parents=True, exist_ok=True) # pyright: ignore[reportUnusedCallResult] + _time_stamp: str = datetime.datetime.now(tz=datetime.UTC).strftime( + "%Y-%m-%d_%H-%M-%S_%f" + ) _local_staging_file = _local_staging_dir.joinpath( - f"{file_path.stem}_{datetime.now().strftime('%Y-%m-%d_%H-%M-%S_%f')[:-3]}.file" + f"{file_path.stem}_{_time_stamp[:-3]}.file" ) - shutil.copy(file_path, _local_staging_file) + _local_staging_file = typing.cast("pathlib.Path", _local_staging_file) + _ = shutil.copy(file_path, _local_staging_file) file_path = _local_staging_file _file_size = file_path.stat().st_size diff --git a/simvue/api/objects/artifact/object.py b/simvue/api/objects/artifact/object.py index bfbdd46e..3c1b7bad 100644 --- a/simvue/api/objects/artifact/object.py +++ b/simvue/api/objects/artifact/object.py @@ -1,24 +1,42 @@ -from .base import ArtifactBase +"""Object artifact handling.""" + +import io +import sys +import typing + +import pydantic + from simvue.models import NAME_REGEX from simvue.serialization import serialize_object from simvue.utilities import calculate_sha256 -import pydantic -import typing -import sys -import io +from .base import ArtifactBase + +if typing.TYPE_CHECKING: + import pathlib + + from _typeshed import ReadableBuffer try: from typing import Self except ImportError: - from typing_extensions import Self + from typing import Self + +try: + from typing import override +except ImportError: + from typing_extensions import override # noqa: UP035 class ObjectArtifact(ArtifactBase): """Object based artifact modification and creation class.""" def __init__( - self, identifier: str | None = None, _read_only: bool = True, **kwargs + self, + identifier: str | None = None, + *, + _read_only: bool = True, + **kwargs: object, ) -> None: """Initialise a new object artifact connection. @@ -27,24 +45,25 @@ def __init__( identifier : str, optional the identifier of this object on the server. """ - kwargs.pop("original_path", None) - super().__init__(identifier, _read_only, original_path="", **kwargs) + _ = kwargs.pop("original_path", None) + super().__init__(identifier, _read_only=_read_only, original_path="", **kwargs) @classmethod @pydantic.validate_call + @override def new( cls, *, name: typing.Annotated[str, pydantic.Field(pattern=NAME_REGEX)], storage: str | None, - obj: typing.Any, - metadata: dict[str, typing.Any] | None, + obj: object, + metadata: dict[str, object] | None, upload_timeout: int | None = None, allow_pickling: bool = True, offline: bool = False, - **kwargs, + **kwargs: object, ) -> Self: - """Create a new artifact either locally or on the server + """Create a new artifact either locally or on the server. Note all arguments are keyword arguments @@ -73,25 +92,30 @@ def new( _data_type = kwargs.pop("mime_type") _serialized = kwargs.pop("serialized") _checksum = kwargs.pop("checksum") - kwargs.pop("size") - kwargs.pop("original_path") - except KeyError: - raise ValueError("Must provide an object to be saved, not None.") + _ = kwargs.pop("size") + _ = kwargs.pop("original_path") + except KeyError as e: + raise ValueError("Must provide an object to be saved, not None.") from e else: _serialization = serialize_object(obj, allow_pickling) if not _serialization or not (_serialized := _serialization[0]): - raise ValueError(f"Could not serialize object of type '{type(obj)}'") + _out_msg: str = f"Could not serialize object of type '{type(obj)}'" + raise ValueError(_out_msg) if not (_data_type := _serialization[1]) and not allow_pickling: - raise ValueError( + _out_msg = ( f"Could not serialize object of type '{type(obj)}' without pickling" ) + raise ValueError(_out_msg) _checksum = calculate_sha256(_serialized, is_file=False) - _artifact = ObjectArtifact( + _serialized = typing.cast("ReadableBuffer", _serialized) + + _artifact = cls( + identifier=None, name=name, storage=storage, size=sys.getsizeof(_serialized), @@ -107,17 +131,23 @@ def new( _artifact._init_data = {} _artifact._staging["obj"] = None _artifact._local_staging_file.parent.mkdir(parents=True, exist_ok=True) - with open( - _artifact._local_staging_file.parent.joinpath(f"{_artifact.id}.object"), - "wb", - ) as file: - file.write(_serialized) + _file_path: pathlib.Path = _artifact._local_staging_file.parent.joinpath( + f"{_artifact.id}.object" + ) + with _file_path.open("wb") as file: + _ = file.write(_serialized) else: - _artifact._init_data = _artifact._post_single(**_artifact._staging) + _single_post_data = typing.cast( + "dict[str, dict[str, object]]", + _artifact._post_single(**_artifact._staging), + ) + _artifact._init_data = _single_post_data _artifact._staging["url"] = _artifact._init_data["url"] - _artifact._init_data["runs"] = kwargs.get("runs") or {} + _artifact._init_data["runs"] = typing.cast( + "dict[str, object]", kwargs.get("runs") or {} + ) if offline: return _artifact diff --git a/simvue/api/objects/storage/__init__.py b/simvue/api/objects/storage/__init__.py index cfb43777..6a7ea965 100644 --- a/simvue/api/objects/storage/__init__.py +++ b/simvue/api/objects/storage/__init__.py @@ -1,6 +1,4 @@ -""" -Simvue Storage -============== +"""Simvue Storage. Contains classes for interacting with Simvue storage objects, the storage types are split into classes to ensure correct @@ -8,6 +6,8 @@ """ -from .file import FileStorage as FileStorage -from .s3 import S3Storage as S3Storage -from .fetch import Storage as Storage +from .fetch import Storage +from .file import FileStorage +from .s3 import S3Storage + +__all__ = ["FileStorage", "S3Storage", "Storage"] diff --git a/simvue/api/objects/storage/base.py b/simvue/api/objects/storage/base.py index 24bb7cc9..53e69f66 100644 --- a/simvue/api/objects/storage/base.py +++ b/simvue/api/objects/storage/base.py @@ -1,17 +1,25 @@ -""" -Simvue Storage Base -=================== +"""Simvue Storage Base. Contains general definitions for Simvue Storage objects. """ +import datetime import typing import pydantic -import datetime from simvue.api.objects.base import SimvueObject, staging_check, write_only -from simvue.models import NAME_REGEX, DATETIME_FORMAT +from simvue.models import DATETIME_FORMAT, NAME_REGEX + +try: + from typing import Self +except ImportError: + from typing import Self + +try: + from typing import override +except ImportError: + from typing_extensions import override # noqa: UP035 class StorageBase(SimvueObject): @@ -24,24 +32,37 @@ class StorageBase(SimvueObject): def __init__( self, identifier: str | None = None, + *, _read_only: bool = False, - **kwargs, + _offline: bool = False, + _user_agent: str | None = None, + _local: bool = False, + **kwargs: object, ) -> None: - """Retrieve an alert from the Simvue server by identifier""" - self._label = "storage" - self._endpoint = self._label - super().__init__(identifier, _read_only=_read_only, **kwargs) + """Retrieve an alert from the Simvue server by identifier.""" + self._label: str = "storage" + self._endpoint: str = self._label + super().__init__( + identifier, + _read_only=_read_only, + _offline=_offline, + _user_agent=_user_agent, + _local=_local, + **kwargs, + ) @classmethod - def new(cls, **_): - """Create a new instance of a storage type""" - pass + @override + def new(cls, **_: typing.Any) -> Self: + """Create a new instance of a storage type.""" + raise NotImplementedError @property @staging_check def name(self) -> str: - """Retrieve the name for this storage""" - return self._get_attribute("name") + """Retrieve the name for this storage.""" + _name: str = typing.cast("str", self._get_attribute("name")) + return _name @name.setter @write_only @@ -49,57 +70,67 @@ def name(self) -> str: def name( self, name: typing.Annotated[str, pydantic.Field(pattern=NAME_REGEX)] ) -> None: - """Set name assigned to this folder""" + """Set name assigned to this folder.""" self._staging["name"] = name @property def backend(self) -> str: - """Retrieve the backend of storage""" - return self._get_attribute("backend") + """Retrieve the backend of storage.""" + _backend: str = typing.cast("str", self._get_attribute("backend")) + return _backend @property @staging_check def is_default(self) -> bool: - """Retrieve if this is the default storage for the user""" - return self._get_attribute("is_default") + """Retrieve if this is the default storage for the user.""" + _is_default: bool = typing.cast("bool", self._get_attribute("is_default")) + return _is_default @is_default.setter @write_only @pydantic.validate_call def is_default(self, is_default: bool) -> None: - """Set this storage to be the default""" + """Set this storage to be the default.""" self._staging["is_default"] = is_default @property @staging_check def is_tenant_useable(self) -> bool: - """Retrieve if this is usable by the current user tenant""" - return self._get_attribute("is_tenant_useable") + """Retrieve if this is usable by the current user tenant.""" + _is_tenant_useable: bool = typing.cast( + "bool", self._get_attribute("is_tenant_useable") + ) + return _is_tenant_useable @is_tenant_useable.setter @write_only @pydantic.validate_call def is_tenant_useable(self, is_tenant_useable: bool) -> None: - """Set this storage to be usable by the current user tenant""" + """Set this storage to be usable by the current user tenant.""" self._staging["is_tenant_useable"] = is_tenant_useable @property @staging_check def is_enabled(self) -> bool: - """Retrieve if this is enabled""" - return self._get_attribute("is_enabled") + """Retrieve if this is enabled.""" + _is_enabled: bool = typing.cast("bool", self._get_attribute("is_enabled")) + return _is_enabled @is_enabled.setter @write_only @pydantic.validate_call def is_enabled(self, is_enabled: bool) -> None: - """Set this storage to be usable by the current user tenant""" + """Set this storage to be usable by the current user tenant.""" self._staging["is_enabled"] = is_enabled @property def created(self) -> datetime.datetime | None: - """Retrieve created datetime for the artifact""" - _created: str | None = self._get_attribute("created") + """Retrieve created datetime for the artifact.""" + _created: str | None = typing.cast("str | None", self._get_attribute("created")) return ( - datetime.datetime.strptime(_created, DATETIME_FORMAT) if _created else None + datetime.datetime.strptime(_created, DATETIME_FORMAT).astimezone( + datetime.UTC + ) + if _created + else None ) diff --git a/simvue/api/objects/storage/fetch.py b/simvue/api/objects/storage/fetch.py index e879c98d..01ee36cb 100644 --- a/simvue/api/objects/storage/fetch.py +++ b/simvue/api/objects/storage/fetch.py @@ -1,27 +1,37 @@ -""" -Simvue Storage Retrieval -==============--======== +"""Simvue Storage Retrieval. To simplify case whereby user does not know the storage type associated with an identifier, use a generic storage object. """ -import typing import http +import typing +from collections.abc import Generator + import pydantic -from simvue.api.request import get_json_from_response from simvue.api.request import get as sv_get +from simvue.api.request import get_json_from_response +from simvue.config.user import SimvueConfiguration -from .s3 import S3Storage -from .file import FileStorage from .base import StorageBase +from .file import FileStorage +from .s3 import S3Storage class Storage: - """Generic Simvue storage retrieval class""" - - def __init__(self, identifier: str | None = None, *args, **kwargs) -> None: + """Generic Simvue storage retrieval class.""" + + def __init__( + self, + identifier: str | None = None, + *, + _read_only: bool = False, + _offline: bool = False, + _user_agent: str | None = None, + _local: bool = False, + **kwargs: object, + ) -> None: """Initialise an instance of generic storage retriever. Parameters @@ -29,24 +39,56 @@ def __init__(self, identifier: str | None = None, *args, **kwargs) -> None: identifier : str identifier of storage object to retrieve """ - super().__init__(identifier=identifier, *args, **kwargs) - def __new__(cls, identifier: str | None = None, **kwargs): - """Retrieve an object representing an storage either locally or on the server by id""" - _storage_pre = StorageBase(identifier=identifier, **kwargs) + def __new__( + cls, + identifier: str | None = None, + *, + _read_only: bool = False, + _offline: bool = False, + _user_agent: str | None = None, + _local: bool = False, + **kwargs: object, + ) -> S3Storage | FileStorage: + """Retrieve a storage object either locally or on the server by id.""" + _storage_pre = StorageBase( + identifier=identifier, + _read_only=True, + _local=True, + _offline=False, + _user_agent=None, + ) if _storage_pre.backend == "S3": - return S3Storage(identifier=identifier, **kwargs) - elif _storage_pre.backend == "File": - return FileStorage(identifier=identifier, **kwargs) - - raise RuntimeError(f"Unknown backend '{_storage_pre.backend}'") + return S3Storage( + _local=True, + _read_only=True, + identifier=identifier, + _user_agent=None, + _offline=False, + **kwargs, + ) + if _storage_pre.backend == "File": + return FileStorage( + _local=True, + _read_only=True, + identifier=identifier, + _user_agent=None, + _offline=False, + **kwargs, + ) + + _out_msg: str = f"Unknown backend '{_storage_pre.backend}'" + raise RuntimeError(_out_msg) @classmethod @pydantic.validate_call def get( - cls, count: int | None = None, offset: int | None = None, **kwargs - ) -> typing.Generator[tuple[str, FileStorage | S3Storage], None, None]: - """Returns storage systems accessible to the current user. + cls, + count: int | None = None, + offset: int | None = None, + **kwargs: str | float | None, + ) -> Generator[tuple[str, FileStorage | S3Storage]]: + """Return storage systems accessible to the current user. Parameters ---------- @@ -61,16 +103,21 @@ def get( identifier for a storage the storage itself as a class instance """ - # Currently no storage filters - kwargs.pop("filters", None) + _ = kwargs.pop("filters", None) _class_instance = StorageBase(_local=True, _read_only=True) - _url = f"{_class_instance._base_url}" + _config: SimvueConfiguration = SimvueConfiguration.fetch() + _url = f"{_class_instance.base_url}" + _params: dict[str, int | float | str | None | list[str]] = { + "start": offset, + "count": count, + } + _params |= kwargs _response = sv_get( _url, - headers=_class_instance._headers, - params={"start": offset, "count": count} | kwargs, + headers=_config.headers, # pyright: ignore[reportUnknownArgumentType] + params=_params, ) _label: str = _class_instance.__class__.__name__.lower() _label = _label.replace("base", "") @@ -81,21 +128,36 @@ def get( expected_type=list, ) + _json_response = typing.cast("list[dict[str, object]]", _json_response) + _out_dict: dict[str, FileStorage | S3Storage] = {} for _entry in _json_response: - _id = _entry.pop("id") + _id = typing.cast("str", _entry.pop("id")) if _entry["backend"] == "S3": yield ( _id, - S3Storage(_local=True, _read_only=True, identifier=_id, **_entry), + S3Storage( + _local=True, + _read_only=True, + identifier=_id, + _user_agent=None, + _offline=False, + **_entry, + ), ) elif _entry["backend"] == "File": yield ( _id, - FileStorage(_local=True, _read_only=True, identifier=_id, **_entry), + FileStorage( + _local=True, + _read_only=True, + identifier=_id, + _user_agent=None, + _offline=False, + **_entry, + ), ) else: - raise RuntimeError( - f"Unrecognised storage backend '{_entry['backend']}'" - ) + _out_msg: str = f"Unrecognised storage backend '{_entry['backend']}'" + raise RuntimeError(_out_msg) diff --git a/simvue/api/objects/storage/file.py b/simvue/api/objects/storage/file.py index 7f7ac186..dce15159 100644 --- a/simvue/api/objects/storage/file.py +++ b/simvue/api/objects/storage/file.py @@ -1,6 +1,4 @@ -""" -Simvue File Storage -=================== +"""Simvue File Storage. Class for interacting with a file based storage on the server. @@ -11,18 +9,26 @@ try: from typing import Self except ImportError: - from typing_extensions import Self + from typing_extensions import Self # noqa: UP035 + import pydantic -from .base import StorageBase from simvue.models import NAME_REGEX +from .base import StorageBase + +try: + from typing import override +except ImportError: + from typing_extensions import override # noqa: UP035 + class FileStorage(StorageBase): """Class for defining/accessing a File based storage system on the server.""" @classmethod @pydantic.validate_call + @override def new( cls, *, @@ -32,7 +38,7 @@ def new( is_enabled: bool, is_default: bool, offline: bool = False, - **_, + **_: object, ) -> Self: """Create a new file storage object. @@ -56,7 +62,7 @@ def new( FileStorage instance of storage system with staged changes """ - return FileStorage( + return cls( name=name, backend="File", disable_check=disable_check, diff --git a/simvue/api/objects/storage/s3.py b/simvue/api/objects/storage/s3.py index 87ff3c3c..ac8f5e0e 100644 --- a/simvue/api/objects/storage/s3.py +++ b/simvue/api/objects/storage/s3.py @@ -1,34 +1,57 @@ -""" -Simvue S3 Storage -================= +"""Simvue S3 Storage. Class for interacting with an S3 based storage on the server. """ +from __future__ import annotations + import typing try: from typing import Self except ImportError: - from typing_extensions import Self -import pydantic + from typing import Self -from simvue.api.objects.base import write_only +try: + from typing import override +except ImportError: + from typing_extensions import override # noqa: UP035 -from .base import StorageBase, staging_check +import pydantic + +from simvue.api.objects.base import staging_check, write_only from simvue.models import NAME_REGEX +from .base import StorageBase + class S3Storage(StorageBase): """Class for defining/accessing an S3 based storage system on the server.""" - def __init__(self, identifier: str | None = None, **kwargs) -> None: - """Initialise an S3Storage instance attaching a configuration""" - self.config = Config(self) - super().__init__(identifier, **kwargs) + def __init__( + self, + identifier: str | None = None, + *, + _read_only: bool = False, + _offline: bool = False, + _user_agent: str | None = None, + _local: bool = False, + **kwargs: object, + ) -> None: + """Initialise an S3Storage instance attaching a configuration.""" + self.config: Config = Config(self) + super().__init__( + identifier, + _local=_local, + _read_only=_read_only, + _offline=_offline, + _user_agent=_user_agent, + **kwargs, + ) @classmethod + @override @pydantic.validate_call def new( cls, @@ -44,7 +67,7 @@ def new( is_default: bool, is_enabled: bool, offline: bool = False, - **__, + **__: object, ) -> Self: """Create a new S3 storage object. @@ -86,7 +109,7 @@ def new( "secret_access_key": secret_access_key.get_secret_value(), "bucket": bucket, } - _storage = S3Storage( + _storage = cls( name=name, backend="S3", config=_config, @@ -101,20 +124,23 @@ def new( return _storage @staging_check - def get_config(self) -> dict[str, typing.Any]: - """Retrieve configuration""" + def get_config(self) -> dict[str, float | None | str]: + """Retrieve configuration.""" try: - return self._get_attribute("config") + _config: dict[str, float | None | str] = typing.cast( + "dict[str, float | None | str]", self._get_attribute("config") + ) except AttributeError: return {} + return _config class Config: - """S3 Configuration interface""" + """S3 Configuration interface.""" def __init__(self, storage: S3Storage) -> None: - """Initialise a new configuration using an S3Storage object""" - self._sv_obj = storage + """Initialise a new configuration using an S3Storage object.""" + self._sv_obj: S3Storage = storage @property @staging_check @@ -126,6 +152,9 @@ def endpoint_url(self) -> str: str the endpoint for this storage object """ + if not self._sv_obj: + raise RuntimeError("Expected S3Storage instance but none found.") + try: return self._sv_obj.get_config()["endpoint_url"] except KeyError as e: diff --git a/simvue/config/user.py b/simvue/config/user.py index bdbab137..52145026 100644 --- a/simvue/config/user.py +++ b/simvue/config/user.py @@ -12,6 +12,7 @@ import os import pathlib import typing +from collections.abc import Mapping import pydantic import semver @@ -61,8 +62,8 @@ class SimvueConfiguration(pydantic.BaseModel): eco: EcoConfig = EcoConfig() @classmethod - def _load_pyproject_configs(cls) -> dict | None: - """Recover any Simvue non-authentication configurations from pyproject.toml""" + def _load_pyproject_configs(cls) -> Mapping[str, object] | None: + """Recover any Simvue non-authentication configurations from pyproject.toml.""" _pyproject_toml = sv_util.find_first_instance_of_file( file_names=["pyproject.toml"], check_user_space=False ) From 7f610c3b1c7b3991e5874122c7c2c17f5f5805c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Zar=C4=99bski?= Date: Mon, 13 Oct 2025 07:56:09 +0100 Subject: [PATCH 08/23] =?UTF-8?q?=F0=9F=9A=A7=20Continued=20linting=20fixe?= =?UTF-8?q?s=20on=20LLAPI=20base=20classes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- simvue/api/objects/base.py | 31 +++++++++++++++++-------------- simvue/api/objects/storage/s3.py | 32 +++++++++++++++++--------------- 2 files changed, 34 insertions(+), 29 deletions(-) diff --git a/simvue/api/objects/base.py b/simvue/api/objects/base.py index 204c8861..aa679e7b 100644 --- a/simvue/api/objects/base.py +++ b/simvue/api/objects/base.py @@ -53,18 +53,17 @@ if typing.TYPE_CHECKING: import pathlib -# Need to use this inside of Generator typing to fix bug -# present in Python 3.10 - see issue #745 T = typing.TypeVar("T", bound="SimvueObject") -U = typing.TypeVar("U", bound="SimvueObject | Visibility") +C = typing.TypeVar("C") +U = typing.TypeVar("U") def staging_check( - member_func: typing.Callable[[U], typing.Any | None], -) -> typing.Callable[[U], typing.Any | None]: + member_func: typing.Callable[[T], C], +) -> typing.Callable[[T], C]: """Check if requested attribute has uncommitted changes via decorator.""" - def _wrapper(self: U) -> object: + def _wrapper(self: T) -> C: _sv_obj = typing.cast("SimvueObject", getattr(self, "_sv_obj", self)) if not hasattr(_sv_obj, "_offline"): _out_msg: str = ( @@ -89,11 +88,11 @@ def _wrapper(self: U) -> object: def write_only( - attribute_func: typing.Callable[[U, typing.Any], typing.Any | None], -) -> typing.Callable[[U, typing.Any], typing.Any | None]: + attribute_func: typing.Callable[[T, U], None], +) -> typing.Callable[[T, U], None]: """Check if function only available in write mode.""" - def _wrapper(self: U, *args: object, **kwargs: object) -> object | None: + def _wrapper(self: T, *args: U, **kwargs: U) -> object | None: _sv_obj: SimvueObject = typing.cast( "SimvueObject", getattr(self, "_sv_obj", self) ) @@ -114,15 +113,19 @@ def _wrapper(self: U, *args: object, **kwargs: object) -> object | None: return _wrapper -class Visibility: - """Interface for object visibility definition.""" +class SimvueObjectAttribute(typing.Generic[T]): + """Interface for attributes to Simvue objects with sub-attributes.""" - def __init__(self, sv_obj: SimvueObject) -> None: + def __init__(self, sv_obj: T) -> None: """Initialise visibility with target object.""" - self._sv_obj: SimvueObject = sv_obj + self._sv_obj: T = sv_obj + + +class Visibility(SimvueObjectAttribute["SimvueObject"]): + """Interface for object visibility definition.""" def _update_visibility(self, key: str, value: object) -> None: - """Update the visibility configuration for this object.""" + """Tpdate the visibility configuration for this object.""" _visibility = self._sv_obj._get_visibility() | {key: value} # noqa: SLF001 self._sv_obj._staging["visibility"] = _visibility # noqa: SLF001 diff --git a/simvue/api/objects/storage/s3.py b/simvue/api/objects/storage/s3.py index ac8f5e0e..2479d2c8 100644 --- a/simvue/api/objects/storage/s3.py +++ b/simvue/api/objects/storage/s3.py @@ -20,7 +20,7 @@ import pydantic -from simvue.api.objects.base import staging_check, write_only +from simvue.api.objects.base import SimvueObjectAttribute, staging_check, write_only from simvue.models import NAME_REGEX from .base import StorageBase @@ -135,13 +135,9 @@ def get_config(self) -> dict[str, float | None | str]: return _config -class Config: +class Config(SimvueObjectAttribute[S3Storage]): """S3 Configuration interface.""" - def __init__(self, storage: S3Storage) -> None: - """Initialise a new configuration using an S3Storage object.""" - self._sv_obj: S3Storage = storage - @property @staging_check def endpoint_url(self) -> str: @@ -156,25 +152,31 @@ def endpoint_url(self) -> str: raise RuntimeError("Expected S3Storage instance but none found.") try: - return self._sv_obj.get_config()["endpoint_url"] + _http_url: str = typing.cast( + "str", self._sv_obj.get_config()["endpoint_url"] + ) except KeyError as e: raise RuntimeError( "Expected key 'endpoint_url' in alert definition retrieval" ) from e + return _http_url @endpoint_url.setter @write_only @pydantic.validate_call - def endpoint_url(self, endpoint_url: pydantic.HttpUrl) -> None: + def endpoint_url( + self, + endpoint_url: typing.Annotated[str, pydantic.BeforeValidator(pydantic.HttpUrl)], + ) -> None: _config = self._sv_obj.get_config() | {"endpoint_url": endpoint_url.__str__()} - self._sv_obj._staging["config"] = _config + self._sv_obj._staging["config"] = _config # noqa: SLF001 @property @staging_check def region_name(self) -> str: - """Retrieve the region name for this storage""" + """Retrieve the region name for this storage.""" try: - return self._sv_obj.get_config()["region_name"] + return typing.cast("str", self._sv_obj.get_config()["region_name"]) except KeyError as e: raise RuntimeError( "Expected key 'region_name' in alert definition retrieval" @@ -184,16 +186,16 @@ def region_name(self) -> str: @write_only @pydantic.validate_call def region_name(self, region_name: str) -> None: - """Modify the region name for this storage""" + """Modify the region name for this storage.""" _config = self._sv_obj.get_config() | {"region_name": region_name} - self._sv_obj._staging["config"] = _config + self._sv_obj._staging["config"] = _config # noqa: SLF001 @property @staging_check def bucket(self) -> str: - """Retrieve the bucket label for this storage""" + """Retrieve the bucket label for this storage.""" try: - return self._sv_obj.get_config()["bucket"] + return typing.cast("str", self._sv_obj.get_config()["bucket"]) except KeyError as e: raise RuntimeError( "Expected key 'bucket' in alert definition retrieval" From 326167ee390d88242039f285dcabf77e7d115a39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Zar=C4=99bski?= Date: Tue, 21 Oct 2025 08:45:53 +0100 Subject: [PATCH 09/23] =?UTF-8?q?=F0=9F=9A=A7=20More=20linter=20fixes=20to?= =?UTF-8?q?=20API=20objects?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 1 + simvue/api/objects/alert/base.py | 2 +- simvue/api/objects/alert/fetch.py | 139 +++++++++++++++++++++++------- simvue/api/objects/base.py | 10 +++ simvue/api/objects/events.py | 33 +++++-- simvue/api/objects/metrics.py | 4 +- simvue/api/objects/run.py | 6 +- tests/unit/test_stats.py | 2 +- 8 files changed, 150 insertions(+), 47 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 5ee37078..926b159e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -139,6 +139,7 @@ reportAny = "none" # ruff already does this reportMissingTypeStubs = "none" reportPrivateUsage = "none" # ruff already does this reportImportCycles = "none" +reportIncompatibleMethodOverride = "none" [tool.mypy] ignore_missing_imports = true diff --git a/simvue/api/objects/alert/base.py b/simvue/api/objects/alert/base.py index 2a62a677..02bfc727 100644 --- a/simvue/api/objects/alert/base.py +++ b/simvue/api/objects/alert/base.py @@ -38,9 +38,9 @@ class AlertBase(SimvueObject, abc.ABC): @classmethod @override - @abc.abstractmethod def new(cls, **_: typing.Any) -> Self: """Create a new alert.""" + raise NotImplementedError def __init__( self, diff --git a/simvue/api/objects/alert/fetch.py b/simvue/api/objects/alert/fetch.py index 4c8b6ec0..ea85fcd2 100644 --- a/simvue/api/objects/alert/fetch.py +++ b/simvue/api/objects/alert/fetch.py @@ -1,6 +1,4 @@ -""" -Simvue Alert Retrieval -====================== +"""Simvue Alert Retrieval. To simplify case whereby user does not know the alert type associated with an identifier, use a generic alert object. @@ -9,6 +7,7 @@ import http import json import typing +from collections.abc import Generator import pydantic @@ -25,54 +24,105 @@ class AlertSort(Sort): + """Sorting object for Alert retrieval.""" + @pydantic.field_validator("column") @classmethod def check_column(cls, column: str) -> str: + """Check specified column is permitted.""" if column and column not in ("name", "created"): - raise ValueError(f"Invalid sort column for alerts '{column}'") + _out_msg = f"Invalid sort column for alerts '{column}'" + raise ValueError(_out_msg) return column class Alert: - """Generic Simvue alert retrieval class""" + """Generic Simvue alert retrieval class.""" @pydantic.validate_call() def __new__( - cls, identifier: str, _local: bool = False, **kwargs: object + cls, + identifier: str | None, + *, + _local: bool = False, + _read_only: bool = True, + _user_agent: str | None = None, + _offline: bool = False, + **kwargs: object, ) -> AlertType: - """Retrieve an object representing an alert either locally or on the server by id""" - _alert_pre = AlertBase(identifier=identifier, **kwargs) + """Retrieve an object representing an alert locally or on the server by id.""" + _alert_pre = AlertBase( + identifier=identifier, + _local=True, + _offline=False, + _user_agent=None, + _read_only=True, + **kwargs, + ) if ( identifier is not None and identifier.startswith("offline_") - and not _alert_pre._staging.get("source", None) + and not _alert_pre.staging.get("source", None) ): raise RuntimeError( - "Cannot determine Alert type - this is likely because you are attempting to reconnect " - "to an offline alert which has already been sent to the server. To fix this, use the " - "exact Alert type instead (eg MetricThresholdAlert, MetricRangeAlert etc)." + "Cannot determine Alert type - " + "this is likely because you are attempting to reconnect " + "to an offline alert which has already been sent to the server." + " To fix this, use the " + "exact Alert type instead " + "(eg MetricThresholdAlert, MetricRangeAlert etc)." ) if _alert_pre.source == "events": - return EventsAlert(identifier=identifier, _local=_local, **kwargs) + return EventsAlert( + identifier=identifier, + _local=_local, + _read_only=_read_only, + _user_agent=_user_agent, + _offline=_offline, + **kwargs, + ) if _alert_pre.source == "metrics" and _alert_pre.get_alert().get("threshold"): - return MetricsThresholdAlert(identifier=identifier, _local=_local, **kwargs) + return MetricsThresholdAlert( + identifier=identifier, + _local=_local, + _read_only=_read_only, + _user_agent=_user_agent, + _offline=_offline, + **kwargs, + ) if _alert_pre.source == "metrics": - return MetricsRangeAlert(identifier=identifier, _local=_local, **kwargs) + return MetricsRangeAlert( + identifier=identifier, + _local=_local, + _read_only=_read_only, + _user_agent=_user_agent, + _offline=_offline, + **kwargs, + ) if _alert_pre.source == "user": - return UserAlert(identifier=identifier, _local=_local, **kwargs) + return UserAlert( + identifier=identifier, + _local=_local, + _read_only=_read_only, + _user_agent=_user_agent, + _offline=_offline, + **kwargs, + ) - raise RuntimeError(f"Unknown source type '{_alert_pre.source}'") + _out_msg: str = f"Unknown source type '{_alert_pre.source}'" + raise RuntimeError(_out_msg) @classmethod @pydantic.validate_call def get( cls, + *, offline: bool = False, count: int | None = None, offset: int | None = None, sorting: list[AlertSort] | None = None, - **kwargs, - ) -> typing.Generator[tuple[str, AlertType]]: + **kwargs: str | float | None | list[str], + ) -> Generator[tuple[str, AlertType]]: """Fetch all alerts from the server for the current user. Parameters @@ -94,18 +144,18 @@ def get( return # Currently no alert filters - kwargs.pop("filters", None) + _ = kwargs.pop("filters", None) _class_instance = AlertBase(_local=True, _read_only=True) - _url = f"{_class_instance._base_url}" - _params: dict[str, int | str] = {"start": offset, "count": count} + _url = f"{_class_instance.base_url}" + _params: dict[str, int | str | None] = {"start": offset, "count": count} if sorting: _params["sorting"] = json.dumps([sort.to_params() for sort in sorting]) _response = sv_get( _url, - headers=_class_instance._headers, + headers=_class_instance.headers, params=_params | kwargs, ) @@ -116,37 +166,60 @@ def get( expected_status=[http.HTTPStatus.OK], scenario=f"Retrieval of {_label}s", ) + _json_response = typing.cast( + "dict[str, list[dict[str, object]]]", _json_response + ) if (_data := _json_response.get("data")) is None: - raise RuntimeError(f"Expected key 'data' for retrieval of {_label}s") + _out_msg: str = f"Expected key 'data' for retrieval of {_label}s" + raise RuntimeError(_out_msg) _out_dict: dict[str, AlertType] = {} for _entry in _json_response["data"]: - _id = _entry.pop("id") + _id = typing.cast("str", _entry.pop("id")) if _entry["source"] == "events": yield ( _id, - EventsAlert(_read_only=True, identifier=_id, _local=True, **_entry), + EventsAlert( + _read_only=True, + identifier=_id, + _local=True, + _offline=False, + _user_agent=None, + **_entry, + ), ) elif _entry["source"] == "user": yield ( _id, - UserAlert(_read_only=True, identifier=_id, _local=True, **_entry), + UserAlert( + _read_only=True, + identifier=_id, + _local=True, + _offline=False, + _user_agent=None, + **_entry, + ), ) elif ( _entry["source"] == "metrics" - and _entry.get("alert", {}).get("threshold") is not None + and _entry.get("alert", {}).get("threshold") is not None # pyright: ignore[reportAttributeAccessIssue] ): yield ( _id, MetricsThresholdAlert( - _local=True, _read_only=True, identifier=_id, **_entry + _local=True, + _read_only=True, + identifier=_id, + _offline=False, + _user_agent=None, + **_entry, ), ) elif ( _entry["source"] == "metrics" - and _entry.get("alert", {}).get("range_low") is not None + and _entry.get("alert", {}).get("range_low") is not None # pyright: ignore[reportAttributeAccessIssue] ): yield ( _id, @@ -155,6 +228,8 @@ def get( ), ) else: - raise RuntimeError( - f"Unrecognised alert source '{_entry['source']}' with data '{_entry}'" + _out_msg = ( + f"Unrecognised alert source '{_entry['source']}'" + f" with data '{_entry}'" ) + raise RuntimeError(_out_msg) diff --git a/simvue/api/objects/base.py b/simvue/api/objects/base.py index 11857826..f46c5c36 100644 --- a/simvue/api/objects/base.py +++ b/simvue/api/objects/base.py @@ -676,6 +676,16 @@ def offline(self) -> bool: """Return of object in offline mode.""" return self._offline + @property + def staging(self) -> dict[str, typing.Any]: + """Return staging mapping.""" + return self._staging + + @property + def headers(self) -> dict[str, str]: + """Return request headers.""" + return self._headers + @property def id(self) -> str | None: """The identifier for this object if applicable. diff --git a/simvue/api/objects/events.py b/simvue/api/objects/events.py index efbd3c40..f5ca054b 100644 --- a/simvue/api/objects/events.py +++ b/simvue/api/objects/events.py @@ -13,7 +13,7 @@ import pydantic from simvue.api.request import get as sv_get -from simvue.api.request import get_json_from_response, sv_get +from simvue.api.request import get_json_from_response from simvue.models import EventSet, simvue_timestamp from .base import SimvueObject @@ -21,7 +21,13 @@ try: from typing import Self except ImportError: - from typing import Self + from typing_extensions import Self # noqa: UP035 + + +try: + from typing import override +except ImportError: + from typing_extensions import override # noqa: UP035 if typing.TYPE_CHECKING: from simvue.api.url import URL @@ -59,7 +65,14 @@ def get( _class_instance = cls(_read_only=True, _local=True) _count: int = 0 - for response in cls._get_all_objects(offset, count=count, run=run_id, **kwargs): + for response in cls._get_all_objects( + offset=offset, + endpoint=None, + count=count, + run=run_id, + expected_type=dict, + **kwargs, + ): _data = typing.cast( "list[dict[str, str | float | None]] | None", response.get("data") ) @@ -96,11 +109,15 @@ def new( **kwargs, # pyright: ignore[reportArgumentType] ) - def _post_single(self, **kwargs: object) -> dict[str, typing.Any]: - return super()._post_single(is_json=False, **kwargs) + @override + def _post_single( + self, + data: list[dict[str, object]] | dict[str, object] | None = None, + **kwargs: object, + ) -> dict[str, typing.Any] | list[dict[str, typing.Any]]: + return super()._post_single(is_json=False, data=data, **kwargs) - @typing.override - def _put(self, **kwargs: object) -> dict[str, typing.Any]: + def _put(self, **_: object) -> dict[str, typing.Any]: raise NotImplementedError("Method 'put' is not available for type Events") @pydantic.validate_call @@ -134,7 +151,7 @@ def histogram( "Invalid arguments for datetime range, " "value difference must be greater than window" ) - _url: URL = self._base_url / "histogram" + _url: URL = self.base_url / "histogram" _time_begin: str = simvue_timestamp(timestamp_begin) _time_end: str = simvue_timestamp(timestamp_end) _response = sv_get( diff --git a/simvue/api/objects/metrics.py b/simvue/api/objects/metrics.py index 810b2c6b..878c8541 100644 --- a/simvue/api/objects/metrics.py +++ b/simvue/api/objects/metrics.py @@ -116,7 +116,7 @@ def get( @pydantic.validate_call def span(self, run_ids: list[str]) -> dict[str, int | float]: """Returns the metrics span for the given runs""" - _url = self._base_url / "span" + _url = self.base_url / "span" _response = sv_get(url=f"{_url}", headers=self._headers, json=run_ids) return get_json_from_response( response=_response, @@ -127,7 +127,7 @@ def span(self, run_ids: list[str]) -> dict[str, int | float]: @pydantic.validate_call def names(self, run_ids: list[str]) -> list[str]: """Returns the metric names for the given runs""" - _url = self._base_url / "names" + _url = self.base_url / "names" _response = sv_get( url=f"{_url}", headers=self._headers, params={"runs": json.dumps(run_ids)} ) diff --git a/simvue/api/objects/run.py b/simvue/api/objects/run.py index 6bef483d..178c0b9a 100644 --- a/simvue/api/objects/run.py +++ b/simvue/api/objects/run.py @@ -484,7 +484,7 @@ def started(self) -> datetime.datetime | None: _started: str | None = self._get_attribute("started") return ( datetime.datetime.strptime(_started, DATETIME_FORMAT).replace( - tzinfo=datetime.timezone.utc + tzinfo=datetime.UTC ) if _started else None @@ -508,7 +508,7 @@ def endtime(self) -> datetime.datetime | None: _endtime: str | None = self._get_attribute("endtime") return ( datetime.datetime.strptime(_endtime, DATETIME_FORMAT).replace( - tzinfo=datetime.timezone.utc + tzinfo=datetime.UTC ) if _endtime else None @@ -574,7 +574,7 @@ def send_heartbeat(self) -> dict[str, typing.Any] | None: _heartbeat_file.touch() return None - _url = self._base_url + _url = self.base_url _url /= f"{self._identifier}/heartbeat" _response = sv_put(f"{_url}", headers=self._headers, data={}) return get_json_from_response( diff --git a/tests/unit/test_stats.py b/tests/unit/test_stats.py index e2e67f1d..aaddefc8 100644 --- a/tests/unit/test_stats.py +++ b/tests/unit/test_stats.py @@ -6,7 +6,7 @@ @pytest.mark.online def test_stats() -> None: _statistics = Stats() - assert f"{_statistics.url}" == f"{_statistics._base_url}" + assert f"{_statistics.url}" == f"{_statistics.base_url}" assert isinstance(_statistics.runs.created, int) assert isinstance(_statistics.runs.running, int) assert isinstance(_statistics.runs.completed, int) From ea00af7e92d0833e83e325fb7aafacbed9f7a4b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Zar=C4=99bski?= Date: Fri, 6 Feb 2026 16:13:10 +0000 Subject: [PATCH 10/23] Continued linting fixes --- .pre-commit-config.yaml | 37 +++--- pyproject.toml | 125 ++++++++++++--------- simvue/api/__init__.py | 4 +- simvue/api/objects/administrator/tenant.py | 8 +- simvue/api/objects/administrator/user.py | 8 +- simvue/api/objects/alert/metrics.py | 69 +++++++----- simvue/api/objects/alert/user.py | 46 +++++--- simvue/api/objects/base.py | 2 +- simvue/api/objects/events.py | 1 + simvue/api/objects/folder.py | 2 +- simvue/api/objects/grids.py | 88 ++++++++------- simvue/api/objects/storage/base.py | 2 + simvue/api/objects/storage/file.py | 21 ++++ simvue/api/objects/storage/s3.py | 10 +- simvue/api/request.py | 4 +- simvue/api/url.py | 18 ++- 16 files changed, 266 insertions(+), 179 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f8fb40ae..3eaef7e3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,45 +1,42 @@ -exclude: '^tests|^docs|^examples|^notebooks' +exclude: "^tests|^docs|^examples|^notebooks" ci: - autofix_prs: false - autoupdate_branch: '' - autoupdate_commit_msg: '[pre-commit.ci] pre-commit autoupdate' - autoupdate_schedule: weekly - skip: [] - submodules: false + autofix_prs: false + autoupdate_branch: "" + autoupdate_commit_msg: "[pre-commit.ci] pre-commit autoupdate" + autoupdate_schedule: weekly + skip: [] + submodules: false repos: - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v6.0.0 + - repo: builtin hooks: - id: check-toml - id: check-yaml - - id: pretty-format-json - args: [--autofix] - id: trailing-whitespace - language: python + language: system - id: check-merge-conflict - id: end-of-file-fixer - id: mixed-line-ending + args: [--fix=lf] - id: no-commit-to-branch args: [--branch, main, --branch, dev] - id: check-added-large-files - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.14 + rev: v0.15.0 hooks: - - id: ruff - args: [ --fix, --exit-non-zero-on-fix, "--ignore=C901" ] + - id: ruff-check + args: [--fix, --exit-non-zero-on-fix, "--ignore=C901"] - id: ruff-format - repo: https://github.com/conorfalvey/check_pdb_hook rev: 0.0.9 hooks: - id: check_pdb_hook pass_filenames: false - - repo: https://github.com/PyCQA/bandit.git rev: 1.9.3 hooks: - - id: bandit + - id: bandit args: [-lll, --recursive, clumper] - - repo: https://github.com/DetachHead/basedpyright-pre-commit-mirror - rev: 1.31.6 + - repo: https://github.com/DetachHead/basedpyright-prek-mirror + rev: 1.37.4 hooks: - - id: basedpyright + - id: basedpyright diff --git a/pyproject.toml b/pyproject.toml index f4d36087..58cb99d0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,37 +2,35 @@ name = "simvue" version = "2.3.1" description = "Simulation tracking and monitoring" -authors = [ - {name = "Simvue Development Team", email = "info@simvue.io"} -] +authors = [{ name = "Simvue Development Team", email = "info@simvue.io" }] license = "Apache v2" requires-python = ">=3.10,<3.15" readme = "README.md" classifiers = [ - "Development Status :: 5 - Production/Stable", - "Intended Audience :: Science/Research", - "License :: OSI Approved :: Apache Software License", - "Natural Language :: English", - "Operating System :: Unix", - "Operating System :: Microsoft :: Windows", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", - "Programming Language :: Python :: 3.14", - "Topic :: Scientific/Engineering", - "Topic :: System :: Monitoring", - "Topic :: Utilities", - "Typing :: Typed" + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: Apache Software License", + "Natural Language :: English", + "Operating System :: Unix", + "Operating System :: Microsoft :: Windows", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Topic :: Scientific/Engineering", + "Topic :: System :: Monitoring", + "Topic :: Utilities", + "Typing :: Typed", ] keywords = [ - "tracking", - "monitoring", - "metrics", - "alerting", - "metrics-gathering" + "tracking", + "monitoring", + "metrics", + "alerting", + "metrics-gathering", ] dependencies = [ "requests (>=2.32.3,<3.0.0)", @@ -102,26 +100,24 @@ pydocstyle.convention = "numpy" [tool.pytest.ini_options] addopts = "-p no:warnings --no-cov -n 0" -testpaths = [ - "tests" -] +testpaths = ["tests"] markers = [ - "eco: tests for emission metrics", - "client: tests of Simvue client", - "dispatch: test data dispatcher", - "run: test the simvue Run class", - "utilities: test simvue utilities module", - "scenario: test scenarios", - "executor: tests of executors", - "config: tests of simvue configuration", - "api: tests of RestAPI functionality", - "unix: tests for UNIX systems only", - "metadata: tests of metadata gathering functions", - "online: tests for online functionality", - "offline: tests for offline functionality", - "local: tests of functionality which do not involve a server or writing to an offline cache file", - "object_retrieval: tests relating to retrieval of objects from the server", - "object_removal: tests relating to removal of objects from the server", + "eco: tests for emission metrics", + "client: tests of Simvue client", + "dispatch: test data dispatcher", + "run: test the simvue Run class", + "utilities: test simvue utilities module", + "scenario: test scenarios", + "executor: tests of executors", + "config: tests of simvue configuration", + "api: tests of RestAPI functionality", + "unix: tests for UNIX systems only", + "metadata: tests of metadata gathering functions", + "online: tests for online functionality", + "offline: tests for offline functionality", + "local: tests of functionality which do not involve a server or writing to an offline cache file", + "object_retrieval: tests relating to retrieval of objects from the server", + "object_removal: tests relating to removal of objects from the server", ] [tool.interrogate] @@ -131,15 +127,40 @@ verbose = 1 exclude = ["docs", "tests"] [tool.basedpyright] -reportImplicitStringConcatenation = "none" -reportUnreachable = "none" -reportUnknownMemberType = "none" -reportUnknownVariableType = "none" -reportAny = "none" # ruff already does this -reportMissingTypeStubs = "none" -reportPrivateUsage = "none" # ruff already does this -reportImportCycles = "none" -reportIncompatibleMethodOverride = "none" +include = ["src", "examples"] +pythonVersion = "3.13" +typeCheckingMode = "off" +deprecateTypingAliases = true +reportAttributeAccessIssue = "error" +reportDuplicateImport = "error" +reportGeneralTypeIssues = "warning" +reportImplicitOverride = "error" +reportImportCycles = "error" +reportInconsistentOverload = "error" +reportIndexIssue = "error" +reportInvalidTypeForm = "error" +reportMissingTypeArgument = "error" +reportOperatorIssue = "error" +reportOptionalIterable = "error" +reportOptionalSubscript = "error" +reportPropertyTypeMismatch = "error" +reportRedeclaration = "warning" +reportUnusedClass = "error" +reportUnusedFunction = "error" +reportUnusedImport = "error" +reportUnusedVariable = "error" +reportConstantRedefinition = "error" +reportUnecessaryComparison = "error" +reportUnecessaryContains = "error" +reportUnecessaryCast = "error" +reportSelfClsParameterName = "error" +reportUndefinedVariable = "error" +reportUnboundVariable = "error" +reportUnhashable = "error" +reportUnusedExcept = "error" +reportUnusedExpression = "error" +reportUnreachable = "error" +reportUnusedParameter = "error" [tool.mypy] ignore_missing_imports = true diff --git a/simvue/api/__init__.py b/simvue/api/__init__.py index f56eb001..3a9b54b3 100644 --- a/simvue/api/__init__.py +++ b/simvue/api/__init__.py @@ -1,6 +1,4 @@ -""" -Simvue API -========== +"""Simvue API. Module contains methods for interacting with a Simvue server including accessing/updating objects. diff --git a/simvue/api/objects/administrator/tenant.py b/simvue/api/objects/administrator/tenant.py index f5fb908a..0c058626 100644 --- a/simvue/api/objects/administrator/tenant.py +++ b/simvue/api/objects/administrator/tenant.py @@ -148,7 +148,11 @@ def created(self) -> datetime.datetime | None: ------- datetime.datetime """ - _created: str | None = self._get_attribute("created") + _created: str | None = typing.cast("str | None", self._get_attribute("created")) return ( - datetime.datetime.strptime(_created, DATETIME_FORMAT) if _created else None + datetime.datetime.strptime(_created, DATETIME_FORMAT).astimezone( + datetime.UTC + ) + if _created + else None ) diff --git a/simvue/api/objects/administrator/user.py b/simvue/api/objects/administrator/user.py index 02481dde..7922b9af 100644 --- a/simvue/api/objects/administrator/user.py +++ b/simvue/api/objects/administrator/user.py @@ -255,7 +255,11 @@ def created(self) -> datetime.datetime | None: ------- datetime.datetime """ - _created: str | None = self._get_attribute("created") + _created: str | None = typing.cast("str | None", self._get_attribute("created")) return ( - datetime.datetime.strptime(_created, DATETIME_FORMAT) if _created else None + datetime.datetime.strptime(_created, DATETIME_FORMAT).astimezone( + datetime.UTC + ) + if _created + else None ) diff --git a/simvue/api/objects/alert/metrics.py b/simvue/api/objects/alert/metrics.py index 8570e381..2ca20361 100644 --- a/simvue/api/objects/alert/metrics.py +++ b/simvue/api/objects/alert/metrics.py @@ -10,9 +10,9 @@ import pydantic try: - from typing import Self + from typing import Self, override except ImportError: - from typing import Self + from typing import Self, override from simvue.api.objects.base import write_only from simvue.models import NAME_REGEX @@ -30,7 +30,7 @@ class MetricsThresholdAlert(AlertBase): """ def __init__(self, identifier: str | None = None, **kwargs: object) -> None: - """Connect to a local or remote threshold alert by identifier""" + """Connect to a local or remote threshold alert by identifier.""" self.alert = MetricThresholdAlertDefinition(self) super().__init__(identifier, **kwargs) self._local_only_args += [ @@ -41,13 +41,15 @@ def __init__(self, identifier: str | None = None, **kwargs: object) -> None: ] @classmethod + @override def get( cls, count: int | None = None, offset: int | None = None ) -> dict[str, typing.Any]: - """Retrieve only MetricsThresholdAlerts""" + """Retrieve only MetricsThresholdAlerts.""" raise NotImplementedError("Retrieve of only metric alerts is not yet supported") @classmethod + @override @pydantic.validate_call def new( cls, @@ -63,9 +65,9 @@ def new( frequency: pydantic.PositiveInt, enabled: bool = True, offline: bool = False, - **_, + **_: object, ) -> Self: - """Create a new metric threshold alert either locally or on the server + """Create a new metric threshold alert either locally or on the server. Note all arguments are keyword arguments. @@ -120,10 +122,10 @@ def new( class MetricsRangeAlert(AlertBase): - """Class for connecting to/creating a local or remotely defined metric range alert""" + """Class for connecting to/creating a local/remotely defined metric range alert.""" - def __init__(self, identifier: str | None = None, **kwargs) -> None: - """Connect to a local or remote threshold alert by identifier""" + def __init__(self, identifier: str | None = None, **kwargs: object) -> None: + """Connect to a local or remote threshold alert by identifier.""" self.alert = MetricRangeAlertDefinition(self) super().__init__(identifier, **kwargs) self._local_only_args += [ @@ -134,12 +136,14 @@ def __init__(self, identifier: str | None = None, **kwargs) -> None: "range_high", ] + @override def compare(self, other: "MetricsRangeAlert") -> bool: - """Compare two MetricRangeAlerts""" + """Compare two MetricRangeAlerts.""" return self.alert.compare(other) if super().compare(other) else False @classmethod @pydantic.validate_call + @override def new( cls, *, @@ -155,9 +159,9 @@ def new( frequency: pydantic.PositiveInt, enabled: bool = True, offline: bool = False, - **_, + **_: object, ) -> Self: - """Create a new metric range alert either locally or on the server + """Create a new metric range alert either locally or on the server. Note all arguments are keyword arguments. @@ -190,7 +194,8 @@ def new( """ if range_low >= range_high: - raise ValueError(f"Invalid arguments for range [{range_low}, {range_high}]") + _out_msg: str = f"Invalid arguments for range [{range_low}, {range_high}]" + raise ValueError(_out_msg) _alert_definition = { "rule": rule, @@ -217,14 +222,14 @@ def new( class MetricsAlertDefinition: - """General alert definition for a metric alert""" + """General alert definition for a metric alert.""" def __init__(self, alert: MetricsRangeAlert) -> None: - """Initialise definition with target alert""" + """Initialise definition with target alert.""" self._sv_obj = alert def compare(self, other: "MetricsAlertDefinition") -> bool: - """Compare a MetricsAlertDefinition with another""" + """Compare a MetricsAlertDefinition with another.""" return all( [ self.aggregation == other.aggregation, @@ -236,7 +241,7 @@ def compare(self, other: "MetricsAlertDefinition") -> bool: @property def aggregation(self) -> Aggregate: - """Retrieve the aggregation strategy for this alert""" + """Retrieve the aggregation strategy for this alert.""" if not (_aggregation := self._sv_obj.get_alert().get("aggregation")): raise RuntimeError( "Expected key 'aggregation' in alert definition retrieval" @@ -245,14 +250,14 @@ def aggregation(self) -> Aggregate: @property def rule(self) -> Rule: - """Retrieve the rule for this alert""" + """Retrieve the rule for this alert.""" if not (_rule := self._sv_obj.get_alert().get("rule")): raise RuntimeError("Expected key 'rule' in alert definition retrieval") return _rule @property def window(self) -> int: - """Retrieve the aggregation window for this alert""" + """Retrieve the aggregation window for this alert.""" if not (_window := self._sv_obj.get_alert().get("window")): raise RuntimeError("Expected key 'window' in alert definition retrieval") return _window @@ -260,7 +265,7 @@ def window(self) -> int: @property @staging_check def frequency(self) -> int: - """Retrieve the monitor frequency for this alert""" + """Retrieve the monitor frequency for this alert.""" try: return self._sv_obj.get_alert()["frequency"] except KeyError as e: @@ -272,16 +277,17 @@ def frequency(self) -> int: @write_only @pydantic.validate_call def frequency(self, frequency: int) -> None: - """Set the monitor frequency for this alert""" + """Set the monitor frequency for this alert.""" _alert = self._sv_obj.get_alert() | {"frequency": frequency} - self._sv_obj._staging["alert"] = _alert + self._sv_obj._staging["alert"] = _alert # noqa: SLF001 class MetricThresholdAlertDefinition(MetricsAlertDefinition): - """Alert definition for metric threshold alerts""" + """Alert definition for metric threshold alerts.""" - def compare(self, other: "MetricThresholdAlertDefinition") -> bool: - """Compare this MetricThresholdAlertDefinition with another""" + @override + def compare(self, other: "MetricThresholdAlertDefinition | object") -> bool: + """Compare this MetricThresholdAlertDefinition with another.""" if not isinstance(other, MetricThresholdAlertDefinition): return False @@ -289,17 +295,18 @@ def compare(self, other: "MetricThresholdAlertDefinition") -> bool: @property def threshold(self) -> float: - """Retrieve the threshold value for this alert""" + """Retrieve the threshold value for this alert.""" if not (threshold_l := self._sv_obj.get_alert().get("threshold")): raise RuntimeError("Expected key 'threshold' in alert definition retrieval") return threshold_l class MetricRangeAlertDefinition(MetricsAlertDefinition): - """Alert definition for metric range alerts""" + """Alert definition for metric range alerts.""" - def compare(self, other: "MetricRangeAlertDefinition") -> bool: - """Compare a MetricRangeAlertDefinition with another""" + @override + def compare(self, other: "MetricRangeAlertDefinition | object") -> bool: + """Compare a MetricRangeAlertDefinition with another.""" if not isinstance(other, MetricRangeAlertDefinition): return False @@ -313,14 +320,14 @@ def compare(self, other: "MetricRangeAlertDefinition") -> bool: @property def range_low(self) -> float: - """Retrieve the lower limit for metric range""" + """Retrieve the lower limit for metric range.""" if not (range_l := self._sv_obj.get_alert().get("range_low")): raise RuntimeError("Expected key 'range_low' in alert definition retrieval") return range_l @property def range_high(self) -> float: - """Retrieve upper limit for metric range""" + """Retrieve upper limit for metric range.""" if not (range_u := self._sv_obj.get_alert().get("range_high")): raise RuntimeError( "Expected key 'range_high' in alert definition retrieval" diff --git a/simvue/api/objects/alert/user.py b/simvue/api/objects/alert/user.py index 7fdcee43..1eced8f5 100644 --- a/simvue/api/objects/alert/user.py +++ b/simvue/api/objects/alert/user.py @@ -1,34 +1,42 @@ -""" -Simvue User Alert -================= +"""Simvue User Alert. Class for connecting with a local/remote user defined alert. """ -import pydantic import typing +from typing import cast + +import pydantic try: - from typing import Self + from typing import Self, override except ImportError: - from typing_extensions import Self + from typing import Self, override + import http -from simvue.api.request import get_json_from_response, put as sv_put -from .base import AlertBase +from simvue.api.request import get_json_from_response +from simvue.api.request import put as sv_put from simvue.models import NAME_REGEX +from .base import AlertBase + class UserAlert(AlertBase): - """Connect to/create a user defined alert either locally or on server""" + """Connect to/create a user defined alert either locally or on server.""" - def __init__(self, identifier: str | None = None, **kwargs) -> None: + def __init__( + self, identifier: str | None = None, **kwargs: object | dict[str, str | None] + ) -> None: super().__init__(identifier, **kwargs) - self._local_status: dict[str, str | None] = kwargs.pop("status", {}) + self._local_status: dict[str, str | None] = cast( + "dict[str, str | None]", kwargs.pop("status", {}) + ) @classmethod @pydantic.validate_call + @override def new( cls, *, @@ -37,9 +45,9 @@ def new( notification: typing.Literal["none", "email"], enabled: bool = True, offline: bool = False, - **_, + **_: object, ) -> Self: - """Create a new user-defined alert + """Create a new user-defined alert. Note all arguments are keyword arguments. @@ -70,27 +78,31 @@ def new( return _alert @classmethod + @override def get( cls, count: int | None = None, offset: int | None = None ) -> dict[str, typing.Any]: - """Return only UserAlerts""" + """Return only UserAlerts.""" raise NotImplementedError("Retrieve of only user alerts is not yet supported") + @override def get_status(self, run_id: str) -> typing.Literal["ok", "critical"] | None: - """Retrieve current alert status for the given run""" + """Retrieve current alert status for the given run.""" if self._offline: return self._staging.get("status", self._local_status).get(run_id) return super().get_status(run_id) + @override def on_reconnect(self, id_mapping: dict[str, str]) -> None: - """Set status update on reconnect""" + """Set status update on reconnect.""" for offline_id, status in self._staging.get("status", {}).items(): self.set_status(id_mapping.get(offline_id), status) @pydantic.validate_call + @override def set_status(self, run_id: str, status: typing.Literal["ok", "critical"]) -> None: - """Set the status of this alert for a given run""" + """Set the status of this alert for a given run.""" if self._offline: if "status" not in self._staging: self._staging["status"] = {} diff --git a/simvue/api/objects/base.py b/simvue/api/objects/base.py index 1b783c84..01142233 100644 --- a/simvue/api/objects/base.py +++ b/simvue/api/objects/base.py @@ -288,7 +288,7 @@ def __init__( self._user_config.headers if not self._offline else {} ) - self._params: dict[str, str] = {} + self._params: dict[str, str | bool | int | float] = {} self._staging: dict[str, typing.Any] = {} diff --git a/simvue/api/objects/events.py b/simvue/api/objects/events.py index f5ca054b..fe95177c 100644 --- a/simvue/api/objects/events.py +++ b/simvue/api/objects/events.py @@ -117,6 +117,7 @@ def _post_single( ) -> dict[str, typing.Any] | list[dict[str, typing.Any]]: return super()._post_single(is_json=False, data=data, **kwargs) + @override def _put(self, **_: object) -> dict[str, typing.Any]: raise NotImplementedError("Method 'put' is not available for type Events") diff --git a/simvue/api/objects/folder.py b/simvue/api/objects/folder.py index c313a0ea..47e40722 100644 --- a/simvue/api/objects/folder.py +++ b/simvue/api/objects/folder.py @@ -119,7 +119,7 @@ def tree(self) -> dict[str, object]: _paths = [folder.path.split("/") for _, folder in _folders] _paths = sorted(_paths, key=len) _out_dict: dict[str, object] = {} - _modifier = None + _modifier: dict[str, object] = {} for path in _paths: if len(path) <= _level: continue diff --git a/simvue/api/objects/grids.py b/simvue/api/objects/grids.py index 5fc1c1cf..9bd900a9 100644 --- a/simvue/api/objects/grids.py +++ b/simvue/api/objects/grids.py @@ -1,6 +1,4 @@ -""" -Simvue Server Grid -================== +"""Simvue Server Grid. Contains a class for remotely connecting to a Simvue grid, or defining a new grid given relevant arguments. @@ -8,44 +6,50 @@ """ import http -import msgpack -import numpy import typing +import msgpack +import numpy as np import pydantic -from simvue.api.url import URL -from simvue.models import GridMetricSet - - -from .base import SimvueObject, write_only from simvue.api.request import ( get as sv_get, - put as sv_put, - post as sv_post, +) +from simvue.api.request import ( get_json_from_response, ) +from simvue.api.request import ( + post as sv_post, +) +from simvue.api.request import ( + put as sv_put, +) +from simvue.api.url import URL +from simvue.models import GridMetricSet + +from .base import SimvueObject, write_only try: - from typing import Self + from typing import Self, override except ImportError: - from typing_extensions import Self + from typing import Self, override __all__ = ["Grid"] def check_ordered_array( - axis_ticks: list[list[float]] | numpy.ndarray, + axis_ticks: list[list[float]] | np.ndarray, ) -> list[list[float]]: - """Returns if array is ordered or reverse ordered.""" - if isinstance(axis_ticks, numpy.ndarray): + """Return if array is ordered or reverse ordered.""" + if isinstance(axis_ticks, np.ndarray): axis_ticks = axis_ticks.tolist() for i, _array in enumerate(axis_ticks): - _array = numpy.array(_array) - if not numpy.all(numpy.sort(_array) == _array) or numpy.all( - reversed(numpy.sort(_array)) == _array + _array = np.array(_array) + if not np.all(np.sort(_array) == _array) or np.all( + reversed(np.sort(_array)) == _array ): - raise ValueError(f"Axis {i} has unordered values.") + _out_msg: str = f"Axis {i} has unordered values." + raise ValueError(_out_msg) return axis_ticks @@ -60,7 +64,7 @@ def attach_metric_for_run(self, run_id: str, metric_name: str) -> None: self._staging.setdefault("runs", []) self._staging["runs"].append((run_id, metric_name)) super().commit() - return + return None _response = sv_put( url=f"{self.run_data_url(run_id)}", @@ -77,6 +81,7 @@ def attach_metric_for_run(self, run_id: str, metric_name: str) -> None: response=_response, ) + @override def on_reconnect(self, id_mapping: dict[str, str]) -> None: """Operations performed when this grid is switched from offline to online mode. @@ -93,19 +98,22 @@ def on_reconnect(self, id_mapping: dict[str, str]) -> None: for run_id, metric_name in _online_runs: try: self.attach_metric_for_run(run_id=run_id, metric_name=metric_name) - except KeyError: - raise RuntimeError("Failed to retrieve online run identifier.") + except KeyError as e: + raise RuntimeError("Failed to retrieve online run identifier.") from e @property def grid(self) -> list[list[float]]: + """Return the grid as list.""" return self._get_attribute("grid") @property def name(self) -> str: + """Retrieve the name.""" return self._get_attribute("name") @classmethod @pydantic.validate_call(config={"arbitrary_types_allowed": True}) + @override def new( cls, *, @@ -119,7 +127,7 @@ def new( ], labels: list[str], offline: bool = False, - **kwargs, + **kwargs: object, ) -> Self: """Create a new Grid on the Simvue server. @@ -140,12 +148,12 @@ def new( Metrics metrics object """ - if len(labels) != len(grid): - raise AssertionError( + _out_msg = ( "Length of argument 'labels' must match first " f"grid dimension {len(grid)}." ) + raise AssertionError(_out_msg) return Grid( grid=grid, @@ -162,13 +170,13 @@ def dimensions(self) -> tuple[int, int]: return len(self.grid) def run_data_url(self, run_id: str) -> URL: - """Returns the URL for grid data for a specific run.""" + """Return the URL for grid data for a specific run.""" return URL( f"{self._user_config.server.url}/runs/{run_id}/grids/{self._identifier}" ) def run_metric_url(self, run_id: str, metric_name: str) -> URL: - """Returns the URL for the values for a given run metric.""" + """Return the URL for the values for a given run metric.""" return URL( f"{self._user_config.server.url}/runs/{run_id}/metrics/{metric_name}/" ) @@ -176,8 +184,8 @@ def run_metric_url(self, run_id: str, metric_name: str) -> URL: @pydantic.validate_call def get_run_metric_values( self, *, run_id: str, metric_name: str, step: int - ) -> dict: - """Retrieve values for this grid from the server for a given run at a given step. + ) -> dict[str, str | float | list[object]]: + """Retrieve grid values from the server for a given run at a given step. Parameters ---------- @@ -189,12 +197,12 @@ def get_run_metric_values( time step to retrieve values for. Returns - ------ - dict[str, list[dict[str, float]] - dictionary containing values from this for the run at specified step. + ------- + dict[str, str | float | list[object]] + dictionary containing values from this Grid for the run at specified step. """ _response = sv_get( - url=f"{self.run_metric_values_url(run_id, metric_name) / 'values'}", + url=f"{self.run_metric_url(run_id, metric_name) / 'values'}", headers=self._headers, params={"step": step}, ) @@ -205,12 +213,14 @@ def get_run_metric_values( expected_type=dict, scenario=( f"Retrieving '{metric_name}' grid values " - f"for run '{self._run_id}' at step {step}", + f"for run '{run_id}' at step {step}", ), ) @pydantic.validate_call - def get_run_metric_span(self, *, run_id: str, metric_name: str) -> dict: + def get_run_metric_span( + self, *, run_id: str, metric_name: str + ) -> dict[str, int | float | str]: """Retrieve span for this grid from the server for a given run. Parameters @@ -339,7 +349,7 @@ def get( def commit(self) -> dict | None: if not (_run_staging := self._staging.pop("data", None)): - return + return None return self._log_values(_run_staging) def on_reconnect(self, id_mapping: dict[str, str]) -> None: @@ -370,7 +380,7 @@ def _log_values(self, metrics: list[GridMetricSet]) -> None: self._staging.setdefault("data", []) self._staging["data"] += metrics super().commit() - return + return None _response = sv_post( url=f"{self._user_config.server.url}/{self.run_grids_endpoint(self._run_id)}", diff --git a/simvue/api/objects/storage/base.py b/simvue/api/objects/storage/base.py index 53e69f66..937fb943 100644 --- a/simvue/api/objects/storage/base.py +++ b/simvue/api/objects/storage/base.py @@ -31,6 +31,7 @@ class StorageBase(SimvueObject): def __init__( self, + obj_type: str, identifier: str | None = None, *, _read_only: bool = False, @@ -42,6 +43,7 @@ def __init__( """Retrieve an alert from the Simvue server by identifier.""" self._label: str = "storage" self._endpoint: str = self._label + self.type: str = obj_type super().__init__( identifier, _read_only=_read_only, diff --git a/simvue/api/objects/storage/file.py b/simvue/api/objects/storage/file.py index dce15159..bd914f73 100644 --- a/simvue/api/objects/storage/file.py +++ b/simvue/api/objects/storage/file.py @@ -26,6 +26,27 @@ class FileStorage(StorageBase): """Class for defining/accessing a File based storage system on the server.""" + def __init__( + self, + identifier: str | None = None, + *, + _read_only: bool = False, + _offline: bool = False, + _user_agent: str | None = None, + _local: bool = False, + **kwargs: object, + ) -> None: + """Initialise an File storage instance attaching a configuration.""" + super().__init__( + identifier, + obj_type="file", + _read_only=_read_only, + _offline=_offline, + _user_agent=_user_agent, + _local=_local, + **kwargs, + ) + @classmethod @pydantic.validate_call @override diff --git a/simvue/api/objects/storage/s3.py b/simvue/api/objects/storage/s3.py index 3989fb3d..5df09244 100644 --- a/simvue/api/objects/storage/s3.py +++ b/simvue/api/objects/storage/s3.py @@ -43,6 +43,7 @@ def __init__( self.config: Config = Config(self) super().__init__( identifier, + obj_type="s3", _local=_local, _read_only=_read_only, _offline=_offline, @@ -212,11 +213,12 @@ def bucket(self) -> str: @write_only @pydantic.validate_call def bucket(self, bucket: str) -> None: - """Modify the bucket label for this storage""" - if self._sv_obj.type == "file": - raise ValueError( + """Modify the bucket label for this storage.""" + if self._sv_obj.type != "s3": + _out_msg = ( f"Cannot set attribute 'bucket' for storage type '{self._sv_obj.type}'" ) + raise ValueError(_out_msg) _config = self._sv_obj.get_config() | {"bucket": bucket} - self._sv_obj._staging["config"] = _config + self._sv_obj._staging["config"] = _config # noqa: SLF001 diff --git a/simvue/api/request.py b/simvue/api/request.py index 07852f87..51ef4621 100644 --- a/simvue/api/request.py +++ b/simvue/api/request.py @@ -5,6 +5,7 @@ to a JSON string """ +from collections.abc import Generator import copy import http import json as json_module @@ -285,7 +286,6 @@ def get_json_from_response( details = f"expected type '{expected_type.__name__}' but got '{type(json_response).__name__}'" else: return json_response - else: elif isinstance(json_response, dict): error_str += f" with status {_status_code}" details = (json_response or {}).get("detail") @@ -311,7 +311,7 @@ def get_paginated( offset: int | None = None, count: int | None = None, **params, -) -> typing.Generator[requests.Response]: +) -> Generator[requests.Response]: """Paginate results of a server query. Parameters diff --git a/simvue/api/url.py b/simvue/api/url.py index 9c52f6c1..c236890a 100644 --- a/simvue/api/url.py +++ b/simvue/api/url.py @@ -5,9 +5,10 @@ """ try: - from typing import Self + from typing import Self, override except ImportError: - from typing import Self + from typing import Self, override + import copy import urllib.parse @@ -36,14 +37,15 @@ def __truediv__(self, other: str) -> Self: _new /= other return _new + @override def __repr__(self) -> str: - """Representation of URL""" + """Representation of URL.""" _out_str = f"{self.__class__.__module__}.{self.__class__.__qualname__}" return f"{_out_str}(url={self.__str__()!r})" @pydantic.validate_call def __itruediv__(self, other: str) -> Self: - """Define URL extension through use of '/'""" + """Define URL extension through use of '/'.""" other = other.removeprefix("/") other = other.removesuffix("/") @@ -52,26 +54,32 @@ def __itruediv__(self, other: str) -> Self: @property def scheme(self) -> str: + """Return URL scheme.""" return self._scheme @property def path(self) -> str: + """Return URL path.""" return self._path @property def hostname(self) -> str | None: + """Return URL hostname.""" return self._host @property def fragment(self) -> str: + """Return URL fragment.""" return self._fragment @property def port(self) -> int | None: + """Return URL port.""" return self._port + @override def __str__(self) -> str: - """Construct string form of the URL""" + """Construct string form of the URL.""" _out_str: str = "" if self.scheme: _out_str += f"{self.scheme}://" From 0b35cc22a1862a99c63028ca2852f15a3f88c471 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Zar=C4=99bski?= Date: Fri, 6 Feb 2026 16:14:01 +0000 Subject: [PATCH 11/23] =?UTF-8?q?=F0=9F=90=9B=20Fixed=20grid=20metric=20an?= =?UTF-8?q?d=20span=20retrieval?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- simvue/api/objects/grids.py | 26 +++++++++++++++++--------- tests/unit/test_grids.py | 2 ++ 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/simvue/api/objects/grids.py b/simvue/api/objects/grids.py index 9bd900a9..4e2c0fe6 100644 --- a/simvue/api/objects/grids.py +++ b/simvue/api/objects/grids.py @@ -231,28 +231,36 @@ def get_run_metric_span( metric to retrieve span information for. Returns - ------ + ------- dict[str, list[dict[str, float]] dictionary containing span from this for the run at specified step. """ _response = sv_get( - url=f"{self.run_metric_values_url(run_id, metric_name) / 'span'}", + url=f"{self.run_metric_url(run_id, metric_name) / 'span'}", headers=self._headers, ) - return get_json_from_response( - response=_response, - expected_status=[http.HTTPStatus.OK], - expected_type=dict, - scenario=f"Retrieving grid span for run '{run_id}'", + _response = typing.cast( + "list[dict[str, int | float | str]]", + get_json_from_response( + response=_response, + expected_status=[http.HTTPStatus.OK], + expected_type=list, + scenario=f"Retrieving grid span for run '{run_id}'", + ), ) + if not _response: + raise RuntimeError("Failed to retrieve span, no data.") + + return _response[0] + @classmethod def get( cls, *_, **__, - ) -> typing.Generator[tuple[str, Self | None], None, None]: + ) -> typing.Generator[tuple[str, Self | None]]: raise NotImplementedError @@ -319,7 +327,7 @@ def get( step: pydantic.NonNegativeInt, spans: bool = False, **kwargs, - ) -> typing.Generator[dict[str, dict[str, list[dict[str, float]]]], None, None]: + ) -> typing.Generator[dict[str, dict[str, list[dict[str, float]]]]]: """Retrieve tensor-metrics from the server for a given set of runs. Parameters diff --git a/tests/unit/test_grids.py b/tests/unit/test_grids.py index 87a745d9..8bfbdb09 100644 --- a/tests/unit/test_grids.py +++ b/tests/unit/test_grids.py @@ -132,6 +132,8 @@ def test_grid_metrics_creation_online() -> None: time.sleep(1) # Online metrics assert list(GridMetrics.get(runs=[_run.id], metrics=["A"], step=_step)) + assert _grid.get_run_metric_values(run_id=_run.id, metric_name="A", step=_step) + assert _grid.get_run_metric_span(run_id=_run.id, metric_name="A").get("max_timestamp") _run.delete() _folder.delete(recursive=True, delete_runs=True, runs_only=False) From 52b76f0bc5e65fcacff2f199062aa9e2b8317b44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Zar=C4=99bski?= Date: Fri, 6 Feb 2026 17:10:40 +0000 Subject: [PATCH 12/23] More linting fixes --- simvue/api/objects/grids.py | 38 +++++++++------ simvue/api/objects/metrics.py | 31 +++++++----- simvue/api/objects/run.py | 85 ++++++++++++++++++++------------- simvue/api/objects/stats.py | 89 +++++++++++++++++++++++------------ simvue/api/objects/tag.py | 33 ++++++------- simvue/api/request.py | 17 ++++--- simvue/bin/sender.py | 10 ++-- simvue/config/__init__.py | 8 ++-- simvue/config/files.py | 4 +- simvue/config/parameters.py | 33 +++++++++---- simvue/config/user.py | 60 +++++++++++++++-------- simvue/utilities.py | 2 +- 12 files changed, 259 insertions(+), 151 deletions(-) diff --git a/simvue/api/objects/grids.py b/simvue/api/objects/grids.py index 4e2c0fe6..b88a9dcc 100644 --- a/simvue/api/objects/grids.py +++ b/simvue/api/objects/grids.py @@ -7,6 +7,7 @@ import http import typing +from collections.abc import Generator import msgpack import numpy as np @@ -256,20 +257,22 @@ def get_run_metric_span( return _response[0] @classmethod + @override def get( cls, - *_, - **__, - ) -> typing.Generator[tuple[str, Self | None]]: + *_: object, + **__: object, + ) -> Generator[tuple[str, Self | None]]: raise NotImplementedError class GridMetrics(SimvueObject): def __init__( self, + *, _read_only: bool = True, _local: bool = False, - **kwargs, + **kwargs: object, ) -> None: """Initialise a GridMetrics object instance.""" self._label = "grid_metric" @@ -279,10 +282,11 @@ def __init__( @staticmethod def run_grids_endpoint(run: str | None = None) -> URL: - """Returns the URL for grids for a specific run.""" + """Return the URL for grids for a specific run.""" return URL(f"runs/{run}/metrics/") - def _get_attribute(self, attribute: str, *default) -> typing.Any: + @override + def _get_attribute(self, attribute: str, *default: object) -> typing.Any: return super()._get_attribute( attribute, *default, @@ -291,8 +295,14 @@ def _get_attribute(self, attribute: str, *default) -> typing.Any: @classmethod @pydantic.validate_call + @override def new( - cls, *, run: str, data: list[GridMetricSet], offline: bool = False, **kwargs + cls, + *, + run: str, + data: list[GridMetricSet], + offline: bool = False, + **kwargs: object, ) -> Self: """Create a new GridMetrics object for n-dimensional metric submission. @@ -319,15 +329,15 @@ def new( @classmethod @pydantic.validate_call + @override def get( cls, *, runs: list[str], metrics: list[str], step: pydantic.NonNegativeInt, - spans: bool = False, - **kwargs, - ) -> typing.Generator[dict[str, dict[str, list[dict[str, float]]]]]: + **kwargs: object, + ) -> Generator[dict[str, dict[str, list[dict[str, float]]]]]: """Retrieve tensor-metrics from the server for a given set of runs. Parameters @@ -338,8 +348,6 @@ def get( list of metrics to retrieve. step : int the timestep to retrieve grid metrics for - spans : bool, optional - return spans informations Yields ------ @@ -355,13 +363,15 @@ def get( count=None, ) - def commit(self) -> dict | None: + @override + def commit(self) -> dict[str, object] | None: if not (_run_staging := self._staging.pop("data", None)): return None return self._log_values(_run_staging) + @override def on_reconnect(self, id_mapping: dict[str, str]) -> None: - """Operations performed when this grid metrics object is switched from offline to online mode. + """Operations performed when object is switched from offline to online mode. Parameters ---------- diff --git a/simvue/api/objects/metrics.py b/simvue/api/objects/metrics.py index 878c8541..22d6d96c 100644 --- a/simvue/api/objects/metrics.py +++ b/simvue/api/objects/metrics.py @@ -8,6 +8,7 @@ import http import json import typing +from collections.abc import Generator import pydantic @@ -18,9 +19,9 @@ from .base import SimvueObject try: - from typing import Self + from typing import Self, override except ImportError: - from typing import Self + from typing import Self, override __all__ = ["Metrics"] @@ -43,8 +44,9 @@ def __init__( @classmethod @pydantic.validate_call + @override def new( - cls, *, run: str, metrics: list[MetricSet], offline: bool = False, **kwargs + cls, *, run: str, metrics: list[MetricSet], offline: bool = False, **_: object ) -> Self: """Create a new Metrics entry on the Simvue server. @@ -71,6 +73,7 @@ def new( @classmethod @pydantic.validate_call + @override def get( cls, metrics: list[str], @@ -79,8 +82,8 @@ def get( *, count: pydantic.PositiveInt | None = None, offset: pydantic.PositiveInt | None = None, - **kwargs, - ) -> typing.Generator[dict[str, dict[str, list[dict[str, float]]]]]: + **kwargs: object, + ) -> Generator[dict[str, dict[str, list[dict[str, float]]]]]: """Retrieve metrics from the server for a given set of runs. Parameters @@ -115,7 +118,7 @@ def get( @pydantic.validate_call def span(self, run_ids: list[str]) -> dict[str, int | float]: - """Returns the metrics span for the given runs""" + """Return the metrics span for the given runs.""" _url = self.base_url / "span" _response = sv_get(url=f"{_url}", headers=self._headers, json=run_ids) return get_json_from_response( @@ -126,7 +129,7 @@ def span(self, run_ids: list[str]) -> dict[str, int | float]: @pydantic.validate_call def names(self, run_ids: list[str]) -> list[str]: - """Returns the metric names for the given runs""" + """Return the metric names for the given runs.""" _url = self.base_url / "names" _response = sv_get( url=f"{_url}", headers=self._headers, params={"runs": json.dumps(run_ids)} @@ -138,15 +141,18 @@ def names(self, run_ids: list[str]) -> list[str]: expected_type=list, ) - def _post_single(self, **kwargs) -> dict[str, typing.Any]: + @override + def _post_single(self, **kwargs: object) -> dict[str, typing.Any]: return super()._post_single(is_json=False, **kwargs) - def delete(self, **kwargs) -> dict[str, typing.Any]: - """Metrics cannot be deleted""" + @override + def delete(self, **_: object) -> dict[str, typing.Any]: + """Metrics cannot be deleted.""" raise NotImplementedError("Cannot delete metric set") - def on_reconnect(self, id_mapping: dict[str, str]): - """Action performed when mode switched from offline to online. + @override + def on_reconnect(self, id_mapping: dict[str, str]) -> None: + """Execute when mode switched from offline to online. Parameters ---------- @@ -157,6 +163,7 @@ def on_reconnect(self, id_mapping: dict[str, str]): if online_run_id := id_mapping.get(self._staging["run"]): self._staging["run"] = online_run_id + @override def to_dict(self) -> dict[str, typing.Any]: """Convert metrics object to dictionary. diff --git a/simvue/api/objects/run.py b/simvue/api/objects/run.py index d3f44c3d..b6b4aba7 100644 --- a/simvue/api/objects/run.py +++ b/simvue/api/objects/run.py @@ -1,6 +1,4 @@ -""" -Simvue Runs -=========== +"""Simvue Runs. Contains a class for remotely connecting to Simvue runs, or defining a new run given relevant arguments. @@ -16,10 +14,12 @@ import pydantic +from simvue.utilities import simvue_timestamp + try: - from typing import Self + from typing import Self, override except ImportError: - from typing import Self + from typing import Self, override from simvue.api.request import ( get as sv_get, @@ -47,7 +47,8 @@ "lost", "failed", "completed", "terminated", "running", "created" ] -# Need to use this inside of Generator typing to fix bug present in Python 3.10 - see issue #745 +# Need to use this inside of Generator typing to +# fix bug present in Python 3.10 - see issue #745 T = typing.TypeVar("T", bound="Run") __all__ = ["Run"] @@ -64,7 +65,8 @@ def check_column(cls, column: str) -> str: and not column.startswith("metadata.") and column not in ("created", "started", "endtime", "modified") ): - raise ValueError(f"Invalid sort column for runs '{column}'") + _out_msg: str = f"Invalid sort column for runs '{column}'" + raise ValueError(_out_msg) return column @@ -84,7 +86,7 @@ class RunBatchArgs(ObjectBatchArgs): class Run(SimvueObject): """Class for directly interacting with/creating runs on the server.""" - def __init__(self, identifier: str | None = None, **kwargs) -> None: + def __init__(self, identifier: str | None = None, **kwargs: object) -> None: """Initialise a Run. If an identifier is provided a connection will be made to the @@ -103,6 +105,7 @@ def __init__(self, identifier: str | None = None, **kwargs) -> None: @classmethod @pydantic.validate_call + @override def new( cls, *, @@ -112,7 +115,7 @@ def new( "terminated", "created", "failed", "completed", "lost", "running" ] = "created", offline: bool = False, - **kwargs, + **kwargs: object, ) -> Self: """Create a new Run on the Simvue server. @@ -151,6 +154,7 @@ def new( @classmethod @pydantic.validate_call + @override def batch_create( cls, entries: Iterable[RunBatchArgs], @@ -169,7 +173,8 @@ def batch_create( visibility : VisibilityBatchArgs | None, optional specify visibility options for these runs, default is None. folder : str, optional - override folder specification for these runs to be a single folder, default None. + override folder specification for these runs to be a single folder, + default None. metadata : dict[str, int | str | float | bool], optional override metadata specification for these runs, default None. @@ -248,12 +253,12 @@ def status(self, status: Status) -> None: @property @staging_check - def ttl(self) -> int: + def ttl(self) -> int | None: """Set/retrieve the retention period for this run. Returns ------- - int + int | None """ return self._get_attribute("ttl") @@ -306,12 +311,12 @@ def user(self) -> str: @property @staging_check - def description(self) -> str: + def description(self) -> str | None: """Set/retrieve the description for this run. Returns ------- - str + str | None """ return self._get_attribute("description") @@ -363,7 +368,9 @@ def notifications(self) -> typing.Literal["none", "all", "error", "lost"]: ------- "none" | "all" | "error" | "lost" """ - return self._get_attribute("notifications")["state"] + return typing.cast("dict[str, object]", self._get_attribute("notifications"))[ + "state" + ] @notifications.setter @write_only @@ -389,12 +396,13 @@ def alerts(self) -> list[str]: @classmethod @pydantic.validate_call + @override def get( cls, count: pydantic.PositiveInt | None = None, offset: pydantic.NonNegativeInt | None = None, sorting: list[RunSort] | None = None, - **kwargs, + **kwargs: object, ) -> Generator[tuple[str, Self]]: """Get runs from the server. @@ -440,9 +448,10 @@ def get_alert_details(self) -> Generator[dict[str, object]]: """ if self._offline: raise RuntimeError( - "Cannot get alert details from an offline run - use .alerts to access a list of IDs instead" + "Cannot get alert details from an offline run - " + "use member 'alerts' to access a list of IDs instead" ) - for alert in self._get_attribute("alerts"): + for alert in typing.cast("dict[str, object]", self._get_attribute("alerts")): yield alert["alert"] @property @@ -456,7 +465,11 @@ def created(self) -> datetime.datetime | None: """ _created: str | None = self._get_attribute("created") return ( - datetime.datetime.strptime(_created, DATETIME_FORMAT) if _created else None + datetime.datetime.strptime(_created, DATETIME_FORMAT).astimezone( + datetime.UTC + ) + if _created + else None ) @created.setter @@ -468,7 +481,7 @@ def created(self, created: datetime.datetime) -> None: @property @staging_check def runtime(self) -> datetime.datetime | None: - """Retrieve execution time for the run""" + """Retrieve execution time for the run.""" _runtime: str | None = self._get_attribute("runtime") return time.strptime(_runtime, "%H:%M:%S.%f") if _runtime else None @@ -523,7 +536,7 @@ def endtime(self, endtime: datetime.datetime) -> None: @property def metrics( self, - ) -> typing.Generator[tuple[str, dict[str, int | float | bool]]]: + ) -> Generator[tuple[str, dict[str, int | float | bool]]]: """Retrieve metrics for this run from the server. Yields @@ -535,12 +548,14 @@ def metrics( ------- Generator[tuple[str, dict[str, int | float | bool]] """ - yield from self._get_attribute("metrics").items() + yield from typing.cast( + "dict[str, dict[str, object]]", self._get_attribute("metrics") + ).items() @property def events( self, - ) -> typing.Generator[tuple[str, dict[str, typing.Any]]]: + ) -> Generator[tuple[str, dict[str, typing.Any]]]: """Returns events information for this run from the server. Yields @@ -552,7 +567,9 @@ def events( ------- Generator[tuple[str, dict[str, Any]] """ - yield from self._get_attribute("events").items() + yield from typing.cast( + "dict[str, dict[str, object]]", self._get_attribute("events") + ).items() @write_only def send_heartbeat(self) -> dict[str, typing.Any] | None: @@ -616,10 +633,13 @@ def abort_trigger(self) -> bool: return False _response = sv_get(f"{self._abort_url}", headers=self._headers) - _json_response = get_json_from_response( - response=_response, - expected_status=[http.HTTPStatus.OK], - scenario=f"Retrieving abort status for run '{self.id}'", + _json_response = typing.cast( + "dict[str, object]", + get_json_from_response( + response=_response, + expected_status=[http.HTTPStatus.OK], + scenario=f"Retrieving abort status for run '{self.id}'", + ), ) return _json_response.get("status", False) @@ -693,17 +713,18 @@ def abort(self, reason: str) -> dict[str, typing.Any]: response=_response, ) + @override def on_reconnect(self, id_mapping: dict[str, str]) -> None: - """Executed when a run switches from offline to online mode. + """Execute when a run switches from offline to online mode. Parameters ---------- id_mapping: dict[str, str] A mapping from offline identifier to online identifier. """ - online_alert_ids: list[str] = list( - set(id_mapping.get(_id) for _id in self._staging.get("alerts", [])) + online_alert_ids: Generator[str] = ( + id_mapping.get(_id) for _id in self._staging.get("alerts", []) ) - if not all(online_alert_ids): + if not all(set(online_alert_ids)): raise KeyError("Could not find alert ID in offline to online ID mapping.") self._staging["alerts"] = online_alert_ids diff --git a/simvue/api/objects/stats.py b/simvue/api/objects/stats.py index 2b4499bf..1b5b0216 100644 --- a/simvue/api/objects/stats.py +++ b/simvue/api/objects/stats.py @@ -1,6 +1,4 @@ -""" -Simvue Stats -============ +"""Simvue Stats. Statistics accessible to the current user. @@ -9,13 +7,21 @@ import http import typing -from pydantic import BaseModel import pydantic +from pydantic import BaseModel -from .base import SimvueObject -from simvue.api.request import get as sv_get, get_json_from_response +from simvue.api.request import get as sv_get +from simvue.api.request import get_json_from_response from simvue.api.url import URL +from .base import SimvueObject + +try: + from typing import override +except ImportError: + from typing import override + + __all__ = ["Stats"] @@ -48,7 +54,8 @@ def __init__(self) -> None: self._identifier = "" @classmethod - def new(cls, **kwargs) -> None: + @override + def new(cls, **_: object) -> None: """Creation of multiple stats objects is not logical here. Raises @@ -58,8 +65,9 @@ def new(cls, **kwargs) -> None: raise AttributeError("Creation of statistics objects is not supported") @classmethod - def delete(cls, **kwargs) -> None: - """Deletion of stats object is not logical here. + @override + def delete(cls, **_: object) -> None: + """Delete action of stats object is not logical here. Raises ------ @@ -67,6 +75,7 @@ def delete(cls, **kwargs) -> None: """ raise AttributeError("Deletion of statistics is not supported") + @override def read_only(self) -> None: """Statistics can only be read-only. @@ -76,6 +85,7 @@ def read_only(self) -> None: """ raise NotImplementedError("Statistics are not modifiable.") + @override def id(self) -> None: """No unique indentifier for statistics retrieval. @@ -83,15 +93,16 @@ def id(self) -> None: ------- None """ - return None + return - def on_reconnect(self, **_) -> None: + @override + def on_reconnect(self, **_: object) -> None: """No offline to online reconnect functionality for statistics.""" - pass @classmethod - def get(cls, **kwargs) -> None: - """Retrieval of multiple stats object is not logical here. + @override + def get(cls, **_: object) -> None: + """Retrieve action for multiple stats object is not logical here. Raises ------ @@ -102,8 +113,9 @@ def get(cls, **kwargs) -> None: ) @classmethod - def ids(cls, **kwargs) -> None: - """Retrieval of identifiers is not logical here. + @override + def ids(cls, **_: object) -> None: + """Retrieve action for identifiers is not logical here. Raises ------ @@ -128,19 +140,22 @@ def whoami(self) -> dict[str, str]: ) def _get_run_stats(self) -> dict[str, int]: - """Retrieve the run statistics""" + """Retrieve the run statistics.""" return self._get_attribute("runs") + @override def _get_local_staged(self) -> dict[str, typing.Any]: - """No staging for stats so returns empty dict""" + """No staging for stats so returns empty dict.""" return {} + @override def _get_visibility(self) -> dict[str, bool | list[str]]: - """Visibility does not apply here""" + """Visibility does not apply here.""" return {} + @override def to_dict(self) -> dict[str, typing.Any]: - """Dictionary form of current user statistics. + """Return dictionary form of current user statistics. Returns ------- @@ -150,6 +165,18 @@ def to_dict(self) -> dict[str, typing.Any]: return {"runs": self._get_run_stats()} def admin_stats(self, *, tenant: str | None = None) -> dict[str, dict[str, int]]: + """Return adminstrator level stats. + + Parameters + ---------- + tenant : str, optional + specify tenant for stats retrieval. + + Returns + ------- + dict[str, dict[str, int]] + statistics of server objects for each user. + """ return { name: UserStatistics(**entry) for name, entry in self._get( @@ -157,41 +184,41 @@ def admin_stats(self, *, tenant: str | None = None) -> dict[str, dict[str, int]] ).items() } + @override def commit(self) -> None: - """Does nothing, no data sendable to server.""" - pass + """Do nothing, no data sendable to server.""" class RunStatistics: - """Interface to the run section of statistics output""" + """Interface to the run section of statistics output.""" def __init__(self, sv_obj: Stats) -> None: self._sv_obj = sv_obj @property def created(self) -> int: - """Number of created runs""" - if (_created := self._sv_obj._get_run_stats().get("created")) is None: + """Number of created runs.""" + if (_created := self._sv_obj._get_run_stats().get("created")) is None: # noqa: SLF001 raise RuntimeError("Expected key 'created' in run statistics retrieval") return _created @property def running(self) -> int: - """Number of running runs""" - if (_running := self._sv_obj._get_run_stats().get("running")) is None: + """Number of running runs.""" + if (_running := self._sv_obj._get_run_stats().get("running")) is None: # noqa: SLF001 raise RuntimeError("Expected key 'running' in run statistics retrieval") return _running @property def completed(self) -> int: - """Number of completed runs""" - if (_completed := self._sv_obj._get_run_stats().get("running")) is None: + """Number of completed runs.""" + if (_completed := self._sv_obj._get_run_stats().get("running")) is None: # noqa: SLF001 raise RuntimeError("Expected key 'completed' in run statistics retrieval") return _completed @property def data(self) -> int: - """Data count""" - if (_data := self._sv_obj._get_run_stats().get("running")) is None: + """Data count.""" + if (_data := self._sv_obj._get_run_stats().get("running")) is None: # noqa: SLF001 raise RuntimeError("Expected key 'data' in run statistics retrieval") return _data diff --git a/simvue/api/objects/tag.py b/simvue/api/objects/tag.py index c95a3586..1dff9740 100644 --- a/simvue/api/objects/tag.py +++ b/simvue/api/objects/tag.py @@ -1,6 +1,4 @@ -""" -Simvue Server Tag -================= +"""Simvue Server Tag. Contains a class for remotely connecting to a Simvue Tag, or defining a new tag given relevant arguments. @@ -19,9 +17,9 @@ from simvue.models import DATETIME_FORMAT try: - from typing import Self + from typing import Self, override except ImportError: - from typing import Self + from typing import Self, override __all__ = ["Tag"] @@ -31,16 +29,18 @@ class TagSort(Sort): @classmethod def check_column(cls, column: str) -> str: if column and column not in ("created", "name"): - raise ValueError(f"Invalid sort column for tags '{column}") + _out_msg: str = f"Invalid sort column for tags '{column}" + raise ValueError(_out_msg) return column class Tag(SimvueObject): - """Class for creation/interaction with tag object on server""" + """Class for creation/interaction with tag object on server.""" @classmethod @pydantic.validate_call - def new(cls, *, name: str, offline: bool = False, **kwargs) -> Self: + @override + def new(cls, *, name: str, offline: bool = False, **kwargs: object) -> Self: """Create a new Tag on the Simvue server. Parameters @@ -61,45 +61,45 @@ def new(cls, *, name: str, offline: bool = False, **kwargs) -> Self: @property @staging_check def name(self) -> str: - """Retrieve the tag name""" + """Retrieve the tag name.""" return self._get_attribute("name") @name.setter @write_only @pydantic.validate_call def name(self, name: str) -> None: - """Set the tag name""" + """Set the tag name.""" self._staging["name"] = name @property @staging_check - def colour(self) -> pyd_color.RGBA: - """Retrieve the tag colour""" + def colour(self) -> pyd_color.Color: + """Retrieve the tag colour.""" return pyd_color.parse_str(self._get_attribute("colour")) @colour.setter @write_only @pydantic.validate_call def colour(self, colour: pyd_color.Color) -> None: - """Set the tag colour""" + """Set the tag colour.""" self._staging["colour"] = colour.as_hex() @property @staging_check def description(self) -> str: - """Get description for this tag""" + """Get description for this tag.""" return self._get_attribute("description") @description.setter @write_only @pydantic.validate_call def description(self, description: str) -> None: - """Set the description for this tag""" + """Set the description for this tag.""" self._staging["description"] = description @property def created(self) -> datetime.datetime | None: - """Retrieve created datetime for the run""" + """Retrieve created datetime for the run.""" _created: str | None = self._get_attribute("created") return ( datetime.datetime.strptime(_created, DATETIME_FORMAT).replace( @@ -111,6 +111,7 @@ def created(self) -> datetime.datetime | None: @classmethod @pydantic.validate_call + @override def get( cls, *, diff --git a/simvue/api/request.py b/simvue/api/request.py index 51ef4621..fe5d0692 100644 --- a/simvue/api/request.py +++ b/simvue/api/request.py @@ -5,12 +5,12 @@ to a JSON string """ -from collections.abc import Generator import copy import http import json as json_module import logging import typing +from collections.abc import Generator import requests from tenacity import ( @@ -283,7 +283,10 @@ def get_json_from_response( if json_response is None: details = f"could not request JSON response: {decode_error}" elif not isinstance(json_response, expected_type): - details = f"expected type '{expected_type.__name__}' but got '{type(json_response).__name__}'" + details = ( + f"expected type '{expected_type.__name__}' but got" + f"'{type(json_response).__name__}'" + ) else: return json_response elif isinstance(json_response, dict): @@ -310,7 +313,7 @@ def get_paginated( json: dict[str, typing.Any] | None = None, offset: int | None = None, count: int | None = None, - **params, + **params: object, ) -> Generator[requests.Response]: """Paginate results of a server query. @@ -354,7 +357,9 @@ def get_paginated( _response.json().get("count", 0) < _offset ): break - except json_module.JSONDecodeError: - raise RuntimeError( - f"[{_response.status_code}] Failed to retrieve content from server: {_response.text}" + except json_module.JSONDecodeError as e: + _out_msg: str = ( + f"[{_response.status_code}] Failed to retrieve content " + f"from server: {_response.text}" ) + raise RuntimeError(_out_msg) from e diff --git a/simvue/bin/sender.py b/simvue/bin/sender.py index 330e2b20..4a17f07e 100644 --- a/simvue/bin/sender.py +++ b/simvue/bin/sender.py @@ -2,10 +2,10 @@ import logging import pathlib -import click -from simvue.sender import Sender, UPLOAD_ORDER, UploadItem +import click +from simvue.sender import UPLOAD_ORDER, Sender, UploadItem _logger = logging.getLogger(__name__) _logger.setLevel(logging.INFO) @@ -26,7 +26,8 @@ type=int, required=False, default=10, - help="The number of objects of a given type above which items will be sent to the server in parallel, by default 10", + help="The number of objects of a given type above which items will be sent " + "to the server in parallel, by default 10", ) @click.option( "-o", @@ -57,6 +58,7 @@ def run( threading_threshold: int, max_workers: int, ) -> None: + """Execute sender for upload of local items to server.""" try: _logger.info("Starting Simvue Sender") _sender = Sender( @@ -68,4 +70,4 @@ def run( _sender.upload(objects_to_upload) except Exception as err: _logger.critical("Exception running sender: %s", str(err)) - raise click.Abort + raise click.Abort from err diff --git a/simvue/config/__init__.py b/simvue/config/__init__.py index 28232a84..49d7ff94 100644 --- a/simvue/config/__init__.py +++ b/simvue/config/__init__.py @@ -1,7 +1,9 @@ -""" -Simvue Configuration -==================== +"""Simvue Configuration. This module contains definitions for the Simvue configuration options """ + +from .user import SimvueConfiguration + +__all__ = ["SimvueConfiguration"] diff --git a/simvue/config/files.py b/simvue/config/files.py index f00bc6dc..15d78875 100644 --- a/simvue/config/files.py +++ b/simvue/config/files.py @@ -1,6 +1,4 @@ -""" -Simvue Config File Lists -======================== +"""Simvue Config File Lists. Contains lists of valid Simvue configuration file names. diff --git a/simvue/config/parameters.py b/simvue/config/parameters.py index fb7614fb..8b717e97 100644 --- a/simvue/config/parameters.py +++ b/simvue/config/parameters.py @@ -1,6 +1,4 @@ -""" -Simvue Configuration File Models -================================ +"""Simvue Configuration File Models. Pydantic models for elements of the Simvue configuration file @@ -19,10 +17,12 @@ from simvue.config.files import DEFAULT_OFFLINE_DIRECTORY from simvue.utilities import get_expiry -logger = logging.getLogger(__file__) +logger = logging.getLogger(__name__) class ServerSpecifications(pydantic.BaseModel): + """Specify server configurations.""" + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( extra="forbid", strict=True, @@ -32,14 +32,17 @@ class ServerSpecifications(pydantic.BaseModel): @pydantic.field_validator("url") @classmethod - def url_to_api_url(cls, v: typing.Any) -> str | None: + def url_to_api_url(cls, v: object) -> str | None: + """Convert URL to server API endpoint.""" if f"{v}".endswith("/api"): return f"{v}" _url = URL(f"{v}") / "api" return f"{_url}" @pydantic.field_validator("token") - def check_token(cls, v: typing.Any) -> str | None: + @classmethod + def check_token(cls, v: pydantic.SecretStr) -> str | None: + """Confirm valid server token.""" value = v.get_secret_value() if not (expiry := get_expiry(value)): raise AssertionError("Failed to parse Simvue token - invalid token form") @@ -49,26 +52,36 @@ def check_token(cls, v: typing.Any) -> str | None: class OfflineSpecifications(pydantic.BaseModel): + """Specify offline mode configurations.""" + cache: pathlib.Path = pathlib.Path(DEFAULT_OFFLINE_DIRECTORY) @pydantic.field_validator("cache") @classmethod def check_valid_cache_path(cls, cache: pathlib.Path) -> pathlib.Path: + """Check cache path is valid.""" if not cache.parent.exists(): - raise FileNotFoundError(f"No such directory '{cache.parent}'.") + _out_msg: str = f"No such directory '{cache.parent}'." + raise FileNotFoundError(_out_msg) if not cache.parent.is_dir(): - raise FileNotFoundError(f"'{cache.parent}' is not a directory.") + _out_msg = f"'{cache.parent}' is not a directory." + raise FileNotFoundError(_out_msg) if not os.access(cache.parent, os.W_OK): - raise AssertionError(f"'{cache.parent}' is not a writable location.") + _out_msg = f"'{cache.parent}' is not a writable location." + raise AssertionError(_out_msg) return cache class MetricsSpecifications(pydantic.BaseModel): + """Specify metric configurations.""" + system_metrics_interval: pydantic.PositiveInt | None = -1 enable_emission_metrics: bool = False class DefaultRunSpecifications(pydantic.BaseModel): + """Specify run default configurations.""" + name: str | None = None description: str | None = None tags: list[str] | None = None @@ -79,4 +92,6 @@ class DefaultRunSpecifications(pydantic.BaseModel): class ClientGeneralOptions(pydantic.BaseModel): + """Specify client options.""" + debug: bool = False diff --git a/simvue/config/user.py b/simvue/config/user.py index e0bac161..8ca43e9a 100644 --- a/simvue/config/user.py +++ b/simvue/config/user.py @@ -1,6 +1,4 @@ -""" -Simvue Configuration File Model -=============================== +"""Simvue Configuration File Model. Pydantic model for the Simvue TOML configuration file @@ -15,6 +13,7 @@ from collections.abc import Mapping import pydantic +from requests import Response import semver import toml @@ -51,8 +50,12 @@ SIMVUE_SERVER_UPPER_CONSTRAINT: semver.Version | None = semver.Version.parse("2.0.0") SIMVUE_SERVER_LOWER_CONSTRAINT: semver.Version | None = semver.Version.parse("1.1.0") +__all__ = ["SimvueConfiguration"] + class SimvueConfiguration(pydantic.BaseModel): + """Configure session.""" + # Hide values as they contain token and URL model_config = pydantic.ConfigDict( hide_input_in_errors=True, @@ -96,7 +99,8 @@ def _load_pyproject_configs(cls) -> Mapping[str, object] | None: ] ): raise RuntimeError( - "Provision of Simvue URL, Token or offline directory in pyproject.toml is not allowed." + "Provision of Simvue URL, Token or offline directory " + "in pyproject.toml is not allowed." ) return _simvue_setup @@ -113,22 +117,25 @@ def _check_server( "Authorization": f"Bearer {token}", "User-Agent": f"Simvue Python client {__version__}", } - try: - _url = URL(url) / "version" - _response = sv_get(f"{_url}", headers) - if _response.status_code != http.HTTPStatus.OK or not ( - _version_str := _response.json().get("version") + def _retrieve_version_str(response: Response) -> str: + if response.status_code != http.HTTPStatus.OK or not ( + _version_str := response.json().get("version") ): raise AssertionError - if _response.status_code == http.HTTPStatus.UNAUTHORIZED: + if response.status_code == http.HTTPStatus.UNAUTHORIZED: raise AssertionError("Unauthorised token") + return _version_str + + try: + _url = URL(url) / "version" + _response = sv_get(f"{_url}", headers) + _version_str = _retrieve_version_str(_response) except Exception as err: - raise AssertionError( - f"Exception retrieving server version:\n {err!s}" - ) from err + _out_msg = f"Exception retrieving server version:\n {err!s}" + raise AssertionError(_out_msg) from err _version = semver.Version.parse(_version_str) @@ -136,23 +143,36 @@ def _check_server( SIMVUE_SERVER_UPPER_CONSTRAINT and _version >= SIMVUE_SERVER_UPPER_CONSTRAINT ): - raise AssertionError( - f"Python API v{_version_str} is not compatible with Simvue server versions " + _out_msg = ( + f"Python API v{_version_str} is not compatible " + "with Simvue server versions " f">= {SIMVUE_SERVER_UPPER_CONSTRAINT}" ) + raise AssertionError(_out_msg) + if SIMVUE_SERVER_LOWER_CONSTRAINT and _version < SIMVUE_SERVER_LOWER_CONSTRAINT: - raise AssertionError( - f"Python API v{_version_str} is not compatible with Simvue server versions " + _out_msg = ( + f"Python API v{_version_str} is not compatible " + "with Simvue server versions " f"< {SIMVUE_SERVER_LOWER_CONSTRAINT}" ) + raise AssertionError(_out_msg) @pydantic.validate_call def write(self, out_directory: pydantic.DirectoryPath) -> None: + """Write configuration to file. + + Parameters + ---------- + output_directory : pathlib.path + location to write configuration file. + """ with out_directory.joinpath(CONFIG_FILE_NAMES[0]).open("w") as out_f: toml.dump(self.model_dump(), out_f) @pydantic.model_validator(mode="after") def check_valid_server(self) -> Self: + """Check if the server is a valid URL.""" if os.environ.get("SIMVUE_NO_SERVER_CHECK"): return self @@ -248,7 +268,7 @@ def fetch( @classmethod @functools.lru_cache def config_file(cls) -> pathlib.Path: - """Returns the path of top level configuration file used for the session""" + """Return the path of top level configuration file used for the session.""" _config_file: pathlib.Path | None = sv_util.find_first_instance_of_file( CONFIG_FILE_NAMES, check_user_space=True ) @@ -258,8 +278,8 @@ def config_file(cls) -> pathlib.Path: CONFIG_INI_FILE_NAMES, check_user_space=True ): raise RuntimeError( - "Simvue INI configuration file format has been deprecated in simvue>=1.2, " - "please use TOML file" + "Simvue INI configuration file format has been deprecated " + "in simvue>=1.2, please use TOML file" ) if not _config_file: diff --git a/simvue/utilities.py b/simvue/utilities.py index 8ffd9290..f830a42a 100644 --- a/simvue/utilities.py +++ b/simvue/utilities.py @@ -29,7 +29,7 @@ def find_first_instance_of_file( file_names: list[str] | str, check_user_space: bool = True ) -> pathlib.Path | None: - """Traverses a file hierarchy from bottom upwards to find file + """Traverses a file hierarchy from bottom upwards to find file. Returns the first instance of 'file_names' found when moving upward from the current directory. From 70959abb83f22ab0edd3dc93559e4029ffaac883 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Zar=C4=99bski?= Date: Thu, 24 Sep 2026 08:44:32 +0100 Subject: [PATCH 13/23] Fixed all linting errors --- .pre-commit-config.yaml | 42 - README.md | 51 +- poetry.lock | 2 +- prek.toml | 33 +- pyproject.toml | 25 +- simvue/api/objects/alert/base.py | 10 +- simvue/api/objects/alert/events.py | 7 +- simvue/api/objects/alert/fetch.py | 9 +- simvue/api/objects/alert/metrics.py | 6 + simvue/api/objects/artifact/fetch.py | 4 +- simvue/api/objects/artifact/file.py | 7 +- simvue/api/objects/artifact/object.py | 4 +- simvue/api/objects/base.py | 92 +- simvue/api/objects/events.py | 6 +- simvue/api/objects/folder.py | 14 +- simvue/api/objects/grids.py | 20 +- simvue/api/objects/metrics.py | 3 +- simvue/api/objects/run.py | 10 +- simvue/api/objects/stats.py | 10 +- simvue/api/objects/storage/base.py | 9 +- simvue/api/objects/storage/fetch.py | 2 +- simvue/api/objects/storage/file.py | 2 + simvue/api/objects/storage/s3.py | 8 +- simvue/api/request.py | 84 +- simvue/api/url.py | 4 +- simvue/bin/sender.py | 2 +- simvue/client.py | 42 +- simvue/config/parameters.py | 1 + simvue/config/user.py | 55 +- simvue/converters.py | 41 +- simvue/dispatch/base.py | 2 +- simvue/dispatch/direct.py | 12 + simvue/dispatch/dispatcher.py | 2 +- simvue/dispatch/queued.py | 11 +- simvue/eco/api_client.py | 11 +- simvue/eco/emissions_monitor.py | 18 +- simvue/exception.py | 8 +- simvue/executor.py | 19 +- simvue/handler.py | 2 +- simvue/metadata.py | 35 +- simvue/metrics.py | 6 +- simvue/models.py | 3 +- simvue/pynvml.py | 2 + simvue/run.py | 26 +- simvue/sender/actions.py | 3 +- simvue/sender/base.py | 6 +- simvue/serialization.py | 30 +- simvue/system.py | 9 +- simvue/utilities.py | 22 +- simvue/version.py | 3 +- uv.lock | 1661 ++++++++++++++----------- 51 files changed, 1382 insertions(+), 1114 deletions(-) delete mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index 75a81435..00000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,42 +0,0 @@ -exclude: "^tests|^docs|^examples|^notebooks" -ci: - autofix_prs: false - autoupdate_branch: "" - autoupdate_commit_msg: "[pre-commit.ci] pre-commit autoupdate" - autoupdate_schedule: weekly - skip: [] - submodules: false -repos: - - repo: builtin - hooks: - - id: check-toml - - id: check-yaml - - id: trailing-whitespace - language: system - - id: check-merge-conflict - - id: end-of-file-fixer - - id: mixed-line-ending - args: [--fix=lf] - - id: no-commit-to-branch - args: [--branch, main, --branch, dev] - - id: check-added-large-files - - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.10 - hooks: - - id: ruff-check - args: [--fix, --exit-non-zero-on-fix, "--ignore=C901"] - - id: ruff-format - - repo: https://github.com/conorfalvey/check_pdb_hook - rev: 0.0.9 - hooks: - - id: check_pdb_hook - pass_filenames: false - - repo: https://github.com/PyCQA/bandit.git - rev: 1.9.4 - hooks: - - id: bandit - args: [-lll, --recursive, clumper] - - repo: https://github.com/DetachHead/basedpyright-prek-mirror - rev: 1.37.4 - hooks: - - id: basedpyright diff --git a/README.md b/README.md index b75d6736..39181443 100644 --- a/README.md +++ b/README.md @@ -44,56 +44,61 @@ from simvue import Run ... if __name__ == "__main__": - ... # Using a context manager means that the status will be set to completed automatically, # and also means that if the code exits with an exception this will be reported to Simvue with Run() as run: - # Specify a run name, metadata (dict), tags (list), description, folder - run.init('example-run-name', - {'learning_rate': 0.001, 'training_steps': 2000, 'batch_size': 32}, # Metadaata - ['tensorflow'], # Tags - 'This is a test.', # Description - '/Project-A/part1') # Folder full path + run.init( + "example-run-name", + { + "learning_rate": 0.001, + "training_steps": 2000, + "batch_size": 32, + }, # Metadaata + ["tensorflow"], # Tags + "This is a test.", # Description + "/Project-A/part1", + ) # Folder full path # Set folder details if necessary - run.set_folder_details('/Project-A/part1', # Folder full path - metadata={}, # Metadata - tags=['tensorflow'], # Tags - description='This is part 1 of a test') # Description + run.set_folder_details( + "/Project-A/part1", # Folder full path + metadata={}, # Metadata + tags=["tensorflow"], # Tags + description="This is part 1 of a test", + ) # Description # Upload the code - run.save_file('training.py', 'code') + run.save_file("training.py", "code") # Upload an input file - run.save_file('params.in', 'input') + run.save_file("params.in", "input") # Add an alert (the alert definition will be created if necessary) run.create_metric_threshold_alert( - name='loss-too-high', # Name - rule='is above', # Rule - metric='loss', # Metric - frequency=1, # Frequency - window=1, # Window - threshold=10, # Threshold - notification='email' # Notification type + name="loss-too-high", # Name + rule="is above", # Rule + metric="loss", # Metric + frequency=1, # Frequency + window=1, # Window + threshold=10, # Threshold + notification="email", # Notification type ) ... while not converged: - ... # Send metrics inside main application loop - run.log_metrics({'loss': 0.5, 'density': 34.4}) + run.log_metrics({"loss": 0.5, "density": 34.4}) ... # Upload an output file - run.save_file('output.cdf', 'output') + run.save_file("output.cdf", "output") # If we weren't using a context manager we'd need to end the run # run.close() diff --git a/poetry.lock b/poetry.lock index c75b10f0..d4b80634 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3032,4 +3032,4 @@ plot = ["matplotlib", "plotly"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<3.15" -content-hash = "6e313946685e97cb853ea060baa2bd52e61e7b45d67269d8b8ed80369c4fcf44" +content-hash = "49cb62d64368313661e6fceec273dfd2ccb3d9aee2e0fdd4f43ddbeae32774c5" diff --git a/prek.toml b/prek.toml index 7d9dc3de..0db5fefe 100644 --- a/prek.toml +++ b/prek.toml @@ -34,10 +34,11 @@ id = "check-added-large-files" [[repos]] repo = "https://github.com/astral-sh/ruff-pre-commit" -rev = "v0.15.14" +rev = "v0.16.8" + [[repos.hooks]] id = "ruff-check" -args = ["--fix", "--exit-non-zero-on-fix", "--ignore=C901"] +args = ["--fix", "--exit-non-zero-on-fix", "--ignore=C901", "--preview"] [[repos.hooks]] id = "ruff-format" @@ -71,3 +72,31 @@ id = "poetry-lock" [[repos.hooks]] id = "poetry-install" args = ["--all-extras", "--all-groups"] + +[[repos]] +repo = "https://github.com/GabDug/sync-pre-commit-lock" +rev = "v0.9.0" + +[[repos.hooks]] +id = "sync-pre-commit-uv" + +[[repos]] +repo = "https://github.com/astral-sh/uv-pre-commit" +rev = "0.11.32" + +[[repos.hooks]] +id = "uv-lock" + +[[repos]] +repo = "https://github.com/citation-file-format/cffconvert" +rev = "b6045d78aac9e02b039703b030588d54d53262ac" + +[[repos.hooks]] +id = "validate-cff" + +[[repos]] +repo = "https://github.com/DetachHead/basedpyright-prek-mirror" +rev = "1.39.9" + +[[repos.hooks]] +id = "basedpyright" diff --git a/pyproject.toml b/pyproject.toml index df9f0bcb..12377e5f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ dependencies = [ "pydantic (>=2.11,<3.0.0)", "tabulate (>=0.9.0,<0.11.0)", "pyjwt (>=2.13.0,<3.0.0)", - "pandas>=2.2.3,<3.0.6", + "pandas>=2.2.3,<3.0.7", "toml (>=0.10.2,<0.11.0)", "click (>=8.4.1,<9.0.0)", "gitpython (>=3.1.59,<4.0.0)", @@ -84,20 +84,28 @@ dev = [ "pytest-sugar>=1.1.1", "pytest-timeout>=2.4.0", "pytest-xdist>=3.8.0", - "ruff>=0.15.14", + "ruff>=0.16.8", "types-requests>=2.33.0.20260518", ] [tool.ruff] extend-exclude = ["tests", "examples", "notebooks", "simvue/pynvml.py"] -preview = true [tool.ruff.lint] -ignore = ["COM812", "PLR0904", "PLR6301", "D203", "D212", "PLW0717"] +ignore = [ + "COM812", + "PLR0904", + "PLR6301", + "D203", + "D212", + "D417", + "S404", + "S603", + "PLW0717", +] dummy-variable-rgx = "^_+$" extend-select = [ "C90", - "D417", "E", "F", "W", @@ -129,7 +137,6 @@ extend-select = [ "TC", "ARG", "I", - "D417", "ERA", "PL", "UP", @@ -185,7 +192,7 @@ reportAttributeAccessIssue = "error" reportDuplicateImport = "error" reportGeneralTypeIssues = "warning" reportImplicitOverride = "error" -reportImportCycles = "error" +reportImportCycles = "none" reportInconsistentOverload = "error" reportIndexIssue = "error" reportInvalidTypeForm = "error" @@ -199,10 +206,6 @@ reportUnusedClass = "error" reportUnusedFunction = "error" reportUnusedImport = "error" reportUnusedVariable = "error" -reportConstantRedefinition = "error" -reportUnecessaryComparison = "error" -reportUnecessaryContains = "error" -reportUnecessaryCast = "error" reportSelfClsParameterName = "error" reportUndefinedVariable = "error" reportUnboundVariable = "error" diff --git a/simvue/api/objects/alert/base.py b/simvue/api/objects/alert/base.py index 188b608a..b3d5659d 100644 --- a/simvue/api/objects/alert/base.py +++ b/simvue/api/objects/alert/base.py @@ -232,10 +232,14 @@ def abort(self, abort: bool) -> None: self._staging["abort"] = abort @pydantic.validate_call - def set_status(self, _: str, __: typing.Literal["ok", "critical"]) -> None: + def set_status( + self, message: str, err_type: typing.Literal["ok", "critical"] + ) -> None: """Set the status of this alert for a given run.""" - raise AttributeError( - f"Cannot update state for alert of type '{self.__class__.__name__}'", + _ = message + _ = err_type + _out_msg: str = ( + f"Cannot update state for alert of type '{self.__class__.__name__}'" ) raise AttributeError(_out_msg) diff --git a/simvue/api/objects/alert/events.py b/simvue/api/objects/alert/events.py index 6833430a..75e43d1e 100644 --- a/simvue/api/objects/alert/events.py +++ b/simvue/api/objects/alert/events.py @@ -169,14 +169,16 @@ def __eq__(self, other: "EventAlertDefinition") -> bool: ], ) + @override def __hash__(self) -> int: return hash(f"{self.frequency}+{self.pattern}") @property def pattern(self) -> str: """Retrieve the event log pattern monitored by this alert.""" + _alert: dict[str, str | float] = self._sv_obj.get_alert() try: - return _alert["pattern"] + return typing.cast("str", _alert["pattern"]) except KeyError as e: raise RuntimeError( "Expected key 'pattern' in alert definition retrieval", @@ -186,8 +188,9 @@ def pattern(self) -> str: @staging_check def frequency(self) -> int: """Retrieve the update frequency for this alert.""" + _alert: dict[str, str | float] = self._sv_obj.get_alert() try: - return _alert["frequency"] + return typing.cast("float", _alert["frequency"]) except KeyError as e: raise RuntimeError( "Expected key 'frequency' in alert definition retrieval", diff --git a/simvue/api/objects/alert/fetch.py b/simvue/api/objects/alert/fetch.py index 217d5f5e..223123ee 100644 --- a/simvue/api/objects/alert/fetch.py +++ b/simvue/api/objects/alert/fetch.py @@ -222,7 +222,10 @@ def get( ) elif ( _entry["source"] == "metrics" - and _entry.get("alert", {}).get("threshold") is not None + and typing.cast("dict[str, float]", _entry.get("alert", {})).get( + "threshold" + ) + is not None ): yield ( _id, @@ -262,8 +265,8 @@ def get( ), ) else: - raise RuntimeError( + _out_msg = ( f"Unrecognised alert source '{_entry['source']}' " - f"with data '{_entry}'", + + f"with data '{_entry}'", ) raise RuntimeError(_out_msg) diff --git a/simvue/api/objects/alert/metrics.py b/simvue/api/objects/alert/metrics.py index 201371a7..1bb68781 100644 --- a/simvue/api/objects/alert/metrics.py +++ b/simvue/api/objects/alert/metrics.py @@ -339,6 +339,7 @@ def __init__(self, alert: MetricsRangeAlert) -> None: """Initialise definition with target alert.""" self._sv_obj = alert + @override def __eq__(self, other: "MetricsAlertDefinition") -> bool: """Compare a MetricsAlertDefinition with another.""" return all( @@ -350,6 +351,7 @@ def __eq__(self, other: "MetricsAlertDefinition") -> bool: ], ) + @override def __hash__(self) -> int: """Return definition hash.""" return hash(f"{self.aggregation}-{self.frequency}-{self.rule}-{self.window}") @@ -400,6 +402,7 @@ def frequency(self, frequency: int) -> None: class MetricThresholdAlertDefinition(MetricsAlertDefinition): """Alert definition for metric threshold alerts.""" + @override def __eq__(self, other: "MetricThresholdAlertDefinition") -> bool: """Compare this MetricThresholdAlertDefinition with another.""" if not super().__eq__(other): @@ -407,6 +410,7 @@ def __eq__(self, other: "MetricThresholdAlertDefinition") -> bool: return self.threshold == other.threshold + @override def __hash__(self) -> int: return hash(f"{super().__hash__()}+{self.threshold}") @@ -421,6 +425,7 @@ def threshold(self) -> float: class MetricRangeAlertDefinition(MetricsAlertDefinition): """Alert definition for metric range alerts.""" + @override def __eq__(self, other: "MetricRangeAlertDefinition") -> bool: """Compare a MetricRangeAlertDefinition with another.""" if not super().__eq__(other): @@ -433,6 +438,7 @@ def __eq__(self, other: "MetricRangeAlertDefinition") -> bool: ], ) + @override def __hash__(self) -> int: return hash(f"{super().__hash__()}+{self.range_high}+{self.range_low}") diff --git a/simvue/api/objects/artifact/fetch.py b/simvue/api/objects/artifact/fetch.py index 037d13ba..8dbb3626 100644 --- a/simvue/api/objects/artifact/fetch.py +++ b/simvue/api/objects/artifact/fetch.py @@ -237,9 +237,9 @@ def from_name( ) if (_n_res := len(_json_response)) > 1 and not force_overwrite: - raise RuntimeError( + _out_msg: str = ( f"Expected single result for artifact '{name}' for run '{run_id}'" - f" but got {_n_res}", + + f" but got {_n_res}", ) raise RuntimeError(_out_msg) diff --git a/simvue/api/objects/artifact/file.py b/simvue/api/objects/artifact/file.py index 3344eedb..e558a2b0 100644 --- a/simvue/api/objects/artifact/file.py +++ b/simvue/api/objects/artifact/file.py @@ -153,10 +153,9 @@ def new( "artifacts", ) _local_staging_dir.mkdir(parents=True, exist_ok=True) - _local_staging_file = _local_staging_dir.joinpath( - f"{file_path.stem}_" - f"{datetime.datetime.now(tz=datetime.timezone.utc).strftime('%Y-%m-%d_%H-%M-%S_%f')[:-3]}.file", - ) + _time_stamp: str = datetime.datetime.now( + tz=datetime.timezone.utc + ).strftime("%Y-%m-%d_%H-%M-%S_%f") _local_staging_file = _local_staging_dir.joinpath( f"{file_path.stem}_{_time_stamp[:-3]}.file" ) diff --git a/simvue/api/objects/artifact/object.py b/simvue/api/objects/artifact/object.py index 1e47261d..68658ee2 100644 --- a/simvue/api/objects/artifact/object.py +++ b/simvue/api/objects/artifact/object.py @@ -137,9 +137,9 @@ def new( raise ValueError(_out_msg) if not (_data_type := _serialization[1]) and not allow_pickling: - raise ValueError( + _out_msg = ( f"Could not serialize object of type '{type(obj)}' " - "without pickling", + + "without pickling", ) raise ValueError(_out_msg) diff --git a/simvue/api/objects/base.py b/simvue/api/objects/base.py index 20497a6c..59e4082a 100644 --- a/simvue/api/objects/base.py +++ b/simvue/api/objects/base.py @@ -13,7 +13,9 @@ import types import typing import uuid -from collections.abc import Callable, Generator + +if typing.TYPE_CHECKING: + from collections.abc import Callable, Generator import msgpack import pydantic @@ -54,7 +56,9 @@ U = typing.TypeVar("U") -def staging_check(member_func: Callable) -> Callable: +def staging_check( + member_func: Callable[[typing.Any], typing.Any], +) -> Callable[[typing.Any], typing.Any]: """Decorator for checking if requested attribute has uncommitted changes.""" def _wrapper(self) -> typing.Any: @@ -84,13 +88,15 @@ def _wrapper(self) -> typing.Any: return _wrapper -def write_only(attribute_func: Callable) -> Callable: - def _wrapper(self: "SimvueObject", *args, **kwargs) -> typing.Any: +def write_only( + attribute_func: Callable[[typing.Any], typing.Any], +) -> Callable[[typing.Any], typing.Any]: + def _wrapper(self: SimvueObject, *args, **kwargs) -> typing.Any: _sv_obj = getattr(self, "_sv_obj", self) if _sv_obj.is_read_only: - raise AssertionError( + _out_msg: str = ( f"Cannot set property '{attribute_func.__name__}' " - f"on read-only object of type '{self.label()}'", + + f"on read-only object of type '{self.label()}'", ) raise AssertionError(_out_msg) return attribute_func(self, *args, **kwargs) @@ -107,7 +113,7 @@ def _wrapper(self: "SimvueObject", *args, **kwargs) -> typing.Any: class Visibility: """Interface for object visibility definition.""" - def __init__(self, sv_obj: "SimvueObject") -> None: + def __init__(self, sv_obj: SimvueObject) -> None: """Initialise visibility with target object.""" self._sv_obj = sv_obj @@ -354,8 +360,9 @@ def _get_attribute( return _attribute _out_msg: str = ( f"Could not retrieve attribute '{attribute}' " - f"for {self.label()} '{self._identifier}' from cached data", - ) from e + + f"for {self.label()} '{self._identifier}' from cached data", + ) + raise AttributeError(_out_msg) from e try: self._logger.debug( @@ -369,12 +376,14 @@ def _get_attribute( if self._offline: _out_msg = ( f"A value for attribute '{attribute}' has " - f"not yet been committed for offline {self.label()}" - f" '{self._identifier}'", - ) from e - raise RuntimeError( + + f"not yet been committed for offline {self.label()}" + + f" '{self._identifier}'", + ) + raise AttributeError(_out_msg) from e + _out_msg = ( f"Expected key '{attribute}' for {self.label()} '{self._identifier}'", - ) from e + ) + raise RuntimeError(_out_msg) from e def _clear_staging(self) -> None: self._staging = {} @@ -401,7 +410,7 @@ def get_visibility(self) -> dict[str, bool | list[str]]: @classmethod @abc.abstractmethod - def new(cls, **_: typing.Any) -> Self: # noqa: ANN401 + def new(cls, **_: typing.Any) -> Self: """Define new instance of this object.""" @classmethod @@ -454,13 +463,12 @@ def ids( **kwargs, ): if (_data := response.get("data")) is None: - raise RuntimeError( + _out_msg: str = ( f"Expected key 'data' for retrieval of {cls.__name__.lower()}s", ) raise RuntimeError(_out_msg) - for entry in _data: - _id = typing.cast("str", entry["id"]) - yield _id + for entry in typing.cast("dict[str, object]", _data): + yield typing.cast("str", entry["id"]) _count += 1 if count and _count > count: return @@ -513,7 +521,7 @@ def get( if count and _count > count: return if (_data := _response.get("data")) is None: - raise RuntimeError( + _out_msg: str = ( f"Expected key 'data' for retrieval of {cls.__name__.lower()}s", ) raise RuntimeError(_out_msg) @@ -522,7 +530,7 @@ def get( if not _data: return - for entry in _data: + for entry in typing.cast("list[dict[str, object]]", _data): _id = entry["id"] yield ( _id, @@ -571,11 +579,11 @@ def count( **kwargs, ): if not (_count := _data.get("count")): - raise RuntimeError( + _out_msg: str = ( f"Expected key 'count' for retrieval of {cls.__name__.lower()}s", ) raise RuntimeError(_out_msg) - _count_total += _count + _count_total += typing.cast("int", _count) return _count_total @classmethod @@ -588,7 +596,7 @@ def _get_all_objects( endpoint: str | None = None, expected_type: type = dict, **kwargs, - ) -> Generator[dict, None, None]: + ) -> Generator[dict[str, dict[str, object] | object], None, None]: _config: SimvueConfiguration = SimvueConfiguration.fetch( mode="online", server_url=server_url, @@ -626,7 +634,12 @@ def is_read_only(self) -> bool: """Returns if this instance is in read-only mode.""" return self._read_only - def read_only(self, is_read_only: bool, *, clear_staged: bool = True) -> None: # noqa: FBT001 + def read_only( + self, + is_read_only: bool, # # ruff: ignore[boolean-type-hint-positional-argument] + *, + clear_staged: bool = True, + ) -> None: """Set whether this object is in read only state. Parameters @@ -747,9 +760,9 @@ def _post_batch( ) if _response.status_code == http.HTTPStatus.FORBIDDEN: - raise RuntimeError( + _out_msg: str = ( "Forbidden: You do not have permission to " - f"create object of type '{self.label()}'", + + f"create object of type '{self.label()}'", ) raise RuntimeError(_out_msg) @@ -760,11 +773,10 @@ def _post_batch( expected_type=list, ) - if not len(batch_data) == (_n_created := len(_json_response)): - raise RuntimeError( - "Expected %s to be created, but only %s found.", - len(batch_data), - _n_created, + if len(batch_data) != (_n_created := len(_json_response)): + _out_msg = ( + f"Expected {len(batch_data)} to be created, but only " + + f"{_n_created} found.", ) raise RuntimeError(_out_msg) @@ -776,7 +788,7 @@ def _post_single( self, *, is_json: bool = True, - data: list | dict | None = None, + data: list[dict[str, object]] | dict[str, object] | None = None, **kwargs, ) -> dict[str, typing.Any] | list[dict[str, typing.Any]]: _data = kwargs if is_json else msgpack.packb(data or kwargs, use_bin_type=True) @@ -795,9 +807,9 @@ def _post_single( ) if _response.status_code == http.HTTPStatus.FORBIDDEN: - raise RuntimeError( + _out_msg: str = ( "Forbidden: You do not have permission to create " - f"object of type '{self.label()}'", + + f"object of type '{self.label()}'", ) raise RuntimeError(_out_msg) @@ -843,9 +855,9 @@ def _put(self, **kwargs: object) -> dict[str, object]: ) if _response.status_code == http.HTTPStatus.FORBIDDEN: - raise RuntimeError( + _out_msg: str = ( "Forbidden: You do not have permission to " - f"create object of type '{self.label()}'", + + f"create object of type '{self.label()}'", ) raise RuntimeError(_out_msg) @@ -925,10 +937,10 @@ def _get( self._logger.debug("'%s' retrieved successfully", self._identifier) if not isinstance(_json_response, dict): - raise TypeError( + _out_msg: str = ( "Expected dictionary from JSON response " - f"during {self.label()} retrieval " - f"but got '{type(_json_response)}'", + + f"during {self.label()} retrieval " + + f"but got '{type(_json_response)}'", ) raise TypeError(_out_msg) return _json_response diff --git a/simvue/api/objects/events.py b/simvue/api/objects/events.py index d2baca33..1f5981de 100644 --- a/simvue/api/objects/events.py +++ b/simvue/api/objects/events.py @@ -136,13 +136,13 @@ def get( **kwargs, ): if (_data := response.get("data")) is None: - raise RuntimeError( + _out_msg: str = ( "Expected key 'data' for retrieval of " - f"{_class_instance.__class__.__name__.lower()}s", + + f"{_class_instance.__class__.__name__.lower()}s", ) raise RuntimeError(_out_msg) - for _entry in _data: + for _entry in typing.cast("list[dict[str, object]]", _data): yield EventSet(**_entry) # pyright: ignore[reportArgumentType] _count += 1 if count and _count > count: diff --git a/simvue/api/objects/folder.py b/simvue/api/objects/folder.py index 998d683f..1afdc36f 100644 --- a/simvue/api/objects/folder.py +++ b/simvue/api/objects/folder.py @@ -169,7 +169,7 @@ def filter(cls) -> FoldersFilter: return _filter_instance @override - def commit(self) -> dict | list[dict] | None: + def commit(self) -> dict[str, object] | list[dict[str, object]] | None: if "starred" in self._staging: _star_run: bool = self._staging.pop("starred") self._set_favourite(starred=_star_run) @@ -186,7 +186,7 @@ def tree(self) -> dict[str, object]: """ _level: int = len(self.path.split("/")) - _folders = self.__class__.get( + _folders: Generator[tuple[str, Folder]] = self.__class__.get( filters=json.dumps([f"path contains {self.path}"]), ) _paths = [folder.path.split("/") for _, folder in _folders] @@ -248,14 +248,18 @@ def name(self, name: str) -> None: @property @staging_check - def metadata(self) -> dict[str, int | str | float | dict | None] | None: + def metadata( + self, + ) -> dict[str, int | str | float | dict[str, object] | None] | None: """Return the folder metadata.""" return self._get().get("metadata") @metadata.setter @write_only @pydantic.validate_call - def metadata(self, metadata: dict[str, int | float | str | dict | None]) -> None: + def metadata( + self, metadata: dict[str, int | float | str | dict[str, object] | None] + ) -> None: """Update the folder metadata.""" self._staging["metadata"] = metadata @@ -327,7 +331,7 @@ def created(self) -> datetime.datetime | None: else None ) - def _set_favourite(self, *, starred: bool) -> dict: + def _set_favourite(self, *, starred: bool) -> dict[str, object]: """Set starred status.""" _url = self.url / "starred" _response = sv_put( diff --git a/simvue/api/objects/grids.py b/simvue/api/objects/grids.py index 0c4efc4a..9995e4cb 100644 --- a/simvue/api/objects/grids.py +++ b/simvue/api/objects/grids.py @@ -130,8 +130,10 @@ def attach_metric_for_run(self, run_id: str, metric_name: str) -> None: return get_json_from_response( expected_status=[http.HTTPStatus.OK], scenario=( - f"Adding '{metric_name}' to grid " - f"'{self._identifier}' to run '{run_id}'", + ( + f"Adding '{metric_name}' to grid " + f"'{self._identifier}' to run '{run_id}'" + ), ), response=_response, ) @@ -224,8 +226,10 @@ def new( """ if len(labels) != len(grid): _out_msg = ( - "Length of argument 'labels' must match first " - f"grid dimension {len(grid)}.", + ( + "Length of argument 'labels' must match first " + f"grid dimension {len(grid)}." + ), ) raise AssertionError(_out_msg) @@ -264,7 +268,7 @@ def get_run_metric_values( run_id: str, metric_name: str, step: int, - ) -> dict: + ) -> dict[str, float | bool]: """Retrieve values for grid given run at a given step. Parameters @@ -294,8 +298,10 @@ def get_run_metric_values( expected_status=[http.HTTPStatus.OK], expected_type=dict, scenario=( - f"Retrieving '{metric_name}' grid values " - f"for run '{run_id}' at step {step}", + ( + f"Retrieving '{metric_name}' grid values " + f"for run '{run_id}' at step {step}" + ), ), ) diff --git a/simvue/api/objects/metrics.py b/simvue/api/objects/metrics.py index 420da4bb..d4cd4e86 100644 --- a/simvue/api/objects/metrics.py +++ b/simvue/api/objects/metrics.py @@ -171,7 +171,7 @@ def get( @pydantic.validate_call def span(self, run_ids: list[str]) -> dict[str, int | float]: """Returns the metrics span for the given runs""" - _url = self._base_url / "span" + _url = self.base_url / "span" _response = sv_get( url=f"{_url}", headers=self._headers, @@ -205,6 +205,7 @@ def names(self, run_ids: list[str]) -> list[str]: def _post_single(self, **kwargs: object) -> dict[str, typing.Any]: return super()._post_single(is_json=False, **kwargs) + @override def delete(self, **kwargs) -> dict[str, typing.Any]: """Metrics cannot be deleted.""" raise NotImplementedError("Cannot delete metric set") diff --git a/simvue/api/objects/run.py b/simvue/api/objects/run.py index 4dcdd6ca..eed17b4f 100644 --- a/simvue/api/objects/run.py +++ b/simvue/api/objects/run.py @@ -141,7 +141,7 @@ def filter(cls) -> RunsFilter: return _run_filter @override - def commit(self) -> dict | list[dict] | None: + def commit(self) -> dict[str, object] | list[dict[str, object]] | None: if "starred" in self._staging: _star_run: bool = self._staging.pop("starred") self._set_favourite(starred=_star_run) @@ -639,7 +639,7 @@ def star(self, is_true: bool = True) -> None: """Star this folder as a favourite.""" self._staging["starred"] = is_true - def _set_favourite(self, *, starred: bool) -> dict: + def _set_favourite(self, *, starred: bool) -> dict[str, object]: """Set starred status.""" _url = self.url / "starred" _response = sv_put( @@ -791,7 +791,7 @@ def abort_trigger(self) -> bool: headers=self._headers, verify=self._user_config.server_verify, ) - _json_response = get_json_from_response( + _json_response: dict[str, bool] = get_json_from_response( response=_response, expected_status=[http.HTTPStatus.OK], scenario=f"Retrieving abort status for run '{self.id}'", @@ -851,7 +851,7 @@ def grids(self) -> list[dict[str, str]]: ) @pydantic.validate_call - def abort(self, reason: str) -> dict[str, typing.Any]: + def abort(self, reason: str) -> dict[str, object]: """Trigger an abort for this run by notifying the server. Parameters @@ -861,7 +861,7 @@ def abort(self, reason: str) -> dict[str, typing.Any]: Returns ------- - dict[str, Any] + dict[str, object] server response after updating abort status. Raises diff --git a/simvue/api/objects/stats.py b/simvue/api/objects/stats.py index 6c047a66..dca4677f 100644 --- a/simvue/api/objects/stats.py +++ b/simvue/api/objects/stats.py @@ -16,6 +16,11 @@ from .base import SimvueObject +try: + from typing import override +except ImportError: + from typing_extensions import override + __all__ = ["Stats"] @@ -69,6 +74,7 @@ def __init__( # set it to empty string so not None self._identifier = "" + @override @classmethod def new(cls, **_) -> None: """Creation of multiple stats objects is not logical here. @@ -80,6 +86,7 @@ def new(cls, **_) -> None: """ raise AttributeError("Creation of statistics objects is not supported") + @override @classmethod def delete(cls, **_) -> None: """Deletion of stats object is not logical here. @@ -117,6 +124,7 @@ def id(self) -> None: def on_reconnect(self, **_: object) -> None: """No offline to online reconnect functionality for statistics.""" + @override @classmethod def get(cls, **_) -> None: """Retrieval of multiple stats object is not logical here. @@ -130,6 +138,7 @@ def get(cls, **_) -> None: "Retrieval of multiple of statistics objects is not supported", ) + @override @classmethod def ids(cls, **_) -> None: """Retrieval of identifiers is not logical here. @@ -169,7 +178,6 @@ def _get_local_staged(self) -> dict[str, typing.Any]: """No staging for stats so returns empty dict.""" return {} - @override def _get_visibility(self) -> dict[str, bool | list[str]]: """Visibility does not apply here.""" return {} diff --git a/simvue/api/objects/storage/base.py b/simvue/api/objects/storage/base.py index c5656708..ec789dff 100644 --- a/simvue/api/objects/storage/base.py +++ b/simvue/api/objects/storage/base.py @@ -1,5 +1,4 @@ """Simvue Storage Base. -=================== Contains general definitions for Simvue Storage objects. """ @@ -29,6 +28,7 @@ class StorageBase(SimvueObject): _label: str = "storage" _endpoint: str = "storage" + _type: str @override def __init__( @@ -47,6 +47,7 @@ def __init__( **kwargs, ) + @override @classmethod def new( cls, @@ -57,6 +58,12 @@ def new( ) -> Self: """Create a new instance of a storage type.""" + @property + @classmethod + def type(cls) -> str: + """Return storage type.""" + return cls._type + @property @staging_check def name(self) -> str: diff --git a/simvue/api/objects/storage/fetch.py b/simvue/api/objects/storage/fetch.py index 7ea81636..4971ae1c 100644 --- a/simvue/api/objects/storage/fetch.py +++ b/simvue/api/objects/storage/fetch.py @@ -127,7 +127,7 @@ def get( ) _label: str = _class_instance.__class__.__name__.lower() _label = _label.replace("base", "") - _json_response = get_json_from_response( + _json_response: list[dict[str, object]] = get_json_from_response( response=_response, expected_status=[http.HTTPStatus.OK], scenario=f"Retrieval of {_label}s", diff --git a/simvue/api/objects/storage/file.py b/simvue/api/objects/storage/file.py index 0c96255e..efcdd657 100644 --- a/simvue/api/objects/storage/file.py +++ b/simvue/api/objects/storage/file.py @@ -25,6 +25,8 @@ class FileStorage(StorageBase): """ + _type: str = "file" + @override def __init__( self, diff --git a/simvue/api/objects/storage/s3.py b/simvue/api/objects/storage/s3.py index cda57896..7f4a1e5b 100644 --- a/simvue/api/objects/storage/s3.py +++ b/simvue/api/objects/storage/s3.py @@ -28,6 +28,8 @@ class S3Storage(StorageBase): """ + _type: str = "s3" + @override def __init__( self, @@ -174,7 +176,7 @@ def get_config(self) -> dict[str, typing.Any]: """Retrieve configuration.""" try: _config: dict[str, float | str | None] = typing.cast( - "dict[str, float | None | str]", self._get_attribute("config") + "dict[str, float | str | None]", self._get_attribute("config") ) except AttributeError: return {} @@ -251,8 +253,8 @@ def bucket(self) -> str: @pydantic.validate_call def bucket(self, bucket: str) -> None: """Modify the bucket label for this storage.""" - if self._sv_obj.type == "file": - raise ValueError( + if self._sv_obj.type != "s3": + _out_msg: str = ( f"Cannot set attribute 'bucket' for storage type '{self._sv_obj.type}'", ) raise ValueError(_out_msg) diff --git a/simvue/api/request.py b/simvue/api/request.py index 61608b09..cc67713b 100644 --- a/simvue/api/request.py +++ b/simvue/api/request.py @@ -10,7 +10,7 @@ import json as json_module import logging import typing -from collections.abc import Generator +from collections.abc import Callable, Generator import requests from tenacity import ( @@ -23,6 +23,14 @@ from simvue.utilities import parse_validation_response + +class Seekable(typing.Protocol): + def seek(self, offset: int, whence: int = 0) -> int: ... + + +FileValue = Seekable | tuple[str, Seekable] + + DEFAULT_API_TIMEOUT = 10 RETRY_MULTIPLIER = 1 RETRY_MIN = 4 @@ -49,13 +57,23 @@ class RetryableHTTPError(Exception): def _rewind_request_streams(retry_state: RetryCallState) -> None: """Rewind file-like request bodies before retrying.""" - files = retry_state.kwargs.get("files") or {} - streams = (*files.values(), retry_state.kwargs.get("data")) + _kwargs = typing.cast("dict[str, typing.Any]", retry_state.kwargs) + _files = typing.cast("dict[str, FileValue]", _kwargs.get("files") or {}) + _file_values: list[FileValue] = list(_files.values()) - for value in streams: - stream = value[1] if isinstance(value, tuple) else value - if callable(seek := getattr(stream, "seek", None)): - seek(0) + if _data := typing.cast("FileValue", _kwargs.get("data")): + _file_values.append(_data) + + for value in _file_values: + if isinstance(value, tuple): + _stream, *_ = value + else: + _stream = value + _seek = typing.cast( + "Callable[[int], object] | None", getattr(_stream, "seek", None) + ) + if _seek: + _ = _seek(0) @retry( @@ -77,7 +95,6 @@ def post( headers: dict[str, str], params: dict[str, str], data: object, - *, is_json: bool = True, timeout: int | None = None, verify: str | bool = True, @@ -112,26 +129,28 @@ def post( """ if is_json: - data_sent: str | dict[str, typing.Any] | object = json_module.dumps(data) + _data_sent: str | dict[str, typing.Any] | object = json_module.dumps(data) headers = set_json_header(headers) else: - data_sent = data + _data_sent = data response = requests.post( url, headers=headers, params=params, - data=data_sent, + data=_data_sent, # pyright: ignore[reportArgumentType] timeout=timeout, files=files, verify=verify, ) if response.status_code == http.HTTPStatus.UNPROCESSABLE_ENTITY: - _parsed_response = parse_validation_response(response.json()) - raise ValueError( + _parsed_response = parse_validation_response( + typing.cast("dict[str, typing.Any]", response.json()) + ) + _out_msg = ( f"Validation error for '{url}' " - f"[{response.status_code}]:\n{_parsed_response}", + + f"[{response.status_code}]:\n{_parsed_response}" ) raise ValueError(_out_msg) @@ -162,7 +181,6 @@ def put( headers: dict[str, str], data: dict[str, typing.Any] | None = None, json: dict[str, typing.Any] | None = None, - *, is_json: bool = True, verify: bool | str = True, timeout: int = DEFAULT_API_TIMEOUT, @@ -234,7 +252,7 @@ def get( url: str, *, headers: dict[str, str] | None = None, - params: dict[str, str | int | float | None | list[str]] | None = None, + params: dict[str, str | int | float | list[str] | None] | None = None, timeout: int = DEFAULT_API_TIMEOUT, json: dict[str, typing.Any] | None = None, verify: str | bool = True, @@ -336,13 +354,15 @@ def get_json_from_response( expected_status: list[int], scenario: str, response: requests.Response, - *, allow_parse_failure: bool = False, - expected_type: type[dict | list] = dict, -) -> dict | list: + expected_type: type[dict[str, object] | list[object]] = dict, +) -> dict[str, object] | list[dict[str, object]]: try: - json_response: list[dict[str, object]] | dict[str, object] | None = ( - response.json() + json_response: list[dict[str, object]] | dict[str, str | object] | None = ( + typing.cast( + "list[dict[str, object]] | dict[str, str | object] | None", + response.json(), + ) ) json_response = json_response or ({} if expected_type is dict else []) decode_error = "" @@ -359,20 +379,13 @@ def get_json_from_response( f"expected type '{expected_type.__name__}' " f"but got '{type(json_response).__name__}'" ) - elif json_response is not None: - return json_response - else: + elif not json_response: details = f"could not request JSON response: {decode_error}" - elif not isinstance(json_response, expected_type): - details = ( - f"expected type '{expected_type.__name__}' but got" - f"'{type(json_response).__name__}'" - ) else: return json_response elif isinstance(json_response, dict): error_str += f" with status {_status_code}" - details = (json_response or {}).get("detail") + details = typing.cast("str", (json_response or {}).get("detail")) try: txt_response = response.text @@ -395,7 +408,7 @@ def get_paginated( json: dict[str, typing.Any] | None = None, count: int | None = None, offset: int | None = None, - **params, + **params: str | float | list[str] | None, ) -> Generator[requests.Response]: """Paginate results of a server query. @@ -428,6 +441,7 @@ def get_paginated( # if the count requested is below page limit use this value # else if undefined or greater than the page limit use the limit _request_count: int = min(count or MAX_ENTRIES_PER_PAGE, MAX_ENTRIES_PER_PAGE) + _response: requests.Response | None = None try: while ( @@ -443,11 +457,13 @@ def get_paginated( _offset += MAX_ENTRIES_PER_PAGE if (count and _offset > count) or ( - _response.json().get("count", 0) < _offset + typing.cast("dict[str, int]", _response.json()).get("count", 0) + < _offset ): break except requests.exceptions.JSONDecodeError: + _code: str = f"[{_response.status_code}] " if _response else "" raise RuntimeError( - f"[{_response.status_code}] Failed to retrieve content from server: " - + _response.text, + f"{_code}Failed to retrieve content from server" + + (f": {_response.text}" if _response else ""), ) from None diff --git a/simvue/api/url.py b/simvue/api/url.py index 0aadfd3d..744593db 100644 --- a/simvue/api/url.py +++ b/simvue/api/url.py @@ -7,7 +7,7 @@ try: from typing import Self, override except ImportError: - from typing_extensions import Self + from typing_extensions import Self, override import copy import urllib.parse @@ -20,7 +20,7 @@ class URL: @pydantic.validate_call def __init__(self, url: str) -> None: """Initialise a url from string form.""" - url = url.removesuffix("/") + _url = url.removesuffix("/") _parsed_url = urllib.parse.urlparse(_url) self._scheme: str = _parsed_url.scheme diff --git a/simvue/bin/sender.py b/simvue/bin/sender.py index 4022b98c..d09f4bc0 100644 --- a/simvue/bin/sender.py +++ b/simvue/bin/sender.py @@ -68,6 +68,6 @@ def sender_cli( throw_exceptions=True, ) _sender.upload(objects_to_upload) - except Exception as err: # noqa: BLE001 + except Exception as err: # ruff: ignore[blind-except] _logger.critical("Exception running sender: %s", err) raise click.Abort from None diff --git a/simvue/client.py b/simvue/client.py index b847a1e5..2cfa5acb 100644 --- a/simvue/client.py +++ b/simvue/client.py @@ -6,6 +6,7 @@ import contextlib import http +import itertools import json import logging import pathlib @@ -18,8 +19,12 @@ from pandas import DataFrame from simvue.api.objects.alert.base import AlertBase +from simvue.api.objects.alert.fetch import AlertSort from simvue.exception import ObjectNotFoundError +if typing.TYPE_CHECKING: + from simvue.api.objects.artifact.base import ArtifactBase + from .api.objects import ( Alert, Artifact, @@ -311,7 +316,7 @@ def get_runs( @prettify_pydantic @pydantic.validate_call - def delete_run(self, run_id: str) -> dict | None: + def delete_run(self, run_id: str) -> dict[str, object] | None: """Delete run by identifier. Parameters @@ -387,8 +392,8 @@ def _get_folder_id_from_path(self, path: str) -> str | None: return None if next(_ids, None): - raise RuntimeError( - f"Expected single folder match for '{path}', but found duplicate.", + _out_msg: str = ( + f"Expected single folder match for '{path}', but found duplicate." ) raise RuntimeError(_out_msg) @@ -399,7 +404,7 @@ def _get_folder_id_from_path(self, path: str) -> str | None: def delete_runs( self, folder_path: typing.Annotated[str, pydantic.Field(pattern=FOLDER_REGEX)], - ) -> list | None: + ) -> list[dict[str, object]] | None: """Delete runs in a named folder. Parameters @@ -434,7 +439,7 @@ def delete_folder( recursive: bool = False, remove_runs: bool = False, allow_missing: bool = False, - ) -> list | None: + ) -> list[dict[str, object]] | None: """Delete a folder by name. Parameters @@ -557,7 +562,9 @@ def _retrieve_artifacts_from_server( @prettify_pydantic @pydantic.validate_call - def abort_run(self, run_id: str, reason: str) -> dict | list: + def abort_run( + self, run_id: str, reason: str + ) -> dict[str, object] | list[dict[str, object]]: """Abort a currently active run on the server. Parameters @@ -701,7 +708,7 @@ def get_artifacts_as_files( if there was a failure retrieving artifacts from the server """ - _artifacts: Generator[tuple[str, Artifact]] = Artifact.from_run( + _artifacts: Generator[tuple[str, ArtifactBase]] = Artifact.from_run( server_url=self._user_config.server.url, server_token=self._user_config.server.token, run_id=run_id, @@ -849,7 +856,6 @@ def _get_run_metrics_from_server( metric_names: list[str], run_ids: list[str], xaxis: str, - *, aggregate: bool, max_points: int | None = None, ) -> dict[str, object]: @@ -889,7 +895,7 @@ def get_metric_values( use_run_names: bool = False, aggregate: bool = False, max_points: pydantic.PositiveInt | None = None, - ) -> dict | DataFrame | None: + ) -> "dict[str, dict[tuple[float, str], float]] | DataFrame | None": """Retrieve the values for a given metric across multiple runs. Uses filters to specify which runs should be retrieved. @@ -1018,7 +1024,7 @@ def plot_metrics( if invalid arguments are provided """ - data: DataFrame = self.get_metric_values( + _data: DataFrame = self.get_metric_values( run_ids=run_ids, metric_names=metric_names, xaxis=xaxis, @@ -1027,17 +1033,17 @@ def plot_metrics( aggregate=False, ) - if data is None: - raise RuntimeError( + if _data is None: + _out_msg: str = ( f"Cannot plot metrics {metric_names}, " - f"no data found for runs {run_ids}.", + + f"no data found for runs {run_ids}.", ) raise RuntimeError(_out_msg) # Undo multi-indexing flattened_df = _data.reset_index() - import matplotlib.pyplot as plt # noqa: PLC0415 + import matplotlib.pyplot as plt # ruff: ignore[import-outside-top-level] for run, name in itertools.product(run_ids, metric_names): label = None @@ -1098,7 +1104,7 @@ def get_events( if there was a failure retrieving information from the server """ - msg_filter: str = ( + _msg_filter: str = ( json.dumps([f"event.message contains {message_contains}"]) if message_contains else "" @@ -1114,7 +1120,7 @@ def get_events( _response = requests.get( f"{self._user_config.server.url}/events", headers=self._headers, - params=params, + params=_params, timeout=DEFAULT_API_TIMEOUT, ) @@ -1174,9 +1180,9 @@ def get_alerts( """ if not run_id: if critical_only: - raise RuntimeError( + _out_msg: str = ( "critical_only is ambiguous when returning alerts " - "with no run ID specified.", + + "with no run ID specified.", ) raise RuntimeError(_out_msg) _sorting = ( diff --git a/simvue/config/parameters.py b/simvue/config/parameters.py index 7feb6379..897c3885 100644 --- a/simvue/config/parameters.py +++ b/simvue/config/parameters.py @@ -14,6 +14,7 @@ import simvue.models as sv_models from simvue.api.url import URL +from simvue.config.files import DEFAULT_OFFLINE_DIRECTORY from simvue.utilities import get_expiry logger = logging.getLogger(__name__) diff --git a/simvue/config/user.py b/simvue/config/user.py index 6d2e39d1..5bfeebd4 100644 --- a/simvue/config/user.py +++ b/simvue/config/user.py @@ -97,7 +97,7 @@ def nosim_version(self) -> semver.Version | None: return self._nosim_version @classmethod - def _load_pyproject_configs(cls) -> dict | None: + def _load_pyproject_configs(cls) -> dict[str, object] | None: """Recover any Simvue non-authentication configurations from pyproject.toml.""" _pyproject_toml = sv_util.find_first_instance_of_file( file_names=["pyproject.toml"], @@ -184,25 +184,19 @@ def _check_server( _url = URL(url) / "version" _response = sv_get(f"{_url}", headers=headers, verify=verify) - if response.status_code == http.HTTPStatus.UNAUTHORIZED: - raise AssertionError("Unauthorised token") - return _version_str - - try: - _url = URL(url) / "version" - _response = sv_get(f"{_url}", headers) - _version_str = _retrieve_version_str(_response) - - if _response.status_code != http.HTTPStatus.OK or not ( - _version_str := _response.json().get("version") - ): - raise AssertionError("Failed to retrieve version from server response.") - except Exception as err: raise AssertionError( f"Exception retrieving server version:\n {err!s}", ) from err + if _response.status_code == http.HTTPStatus.UNAUTHORIZED: + raise AssertionError("Unauthorised token") + + if _response.status_code != http.HTTPStatus.OK or not ( + _version_str := _response.json().get("version") + ): + raise AssertionError("Failed to retrieve version from server response.") + _nosim_version_str: str = "" _nosim_version: semver.Version | None = None @@ -225,18 +219,18 @@ def _check_server( SIMVUE_SERVER_UPPER_CONSTRAINT and _version >= SIMVUE_SERVER_UPPER_CONSTRAINT ): - raise AssertionError( + _out_msg: str = ( f"Python API v{__version__} is not compatible " - "with the current Simvue server version: " - f"{_version_str} >= {SIMVUE_SERVER_UPPER_CONSTRAINT}", + + "with the current Simvue server version: " + + f"{_version_str} >= {SIMVUE_SERVER_UPPER_CONSTRAINT}", ) raise AssertionError(_out_msg) if SIMVUE_SERVER_LOWER_CONSTRAINT and _version < SIMVUE_SERVER_LOWER_CONSTRAINT: - raise AssertionError( + _out_msg = ( f"Python API v{__version__} is not compatible " - "with the current Simvue server version: " - f"{_version_str} < {SIMVUE_SERVER_LOWER_CONSTRAINT}", + + "with the current Simvue server version: " + + f"{_version_str} < {SIMVUE_SERVER_LOWER_CONSTRAINT}", ) raise AssertionError(_out_msg) @@ -303,7 +297,9 @@ def fetch( object containing configurations """ - _config_dict: dict[str, dict[str, str]] = cls._load_pyproject_configs() or {} + _config_dict: dict[str, dict[str, str | dict[str, str]]] = ( + cls._load_pyproject_configs() or {} + ) profile = os.environ.get("SIMVUE_SERVER_PROFILE", profile) @@ -319,11 +315,11 @@ def fetch( if not profile: _config_dict["server"] = _config_dict.get("server", {}) elif not _config_dict.get("profiles", {}).get(profile): - _exc_msg: str = ( + _out_msg: str = ( f"Cannot load server configuration for '{profile}', " - "profile not found in configurations.", + + "profile not found in configurations.", ) - raise RuntimeError(_exc_msg) + raise RuntimeError(_out_msg) else: _config_dict["server"] = _config_dict["profiles"][profile] @@ -344,9 +340,10 @@ def fetch( # Ranking of configurations for token and URl is: # Environment Variables > Run Definition > Configuration File + _server_opts: dict[str, str] = _config_dict["server"] _server_url = os.environ.get( "SIMVUE_URL", - server_url or _config_dict["server"].get("url"), + server_url or _server_opts.get("url"), ) if isinstance(_server_url, URL): @@ -354,7 +351,7 @@ def fetch( _server_token = os.environ.get( "SIMVUE_TOKEN", - server_token or _config_dict["server"].get("token"), + server_token or _server_opts.get("token"), ) _run_mode = mode or _config_dict["run"].get("mode") or "online" @@ -365,8 +362,8 @@ def fetch( if not _server_token and _run_mode != "offline": raise RuntimeError("No server token was specified") - _config_dict["server"]["token"] = _server_token - _config_dict["server"]["url"] = _server_url + _config_dict["server"]["token"] = _server_token # pyright: ignore[reportIndexIssue] + _config_dict["server"]["url"] = _server_url # pyright: ignore[reportIndexIssue] _config_dict["run"]["mode"] = _run_mode _user_config = SimvueConfiguration(current_profile=profile, **_config_dict) diff --git a/simvue/converters.py b/simvue/converters.py index 56033cc2..a1e77720 100644 --- a/simvue/converters.py +++ b/simvue/converters.py @@ -10,6 +10,8 @@ import pandas as pd if typing.TYPE_CHECKING: + from collections.abc import Iterable, Mapping + from pd import DataFrame @@ -43,7 +45,9 @@ def aggregated_metrics_to_dataframe( _all_steps: list[float] = sorted( { d[xaxis] - for sublist in request_response_data.values() + for sublist in typing.cast( + "Iterable[list[str]]", request_response_data.values() + ) for d in sublist if xaxis in d }, @@ -59,6 +63,8 @@ def aggregated_metrics_to_dataframe( metric_name: {} for metric_name in request_response_data } + metrics: list[dict[str, float]] + for metric_name, metrics in request_response_data.items(): metrics_iterator = iter(metrics) _metric_steps = (d[xaxis] for d in metrics) @@ -77,9 +83,8 @@ def aggregated_metrics_to_dataframe( _data_frame = pd.DataFrame(result_dict) _data_frame.index.name = xaxis return _data_frame - if parse_to == "dict": - return result_dict - raise ValueError(f"Unrecognised parse format '{parse_to}'") + + return result_dict def parse_run_set_metrics( @@ -120,10 +125,14 @@ def parse_run_set_metrics( if not request_response_data: return pd.DataFrame({}) if parse_to == "dataframe" else {} + _request_values: Iterable[dict[str, list[dict[str, float]]]] = ( + request_response_data.values() + ) + _all_steps: list[float] = sorted( { d[xaxis] - for run_data in request_response_data.values() + for run_data in _request_values for sublist in run_data.values() for d in sublist if xaxis in d @@ -131,11 +140,11 @@ def parse_run_set_metrics( ) _all_metrics: list[str] = sorted( - {key for run_data in request_response_data.values() for key in run_data}, + {key for run_data in _request_values for key in run_data}, ) # Get the keys from the aggregate which are not the xaxis label - _first_run = next(iter(request_response_data.values())) + _first_run = next(iter(_request_values)) _first_metric_set = next(iter(_first_run.values())) _value_types = next(iter(_first_metric_set)).keys() _value_types = list(_value_types) @@ -147,7 +156,7 @@ def parse_run_set_metrics( for run_label, run_data in zip( run_labels, - request_response_data.values(), + _request_values, strict=True, ): for metric_name in _all_metrics: @@ -167,22 +176,20 @@ def parse_run_set_metrics( if parse_to == "dataframe": return pd.DataFrame( - result_dict, + _result_dict, index=pd.MultiIndex.from_product( [_all_steps, run_labels], names=(xaxis, "run"), ), ) - if parse_to == "dict": - return result_dict - raise ValueError(f"Unrecognised parse format '{parse_to}'") + return _result_dict def to_dataframe(data) -> pd.DataFrame: """Convert runs to dataframe.""" - metadata = [] - system_columns = [] - columns = { + _metadata: list[dict[str, object]] = [] + _system_columns: list[str] = [] + _columns = { "name": [], "status": [], "folder": [], @@ -204,7 +211,7 @@ def to_dataframe(data) -> pd.DataFrame: _system_columns += [ col_name for sub_item in value - if (col_name := f"system.{item}.{sub_item}") not in system_columns + if (col_name := f"system.{item}.{sub_item}") not in _system_columns ] elif f"system.{item}" not in _system_columns: _system_columns.append(f"system.{item}") @@ -221,7 +228,7 @@ def to_dataframe(data) -> pd.DataFrame: except TypeError: value_.append(None) - return pd.DataFrame(data=columns) + return pd.DataFrame(data=_columns) def metric_time_series_to_dataframe( diff --git a/simvue/dispatch/base.py b/simvue/dispatch/base.py index dd5c57ba..c2d0c0fd 100644 --- a/simvue/dispatch/base.py +++ b/simvue/dispatch/base.py @@ -50,7 +50,7 @@ def add_item( *, object_type: str, metadata: dict[str, int | float] | None = None, - **__, + **_, ) -> None: """Add an item to the dispatcher. diff --git a/simvue/dispatch/direct.py b/simvue/dispatch/direct.py index 750c76f0..6164f132 100644 --- a/simvue/dispatch/direct.py +++ b/simvue/dispatch/direct.py @@ -5,6 +5,11 @@ from .base import DispatcherBaseClass +try: + from typing import override +except ImportError: + from typing_extensions import override + class DirectDispatcher(DispatcherBaseClass): """The DirectDispatcher executes the provided callback immediately.""" @@ -40,6 +45,7 @@ def __init__( thresholds=thresholds, ) + @override def add_item( self, item: typing.Any, @@ -52,22 +58,28 @@ def add_item( super().add_item(item, object_type=object_type, metadata=metadata) self._callback([item], object_type) + @override def run(self) -> None: """Run does not execute anything in this context.""" + @override def start(self) -> None: """Start does not execute anything in this context.""" + @override def join(self) -> None: """Join does not execute anything in this context.""" + @override def purge(self) -> None: """Purge does not execute anything in this context.""" + @override def is_alive(self) -> bool: """As unthreaded, state as not alive always.""" return False + @override @property def empty(self) -> bool: """No queue so always empty.""" diff --git a/simvue/dispatch/dispatcher.py b/simvue/dispatch/dispatcher.py index e973cc58..95b03c8b 100644 --- a/simvue/dispatch/dispatcher.py +++ b/simvue/dispatch/dispatcher.py @@ -17,7 +17,7 @@ from .base import DispatcherBaseClass -def Dispatcher( # noqa: N802 +def Dispatcher( # ruff: ignore[invalid-function-name] mode: typing.Literal["direct", "queued"], callback: typing.Callable[[list[typing.Any], str], None], object_types: list[str], diff --git a/simvue/dispatch/queued.py b/simvue/dispatch/queued.py index c18cf0b1..0f392d22 100644 --- a/simvue/dispatch/queued.py +++ b/simvue/dispatch/queued.py @@ -14,6 +14,11 @@ from .base import DispatcherBaseClass +try: + from typing import override +except ImportError: + from typing_extensions import override + MAX_REQUESTS_PER_SECOND: float = 1.0 MAX_BUFFER_SIZE: int = 16000 QUEUE_SIZE = 10000 @@ -79,8 +84,9 @@ def __init__( } self._max_read_rate: float = max_read_rate self._max_buffer_size: int = max_buffer_size - self._send_timer: int = 0 + self._send_timer: float = 0 + @override def add_item( self, item: typing.Any, @@ -100,11 +106,13 @@ def add_item( raise KeyError(f"No queue '{object_type}' found") self._queues[object_type].put((item, metadata or {}), block=blocking) + @override @property def empty(self) -> bool: """Returns if all queues are empty.""" return all(queue.empty() for queue in self._queues.values()) + @override def purge(self) -> None: """Purge all queues.""" for q in self._queues.values(): @@ -145,6 +153,7 @@ def _create_buffer(self, queue_label: str) -> list[typing.Any]: return _buffer + @override def run(self) -> None: """Execute the dispatcher action. diff --git a/simvue/eco/api_client.py b/simvue/eco/api_client.py index 645e1c8a..a605ca6e 100644 --- a/simvue/eco/api_client.py +++ b/simvue/eco/api_client.py @@ -11,10 +11,9 @@ import functools import http import logging -import typing import geocoder -import geocoder.location +import geocoder.ipinfo import pydantic import requests @@ -43,7 +42,7 @@ class CO2SignalResponse(pydantic.BaseModel): carbon_intensity_units: str @classmethod - def from_json_response(cls, json_response: dict) -> "CO2SignalResponse": + def from_json_response(cls, json_response: dict[str, str]) -> "CO2SignalResponse": _co2_signal_data = CO2SignalData( datetime=datetime.datetime.fromisoformat( json_response["datetime"].replace("Z", "+00:00"), @@ -58,7 +57,7 @@ def from_json_response(cls, json_response: dict) -> "CO2SignalResponse": @functools.lru_cache -def _call_geocoder_query() -> typing.Any: +def _call_geocoder_query() -> geocoder.ipinfo.IpinfoQuery: """Call GeoCoder API for IP location. Cached so this API is only called once per session as required. @@ -99,9 +98,11 @@ def post_init(self) -> Self: def _get_user_location_info(self) -> None: """Retrieve location information for the current user.""" logger.info("📍 Determining current user location.") - _current_user_loc_data: geocoder.location.BBox = _call_geocoder_query() + _current_user_loc_data = _call_geocoder_query() self._latitude: float self._longitude: float + if not _current_user_loc_data.latlng or not _current_user_loc_data.country: + raise RuntimeError("Failed to get user location.") self._latitude, self._longitude = _current_user_loc_data.latlng self._two_letter_country_code: str = _current_user_loc_data.country diff --git a/simvue/eco/emissions_monitor.py b/simvue/eco/emissions_monitor.py index 702abdfd..432a78b1 100644 --- a/simvue/eco/emissions_monitor.py +++ b/simvue/eco/emissions_monitor.py @@ -3,9 +3,6 @@ Provides an interface for estimating CO2 usage for processes on the CPU. """ -__author__ = "Kristian Zarebski" -__date__ = "2025-02-27" - import dataclasses import datetime import json @@ -180,6 +177,8 @@ def now(self) -> str: @property def outdated(self) -> bool: """Checks if the current data is out of date.""" + if not self._local_data: + raise RuntimeError("Expected local data to be initialised for CO2 monitor") if not self.intensity_refresh_interval: return False @@ -188,7 +187,9 @@ def outdated(self) -> bool: self._local_data["last_updated"], TIME_FORMAT, ).replace(tzinfo=datetime.timezone.utc) - return (_now - _latest_time).seconds > self.intensity_refresh_interval + return (_now - _latest_time).seconds > typing.cast( + "int", self.intensity_refresh_interval + ) def _load_local_data(self) -> dict[str, str | dict[str, str | float]] | None: """Loads locally stored CO2 intensity data.""" @@ -240,6 +241,10 @@ def check_refresh(self) -> bool: ): logger.info("🌍 CO2 emission outdated, calling API.") _data: CO2SignalResponse = self._client.get() + if self._local_data is None: + raise RuntimeError( + "Expected local data to be initialised for CO2 monitor" + ) self._local_data[self._client.country_code] = _data.model_dump(mode="json") self._local_data["last_updated"] = self.now() with self._data_file_path.open("w") as out_f: @@ -268,6 +273,11 @@ def estimate_co2_emissions( if not self._data_file_path: raise RuntimeError("Expected local data file to be defined.") + if self.thermal_design_power_per_cpu is None or self.n_cores_per_cpu is None: + raise ValueError( + "Expected values for thermal design power and number of cores for CPU" + ) + if not (_process := self._processes.get(process_id)): self._processes[process_id] = (_process := ProcessData()) diff --git a/simvue/exception.py b/simvue/exception.py index a119e309..3002bc8c 100644 --- a/simvue/exception.py +++ b/simvue/exception.py @@ -10,9 +10,11 @@ class ObjectNotFoundError(Exception): def __init__(self, obj_type: str, name: str, extra: str | None = None) -> None: _msg: str = ( - f"Failed to retrieve '{name}' of type '{obj_type}' " - f"{f'{extra}, ' if extra else ''}" - "no such object", + ( + f"Failed to retrieve '{name}' of type '{obj_type}' " + f"{f'{extra}, ' if extra else ''}" + "no such object" + ), ) super().__init__(_msg) diff --git a/simvue/executor.py b/simvue/executor.py index 8333bf3b..d0dfad94 100644 --- a/simvue/executor.py +++ b/simvue/executor.py @@ -6,16 +6,13 @@ Stdout and Stderr are sent to Simvue as artifacts. """ -__author__ = "Kristian Zarebski" -__date__ = "2023-11-15" - import contextlib import logging import multiprocessing.synchronize import os import pathlib import shutil -import subprocess # noqa: S404 +import subprocess import threading import time import typing @@ -54,7 +51,7 @@ def _execute_process( completion_trigger: threading.Event | None = None, environment: dict[str, str] | None = None, cwd: pathlib.Path | None = None, -) -> tuple[subprocess.Popen, threading.Thread | None]: +) -> tuple[subprocess.Popen[bytes], threading.Thread | None]: thread_out = None logger.debug("Launching process '%s'.", " ".join(command)) @@ -63,7 +60,7 @@ def _execute_process( pathlib.Path(f"{runner_name}_{proc_id}.err").open("w", encoding="utf-8") as err, pathlib.Path(f"{runner_name}_{proc_id}.out").open("w", encoding="utf-8") as out, ): - _result = subprocess.Popen( # noqa: S603 + _result = subprocess.Popen( command, stdout=out, stderr=err, @@ -77,7 +74,7 @@ def _execute_process( def trigger_check( completion_callback: CompletionCallback | None, trigger_to_set: multiprocessing.synchronize.Event | None, - process: subprocess.Popen, + process: subprocess.Popen[bytes], ) -> None: while process.poll() is None: time.sleep(1) @@ -132,15 +129,15 @@ def __init__(self, simvue_runner: "simvue.Run", *, keep_logs: bool = True) -> No """ self._runner = simvue_runner self._keep_logs = keep_logs - self._completion_callbacks: dict[str, CompletionCallback] | None = {} + self._completion_callbacks: dict[str, CompletionCallback] = {} self._completion_triggers: dict[ str, multiprocessing.synchronize.Event | None, ] = {} - self._completion_processes: dict[str, threading.Thread] | None = {} + self._completion_processes: dict[str, threading.Thread] = {} self._alert_ids: dict[str, str] = {} self.command_str: dict[str, str] = {} - self._processes: dict[str, subprocess.Popen] = {} + self._processes: dict[str, subprocess.Popen[bytes]] = {} self._all_processes: list[psutil.Process] = [] def std_out(self, process_id: str) -> str | None: @@ -535,7 +532,7 @@ def kill_process( if (process := self._processes.get(process_id)) is None: logger.error( "Failed to terminate process '%s' no such identifier.", - process.id, + process_id, ) return try: diff --git a/simvue/handler.py b/simvue/handler.py index dcf582f2..a1963497 100644 --- a/simvue/handler.py +++ b/simvue/handler.py @@ -36,7 +36,7 @@ def emit(self, record: logging.LogRecord) -> None: try: self._run_object.log_event(_msg) - except Exception: # noqa: BLE001 + except Exception: # ruff: ignore[blind-except] logging.Handler.handleError(self, record) @override diff --git a/simvue/metadata.py b/simvue/metadata.py index 8c7e5714..ab4275df 100644 --- a/simvue/metadata.py +++ b/simvue/metadata.py @@ -14,7 +14,7 @@ import toml import yaml -from pip._internal.operations.freeze import freeze # noqa: PLC2701 +from pip._internal.operations.freeze import freeze # ruff: ignore[import-private-name] from simvue.models import simvue_timestamp @@ -40,7 +40,7 @@ def git_info(repository: pathlib.Path) -> dict[str, typing.Any]: """ try: - import git # noqa: PLC0415 + import git # ruff: ignore[import-outside-top-level] except ImportError: return {} @@ -65,20 +65,21 @@ def git_info(repository: pathlib.Path) -> dict[str, typing.Any]: (tag.name for tag in git_repo.tags if tag.commit == current_commit), current_commit.hexsha, ) - return { - "git": { - "authors": list(author_list), - "ref": ref, - "msg": current_commit.message.strip(), - "time_stamp": simvue_timestamp(current_commit.committed_datetime), - "blame": blame, - "url": git_repo.remote().url, - "dirty": dirty, - }, - } except (git.InvalidGitRepositoryError, ValueError): return {} + return { + "git": { + "authors": list(author_list), + "ref": ref, + "msg": current_commit.message.strip(), + "time_stamp": simvue_timestamp(current_commit.committed_datetime), + "blame": blame, + "url": git_repo.remote().url, + "dirty": dirty, + }, + } + def _conda_dependency_parse(dependency: str) -> tuple[str, str] | None: """Parse a dependency definition into module-version.""" @@ -155,7 +156,7 @@ def _conda_env(environment_file: pathlib.Path) -> dict[str, str]: def _python_env(repository: pathlib.Path) -> dict[str, typing.Any]: """Retrieve a dictionary of Python dependencies if lock file is available.""" - python_meta: dict[str, dict] = {} + python_meta: dict[str, dict[str, str]] = {} if (pyproject_file := pathlib.Path(repository).joinpath("pyproject.toml")).exists(): content = toml.load(pyproject_file) @@ -208,7 +209,7 @@ def _python_env(repository: pathlib.Path) -> dict[str, typing.Any]: def _rust_env(repository: pathlib.Path) -> dict[str, typing.Any]: """Retrieve a dictionary of Rust dependencies if lock file available.""" - rust_meta: dict[str, dict] = {} + rust_meta: dict[str, dict[str, str]] = {} if (cargo_file := pathlib.Path(repository).joinpath("Cargo.toml")).exists(): content = toml.load(cargo_file).get("package", {}) @@ -232,7 +233,7 @@ def _rust_env(repository: pathlib.Path) -> dict[str, typing.Any]: def _julia_env(repository: pathlib.Path) -> dict[str, typing.Any]: """Retrieve a dictionary of Julia dependencies if a project file is available.""" - julia_meta: dict[str, dict] = {} + julia_meta: dict[str, dict[str, str]] = {} if (project_file := pathlib.Path(repository).joinpath("Project.toml")).exists(): content = toml.load(project_file) julia_meta["project"] = { @@ -243,7 +244,7 @@ def _julia_env(repository: pathlib.Path) -> dict[str, typing.Any]: def _node_js_env(repository: pathlib.Path) -> dict[str, typing.Any]: - js_meta: dict[str, dict] = {} + js_meta: dict[str, dict[str, str]] = {} if ( project_file := pathlib.Path(repository).joinpath("package-lock.json") ).exists(): diff --git a/simvue/metrics.py b/simvue/metrics.py index 45c3528d..2feda762 100644 --- a/simvue/metrics.py +++ b/simvue/metrics.py @@ -6,6 +6,7 @@ import contextlib import logging +import typing import psutil @@ -128,7 +129,8 @@ def get_gpu_metrics(processes: list[psutil.Process]) -> list[tuple[float, float] if is_gpu_used(handle, processes): utilisation_percent = nvmlDeviceGetUtilizationRates(handle).gpu memory = nvmlDeviceGetMemoryInfo(handle) - memory_percent = 100 * memory.free / memory.total + memory_percent = 100 * typing.cast("float", memory.free) + memory_percent /= typing.cast("float", memory.total) gpu_metrics.append((utilisation_percent, memory_percent)) nvmlShutdown() @@ -156,7 +158,7 @@ def __init__( """ self.cpu_percent: float | None = get_process_cpu(processes, interval=interval) self.cpu_memory: float | None = get_process_memory(processes) - self.gpus: list[dict[str, float]] = get_gpu_metrics(processes) + self.gpus: list[tuple[float, float]] = get_gpu_metrics(processes) def to_dict(self) -> dict[str, float]: """Create metrics dictionary for sending to a Simvue server.""" diff --git a/simvue/models.py b/simvue/models.py index 1ecca6c8..36725b39 100644 --- a/simvue/models.py +++ b/simvue/models.py @@ -81,8 +81,7 @@ def simvue_timestamp( elif isinstance(date_time, str): _local_time = datetime.datetime.now(datetime.timezone.utc).astimezone().tzinfo date_time = ( - datetime.datetime - .strptime(date_time, DATETIME_FORMAT) + datetime.datetime.strptime(date_time, DATETIME_FORMAT) .replace(tzinfo=_local_time) .astimezone(datetime.UTC) ) diff --git a/simvue/pynvml.py b/simvue/pynvml.py index ff4d218a..6b5ee013 100644 --- a/simvue/pynvml.py +++ b/simvue/pynvml.py @@ -1,3 +1,5 @@ +# pyright: reportSelfClsParameterName=false,reportImplicitOverride=false +# pyright: reportAttributeAccessIssue=false,reportUnreachable=false ##### # Based on nvidia-ml-py version 11.515.75 from https://pypi.org/project/nvidia-ml-py/ # including changes to fix nvmlDeviceGet*RunningProcesses from diff --git a/simvue/run.py b/simvue/run.py index 5c69a7a0..6ed7267d 100644 --- a/simvue/run.py +++ b/simvue/run.py @@ -99,7 +99,7 @@ def check_run_initialised( """ @functools.wraps(function) - def _wrapper(self: Self, *args: typing.Any, **kwargs: typing.Any) -> typing.Any: + def _wrapper(self: "Run", *args: typing.Any, **kwargs: typing.Any) -> typing.Any: # Tidy pydantic errors _function = prettify_pydantic(function) @@ -329,7 +329,7 @@ def processes(self) -> list[psutil.Process]: return process_list process_list += [self._parent_process] - process_list += self._child_processes + process_list += self._child_processes or [] return list(set(process_list)) @@ -504,7 +504,13 @@ def _heartbeat( def _create_dispatch_callback( self, - ) -> typing.Callable: + ) -> typing.Callable[ + [ + list[typing.Any], + typing.Literal["events", "metrics_tensor", "metrics_regular"], + ], + None, + ]: """Generates the relevant callback for posting of metrics and events. The generated callback is assigned to the dispatcher instance and is @@ -777,7 +783,7 @@ def init( # Parse the time to live/retention time if specified try: if retention_period: - self._retention: int | None = int( + self._retention = int( humanfriendly.parse_timespan(retention_period), ) else: @@ -839,7 +845,7 @@ def init( ) click.secho( "[simvue] Monitor in the UI at " - f"{self._user_config.server.url.rsplit('/api', 1)[0]}" + f"{str(self._user_config.server.url).rsplit('/api', 1)[0]}" f"/dashboard/runs/run/{self.id}", bold=self._term_color, fg="green" if self._term_color else None, @@ -1308,10 +1314,6 @@ def update_metadata(self, metadata: dict[str, typing.Any]) -> bool: self._error("Cannot update metadata, run not initialised") return False - if not isinstance(metadata, dict): - self._error("metadata must be a dict") - return False - if self._sv_obj: self._sv_obj.metadata = metadata self._sv_obj.commit() @@ -1391,7 +1393,7 @@ def update_tags(self, tags: list[str]) -> bool: try: self.set_tags(list(set(current_tags + tags))) - except Exception as err: # noqa: BLE001 + except Exception as err: # ruff: ignore[blind-except] self._error(f"Failed to update tags: {err}") return False @@ -1461,7 +1463,7 @@ def log_event( return False # FIXME: Temporary, this will eventually be removed - import semver # noqa: PLC0415 + import semver # ruff: ignore[import-outside-top-level] _log_level_server_version = semver.Version.parse("1.2.16") if ( @@ -2745,7 +2747,7 @@ def set_metric_units( self._meta_cache.setdefault("metrics", {}) try: - _unit_obj = unyt_quantity.from_string(units) + _unit_obj: unyt_quantity = unyt_quantity.from_string(units) self._meta_cache["metrics"][metric_name] = { "units": units, "mks_conversion": mks_conversion or float(_unit_obj.in_mks().value), diff --git a/simvue/sender/actions.py b/simvue/sender/actions.py index 91af41a7..f24f5009 100644 --- a/simvue/sender/actions.py +++ b/simvue/sender/actions.py @@ -285,7 +285,7 @@ def _single_item_upload( if upload_status is not None: with thread_lock: upload_status.setdefault(cls.object_type, 0) - upload_status[cls.object_type] += 1 + upload_status[cls.object_type] += 1 # pyright: ignore[reportOperatorIssue] if simvue_monitor_run: simvue_monitor_run.log_metrics( {f"uploads.{cls.object_type}": upload_status[cls.object_type]}, @@ -1201,6 +1201,7 @@ def upload( indent=2, ) + @override @classmethod def json_file(cls, cache_directory: pathlib.Path, *_: object) -> pathlib.Path: """Returns the local cache JSON file for an upload. diff --git a/simvue/sender/base.py b/simvue/sender/base.py index f3ba20a7..ff032c14 100644 --- a/simvue/sender/base.py +++ b/simvue/sender/base.py @@ -85,7 +85,9 @@ def __init__( self._max_workers = max_workers self._lock_path = self._cache_directory.joinpath("sender.lock") self._thread_lock = threading.Lock() - self._run_notification: typing.Literal["none", "email"] = run_notification + self._run_notification: typing.Literal["none", "all", "email"] = ( + run_notification + ) self._run_retention_period: str | None = run_retention_period self._upload_status: dict[str, str | float] = {} self._monitor_uploads: bool = monitor_uploads @@ -146,7 +148,7 @@ def _initialise_monitor_run(self) -> Run: trigger_abort=False, ) - _run.upload_count = 0 + _run.upload_count = 0 # pyright: ignore[reportAttributeAccessIssue] return _run diff --git a/simvue/serialization.py b/simvue/serialization.py index 57a66a99..4b4155ff 100644 --- a/simvue/serialization.py +++ b/simvue/serialization.py @@ -5,7 +5,6 @@ import contextlib import json -import pickle # noqa: S403 import typing from io import BytesIO @@ -85,7 +84,7 @@ def serialize_object(data: typing.Any, *, allow_pickle: bool) -> tuple[str, str] return _serialize_torch_tensor(data) if module_name == "builtins" and class_name == "module" and not allow_pickle: with contextlib.suppress(ImportError): - import matplotlib.pyplot as plt # noqa: PLC0415 + import matplotlib.pyplot as plt # ruff: ignore[import-outside-top-level] if data == plt: return _serialize_matplotlib(data) @@ -98,7 +97,7 @@ def serialize_object(data: typing.Any, *, allow_pickle: bool) -> tuple[str, str] @check_extra("plot") def _serialize_plotly_figure(data: typing.Any) -> tuple[str, str]: try: - import plotly # noqa: PLC0415 + import plotly # ruff: ignore[import-outside-top-level] except ImportError: return None mimetype = "application/vnd.plotly.v1+json" @@ -113,7 +112,7 @@ def _serialize_plotly_figure(data: typing.Any) -> tuple[str, str]: @check_extra("plot") def _serialize_matplotlib(data: typing.Any) -> tuple[str, str] | None: try: - import plotly # noqa: PLC0415 + import plotly # ruff: ignore[import-outside-top-level] except ImportError: return None mimetype = "application/vnd.plotly.v1+json" @@ -128,7 +127,7 @@ def _serialize_matplotlib(data: typing.Any) -> tuple[str, str] | None: @check_extra("plot") def _serialize_matplotlib_figure(data: typing.Any) -> tuple[str, str] | None: try: - import plotly # noqa: PLC0415 + import plotly # ruff: ignore[import-outside-top-level] except ImportError: return None mimetype = "application/vnd.plotly.v1+json" @@ -161,7 +160,7 @@ def _serialize_dataframe(data: typing.Any) -> tuple[str, str] | None: @check_extra("torch") def _serialize_torch_tensor(data: typing.Any) -> tuple[str, str] | None: try: - import torch # noqa: PLC0415 + import torch # ruff: ignore[import-outside-top-level] except ImportError: torch = None return None @@ -187,6 +186,8 @@ def _serialize_json(data: typing.Any) -> tuple[str, str] | None: def _serialize_pickle(data: typing.Any) -> tuple[str, str] | None: + import pickle # ruff: ignore[import-outside-top-level,suspicious-pickle-import] + mimetype = "application/octet-stream" data = pickle.dumps(data) return data, mimetype @@ -217,16 +218,7 @@ def deserialize_data( @check_extra("plot") def _deserialize_plotly_figure(data: "Buffer") -> "Figure | None": try: - import plotly # noqa: PLC0415 - except ImportError: - return None - return plotly.io.from_json(data) - - -@check_extra("plot") -def _deserialize_matplotlib_figure(data: "Buffer") -> "Figure | None": - try: - import plotly # noqa: PLC0415 + import plotly # ruff: ignore[import-outside-top-level] except ImportError: return None return plotly.io.from_json(data) @@ -247,7 +239,7 @@ def _deserialize_dataframe(data: "Buffer") -> "DataFrame | None": @check_extra("torch") def _deserialize_torch_tensor(data: "Buffer") -> "Tensor | None": try: - import torch # noqa: PLC0415 + import torch # ruff: ignore[import-outside-top-level] except ImportError: torch = None return None @@ -258,7 +250,9 @@ def _deserialize_torch_tensor(data: "Buffer") -> "Tensor | None": def _deserialize_pickle(data) -> typing.Any | None: - return pickle.loads(data) # noqa: S301 + import pickle # ruff: ignore[import-outside-top-level,suspicious-pickle-import] + + return pickle.loads(data) # ruff: ignore[suspicious-pickle-usage] def _deserialize_json(data) -> typing.Any | None: diff --git a/simvue/system.py b/simvue/system.py index d12bdff9..52528d84 100644 --- a/simvue/system.py +++ b/simvue/system.py @@ -5,7 +5,7 @@ import platform import shutil import socket -import subprocess # noqa: S404 +import subprocess import sys import typing @@ -24,7 +24,7 @@ def get_cpu_info() -> tuple[str, str]: if _lscpu := shutil.which("lscpu"): with contextlib.suppress(subprocess.CalledProcessError): - _info = subprocess.check_output(_lscpu).decode().strip() # noqa: S603 + _info = subprocess.check_output(_lscpu).decode().strip() for line in _info.split("\n"): if "Model name" in line: _model_name = line.split(":")[1].strip() @@ -36,8 +36,7 @@ def get_cpu_info() -> tuple[str, str]: if not _model_name and (_sysctl := shutil.which("sysctl")): with contextlib.suppress(subprocess.CalledProcessError): info = ( - subprocess # noqa: S603 - .check_output([_sysctl, "machdep.cpu.brand_string"]) + subprocess.check_output([_sysctl, "machdep.cpu.brand_string"]) .decode() .strip() ) @@ -63,7 +62,7 @@ def get_gpu_info() -> dict[str, str]: return _gpu_info with contextlib.suppress(subprocess.CalledProcessError, IndexError): - output = subprocess.check_output( # noqa: S603 + output = subprocess.check_output( [_nvidia_smi, "--query-gpu=name,driver_version", "--format=csv"], ) lines = output.split(b"\n") diff --git a/simvue/utilities.py b/simvue/utilities.py index d8b7c0a2..21481fc1 100644 --- a/simvue/utilities.py +++ b/simvue/utilities.py @@ -1,6 +1,7 @@ """Utility and global helper functions.""" import contextlib +import datetime import functools import hashlib import importlib.util @@ -12,10 +13,13 @@ import platform import typing +import deepmerge import jwt import pydantic import tabulate +from .models import DATETIME_FORMAT + CHECKSUM_BLOCK_SIZE = 4096 EXTRAS: tuple[str, ...] = ("plot", "torch") @@ -157,7 +161,7 @@ def parse_validation_response( return str(_table) -def check_extra(extra_name: str) -> typing.Callable: +def check_extra(extra_name: str) -> typing.Callable[[typing.Any], typing.Any]: """Check for the presence of a given module extra. Some features are unlocked by specifying an 'extra' when installing. @@ -273,7 +277,7 @@ def skip_if_failed( failure_attr: str, ignore_exc_attr: str, on_failure_return: typing.Any | None = None, -) -> typing.Callable: +) -> typing.Callable[[typing.Any], typing.Any]: """Decorator for ensuring if Simvue throws an exception any other code continues. If Simvue throws an exception and the user has specified that such failure @@ -298,7 +302,9 @@ def skip_if_failed( """ - def decorator(class_func: typing.Callable) -> typing.Callable: + def decorator( + class_func: typing.Callable[[typing.Any], typing.Any], + ) -> typing.Callable[[typing.Any], typing.Any]: @functools.wraps(class_func) def wrapper(self: "Run", *args, **kwargs) -> typing.Any: if getattr(self, failure_attr, None) and getattr( @@ -329,7 +335,9 @@ def wrapper(self: "Run", *args, **kwargs) -> typing.Any: return decorator -def prettify_pydantic(func: typing.Callable) -> typing.Callable: +def prettify_pydantic( + func: typing.Callable[[typing.Any], typing.Any], +) -> typing.Callable[[typing.Any], typing.Any]: """Converts pydantic validation errors to a table. Parameters @@ -394,7 +402,9 @@ def validate_timestamp(timestamp): Validate a user-provided timestamp """ try: - datetime.datetime.strptime(timestamp, DATETIME_FORMAT) + _ = datetime.datetime.strptime(timestamp, DATETIME_FORMAT).astimezone( + datetime.timezone.utc + ) except ValueError: return False @@ -415,7 +425,7 @@ def simvue_timestamp(date_time: datetime.datetime | None = None) -> str: Datetime string valid for the Simvue server """ if not date_time: - date_time = datetime.datetime.now(datetime.UTC) + date_time = datetime.datetime.now(datetime.timezone.utc) return date_time.strftime(DATETIME_FORMAT) diff --git a/simvue/version.py b/simvue/version.py index 1a50cfdc..9a33f5b6 100644 --- a/simvue/version.py +++ b/simvue/version.py @@ -9,8 +9,7 @@ __version__ = importlib.metadata.version("simvue") except importlib.metadata.PackageNotFoundError: _metadata = ( - pathlib - .Path(__file__) + pathlib.Path(__file__) .parents[2] .joinpath( "pyproject.toml", diff --git a/uv.lock b/uv.lock index 5d7e1232..fa50e562 100644 --- a/uv.lock +++ b/uv.lock @@ -43,101 +43,142 @@ wheels = [ [[package]] name = "charset-normalizer" -version = "3.4.9" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", size = 152439, upload-time = "2026-07-07T14:34:58.454Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ad/81/8e983840c6e5b93b33c2ba81aa3d52c2e42f0e9a690ce7607a2e61da4a5c/charset_normalizer-3.4.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd6280cf040f233bd7d3407b743b4b4c74f70e8e1c4199cb112a62c941c0772a", size = 322240, upload-time = "2026-07-07T14:32:36.236Z" }, - { url = "https://files.pythonhosted.org/packages/de/d1/b4319dc3229d8272fba305e206fc0a148e2de8d4087917ce62ae6382f359/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616", size = 216475, upload-time = "2026-07-07T14:32:38.142Z" }, - { url = "https://files.pythonhosted.org/packages/80/33/6c99c1b3e6b8bf730e1bc809b9a2608f224145069114c479a2e9e1494346/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c1225416b463483160e4af85d5fc3a9690ccb53fd4b1865a6437825f5ede3209", size = 238670, upload-time = "2026-07-07T14:32:39.658Z" }, - { url = "https://files.pythonhosted.org/packages/7f/f4/ffbb83546e1f198ecc70ecd372b65cf2b50f9068b380abd67640f17a8e18/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:16d10d789dd9bcca1173c95af82c58433122564b7bc39385124be735a35cbe99", size = 233476, upload-time = "2026-07-07T14:32:41.155Z" }, - { url = "https://files.pythonhosted.org/packages/e8/5f/b98b8da398637b551e427e7be922bdec19177dc54d6811dcdaa503f23aac/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bb41182d93ea91f60b4bc8fbf4c820c69ef8a12ab2d917f3f1834f1acad07e8", size = 223817, upload-time = "2026-07-07T14:32:42.592Z" }, - { url = "https://files.pythonhosted.org/packages/36/31/a276bb2e66243072a3fd06fdcab9cbb61a305b02143d70d2bda21d888fa8/charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:bcf74c1df76758a395bf0af608c04c82257523f55c9868b334f06270d0f2112b", size = 207974, upload-time = "2026-07-07T14:32:44.258Z" }, - { url = "https://files.pythonhosted.org/packages/5e/be/7ee4453d7e88dfbc4104ccd34900b9f2c7c17dac22881865fe0e82424a25/charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b5314963fce9b0b12743891de876e724997864ee22aa496f903f426c7e2fa5b2", size = 221655, upload-time = "2026-07-07T14:32:45.64Z" }, - { url = "https://files.pythonhosted.org/packages/1d/85/181c652953eb5276d198f375b1dd641047392050098100a3a02d6534f657/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e9701d0049d92c16703a42771b98d560b95248949f23f8cf7b4eddd201814fb9", size = 219229, upload-time = "2026-07-07T14:32:47.376Z" }, - { url = "https://files.pythonhosted.org/packages/0c/e7/aaf6da33fc9f4691cda8f7efbc9f69179d3d39ec8a4799baf273ee1d8db0/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:65a7ff3f705e57d392f7261b6d0550fe137c3019477431f1c355e0db0a7d3e15", size = 209704, upload-time = "2026-07-07T14:32:48.855Z" }, - { url = "https://files.pythonhosted.org/packages/63/01/f2fb3bd3a73be48b173ee0c6aa8d2497af97d5663a8c4c4b491de4c62f7a/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79580094b00d1789d1f93ea55bc43cb2f611910c72235b7657f3482ddcc1b22d", size = 226243, upload-time = "2026-07-07T14:32:50.239Z" }, - { url = "https://files.pythonhosted.org/packages/c4/02/c57a22739fe05246b0b5783b3bfb6afaac4eebb46f3ececdfb2f048f780e/charset_normalizer-3.4.9-cp310-cp310-win32.whl", hash = "sha256:432786d3561e69aeeae6c7e8648964ce0ad05736120135601f87ac26b9c83381", size = 150935, upload-time = "2026-07-07T14:32:51.676Z" }, - { url = "https://files.pythonhosted.org/packages/37/8d/ca39a7559a4797505530d084fd3a49a2c959efbbbff146302fb7be4e3b35/charset_normalizer-3.4.9-cp310-cp310-win_amd64.whl", hash = "sha256:8c041122946b7ba21bb32c45b1aa57b1be35527690aeb3c5c234521085632eee", size = 162314, upload-time = "2026-07-07T14:32:53.193Z" }, - { url = "https://files.pythonhosted.org/packages/01/da/a44bd7a13d426e69e4894557106cd58669097bfad4a8681123b618fbfc5d/charset_normalizer-3.4.9-cp310-cp310-win_arm64.whl", hash = "sha256:375b83ed0aecfce76c16d198fbc21f3b11b337d68662bea0a995046682a11419", size = 153075, upload-time = "2026-07-07T14:32:54.554Z" }, - { url = "https://files.pythonhosted.org/packages/0b/e3/85ec501f206fb049259288c1f3506e53876937fb00edb47009348e66756b/charset_normalizer-3.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5", size = 317075, upload-time = "2026-07-07T14:32:56.021Z" }, - { url = "https://files.pythonhosted.org/packages/c3/69/2a5385192e67175f7d8bd5ce4f57c24bc956439adeae5c13a99aa28a53d1/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2", size = 213837, upload-time = "2026-07-07T14:32:57.78Z" }, - { url = "https://files.pythonhosted.org/packages/b3/46/03ddc7da576d814fe0a36dd1f0fd3258e95404b4b2e3c026b7923d7e133f/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a", size = 235503, upload-time = "2026-07-07T14:32:59.205Z" }, - { url = "https://files.pythonhosted.org/packages/4e/6e/de0229a7ef40f6f9d28a837eebf4ec47bdca5dab4e900c84f22919af636a/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29", size = 229944, upload-time = "2026-07-07T14:33:00.803Z" }, - { url = "https://files.pythonhosted.org/packages/a5/34/49b9060e8418b14fb5cba9cf6bfb383111e2538a03a1fb18e66a95aeb3d5/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c", size = 221276, upload-time = "2026-07-07T14:33:02.199Z" }, - { url = "https://files.pythonhosted.org/packages/44/95/80282cce0fae9c3061203d723ee87da996aed79679e65d8935050ee7ca1f/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b", size = 205260, upload-time = "2026-07-07T14:33:03.698Z" }, - { url = "https://files.pythonhosted.org/packages/0c/74/2f62c8821b969ea3bd67cc2e6976834f48ca5d12664d2559ebcd9bcfbed7/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db", size = 217786, upload-time = "2026-07-07T14:33:05.12Z" }, - { url = "https://files.pythonhosted.org/packages/d9/8d/feabb82cb49fcad14515b1d7d1ca4787b0da7fc723a212bf89bc9e0fac52/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993", size = 216798, upload-time = "2026-07-07T14:33:06.629Z" }, - { url = "https://files.pythonhosted.org/packages/a5/ff/c946d63bc3786d5b84d960b0f7ab7e25b828486a946b5aa997625bcaf6a6/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da", size = 206429, upload-time = "2026-07-07T14:33:08.006Z" }, - { url = "https://files.pythonhosted.org/packages/af/ba/5e5007c370702f85d2ef75791fac7943ed41e080364a673b20142e430e3e/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3", size = 223066, upload-time = "2026-07-07T14:33:09.783Z" }, - { url = "https://files.pythonhosted.org/packages/83/d5/9096aa3cf532dfad237861544eb47a0f20d5adbf1039760fed8eaae935d9/charset_normalizer-3.4.9-cp311-cp311-win32.whl", hash = "sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d", size = 150456, upload-time = "2026-07-07T14:33:11.217Z" }, - { url = "https://files.pythonhosted.org/packages/ed/a1/e29995109e455dc8eff8d0fac6ae509be39561318a7cfeac5d33ad029213/charset_normalizer-3.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1", size = 161410, upload-time = "2026-07-07T14:33:12.743Z" }, - { url = "https://files.pythonhosted.org/packages/4f/8d/1569f4d0032d6ba2a4fe4591c35bf87868c600c41a71eb5c2e1ffa8464c2/charset_normalizer-3.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec", size = 152649, upload-time = "2026-07-07T14:33:14.173Z" }, - { url = "https://files.pythonhosted.org/packages/70/4a/ecbd131485c07fcdfad54e28946d513e3da22ef3b4bd854dcafae54ec739/charset_normalizer-3.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0", size = 319300, upload-time = "2026-07-07T14:33:15.666Z" }, - { url = "https://files.pythonhosted.org/packages/ec/96/5d9364e3342d69f3a045e1777bc47c85c383e6e9466d561b33fdb419d1f9/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9", size = 215802, upload-time = "2026-07-07T14:33:17.031Z" }, - { url = "https://files.pythonhosted.org/packages/4b/4c/5361f9aa7f2cb58d94f2ab831b3d493f69efb1d239654b4744e3c09527cb/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44", size = 237171, upload-time = "2026-07-07T14:33:18.576Z" }, - { url = "https://files.pythonhosted.org/packages/50/78/ce342ca4ff30b2eb49fe6d9578df85974f90c67d294113e94efdd9664cbd/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9", size = 233075, upload-time = "2026-07-07T14:33:20.084Z" }, - { url = "https://files.pythonhosted.org/packages/01/c4/4fa4c8b3097a11f3c5f09a35b72ed6855fb1d332469504962ab7bafcc702/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd", size = 224256, upload-time = "2026-07-07T14:33:21.747Z" }, - { url = "https://files.pythonhosted.org/packages/87/3a/ad914516df7e358a81aae018caa5e0470ba827fa6d763b1d2e87d920a5f6/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84", size = 208784, upload-time = "2026-07-07T14:33:23.313Z" }, - { url = "https://files.pythonhosted.org/packages/d7/74/3c12f9755717dfe5c5c87da63f35d765fa0c00382ec26bf23f7fae34f2ba/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b", size = 219928, upload-time = "2026-07-07T14:33:24.814Z" }, - { url = "https://files.pythonhosted.org/packages/33/9a/895095b83e7907abd6d3d99aad3a38ad0d9686cc186cb0c94c24320fe63e/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde", size = 218489, upload-time = "2026-07-07T14:33:26.42Z" }, - { url = "https://files.pythonhosted.org/packages/a1/34/ef5c05f412f42520d7709b7d3784d19640839eb7366ded1755511585429f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39", size = 210267, upload-time = "2026-07-07T14:33:27.952Z" }, - { url = "https://files.pythonhosted.org/packages/83/dc/9b29fa4412b318bf3bfea985c35d67eb55e04b59a7c3f2237168b0e0be6f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62", size = 226030, upload-time = "2026-07-07T14:33:29.397Z" }, - { url = "https://files.pythonhosted.org/packages/0e/42/6dbc00b8cd16011691203e33570fa42ed5746599a2e878112d16eab403a3/charset_normalizer-3.4.9-cp312-cp312-win32.whl", hash = "sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642", size = 151185, upload-time = "2026-07-07T14:33:30.781Z" }, - { url = "https://files.pythonhosted.org/packages/80/cc/f920afd1a23c58ccd53c1d36085a71893a4737ff5e66e0371efab6809850/charset_normalizer-3.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0", size = 162557, upload-time = "2026-07-07T14:33:32.176Z" }, - { url = "https://files.pythonhosted.org/packages/f0/e6/0386d43a261ff4e4b30c5857af7df877254b46bec7b9d1b74b6bf969a90b/charset_normalizer-3.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2", size = 152665, upload-time = "2026-07-07T14:33:33.711Z" }, - { url = "https://files.pythonhosted.org/packages/b2/06/97ec2aeae780b31d742b6352218b43841a6871e2564578ca522dce4a45c3/charset_normalizer-3.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614", size = 317688, upload-time = "2026-07-07T14:33:35.408Z" }, - { url = "https://files.pythonhosted.org/packages/d0/39/8ff066c672434225f8d25f8b739f992af250944392173dcc88362681c9bf/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698", size = 214982, upload-time = "2026-07-07T14:33:36.996Z" }, - { url = "https://files.pythonhosted.org/packages/92/8f/3a47a3667c83c2df9483d91644c6c107de3bf8874aa1793da9d3012eb986/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b", size = 236460, upload-time = "2026-07-07T14:33:38.536Z" }, - { url = "https://files.pythonhosted.org/packages/f1/60/b22cdbee7e4013dab8b0d7647fc6181120fbbbc8f7025c226d15bd5a47fc/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9", size = 232003, upload-time = "2026-07-07T14:33:40.059Z" }, - { url = "https://files.pythonhosted.org/packages/ea/f8/72eb13dcabe7257035cea8aefd922caad2f110d252bf9f67c4c2ca763aee/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33", size = 223149, upload-time = "2026-07-07T14:33:41.631Z" }, - { url = "https://files.pythonhosted.org/packages/b0/3e/faee8f9de92b14ee1198e9163252bb15efee7301b31256a3b6d9ebfdd0dd/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63", size = 207901, upload-time = "2026-07-07T14:33:43.209Z" }, - { url = "https://files.pythonhosted.org/packages/3a/25/45f30093ae27dd7b92a793b61882a38685f993700113ca36e0c9c14965e1/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0", size = 219176, upload-time = "2026-07-07T14:33:44.725Z" }, - { url = "https://files.pythonhosted.org/packages/48/18/c8f397329c35e32f6a837e488986f4ae03bd2abebc453b48714991630c2f/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe", size = 217356, upload-time = "2026-07-07T14:33:46.192Z" }, - { url = "https://files.pythonhosted.org/packages/86/7e/5ce0bba863470fd1902d5e5843968951bddf38abe4742fc97116ef4598b3/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35", size = 209614, upload-time = "2026-07-07T14:33:47.705Z" }, - { url = "https://files.pythonhosted.org/packages/6c/ef/2473d3c4d869155be4af1191111d59c4d5c4e0173026f7e85b176e23bf65/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8", size = 224991, upload-time = "2026-07-07T14:33:49.238Z" }, - { url = "https://files.pythonhosted.org/packages/d0/a3/53ddae3db108a088156aa8ddfafd411ebbc1340f48c5573f697b27f69a39/charset_normalizer-3.4.9-cp313-cp313-win32.whl", hash = "sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9", size = 150622, upload-time = "2026-07-07T14:33:50.711Z" }, - { url = "https://files.pythonhosted.org/packages/e8/ef/6953a77c7cf2c2ff9998e6f575ab3e380119f100223381565a4f94c1f836/charset_normalizer-3.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115", size = 161947, upload-time = "2026-07-07T14:33:52.197Z" }, - { url = "https://files.pythonhosted.org/packages/6e/fb/d560d1d1555debbfe7849d9cac6145c1b537709d79576bf22557ed803b82/charset_normalizer-3.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012", size = 152594, upload-time = "2026-07-07T14:33:53.486Z" }, - { url = "https://files.pythonhosted.org/packages/7e/8d/496817fa0944239ecae662dd57ea765cfeaec6a735f9f025d4b7b72e7143/charset_normalizer-3.4.9-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380", size = 317253, upload-time = "2026-07-07T14:33:54.994Z" }, - { url = "https://files.pythonhosted.org/packages/2b/f9/ef4a69ea338ad3c0deceea0f5f7d2380ae8b52132b06d652cb0d2cd86706/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9", size = 215898, upload-time = "2026-07-07T14:33:56.334Z" }, - { url = "https://files.pythonhosted.org/packages/8c/e7/5ddfd76fc061eb52de219658a4aa431cbacadf0a0219c8854f00da50d289/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4", size = 236718, upload-time = "2026-07-07T14:33:57.9Z" }, - { url = "https://files.pythonhosted.org/packages/49/ba/768fa3f36048d81c477a0ce61f813bc1454d80917ccfe550abd9f44f5e24/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a", size = 232519, upload-time = "2026-07-07T14:33:59.811Z" }, - { url = "https://files.pythonhosted.org/packages/f4/c4/b3e049d2aa3766180c78507110543d9d50894cc97f57de543f1be521dcdc/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046", size = 223143, upload-time = "2026-07-07T14:34:01.517Z" }, - { url = "https://files.pythonhosted.org/packages/19/79/55c32d06d76ae4feafe053f061f3e3ab70bcf19f4007797ce8c3efda7830/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81", size = 206742, upload-time = "2026-07-07T14:34:03.04Z" }, - { url = "https://files.pythonhosted.org/packages/10/e0/47c079dd82d217c807479cd59ffd30af56307ea31c108b75758970459ad3/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917", size = 219191, upload-time = "2026-07-07T14:34:04.657Z" }, - { url = "https://files.pythonhosted.org/packages/42/ab/b9bc2e77d6b44a7e46ef62ec5cac1c9a6ba7b9135a5d560f002696ec9995/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41", size = 218328, upload-time = "2026-07-07T14:34:06.115Z" }, - { url = "https://files.pythonhosted.org/packages/f1/78/c9c71d599f5aa2d42bcdd35cbbd46d7f535351a57e40ff7d8e5a7e219401/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1", size = 207406, upload-time = "2026-07-07T14:34:07.554Z" }, - { url = "https://files.pythonhosted.org/packages/f6/39/c914445c321a845097ce4f6ac7de9a18228a77b766272125a1ce00d851eb/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf", size = 225157, upload-time = "2026-07-07T14:34:09.061Z" }, - { url = "https://files.pythonhosted.org/packages/9b/f2/c0d4b8508565a36bc5c624e88ed297f5b0b1095011034d7f5b83a69908b5/charset_normalizer-3.4.9-cp314-cp314-win32.whl", hash = "sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48", size = 151095, upload-time = "2026-07-07T14:34:10.901Z" }, - { url = "https://files.pythonhosted.org/packages/49/fd/a1d26144398c67486422a72bf5812cda22cb4ccfcd95a290fb41ceb4b8e2/charset_normalizer-3.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b", size = 162796, upload-time = "2026-07-07T14:34:12.47Z" }, - { url = "https://files.pythonhosted.org/packages/20/95/d75e82f8ce9fd323ebf059c16c9aadefb22a1ecde13b7840b35835e4886c/charset_normalizer-3.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519", size = 153334, upload-time = "2026-07-07T14:34:14.044Z" }, - { url = "https://files.pythonhosted.org/packages/00/5e/17398df3a139985ba9d11ed072531986f408c8fca952835ef1ab1820c02b/charset_normalizer-3.4.9-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198", size = 338848, upload-time = "2026-07-07T14:34:15.688Z" }, - { url = "https://files.pythonhosted.org/packages/cd/91/7253a32e86b7e1d1239b1b36ba6dd0f021a21107ab33054b53119cc083b9/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32", size = 223022, upload-time = "2026-07-07T14:34:17.248Z" }, - { url = "https://files.pythonhosted.org/packages/cb/32/2e64bd2be10e89c61e57ebe6a93fd98ae88eb7ebe414b5121f22c96c69eb/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632", size = 241590, upload-time = "2026-07-07T14:34:18.813Z" }, - { url = "https://files.pythonhosted.org/packages/3d/ef/d96ec496cfea0c21db43b0ad03891308b02388d054cc902cf0e5a1ad6a88/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf", size = 239584, upload-time = "2026-07-07T14:34:20.52Z" }, - { url = "https://files.pythonhosted.org/packages/d4/ce/9af95f7876194bd7a14e3dfe4a4de2e0bff02666a3910d72beafd06cc297/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990", size = 230224, upload-time = "2026-07-07T14:34:22.189Z" }, - { url = "https://files.pythonhosted.org/packages/52/94/af74dde74a3996bd959c350709bfe50e297823d70a8c1cbd54b838880863/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d", size = 212667, upload-time = "2026-07-07T14:34:23.857Z" }, - { url = "https://files.pythonhosted.org/packages/ee/f0/f1c4fe746c395922961b5916ed1d7d6e7d4c84851d19ed43cc89980ec953/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e", size = 227179, upload-time = "2026-07-07T14:34:25.586Z" }, - { url = "https://files.pythonhosted.org/packages/e4/56/6c745619ac397e8871e2bcd3cea1eec86b877488f33888b3aef5c3ed506e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c", size = 225372, upload-time = "2026-07-07T14:34:27.212Z" }, - { url = "https://files.pythonhosted.org/packages/78/ad/98aae8630ac71f16711968e38a5acfecce41b778bf2f0312851020f565a8/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2", size = 215222, upload-time = "2026-07-07T14:34:28.774Z" }, - { url = "https://files.pythonhosted.org/packages/f7/40/9593d54209765207a7f11073c06494c1721e4ca4a0a426c597679bf7f91e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534", size = 231958, upload-time = "2026-07-07T14:34:30.345Z" }, - { url = "https://files.pythonhosted.org/packages/b1/27/693ee5e8a18191eb38647360c51cd505013e2bd3b366aa43fd5344c21e3c/charset_normalizer-3.4.9-cp314-cp314t-win32.whl", hash = "sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226", size = 155580, upload-time = "2026-07-07T14:34:31.884Z" }, - { url = "https://files.pythonhosted.org/packages/80/3f/bd97d3d9c613013d07cb7733d299385b41df37f0471310f5a73dc359f0b8/charset_normalizer-3.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177", size = 167620, upload-time = "2026-07-07T14:34:33.438Z" }, - { url = "https://files.pythonhosted.org/packages/3d/c6/eee9dca4439b1061f76373f06ea855678cc4a64c1c3c90b50e479edbb8eb/charset_normalizer-3.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501", size = 158037, upload-time = "2026-07-07T14:34:35.018Z" }, - { url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538, upload-time = "2026-07-07T14:34:56.993Z" }, +version = "3.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/3f/143b048436775b0f76ac3eec145c019e8173ccc2885c8f20319b996d5e83/charset_normalizer-3.5.1.tar.gz", hash = "sha256:6117b84ea48435e5356dc737f5121485c30920ba43375fa7b434fd753df0eac3", size = 171764, upload-time = "2026-08-15T08:20:44.807Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/aa/554e2614f38fc34c58ff1d0911ae8535ad2516440d5482d76fe59f1088b0/charset_normalizer-3.5.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1ee1e296209fdce05b81b663250eefa02213a2da7b41bf26f7829b8ba3545aa", size = 369072, upload-time = "2026-08-15T08:16:22.964Z" }, + { url = "https://files.pythonhosted.org/packages/03/6d/439231dfc3ccfa6f8c06477b7da2219cbd41a2de3d49084df8ec7b5100f2/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9fbdce1e47394b09bc9f26ab117dfc8d6491977a11d86f592bb42c779db2fda", size = 251142, upload-time = "2026-08-15T08:16:24.81Z" }, + { url = "https://files.pythonhosted.org/packages/55/53/7d819bd23a00ef45039146fa2cce1daa2f0771e758c5653ee1f6edac91ed/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:00668ebb0609751758682eb0b5857e7c35b9f00e84dfdef062e103244ec94d45", size = 240714, upload-time = "2026-08-15T08:16:26.392Z" }, + { url = "https://files.pythonhosted.org/packages/b2/2c/45847198c16f4b38090cc7423b2b6a9008e438704d8ab413211832498d31/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba2f37ee79e6338845261a3c5b1784e5d1acdff2c0785b284f1b633033d136ab", size = 279637, upload-time = "2026-08-15T08:16:27.961Z" }, + { url = "https://files.pythonhosted.org/packages/69/2b/d8be3523ddf9f0b0f3e56d1359034aa10653a4d11564c697f802b4775766/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ce854f5f478050ade5a238731c4ca985a7d3b3cb53ff600a9b5c3b689b5f0a7a", size = 276543, upload-time = "2026-08-15T08:16:29.399Z" }, + { url = "https://files.pythonhosted.org/packages/32/cd/4f564b8f132de25db594efc706897069f016790cea63a5669c9df2675f64/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:96eefc178f8636b9c760c5829345307fd81cfae9ab1e80997dbddeb0f54ee9a3", size = 261644, upload-time = "2026-08-15T08:16:30.722Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e3/38b975422534a608f98c360e79c2f07c763d66dd4272300d45fb1fee54b0/charset_normalizer-3.5.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:366ec70f5547c640d3ce1985722490f23faf4eb5216a7eeba78277490e78dacb", size = 259609, upload-time = "2026-08-15T08:16:32.248Z" }, + { url = "https://files.pythonhosted.org/packages/87/bd/fbc24d825c66f1c74f6ccdea3742c3d8354a4888e86d1315a197fee69061/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:950f23cb393f85543777b0433f082cddd25b51ab398eac7971146495679efe5f", size = 252457, upload-time = "2026-08-15T08:16:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/b9/2d/918d0e98a0e679469ed05bb2d90c2088b4d315bb612969d8499f76fb5210/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c1dcc36dcb96abc02236e182d17e0f71430152a6c2c7447421da2d2dc144edea", size = 242240, upload-time = "2026-08-15T08:16:35.396Z" }, + { url = "https://files.pythonhosted.org/packages/20/c8/c36f6e0b2dfec351bd38cbc05362697e58bcd073d7dbd95154290c9714ce/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:07ffd07412fc5d5e84cd8952acf9ff7e4ed7a708e69d1bada19d8ba91711353f", size = 280308, upload-time = "2026-08-15T08:16:36.825Z" }, + { url = "https://files.pythonhosted.org/packages/ca/7b/311b3e02e8c4092400c449c850a760d8c45d900983c83a70cc07208c551d/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:f5542f9b941279d82d41eb0aa9f98eba36fe4df5c7086c651df7944935b37182", size = 258679, upload-time = "2026-08-15T08:16:38.22Z" }, + { url = "https://files.pythonhosted.org/packages/b9/90/082cc45599c392f28c036a497f49e0634041a785fc3849c80ccf396d096f/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a545775cfe815855ea32d7c27731d79da358ef2055b4a25830231b1622dd18aa", size = 277221, upload-time = "2026-08-15T08:16:39.62Z" }, + { url = "https://files.pythonhosted.org/packages/58/ad/b9aecf38d805cbcf84fa94f14c5d972a16561e20296a11dc799a5dcf3763/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:494b70049a4d69aec6e8137c13af4cf8db8c9f9820a1392ac293b0dd2987a818", size = 263799, upload-time = "2026-08-15T08:16:40.885Z" }, + { url = "https://files.pythonhosted.org/packages/b7/23/b38a20598d5a825f85d9d7636860e56ff0db1479f86497a6e485aa9326f7/charset_normalizer-3.5.1-cp310-cp310-win32.whl", hash = "sha256:94fbf1c0c6cc0d3d5e50f9a9313a8cdca90dd696d34b381cd1704f8c9e939f20", size = 182037, upload-time = "2026-08-15T08:16:42.198Z" }, + { url = "https://files.pythonhosted.org/packages/d2/21/83fffb77864408b8bf0fe1ca603926401d6f8775a8e150b39aacc9958f8a/charset_normalizer-3.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:be47f99644b208bff7766314013f9acf57b056b04191d570d68ad14022cf5b1d", size = 206030, upload-time = "2026-08-15T08:16:43.787Z" }, + { url = "https://files.pythonhosted.org/packages/86/2e/b93135b5034b1157fb29554b0d06d4844ce62282f0e0a14036f93d7ee2e7/charset_normalizer-3.5.1-cp310-cp310-win_arm64.whl", hash = "sha256:a6d095662e73e74f0a49988e0593373e243e3a52e27bfeea0a859e88acf4a0f5", size = 185092, upload-time = "2026-08-15T08:16:45.177Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b6/034f6802e9c3f6418966cfabb7db8c9252cc2429c5098f41cc43af804149/charset_normalizer-3.5.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eda059b6bc8bc0812d626fd91a7ce01bf583df0a61296eff390fd94141a34e30", size = 363585, upload-time = "2026-08-15T08:16:46.646Z" }, + { url = "https://files.pythonhosted.org/packages/d5/fa/6a7e2a7c4b5451912b8c417732df79574354443592a88d616de03da66ae5/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa2bb0b37202dca27175591f761108b5d34096ade1191ffe4808bdf6b1571488", size = 251189, upload-time = "2026-08-15T08:16:48.287Z" }, + { url = "https://files.pythonhosted.org/packages/a4/c8/ab42b07cfd82e919f427fcfaa7c41abae8242833ad1aad66d42bae40b669/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b2b1b3fa5670c127b246df1d0c059defd41f689a868a3b9d79df9b1cac42d22", size = 239724, upload-time = "2026-08-15T08:16:49.67Z" }, + { url = "https://files.pythonhosted.org/packages/e7/80/b9348b5d3041209f98b4cdad7655766369233f1d533f4f4f7558e9717bec/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6e5e4d73d588ca5ed09df1b7dcd1b203d1df3c542e3f50d126c947d432b10731", size = 280078, upload-time = "2026-08-15T08:16:51.228Z" }, + { url = "https://files.pythonhosted.org/packages/82/38/083a24028304bc85bb9e376fed801178423dcbb67495f73b6ea0624e1894/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b54e7e13267d49ffbfe68e25b3cbd774dab38fa37238f71265e91b36146eb21c", size = 276650, upload-time = "2026-08-15T08:16:52.625Z" }, + { url = "https://files.pythonhosted.org/packages/0d/35/731ac04aa0a097fc1c97f0994c375bdb230c6c96619db794208fe664e9ce/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7b742bf31c88566b4bb6335a7f393bb322e580b6bb98df7bd0c25e6e3519ce8", size = 262325, upload-time = "2026-08-15T08:16:54.085Z" }, + { url = "https://files.pythonhosted.org/packages/f5/28/c2028e7021fb89c6e56868ed0e387b8e9aa811abdd2ab3208d6578d2c930/charset_normalizer-3.5.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6ba32c4d2abf1d2fe7cf27d280f4cca5664233b0f885549c7761719eb977f486", size = 261140, upload-time = "2026-08-15T08:16:55.604Z" }, + { url = "https://files.pythonhosted.org/packages/28/f0/0c0ceec6d98b7daa62e361e418135d59685811d79ba11529aad5cdf15e84/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0722590aabf9dc6a6c0343d523c05458fa2b5047dbe6302fd526bb570600753f", size = 252791, upload-time = "2026-08-15T08:16:57.103Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3e/48f4cd187b1c33189d86039e9cbe4f92c05454175504b44ff81806d4d1bf/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:aa1099b956fb795e686d073568f6dc002a0bb89765ea6d5b055dd7d9bf1b116c", size = 240730, upload-time = "2026-08-15T08:16:58.418Z" }, + { url = "https://files.pythonhosted.org/packages/42/85/f9e22af69af67c54cce42be9455d9c81294f918b4ccc454db01f66efcac2/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:bd6c173f04743d483881bffa1478d5a4624475b8cd1d2194956a75548e191c18", size = 280791, upload-time = "2026-08-15T08:16:59.918Z" }, + { url = "https://files.pythonhosted.org/packages/fd/4c/9044135f42127630b6fa742feb51256353f6ab87a78f2fdd1de3de955a7f/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f298e218441525d3794428b4c8b8fb8662c6d3ea79925d4807ee6b9a96a3bca5", size = 259598, upload-time = "2026-08-15T08:17:01.421Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ed/1dd7cfebb4e75812934c49ca3b79757d11948053f7937ab7070c151f3c55/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:6e2912d4babbc65196ac13c2f53468dc57fb8b9c25ef913e8c59ddf7c6dc0e1b", size = 278217, upload-time = "2026-08-15T08:17:02.782Z" }, + { url = "https://files.pythonhosted.org/packages/bf/eb/239c84503cc9e3ba6eb34686a24bc66e84f3924efdd7e38e751a19f6bc10/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3d27167433c0d5f18dc850f07d0b3816221984fecdc405d6c157a6f0b8f8e9e6", size = 263417, upload-time = "2026-08-15T08:17:04.216Z" }, + { url = "https://files.pythonhosted.org/packages/37/ab/4e4510e1e288478e2c8333131d1c1382382ba8cd2165053c79e39d1da961/charset_normalizer-3.5.1-cp311-cp311-win32.whl", hash = "sha256:ac00177c4831ffa650f8609e4bdddd5fe09c03b1c0c47acece7e6ea20421598b", size = 181774, upload-time = "2026-08-15T08:17:05.58Z" }, + { url = "https://files.pythonhosted.org/packages/e3/57/32f0ccea59e8612057c61d6fd22ef2cb63cca93c9fe594094919696ac170/charset_normalizer-3.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:f9b1e28d0e8dbfa858abdba91d6b547beaf2df1a59bec6da6faae7b96a4991a9", size = 206653, upload-time = "2026-08-15T08:17:07.075Z" }, + { url = "https://files.pythonhosted.org/packages/17/d4/b65c433fc521e58b5f54293982a5e51c05cb5f2dd3f1c7a6acb65b75324e/charset_normalizer-3.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:ae31a1a1db2ee6cc2942fccaf695c934bc7f3db9f2133a3fef1f367cf1a4ab10", size = 185630, upload-time = "2026-08-15T08:17:08.502Z" }, + { url = "https://files.pythonhosted.org/packages/30/27/78873dc8b6a56357517b74b6bb9568b80450e7bb4f6ef7e3fa9d22aa0bd7/charset_normalizer-3.5.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5b6d1386bf0096d26d3a863dc0a487a5b4eb9aa93cf5ba69683d29dde6b9d60f", size = 344456, upload-time = "2026-08-15T08:17:10.072Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4c/be49ada26b1f0232d57aa89bbebf997a5cc2332a5616b6eca26ff680044d/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4582c27e8c889d64811987b5967fbd3ae0c823fe1fd933b543d55ac20bb475fa", size = 238530, upload-time = "2026-08-15T08:17:11.563Z" }, + { url = "https://files.pythonhosted.org/packages/76/84/6f1290fa07ae6978d3960caa3eb1b8019bf9284ab7c2297b00c099ef4250/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1d1c7a53a6c2103925cdd6d7229f8c567379f211c869793df679f2e9f738c369", size = 230200, upload-time = "2026-08-15T08:17:12.919Z" }, + { url = "https://files.pythonhosted.org/packages/e7/a0/47b18adeed31c8f16ba9700f32c1b18594cfa09f47eb672a488c273c22bf/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e6621fb2a4988d6e53eedc455e5903e2679f3967b8acb3d639f1b63c14a2e893", size = 262222, upload-time = "2026-08-15T08:17:14.571Z" }, + { url = "https://files.pythonhosted.org/packages/38/fe/341861ac118dae06f3ec0eb487488af52128f2ef2faf0b11003944d22259/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7c0c10730342b0c9b35dd1d619beb8214e520bd96a1f870f452680b238aab3e0", size = 258951, upload-time = "2026-08-15T08:17:16.158Z" }, + { url = "https://files.pythonhosted.org/packages/6f/89/bb5108dc6c3651dca963f2b0a3ba19bbcb370c94e1b6d3e0e844a58e6dca/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9af956078716df40d985fb0dfeb2c2120c5ca92ba4ff4b388acfd01cdc14d08", size = 248801, upload-time = "2026-08-15T08:17:17.683Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ba/ef83ae3aca816393decfa3530976f38a79812d707b80b580ac33b83f9877/charset_normalizer-3.5.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9f8405c2c758532c74fed975dbee57be1f31a6e865c031870c79a6ed3212ada", size = 244070, upload-time = "2026-08-15T08:17:19.191Z" }, + { url = "https://files.pythonhosted.org/packages/f6/0b/c5292a2462d69b7378ea89793bbb5b2b6fcf6f7dd6d1667f9619094ad553/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:96fef3e886d6a9874b14f27fc193fbdc69d5d8035783d86aa4e1cea594e695f9", size = 240110, upload-time = "2026-08-15T08:17:20.547Z" }, + { url = "https://files.pythonhosted.org/packages/46/22/111e5be3b740d5c2a5bfcedb3d237b6591e5c2e82ae9d6ffcb121fe0909c/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5d8531a6569d025f68e2321e7638fb7978f23db58e5f69f56913837aae03816e", size = 232836, upload-time = "2026-08-15T08:17:21.895Z" }, + { url = "https://files.pythonhosted.org/packages/f9/d2/d2aad6fe0dbb44b194bf3becb60f5a0ac48446ade999a47fe7bb41eb09a7/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:aae2ee51122d3ae968a3837d97dc24a0aeebb0dea23694422cd172bd30017cd6", size = 262712, upload-time = "2026-08-15T08:17:23.727Z" }, + { url = "https://files.pythonhosted.org/packages/35/5a/337e4663a5eae6de99db940ee8066d4145caafb61327db62deda15313cce/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7235dc28fc6dd9d832ac7c7bce95367dedb85929f17368a0c2bee1e080b9acbf", size = 242977, upload-time = "2026-08-15T08:17:25.157Z" }, + { url = "https://files.pythonhosted.org/packages/ca/85/f82f8a92e31c7519410e2e1afdc630f28ec47490ce2c09a11c1a43cbb459/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4abdc5f9ad448c1ecbfae2974b820535d6bc6e7eef63babbab3d81cf46968c71", size = 260207, upload-time = "2026-08-15T08:17:26.602Z" }, + { url = "https://files.pythonhosted.org/packages/b7/52/643d11ffd60e9ac2fd1fb87e167a19285b9eefeff4a40e63c87cbfbeab36/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ba501e667c17d8411f98e67a022d9604ef179aff0e459b7e292c796837c13573", size = 250562, upload-time = "2026-08-15T08:17:27.971Z" }, + { url = "https://files.pythonhosted.org/packages/62/16/46556278c2168d12df9da7fede5dc6fc70e60301b26a82bbeec238c9cfe3/charset_normalizer-3.5.1-cp312-cp312-win32.whl", hash = "sha256:cfa1c0cc3a8f9f53f1243a5a99ac36fd003880199383b37672e86ddda9cb07e2", size = 178507, upload-time = "2026-08-15T08:17:29.277Z" }, + { url = "https://files.pythonhosted.org/packages/9d/7a/4c6c298171e6b3e745633180ff59350fc0ca0db1ffd28df1e369e0579f71/charset_normalizer-3.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:3617ac3cfd8b9888f145ad89dd6e692285834b0201c6074a5eeaad3fd4d668c2", size = 200551, upload-time = "2026-08-15T08:17:30.668Z" }, + { url = "https://files.pythonhosted.org/packages/cd/d7/eb95a042f0dd22e304b0b6472b154f3546a1a039a9ee89ccb2a7f61591fc/charset_normalizer-3.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:88e85ab89cb822c1e635f51d6d32e488f94e002e70e2f492bdb8b945543f345a", size = 180700, upload-time = "2026-08-15T08:17:32.028Z" }, + { url = "https://files.pythonhosted.org/packages/bc/61/2cb6ad133dbbb449fa2d37ccae973232f4827e799af258d15e589a3d1e9e/charset_normalizer-3.5.1-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:4f298bdadb8f0b9e5672877f647d1be9373ef5320c9e2f049795e26cad28b6a9", size = 211584, upload-time = "2026-08-15T08:17:33.597Z" }, + { url = "https://files.pythonhosted.org/packages/18/57/a305c968be1ca13f3dd1b32f445877e97addf55d80b65c7cb35fac82b777/charset_normalizer-3.5.1-cp313-cp313-android_24_x86_64.whl", hash = "sha256:88ca277405c2d3b71c4e1c2ee0e7966e807bcba86a69d11e19ba199d18ae4491", size = 223359, upload-time = "2026-08-15T08:17:35.022Z" }, + { url = "https://files.pythonhosted.org/packages/09/0a/d3646670292ce8d8f8cc11ac067d44885e697a5591f57a9221128da5e7b3/charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9362dd90aa7dab48c0054a21187791ccf05473f7dba5d92b8033ae62164675e7", size = 194464, upload-time = "2026-08-15T08:17:36.452Z" }, + { url = "https://files.pythonhosted.org/packages/de/93/d51ec556e01042fed6f993ea859311bc7917b466684182fbbceb6ca24762/charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:977cdbd483a9cff38179bea4fd754289a6f2195c7abd414aba85410b3e66cc5e", size = 197676, upload-time = "2026-08-15T08:17:37.819Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a0/562247944386f7d4ef94467e84876600cc1e0f1b93239aaa9213d2bc3cbd/charset_normalizer-3.5.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e90251c0c7bdd54a100a0dce3c07b7e637278c93af29dbf78ebb89a58c4bac7d", size = 340473, upload-time = "2026-08-15T08:17:39.303Z" }, + { url = "https://files.pythonhosted.org/packages/31/e7/1d994be1b93d41e9502b8b0460eaa88a1dd8df335df415db87d6c3e91ab2/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94d78ecec2605a8d0398b0f365d5f12a63248438516f5dac536a5eff7337df4a", size = 240156, upload-time = "2026-08-15T08:17:40.66Z" }, + { url = "https://files.pythonhosted.org/packages/09/53/27923ce5cc6cbccb832037b27dca98882d9c53e9b69e866bbbef4aae7fc8/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d59b75732e9b6f27388e10c14b0259cc5f2e48c78627d185e6a177b58ad3cffe", size = 228246, upload-time = "2026-08-15T08:17:42.003Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/5a97e84d63af1d55c07439cb80e56d99a8efb4295700eb4e18c0d1615d2c/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0d929fc574b4d6fd9e7c0f5c2ede8716a41911923aa7fa5fce38e0818aa4a1ac", size = 263660, upload-time = "2026-08-15T08:17:43.627Z" }, + { url = "https://files.pythonhosted.org/packages/7a/c2/071575791dcc88316c0a9a65ce38897a82e4cfe4a325f0f7fe1b1ac47bcf/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:394fea06235c8543390050ed5f529187074b029fb027213f6c46ac11ab5d950e", size = 260354, upload-time = "2026-08-15T08:17:45.094Z" }, + { url = "https://files.pythonhosted.org/packages/fb/af/63240b0c0248c075c2535a1f1bd992821d8251b9f173abc13329661d09e4/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62b55f6722735a6c472f88361cde6640608773d9443cebdbb51abf436a1fcdd3", size = 250638, upload-time = "2026-08-15T08:17:46.496Z" }, + { url = "https://files.pythonhosted.org/packages/4d/66/70dfad64f15be09c15ccfee81330a7e515895dbe296dd23114e9a231268a/charset_normalizer-3.5.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa48b1b63d639f9483e0633e092f5851e2348c352f1f9bb6c8182f87884ef876", size = 244583, upload-time = "2026-08-15T08:17:47.963Z" }, + { url = "https://files.pythonhosted.org/packages/c0/24/ef36367d38b9ddd4bccbf72888c342e8de1f5ae506fa0b2dcf970e2732a1/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c71fb0d56c920c269cd3e2e3fe7c610e3f1fdb21a6ce60efa6430ff63676cea6", size = 242038, upload-time = "2026-08-15T08:17:49.481Z" }, + { url = "https://files.pythonhosted.org/packages/db/ab/55e683ba0fff2e43adafc10daa3001eac90fdaa419a97227d5a7067eedde/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:485a0d363cafefcd2538a73c7c838daa2035f09b2c9f9b5e3133f80c6aeb84c2", size = 233677, upload-time = "2026-08-15T08:17:50.845Z" }, + { url = "https://files.pythonhosted.org/packages/bd/67/0f40eaf8d1b6e7cf15e82382a2965efaca787fc1c2794b7021d37aaf5036/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c0ea61a470e070686aa30892fed79e297d2c8d0ab46b8bcdf027d38c51da591", size = 264491, upload-time = "2026-08-15T08:17:52.61Z" }, + { url = "https://files.pythonhosted.org/packages/5c/64/12b4c2a11ee8df4fcc518c78b0d93e3a92bd3d5253d1617ce74ff0e8c7ef/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:90b7481fb62fbe172c558bc6fd1c4c98d82004a54a7551f20e11ac9bf0b8708c", size = 245196, upload-time = "2026-08-15T08:17:54.023Z" }, + { url = "https://files.pythonhosted.org/packages/37/2e/651d910af6d0fba325eee1cda37ec5443462ed25360e666c144166eb6091/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:35fe081843b35aad20ffeccec3eeffbe637b15d14f3fb22cc1b59cd8ec17e93c", size = 261660, upload-time = "2026-08-15T08:17:55.491Z" }, + { url = "https://files.pythonhosted.org/packages/90/c6/b09e05e6db7f64338e0dc067c79577b1138da86c1e38369096851d96be88/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fd0350afdc3aabd5576f60ea109228bd5538139713c7b094c5cd27c73a98bc6f", size = 252618, upload-time = "2026-08-15T08:17:57.025Z" }, + { url = "https://files.pythonhosted.org/packages/76/4e/362d4f9fdcdf5556fb2aa3ce7d4a58ebce03ed1ff03aa1d9aca8d02f13f3/charset_normalizer-3.5.1-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:9d9a0dc7cbe9bec24c3f767c9122c41fe5a1bc43f47cd099d00d393e09769de4", size = 140362, upload-time = "2026-08-15T08:17:58.425Z" }, + { url = "https://files.pythonhosted.org/packages/b4/d4/703be739b26acce318bd29eb3b25b7209e1b1f527f9eae3d1f1f01fdde2b/charset_normalizer-3.5.1-cp313-cp313-win32.whl", hash = "sha256:d63600d620ad0064c3a748b950ac5ea38a80190e5498532efefa4b7b3f1da1f3", size = 177755, upload-time = "2026-08-15T08:18:00.037Z" }, + { url = "https://files.pythonhosted.org/packages/8a/33/56d97ade41c8db611e727168c52ae46c9224c362ec28d4b65d7e9869e8da/charset_normalizer-3.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:aea996a6aba25260827c9ea511d1addfde2da9eb686ac961838509086188b7e6", size = 199295, upload-time = "2026-08-15T08:18:01.506Z" }, + { url = "https://files.pythonhosted.org/packages/5b/75/5b20dd1e6573a01a08158fe104104fa2c8abf941745596954185726cd46c/charset_normalizer-3.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:fd0a274c0e5f9a21565cd9d3dd749b61f96b7aa1e20a93aa1ba4029518f2e5c0", size = 179856, upload-time = "2026-08-15T08:18:02.929Z" }, + { url = "https://files.pythonhosted.org/packages/29/cd/2b812ce5e888f1ce69a5350281e58aab07ae64a958ecae8912f30865718e/charset_normalizer-3.5.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:774d157f112367ff4abd29019f38f023c24e00e56edc7829c20e358a5a913ad8", size = 212318, upload-time = "2026-08-15T08:18:04.403Z" }, + { url = "https://files.pythonhosted.org/packages/9e/4a/a6ee107430768a5334e6d63f31f148a04a1a491ef161a1ac9415a73f2fa8/charset_normalizer-3.5.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:26422d45fd13551cf564c58932f7d72b4f58b93b0fcf18c35ba6be12b46bb102", size = 224897, upload-time = "2026-08-15T08:18:05.997Z" }, + { url = "https://files.pythonhosted.org/packages/c3/d9/35ae3f64f29d0179c35c3baefe575904df2913dde519129c7f75995a2b1d/charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:09a7bba9f739468c8e78c36a75c33768e53cb1959fc638f510454c14683f00d5", size = 194848, upload-time = "2026-08-15T08:18:07.397Z" }, + { url = "https://files.pythonhosted.org/packages/74/76/f2fc7380f056cc273a53af37f50d08ad54b2c59f61078f31432edcf1c2bd/charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4c9548dc78002099910abaebc0a72ac58b7d30931869e0351c09b507dff4ece3", size = 198163, upload-time = "2026-08-15T08:18:08.989Z" }, + { url = "https://files.pythonhosted.org/packages/e9/40/095ce62fa078483cccc1fa2b36e6bc9580b85422a20ee9f925341c50e44f/charset_normalizer-3.5.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c428c6c31eb5f4277d7f8eccaf767fbd548ddd5ce3c8b4f4cbbfab3d96b5904c", size = 341823, upload-time = "2026-08-15T08:18:10.458Z" }, + { url = "https://files.pythonhosted.org/packages/f1/5a/0e58b1c04a1596e0256f407274a92d5fb2ee21324409d1fab1da48a65b5b/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f06b7eae9dbe77fe1d644ca244dad508de8d302870a43f3c559b521270938a0", size = 242458, upload-time = "2026-08-15T08:18:11.989Z" }, + { url = "https://files.pythonhosted.org/packages/22/95/b4618ce912e6db0b1aae89ba788e38e8a7eba0f3025cc66e8c0699f977b2/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b7430cf5728e68f6c462254009a6ef4086e1bea43cf2f57aa9c55fb4f50ff96", size = 226717, upload-time = "2026-08-15T08:18:13.401Z" }, + { url = "https://files.pythonhosted.org/packages/8a/76/c681192bbda3d55356db5dadd64381d5202b37c6b598fcda5282e88b5d3d/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab743e9bc90c1f73552ec33e10e3331315acd2c397b36065b591b0181de533cc", size = 266111, upload-time = "2026-08-15T08:18:14.961Z" }, + { url = "https://files.pythonhosted.org/packages/88/be/55127bfca72c0cff6c022488d140d7c5b04c771e3b72e9bdb4836d54979d/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6f7deae3feb4edfa2efaf7c574fe88cbf055038a6abdb40188e4fff66d5699f", size = 263128, upload-time = "2026-08-15T08:18:16.515Z" }, + { url = "https://files.pythonhosted.org/packages/e0/91/39c3af510b0aa32bbda03374259200f28430febfd1bf5e511fe765282ce5/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15f024313246a4ed976c60f440bb8d257815513a681d212ff74fd46f7d715a90", size = 251240, upload-time = "2026-08-15T08:18:18.127Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a5/cbe418bbc6ecdfc3e05a0116002897c4b403a5e838d697e64c78e9f0190d/charset_normalizer-3.5.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:823f82903d189af463d7df250ef1f7f696f3cee08cc8d91deb565e8d425f6506", size = 245282, upload-time = "2026-08-15T08:18:19.625Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a4/689bb42e8e7cd492f3cb64907c6bc00ad247ec9a3628cd3f8eed126e8ae1/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:01e93745f7f219b703b60ba7afead36cfc4242782be5af484673fc500df12da5", size = 244597, upload-time = "2026-08-15T08:18:21.121Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ce/9962938e179cf9f699d3f1e7b3114b5d7642dee6a893745229f9dd04f274/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:329fc3ccb63ad22d867d84c2adea759a64079a37ba4a343433b02c7a2816871e", size = 231376, upload-time = "2026-08-15T08:18:22.57Z" }, + { url = "https://files.pythonhosted.org/packages/85/54/46000450ada53bd9eac5429a2c8c54cd2d9b39c0c255f229aea9af0948a5/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:bb57753e36e4855b8ca375069482250a6246372331a3e4f3407eaebb007443f5", size = 266715, upload-time = "2026-08-15T08:18:24.235Z" }, + { url = "https://files.pythonhosted.org/packages/3d/bb/618749d70f792b44252a777bf89bfb86823b9bbc1ea13fe8ce759b07f38a/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fce8cbd4997efeb450bd298b54f755dcdff18d496f7a5ddbb4867c6d7c88fdc3", size = 245848, upload-time = "2026-08-15T08:18:25.726Z" }, + { url = "https://files.pythonhosted.org/packages/7e/3f/ffb64458527c7668031d5eb095d978de561958dc9f5b53f8e488a533e603/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6c9cdde8becb25a7fde49924511aa2644d6f8081cc8df8e9452724303348d8e3", size = 264521, upload-time = "2026-08-15T08:18:27.193Z" }, + { url = "https://files.pythonhosted.org/packages/4f/ab/74a55fd803916a35ac461daf002708191aac19b546b80dc8cabfedc63d98/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9ac4444d8d4fd4c4bd08bf451ed3167aa9e7ec6cdb41b648794f1d1103652e36", size = 253054, upload-time = "2026-08-15T08:18:28.568Z" }, + { url = "https://files.pythonhosted.org/packages/a0/2a/6a9034b7d3c60b17499afb482df5878bf9fa20b50cc3887d5ef017a833db/charset_normalizer-3.5.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:f03ac127268b43ef4fe9e6ab6794a6794b49485a0cc0c1db79876d2f33f75bc7", size = 140580, upload-time = "2026-08-15T08:18:30.214Z" }, + { url = "https://files.pythonhosted.org/packages/f3/46/1d362e1a00d035d66b9869e1281eee115907f7e390a16a07824ab5737360/charset_normalizer-3.5.1-cp314-cp314-win32.whl", hash = "sha256:1f5883d77fd409a261abb5dc8ccbe335720d798b1de4abb3b1d47ccbbc76b53b", size = 180325, upload-time = "2026-08-15T08:18:31.877Z" }, + { url = "https://files.pythonhosted.org/packages/7a/7c/4938c329b6a9d446f6a59aa2092ff7118f274209b5ed0e26893d1d30a63c/charset_normalizer-3.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:c658c50ac0c98cd755a2dd50b7977d3bca7df401dcc47fbdfa87db53ef7d4e8b", size = 204175, upload-time = "2026-08-15T08:18:33.466Z" }, + { url = "https://files.pythonhosted.org/packages/ac/33/eeb384dbd8dec570661354592f4f2e1b2fcc92585624d146a000caf53841/charset_normalizer-3.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:4bea7f8ebe90bbd7f0e4a2de42ca6924ba23e3e76418c408ff82f1d46fabd687", size = 184123, upload-time = "2026-08-15T08:18:34.913Z" }, + { url = "https://files.pythonhosted.org/packages/1c/6c/c73fa9d5a85f6ab05395de61c5f6984e0a9ff40bb5ff888d46dff02526c6/charset_normalizer-3.5.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:fbc597639158fd7c14d55e808718848319540f51b0e6746e3eefa59723a4a348", size = 381682, upload-time = "2026-08-15T08:18:36.349Z" }, + { url = "https://files.pythonhosted.org/packages/30/c7/63565f860921457feba93bae6c86fb7746deb4cffeed2f375cb845318146/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e71c909f353863b2b89c83de2ebed71ea6d0df8a6ef65a128193c5e650766bef", size = 240826, upload-time = "2026-08-15T08:18:37.887Z" }, + { url = "https://files.pythonhosted.org/packages/06/ae/7ae8807410dfa33f8e6f1715740adeaafa8a816cc4cb33508f54b1f7c896/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7ac76cf9afd34929d76eb7fcb63be476a4853d8a96f0dcf2d0db68a0cbdf9885", size = 227861, upload-time = "2026-08-15T08:18:39.315Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a3/887c1642f0da26000b0e0652d91071113c0e72cea33952e225cf589f49a9/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a3a370082ce34d0612f421e15fe011c53bb1feff21a26d06ad4fb244dab5a375", size = 260758, upload-time = "2026-08-15T08:18:40.88Z" }, + { url = "https://files.pythonhosted.org/packages/3e/11/e6f5b9a3d0e55b0ef7505cd3765cdd48f22db89994c947b316f52f801fd8/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:256dd4d85d9e4dc595e2bc983c980e73f62ddeb3165c58b4c3dfe78c5c8548c1", size = 259950, upload-time = "2026-08-15T08:18:42.351Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ee/e4e10a94d51cd1ee638aa7e00b65399e6b2a4e8376ab6d2eac9f95586671/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:58d4aa13a59c969dbfdf9e6a9560e242cbfd9e8a8f50c2747714df1a423adf65", size = 249329, upload-time = "2026-08-15T08:18:43.914Z" }, + { url = "https://files.pythonhosted.org/packages/c4/25/d5f4198819e6059735a84e8d0bfb72dc33976da67b97adcd3fb5a5e07ec6/charset_normalizer-3.5.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0c6dfb5ca6723eeed15aa8e564a014d69fcb8812f94eef11fe3631e0508199f5", size = 243137, upload-time = "2026-08-15T08:18:45.368Z" }, + { url = "https://files.pythonhosted.org/packages/a5/e9/e925ca7569cf9fb9701fd82503fee73eea5268fdb856bdd64947092d3daa/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c010f5581d9c612804cc59fcf7b524b707fbcb72828551237ab545bb5c7034af", size = 242820, upload-time = "2026-08-15T08:18:46.842Z" }, + { url = "https://files.pythonhosted.org/packages/34/17/672c251a888ed2aebcdd2fe830ad0104e25ff83c43f5c4f9c15e9fc6853c/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:52ec005752a56ae79547a05c0139ca2501a0c866390b6115008456b9f0e7cde1", size = 230504, upload-time = "2026-08-15T08:18:48.353Z" }, + { url = "https://files.pythonhosted.org/packages/3f/fc/f6a85abebd42ce4da2f1db0aa56cc6a0df1995e318b3875d14401b8381d1/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2bced4061f000f7187254a02ad3433ae17eaf991747ceea2f478422590a5bba9", size = 263087, upload-time = "2026-08-15T08:18:49.859Z" }, + { url = "https://files.pythonhosted.org/packages/98/66/7c42677e739ba66746b297e2046918d793078094dc239e1e72768cffccc6/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9eea3ab2597a5e65fe65296e2d6a84570845a6b55532d90333d740d48bbc850a", size = 243269, upload-time = "2026-08-15T08:18:51.601Z" }, + { url = "https://files.pythonhosted.org/packages/de/d8/a50b79237f417af10f8c2a501ce8d1ca87829a22e69117891ca4ba20a69e/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:496846868fea80e479324862fa877f02411f2fd0f83b79ccee2607aa68b2a032", size = 258766, upload-time = "2026-08-15T08:18:53.23Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1d/0fc91aeaeb3c83b748f532399ce67cf84604b48297405d740000f7a9e786/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:85d5855daafc240cc045c026d7a15fd198a09b0fc8ff6f5ecbb5297b509cb11e", size = 250814, upload-time = "2026-08-15T08:18:54.768Z" }, + { url = "https://files.pythonhosted.org/packages/ae/10/3d8c777cf9024615295aa1b808324ad5b4a77855869c00824bad74ffaf8a/charset_normalizer-3.5.1-cp314-cp314t-win32.whl", hash = "sha256:58d3e12c88e0950bca850ae1f7c256055c097639c2edb9eb123af9807d8b15e4", size = 191074, upload-time = "2026-08-15T08:18:56.305Z" }, + { url = "https://files.pythonhosted.org/packages/4d/81/ae557d3c44d1a1d688696d60563413a0866a91b7ebc50f20df838be3d8c8/charset_normalizer-3.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:acaf604462bf330b0d07e7a07c1d6e4adac79e5fb13e9c5140590542cafacc00", size = 216476, upload-time = "2026-08-15T08:18:57.889Z" }, + { url = "https://files.pythonhosted.org/packages/27/e9/61c01fb8b804692569c036b3fc50495814502dcf13a60649c6055390b02c/charset_normalizer-3.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:fdb8a068947befafba9952162645dc2fecaeb400e64584829ed5e9b2fbe21a7f", size = 194115, upload-time = "2026-08-15T08:18:59.418Z" }, + { url = "https://files.pythonhosted.org/packages/5b/97/fb4e82231aba271ffd775a1b4993b0defc4e3059f286ae41d9433409fe85/charset_normalizer-3.5.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:41876ee62a3dddf48ff1121ad8f0798032aa03f2fd35f21f34a4cab14f18d8d2", size = 331467, upload-time = "2026-08-15T08:19:50.959Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2f/fe3f187327aac18e2d54e9d2b08e15d27bf9b642d9e51c219f130fc34d1a/charset_normalizer-3.5.1-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6dac12ff6b846103483683f60c5f8fee205121adc58ffd87e90a90a3af69e99", size = 253057, upload-time = "2026-08-15T08:19:52.654Z" }, + { url = "https://files.pythonhosted.org/packages/d7/c7/9e48cee5c161fe24da823b61bf381921d77cb994a0a4de148e95018c1984/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cee5dd7c6fb5dd52a0fe2a740f9bc6e3593f5f8b1788bde49de02086f30182b2", size = 240930, upload-time = "2026-08-15T08:19:54.163Z" }, + { url = "https://files.pythonhosted.org/packages/49/e0/716601f3cc69be7b198951150c75ead1ece33c3c8036ff6ffa46029659a0/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:343fb4f2821043bd87095f7b08a1a181febc8e36ac64212143bbfd0a0e1bc235", size = 230822, upload-time = "2026-08-15T08:19:55.807Z" }, + { url = "https://files.pythonhosted.org/packages/d3/05/71bfc5caa0abcc45aea1f6a4d50ac68e59605ddc7666fe8494f4cd229665/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ae4a097991662cd4fff0ddc74e0fe7874f82e00042fa0ea00855645ed0c79598", size = 260037, upload-time = "2026-08-15T08:19:57.312Z" }, + { url = "https://files.pythonhosted.org/packages/c3/92/de7e32ed05341e7a9c4c877c318418197b7f2d66a3b68d561bf2ac57ca3e/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b599739b93b2cbeded49645ae3c8d1405c29ddfbceac1545c87a3f9580a9e96", size = 255097, upload-time = "2026-08-15T08:19:59.056Z" }, + { url = "https://files.pythonhosted.org/packages/f5/7b/ade0a122600319dfa0b1000ab0f9731c94a817904cf3c5de408c73a4ede7/charset_normalizer-3.5.1-cp37-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b39b69b347e5e47a3b5b8cfc005c68c1ba347474e3960236c4944a8ecd174962", size = 250166, upload-time = "2026-08-15T08:20:00.612Z" }, + { url = "https://files.pythonhosted.org/packages/75/9c/019fbb9f4834491a160951349b1a3714439376f66e5f7cf18b4f18f0c7aa/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a2028475ba855475b8b4d3cfeb4994269c967aea8b9892dfba907f4263a863a3", size = 241821, upload-time = "2026-08-15T08:20:02.321Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b8/11d4840bfc99330cc7fbcc2681ee5a044553a6e77655508d8f9b2bff7b34/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:36047af20e17097c3bb9476c2b7655f2f7aa51322c0ba58c07695bedf755a950", size = 232529, upload-time = "2026-08-15T08:20:04.008Z" }, + { url = "https://files.pythonhosted.org/packages/18/96/2b3a21492d9f65171ac75d872f5018260013d00bfa0ff70ec9f179148cbd/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4c4fb141a727957c93edfe5c32a26ceb6b5f6461d67146e2d39f51e16170bea8", size = 260348, upload-time = "2026-08-15T08:20:05.877Z" }, + { url = "https://files.pythonhosted.org/packages/d6/aa/a69a2028e8bd052476c245460ab19d7de595de084dd968f2d75cd50c3e25/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:2f293479cce755c75f1697e87c409b7ae4c555c7dfecb6e988ad13abba943031", size = 247234, upload-time = "2026-08-15T08:20:07.487Z" }, + { url = "https://files.pythonhosted.org/packages/35/8a/3d130aeabcaf3d2466af76b7b141c08d9e89c9016ab4b7cdd0f7dc2d1c62/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_s390x.whl", hash = "sha256:3588e376b3ea2eea84976f67273d679f229e24c66dce7b82ae45aef04ff6e072", size = 256917, upload-time = "2026-08-15T08:20:09.142Z" }, + { url = "https://files.pythonhosted.org/packages/80/c2/a7379b840292d0c1ab9fbd17d1f3967aa81794dc95bc74be8999d7fedcf7/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e199fb99720074809a7720f1c0b4d919eea8b87e88713e0f8f602f7bef543d9d", size = 254846, upload-time = "2026-08-15T08:20:10.727Z" }, + { url = "https://files.pythonhosted.org/packages/01/65/d43b714731bb2f40d4053dfa00ecfc1c5a301f8e3316c5db3a09af59fe94/charset_normalizer-3.5.1-cp37-abi3-win32.whl", hash = "sha256:dd732602a7009217f658d5863d12d79d373a4de0eebc111094bcdd3bb8e0a6cc", size = 174216, upload-time = "2026-08-15T08:20:12.334Z" }, + { url = "https://files.pythonhosted.org/packages/35/4f/b911ed898b26a09789eba9c9200c999aff6c61b4bafaf4838e56d1a1e1a3/charset_normalizer-3.5.1-cp37-abi3-win_amd64.whl", hash = "sha256:70055ff39b97c99e7ae40ea3e393fb62aa2e44dbd9b29f8d14f42fb0025c3959", size = 199764, upload-time = "2026-08-15T08:20:13.908Z" }, + { url = "https://files.pythonhosted.org/packages/f0/a7/920baf467bfd9bf689f3b318340f37aee4572a71f162bd8db51da55ba4fa/charset_normalizer-3.5.1-cp37-abi3-win_arm64.whl", hash = "sha256:87e4f41d375c0b9be2fb5251aee4b8a689169e134535aed81bf085c3b647451e", size = 287318, upload-time = "2026-08-15T08:20:15.551Z" }, + { url = "https://files.pythonhosted.org/packages/cc/61/d01fc49b8dea277640b55a9e15960dbca9fdc8c9fde18e572d39c59f4019/charset_normalizer-3.5.1-py3-none-any.whl", hash = "sha256:6df0ec430f9a831772c23ca5a224cba36517a58a84bb32c32bb59a9fa67c47f6", size = 68658, upload-time = "2026-08-15T08:20:43.306Z" }, ] [[package]] name = "click" -version = "8.4.2" +version = "8.5.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/0e/7fa0ef50764b67090eca4114772a2abf8b6148198475e54c660b97caeee6/click-8.5.0.tar.gz", hash = "sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34", size = 382235, upload-time = "2026-08-26T13:33:14.56Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, + { url = "https://files.pythonhosted.org/packages/58/50/6c0d534c5f134586a8e1ba4e330569e32f057e33372ae556463212fb4cd3/click-8.5.0-py3-none-any.whl", hash = "sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360", size = 125251, upload-time = "2026-08-26T13:33:12.928Z" }, ] [[package]] @@ -224,19 +265,12 @@ name = "contourpy" version = "1.3.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'emscripten'", "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } wheels = [ @@ -313,102 +347,167 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" }, ] +[[package]] +name = "contourpy" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "numpy", version = "2.5.3", source = { registry = "https://pypi.org/simple" } }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/5a/a55177dd22553a277388e8a1b3220e92de91bacb28356cdc73caa240121d/contourpy-1.4.0.tar.gz", hash = "sha256:20156f5a1ac4f8ce02656e39a61e82164a3d359796dc8026f75b062783d500e1", size = 13323726, upload-time = "2026-09-11T19:05:05.808Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/ba/01bde9753bdbba04a6da9d2bff3881f021bc708f654655e95fae26d3b3a3/contourpy-1.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:186ba929df36d61b6127da2e89cd1357e4cb79aca647381ab1a6feb0b152877b", size = 297302, upload-time = "2026-09-11T19:02:45.103Z" }, + { url = "https://files.pythonhosted.org/packages/dd/f5/c5bf67522d49a2222f3154fe46879451d26f0e35ece41770758a64ea78cb/contourpy-1.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c76f3a5318164db7d9401132fc1b5364b784c613c93fa506e3b5e6d1bf353ec8", size = 284921, upload-time = "2026-09-11T19:02:48.049Z" }, + { url = "https://files.pythonhosted.org/packages/0f/cc/cb989599eec12fda312e127eb8e04a8b21a7a6c94bf7ff1bc68273334a42/contourpy-1.4.0-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:43c3ccbb32c6294b183dcc8e8c46dacf5ecef809497e3d48a5be298eef185ad0", size = 356332, upload-time = "2026-09-11T19:02:51.408Z" }, + { url = "https://files.pythonhosted.org/packages/47/fb/6f620d7602507817b0b4c21dd790ed68688f1f4ae9c29aacf21f08db5cf1/contourpy-1.4.0-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:86d05cec773c9507a3950122e0e40ce77c23c75ceeb2fc189514e71893cbb34b", size = 405878, upload-time = "2026-09-11T19:02:54.093Z" }, + { url = "https://files.pythonhosted.org/packages/32/4e/693c6d6bdece679f0953775eef3a1b66d56be55478d936e7deb1f3004422/contourpy-1.4.0-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2deb580178ca19437a84bd77e4bc2cd91a8ccad212413a83c273900868b5978c", size = 408296, upload-time = "2026-09-11T19:02:56.528Z" }, + { url = "https://files.pythonhosted.org/packages/78/f9/b6831508960d559581c448532ba4df312217072975486dbf2b24fcc5b76f/contourpy-1.4.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:875f42444c9cf48d56f724f2637e60d0f73b3b12c9041e1484580a233edf9591", size = 383243, upload-time = "2026-09-11T19:02:58.638Z" }, + { url = "https://files.pythonhosted.org/packages/f2/21/52903825a0ae7bb625e8bd30a09816d4c3c5d6174a469c10a616166cf780/contourpy-1.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f863c6100bf926cf47d13f3cd75f9bb8ebd98aaab230eeb4468b91f98f39a6b3", size = 1357306, upload-time = "2026-09-11T19:03:01.036Z" }, + { url = "https://files.pythonhosted.org/packages/d8/79/d68b1ce8539e4071518fed9523f558398f34dcd078b8927b109c72dad2ef/contourpy-1.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3863ef2e2b13fe93f8c0ebb08ee400cb07153b8b0e91c5acb26e4537f283634f", size = 1427243, upload-time = "2026-09-11T19:03:03.866Z" }, + { url = "https://files.pythonhosted.org/packages/ac/f0/b75a10e9d0616b97a30de277b6dfe83f1879880854c7330337309530eb48/contourpy-1.4.0-cp312-cp312-win32.whl", hash = "sha256:5450f091ac1be0be3ad3a2a3b3f23b5e443e78c670ced4fd347d626f92a28fd2", size = 347345, upload-time = "2026-09-11T19:03:06.192Z" }, + { url = "https://files.pythonhosted.org/packages/50/a9/dab08786bb4d77ef9a046b3c1be923be4e73c5d07d91f5a54e8ea9b09e41/contourpy-1.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e697d94e69f499ff6bebb899cae97a58d5d14f0e1fe9568b43a0248d2f9af8c", size = 234083, upload-time = "2026-09-11T19:03:08.122Z" }, + { url = "https://files.pythonhosted.org/packages/47/b9/3ba509755a970dbde5948e7142ac21f266be72f38017cc4087a05fdceee1/contourpy-1.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:0c7a4c2716a4e98342221954416a836cca77c996c14ddbf22b5f01d5d93ca09c", size = 559521, upload-time = "2026-09-11T19:03:10.047Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8b/62a6eacea08ea0b6ab0159a7783df7ed421ff9c1ff58fef7c31f58fa5e52/contourpy-1.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e21d1d4a9db5b3a793649c7ab0ddb4697f2818929eec871a7b9c45fce7b2a1ea", size = 297330, upload-time = "2026-09-11T19:03:11.809Z" }, + { url = "https://files.pythonhosted.org/packages/3f/2f/103cdc5b7077cd569989d3ddb99ee3d4e96ee31bde8df2a6e426b2dc6195/contourpy-1.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:196759a3e4db60e1546167e361090ebb6d199aa3aafc9e34ea17a5ac3b23e814", size = 285037, upload-time = "2026-09-11T19:03:13.781Z" }, + { url = "https://files.pythonhosted.org/packages/bf/25/97001d1fdf940e6f5adfc592d4f5899fff95daf7dceead3988fe09987d2d/contourpy-1.4.0-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c0e07c691f3b3321913ed9b8161c50ea5f77fadd006f52f5e755359b0dcbfc5", size = 356675, upload-time = "2026-09-11T19:03:15.374Z" }, + { url = "https://files.pythonhosted.org/packages/a6/5e/9a7241a89e2f74402aefa2569e1349ad2750ff77823c04c788eaaedb7fdd/contourpy-1.4.0-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7f861af508fca2384eef392bc1d8377cfa349b2b854cb11b45d89c75780e2561", size = 406990, upload-time = "2026-09-11T19:03:17.44Z" }, + { url = "https://files.pythonhosted.org/packages/36/81/3a4fbbf6a806eada5453a6d16f6c7d96b9f4b3e57a3127368fb545986b8a/contourpy-1.4.0-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a03b32c2e7eda8b17757c022162c13c86f5c3d6f937dddf9ba3c3ff7b513953b", size = 407271, upload-time = "2026-09-11T19:03:19.523Z" }, + { url = "https://files.pythonhosted.org/packages/68/77/8f93ecbde1a4d11dfd460cd19a0c159f5487352e751fa87f5d78cc9db08e/contourpy-1.4.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f1219a8898523cba821085f2da8a1b962cc696325763f09d7f93538ba43b2d70", size = 384707, upload-time = "2026-09-11T19:03:21.208Z" }, + { url = "https://files.pythonhosted.org/packages/05/07/e5cdf94d68961ee464de6b4fedd3d2f666584f59ccd124bb1290597122ab/contourpy-1.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:20da2f86bfaed60dbba729fe738c8241b1dd22b6cf7e8cbbf30df290bd04ba28", size = 1356741, upload-time = "2026-09-11T19:03:23.621Z" }, + { url = "https://files.pythonhosted.org/packages/fd/8a/2301d07accb257ac08daeb631875fada06fedd019c20ff7b00ee07e9d211/contourpy-1.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6ced1670fafee703b8277ab1645072a226e74f167166eae146fb743916119b67", size = 1426793, upload-time = "2026-09-11T19:03:25.955Z" }, + { url = "https://files.pythonhosted.org/packages/7d/5d/a62648f06259d15c139b14eb8cf3c58fe53cb970a55f2bb77d1070e80a41/contourpy-1.4.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:abd0f7e51ecc52bf8c95713bd80b1c17c516a71fa391c54475308a4377903c2b", size = 117961, upload-time = "2026-09-11T19:03:27.713Z" }, + { url = "https://files.pythonhosted.org/packages/71/31/c5b7771ba6982f319ec90c0cf5c425b36891adfeee2d9f667f90b2d0c166/contourpy-1.4.0-cp313-cp313-win32.whl", hash = "sha256:510f7d93d94cf6ebf4e2cd0640bea16ab8affac7547230175b6745b596ce0599", size = 347438, upload-time = "2026-09-11T19:03:29.829Z" }, + { url = "https://files.pythonhosted.org/packages/68/c1/a59df0754bf83a6f3e153cc19d550a5b01f607e389b1c4d57f436f9139e0/contourpy-1.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:6507a016976a75a15aee47809a34605de6244ccc056e6164bf9fb14b27eecad4", size = 234084, upload-time = "2026-09-11T19:03:31.657Z" }, + { url = "https://files.pythonhosted.org/packages/41/48/97224f5decb1efa5a610f912195deb45321f73ec9b9dac485cd6fe2d5a6a/contourpy-1.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:423bd5f4b3f11d54a8c597234e513382e3454bb106b8ec7ae32ba4ed63f8d99b", size = 559559, upload-time = "2026-09-11T19:03:33.528Z" }, + { url = "https://files.pythonhosted.org/packages/75/b0/b548628c8dddf6e5e0a981b7fa8e4c008024df09c7d17c6a9e271edfa0b3/contourpy-1.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a3e67bc1a6a4618a1dac7c3053a9ffece5ddfa2046b2670b342cf430bb0b87d1", size = 297354, upload-time = "2026-09-11T19:03:35.017Z" }, + { url = "https://files.pythonhosted.org/packages/04/5a/513484208742f65af2648c519f0b7ab034616e183c4403538024c136a1ab/contourpy-1.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:072a702e2e178f4fcf96f04775d0c059e4c917925abf0d3208ebb6865df3c23d", size = 283729, upload-time = "2026-09-11T19:03:36.818Z" }, + { url = "https://files.pythonhosted.org/packages/35/da/5a6562febc994b2c4bf9d01e57a50458ef8a2051bb3e7b35a00333e0fc26/contourpy-1.4.0-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:404dbcd9233513dfd1323b66ea593fef90e899f658f6b8a9ed8e93bd0ca669db", size = 357819, upload-time = "2026-09-11T19:03:39.058Z" }, + { url = "https://files.pythonhosted.org/packages/6d/99/7b358ce888aaa426f755daa096c99c59d17c352037344e836f20e5d3b957/contourpy-1.4.0-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:61624f6722480aa7e168fd746164e7bfdf48f8ccb6542f1613741200dc37e2b1", size = 407795, upload-time = "2026-09-11T19:03:41.293Z" }, + { url = "https://files.pythonhosted.org/packages/be/02/34d7548adf08c60967435d79d7a90882eacc84f5093435287974c7e99943/contourpy-1.4.0-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2e741a39dfc96babe1722561e81351aeec104526c57dbe91c167c1db606c2d55", size = 408552, upload-time = "2026-09-11T19:03:42.728Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8c/9677dad226e5aaac5b823d5b09d881ebc32ff1648af417597649841e516c/contourpy-1.4.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e439ab450c93455feb0218532ded3e946ec7a9b5f459069f122cfa44b89229f6", size = 384850, upload-time = "2026-09-11T19:03:44.402Z" }, + { url = "https://files.pythonhosted.org/packages/08/f5/e3fbeaa489c223629fbb434737cb3de4ecaca9cae4825e77b92490f640f8/contourpy-1.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:417be048f7e122cefbe2a34c51a1f2b0410f8eb35796f794d45475ddb7872d9d", size = 1357601, upload-time = "2026-09-11T19:03:46.746Z" }, + { url = "https://files.pythonhosted.org/packages/88/be/55fec293d342c222ed58a5d4056df4362e192af2e59527a3d5d822de8828/contourpy-1.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:43d80072a32299bf945de0a6dd4e034ea0162e832261de2e07a274a5adbba9c9", size = 1427340, upload-time = "2026-09-11T19:03:48.745Z" }, + { url = "https://files.pythonhosted.org/packages/86/40/0faabf453edf59b0e3633381d7ca72c3414fb9b72b5cbb08b06ed792d3e5/contourpy-1.4.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:fa1b787362a3856e63dd2b89449f6c384d55ee1beb8f94f4bcc871b40f02462c", size = 118355, upload-time = "2026-09-11T19:03:50.047Z" }, + { url = "https://files.pythonhosted.org/packages/38/0c/7ea75aa3559ae8a6805015e15a19c92b6480752ecc92526361e5776e88c5/contourpy-1.4.0-cp314-cp314-win32.whl", hash = "sha256:738c44fa71735a617f36da58e32810512407d283d5d1bb5b1cecb00d7eeba7cf", size = 356977, upload-time = "2026-09-11T19:04:11.401Z" }, + { url = "https://files.pythonhosted.org/packages/15/41/3df8cc14bb572c8b447f9dfbc876703f0f2f2896058285b5c87a1d4a638d/contourpy-1.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:181bea01bc742734ae672fa00c717d855dd35e1f029536406538c52e7b0cd73d", size = 240101, upload-time = "2026-09-11T19:04:12.772Z" }, + { url = "https://files.pythonhosted.org/packages/66/01/ee6830a5aa4565662a345172f6f35eeeed625d20f1c9d4ecd35318fe4ade/contourpy-1.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:64039341e2d8804f1a13bda8c69083eab935167bbd7e856cf096241f17d5bd45", size = 576359, upload-time = "2026-09-11T19:04:14.983Z" }, + { url = "https://files.pythonhosted.org/packages/51/af/49ee9c9cf012699e505c1b63c531cc99f19df50f205173092c4d541e3ec7/contourpy-1.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ef47fef9a912c77e3d84b014f701e5a9340f25703e9ed3bcebe37f91fb69dc49", size = 313601, upload-time = "2026-09-11T19:03:51.452Z" }, + { url = "https://files.pythonhosted.org/packages/ad/59/ce411ab2be0f805038626d8f4ca9ce563487ff51ff6c363e7512271564ed/contourpy-1.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:96019f059bcb3774acc0104be3360a57b36d33eaaa2e1d7f77c800ca8e07618b", size = 302812, upload-time = "2026-09-11T19:03:53.022Z" }, + { url = "https://files.pythonhosted.org/packages/95/b0/390915f9af14c1e2d3b2a9ad99d85b98df4352346a51e368cbb825f813c3/contourpy-1.4.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ddf5a2596d716fd3793434844caf31abc7a40b1e8718431420c89858268fb909", size = 348996, upload-time = "2026-09-11T19:03:54.594Z" }, + { url = "https://files.pythonhosted.org/packages/c7/85/e0952576d54322f3e9983b1e049a7a31f42ce82c5c29d01821e6dd3780d7/contourpy-1.4.0-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dd59df9e2fb0aff8bd7fd1adb9f349b7c835245a9d0f18c2f2deeb537190f2b1", size = 400272, upload-time = "2026-09-11T19:03:56.822Z" }, + { url = "https://files.pythonhosted.org/packages/d6/7d/f13543a5e1598e4a4ad01d9d5e2d65f9c09c37660616429913d606c5977d/contourpy-1.4.0-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b7794ea07cab575633daad8d6e963b662053391022294d3aacc1d9ba9ef54114", size = 412895, upload-time = "2026-09-11T19:03:58.487Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a8/6e5eaff53507dfd2dfc428240eecda32453067277d4b3e7ca71210b2d611/contourpy-1.4.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a1c8a74744fa746eeaa12f0f9ed7f8b4add9d8f1b14d95e3b5e133675eac888f", size = 377060, upload-time = "2026-09-11T19:04:00.122Z" }, + { url = "https://files.pythonhosted.org/packages/16/a3/d00e44d35511b3cead5bd794ee672d4eecf30d9de4ceac57a49d6204064f/contourpy-1.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:107ec46f7aa1664266d69b1181aadc953d58d39f7d8d648fa5b795d4a060b0de", size = 1345688, upload-time = "2026-09-11T19:04:02.705Z" }, + { url = "https://files.pythonhosted.org/packages/1e/01/1e533b0cae32c8edd63fe40435ec3b29345dab87321afcd28722d7f957d1/contourpy-1.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5a4c89c7f38a0d7a94356d74e3391073c4325c4d47ad2fd065c3fe7dbae16c0c", size = 1416140, upload-time = "2026-09-11T19:04:04.77Z" }, + { url = "https://files.pythonhosted.org/packages/6d/0c/4ee66759f00d6a5d6909782f14f441d639fa18a4a43142e334351efc47f8/contourpy-1.4.0-cp314-cp314t-win32.whl", hash = "sha256:912c6afaa106e2f74ba22b30416b77ef7eb94e3bdc5a4df51ab147845dba9b6d", size = 371982, upload-time = "2026-09-11T19:04:06.233Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ac/b3e5324138c3e3741f526663f4f265f4f69dfdf26415a39428940864a5f6/contourpy-1.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:ef9440f6f8506246269a82734f5ff9e2e4c5e775b3996fc883cc491c5257eca6", size = 262907, upload-time = "2026-09-11T19:04:07.882Z" }, + { url = "https://files.pythonhosted.org/packages/19/37/c9aa45e47819dc15a38fc5c81a2fb987fde55e9d3b991fbde514e3b6b5f5/contourpy-1.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:fc9feef8f1f001c5b87decadc67c4a5d1eebb62ca39c4763d1237ff62cf2b707", size = 587071, upload-time = "2026-09-11T19:04:09.898Z" }, +] + [[package]] name = "coverage" -version = "7.15.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/be/c3/4f2195f512fb172aa425a8803a874b2baa9ba7f80ff7b6080998761fc701/coverage-7.15.4.tar.gz", hash = "sha256:0548198fff07ccf4faf469520bce1c2eceb1ce3e62891921138dec10907f9d00", size = 936952, upload-time = "2026-08-06T13:50:24.442Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/30/70/b052a519a584663a7bd052841a2debe11c8309ec49a7786340003f9c0a02/coverage-7.15.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d0be6daac4cce6b8c8dc65886bae1b082ddbca4da8e5cbb5e15166acf253e264", size = 222245, upload-time = "2026-08-06T13:46:55.253Z" }, - { url = "https://files.pythonhosted.org/packages/67/39/892fa511aba3d1c3c8f49509a0ff5c71eab9f9f88d08e1a38da395821660/coverage-7.15.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b24e078eabcd6a9caa8b0713f9bc1eeb310bcc960a29d45a3b4fcd4b16d5b11d", size = 222762, upload-time = "2026-08-06T13:46:57.848Z" }, - { url = "https://files.pythonhosted.org/packages/9f/95/b2c724ce1e64bc23cb5b1d7eeffa9548dc3d811f7a6297b2d01607f4e062/coverage-7.15.4-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cfe20cc8cf8821d4fe54f89106cbf06aa27f37b5bbe3535568065a81539b4150", size = 249498, upload-time = "2026-08-06T13:46:59.012Z" }, - { url = "https://files.pythonhosted.org/packages/0b/4f/b1973f67a1382af65b572a31ed692f8e490a6ad707191eab59148376832a/coverage-7.15.4-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:83cf06cdd687677742caff1a9134833b7a8b75f111519d2cb0e0ba1b9a851e15", size = 251328, upload-time = "2026-08-06T13:47:00.764Z" }, - { url = "https://files.pythonhosted.org/packages/a2/09/03efa6722a132abcac91b32a60b64b240dd707c189c64eee697e48992c96/coverage-7.15.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8fa4de68e2a752468ff14b4e15db7def689a71be759e826a31ccecbef69c5fd0", size = 253194, upload-time = "2026-08-06T13:47:01.976Z" }, - { url = "https://files.pythonhosted.org/packages/45/63/8299201d9c80fb65551ce99c966cab83d706ec4066ac999bef08201346de/coverage-7.15.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4dff9daa47d83120c3ec38ce921214242944a832aa04e903e50b5b7ebac8972d", size = 255106, upload-time = "2026-08-06T13:47:03.281Z" }, - { url = "https://files.pythonhosted.org/packages/ee/16/26fd8a691eb8d9a230128685f6d23309d7402cb030aa553001788c8c50fc/coverage-7.15.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a093fd37229918976f602aa07aa59e0973cde82186f220c8e197f721f5be0ce4", size = 250177, upload-time = "2026-08-06T13:47:04.713Z" }, - { url = "https://files.pythonhosted.org/packages/ad/ef/3c7556f33783a0a566e01443ca62bd8eb2cdfe22d271efdc02e08beb5654/coverage-7.15.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:317db01a2cb02552fd67e2b1cca77a4b528a2a277176c5e0bf2cecbb639d3f54", size = 251234, upload-time = "2026-08-06T13:47:06.104Z" }, - { url = "https://files.pythonhosted.org/packages/29/49/640a34043edac950738f36a3567832db5731d4cb2ed84b59cdb89c6bccbf/coverage-7.15.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:8ee3838dcb656602c3b51e16aed9bfb0822f8d8d6d1c5966d32ec8c104be8e20", size = 249237, upload-time = "2026-08-06T13:47:07.467Z" }, - { url = "https://files.pythonhosted.org/packages/48/f5/e80f212669dd1be954ff844f883ef11a437ef4fd0089c6e0effc7b66b15d/coverage-7.15.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:425920379052ff1fe465268f3361d35804a241bbdd5a1b592c8cb60df4c52325", size = 253050, upload-time = "2026-08-06T13:47:08.748Z" }, - { url = "https://files.pythonhosted.org/packages/c7/e9/e5da0fe39f7fde1bca9edc09c60921bb5fdba4cec7db5bbad41ddfd8c230/coverage-7.15.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:69bb2400abef928e365ea7d4d9925169ada78ed2295546780002d4b65de3df88", size = 249508, upload-time = "2026-08-06T13:47:10.072Z" }, - { url = "https://files.pythonhosted.org/packages/7d/38/41bf25774a0c8bba6b467f917cb1c9a0a2605e02dc93aad489fc7050ed59/coverage-7.15.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:81661f82d302484e3119e7c80c519c02fa9bcc2a6b339baf67d67bc89c580f04", size = 250110, upload-time = "2026-08-06T13:47:11.35Z" }, - { url = "https://files.pythonhosted.org/packages/89/6e/26f2e54b79acc29d179ee4272922625aedb69198c4eb61f7ff4f098f3c78/coverage-7.15.4-cp310-cp310-win32.whl", hash = "sha256:cb476b2e828ecb71cb6b6a928d23fd20a7ddb501188022dae1c37499149cc338", size = 224294, upload-time = "2026-08-06T13:47:12.753Z" }, - { url = "https://files.pythonhosted.org/packages/7b/06/9a318fc3ae040d4d6cb2d86101c6aa963fab20899a5c58666adf52cde0ca/coverage-7.15.4-cp310-cp310-win_amd64.whl", hash = "sha256:3fc2130bf37df31852a8384f12601563a45a0024bccc6624f38355cba7a8d360", size = 224919, upload-time = "2026-08-06T13:47:14.17Z" }, - { url = "https://files.pythonhosted.org/packages/2a/66/edcec7d7a0b524aa8923e22925fde6fe50ce005a113dca13ae1581455c4c/coverage-7.15.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bbac5abad70df71019988f83f26ac7092ff2642975def4429e98dc7585ef3490", size = 222367, upload-time = "2026-08-06T13:47:15.578Z" }, - { url = "https://files.pythonhosted.org/packages/e6/c6/ab8de429e2e8548faf58ec7e1674a4ce00414b4113942d3fe87109cf0f68/coverage-7.15.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:357a173465c7ce028d07a95cc2b63b5bf59f50ecdd5ad75c5cbb78ada984048e", size = 222874, upload-time = "2026-08-06T13:47:16.961Z" }, - { url = "https://files.pythonhosted.org/packages/be/c4/3b7b49587e8a6b9af79b3eb468d443d6042b6d65b47aa26586846a0d6566/coverage-7.15.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:21b803935e2efc3acebe9697197a294fccf5dc4e5382bd6369542ff7a7d2a1d7", size = 253287, upload-time = "2026-08-06T13:47:18.291Z" }, - { url = "https://files.pythonhosted.org/packages/fb/65/ec03b743a2a229c72cc1eff3e57be9d3564e9c6b4d5aba2d70744a3fc0d8/coverage-7.15.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7a2b580774a4786c1053157c0165e04476e03ff293993d7c148eee784a94bae6", size = 255199, upload-time = "2026-08-06T13:47:19.765Z" }, - { url = "https://files.pythonhosted.org/packages/41/4b/5163729e4b6582d61975cfd3ccab45b4ec53e21cf156d9941cb025188468/coverage-7.15.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a9464451c4efffe8d47ace5a540b10b0dc10e879066290f8600872b7f54a419d", size = 257308, upload-time = "2026-08-06T13:47:21.206Z" }, - { url = "https://files.pythonhosted.org/packages/86/08/2167a0f08fb87d702fa423a48578a32865464b7c9e1db3911ad7812ab414/coverage-7.15.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de602f34123c2f4af1c1869c6dbbbd60da6d5983bf01937367295d135cccbfce", size = 259268, upload-time = "2026-08-06T13:47:22.503Z" }, - { url = "https://files.pythonhosted.org/packages/1e/e5/68eebae3053dbd48508edea559c21b23fbdf3460784f91370c83a86a6acd/coverage-7.15.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6879ded16a27f3eeca19b900c147e81616e7054db451471a611b2755ee5249f7", size = 253392, upload-time = "2026-08-06T13:47:23.88Z" }, - { url = "https://files.pythonhosted.org/packages/1a/46/fd4ced40a2b691c774e515c9b69500bfa64c7960b67fcee4b2f6fad97fc3/coverage-7.15.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:986be58c3ab54aae8d3496a6225eea74f760fdbe739b38bd442c7e8d133aa53b", size = 255001, upload-time = "2026-08-06T13:47:25.469Z" }, - { url = "https://files.pythonhosted.org/packages/53/25/ae2e5fa710bb6957a9aadeb9e3598d3b3e4af6587ce857ad42e8639a3f30/coverage-7.15.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c6103639613fe6c1e989082948419bc77a2d26b6c825c99d7fad25f7d3d87afc", size = 253061, upload-time = "2026-08-06T13:47:26.845Z" }, - { url = "https://files.pythonhosted.org/packages/d7/31/67ddc0365db2c6e93ac8580bc4bbc50f65273262f973f63ebcdbc15c0495/coverage-7.15.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d3af93dddb5659276c63bc16ac6466ac2033a70ca816097bbc06345b8ccdf571", size = 256831, upload-time = "2026-08-06T13:47:28.217Z" }, - { url = "https://files.pythonhosted.org/packages/f6/78/82b8fd18f57fb13f12d98fe874995bb2c4f9f17be8aff762c426323fdb96/coverage-7.15.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:b10075e5421d04265766a6d1dac809bbeb8a946fbb23c8f82c227409b2190719", size = 252781, upload-time = "2026-08-06T13:47:29.712Z" }, - { url = "https://files.pythonhosted.org/packages/0a/eb/6c74ef4dd12b252e573c49bdef9e2ac265bf3dbb79b8d7feb3266e084e9e/coverage-7.15.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a67a9f78b2942d87ba8ce3059c642164d2aedd65337377fb52fe9803656bc5c7", size = 253692, upload-time = "2026-08-06T13:47:31.192Z" }, - { url = "https://files.pythonhosted.org/packages/5a/66/eb9aed1c3fd2d36ee00eb173f434b14fa607fc056739c9a89ff4244010ea/coverage-7.15.4-cp311-cp311-win32.whl", hash = "sha256:69484d1aca26e322e1c3ce03f09341e84524ababad2d7202161738d83cc9f82e", size = 224461, upload-time = "2026-08-06T13:47:32.572Z" }, - { url = "https://files.pythonhosted.org/packages/e2/6d/81fa4161dfb3ed9d74e40d58647eff83a56b7612e78352581280fce2f477/coverage-7.15.4-cp311-cp311-win_amd64.whl", hash = "sha256:63fd6fcd1dd6e158f7eb78606e72933b3f6d01e7b747f99c6c12d764307a0fdc", size = 224937, upload-time = "2026-08-06T13:47:34.205Z" }, - { url = "https://files.pythonhosted.org/packages/5b/c1/d8dacf683c6cad3cf85ce68fd3774a6774ec402128822fdfaed920f11e6a/coverage-7.15.4-cp311-cp311-win_arm64.whl", hash = "sha256:ea82116c9893fa89e929b7f197ee5a1950a76e91cc5c85ba503fc02379d04890", size = 224479, upload-time = "2026-08-06T13:47:36.118Z" }, - { url = "https://files.pythonhosted.org/packages/1d/48/bc8d4ba7b37551a767bd863f15b3f80182b271c2f55975356f5f7dbe94c2/coverage-7.15.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d4fedd1f7f428f9fe83b1ead5e7cc87a43427be31aadafbac3ac0636dc7abb22", size = 222543, upload-time = "2026-08-06T13:47:37.562Z" }, - { url = "https://files.pythonhosted.org/packages/20/dd/88d6f83f1fffc974a3691a34a97951c5b12df7512a6782c5963883cbc058/coverage-7.15.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:37e2f0cdf58e2e1fed4e4d5a8f8786ae2f7eb80b478016876667dc4a01d60a97", size = 222905, upload-time = "2026-08-06T13:47:38.927Z" }, - { url = "https://files.pythonhosted.org/packages/bd/5c/54ee0d4748585bb0acab9891cd8d92f2d3593165b4e59fc9de113bfb3140/coverage-7.15.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fb55d0e70bb15f2e81477613627286581414693d74ac7963c93a790dd453ca9d", size = 254407, upload-time = "2026-08-06T13:47:40.488Z" }, - { url = "https://files.pythonhosted.org/packages/8c/3f/f0642a372f494bd0d7dad3b497083b910194a5f1c88be2c94fef707c3b59/coverage-7.15.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:899b9da30f3c6c336566e3707495bb23e8302d39d862f01fa78c48b99b9437e2", size = 257145, upload-time = "2026-08-06T13:47:41.931Z" }, - { url = "https://files.pythonhosted.org/packages/71/17/8b46d0ed68251016002ec972c8fc0119961a765d0984cafb8bf317c43758/coverage-7.15.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d15715e8c46552827e5e4f30a35575a2dbcad14454cf3284c54483946bd16931", size = 258257, upload-time = "2026-08-06T13:47:43.527Z" }, - { url = "https://files.pythonhosted.org/packages/30/b8/8498a0e72d0adbe15477dd07463d2b3bb2c9f6a4815e8589e50939e2c3ae/coverage-7.15.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:002a438859f7b430bc99afeaf01a6d187dad1d0dc907b64cdeffc632a5db8fd8", size = 260517, upload-time = "2026-08-06T13:47:45.121Z" }, - { url = "https://files.pythonhosted.org/packages/41/e1/7dce19c3bdb1e3dd63e769508216500edad81bd5f69a26d724e32aceaf78/coverage-7.15.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e4193a04b518f7968f3099755f5509ee7cccc6dc2b92a6b14841934d22e222c9", size = 254785, upload-time = "2026-08-06T13:47:46.541Z" }, - { url = "https://files.pythonhosted.org/packages/dd/b1/e1494703c675a2561723cd9b89f45c9168782c31280c611b1f767851e57c/coverage-7.15.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e98dcc55d572b38e69d117da7e8e8efb8500f1f5eaf81ecd460a63220790b839", size = 256176, upload-time = "2026-08-06T13:47:48.155Z" }, - { url = "https://files.pythonhosted.org/packages/73/76/a5629d270fb638a43a4b10466f51e2f49d532c1aa4da2913cbbb150bbe0a/coverage-7.15.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:af6c538498ce66c10d3fd541c2a8d5b03da5850355add34e6cba564210cb9e72", size = 254321, upload-time = "2026-08-06T13:47:49.757Z" }, - { url = "https://files.pythonhosted.org/packages/ff/4f/9c44447218435d5766b911534f9d798144a5560f85e9a54ebe5f3f5d19f9/coverage-7.15.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1d10025d96ea89fc2f73714dbc4cbd433fe012c1ac9e23f895d7728b238b6e52", size = 258390, upload-time = "2026-08-06T13:47:51.248Z" }, - { url = "https://files.pythonhosted.org/packages/de/36/c1e127616fb3fa18a9ff71e76c417f2fd7424332a4870015ac224ef4c039/coverage-7.15.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d802e1947603162ded419bff83ac7489820355d2b856dfb09206574e3a37ac0c", size = 253894, upload-time = "2026-08-06T13:47:52.816Z" }, - { url = "https://files.pythonhosted.org/packages/e9/b9/fdb92c8ae7a8bb9b850cc253b7b3b9c8526f68130002048b5671cd510d09/coverage-7.15.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c2de40895718f91951b86712b4c5b694acaf9a0a49be13874896f599a1eed3f4", size = 255763, upload-time = "2026-08-06T13:47:54.296Z" }, - { url = "https://files.pythonhosted.org/packages/6f/c0/a7d51b2587c7bdb76e71b0896d2565bf7d60436b5122fc83e511adb1f7cd/coverage-7.15.4-cp312-cp312-win32.whl", hash = "sha256:5c3431b2161279b7db5c2a1aa58ae02e5cb8c3c42d93a5094be3f5537bd5b11b", size = 224597, upload-time = "2026-08-06T13:47:56.074Z" }, - { url = "https://files.pythonhosted.org/packages/49/b9/5c5f80cc55f5acaaca6dee677626bfcec8c87204a7809b438b08e84f4571/coverage-7.15.4-cp312-cp312-win_amd64.whl", hash = "sha256:6befeab5fb2b51c958ca4ac6c5d141a1e8240f4f76e46350f1911963deda49cd", size = 225135, upload-time = "2026-08-06T13:47:57.52Z" }, - { url = "https://files.pythonhosted.org/packages/47/e4/2a4561f89ff6bf7c925c287d0f2cce8bdf139c3a33735c87e3203401cf94/coverage-7.15.4-cp312-cp312-win_arm64.whl", hash = "sha256:67bc345491ab55b837277d76f5775d057e8c7f1ac44d890d8c2c82adde258c6f", size = 224515, upload-time = "2026-08-06T13:47:58.977Z" }, - { url = "https://files.pythonhosted.org/packages/f1/84/651a9310859673aaa3b3203f1aa1641ca60fcf2494683e1c9474c7172780/coverage-7.15.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c705b28feb2775dc82a25f1d473a370bc37ff93f5177f4e29ce2425f560f6921", size = 222565, upload-time = "2026-08-06T13:48:00.796Z" }, - { url = "https://files.pythonhosted.org/packages/82/f9/4dcf700137e8af550670f4d74d1b63828ce93e1e2b05e5f10710eb2ea987/coverage-7.15.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3ff205ab5e3ecc670f6a4dd19d9cbf12ede53dd41cfc1e15716ec961ea6d314e", size = 222936, upload-time = "2026-08-06T13:48:02.391Z" }, - { url = "https://files.pythonhosted.org/packages/07/4a/612ff1e780b3fbfd637486f542f84adc5503873d8b5d279dec1ffeef9414/coverage-7.15.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5172326e861a38b48b48befca15e0f477a26b283337a33a739c8fed229934e36", size = 253926, upload-time = "2026-08-06T13:48:04.382Z" }, - { url = "https://files.pythonhosted.org/packages/b0/04/d1cff1c2ead4708a6a79c01d3736b6a25bd38a36678398f72a8dd33dfad9/coverage-7.15.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:12b59c90084e3234fb11184886bf4a40f4f16a8c8f867be2e087b81f8e8868d4", size = 256523, upload-time = "2026-08-06T13:48:05.996Z" }, - { url = "https://files.pythonhosted.org/packages/b9/80/d34e13fb4b293cbdb9665838cf5522077b8ad14ef947550631a4bced36a5/coverage-7.15.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:349062d66f00b40fa2c1c222438bad25fabf755631b5d82937fe985c8008615c", size = 257759, upload-time = "2026-08-06T13:48:08.036Z" }, - { url = "https://files.pythonhosted.org/packages/0f/e7/2c5fe7636fdb0732fe0f09f308a5b066864078b7fc61f6678e8478554f2e/coverage-7.15.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4256ced708e598e05209bc1a8ab4074e04a51dba4c62fb45926a229af675ace7", size = 259890, upload-time = "2026-08-06T13:48:09.834Z" }, - { url = "https://files.pythonhosted.org/packages/92/28/9689f0858dfff59c2ea688938ab9fa2925631235df67126a42b6c5c70ae1/coverage-7.15.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d80f974b20782d9612c8b4c9beeca867074c7cf4079d1419843fa25a26428b25", size = 254121, upload-time = "2026-08-06T13:48:11.459Z" }, - { url = "https://files.pythonhosted.org/packages/f9/e2/785077c230c157243eb5aa9a26c3be260ecd02001bead54a3cada3df8e03/coverage-7.15.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2e179f19bfe1d31f8eeeaa12990194d761c4f62f0759661000bca6cd8729f40b", size = 255891, upload-time = "2026-08-06T13:48:13.209Z" }, - { url = "https://files.pythonhosted.org/packages/d4/90/e20371b17b40f912f21305c2db2f30efa3de306f7320fc916804872c85a4/coverage-7.15.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8bc16bb47b7679670eceff71d78bfb7d6e5b143f6c2cd117487ec7c75e0d4b78", size = 253859, upload-time = "2026-08-06T13:48:14.736Z" }, - { url = "https://files.pythonhosted.org/packages/05/49/25371987ee459a5f67c0427fb75c74f9358e65f2c71fe75bf41c1b6c5fcb/coverage-7.15.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1cd685005cd2c4200adfc14cf39a603b9320efab3f18a8f7f156d20c9cc3345f", size = 258011, upload-time = "2026-08-06T13:48:16.464Z" }, - { url = "https://files.pythonhosted.org/packages/30/6e/32e67467f6154bf4f1c4f63b05acc5097cba4237d45bbeeea446b52e8ac1/coverage-7.15.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:337399ad2c93b3acd2a937627dae8b3e86b66707cd3d3e856347999aadf1ef8d", size = 253676, upload-time = "2026-08-06T13:48:18.493Z" }, - { url = "https://files.pythonhosted.org/packages/03/c1/8b24192e89286399765155251f99ee9f070a9d637109018ac23d99b99f6f/coverage-7.15.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:96e257121228ec5cd2bb919276e94ac11074471bc37d68dbae0e8308cce15fff", size = 255453, upload-time = "2026-08-06T13:48:20.057Z" }, - { url = "https://files.pythonhosted.org/packages/16/6f/8b41ebdf67c87854e17c035336a90f1cfbad0c14c2a584301be6ff148718/coverage-7.15.4-cp313-cp313-win32.whl", hash = "sha256:c65a9e0dfc6143491879da4e13b5e30f8be192055de508d737fb14601edbd22c", size = 224605, upload-time = "2026-08-06T13:48:21.655Z" }, - { url = "https://files.pythonhosted.org/packages/e0/e2/2946c7f0b42b152ecb21ff1bdad72e3d301e790c0c487e4a86e8c9f69347/coverage-7.15.4-cp313-cp313-win_amd64.whl", hash = "sha256:2ff8f5e9b8f7a94f0c11c45631eee103dbcb7d63274edd12c56efe1be690b3b4", size = 225148, upload-time = "2026-08-06T13:48:23.376Z" }, - { url = "https://files.pythonhosted.org/packages/9e/83/3f4a69957f48ae7a0aba76c34743f88963d607b19e03f3f8e66f91cae0f9/coverage-7.15.4-cp313-cp313-win_arm64.whl", hash = "sha256:6e0a8a5083b096487d6cfced94cdd514d8f5db6f113610fb36c0620edb1028cf", size = 224536, upload-time = "2026-08-06T13:48:25.117Z" }, - { url = "https://files.pythonhosted.org/packages/ea/ac/748cf29eeb2d6be34a3176ce26a4f49e38085ee08e8935f05f6f26ed7e0f/coverage-7.15.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:770e9325ab5ea6d56f77e59b29ecfe0ac20b57a82a601876f90494a4dda0386f", size = 222608, upload-time = "2026-08-06T13:48:26.806Z" }, - { url = "https://files.pythonhosted.org/packages/0b/02/1abbf5c984677b0aa439cdacaccbf38d248939d8ef8fe1cc7a50d73edb77/coverage-7.15.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d12b33a3a50a1676b7784dc8d00a0c6d66a9f2add4b85a041c19b6a7e53ef23c", size = 222940, upload-time = "2026-08-06T13:48:28.432Z" }, - { url = "https://files.pythonhosted.org/packages/eb/e1/ff8f9f53d9fcf586125b55d0b1f04ec1c14955fee41e83d5814bee141bb5/coverage-7.15.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5669c8378ebde86f5def7a25d29586631b58acc27ffde04399f678f3dfc6e082", size = 253985, upload-time = "2026-08-06T13:48:29.995Z" }, - { url = "https://files.pythonhosted.org/packages/a1/26/595759762e514e81be1d7d01ed03444303bcd152226a6529998d253f9201/coverage-7.15.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ff97a14362eef486483ed44042ca2027ea257df6ff768e62358ee0c9776925ac", size = 256492, upload-time = "2026-08-06T13:48:31.634Z" }, - { url = "https://files.pythonhosted.org/packages/24/68/b79aabac54d482be23b5fcdd4f4662bff24a78edc4ee29201726929936d5/coverage-7.15.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a325e815318638aed1655d9c06e6d7c2d3d46c09231ce988070428a8762d734", size = 257837, upload-time = "2026-08-06T13:48:33.186Z" }, - { url = "https://files.pythonhosted.org/packages/09/0f/bf7f297885a5bf6fd71e5782404e0ff059ca09e8711ceb3a08544abde45a/coverage-7.15.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:474223409d88eb20d2d6a0d37ea60e8647a65a90cc008dc1f0410af5f64f1e0d", size = 260152, upload-time = "2026-08-06T13:48:34.75Z" }, - { url = "https://files.pythonhosted.org/packages/fd/f1/296744e854ff8368542343457414380465e9ceefb9192342feb9d3bc461d/coverage-7.15.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7f2f62ae3cd189dd2e13aece758c57b3eecbd27be070dbd4cbd10936049e5dbf", size = 253978, upload-time = "2026-08-06T13:48:36.434Z" }, - { url = "https://files.pythonhosted.org/packages/55/b0/bbdb2e9057493e66220a2e149ca2d301ba0e3a58a83bd6b90de9826d16f3/coverage-7.15.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:39ece820e29e0a2ba34b3ecb3be83c27e997eed8926f2ba6fe7ce7a0bda5843b", size = 255846, upload-time = "2026-08-06T13:48:38.317Z" }, - { url = "https://files.pythonhosted.org/packages/96/e4/38015b2b6d21258713bd17e76b59d033b191efb5703589cffd037dfbca20/coverage-7.15.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f21b56dcace11dfe013014201f577dcd592b2a9b72182d930361b47cf6f73f25", size = 253808, upload-time = "2026-08-06T13:48:39.993Z" }, - { url = "https://files.pythonhosted.org/packages/0b/64/0d515c1e60ee6fbfd1a0e79c07cd87d388a233b7adc37758735677203808/coverage-7.15.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:93a3a0b662abcc10c73a47cbc72cd60f63618d6989fb2d1286e50eacd974f303", size = 258081, upload-time = "2026-08-06T13:48:41.971Z" }, - { url = "https://files.pythonhosted.org/packages/91/71/04d9e7a3642146c6351338aef4ef85ab11dbbb54744c13245caba1aad1c0/coverage-7.15.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:141fae2cabf5569b782c10afc4c850ce10f618c13f8db54765cba99cc839da1f", size = 253624, upload-time = "2026-08-06T13:48:43.731Z" }, - { url = "https://files.pythonhosted.org/packages/b4/a7/6c28b74c81ebff66987b0e2522ba5cffa3e90b0c33cb6a2eb264d4ee8cf1/coverage-7.15.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:81294c7e6ab30c5f74c0353b11b2fd6320e72d9bee6ac73b357caa8b916323a5", size = 255280, upload-time = "2026-08-06T13:48:45.58Z" }, - { url = "https://files.pythonhosted.org/packages/52/af/bc19996a7014b98d7bbb0f0939453c67074af65784a3aa16a789a07381fa/coverage-7.15.4-cp314-cp314-win32.whl", hash = "sha256:7bbd7d6418e0dab31a206af5203bd43ae36edb8e7fba1940b055d3e9249290d7", size = 224768, upload-time = "2026-08-06T13:48:47.525Z" }, - { url = "https://files.pythonhosted.org/packages/ee/90/219484e476d6e101ba0a444852579e05f5b75c37c611a42ed1190f73ef62/coverage-7.15.4-cp314-cp314-win_amd64.whl", hash = "sha256:f0204ed122758782970526057093f448051a39db9d810d4e344bb87a3546f425", size = 225259, upload-time = "2026-08-06T13:48:49.513Z" }, - { url = "https://files.pythonhosted.org/packages/b7/66/fa77daf4e383e5f776dac62c2409b6af81910ae6fe326bd5170dba74cc63/coverage-7.15.4-cp314-cp314-win_arm64.whl", hash = "sha256:9e71e7bc71c686a123347ae47a0de33a175e797a85bb57b791492adf4eec8ed8", size = 224684, upload-time = "2026-08-06T13:48:51.235Z" }, - { url = "https://files.pythonhosted.org/packages/58/5b/f03bf0ce362bbf3f785fa5219620d00778d4ac6fc9e407734828e9c672f6/coverage-7.15.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7c922735321eef3f87c280a3d39afff6b646723a2880b862cda4ac7a093b8aa8", size = 223338, upload-time = "2026-08-06T13:48:52.896Z" }, - { url = "https://files.pythonhosted.org/packages/0f/76/e77d0ae22501831cc9f92193e8a957a5caa1dd177f90a6d1d9b106242d92/coverage-7.15.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f41c17c4668a655ce96d090d8d5ffdc24ef64b5a02f9753884d08483e8a4a41a", size = 223609, upload-time = "2026-08-06T13:48:54.688Z" }, - { url = "https://files.pythonhosted.org/packages/82/1a/b1f089da8d38ac612fa2dd6dc7f4a1a7657d12f3e261d2996edd3a838d0b/coverage-7.15.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:46822e9b6ff1c6a72b518c162c44a8f45a61a1d609c51084bf5b16c023c5037b", size = 264970, upload-time = "2026-08-06T13:48:56.403Z" }, - { url = "https://files.pythonhosted.org/packages/bf/31/e66d98d6e9c7fcc88470f1e234eaf6b1950dc0dfbf797f7282c1c861da24/coverage-7.15.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3d6f4955b73b5445271379a59e3792b0d978f42d4a01e0cf7a67d9c33a3bb0a5", size = 267088, upload-time = "2026-08-06T13:48:58.41Z" }, - { url = "https://files.pythonhosted.org/packages/59/a1/ae94eb2c541add426378408379f233591e069040b1e2cdb33df9498a0682/coverage-7.15.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3fc9e047706fb4a9abb54f719d3aa643e80e5bb3818182c40aee01ac0f0247ba", size = 269508, upload-time = "2026-08-06T13:49:00.42Z" }, - { url = "https://files.pythonhosted.org/packages/9c/c7/88a10694a1c6a213569766aba9f25847b28155d4ac731b13226db216356d/coverage-7.15.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05e491d4f3165d62d4f5c8fd48dfeabf2ae8f42cbbd484319af33ea851b78982", size = 270629, upload-time = "2026-08-06T13:49:02.234Z" }, - { url = "https://files.pythonhosted.org/packages/b3/34/d8b8232e5e55169933b59aabcef2fedfa4b9d8897361bb80fcbda146505f/coverage-7.15.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:226c66e80ec0598d3b9b4874123df167ccca342aca8714f77cac6829688ee09c", size = 264043, upload-time = "2026-08-06T13:49:04.102Z" }, - { url = "https://files.pythonhosted.org/packages/7e/35/58b009dbf8c471c7224716478b9fed4a7e1af15320e1ed41660978504663/coverage-7.15.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac41cc14bebda0dbfb0628036b7f75706935c95bcc07fefe9a0f93614aa60a57", size = 266963, upload-time = "2026-08-06T13:49:05.821Z" }, - { url = "https://files.pythonhosted.org/packages/62/aa/57fbda1b42c892968273c56b6ee9dc0f1310850859230a507bc7873b1f65/coverage-7.15.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8af623e5cd92080acddd02b38f2f406a2c3a0893c38950b211890361448fbf26", size = 264569, upload-time = "2026-08-06T13:49:07.706Z" }, - { url = "https://files.pythonhosted.org/packages/98/8a/360e6e7f24d477b7e889703af0afa878d15b6d4d8d2a822b2835c169a879/coverage-7.15.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:07545711d4f0f32852a18f18ad11f76f0109909d09e78b9008b4cfc67e829429", size = 268299, upload-time = "2026-08-06T13:49:09.587Z" }, - { url = "https://files.pythonhosted.org/packages/4e/89/6f701261aee21b6b5fa8f7872229406dc917e125069448292223bf213606/coverage-7.15.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a0865421cfdc53654b342d515e5a233187590882d20b95752150e53f65460017", size = 263413, upload-time = "2026-08-06T13:49:11.604Z" }, - { url = "https://files.pythonhosted.org/packages/3f/0f/6f04036edc260ed425af83e834f627fad48941ce97b50bfe6edd8b6fa623/coverage-7.15.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:460115e32ee40566476db5048f9bec1e842c127ad8e6f8be745aad3ac9cbc839", size = 265725, upload-time = "2026-08-06T13:49:13.38Z" }, - { url = "https://files.pythonhosted.org/packages/c4/ce/d19b5d4d5c49a7bfb925fd74310fee7d28bc99520ac3367ccbc54e662518/coverage-7.15.4-cp314-cp314t-win32.whl", hash = "sha256:cbde877ef9dd7baf272b9bfef2b8a25edd45d9170fc326951dd20eb480335e85", size = 225079, upload-time = "2026-08-06T13:49:15.265Z" }, - { url = "https://files.pythonhosted.org/packages/26/bb/7aa1b3b173faee0679037ca950bbbe1247273656697994d8d13f80f8d4b4/coverage-7.15.4-cp314-cp314t-win_amd64.whl", hash = "sha256:3da9e92d1c551fd7563833e9ade686efb0c4b7363ab7681a94283958c950bf5e", size = 225911, upload-time = "2026-08-06T13:49:17.279Z" }, - { url = "https://files.pythonhosted.org/packages/81/1c/4ea9e47426d80038d9222db3c4534cb6021a74b237d3ff97ffd33b6600dd/coverage-7.15.4-cp314-cp314t-win_arm64.whl", hash = "sha256:3a54f5a0d85050c73a38f6793090ee83974531e67fe5e57a1da9bee11398aa5e", size = 225219, upload-time = "2026-08-06T13:49:19.293Z" }, - { url = "https://files.pythonhosted.org/packages/b4/d9/e70c286c979378f061d8266e279b686ab0b0b688e1fe0af864684f23a77d/coverage-7.15.4-py3-none-any.whl", hash = "sha256:964730a1e9de9c0cf11be6a1a3c79ce419c34882842abd256086ba4698705e84", size = 214332, upload-time = "2026-08-06T13:50:22.192Z" }, +version = "7.16.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/2d/c738872f477f5687152acae68635790387425d407ae37dd3d3a8a6692307/coverage-7.16.1.tar.gz", hash = "sha256:f83981779bcf9dfa06fa0a8d4cb43e0faec1706328ce07aa3e7b665b4ac0f210", size = 969651, upload-time = "2026-09-13T19:12:21.422Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/62/a8b4dd53308a4b1571e6bdd77ee562c9b1c1742db9886c0aef2f69463067/coverage-7.16.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4f12a9e27ca7b65e40a8475d27899b2d45064d9020e6a89148939e01987b5853", size = 223178, upload-time = "2026-09-13T19:08:21.805Z" }, + { url = "https://files.pythonhosted.org/packages/5e/7a/b9325b8d486a5c05fd4a9eb1cd9c9f697c361ddeda0e73567d5c2d8959f6/coverage-7.16.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f590d46c30d9e4c1fda3efefe5443f4c2f6a4192c5ca2ba403653e9ebadf097", size = 223701, upload-time = "2026-09-13T19:08:23.391Z" }, + { url = "https://files.pythonhosted.org/packages/ef/ee/3ab3ac6e9e7165a623695b73a83d1bfbdf5069681a68a012cb5abb096060/coverage-7.16.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3df82f0a3cef4e1bcfc799436056f0b978dda319d0bfd4460c6e479b2802d98a", size = 250434, upload-time = "2026-09-13T19:08:24.948Z" }, + { url = "https://files.pythonhosted.org/packages/b6/a9/31ffc231e9879876f611cf1ac7622cf092d76762c07ca64dce47b29a200b/coverage-7.16.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb462d59146656e278d1e8ed913ce374d0ba68a4e081acde1867f6d3377fc881", size = 252264, upload-time = "2026-09-13T19:08:26.734Z" }, + { url = "https://files.pythonhosted.org/packages/c9/f4/4fb4f6b16de07dbae3d3a111ae28d32ad657ce6ff4d364c49c10962daad5/coverage-7.16.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7db888dd0a1df1a653cae7f99d4047430d2187a3626eda51bc847b0fd6b9b43", size = 254127, upload-time = "2026-09-13T19:08:28.254Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3d/048e88eb610f44b7542dac10fa45bb2ffad5fbfcdab0f03289a843da934b/coverage-7.16.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:38a7e16f061504ac2b45370bf5bf97e8250d8d3f25e37385bae884978166554b", size = 256043, upload-time = "2026-09-13T19:08:30.019Z" }, + { url = "https://files.pythonhosted.org/packages/97/59/d27b113c80b04c7fc64c69457132648e1d287b83a1de41189a504b50b52b/coverage-7.16.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2b26b55b18e1a53e1a159dd743728c4ddbbef28ba19000a91aaff5ce023197ec", size = 251092, upload-time = "2026-09-13T19:08:31.57Z" }, + { url = "https://files.pythonhosted.org/packages/45/42/b7eafaceff08dc1dcbfc924fa9233031fd00c8b8287cc4a1a0dcdedcb0f9/coverage-7.16.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fdb2f528b50953e29d22033b3256c396a700193c6e45b2490222ef9c333cbbf9", size = 252170, upload-time = "2026-09-13T19:08:33.191Z" }, + { url = "https://files.pythonhosted.org/packages/7c/8f/57dcc5360aa6d8d24370c12fe855f6ebf5fbf2718c154fb9fdec7161de07/coverage-7.16.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3284754371dc78592aa3ae4d661d30ee20e02b2b6b0de3590a181a936d0d3b38", size = 250173, upload-time = "2026-09-13T19:08:34.75Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/379a8c7ac0f3063769dc43fd2475e9fc9b2722cb88eabe71f4d862660c93/coverage-7.16.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:802d1246c540e07486d4ee1adfa19a797e4b33529ffc371dc140644e8f27da0a", size = 253986, upload-time = "2026-09-13T19:08:36.737Z" }, + { url = "https://files.pythonhosted.org/packages/45/25/70218d2a6077730843b75ee2525fa64a750283d71cc4c063dc0b75ba6979/coverage-7.16.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:6fc735d6fe6d57f803e7ba021be4dde48e43e6aff94e6954350555d5332b0594", size = 250444, upload-time = "2026-09-13T19:08:38.434Z" }, + { url = "https://files.pythonhosted.org/packages/58/ac/6c41c441baa6c8a1fed5615e4e1e7d0ca649b33b472ee28591a92d70a1ff/coverage-7.16.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ed5ade1bb18f62edace1bd198c66f9d4c75a8385d5fd24e87d917ea1a5958773", size = 251046, upload-time = "2026-09-13T19:08:40.133Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f9/01166d6f8aa42acd148b4c2c88173d60eefdd0270ea2bcbba79be35bc193/coverage-7.16.1-cp310-cp310-win32.whl", hash = "sha256:0c309096926b119543dc16438a11ef4c80783d2f4e59ff94f7f462651a944cdc", size = 225242, upload-time = "2026-09-13T19:08:41.672Z" }, + { url = "https://files.pythonhosted.org/packages/d2/28/a76060478919becf0fbea2df2053a6e1a7195f11edf8dfd51c58c1636b43/coverage-7.16.1-cp310-cp310-win_amd64.whl", hash = "sha256:7562f8067ed9360e8b9739e5703403a7686dd1b36bf0f89fc538047c54cdea90", size = 225868, upload-time = "2026-09-13T19:08:43.22Z" }, + { url = "https://files.pythonhosted.org/packages/af/3a/d09495dfd5191b5593852bbe2f037afae768157443378d066d9ecea69a49/coverage-7.16.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:72e013665e25cf9d44779f01f340af26319756f9a76822b7c94ce6b1d93813da", size = 223307, upload-time = "2026-09-13T19:08:44.914Z" }, + { url = "https://files.pythonhosted.org/packages/a3/34/310196a80258e28ebf6e5aa814a5a6ffbb18c03cf3c0c0c04191ed893e88/coverage-7.16.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cb05c0ff98b56ba6969adf35556bc43bcb8d094df8bc9cb403acff53460c4e07", size = 223817, upload-time = "2026-09-13T19:08:46.738Z" }, + { url = "https://files.pythonhosted.org/packages/73/29/ed0fda1fcd440cdfc07e72a07f3c9c3b43f65a6b3d53bff2cba9b9de50e8/coverage-7.16.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0d0ececb32090e3fbb03e0d352b973a0485879b4de6c58daf47227b9988b99e5", size = 254222, upload-time = "2026-09-13T19:08:48.358Z" }, + { url = "https://files.pythonhosted.org/packages/ac/4e/5e2507fbd71ec1047620978f39568b0d2ed6c4468d08495a0c24f5677f65/coverage-7.16.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f0ba3892d81aacf36996c52f16bca04e39af31a6c5de930b7688ab617f4a6475", size = 256134, upload-time = "2026-09-13T19:08:49.926Z" }, + { url = "https://files.pythonhosted.org/packages/7e/b1/a9f97846554bc240473656f9c3874591105f29c08eccf4282daadc6bc281/coverage-7.16.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6410b75fe07d5271eaa95fc24bd0a9177ed588d9d1c10c0cf67829adb8f0567", size = 258240, upload-time = "2026-09-13T19:08:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c3/2b1ab208d06719394851cdd7dc322157870a93b178410b1bda6cac7755f9/coverage-7.16.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d1039cb2de093225d597109342ce1675626bd127565e80b3044f4eca07c15b2e", size = 260202, upload-time = "2026-09-13T19:08:53.204Z" }, + { url = "https://files.pythonhosted.org/packages/af/14/ff4db31d6e3126d5b72aab1e868d35ec94e1b71dee46f5c3b54cb1b2438e/coverage-7.16.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cf047bc39fde5425be2628666d0f435ed8817859111c3aacc84b32d858069f5d", size = 254305, upload-time = "2026-09-13T19:08:54.833Z" }, + { url = "https://files.pythonhosted.org/packages/ce/27/c28faa4818f61c49ca3bfb4a77e1c191e79a40447504b89af0bd859cd6fc/coverage-7.16.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c05913d0d5badf7ac83200f35dcf9514cce5df16a7cc89e7d1d7fff0461813b", size = 255935, upload-time = "2026-09-13T19:08:56.453Z" }, + { url = "https://files.pythonhosted.org/packages/46/5a/42e64e5b716048cae33e2e15f8c6fe04a927467056c533b8e88900da895e/coverage-7.16.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4027bf6d7bc0a16df058ce913b69f10c5687f8e1ca668f08caa659ce101744bf", size = 253995, upload-time = "2026-09-13T19:08:58.159Z" }, + { url = "https://files.pythonhosted.org/packages/84/e5/860287acd5a1e29acd337f302b476b54148203590091b1a1555b2e914b50/coverage-7.16.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4f48b345f831eaf4402ab6333c2dc3e2e2b5bc7b9c1b8fe12680dee3f0538f01", size = 257767, upload-time = "2026-09-13T19:08:59.958Z" }, + { url = "https://files.pythonhosted.org/packages/a5/c5/4b68006da567b4b413352f891776c35776b3557a0479a28187ab57c21659/coverage-7.16.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8643baeb590726c558b2faed6cd59b0917480f9367fcc692026f1a86d824fd08", size = 253715, upload-time = "2026-09-13T19:09:01.778Z" }, + { url = "https://files.pythonhosted.org/packages/a1/0e/b2d0c47cfbf11770e197f1d0f11a92864eb29fd7eaca45bc9915573d9725/coverage-7.16.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d06dcc420b570bf683cdb647cc8fe62b672d9e429ef711c3cbbb7a6880ca1572", size = 254624, upload-time = "2026-09-13T19:09:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/2e/14/3bab8821b2b942155578fc34e88e21b0670d922f039ab23651f6035ba7f7/coverage-7.16.1-cp311-cp311-win32.whl", hash = "sha256:946f58aa59b08bcd6afcc6a7bd0ff54ed5eee844f32ad69fe6814836d15856a2", size = 225404, upload-time = "2026-09-13T19:09:05.174Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ce/4f4ce667a97d1c538b46a79b89161395ad2beffc248b6c782afc9f521927/coverage-7.16.1-cp311-cp311-win_amd64.whl", hash = "sha256:684c7ee9b4c04358fe6ac8b517ab51ec35fcd79d08ff0f105dd8bcd96885bbb7", size = 225879, upload-time = "2026-09-13T19:09:06.81Z" }, + { url = "https://files.pythonhosted.org/packages/26/17/f3dce5e47351ab34522dd037fd64b111742958e24861bdd2820971bc1a26/coverage-7.16.1-cp311-cp311-win_arm64.whl", hash = "sha256:1b24f79e25bcf6c73931aeca7a3dfc7595c0cb5e9364aba3fdf387a3de4b1c22", size = 225428, upload-time = "2026-09-13T19:09:08.502Z" }, + { url = "https://files.pythonhosted.org/packages/1a/f7/7cd4c9f2a3b7574414222ec425d5eee21cc690d867a013315e3be8ba185c/coverage-7.16.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b7f2c26ce6ce0b1e0ca0d5fae96ea510e3a2e78b7207f06e76b7f2c87fa3d0af", size = 223475, upload-time = "2026-09-13T19:09:10.145Z" }, + { url = "https://files.pythonhosted.org/packages/3e/f2/9ac65f9cedd43f91e2d3657c11f13aef82aebae89b2243dc9e44fe58a740/coverage-7.16.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070acb9da788dff743a4d36fc015feee12d68f0349959017542017c79f59c21c", size = 223845, upload-time = "2026-09-13T19:09:11.988Z" }, + { url = "https://files.pythonhosted.org/packages/62/4d/f13db452d4367fe1122abfd98ae330596f65a3b40498f8e1a5811f68f123/coverage-7.16.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e366587b370bc9b8b51b7b7272c610c56db5d5b4795b9e4a29d28ff2f440f809", size = 255341, upload-time = "2026-09-13T19:09:13.708Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6b/85ed86e82835a96ddfbe7705c387c28ce1cbbecd1b6d606f5ddfeeb712df/coverage-7.16.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e82e10b9d290f60b63459cfb245a841aec347603997206296b93881463a93dcf", size = 258078, upload-time = "2026-09-13T19:09:15.368Z" }, + { url = "https://files.pythonhosted.org/packages/cd/d2/f66945853d850b9c05f4e012e37396f1f31d0cd40d062394b229c22e7b56/coverage-7.16.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d73bb1f85c4150ac208fb0755beb04b2e44897bad81414de9380f98dd74729f", size = 259191, upload-time = "2026-09-13T19:09:17.046Z" }, + { url = "https://files.pythonhosted.org/packages/77/a1/7b907abe62461f289035c7ac60ab3e6334efb8c425151aac4abfa0c97820/coverage-7.16.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3397b9032553d281ad6a9253b12675b65e0cc8cd7a3b0633cf48872c9eb13360", size = 261452, upload-time = "2026-09-13T19:09:18.756Z" }, + { url = "https://files.pythonhosted.org/packages/51/e6/e26f4d6b1069a2a2fee3238a132f85c5b4c1dc25aebd88e015451ff0bd68/coverage-7.16.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:77890395cf37026a5907d3ad32376aa51f41c0f163b7477fdbd4f94966cc1d08", size = 255698, upload-time = "2026-09-13T19:09:20.786Z" }, + { url = "https://files.pythonhosted.org/packages/7f/ec/99db7450e813050ebce300e31bbd4f997c76a8c6e3a6d168c5dffc104109/coverage-7.16.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b0944dc3bee3091039bf970d73caaf930c906013128a421bdc132e797494d941", size = 257111, upload-time = "2026-09-13T19:09:22.693Z" }, + { url = "https://files.pythonhosted.org/packages/2e/9d/500df9d3cd8c541ac84b5e0bcab00646be75654e05aa34e8410ec851282d/coverage-7.16.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b89d22a89d5bc05dd95b64e08295b8394aa96dc88e08f8ba210c9ebfebbe0489", size = 255258, upload-time = "2026-09-13T19:09:24.429Z" }, + { url = "https://files.pythonhosted.org/packages/c0/24/53318d96da332bb4946f8ac646fa19fd37fecbd0c49144735c716ac69bf0/coverage-7.16.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:550a2a1faf7559f13d5344f12d1eb886ad87955155d7dfab2a3fe5c8ec8fe776", size = 259326, upload-time = "2026-09-13T19:09:26.32Z" }, + { url = "https://files.pythonhosted.org/packages/c5/ce/abc0462b2e6ae96ae22197d2bc30fe33b6b4e52937e782745436ceb2a761/coverage-7.16.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:55eb268e5b81aefac759766c9162625b06c1bedb7b77d936225bafc4f038a6f6", size = 254827, upload-time = "2026-09-13T19:09:28.191Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e1/0a04eedaaf19196b51f0180968134228c7646e469ed29914e48690a7cf3e/coverage-7.16.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:65a8fc80898c9ce59f04349fe8b4849b1f9787f14e52ead990e5f849ff4727a0", size = 256698, upload-time = "2026-09-13T19:09:29.922Z" }, + { url = "https://files.pythonhosted.org/packages/de/1d/d441a55cf22ce9d8e9e34814806c47441ab844bd534e0f4b64e1c6ae8bd1/coverage-7.16.1-cp312-cp312-win32.whl", hash = "sha256:528a61be40977c340cf201d23b69bd6a6bab507da60e9dbda85f8b30e935d70d", size = 225541, upload-time = "2026-09-13T19:09:31.752Z" }, + { url = "https://files.pythonhosted.org/packages/73/27/ec3d032375735dd331477caa051419678079ff90fcb53d0284a6c2bfb757/coverage-7.16.1-cp312-cp312-win_amd64.whl", hash = "sha256:d0f02c633630e2b74522108ee95a84ad6e1204a8016a6cca5297f335ea27147e", size = 226075, upload-time = "2026-09-13T19:09:33.556Z" }, + { url = "https://files.pythonhosted.org/packages/94/00/90e9f5c4434878494306b9c0ee8068ebbd829483737d8cefccc58884d728/coverage-7.16.1-cp312-cp312-win_arm64.whl", hash = "sha256:2959978f9d1d20a2c0c15d0a68baaeccf615ac1aa214cf4a05a10d6f568926c8", size = 225461, upload-time = "2026-09-13T19:09:35.48Z" }, + { url = "https://files.pythonhosted.org/packages/aa/74/c08c0c4dc9fa6bcd1d90728a63660aa1b17b488a806948598456c48f75d1/coverage-7.16.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ee5465db6e9152a7d09f3215309326878c6aa3ac509195a369f9d264ff4bfbd9", size = 223501, upload-time = "2026-09-13T19:09:37.276Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c8/784986d326663285258a8e39835c46fb730cc85284f0dbcd82078586dd22/coverage-7.16.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2b8256f8b525ba233d2e4cdcdce0d6673c66fc9bf70df1fd5e67c54a74e2d245", size = 223876, upload-time = "2026-09-13T19:09:39.385Z" }, + { url = "https://files.pythonhosted.org/packages/15/76/73fb792928872bbb07e553f920ff55c65ee962c469265feb5d1d4ae5f97a/coverage-7.16.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d57cc400275b9a2892e905fc893f732b21ddb95271bf96406c88e2f6367848b5", size = 254863, upload-time = "2026-09-13T19:09:41.326Z" }, + { url = "https://files.pythonhosted.org/packages/ca/8d/1fd78899513244b065ccecdd1cfc6aa8b8f1bdee796d8c4f2aa8e8e5397d/coverage-7.16.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6b3fd0f3435ebb7a7183b32a6062a8b755f08242ced1f3f22761d30b56b3c2a5", size = 257460, upload-time = "2026-09-13T19:09:43.086Z" }, + { url = "https://files.pythonhosted.org/packages/47/1c/b23ddfcb7ef9bd7b3fdb7be9a5dccf9925ae88e556df7aa5b655283c78f2/coverage-7.16.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e5eb1762e7eb5fad34ef913e8107c7788a66f19d328e598ce95bf7217f9e5c8f", size = 258697, upload-time = "2026-09-13T19:09:45.161Z" }, + { url = "https://files.pythonhosted.org/packages/1c/c9/19d0c6ca35778a7e9415c30c46a0c64c9d4374219d004a35563132296896/coverage-7.16.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:46cd3a73e9140410de62cceb66214bce0e08fb3922b9176fbfc1522fec151b41", size = 260827, upload-time = "2026-09-13T19:09:47.061Z" }, + { url = "https://files.pythonhosted.org/packages/97/fc/5862f7344382c62b85df43d483e579168cc062e007f54d895dfa51fe3e7d/coverage-7.16.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d1ba5142d68dd2cb775cbd0ac8601819298152047803c8efe4eec6d7d7aa7878", size = 255038, upload-time = "2026-09-13T19:09:48.945Z" }, + { url = "https://files.pythonhosted.org/packages/2e/9f/5c26583199a68df8d15124b4590520903f8eb7cef17db293fea712bb0783/coverage-7.16.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c389c6f9d1d518e1249ddcb8a7f158135644ce2c508fa6cc17b680777dad5bf2", size = 256827, upload-time = "2026-09-13T19:09:50.738Z" }, + { url = "https://files.pythonhosted.org/packages/95/13/605198bff079b107336710f28a797312ab132587168600d70a290e7ecd2e/coverage-7.16.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cdc57746c7ac0ea063351b4d651c3bb4dd4fd35e64dbb8e90c10e14eb03c4080", size = 254794, upload-time = "2026-09-13T19:09:52.577Z" }, + { url = "https://files.pythonhosted.org/packages/43/b7/0bbb32dc5ccdac766a13763fdfbffd7d73fc80349a69230c46c6b71e7508/coverage-7.16.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5597180ed7670cc94c04c65347418a467d3a43d5f0cf52fcac647f5425f42037", size = 258947, upload-time = "2026-09-13T19:09:54.372Z" }, + { url = "https://files.pythonhosted.org/packages/13/bd/65c31ddd43ff4e61b63721b3dad17cca412dba6e2ce89f92abdf75734339/coverage-7.16.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:a9647a0ac46255b8fef59a433a2161f03e5483f3a35e1cbd9dfe4600baff0c6b", size = 254613, upload-time = "2026-09-13T19:09:56.198Z" }, + { url = "https://files.pythonhosted.org/packages/25/20/d278115f2ba522b3e3128c6852be265f4e0df7202b2effca4db96cf0217d/coverage-7.16.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e306e98186b9cd109121f3583aeb7978797ad21d948f22944c5c08845cd554d0", size = 256388, upload-time = "2026-09-13T19:09:58.095Z" }, + { url = "https://files.pythonhosted.org/packages/e1/ca/d43aa396fb3a2f71c9411b99d926475f5acaf5c124f605c592865c80c8d5/coverage-7.16.1-cp313-cp313-win32.whl", hash = "sha256:48a78a66fcce49d7f6156524bf979c0ac633d584199717c68c6ffa949fc14e6a", size = 225550, upload-time = "2026-09-13T19:09:59.998Z" }, + { url = "https://files.pythonhosted.org/packages/0e/30/df2f6114f6a17cffe94721bd1d298f77f86aad493717fa5bc03cb291a1a6/coverage-7.16.1-cp313-cp313-win_amd64.whl", hash = "sha256:7af03247d598a353bbbbe1b925deb735276e4d845e7197c4073dc89352b236fa", size = 226091, upload-time = "2026-09-13T19:10:02.331Z" }, + { url = "https://files.pythonhosted.org/packages/ee/31/6f90d8aab72a112492bd50e3b5485b53b772b1f5fa70da0a620e723caae6/coverage-7.16.1-cp313-cp313-win_arm64.whl", hash = "sha256:166adae25b05b04c9a84135912066d9c97482115af38df1a419a38aacc6b6f5d", size = 225481, upload-time = "2026-09-13T19:10:04.151Z" }, + { url = "https://files.pythonhosted.org/packages/8e/b4/2a7c793965bae9f067aabab793a44d7a2f3ee7fb16b01ce1976bbd4a0218/coverage-7.16.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:cc0b37fe6f5ce5f1ccc62ad4fa9b1ad201d8e9b6027fd5e0170877beee4b2d15", size = 223546, upload-time = "2026-09-13T19:10:06.019Z" }, + { url = "https://files.pythonhosted.org/packages/ef/e2/633469076a2dbbea036cc15a268a3a5d6b2c7dd5d9a9567b2553dfc5ad61/coverage-7.16.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6618f481053b63fc6121faf8fc676bd9b7163c2a19d9e984a2e850002c28ab57", size = 223881, upload-time = "2026-09-13T19:10:08.246Z" }, + { url = "https://files.pythonhosted.org/packages/de/c3/f06150c13284569d53273b909f31222874276a595637b7852571dfeb2c18/coverage-7.16.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fa02d561eb1d8d2f8ba43ba6e3cef4c6c402a3b632a9460fa329fcadcd5df6a3", size = 254919, upload-time = "2026-09-13T19:10:10.254Z" }, + { url = "https://files.pythonhosted.org/packages/d5/40/47e25b215ae18a29010c8e29be8782a6e04d18ba6224be2bf6cebfce6427/coverage-7.16.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bc5354a124799f1f87b7637bbe6f18cd4bc66a1f37f6aa2b5db40f9adad531dc", size = 257428, upload-time = "2026-09-13T19:10:12.124Z" }, + { url = "https://files.pythonhosted.org/packages/27/4b/1e2a4267d14cbd12a8489364a9d40020233e6be836d929b363f0e77209e2/coverage-7.16.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34bafe9f4094315248573e6223e11af0ec1b25f9cbca43bf0e9a26a189ba2751", size = 258771, upload-time = "2026-09-13T19:10:14.031Z" }, + { url = "https://files.pythonhosted.org/packages/be/2e/9aa6146cea929fab9185bb2642ffef7f47520a6e5efe407f75f9b12f4cf0/coverage-7.16.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:29c4d3e32a3b5efa420a3dc627c7e570deb80ef997def52c7686a474f5edc7ab", size = 261086, upload-time = "2026-09-13T19:10:16.213Z" }, + { url = "https://files.pythonhosted.org/packages/13/3c/f9ad8bcd4fb3d21c9d20a16d6d6c6f999eee8f4498ed7659a3dbd2f4b74a/coverage-7.16.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2066c447fdd0bca39a9633a082d8ce67bf9a539a203b85059a364a405dc9fe9", size = 254895, upload-time = "2026-09-13T19:10:18.602Z" }, + { url = "https://files.pythonhosted.org/packages/b7/d1/47eda9fd1eaeea39fa7b5b13a63b2bed92ab901841fb120b3f9f5e1dc30c/coverage-7.16.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fd8ac10cd2458b3c6343aac082fb9bd0e3fa806cb2c4975f2280153474b88412", size = 256783, upload-time = "2026-09-13T19:10:20.778Z" }, + { url = "https://files.pythonhosted.org/packages/38/c3/565edf044877cb8cd3373c56885347ffc38f0edfd1f1679a487b208c19a8/coverage-7.16.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9d8c54ec32e5c102b9241f75d88ae26538b53662868ca491736611db448d9c7a", size = 254742, upload-time = "2026-09-13T19:10:22.733Z" }, + { url = "https://files.pythonhosted.org/packages/fd/88/87d2b2aeaba719192b2089ff1c2cf89a06cf73a6d2e9f1f145626617700c/coverage-7.16.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6dd8dda3402a01a1a8fe8b753a282466f615128574a5590a9108acd07b1f8540", size = 259016, upload-time = "2026-09-13T19:10:24.769Z" }, + { url = "https://files.pythonhosted.org/packages/fc/1b/70813185b125768abdcf7899fec4d37edc2e5fc9b60c7045c8f4271ec757/coverage-7.16.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:79afa9726438912e5cddd1fe541815cea9763c92935f594835e4c432565b68a9", size = 254559, upload-time = "2026-09-13T19:10:26.781Z" }, + { url = "https://files.pythonhosted.org/packages/d8/fa/e7aa5af279aafda633a1ede8bfd7d6916b0c8b2082be86759e0b52e73a61/coverage-7.16.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3db3978211c3cead5437a80136ca0556bab8bc7828de15a762884b0598c41361", size = 256215, upload-time = "2026-09-13T19:10:28.714Z" }, + { url = "https://files.pythonhosted.org/packages/38/87/7a894fa4f8c6662d2b6a87a3436950e15b1fa56e01765c9d6634fb2cbeb8/coverage-7.16.1-cp314-cp314-win32.whl", hash = "sha256:49c39c7068a494f8eb427155f5682f44feee43f9b3107fd54b1e52465379c54b", size = 225719, upload-time = "2026-09-13T19:10:30.743Z" }, + { url = "https://files.pythonhosted.org/packages/8b/01/fa7193c8005fb85488f02b0e1cc3c05a233cf2640206dd978af447aeecbf/coverage-7.16.1-cp314-cp314-win_amd64.whl", hash = "sha256:c510dad19552d912058e4c3e3cbec3fb155dbe8d0ce0ceb7e7dbf5c5822bae0b", size = 226208, upload-time = "2026-09-13T19:10:32.698Z" }, + { url = "https://files.pythonhosted.org/packages/da/5c/a08634c714924c3eaef811bb3576c044128aa5e7dfa86c75e52f0761849e/coverage-7.16.1-cp314-cp314-win_arm64.whl", hash = "sha256:b7d4d7e6dcaf33e85f1919f03346403bdcc27437c420a78835f3805bca0ab71f", size = 225633, upload-time = "2026-09-13T19:10:34.79Z" }, + { url = "https://files.pythonhosted.org/packages/43/df/ddb8a4c664046b1a0ee29c9c2d25b993e5dbc8fbde715df3694a64532781/coverage-7.16.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:3d0a3681c12d3e0bcdea3d9414b04087828d6c1a482802d6f7f42c37ed530152", size = 224281, upload-time = "2026-09-13T19:10:36.853Z" }, + { url = "https://files.pythonhosted.org/packages/e2/d0/9076e0c762d8afd91182e60a520fa5c92c4a334785eeb9fd6b8ef8fe7e3c/coverage-7.16.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3f3b4469d3da3ecced775d1a8c9c5d9fc80f259e30b7b89f9fed0700d6035ecb", size = 224547, upload-time = "2026-09-13T19:10:39.359Z" }, + { url = "https://files.pythonhosted.org/packages/03/e5/9c59e64b6161704f35fe91549bb19b2bb355e95caf596c26a2065564807c/coverage-7.16.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c08ae35c1be2fe1ce4b4c628df5c6fc0dc9a87f8e5fe8e20238d249678984741", size = 265906, upload-time = "2026-09-13T19:10:41.434Z" }, + { url = "https://files.pythonhosted.org/packages/57/5a/13ccaffb77f766101bf6f38be9dba9e468b02cc92da4552a57877dbf1c1f/coverage-7.16.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8ee71a38c54bb2676bbe762b8b0943a79ccb1c2fd6a52054f66e63eda392f8c1", size = 268023, upload-time = "2026-09-13T19:10:43.533Z" }, + { url = "https://files.pythonhosted.org/packages/ad/a1/05cfcf01d3c7c922832698ad46e51d3441d820ce87a943014bb5cf5710dd/coverage-7.16.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76491917771f179f9772efe218c5ccc65950dbdb35f4439298d8a8dfc6ec1f72", size = 270442, upload-time = "2026-09-13T19:10:45.895Z" }, + { url = "https://files.pythonhosted.org/packages/72/15/a2f1544b8e3835d7b769f7dabcc9ac0283e0b646ef3344703ff8f18d83e6/coverage-7.16.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4aa0b0a6f81fa3deb211e643f6954e78b4376b62b9c218271236cfa757664e8", size = 271565, upload-time = "2026-09-13T19:10:48.123Z" }, + { url = "https://files.pythonhosted.org/packages/df/5b/963c2993a82bd313f298d663afe03e164b96ace4d9d4c7561740a559e13d/coverage-7.16.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:756ba2d96d073c5a2a55d67fa22784763710fadbe22c41adde2d9cfa4dd78a8c", size = 264959, upload-time = "2026-09-13T19:10:50.195Z" }, + { url = "https://files.pythonhosted.org/packages/12/59/5eba06d1943735d7cd61d46d8c8a20ffe8ddd2da06b3c94366078dadeb9b/coverage-7.16.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:99bf9ea435cefcefd220f8687c3ddbbf78dc2de0bd11b57c3ae9fbbdf8d5561a", size = 267897, upload-time = "2026-09-13T19:10:52.252Z" }, + { url = "https://files.pythonhosted.org/packages/bd/48/af6c30f6ea431bb9b83f9070d268a9cc4fc97490abd32080164177ea999f/coverage-7.16.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:35cbc81f937fc402971df45c897d2df2bfb2014efcd990360032aa0a651635da", size = 265504, upload-time = "2026-09-13T19:10:54.432Z" }, + { url = "https://files.pythonhosted.org/packages/80/f2/6e13852a8656d05fa83284567dd5a5b1e6d89bef79fe3effca2787159eab/coverage-7.16.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:8fae08e85b334ac6ac886002b5041396a31bcf805225bbe19847627203da99e2", size = 269235, upload-time = "2026-09-13T19:10:56.563Z" }, + { url = "https://files.pythonhosted.org/packages/c2/32/b4fe465daa64ece674f83a750dfa4ba0fa3c5c74d6ef5dbb8dfce892cf0d/coverage-7.16.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:83362b64e215ef00b0ba33fcf13655ace6c9fdd144d5ad2ab59ac86c2daf166e", size = 264347, upload-time = "2026-09-13T19:10:58.634Z" }, + { url = "https://files.pythonhosted.org/packages/54/f3/88b5c0e4ca3994c6d5feb7b1bf4c9a62cee205553159184968426930a7b1/coverage-7.16.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:33300f2e140ccf26af3d8152e62bff71993f9310cfc63ba7a20940b0d246a0ae", size = 266660, upload-time = "2026-09-13T19:11:00.746Z" }, + { url = "https://files.pythonhosted.org/packages/97/72/6eff5456d7ba7f1c4678af531c33f9d957cae3201bd229b056fd13a204a3/coverage-7.16.1-cp314-cp314t-win32.whl", hash = "sha256:5539304fdbb2cc144df684d35a33b81145334d23e1c2367b5a923d25107f70b2", size = 226026, upload-time = "2026-09-13T19:11:02.846Z" }, + { url = "https://files.pythonhosted.org/packages/8e/c8/6e5ae3d8d4d0f2c0078985bf4db55fafd90e8107b1bf91ee3547a13f5694/coverage-7.16.1-cp314-cp314t-win_amd64.whl", hash = "sha256:715dcb72c3280c428c3a20134b87e42c29acec9669136e899ab2de69ca86218d", size = 226862, upload-time = "2026-09-13T19:11:04.921Z" }, + { url = "https://files.pythonhosted.org/packages/be/c7/68f9f0734afc904a92b974b489545b6a15700f3b1c4bd36eae764561e661/coverage-7.16.1-cp314-cp314t-win_arm64.whl", hash = "sha256:dac8b84c03e6029d272b8249c77018db83de59ca009a9adef7c144b4a62ee5e6", size = 226171, upload-time = "2026-09-13T19:11:06.969Z" }, + { url = "https://files.pythonhosted.org/packages/96/1a/d6d16babd0a5fe4c3fae40702158c570351694e74516d8d81b86c5637448/coverage-7.16.1-py3-none-any.whl", hash = "sha256:3d8bd4e58b6a5c2018d808f297905393c6c61da466a48c3f0596a76a4900ebe4", size = 215264, upload-time = "2026-09-13T19:12:18.895Z" }, ] [package.optional-dependencies] @@ -436,11 +535,11 @@ wheels = [ [[package]] name = "deepmerge" -version = "2.1.0" +version = "3.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2a/78/6e9e20106224083cfb817d2d3c26e80e72258d617b616721a169b87081e0/deepmerge-2.1.0.tar.gz", hash = "sha256:07ca7a7b8935df596c512fa8161877c0487ac61f691c07766e7d71d2b23bdd2f", size = 21449, upload-time = "2026-06-22T05:46:07.669Z" } +sdist = { url = "https://files.pythonhosted.org/packages/38/6e/5cb3548b4d3112fea529375e55e6f3cdc52b8054e3a66f203b1f888ba885/deepmerge-3.0.1.tar.gz", hash = "sha256:35b39a4cb92cf328d6eca61cbbf65f68a37c2ceb3085f0f853cbb2e52a59fc23", size = 22328, upload-time = "2026-09-01T14:09:44.383Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/51/25/2a75b47cb057b1e164c604fb81ab690a6cdb5e2260ce651194eae90f64a3/deepmerge-2.1.0-py3-none-any.whl", hash = "sha256:8f148339a91d680a75ecb74ade235d9e759a93df373a0b04e9d31c8666cfeb75", size = 14345, upload-time = "2026-06-22T05:46:06.742Z" }, + { url = "https://files.pythonhosted.org/packages/20/91/600003aaad107e27553fbc9cbfc57e96fa37e0223d2ef9d7d3a0e8d8d070/deepmerge-3.0.1-py3-none-any.whl", hash = "sha256:35c96f6a68fcf90719a5b31d9f8042ecef6c00fb56836660d33455d0f5cfda65", size = 14909, upload-time = "2026-09-01T14:09:43.364Z" }, ] [[package]] @@ -509,59 +608,122 @@ wheels = [ [[package]] name = "fonttools" -version = "4.63.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/84/69/c97f2c18e0db87d2c7b15da1974dace76ae938f1cfa22e2727a648b7ed43/fonttools-4.63.0.tar.gz", hash = "sha256:caeb583deeb5168e694b65cda8b4ee62abedfa66cf88488734466f2366b9c4e0", size = 3597189, upload-time = "2026-05-14T12:04:30.958Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f2/c9/4141c90a90db20f807c7e10bfd689fe53eb8f7f4caff58ee4d4dfe46919f/fonttools-4.63.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e3297a6a4059b4acc3a1e9a8b04741f240a80044eef08ebd32e8b5bcdddce75b", size = 2884632, upload-time = "2026-05-14T12:02:38.56Z" }, - { url = "https://files.pythonhosted.org/packages/b8/46/ad12b5c10eae602d7ef814b02afa08aacbf89da917fed5b071282b7eadc2/fonttools-4.63.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b1cd75a03ad8cb5bc40c90bfde68c0c47de423aa19e5c0f362b43520645eea94", size = 2429441, upload-time = "2026-05-14T12:02:41.162Z" }, - { url = "https://files.pythonhosted.org/packages/90/8f/bdca24a84c81d56fffed052229cdcff368f6e05882e526f4558891481f65/fonttools-4.63.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0425b277a59cff3d80ca42162a8de360f318438a2ac83570842a678d826d579", size = 4946346, upload-time = "2026-05-14T12:02:43.41Z" }, - { url = "https://files.pythonhosted.org/packages/04/59/a639c0e136441ee91a65b56fdf89e5d075927e7a09c559d1b0f5276577db/fonttools-4.63.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d7e5c9973aa04c95650c96e5f5ad865fbf42d62079163ecfab1e01cbc2504c22", size = 4903184, upload-time = "2026-05-14T12:02:45.742Z" }, - { url = "https://files.pythonhosted.org/packages/e6/53/91b7e0cb45b536f3da1b29ba8cbab89f27e8b986809e0b1982303a3f4eca/fonttools-4.63.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cb014d58140a38135f16064c74c652ed57aa0b75cbf8bb59cac821f7edb5334e", size = 4922967, upload-time = "2026-05-14T12:02:48.386Z" }, - { url = "https://files.pythonhosted.org/packages/c7/b7/87439bf44e6b97c5538cd29d0b7e366a5b8ce2cc132a4134fb67fa3f2fa2/fonttools-4.63.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:032038247a96c1690f9f31e377c389383c902531b085aa4e4dabd6f57f870e69", size = 5042799, upload-time = "2026-05-14T12:02:50.424Z" }, - { url = "https://files.pythonhosted.org/packages/ad/7c/8b96c3263b89ef99cded544c0f0636686f85dbd3c211c4dceef0231fca23/fonttools-4.63.0-cp310-cp310-win32.whl", hash = "sha256:a8b33a82979e0a6a34ff435cc81317be1f95ec1ebb7a3a2d1c8a6a54f02ae44e", size = 1519704, upload-time = "2026-05-14T12:02:52.523Z" }, - { url = "https://files.pythonhosted.org/packages/e5/4d/2c2f0069970b6907de8fb5b05c5c0193cc22f717df151d1c7aef1c738f58/fonttools-4.63.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c18358a155d75034911c5ee397a5b44cd19dd325dbb8b35fb60bf421d6a72ac", size = 1568666, upload-time = "2026-05-14T12:02:54.917Z" }, - { url = "https://files.pythonhosted.org/packages/75/2b/a7f1545bdf5da69c4bda0cea2a5781f0ad2a6623e0277267672db43c5fe6/fonttools-4.63.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2b8ae05d9eacf6081414d759c0a352769ac28ce31280d6bb8e77b03f9e3c449f", size = 2881793, upload-time = "2026-05-14T12:02:56.645Z" }, - { url = "https://files.pythonhosted.org/packages/49/50/965308c703f085f225db2886813b27e015b8b3438c350b22dd65b52c2a2c/fonttools-4.63.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:79cdc9f567aec74a72918fd060283911406750cbc9fd28c1316023deb6ce31a9", size = 2428130, upload-time = "2026-05-14T12:02:58.891Z" }, - { url = "https://files.pythonhosted.org/packages/d8/38/6937fbd7f2dc3a6b48725851bc2c15ec949b9af14d9bbcb5fe83cdf9bdf9/fonttools-4.63.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c14b4fd138c4bafcca294765c547914e1aa431ae1ca94ab99d8db08c958bd3b", size = 5111952, upload-time = "2026-05-14T12:03:01.263Z" }, - { url = "https://files.pythonhosted.org/packages/0b/43/a81f20050a3115b57d62c8e781446949512eac36690dc384ccea65ff4cc1/fonttools-4.63.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76ac49f929aecaf82d83250b8347e099d7aecba0f4726c1d9b6df3b8bb5fe18", size = 5082308, upload-time = "2026-05-14T12:03:03.211Z" }, - { url = "https://files.pythonhosted.org/packages/67/00/cdd9d4944ca6ae280d01e69cc37bde3bf663630b837a6fc6d2cd65d80e0e/fonttools-4.63.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dcf076a4474fe0d7367e5bbf5b052c7284fa1feca729c04176ce513521afd8a0", size = 5087932, upload-time = "2026-05-14T12:03:05.147Z" }, - { url = "https://files.pythonhosted.org/packages/f5/f1/0aa0dbea778c75adbef223c42019fd47d22262b905974d62d829545d485f/fonttools-4.63.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7dd683fef0663e9f0f45cf541d788d24caa3ec9db50796b588e1757d8b3bc007", size = 5213271, upload-time = "2026-05-14T12:03:07.238Z" }, - { url = "https://files.pythonhosted.org/packages/a8/99/253e4056e1f0e67b9390125a154b73b5eb73ad521bece95c004858fdeec2/fonttools-4.63.0-cp311-cp311-win32.whl", hash = "sha256:afefc1ed0a59785a7fb06ea7e1678e849c193e1e387db783579bc7b3056fcfcb", size = 2304473, upload-time = "2026-05-14T12:03:09.271Z" }, - { url = "https://files.pythonhosted.org/packages/08/60/defa5e69641db890a63be281f41345f4c33b157824eaf0b9fad3e08b0dcb/fonttools-4.63.0-cp311-cp311-win_amd64.whl", hash = "sha256:063e08bd17bd5a90127a14123de0d6a952dbc847695fd98b63c043d58057f90c", size = 2356389, upload-time = "2026-05-14T12:03:11.53Z" }, - { url = "https://files.pythonhosted.org/packages/08/ef/b3c6b9b5be2f82416d73fe2ed2e96e2793cd80e7510bd6a17ca79cdd88ec/fonttools-4.63.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:37dd23e621e3b0aef1baa70a303b80aaf38449632cfc8fd2a55fb285bbccfc02", size = 2881131, upload-time = "2026-05-14T12:03:13.386Z" }, - { url = "https://files.pythonhosted.org/packages/44/a0/c815bea63117fa63e4e1c01f8a1110d2112fa003f838e6467094ec2432ce/fonttools-4.63.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a9faff9e0c1f76f9fd55899d2ce785832efebab37eb8ae13995853aef178bef0", size = 2426704, upload-time = "2026-05-14T12:03:15.801Z" }, - { url = "https://files.pythonhosted.org/packages/44/04/0b91d8e916e92ad1fac9e4624760baf0fd5ff2ead614c2f68fb21373f03f/fonttools-4.63.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef3048ef05dbb552b89817713d9cac912e00d0fde4a3105c00d29e52e10c89af", size = 5044298, upload-time = "2026-05-14T12:03:18.085Z" }, - { url = "https://files.pythonhosted.org/packages/77/c7/2342da9830e3e9d4870305ca5d2091d2a83284f2953079b7bdd3b5e029d8/fonttools-4.63.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:58dc6bb86a78d782f00f9190ca02c119cf5bbe2807536e361e18d42019f877d8", size = 4999800, upload-time = "2026-05-14T12:03:20.161Z" }, - { url = "https://files.pythonhosted.org/packages/e6/6d/67fe16c48d7ce050979b33f47e0d28a318f02da030602e944c34f7a16ef3/fonttools-4.63.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee08ebfa58f6e1aeff5697ab9582105bb620008c1caafb681e4c557e7483027b", size = 4982666, upload-time = "2026-05-14T12:03:22.87Z" }, - { url = "https://files.pythonhosted.org/packages/f2/00/3bbab338c07c71fa56269953845e92c951a61457bbbb0f1022551ea266d9/fonttools-4.63.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:27fdc65af8da6f88b9c6121c47a464cbe359fcfff7ff6fc2d37a1f395d755b78", size = 5133598, upload-time = "2026-05-14T12:03:25.168Z" }, - { url = "https://files.pythonhosted.org/packages/62/f2/aa27c7f98db5b064883dadcc5283947e81e034de42e22a33675878d98b54/fonttools-4.63.0-cp312-cp312-win32.whl", hash = "sha256:af2fd1664d00a397d75f806985ddb36282091c2131a73a6485c23b4a34722263", size = 2292575, upload-time = "2026-05-14T12:03:27.496Z" }, - { url = "https://files.pythonhosted.org/packages/87/36/cccb9bc2a6ab63d1b2980374f0dca72ce95ae267c9b4cfe77455bb70d0d4/fonttools-4.63.0-cp312-cp312-win_amd64.whl", hash = "sha256:59ac449f8cca9b4ffa08d2e7bbadad87ce710d69d1eda5c3c1ce579baa987272", size = 2343211, upload-time = "2026-05-14T12:03:30.057Z" }, - { url = "https://files.pythonhosted.org/packages/0f/8d/d8fec3dcde2963f8c908fb315e5ff2cd0ac34f82394bbbf73a2aa5145ce3/fonttools-4.63.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:cd7e9857e5e63738b9d9fd707bc1f59c8b09e5177726d23664db393c59bb08bd", size = 2876062, upload-time = "2026-05-14T12:03:32.554Z" }, - { url = "https://files.pythonhosted.org/packages/ef/71/d935dc54e4ff121bfdd11e08702db63a7e6f25af21d8a3d7b7212df53641/fonttools-4.63.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c2a2a42198b696a6f48fad91709afb55176e66a5e566131219dba372fb7f8c59", size = 2424594, upload-time = "2026-05-14T12:03:34.86Z" }, - { url = "https://files.pythonhosted.org/packages/8e/40/e76320afa1df918e146155ef239b1719ee266092e96f5423bfd075affba1/fonttools-4.63.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e874792a8212b44583ea02189d9e693906b2f78b261f372f95d6c563210ac1d", size = 5024840, upload-time = "2026-05-14T12:03:36.745Z" }, - { url = "https://files.pythonhosted.org/packages/ce/36/0b805d8c485f872f65a509cbe3b58a5d0d17bee855333b54a150c79d3061/fonttools-4.63.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:22135da48a348785c5e2d5d2d9d6bec5ed44adacbaeb9db12d9493bf6c6bfa68", size = 4975801, upload-time = "2026-05-14T12:03:38.833Z" }, - { url = "https://files.pythonhosted.org/packages/c8/26/2cee03d0aa083ab022da5c07aff9ed3f689da1defb81ad6917c9627896da/fonttools-4.63.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ccf41f2efdf56994d22d73bef4ced1052161958169428d06ba9724ea9e9a64be", size = 4965009, upload-time = "2026-05-14T12:03:41.494Z" }, - { url = "https://files.pythonhosted.org/packages/7e/48/cc4b66d9058c0d0982c833fad10127c4b0e9324606aafa41382295ca4102/fonttools-4.63.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9ced0bd02ac751dd6319b0da88aaef24414e3b0dbc32bb4f24944821a3741a27", size = 5105892, upload-time = "2026-05-14T12:03:43.525Z" }, - { url = "https://files.pythonhosted.org/packages/d8/1f/a98a30a814b9ddef3a2e706025f90b9e0bc94890e6cb15254bc86547d11a/fonttools-4.63.0-cp313-cp313-win32.whl", hash = "sha256:85be818f5506e8a7753153def2c9550178f0ecae6a47b5e0e8dbb23f7cc90380", size = 2291313, upload-time = "2026-05-14T12:03:45.594Z" }, - { url = "https://files.pythonhosted.org/packages/92/46/5177b01f3b4abfdd4409f31cca4ab279c9343a26efbe9ec78c97fc612e02/fonttools-4.63.0-cp313-cp313-win_amd64.whl", hash = "sha256:ba04cb5891d4c0c21b6da95eda8d7b090021508a294fff33464fc7d241e0856b", size = 2342299, upload-time = "2026-05-14T12:03:47.414Z" }, - { url = "https://files.pythonhosted.org/packages/27/d2/23d25e3f247b328be58d04a4c9f894178a0d1eda7d42867cfb388adaf416/fonttools-4.63.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fd1e3094f42d806d3d7c79162fc59e5910fcbe3a7360c385b8da969bc4493745", size = 2875338, upload-time = "2026-05-14T12:03:50.052Z" }, - { url = "https://files.pythonhosted.org/packages/cd/58/7dfa0c761cb3b2964e2a84c4dc986c926a87de0cb9fb60d5b28ded3f2914/fonttools-4.63.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6e528da43bc3791085f8cb6141b1d13e459226790240340fcbb4625649238b03", size = 2422661, upload-time = "2026-05-14T12:03:52.154Z" }, - { url = "https://files.pythonhosted.org/packages/dd/87/64cfa18a7a1621d17b7f4502b2b0ed8a135a90c3db51ea590ee99043e76b/fonttools-4.63.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b2248c5decb223562f7902ff6325077a073f608ee8e33e88ad88db734eb9f49", size = 5010526, upload-time = "2026-05-14T12:03:54.647Z" }, - { url = "https://files.pythonhosted.org/packages/36/e1/a8933a72c45a87177fbde2696e0d0755c8c9062f8c077a961c6215fa27b1/fonttools-4.63.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:308f957cdeaf8abe4e5f2f124902ef405448af92c90f80e302a3b771c2e6116b", size = 4923946, upload-time = "2026-05-14T12:03:56.984Z" }, - { url = "https://files.pythonhosted.org/packages/27/60/872e6e233b8c5e8b41413796ff18b7fe479661bd40147e071b450dfad7a1/fonttools-4.63.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bf00f21eb5fb721dbaf73d1e9da6d02a1af7768f2ebcf9798be98beab8ba90f6", size = 4962489, upload-time = "2026-05-14T12:03:59.443Z" }, - { url = "https://files.pythonhosted.org/packages/30/c4/83c24f2ec38b90cfda84bf4b1a1f49df80e84a1db4e7ac6e0d41bf23bc39/fonttools-4.63.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c1aaa4b9c75798400ac043ce04d74e7830376c85095a5a6ed7cba2f17a266bf4", size = 5071870, upload-time = "2026-05-14T12:04:02.122Z" }, - { url = "https://files.pythonhosted.org/packages/de/40/3ae22b60ff1d41ce0bd044b31238cdc72cef99f28b976f1e128ebd618c9b/fonttools-4.63.0-cp314-cp314-win32.whl", hash = "sha256:22693918177bd9ceabec4736d338045f357769416fc6b0b2508eefef75b08616", size = 2295026, upload-time = "2026-05-14T12:04:04.47Z" }, - { url = "https://files.pythonhosted.org/packages/c3/d4/98078064ccc76b45cb0f6c002452011e93c4bd26f6850344f0951cc1fe89/fonttools-4.63.0-cp314-cp314-win_amd64.whl", hash = "sha256:7d782fac32985914c351556f68ac0855391572bcd87de50e05970d3cd4c96fc5", size = 2347454, upload-time = "2026-05-14T12:04:06.752Z" }, - { url = "https://files.pythonhosted.org/packages/49/4e/652d1580c5f4e39f7d103b0c793e4773129ad633dce4addd0cf4dfebde02/fonttools-4.63.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:6db5140a60a5d731d21ec076745b40a310607731b0a565b50776393188649001", size = 2958152, upload-time = "2026-05-14T12:04:08.706Z" }, - { url = "https://files.pythonhosted.org/packages/0e/55/ad864c9a9b219f552eb46b32cd7906c466e5a578ba0c3abfcc0fe7413eb6/fonttools-4.63.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7d76edbff9014094dbf03bd2d074709dfa6ec7aba13d838c937a2b33d2d6a86e", size = 2460809, upload-time = "2026-05-14T12:04:10.783Z" }, - { url = "https://files.pythonhosted.org/packages/ea/2b/0aa8db70f18cf52e49b4ed5ecec68547f981160bf5ded3b5aed6faa0a6f9/fonttools-4.63.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0eac00b9118c3c2f87d272e45341871c5b3066baa3c86897fa634a7c3fb59096", size = 5148649, upload-time = "2026-05-14T12:04:12.747Z" }, - { url = "https://files.pythonhosted.org/packages/7f/63/18e4369c25043096f1048e0c9915951adc4f842bd81c6b18155824d6fa99/fonttools-4.63.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51394295f1a51de8b5f30bdb1e1b9a4231536c7064ef5c6e211eec19fa36036f", size = 4932147, upload-time = "2026-05-14T12:04:14.806Z" }, - { url = "https://files.pythonhosted.org/packages/a1/3f/67f3eac2ffd8a98446c5022f8ed3864eac878a5ff7af8df4c8286dba16cc/fonttools-4.63.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9e12f105d2b6342c559c298afb674006bb2893afc7102dcf8a1b55b0486b4e40", size = 5027237, upload-time = "2026-05-14T12:04:17.675Z" }, - { url = "https://files.pythonhosted.org/packages/1a/ba/4e6214cb38a7b04779e97bb7636de9a5c7f20af7018d03dee0b64c08510a/fonttools-4.63.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:796f27556dbe094c4824f75ca85267e4df776c79036c8441469a4df37038c196", size = 5053933, upload-time = "2026-05-14T12:04:20.818Z" }, - { url = "https://files.pythonhosted.org/packages/34/3b/214dcc19ee31d3d38fb5ad2755c11ef0514e5dc300bbaf41c0b69f393799/fonttools-4.63.0-cp314-cp314t-win32.whl", hash = "sha256:948428a275741f0b64b113c955425a953314f4b9ab9997f73a72c83e68e569c8", size = 2359326, upload-time = "2026-05-14T12:04:24.22Z" }, - { url = "https://files.pythonhosted.org/packages/dd/1e/3ff1a9b523058c2eeb6a9d50f5574e2a738200d0d94107d5bc4105e8da3f/fonttools-4.63.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6d4741eb179121cab9eea4cb2393d24492373a260d7945006358c08cfbf45419", size = 2425829, upload-time = "2026-05-14T12:04:26.829Z" }, - { url = "https://files.pythonhosted.org/packages/2c/47/c99d5268f354002ce80f8d029cd9d7d872969da1de8b93d32de4dc56d6f4/fonttools-4.63.0-py3-none-any.whl", hash = "sha256:445af2eab030a16b9171ea8bdda7ebf7d96bda2df88ee182a464252f6e05e20d", size = 1164562, upload-time = "2026-05-14T12:04:29.092Z" }, +version = "4.65.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/77/51/d63c7e52163ac14393a35bd14bd7c0da95f8f74be5d7cc988092f9965129/fonttools-4.65.0.tar.gz", hash = "sha256:762ba5431358d0dbd4a01982484a1d494fb267e91f974cdcf20b80eab8560f6f", size = 3674467, upload-time = "2026-09-10T15:35:54.955Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/d9/1caaa015dd207da7ccd3feba87289997bd88334ea3e74a367cdc7b0e50a3/fonttools-4.65.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:93a73af2075036d36d7fbf856779c56a1b3b86ffcdae6abede7596604c42c156", size = 3095449, upload-time = "2026-09-10T15:33:04.814Z" }, + { url = "https://files.pythonhosted.org/packages/20/d6/988cd9b33ae2d92b15a51d73eb77c991f7a0fe90a7bc74526e9669247789/fonttools-4.65.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c130be2232e3caf8d2b476854ea78421ec1642917ff5ab695284bac31bbb072b", size = 2587697, upload-time = "2026-09-10T15:33:07.653Z" }, + { url = "https://files.pythonhosted.org/packages/f8/6a/36f465a1c277131f9569f6a56fe99cf135391b4860b9c4f4b23e5b1cd5df/fonttools-4.65.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e3944e0bdba42effb71959e43d91b599326b02b59c78310d5675e8a75525e7d8", size = 5313308, upload-time = "2026-09-10T15:33:09.975Z" }, + { url = "https://files.pythonhosted.org/packages/ee/56/151b5e81d20c63834f48ad37a0cbbbe2f9b248e38f8d10387f0cf5219244/fonttools-4.65.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fb53892b570f7f1f0055e75fc4de32673e32f749c4c8a606b63d5c436650e634", size = 5253649, upload-time = "2026-09-10T15:33:12.225Z" }, + { url = "https://files.pythonhosted.org/packages/c8/22/6389215da9d4f98623aacdca9479c1030bbd516cb658e893bc54b61098ce/fonttools-4.65.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a6c8d184e523580a7c55d21cde37176a3c91cb539cf06c2aa36ffc634fd75296", size = 5277745, upload-time = "2026-09-10T15:33:14.783Z" }, + { url = "https://files.pythonhosted.org/packages/89/e3/c1037a1dfb7c8efe6f2a7d1951ebdde40cbbf82e9c5d796fcb03077e4790/fonttools-4.65.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e5ceccaf2e57d83b753a2b5db5d94aa0a8071886d4afebd2d520c9683e6bef0e", size = 5413259, upload-time = "2026-09-10T15:33:17.619Z" }, + { url = "https://files.pythonhosted.org/packages/44/b9/7dd72330168d39635c329f23a98279393b1e825e42a6db362e09897aad7b/fonttools-4.65.0-cp310-cp310-win32.whl", hash = "sha256:aff640a4fcb021fa83f9879d5bfa115b6931522dae991a24faa75888bd6aeff6", size = 1577124, upload-time = "2026-09-10T15:33:19.864Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c2/e959385b4626989b25b82b9f4f99e7c3ac68377d6846b376239b6f126966/fonttools-4.65.0-cp310-cp310-win_amd64.whl", hash = "sha256:5c1700a60e4ff23a0425d5a64abf43d092e6b55071354825781faf255904dcb4", size = 1633354, upload-time = "2026-09-10T15:33:21.963Z" }, + { url = "https://files.pythonhosted.org/packages/62/9e/58250cdc54d96fcfacb544e12997a6390fa4e6b71ae2241cfcfe5b341803/fonttools-4.65.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:06273c71e692caf5989c0437ca50875a5e49e216ddf653228fe9bb35bdc82c0f", size = 3089141, upload-time = "2026-09-10T15:33:24.509Z" }, + { url = "https://files.pythonhosted.org/packages/3e/67/0f0416069e38da0a1327a847a2e8dd1edb425d0043d8a3e63eb940070209/fonttools-4.65.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ca2b02d74e9ad7e21a1d11e4701425800a4b0c63cf90486e60258262feccbcbf", size = 2584298, upload-time = "2026-09-10T15:33:26.598Z" }, + { url = "https://files.pythonhosted.org/packages/99/0d/7e40e9957359afc0bab081131c215370a6d2d203361bb6f945ab595e924c/fonttools-4.65.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7830e9fa3bebc44dbc27ff44d8201def30ea5c48a773696d58e69e6bcd9cd5d4", size = 5498163, upload-time = "2026-09-10T15:33:29.071Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e6/e48cf0a272a5d4d17a09d44f92727e67f975ddfa94acc8464763d19a654d/fonttools-4.65.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a3991732c87b3f054a2a8cf86dd0d602833fa8cb37c911503173771646e1013d", size = 5457456, upload-time = "2026-09-10T15:33:31.918Z" }, + { url = "https://files.pythonhosted.org/packages/e4/8a/a5c67ddeda82ee5e4ec3bc52ac1cbb685f0bb7a0516badc55643b454ab0d/fonttools-4.65.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6031e77b3fb8c765055ba2b8bd8dcb17030f3bf2484c448b472fdedf4460ba80", size = 5464334, upload-time = "2026-09-10T15:33:34.605Z" }, + { url = "https://files.pythonhosted.org/packages/d7/16/294e77383b2d39c9f8f25144a7ba23fe1cbbc05227cc72545097785ff07c/fonttools-4.65.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6813cc1e2e883bd6c15b3e04f72c78dc65fdc4ca861063adf5f341fbaec2ca62", size = 5595597, upload-time = "2026-09-10T15:33:37.591Z" }, + { url = "https://files.pythonhosted.org/packages/80/01/8e74ce8626c734959c782f2d89af8e9f14d078fd3d4ddf8b5a51401ae475/fonttools-4.65.0-cp311-cp311-win32.whl", hash = "sha256:4a5db8442453da4b6f43ad325879381b726bf2238a2253efd9584be21a2cefc2", size = 2441114, upload-time = "2026-09-10T15:33:40.592Z" }, + { url = "https://files.pythonhosted.org/packages/37/3e/835dc6c658426e2670b7f38c38295492fcbaeb06080e9dce89ce8105993c/fonttools-4.65.0-cp311-cp311-win_amd64.whl", hash = "sha256:9f201796c8e24e657be77c16fa664e798a46122144217f90838982937a964f0a", size = 2498823, upload-time = "2026-09-10T15:33:43.402Z" }, + { url = "https://files.pythonhosted.org/packages/58/db/242fa4fce7f632c5f7ab15585343393b25792510c0c32bd218ad24d59f1c/fonttools-4.65.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e844a45c9e5ced6536f184cf1a65b5d65e8f7e711993b413e10500a8223622e5", size = 3097120, upload-time = "2026-09-10T15:33:46Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b6/42fa4d373416675f74446421cf0b2badb82a4245c60745f05f424f75c649/fonttools-4.65.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b30e953de049bf43fc0a63c7d0c44d205c923e4bbf24716aae1518c0e65f977c", size = 2584658, upload-time = "2026-09-10T15:33:48.473Z" }, + { url = "https://files.pythonhosted.org/packages/75/6f/d589b9d62280a846c77a2c383d852c6dcb79ae8aa02bf0fa46c8577af145/fonttools-4.65.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09c34bdeed8915bfb53bee0c8ed2254dbd8ec69c0014b7f3702f347c049bf358", size = 5425737, upload-time = "2026-09-10T15:33:51.473Z" }, + { url = "https://files.pythonhosted.org/packages/b5/09/de2c0c20a42c18e565a2617932beb08c06697bbdd0d3f62b108262e11583/fonttools-4.65.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:05595385ae99f4b9626cebb973bf171b8fe38a8f40708e6e42abba0ed7537778", size = 5402463, upload-time = "2026-09-10T15:33:54.907Z" }, + { url = "https://files.pythonhosted.org/packages/79/2e/bc0f5c9dce21821454bb5812d3b23410bca33c8bbd5468386d0327aa0cff/fonttools-4.65.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d95b34dd68fbfc0e4a1740c421597656117f979ed8dc85de66e08f9f9981806e", size = 5362168, upload-time = "2026-09-10T15:33:57.481Z" }, + { url = "https://files.pythonhosted.org/packages/1f/0d/2116763ade7e71e0e5d421babe1785d745be9b3d605bf914792ce1c97f79/fonttools-4.65.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:924d06e6130429168318db71c40174a765ad016fc4b56ca811287e3d7373b3a6", size = 5524117, upload-time = "2026-09-10T15:34:00.021Z" }, + { url = "https://files.pythonhosted.org/packages/e2/5e/f9600553b9f645e3068553831685ff1dab6259536a23b38d2e048de38f17/fonttools-4.65.0-cp312-cp312-win32.whl", hash = "sha256:04f73dd01005752a6e75cf4a8dc6b70dc724d1d4bc34cc89522153f4a2f07680", size = 2432089, upload-time = "2026-09-10T15:34:02.803Z" }, + { url = "https://files.pythonhosted.org/packages/3a/02/e436a6a1863b9862bab9f82d6da33055dd7aa3738017edd902a163525dc2/fonttools-4.65.0-cp312-cp312-win_amd64.whl", hash = "sha256:3b5d9ba89edf778b376e669b879ae33a198bf45cf5a23c3f6514f935cf9d0d9d", size = 2483475, upload-time = "2026-09-10T15:34:05.09Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5c/343a4225e83eb06f82c1d8bf41fc5e5f71eab2f62bc7ca215c764722c0a9/fonttools-4.65.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8b7bb52817a24731d2e4f4df0e71fdde05e6c806c8f8f1517b015d142fdacfa5", size = 3094663, upload-time = "2026-09-10T15:34:07.235Z" }, + { url = "https://files.pythonhosted.org/packages/9b/c9/49b2401be932741d9218181c08948c96db32eab21ecaaf79283b752e13e7/fonttools-4.65.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e2c21772fcf70325189707b19f346812690bb1b0bd7e207e6ac205244806b303", size = 2584521, upload-time = "2026-09-10T15:34:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/51/c9/48b07e6c5cf44fa56f758a04c3772a66c0e9ba82bbe22077e4076746a62b/fonttools-4.65.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:64c9b26816415b5e3d899e9077109d327b22140fe3c4066644d8cdbad5bb1569", size = 5395399, upload-time = "2026-09-10T15:34:12.38Z" }, + { url = "https://files.pythonhosted.org/packages/fa/2d/5cc5a10c8ed56079d6c2e9e3e5920622529a2ae045c9f47a6055ccb1a319/fonttools-4.65.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6dd6243f60e2d6160c2966e1e14020dc261ffd741b69a2e4ca8bfd051592e4b7", size = 5376659, upload-time = "2026-09-10T15:34:15.239Z" }, + { url = "https://files.pythonhosted.org/packages/ab/6f/ccf33739d936bb3afa1655a225be7ee5d63d6d3c8b7d0e570bc5a2a7b899/fonttools-4.65.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:834962fd7cf21c58e81ac50a59e6ed2306f9df5e3dd481dad1cd7d2c4c60b773", size = 5336795, upload-time = "2026-09-10T15:34:17.817Z" }, + { url = "https://files.pythonhosted.org/packages/7e/c1/ffd17483f2094f0f4118974295b514b5b82afd4f6e3c80c23f01684e97a6/fonttools-4.65.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:580eb68ff7bd6954a7a76afddd864bfc66eaaf5f5c20dd6ead9186d0055a4ffe", size = 5496530, upload-time = "2026-09-10T15:34:20.241Z" }, + { url = "https://files.pythonhosted.org/packages/b1/19/9aca7712d0676ba5f8d1530ce20478a9bb09cccd0153d56693337379cdcf/fonttools-4.65.0-cp313-cp313-win32.whl", hash = "sha256:7a18b2ffd44249fe84289253197aa65ad4f2de554c0d381f18b1f5939bc6bc60", size = 2430392, upload-time = "2026-09-10T15:34:22.906Z" }, + { url = "https://files.pythonhosted.org/packages/1f/6f/f015dea0f4354e0798751b657cea2dee482914737f88bf1a078349ce92cf/fonttools-4.65.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ae1846b0f192fd485d26a455af19b8f5cf05aff08f9836f533913d8fcea133c", size = 2481620, upload-time = "2026-09-10T15:34:25.816Z" }, + { url = "https://files.pythonhosted.org/packages/64/29/606365ef601668bfebed14cfe3dc72bb7fcd1e23011bbb2833f17fea3065/fonttools-4.65.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:dc87a9f846bec83c3795804f62b4632716d46e3522869a3dd9cd44a5d245b006", size = 3098393, upload-time = "2026-09-10T15:34:28.472Z" }, + { url = "https://files.pythonhosted.org/packages/c7/61/11412939d6b7abf5ac7ce0d61d7f94a0a4fbabc9f1ab0a04fa622e0fc11c/fonttools-4.65.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:aa50dd7b9baf75e2bbd43401fc0d237f7a94a8ad2e0c57ea97160fc631af5eb0", size = 2585766, upload-time = "2026-09-10T15:34:31.207Z" }, + { url = "https://files.pythonhosted.org/packages/db/17/734921d8aee8309801da42590375d32d4d46f771b73373ec9520d5d4220b/fonttools-4.65.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0d2a9892fdb3b7e2d0f4174e3b907d226ff83698249762eeefce08ec5b2de1dd", size = 5380679, upload-time = "2026-09-10T15:34:33.933Z" }, + { url = "https://files.pythonhosted.org/packages/cf/eb/2a4d78d60d978e694cfa04c98e4d8ddbf7f028fd768ddef470bb9da5d69e/fonttools-4.65.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6d815734e7fede0ad1f233f23f0f191cbe8fc64762ff041e589bc0f78e0b2397", size = 5321833, upload-time = "2026-09-10T15:34:36.295Z" }, + { url = "https://files.pythonhosted.org/packages/d9/ca/1cd48b5c11ef9658732787bf2362e1bf3871dad5945d2f6cc8f675ca769c/fonttools-4.65.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:71e4c67b6196a2f447f46476fd2302604721617f5e0a21b0988bdd87b6bb9687", size = 5320938, upload-time = "2026-09-10T15:34:38.992Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0d/90e6051bded926cccabe9dd0bce3b6ca012f4d5779d61167afbf4989ceb6/fonttools-4.65.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b11d8a4a0c3ca74bbd4c105b7ef82501945c939e6096d9934ec7d288cdf5aaa9", size = 5451747, upload-time = "2026-09-10T15:34:41.387Z" }, + { url = "https://files.pythonhosted.org/packages/18/74/23e0268e48029ff0752083f69312c49b738163d5af919add9dc4ac81e907/fonttools-4.65.0-cp314-cp314-win32.whl", hash = "sha256:8e44a34d91b3c793879767eb115867ced74d2eb94974e64e72fe9e2eea71cf1a", size = 2434246, upload-time = "2026-09-10T15:34:44.385Z" }, + { url = "https://files.pythonhosted.org/packages/a1/2d/ee69affecd4bc81cb932a213438d4199fb48bf8ca6d664438ccc7f623c2a/fonttools-4.65.0-cp314-cp314-win_amd64.whl", hash = "sha256:0aa8901db22875c831d6a91796549590d7e747da37438f38b69d771b668be445", size = 2486706, upload-time = "2026-09-10T15:34:46.842Z" }, + { url = "https://files.pythonhosted.org/packages/8e/9c/edee5f785198ce3327e1ddeace91c773122d47f086a67e0a84b800f4a940/fonttools-4.65.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:2e4a380ca40d3a5372e31b340f0da0d53b4583aadbb8e41f6a516afa69c509a4", size = 3172027, upload-time = "2026-09-10T15:34:49.269Z" }, + { url = "https://files.pythonhosted.org/packages/9c/c6/252ec9884381089bc30da75978b072593920249de60219817f16cbc9145f/fonttools-4.65.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:661bd91c4be13721408b2d4b67a9b3fa7736713adc9a6c9780c9c60fc7959f90", size = 2619020, upload-time = "2026-09-10T15:34:51.453Z" }, + { url = "https://files.pythonhosted.org/packages/42/79/f71b0d202b8473bb45b07876c08a474de9fe560ed2c0cde642812a81e22a/fonttools-4.65.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:62c5e42c79449def957adf8a9a65a43018efa7e2a6bc6baa3afe955e0d5fb2ab", size = 5545942, upload-time = "2026-09-10T15:34:54.804Z" }, + { url = "https://files.pythonhosted.org/packages/4a/bd/52e1bf33e0aebfe22ecc9a85c634db707c1dd9f6b1b438efeed98c55b959/fonttools-4.65.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:36fca8efc46b5adfca327c666e739fc05b7a7a6ef17840230f81b22f53230f61", size = 5351477, upload-time = "2026-09-10T15:34:57.514Z" }, + { url = "https://files.pythonhosted.org/packages/5c/76/8c6b2ad20beec95cd446f3a8bdc753c7e4a4fd69ef3e66704c0f7c8cb0b5/fonttools-4.65.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8aa1291e4c767abf1b0b79ca2d6895f7c0b661d9d95d03b5791c883a9d1e1f08", size = 5412271, upload-time = "2026-09-10T15:35:00.895Z" }, + { url = "https://files.pythonhosted.org/packages/79/49/fadbf11bbbd2d699d88a5498a0634280206e01e3bd5da9a4e0c504953ce9/fonttools-4.65.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fcf39949f56911348514b466714efa9118bec3d2be249e1c487263f7cda6edab", size = 5446450, upload-time = "2026-09-10T15:35:03.369Z" }, + { url = "https://files.pythonhosted.org/packages/df/77/5fda646d3a6d5ee26465865c00319cc0925cf484a3b42dd8232d5b39f973/fonttools-4.65.0-cp314-cp314t-win32.whl", hash = "sha256:ffc918702661f1d74d2fbb2f5551036b64f6d2d743139e105289b694bcd16f54", size = 2467858, upload-time = "2026-09-10T15:35:05.744Z" }, + { url = "https://files.pythonhosted.org/packages/ae/0f/afa0f3de70ebe02bba46b32cccb30b1de52624472b14ac2e7cd403d08db9/fonttools-4.65.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5a977e3645dbffaee924209828aa702a215f7ff68bc08010740c10c723787e62", size = 2518248, upload-time = "2026-09-10T15:35:07.846Z" }, + { url = "https://files.pythonhosted.org/packages/e6/35/f894ceb867118c0261d0f69a9bd516b045a3754238f76c88a49513ac7a83/fonttools-4.65.0-py3-none-any.whl", hash = "sha256:3060b8c1fc2329fa20265b7c138614143ea7c1624e26c5c180c76aeb74deae6f", size = 1196441, upload-time = "2026-09-10T15:35:52.347Z" }, +] + +[[package]] +name = "fonttools" +version = "4.66.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/24/86f9930b930b97fc82266083320f3c34643ef67261c32651e893db525aca/fonttools-4.66.0.tar.gz", hash = "sha256:ef0610dfe7bb5bf574d9bdad6f597403ebc9807d124ac6f148d7604b2609be98", size = 3692007, upload-time = "2026-09-23T17:59:14.345Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/61/ea35d7faff1430f875d0b0b80ea043f5b17d4155e62704985af415df8f31/fonttools-4.66.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b432b6b54a0f3be118c6243312c826aa086aa9bcb150160be4f0bc8a21ea3aaa", size = 3088761, upload-time = "2026-09-23T17:56:49.236Z" }, + { url = "https://files.pythonhosted.org/packages/c2/cb/0b243306434748a071d12fb6757f6494534de7fbfe4e05d78ec1d5b1c1b0/fonttools-4.66.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8009e736e1491c569392b2e9782c30dcb6ac879def102c8c2e06add8d2adc10f", size = 2586088, upload-time = "2026-09-23T17:56:51.916Z" }, + { url = "https://files.pythonhosted.org/packages/25/57/9499457eec5e40a1e9eb44fc81ab3451e4590ea767377e55ecfd9931c316/fonttools-4.66.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba65eb3e2c46ad0922d84ef78e287c2f48b7a8cae4420728bf3b8e583282d6d7", size = 5483789, upload-time = "2026-09-23T17:56:53.945Z" }, + { url = "https://files.pythonhosted.org/packages/3a/d2/43fa30bb42476a587381c4a5df8d3f305ad787ef6e5f203800b891ff074a/fonttools-4.66.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3ea59aaec135c96c4cff20d79fdf1d10623993712a77990585a42cfd6a42bc5c", size = 5447057, upload-time = "2026-09-23T17:56:56.466Z" }, + { url = "https://files.pythonhosted.org/packages/3b/58/748a43b023707b1b409e8c773635064428c4383bd048206eff446db935bd/fonttools-4.66.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c3a66c749b69e9abd92e519ccbdea7210522b0fffdba3f6492a8fab461d4708a", size = 5449597, upload-time = "2026-09-23T17:56:58.802Z" }, + { url = "https://files.pythonhosted.org/packages/94/31/b881d8374e482b4fcb3874ba4317222dea099e880b6054f0bab76a45197e/fonttools-4.66.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fd3dcb69d65c05a2fffa5d9b743b77e0cdc13832665ba435065493dad2b57f83", size = 5587418, upload-time = "2026-09-23T17:57:01.707Z" }, + { url = "https://files.pythonhosted.org/packages/d4/a5/941a7e7033026a9942121bdd01a4d595c7b9b1ec76e7278c82d319184419/fonttools-4.66.0-cp311-cp311-win32.whl", hash = "sha256:4513753259f06316bdc2039c9bf3129e74923c1cfad0722b4728a9ce19beeeb2", size = 1577034, upload-time = "2026-09-23T17:57:04.03Z" }, + { url = "https://files.pythonhosted.org/packages/4d/26/440338213c9257d0deb72be98ddde8f4da900a07a694e157474c7621af2c/fonttools-4.66.0-cp311-cp311-win_amd64.whl", hash = "sha256:6371e7ed43cc9e23550fe4a1b72af3205275f28dcecac8c09b80a4582e326802", size = 1634644, upload-time = "2026-09-23T17:57:06.008Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c1/25821e3bddd0644df229cfdc2e8b4101d28e4da15ffbe6e6b563ea6f3acd/fonttools-4.66.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:67ea5af3ca60e1e5c9b2841b1f6dba5ef16aab581bf463abb68515447154ab61", size = 3096788, upload-time = "2026-09-23T17:57:08.302Z" }, + { url = "https://files.pythonhosted.org/packages/0c/3d/d16b4cb63710abd91f1aaa482f2848d8b0c402502de5b8a72ba9d2613cb0/fonttools-4.66.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:dbee639a23c4e067aedfbcf84d2e466a71a1a1156ad04d6d09023791ed167495", size = 2586926, upload-time = "2026-09-23T17:57:10.678Z" }, + { url = "https://files.pythonhosted.org/packages/e2/f0/dffa4fa83ec6780464e8484fa2f30ccc252d480137c8e4d3fc6ac510b3eb/fonttools-4.66.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:35684b562df7154d7a0ebfa512db9199c7fb0a93b5abfb59dac9d022dbee7aaa", size = 5415821, upload-time = "2026-09-23T17:57:13.174Z" }, + { url = "https://files.pythonhosted.org/packages/6d/56/3c862bdd297a272a37e2ba1149da01446fd3f387ee80855699bb939bf5b2/fonttools-4.66.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aea0cc5609a5f2a2500f91bd6e1989fdd22da0f225be00dc1c4cec775838a7d0", size = 5396689, upload-time = "2026-09-23T17:57:15.987Z" }, + { url = "https://files.pythonhosted.org/packages/93/d7/f4b7c81a7a6b316b7226d11f8e8659f023babf1e0957f8e7ddcc4e155a82/fonttools-4.66.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8af85dfc2ad564f95b3e650024578bb2268bc4d827d418aab9650bbde4b6926a", size = 5353218, upload-time = "2026-09-23T17:57:18.803Z" }, + { url = "https://files.pythonhosted.org/packages/70/1e/f12bc535f3da359a4d29d5e7442ab2d7d67579d1516b9c7e1a38890168bd/fonttools-4.66.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2b278e2abe596872b3c9fe611f956ded85588537974e48e2adbe2d6ee159e099", size = 5516965, upload-time = "2026-09-23T17:57:21.077Z" }, + { url = "https://files.pythonhosted.org/packages/14/25/a1df66888c8b5f63c14e089400f2353da0c1ff9d09683cc0bf85afea348e/fonttools-4.66.0-cp312-cp312-win32.whl", hash = "sha256:ee7b9f6c835ea1a9beb5a35c8eb0c62b6a3290f8105d3d0c29a0a18c90f9b8ba", size = 2434425, upload-time = "2026-09-23T17:57:23.463Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f2/fd490f1ddf2e430289c13059982771e6b02a2bf37504401d2a5c53307892/fonttools-4.66.0-cp312-cp312-win_amd64.whl", hash = "sha256:1d120ea0f5260b04e9b5ac0d9239efde4c23d990347d5d5889cdd37221726fec", size = 2486335, upload-time = "2026-09-23T17:57:25.561Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c8/1cf0a4f54c8cf351a4d9f8fcad5201dc0c5fb8676efdb1ba7cc3136ca523/fonttools-4.66.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c144e68572af1dfcb05fe065827d0920d4118b265504928e1d2262e3701a25a1", size = 3094615, upload-time = "2026-09-23T17:57:27.942Z" }, + { url = "https://files.pythonhosted.org/packages/c5/ed/89baf1795841eaad6fcf0ee6f319c236f4379dd98653b4bff294b83d4172/fonttools-4.66.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ada87643b20a8fa5763e6dd4fa0f5e2901cbdbe0833deb6d5c5a9c3186782b94", size = 2586645, upload-time = "2026-09-23T17:57:30.248Z" }, + { url = "https://files.pythonhosted.org/packages/56/f9/982609322f92532bdd769a225c9b3e2bbac0a077c37f7780710db9a8e018/fonttools-4.66.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700c448f190cc7bbd9f059bd96131f4770e90938c9692550b142153bea73b14c", size = 5389194, upload-time = "2026-09-23T17:57:33.07Z" }, + { url = "https://files.pythonhosted.org/packages/7d/22/12177031dd8be493d5afda574323da19f6ea5488c85202f3a5708c94757c/fonttools-4.66.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c519433e8632284dbe64ee4c81b609a17e205bc1afff285553723964880d1c89", size = 5371422, upload-time = "2026-09-23T17:57:35.915Z" }, + { url = "https://files.pythonhosted.org/packages/19/e1/e8ed0950eb846f4a404ff84aea89e4c93393c2475cdfdf1c03a1b3f4e2ef/fonttools-4.66.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8739d77810aeb6d2ecd55cc2e4864d98a00c317fb6131a6d22ca58b3f50cf22a", size = 5332183, upload-time = "2026-09-23T17:57:39.041Z" }, + { url = "https://files.pythonhosted.org/packages/3e/fb/c0310cf9838859d74130530ef351a903735c6ecf47418d7d0dc4cf459a9b/fonttools-4.66.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cffa168522a0e057d3a5628c8ebd43ddf66a34613f42e116bffb85b0b32ce5a0", size = 5490019, upload-time = "2026-09-23T17:57:41.945Z" }, + { url = "https://files.pythonhosted.org/packages/a9/7b/928f334aea18c77f6892895bb1e28388c9e2466e04bc924f93043024c780/fonttools-4.66.0-cp313-cp313-win32.whl", hash = "sha256:379b94fe10651d26caf398eb646b3ecde886a9c504d3cce5e46164d58a78e32d", size = 2432785, upload-time = "2026-09-23T17:57:44.484Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/576072a97256f06fe6d0112c522c118fe72390482baf52ad9bfa3b0d1db6/fonttools-4.66.0-cp313-cp313-win_amd64.whl", hash = "sha256:ee4f85d1341af630d787514eef1a0e1ad883f9d6c11a4c1465faeb9cd0bb5ced", size = 2484475, upload-time = "2026-09-23T17:57:46.625Z" }, + { url = "https://files.pythonhosted.org/packages/38/e6/2ea45d294ce967a8f76b5dec041abdd71c7abb652c9a81b24592edc00579/fonttools-4.66.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:ce818581070527883e3678b92b1ac7ed3569e48c80f51b89b264f97d84be2608", size = 3098619, upload-time = "2026-09-23T17:57:48.843Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d5/225d95df6b88df277d86c3d86fdaa73833cd628dc2918fcf7b73133d6f67/fonttools-4.66.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:cf0033f343cb1592fb24ae3eaf8b58156f208884ac16991094293ee94c585654", size = 2588128, upload-time = "2026-09-23T17:57:52.041Z" }, + { url = "https://files.pythonhosted.org/packages/15/e6/5892522733dc50b0577693bb164032b47d3504f33ee44d92a1a740d2e89b/fonttools-4.66.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3889fdbe63e85dbf2a0d9d7c9d90746827ce60af2cdd24f0bb4738380a9540eb", size = 5373639, upload-time = "2026-09-23T17:57:54.555Z" }, + { url = "https://files.pythonhosted.org/packages/9a/72/72ec66154babfd53a1c705a9f973c49d12fa81084aa72d939e062242ada1/fonttools-4.66.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:850d50b347f31667d3a277a6e66a4219236db9d98afadb2993b8addf9bd4f593", size = 5317190, upload-time = "2026-09-23T17:57:57.31Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c6/a8ca0334c73d1fa4fae1d423ff92e0b0be0885ed98e866e3478a00ad1a47/fonttools-4.66.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:221defad3b1949c1fdde2558d1fe780d42f926b683a71a46a4495a0e6a6614bd", size = 5315603, upload-time = "2026-09-23T17:57:59.842Z" }, + { url = "https://files.pythonhosted.org/packages/d2/34/d3d4cd925d1a934e841d39a0540539b21e952c40dfac27874bac1392f3d2/fonttools-4.66.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b02686b2e47115b2378ab6156abd984c776c6faa4f56898fbcc1c30ee387937d", size = 5448784, upload-time = "2026-09-23T17:58:02.669Z" }, + { url = "https://files.pythonhosted.org/packages/61/b9/269bebce24f590d28f2e59fd8808f3282b5f5511a4a95c5f6413f0507993/fonttools-4.66.0-cp314-cp314-win32.whl", hash = "sha256:b46500e4d6f5708a9127ea12902ba54cb0ec594fc6bec649afc53154a1125b2b", size = 2437006, upload-time = "2026-09-23T17:58:05.39Z" }, + { url = "https://files.pythonhosted.org/packages/ca/bd/9c10c9a62a274451f10295c17a01286c7777f44da87cd9f77d08c5a21d4e/fonttools-4.66.0-cp314-cp314-win_amd64.whl", hash = "sha256:0cf33d1041364ebb7a9958db757e213c6d2a238eb41755fbf935db028c2db20f", size = 2489086, upload-time = "2026-09-23T17:58:07.516Z" }, + { url = "https://files.pythonhosted.org/packages/1f/45/c9bc900dca5ed7760c1692a6b1161c380515071da9d855eddedd4bb21fb5/fonttools-4.66.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d2d10f89c4bf7cd42b84695da617f9130fa594647ee74673bf8c8d001fa04ef9", size = 3172290, upload-time = "2026-09-23T17:58:09.975Z" }, + { url = "https://files.pythonhosted.org/packages/bb/82/1017a312eeb373f30d99d032d04bf55a8ea80858f1670b2e12ed90732574/fonttools-4.66.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:135ce8738a6341fbefeedc061e20c2889350e84405abea7e61b1c9d87e1a1f2f", size = 2621293, upload-time = "2026-09-23T17:58:12.309Z" }, + { url = "https://files.pythonhosted.org/packages/af/c3/b54678b08a6637d5a222b68e1f133c58096c664403b9ead1cce4eab53fd1/fonttools-4.66.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:107aae7c38f05561fef5103baa98a44400b5f4c69d87839c9a5f8125405e7d9a", size = 5546296, upload-time = "2026-09-23T17:58:15.041Z" }, + { url = "https://files.pythonhosted.org/packages/98/2c/42e1c39e4025323bf65e042ea7ed709912aebfa22d422a2937da12268331/fonttools-4.66.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e4677025ee40b3b1420387ca85f55545b8095f7596c3d5d59552dbee19c91f07", size = 5352908, upload-time = "2026-09-23T17:58:17.77Z" }, + { url = "https://files.pythonhosted.org/packages/f1/66/07a6b9e634d7e0edb3e8f23ebb483479599842093624f13bde44e3c7a4bc/fonttools-4.66.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:03922992966a1830b94a750961d1ccfe1a7375d792ca61077a5afb719811788a", size = 5413677, upload-time = "2026-09-23T17:58:20.511Z" }, + { url = "https://files.pythonhosted.org/packages/a4/c6/d649d4d09ebbfdb0eb13cdeea731bf9685d184fef716e0c18f7779eb44fb/fonttools-4.66.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5dda4087a7f8f0b985995489b3c0115bd117f18c0fc236013a0e78e837bb9ac2", size = 5449024, upload-time = "2026-09-23T17:58:23.302Z" }, + { url = "https://files.pythonhosted.org/packages/04/4c/60c6c9a2bc4cc0ae1012df9084bfe4ba69a1a60df06f30830787a4e6c061/fonttools-4.66.0-cp314-cp314t-win32.whl", hash = "sha256:85e2960a29e6882567b981ba408b5a5ca2d8865b8f67240e1b420348997a13be", size = 2470760, upload-time = "2026-09-23T17:58:25.739Z" }, + { url = "https://files.pythonhosted.org/packages/d1/5e/fd8593bbe3da06d2cbf9c096e17becbb48ace6e7a33e0e5e21f82a55c32c/fonttools-4.66.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8ea6265535b25f8254868f5a212a1c9dbd88c2c4050f3df18d9174949df0d6d9", size = 2520614, upload-time = "2026-09-23T17:58:28.078Z" }, + { url = "https://files.pythonhosted.org/packages/83/65/826290863c9df6041f2e36a5ae5d604cd8247fffc8ec7b45581fd2473e4d/fonttools-4.66.0-py3-none-any.whl", hash = "sha256:bc7b7ddc1a1f46c363354304e9a8dd93722e4a6a24f785015650898dacf40df9", size = 1201583, upload-time = "2026-09-23T17:59:12.104Z" }, ] [[package]] @@ -603,14 +765,14 @@ wheels = [ [[package]] name = "gitpython" -version = "3.1.59" +version = "3.1.62" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "gitdb" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ca/dc/126b28e76b24a9268ba931ad3e012f71ebdadf62fd9f17758f7074bb0b20/gitpython-3.1.59.tar.gz", hash = "sha256:0a1475cfdc38a5bfba1a3e9a4a9da52a39749ecec322b772915c019f94e5b7e4", size = 230445, upload-time = "2026-08-10T12:03:20.271Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/db/3ca813cbacb23ab6fe46ff38a9b5ef8e73e970c8051f2ce903aacafe0446/gitpython-3.1.62.tar.gz", hash = "sha256:1791de66309bc0c7cfca40bf8d2e3de7ca091cbf94e6051be1ad0722c61062af", size = 231728, upload-time = "2026-09-07T02:57:21.155Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/ed/ae57eb7d344f43f87b74b3a281ead6ec7d6394eef72a7b1dcb28dd089550/gitpython-3.1.59-py3-none-any.whl", hash = "sha256:67a82f537384578643624c8b2c531938a9b82be431663e575dcf638526631d4c", size = 220996, upload-time = "2026-08-10T12:03:18.804Z" }, + { url = "https://files.pythonhosted.org/packages/d6/0b/29d7965215f8ef830a7ca1f42997fe13e5693d85e9edb18f938d063ef5f2/gitpython-3.1.62-py3-none-any.whl", hash = "sha256:7002251225e10e29d2e1f49e6532613fe5d5d9f0b6f1f02997a52b38fe56899e", size = 222753, upload-time = "2026-09-07T02:57:19.762Z" }, ] [[package]] @@ -627,11 +789,11 @@ wheels = [ [[package]] name = "idna" -version = "3.18" +version = "3.20" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/08/8eea9d4b8302028f3abb2c0813953f7aec26d33b7a8960ed760e65ff29fa/idna-3.20.tar.gz", hash = "sha256:a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44", size = 216463, upload-time = "2026-09-17T14:11:04.752Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, + { url = "https://files.pythonhosted.org/packages/58/a2/bb081bab032533a855d44de1d56f8e8426114ff1ba5d1f07a438a0a654f8/idna-3.20-py3-none-any.whl", hash = "sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c", size = 69583, upload-time = "2026-09-17T14:11:03.168Z" }, ] [[package]] @@ -674,126 +836,117 @@ wheels = [ [[package]] name = "kiwisolver" -version = "1.5.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d0/67/9c61eccb13f0bdca9307614e782fec49ffdde0f7a2314935d489fa93cd9c/kiwisolver-1.5.0.tar.gz", hash = "sha256:d4193f3d9dc3f6f79aaed0e5637f45d98850ebf01f7ca20e69457f3e8946b66a", size = 103482, upload-time = "2026-03-09T13:15:53.382Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ac/f8/06549565caa026e540b7e7bab5c5a90eb7ca986015f4c48dace243cd24d9/kiwisolver-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:32cc0a5365239a6ea0c6ed461e8838d053b57e397443c0ca894dcc8e388d4374", size = 122802, upload-time = "2026-03-09T13:12:37.515Z" }, - { url = "https://files.pythonhosted.org/packages/84/eb/8476a0818850c563ff343ea7c9c05dcdcbd689a38e01aa31657df01f91fa/kiwisolver-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cc0b66c1eec9021353a4b4483afb12dfd50e3669ffbb9152d6842eb34c7e29fd", size = 66216, upload-time = "2026-03-09T13:12:38.812Z" }, - { url = "https://files.pythonhosted.org/packages/f3/c4/f9c8a6b4c21aed4198566e45923512986d6cef530e7263b3a5f823546561/kiwisolver-1.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:86e0287879f75621ae85197b0877ed2f8b7aa57b511c7331dce2eb6f4de7d476", size = 63917, upload-time = "2026-03-09T13:12:40.053Z" }, - { url = "https://files.pythonhosted.org/packages/f1/0e/ba4ae25d03722f64de8b2c13e80d82ab537a06b30fc7065183c6439357e3/kiwisolver-1.5.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:62f59da443c4f4849f73a51a193b1d9d258dcad0c41bc4d1b8fb2bcc04bfeb22", size = 1628776, upload-time = "2026-03-09T13:12:41.976Z" }, - { url = "https://files.pythonhosted.org/packages/8a/e4/3f43a011bc8a0860d1c96f84d32fa87439d3feedf66e672fef03bf5e8bac/kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9190426b7aa26c5229501fa297b8d0653cfd3f5a36f7990c264e157cbf886b3b", size = 1228164, upload-time = "2026-03-09T13:12:44.002Z" }, - { url = "https://files.pythonhosted.org/packages/4b/34/3a901559a1e0c218404f9a61a93be82d45cb8f44453ba43088644980f033/kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c8277104ded0a51e699c8c3aff63ce2c56d4ed5519a5f73e0fd7057f959a2b9e", size = 1246656, upload-time = "2026-03-09T13:12:45.557Z" }, - { url = "https://files.pythonhosted.org/packages/87/9e/f78c466ea20527822b95ad38f141f2de1dcd7f23fb8716b002b0d91bbe59/kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8f9baf6f0a6e7571c45c8863010b45e837c3ee1c2c77fcd6ef423be91b21fedb", size = 1295562, upload-time = "2026-03-09T13:12:47.562Z" }, - { url = "https://files.pythonhosted.org/packages/0a/66/fd0e4a612e3a286c24e6d6f3a5428d11258ed1909bc530ba3b59807fd980/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cff8e5383db4989311f99e814feeb90c4723eb4edca425b9d5d9c3fefcdd9537", size = 2178473, upload-time = "2026-03-09T13:12:50.254Z" }, - { url = "https://files.pythonhosted.org/packages/dc/8e/6cac929e0049539e5ee25c1ee937556f379ba5204840d03008363ced662d/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ebae99ed6764f2b5771c522477b311be313e8841d2e0376db2b10922daebbba4", size = 2274035, upload-time = "2026-03-09T13:12:51.785Z" }, - { url = "https://files.pythonhosted.org/packages/ca/d3/9d0c18f1b52ea8074b792452cf17f1f5a56bd0302a85191f405cfbf9da16/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:d5cd5189fc2b6a538b75ae45433140c4823463918f7b1617c31e68b085c0022c", size = 2443217, upload-time = "2026-03-09T13:12:53.329Z" }, - { url = "https://files.pythonhosted.org/packages/45/2a/6e19368803a038b2a90857bf4ee9e3c7b667216d045866bf22d3439fd75e/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f42c23db5d1521218a3276bb08666dcb662896a0be7347cba864eca45ff64ede", size = 2249196, upload-time = "2026-03-09T13:12:55.057Z" }, - { url = "https://files.pythonhosted.org/packages/75/2b/3f641dfcbe72e222175d626bacf2f72c3b34312afec949dd1c50afa400f5/kiwisolver-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:94eff26096eb5395136634622515b234ecb6c9979824c1f5004c6e3c3c85ccd2", size = 73389, upload-time = "2026-03-09T13:12:56.496Z" }, - { url = "https://files.pythonhosted.org/packages/da/88/299b137b9e0025d8982e03d2d52c123b0a2b159e84b0ef1501ef446339cf/kiwisolver-1.5.0-cp310-cp310-win_arm64.whl", hash = "sha256:dd952e03bfbb096cfe2dd35cd9e00f269969b67536cb4370994afc20ff2d0875", size = 64782, upload-time = "2026-03-09T13:12:57.609Z" }, - { url = "https://files.pythonhosted.org/packages/12/dd/a495a9c104be1c476f0386e714252caf2b7eca883915422a64c50b88c6f5/kiwisolver-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9eed0f7edbb274413b6ee781cca50541c8c0facd3d6fd289779e494340a2b85c", size = 122798, upload-time = "2026-03-09T13:12:58.963Z" }, - { url = "https://files.pythonhosted.org/packages/11/60/37b4047a2af0cf5ef6d8b4b26e91829ae6fc6a2d1f74524bcb0e7cd28a32/kiwisolver-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c4923e404d6bcd91b6779c009542e5647fef32e4a5d75e115e3bbac6f2335eb", size = 66216, upload-time = "2026-03-09T13:13:00.155Z" }, - { url = "https://files.pythonhosted.org/packages/0a/aa/510dc933d87767584abfe03efa445889996c70c2990f6f87c3ebaa0a18c5/kiwisolver-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0df54df7e686afa55e6f21fb86195224a6d9beb71d637e8d7920c95cf0f89aac", size = 63911, upload-time = "2026-03-09T13:13:01.671Z" }, - { url = "https://files.pythonhosted.org/packages/80/46/bddc13df6c2a40741e0cc7865bb1c9ed4796b6760bd04ce5fae3928ef917/kiwisolver-1.5.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2517e24d7315eb51c10664cdb865195df38ab74456c677df67bb47f12d088a27", size = 1438209, upload-time = "2026-03-09T13:13:03.385Z" }, - { url = "https://files.pythonhosted.org/packages/fd/d6/76621246f5165e5372f02f5e6f3f48ea336a8f9e96e43997d45b240ed8cd/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff710414307fefa903e0d9bdf300972f892c23477829f49504e59834f4195398", size = 1248888, upload-time = "2026-03-09T13:13:05.231Z" }, - { url = "https://files.pythonhosted.org/packages/b2/c1/31559ec6fb39a5b48035ce29bb63ade628f321785f38c384dee3e2c08bc1/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6176c1811d9d5a04fa391c490cc44f451e240697a16977f11c6f722efb9041db", size = 1266304, upload-time = "2026-03-09T13:13:06.743Z" }, - { url = "https://files.pythonhosted.org/packages/5e/ef/1cb8276f2d29cc6a41e0a042f27946ca347d3a4a75acf85d0a16aa6dcc82/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50847dca5d197fcbd389c805aa1a1cf32f25d2e7273dc47ab181a517666b68cc", size = 1319650, upload-time = "2026-03-09T13:13:08.607Z" }, - { url = "https://files.pythonhosted.org/packages/4c/e4/5ba3cecd7ce6236ae4a80f67e5d5531287337d0e1f076ca87a5abe4cd5d0/kiwisolver-1.5.0-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:01808c6d15f4c3e8559595d6d1fe6411c68e4a3822b4b9972b44473b24f4e679", size = 970949, upload-time = "2026-03-09T13:13:10.299Z" }, - { url = "https://files.pythonhosted.org/packages/5a/69/dc61f7ae9a2f071f26004ced87f078235b5507ab6e5acd78f40365655034/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f1f9f4121ec58628c96baa3de1a55a4e3a333c5102c8e94b64e23bf7b2083309", size = 2199125, upload-time = "2026-03-09T13:13:11.841Z" }, - { url = "https://files.pythonhosted.org/packages/e5/7b/abbe0f1b5afa85f8d084b73e90e5f801c0939eba16ac2e49af7c61a6c28d/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b7d335370ae48a780c6e6a6bbfa97342f563744c39c35562f3f367665f5c1de2", size = 2293783, upload-time = "2026-03-09T13:13:14.399Z" }, - { url = "https://files.pythonhosted.org/packages/8a/80/5908ae149d96d81580d604c7f8aefd0e98f4fd728cf172f477e9f2a81744/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:800ee55980c18545af444d93fdd60c56b580db5cc54867d8cbf8a1dc0829938c", size = 1960726, upload-time = "2026-03-09T13:13:16.047Z" }, - { url = "https://files.pythonhosted.org/packages/84/08/a78cb776f8c085b7143142ce479859cfec086bd09ee638a317040b6ef420/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:c438f6ca858697c9ab67eb28246c92508af972e114cac34e57a6d4ba17a3ac08", size = 2464738, upload-time = "2026-03-09T13:13:17.897Z" }, - { url = "https://files.pythonhosted.org/packages/b1/e1/65584da5356ed6cb12c63791a10b208860ac40a83de165cb6a6751a686e3/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8c63c91f95173f9c2a67c7c526b2cea976828a0e7fced9cdcead2802dc10f8a4", size = 2270718, upload-time = "2026-03-09T13:13:19.421Z" }, - { url = "https://files.pythonhosted.org/packages/be/6c/28f17390b62b8f2f520e2915095b3c94d88681ecf0041e75389d9667f202/kiwisolver-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:beb7f344487cdcb9e1efe4b7a29681b74d34c08f0043a327a74da852a6749e7b", size = 73480, upload-time = "2026-03-09T13:13:20.818Z" }, - { url = "https://files.pythonhosted.org/packages/d8/0e/2ee5debc4f77a625778fec5501ff3e8036fe361b7ee28ae402a485bb9694/kiwisolver-1.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:ad4ae4ffd1ee9cd11357b4c66b612da9888f4f4daf2f36995eda64bd45370cac", size = 64930, upload-time = "2026-03-09T13:13:21.997Z" }, - { url = "https://files.pythonhosted.org/packages/4d/b2/818b74ebea34dabe6d0c51cb1c572e046730e64844da6ed646d5298c40ce/kiwisolver-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4e9750bc21b886308024f8a54ccb9a2cc38ac9fa813bf4348434e3d54f337ff9", size = 123158, upload-time = "2026-03-09T13:13:23.127Z" }, - { url = "https://files.pythonhosted.org/packages/bf/d9/405320f8077e8e1c5c4bd6adc45e1e6edf6d727b6da7f2e2533cf58bff71/kiwisolver-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:72ec46b7eba5b395e0a7b63025490d3214c11013f4aacb4f5e8d6c3041829588", size = 66388, upload-time = "2026-03-09T13:13:24.765Z" }, - { url = "https://files.pythonhosted.org/packages/99/9f/795fedf35634f746151ca8839d05681ceb6287fbed6cc1c9bf235f7887c2/kiwisolver-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ed3a984b31da7481b103f68776f7128a89ef26ed40f4dc41a2223cda7fb24819", size = 64068, upload-time = "2026-03-09T13:13:25.878Z" }, - { url = "https://files.pythonhosted.org/packages/c4/13/680c54afe3e65767bed7ec1a15571e1a2f1257128733851ade24abcefbcc/kiwisolver-1.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb5136fb5352d3f422df33f0c879a1b0c204004324150cc3b5e3c4f310c9049f", size = 1477934, upload-time = "2026-03-09T13:13:27.166Z" }, - { url = "https://files.pythonhosted.org/packages/c8/2f/cebfcdb60fd6a9b0f6b47a9337198bcbad6fbe15e68189b7011fd914911f/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2af221f268f5af85e776a73d62b0845fc8baf8ef0abfae79d29c77d0e776aaf", size = 1278537, upload-time = "2026-03-09T13:13:28.707Z" }, - { url = "https://files.pythonhosted.org/packages/f2/0d/9b782923aada3fafb1d6b84e13121954515c669b18af0c26e7d21f579855/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b0f172dc8ffaccb8522d7c5d899de00133f2f1ca7b0a49b7da98e901de87bf2d", size = 1296685, upload-time = "2026-03-09T13:13:30.528Z" }, - { url = "https://files.pythonhosted.org/packages/27/70/83241b6634b04fe44e892688d5208332bde130f38e610c0418f9ede47ded/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6ab8ba9152203feec73758dad83af9a0bbe05001eb4639e547207c40cfb52083", size = 1346024, upload-time = "2026-03-09T13:13:32.818Z" }, - { url = "https://files.pythonhosted.org/packages/e4/db/30ed226fb271ae1a6431fc0fe0edffb2efe23cadb01e798caeb9f2ceae8f/kiwisolver-1.5.0-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:cdee07c4d7f6d72008d3f73b9bf027f4e11550224c7c50d8df1ae4a37c1402a6", size = 987241, upload-time = "2026-03-09T13:13:34.435Z" }, - { url = "https://files.pythonhosted.org/packages/ec/bd/c314595208e4c9587652d50959ead9e461995389664e490f4dce7ff0f782/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7c60d3c9b06fb23bd9c6139281ccbdc384297579ae037f08ae90c69f6845c0b1", size = 2227742, upload-time = "2026-03-09T13:13:36.4Z" }, - { url = "https://files.pythonhosted.org/packages/c1/43/0499cec932d935229b5543d073c2b87c9c22846aab48881e9d8d6e742a2d/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e315e5ec90d88e140f57696ff85b484ff68bb311e36f2c414aa4286293e6dee0", size = 2323966, upload-time = "2026-03-09T13:13:38.204Z" }, - { url = "https://files.pythonhosted.org/packages/3d/6f/79b0d760907965acfd9d61826a3d41f8f093c538f55cd2633d3f0db269f6/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:1465387ac63576c3e125e5337a6892b9e99e0627d52317f3ca79e6930d889d15", size = 1977417, upload-time = "2026-03-09T13:13:39.966Z" }, - { url = "https://files.pythonhosted.org/packages/ab/31/01d0537c41cb75a551a438c3c7a80d0c60d60b81f694dac83dd436aec0d0/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:530a3fd64c87cffa844d4b6b9768774763d9caa299e9b75d8eca6a4423b31314", size = 2491238, upload-time = "2026-03-09T13:13:41.698Z" }, - { url = "https://files.pythonhosted.org/packages/e4/34/8aefdd0be9cfd00a44509251ba864f5caf2991e36772e61c408007e7f417/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1d9daea4ea6b9be74fe2f01f7fbade8d6ffab263e781274cffca0dba9be9eec9", size = 2294947, upload-time = "2026-03-09T13:13:43.343Z" }, - { url = "https://files.pythonhosted.org/packages/ad/cf/0348374369ca588f8fe9c338fae49fa4e16eeb10ffb3d012f23a54578a9e/kiwisolver-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:f18c2d9782259a6dc132fdc7a63c168cbc74b35284b6d75c673958982a378384", size = 73569, upload-time = "2026-03-09T13:13:45.792Z" }, - { url = "https://files.pythonhosted.org/packages/28/26/192b26196e2316e2bd29deef67e37cdf9870d9af8e085e521afff0fed526/kiwisolver-1.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:f7c7553b13f69c1b29a5bde08ddc6d9d0c8bfb84f9ed01c30db25944aeb852a7", size = 64997, upload-time = "2026-03-09T13:13:46.878Z" }, - { url = "https://files.pythonhosted.org/packages/9d/69/024d6711d5ba575aa65d5538042e99964104e97fa153a9f10bc369182bc2/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:fd40bb9cd0891c4c3cb1ddf83f8bbfa15731a248fdc8162669405451e2724b09", size = 123166, upload-time = "2026-03-09T13:13:48.032Z" }, - { url = "https://files.pythonhosted.org/packages/ce/48/adbb40df306f587054a348831220812b9b1d787aff714cfbc8556e38fccd/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c0e1403fd7c26d77c1f03e096dc58a5c726503fa0db0456678b8668f76f521e3", size = 66395, upload-time = "2026-03-09T13:13:49.365Z" }, - { url = "https://files.pythonhosted.org/packages/a8/3a/d0a972b34e1c63e2409413104216cd1caa02c5a37cb668d1687d466c1c45/kiwisolver-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dda366d548e89a90d88a86c692377d18d8bd64b39c1fb2b92cb31370e2896bbd", size = 64065, upload-time = "2026-03-09T13:13:50.562Z" }, - { url = "https://files.pythonhosted.org/packages/2b/0a/7b98e1e119878a27ba8618ca1e18b14f992ff1eda40f47bccccf4de44121/kiwisolver-1.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:332b4f0145c30b5f5ad9374881133e5aa64320428a57c2c2b61e9d891a51c2f3", size = 1477903, upload-time = "2026-03-09T13:13:52.084Z" }, - { url = "https://files.pythonhosted.org/packages/18/d8/55638d89ffd27799d5cc3d8aa28e12f4ce7a64d67b285114dbedc8ea4136/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c50b89ffd3e1a911c69a1dd3de7173c0cd10b130f56222e57898683841e4f96", size = 1278751, upload-time = "2026-03-09T13:13:54.673Z" }, - { url = "https://files.pythonhosted.org/packages/b8/97/b4c8d0d18421ecceba20ad8701358453b88e32414e6f6950b5a4bad54e65/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4db576bb8c3ef9365f8b40fe0f671644de6736ae2c27a2c62d7d8a1b4329f099", size = 1296793, upload-time = "2026-03-09T13:13:56.287Z" }, - { url = "https://files.pythonhosted.org/packages/c4/10/f862f94b6389d8957448ec9df59450b81bec4abb318805375c401a1e6892/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0b85aad90cea8ac6797a53b5d5f2e967334fa4d1149f031c4537569972596cb8", size = 1346041, upload-time = "2026-03-09T13:13:58.269Z" }, - { url = "https://files.pythonhosted.org/packages/a3/6a/f1650af35821eaf09de398ec0bc2aefc8f211f0cda50204c9f1673741ba9/kiwisolver-1.5.0-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:d36ca54cb4c6c4686f7cbb7b817f66f5911c12ddb519450bbe86707155028f87", size = 987292, upload-time = "2026-03-09T13:13:59.871Z" }, - { url = "https://files.pythonhosted.org/packages/de/19/d7fb82984b9238115fe629c915007be608ebd23dc8629703d917dbfaffd4/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:38f4a703656f493b0ad185211ccfca7f0386120f022066b018eb5296d8613e23", size = 2227865, upload-time = "2026-03-09T13:14:01.401Z" }, - { url = "https://files.pythonhosted.org/packages/7f/b9/46b7f386589fd222dac9e9de9c956ce5bcefe2ee73b4e79891381dda8654/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3ac2360e93cb41be81121755c6462cff3beaa9967188c866e5fce5cf13170859", size = 2324369, upload-time = "2026-03-09T13:14:02.972Z" }, - { url = "https://files.pythonhosted.org/packages/92/8b/95e237cf3d9c642960153c769ddcbe278f182c8affb20cecc1cc983e7cc5/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c95cab08d1965db3d84a121f1c7ce7479bdd4072c9b3dafd8fecce48a2e6b902", size = 1977989, upload-time = "2026-03-09T13:14:04.503Z" }, - { url = "https://files.pythonhosted.org/packages/1b/95/980c9df53501892784997820136c01f62bc1865e31b82b9560f980c0e649/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fc20894c3d21194d8041a28b65622d5b86db786da6e3cfe73f0c762951a61167", size = 2491645, upload-time = "2026-03-09T13:14:06.106Z" }, - { url = "https://files.pythonhosted.org/packages/cb/32/900647fd0840abebe1561792c6b31e6a7c0e278fc3973d30572a965ca14c/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7a32f72973f0f950c1920475d5c5ea3d971b81b6f0ec53b8d0a956cc965f22e0", size = 2295237, upload-time = "2026-03-09T13:14:08.891Z" }, - { url = "https://files.pythonhosted.org/packages/be/8a/be60e3bbcf513cc5a50f4a3e88e1dcecebb79c1ad607a7222877becaa101/kiwisolver-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bf3acf1419fa93064a4c2189ac0b58e3be7872bf6ee6177b0d4c63dc4cea276", size = 73573, upload-time = "2026-03-09T13:14:12.327Z" }, - { url = "https://files.pythonhosted.org/packages/4d/d2/64be2e429eb4fca7f7e1c52a91b12663aeaf25de3895e5cca0f47ef2a8d0/kiwisolver-1.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:fa8eb9ecdb7efb0b226acec134e0d709e87a909fa4971a54c0c4f6e88635484c", size = 64998, upload-time = "2026-03-09T13:14:13.469Z" }, - { url = "https://files.pythonhosted.org/packages/b0/69/ce68dd0c85755ae2de490bf015b62f2cea5f6b14ff00a463f9d0774449ff/kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:db485b3847d182b908b483b2ed133c66d88d49cacf98fd278fadafe11b4478d1", size = 125700, upload-time = "2026-03-09T13:14:14.636Z" }, - { url = "https://files.pythonhosted.org/packages/74/aa/937aac021cf9d4349990d47eb319309a51355ed1dbdc9c077cdc9224cb11/kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:be12f931839a3bdfe28b584db0e640a65a8bcbc24560ae3fdb025a449b3d754e", size = 67537, upload-time = "2026-03-09T13:14:15.808Z" }, - { url = "https://files.pythonhosted.org/packages/ee/20/3a87fbece2c40ad0f6f0aefa93542559159c5f99831d596050e8afae7a9f/kiwisolver-1.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:16b85d37c2cbb3253226d26e64663f755d88a03439a9c47df6246b35defbdfb7", size = 65514, upload-time = "2026-03-09T13:14:18.035Z" }, - { url = "https://files.pythonhosted.org/packages/f0/7f/f943879cda9007c45e1f7dba216d705c3a18d6b35830e488b6c6a4e7cdf0/kiwisolver-1.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4432b835675f0ea7414aab3d37d119f7226d24869b7a829caeab49ebda407b0c", size = 1584848, upload-time = "2026-03-09T13:14:19.745Z" }, - { url = "https://files.pythonhosted.org/packages/37/f8/4d4f85cc1870c127c88d950913370dd76138482161cd07eabbc450deff01/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b0feb50971481a2cc44d94e88bdb02cdd497618252ae226b8eb1201b957e368", size = 1391542, upload-time = "2026-03-09T13:14:21.54Z" }, - { url = "https://files.pythonhosted.org/packages/04/0b/65dd2916c84d252b244bd405303220f729e7c17c9d7d33dca6feeff9ffc4/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56fa888f10d0f367155e76ce849fa1166fc9730d13bd2d65a2aa13b6f5424489", size = 1404447, upload-time = "2026-03-09T13:14:23.205Z" }, - { url = "https://files.pythonhosted.org/packages/39/5c/2606a373247babce9b1d056c03a04b65f3cf5290a8eac5d7bdead0a17e21/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:940dda65d5e764406b9fb92761cbf462e4e63f712ab60ed98f70552e496f3bf1", size = 1455918, upload-time = "2026-03-09T13:14:24.74Z" }, - { url = "https://files.pythonhosted.org/packages/d5/d1/c6078b5756670658e9192a2ef11e939c92918833d2745f85cd14a6004bdf/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_39_riscv64.whl", hash = "sha256:89fc958c702ee9a745e4700378f5d23fddbc46ff89e8fdbf5395c24d5c1452a3", size = 1072856, upload-time = "2026-03-09T13:14:26.597Z" }, - { url = "https://files.pythonhosted.org/packages/cb/c8/7def6ddf16eb2b3741d8b172bdaa9af882b03c78e9b0772975408801fa63/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9027d773c4ff81487181a925945743413f6069634d0b122d0b37684ccf4f1e18", size = 2333580, upload-time = "2026-03-09T13:14:28.237Z" }, - { url = "https://files.pythonhosted.org/packages/9e/87/2ac1fce0eb1e616fcd3c35caa23e665e9b1948bb984f4764790924594128/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:5b233ea3e165e43e35dba1d2b8ecc21cf070b45b65ae17dd2747d2713d942021", size = 2423018, upload-time = "2026-03-09T13:14:30.018Z" }, - { url = "https://files.pythonhosted.org/packages/67/13/c6700ccc6cc218716bfcda4935e4b2997039869b4ad8a94f364c5a3b8e63/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ce9bf03dad3b46408c08649c6fbd6ca28a9fce0eb32fdfffa6775a13103b5310", size = 2062804, upload-time = "2026-03-09T13:14:32.888Z" }, - { url = "https://files.pythonhosted.org/packages/1b/bd/877056304626943ff0f1f44c08f584300c199b887cb3176cd7e34f1515f1/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:fc4d3f1fb9ca0ae9f97b095963bc6326f1dbfd3779d6679a1e016b9baaa153d3", size = 2597482, upload-time = "2026-03-09T13:14:34.971Z" }, - { url = "https://files.pythonhosted.org/packages/75/19/c60626c47bf0f8ac5dcf72c6c98e266d714f2fbbfd50cf6dab5ede3aaa50/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f443b4825c50a51ee68585522ab4a1d1257fac65896f282b4c6763337ac9f5d2", size = 2394328, upload-time = "2026-03-09T13:14:36.816Z" }, - { url = "https://files.pythonhosted.org/packages/47/84/6a6d5e5bb8273756c27b7d810d47f7ef2f1f9b9fd23c9ee9a3f8c75c9cef/kiwisolver-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:893ff3a711d1b515ba9da14ee090519bad4610ed1962fbe298a434e8c5f8db53", size = 68410, upload-time = "2026-03-09T13:14:38.695Z" }, - { url = "https://files.pythonhosted.org/packages/e4/d7/060f45052f2a01ad5762c8fdecd6d7a752b43400dc29ff75cd47225a40fd/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8df31fe574b8b3993cc61764f40941111b25c2d9fea13d3ce24a49907cd2d615", size = 123231, upload-time = "2026-03-09T13:14:41.323Z" }, - { url = "https://files.pythonhosted.org/packages/c2/a7/78da680eadd06ff35edef6ef68a1ad273bad3e2a0936c9a885103230aece/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1d49a49ac4cbfb7c1375301cd1ec90169dfeae55ff84710d782260ce77a75a02", size = 66489, upload-time = "2026-03-09T13:14:42.534Z" }, - { url = "https://files.pythonhosted.org/packages/49/b2/97980f3ad4fae37dd7fe31626e2bf75fbf8bdf5d303950ec1fab39a12da8/kiwisolver-1.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0cbe94b69b819209a62cb27bdfa5dc2a8977d8de2f89dfd97ba4f53ed3af754e", size = 64063, upload-time = "2026-03-09T13:14:44.759Z" }, - { url = "https://files.pythonhosted.org/packages/e7/f9/b06c934a6aa8bc91f566bd2a214fd04c30506c2d9e2b6b171953216a65b6/kiwisolver-1.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80aa065ffd378ff784822a6d7c3212f2d5f5e9c3589614b5c228b311fd3063ac", size = 1475913, upload-time = "2026-03-09T13:14:46.247Z" }, - { url = "https://files.pythonhosted.org/packages/6b/f0/f768ae564a710135630672981231320bc403cf9152b5596ec5289de0f106/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e7f886f47ab881692f278ae901039a234e4025a68e6dfab514263a0b1c4ae05", size = 1282782, upload-time = "2026-03-09T13:14:48.458Z" }, - { url = "https://files.pythonhosted.org/packages/e2/9f/1de7aad00697325f05238a5f2eafbd487fb637cc27a558b5367a5f37fb7f/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5060731cc3ed12ca3a8b57acd4aeca5bbc2f49216dd0bec1650a1acd89486bcd", size = 1300815, upload-time = "2026-03-09T13:14:50.721Z" }, - { url = "https://files.pythonhosted.org/packages/5a/c2/297f25141d2e468e0ce7f7a7b92e0cf8918143a0cbd3422c1ad627e85a06/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a4aa69609f40fce3cbc3f87b2061f042eee32f94b8f11db707b66a26461591a", size = 1347925, upload-time = "2026-03-09T13:14:52.304Z" }, - { url = "https://files.pythonhosted.org/packages/b9/d3/f4c73a02eb41520c47610207b21afa8cdd18fdbf64ffd94674ae21c4812d/kiwisolver-1.5.0-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:d168fda2dbff7b9b5f38e693182d792a938c31db4dac3a80a4888de603c99554", size = 991322, upload-time = "2026-03-09T13:14:54.637Z" }, - { url = "https://files.pythonhosted.org/packages/7b/46/d3f2efef7732fcda98d22bf4ad5d3d71d545167a852ca710a494f4c15343/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:413b820229730d358efd838ecbab79902fe97094565fdc80ddb6b0a18c18a581", size = 2232857, upload-time = "2026-03-09T13:14:56.471Z" }, - { url = "https://files.pythonhosted.org/packages/3f/ec/2d9756bf2b6d26ae4349b8d3662fb3993f16d80c1f971c179ce862b9dbae/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5124d1ea754509b09e53738ec185584cc609aae4a3b510aaf4ed6aa047ef9303", size = 2329376, upload-time = "2026-03-09T13:14:58.072Z" }, - { url = "https://files.pythonhosted.org/packages/8f/9f/876a0a0f2260f1bde92e002b3019a5fabc35e0939c7d945e0fa66185eb20/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e4415a8db000bf49a6dd1c478bf70062eaacff0f462b92b0ba68791a905861f9", size = 1982549, upload-time = "2026-03-09T13:14:59.668Z" }, - { url = "https://files.pythonhosted.org/packages/6c/4f/ba3624dfac23a64d54ac4179832860cb537c1b0af06024936e82ca4154a0/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d618fd27420381a4f6044faa71f46d8bfd911bd077c555f7138ed88729bfbe79", size = 2494680, upload-time = "2026-03-09T13:15:01.364Z" }, - { url = "https://files.pythonhosted.org/packages/39/b7/97716b190ab98911b20d10bf92eca469121ec483b8ce0edd314f51bc85af/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5092eb5b1172947f57d6ea7d89b2f29650414e4293c47707eb499ec07a0ac796", size = 2297905, upload-time = "2026-03-09T13:15:03.925Z" }, - { url = "https://files.pythonhosted.org/packages/a3/36/4e551e8aa55c9188bca9abb5096805edbf7431072b76e2298e34fd3a3008/kiwisolver-1.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:d76e2d8c75051d58177e762164d2e9ab92886534e3a12e795f103524f221dd8e", size = 75086, upload-time = "2026-03-09T13:15:07.775Z" }, - { url = "https://files.pythonhosted.org/packages/70/15/9b90f7df0e31a003c71649cf66ef61c3c1b862f48c81007fa2383c8bd8d7/kiwisolver-1.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:fa6248cd194edff41d7ea9425ced8ca3a6f838bfb295f6f1d6e6bb694a8518df", size = 66577, upload-time = "2026-03-09T13:15:09.139Z" }, - { url = "https://files.pythonhosted.org/packages/17/01/7dc8c5443ff42b38e72731643ed7cf1ed9bf01691ae5cdca98501999ed83/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d1ffeb80b5676463d7a7d56acbe8e37a20ce725570e09549fe738e02ca6b7e1e", size = 125794, upload-time = "2026-03-09T13:15:10.525Z" }, - { url = "https://files.pythonhosted.org/packages/46/8a/b4ebe46ebaac6a303417fab10c2e165c557ddaff558f9699d302b256bc53/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc4d8e252f532ab46a1de9349e2d27b91fce46736a9eedaa37beaca66f574ed4", size = 67646, upload-time = "2026-03-09T13:15:12.016Z" }, - { url = "https://files.pythonhosted.org/packages/60/35/10a844afc5f19d6f567359bf4789e26661755a2f36200d5d1ed8ad0126e5/kiwisolver-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6783e069732715ad0c3ce96dbf21dbc2235ab0593f2baf6338101f70371f4028", size = 65511, upload-time = "2026-03-09T13:15:13.311Z" }, - { url = "https://files.pythonhosted.org/packages/f8/8a/685b297052dd041dcebce8e8787b58923b6e78acc6115a0dc9189011c44b/kiwisolver-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e7c4c09a490dc4d4a7f8cbee56c606a320f9dc28cf92a7157a39d1ce7676a657", size = 1584858, upload-time = "2026-03-09T13:15:15.103Z" }, - { url = "https://files.pythonhosted.org/packages/9e/80/04865e3d4638ac5bddec28908916df4a3075b8c6cc101786a96803188b96/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a075bd7bd19c70cf67c8badfa36cf7c5d8de3c9ddb8420c51e10d9c50e94920", size = 1392539, upload-time = "2026-03-09T13:15:16.661Z" }, - { url = "https://files.pythonhosted.org/packages/ba/01/77a19cacc0893fa13fafa46d1bba06fb4dc2360b3292baf4b56d8e067b24/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bdd3e53429ff02aa319ba59dfe4ceeec345bf46cf180ec2cf6fd5b942e7975e9", size = 1405310, upload-time = "2026-03-09T13:15:18.229Z" }, - { url = "https://files.pythonhosted.org/packages/53/39/bcaf5d0cca50e604cfa9b4e3ae1d64b50ca1ae5b754122396084599ef903/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cdcb35dc9d807259c981a85531048ede628eabcffb3239adf3d17463518992d", size = 1456244, upload-time = "2026-03-09T13:15:20.444Z" }, - { url = "https://files.pythonhosted.org/packages/d0/7a/72c187abc6975f6978c3e39b7cf67aeb8b3c0a8f9790aa7fd412855e9e1f/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:70d593af6a6ca332d1df73d519fddb5148edb15cd90d5f0155e3746a6d4fcc65", size = 1073154, upload-time = "2026-03-09T13:15:22.039Z" }, - { url = "https://files.pythonhosted.org/packages/c7/ca/cf5b25783ebbd59143b4371ed0c8428a278abe68d6d0104b01865b1bbd0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:377815a8616074cabbf3f53354e1d040c35815a134e01d7614b7692e4bf8acfa", size = 2334377, upload-time = "2026-03-09T13:15:23.741Z" }, - { url = "https://files.pythonhosted.org/packages/4a/e5/b1f492adc516796e88751282276745340e2a72dcd0d36cf7173e0daf3210/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0255a027391d52944eae1dbb5d4cc5903f57092f3674e8e544cdd2622826b3f0", size = 2425288, upload-time = "2026-03-09T13:15:25.789Z" }, - { url = "https://files.pythonhosted.org/packages/e6/e5/9b21fbe91a61b8f409d74a26498706e97a48008bfcd1864373d32a6ba31c/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:012b1eb16e28718fa782b5e61dc6f2da1f0792ca73bd05d54de6cb9561665fc9", size = 2063158, upload-time = "2026-03-09T13:15:27.63Z" }, - { url = "https://files.pythonhosted.org/packages/b1/02/83f47986138310f95ea95531f851b2a62227c11cbc3e690ae1374fe49f0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e3aafb33aed7479377e5e9a82e9d4bf87063741fc99fc7ae48b0f16e32bdd6f", size = 2597260, upload-time = "2026-03-09T13:15:29.421Z" }, - { url = "https://files.pythonhosted.org/packages/07/18/43a5f24608d8c313dd189cf838c8e68d75b115567c6279de7796197cfb6a/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7a116ae737f0000343218c4edf5bd45893bfeaff0993c0b215d7124c9f77646", size = 2394403, upload-time = "2026-03-09T13:15:31.517Z" }, - { url = "https://files.pythonhosted.org/packages/3b/b5/98222136d839b8afabcaa943b09bd05888c2d36355b7e448550211d1fca4/kiwisolver-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1dd9b0b119a350976a6d781e7278ec7aca0b201e1a9e2d23d9804afecb6ca681", size = 79687, upload-time = "2026-03-09T13:15:33.204Z" }, - { url = "https://files.pythonhosted.org/packages/99/a2/ca7dc962848040befed12732dff6acae7fb3c4f6fc4272b3f6c9a30b8713/kiwisolver-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:58f812017cd2985c21fbffb4864d59174d4903dd66fa23815e74bbc7a0e2dd57", size = 70032, upload-time = "2026-03-09T13:15:34.411Z" }, - { url = "https://files.pythonhosted.org/packages/1c/fa/2910df836372d8761bb6eff7d8bdcb1613b5c2e03f260efe7abe34d388a7/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_10_13_x86_64.whl", hash = "sha256:5ae8e62c147495b01a0f4765c878e9bfdf843412446a247e28df59936e99e797", size = 130262, upload-time = "2026-03-09T13:15:35.629Z" }, - { url = "https://files.pythonhosted.org/packages/0f/41/c5f71f9f00aabcc71fee8b7475e3f64747282580c2fe748961ba29b18385/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f6764a4ccab3078db14a632420930f6186058750df066b8ea2a7106df91d3203", size = 138036, upload-time = "2026-03-09T13:15:36.894Z" }, - { url = "https://files.pythonhosted.org/packages/fa/06/7399a607f434119c6e1fdc8ec89a8d51ccccadf3341dee4ead6bd14caaf5/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c31c13da98624f957b0fb1b5bae5383b2333c2c3f6793d9825dd5ce79b525cb7", size = 194295, upload-time = "2026-03-09T13:15:38.22Z" }, - { url = "https://files.pythonhosted.org/packages/b5/91/53255615acd2a1eaca307ede3c90eb550bae9c94581f8c00081b6b1c8f44/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:1f1489f769582498610e015a8ef2d36f28f505ab3096d0e16b4858a9ec214f57", size = 75987, upload-time = "2026-03-09T13:15:39.65Z" }, - { url = "https://files.pythonhosted.org/packages/17/6f/6fd4f690a40c2582fa34b97d2678f718acf3706b91d270c65ecb455d0a06/kiwisolver-1.5.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:295d9ffe712caa9f8a3081de8d32fc60191b4b51c76f02f951fd8407253528f4", size = 59606, upload-time = "2026-03-09T13:15:40.81Z" }, - { url = "https://files.pythonhosted.org/packages/82/a0/2355d5e3b338f13ce63f361abb181e3b6ea5fffdb73f739b3e80efa76159/kiwisolver-1.5.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:51e8c4084897de9f05898c2c2a39af6318044ae969d46ff7a34ed3f96274adca", size = 57537, upload-time = "2026-03-09T13:15:42.071Z" }, - { url = "https://files.pythonhosted.org/packages/c8/b9/1d50e610ecadebe205b71d6728fd224ce0e0ca6aba7b9cbe1da049203ac5/kiwisolver-1.5.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b83af57bdddef03c01a9138034c6ff03181a3028d9a1003b301eb1a55e161a3f", size = 79888, upload-time = "2026-03-09T13:15:43.317Z" }, - { url = "https://files.pythonhosted.org/packages/cd/ee/b85ffcd75afed0357d74f0e6fc02a4507da441165de1ca4760b9f496390d/kiwisolver-1.5.0-pp310-pypy310_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf4679a3d71012a7c2bf360e5cd878fbd5e4fcac0896b56393dec239d81529ed", size = 77584, upload-time = "2026-03-09T13:15:44.605Z" }, - { url = "https://files.pythonhosted.org/packages/6b/dd/644d0dde6010a8583b4cd66dd41c5f83f5325464d15c4f490b3340ab73b4/kiwisolver-1.5.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:41024ed50e44ab1a60d3fe0a9d15a4ccc9f5f2b1d814ff283c8d01134d5b81bc", size = 73390, upload-time = "2026-03-09T13:15:45.832Z" }, - { url = "https://files.pythonhosted.org/packages/e9/eb/5fcbbbf9a0e2c3a35effb88831a483345326bbc3a030a3b5b69aee647f84/kiwisolver-1.5.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ec4c85dc4b687c7f7f15f553ff26a98bfe8c58f5f7f0ac8905f0ba4c7be60232", size = 59532, upload-time = "2026-03-09T13:15:47.047Z" }, - { url = "https://files.pythonhosted.org/packages/c3/9b/e17104555bb4db148fd52327feea1e96be4b88e8e008b029002c281a21ab/kiwisolver-1.5.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:12e91c215a96e39f57989c8912ae761286ac5a9584d04030ceb3368a357f017a", size = 57420, upload-time = "2026-03-09T13:15:48.199Z" }, - { url = "https://files.pythonhosted.org/packages/48/44/2b5b95b7aa39fb2d8d9d956e0f3d5d45aef2ae1d942d4c3ffac2f9cfed1a/kiwisolver-1.5.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be4a51a55833dc29ab5d7503e7bcb3b3af3402d266018137127450005cdfe737", size = 79892, upload-time = "2026-03-09T13:15:49.694Z" }, - { url = "https://files.pythonhosted.org/packages/52/7d/7157f9bba6b455cfb4632ed411e199fc8b8977642c2b12082e1bd9e6d173/kiwisolver-1.5.0-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:daae526907e262de627d8f70058a0f64acc9e2641c164c99c8f594b34a799a16", size = 77603, upload-time = "2026-03-09T13:15:50.945Z" }, - { url = "https://files.pythonhosted.org/packages/0a/dd/8050c947d435c8d4bc94e3252f4d8bb8a76cfb424f043a8680be637a57f1/kiwisolver-1.5.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:59cd8683f575d96df5bb48f6add94afc055012c29e28124fcae2b63661b9efb1", size = 73558, upload-time = "2026-03-09T13:15:52.112Z" }, +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/07/bd78e6a8fae171ea041ef5bba3ed21a003522fa088834b069b1909981f30/kiwisolver-1.5.1.tar.gz", hash = "sha256:f1303ef2eec81262a4b708c3e858afe58d7c75ad91c1c05266eda7673369859a", size = 104395, upload-time = "2026-08-28T10:28:27.153Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/54/1b8d2bc580414cc75b3a5b3d195981a6facacd8a8d986e589d0a3b51a709/kiwisolver-1.5.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1acc7e5b7ef05e9da8bb70cd6c7c4513090213d2e1ad9720f599f0bf6c52aec5", size = 123362, upload-time = "2026-08-28T10:24:43.402Z" }, + { url = "https://files.pythonhosted.org/packages/57/19/92c30f540dcfff86ff625389427c39b53a9aaea16420ddcc09b2ab8d1073/kiwisolver-1.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bad20d4c69c851c982a1e3606f4c293edfd5a87885786c50082412240c4b1ffd", size = 66551, upload-time = "2026-08-28T10:24:44.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ae/30b94088722e7bce8b821cd5ee935a87f80023e7243125f1accf98e39bf7/kiwisolver-1.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0a4faea5c6db201c6a21391d2ac926ea97acf7dacdbc3c417189e1adb1a00837", size = 64076, upload-time = "2026-08-28T10:24:46.184Z" }, + { url = "https://files.pythonhosted.org/packages/f1/0e/9f394d693be549fa3fab62498c2778294595991047adc3dcf10aa99b91c0/kiwisolver-1.5.1-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e05c2f7925f1d88778e53cb44f14e0223204a3bdd09a41664750363acfb1f2ef", size = 1628782, upload-time = "2026-08-28T10:24:47.91Z" }, + { url = "https://files.pythonhosted.org/packages/64/96/26efc04348c0f332b6e6c471dff132bb8b18b2df494a1b65826601674e62/kiwisolver-1.5.1-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3a4e41e3096bf1f0f1b76e2ffd6d828d6547f574f702d59bdbef7acfa59db9c", size = 1228112, upload-time = "2026-08-28T10:24:49.503Z" }, + { url = "https://files.pythonhosted.org/packages/5a/ac/df9ddc19ec972cd97b60b262dd2c4be28c6192b5a779cad429c6657ee680/kiwisolver-1.5.1-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1d56ec54d257d05e0b50f5780d967540cd07beeaf9e5f645b26d50cce79f4d8", size = 1246756, upload-time = "2026-08-28T10:24:51.225Z" }, + { url = "https://files.pythonhosted.org/packages/cf/de/c38a246b8a4f4b293c7291aa650917b05c5549b0d532749bc523296e90c4/kiwisolver-1.5.1-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8de6f2a4ce7e7bd27d23dd94abf0ccafe0e0e5cc9c764b0577191f2c25f08f26", size = 1295555, upload-time = "2026-08-28T10:24:52.953Z" }, + { url = "https://files.pythonhosted.org/packages/b6/48/3c95a5232d8b5783050d8c8089b58bf1f57018f9135232d72c5aa6b0c01b/kiwisolver-1.5.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:16895f553ee6620a827d2da56b871f835fb70b9216cca5d188e885caf6e3bd23", size = 2179476, upload-time = "2026-08-28T10:24:54.568Z" }, + { url = "https://files.pythonhosted.org/packages/e6/e9/b9e999336afc561b154e93682f597832bdc7446f2ee18b467c5b0e924867/kiwisolver-1.5.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:6b92f60017dda7d877fdc546438b5e28f31c523264f49cf5a48c1d0ce1a0dfbc", size = 2275258, upload-time = "2026-08-28T10:24:56.186Z" }, + { url = "https://files.pythonhosted.org/packages/76/cd/c1c550796ada4a59644b8264b425fe332686d2a086c4729fcc640ececdfe/kiwisolver-1.5.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:7824b5e8bdbf0bccb4ccd37bbb115849a1dc45437fb4de8351385ed07c437ee0", size = 2443424, upload-time = "2026-08-28T10:24:58.093Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d4/30f641eb8e10f6ad5bb9af8a15ec8eea386162a6ef491e68995b8680ef4e/kiwisolver-1.5.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:aa7d00b1700966d2917e54d278aba86897890ca9276dd8b76cf6446b6c181b92", size = 2249457, upload-time = "2026-08-28T10:25:00.223Z" }, + { url = "https://files.pythonhosted.org/packages/1c/d9/211e016f8aa5b5bf496dd3bd7ebab4b73b22d91f025f25e20156a44a77fc/kiwisolver-1.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:96c30002424670b5e1e46495c2b8cbffef39cf77c1d79e76462029d50339785b", size = 70643, upload-time = "2026-08-28T10:25:01.888Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8e/ae7007266dea1fd93fcc48541042c1f6583958055de3b424a59ec16def25/kiwisolver-1.5.1-cp310-cp310-win_arm64.whl", hash = "sha256:f0f4a42db92d6ec7677ab9d12830a2a8ec145a9c6d15db2b593466bc875c78d7", size = 68215, upload-time = "2026-08-28T10:25:03.136Z" }, + { url = "https://files.pythonhosted.org/packages/94/7b/2de6908edc668427c149af5f93112e931f87e1fa4cab80bac32c5844dccc/kiwisolver-1.5.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b3d78f7bb2b9d9a30345be1474b9aaa8685430b54afb51ba3639b5c6c11e9ed6", size = 123364, upload-time = "2026-08-28T10:25:04.359Z" }, + { url = "https://files.pythonhosted.org/packages/8a/24/e70914415c77c97be7e22c80a0740869cb7428768cc380fdcdf6703e7084/kiwisolver-1.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5025e36fb4fb275cef0a4e30dbb11cb4ae61d1c83deb90189cb5d7e4cafd6b55", size = 66558, upload-time = "2026-08-28T10:25:05.506Z" }, + { url = "https://files.pythonhosted.org/packages/e8/2b/8b08b11833db4d475b8ef1f36174f8d8a7abd31bedd7e794be78e8814b48/kiwisolver-1.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc1a26b8e53395a01c2c611e58602fa47461f136fba7cd5542e6db6d64be1839", size = 64071, upload-time = "2026-08-28T10:25:06.7Z" }, + { url = "https://files.pythonhosted.org/packages/89/00/05c2d0369ac322d22d5c05f84b5c4a6856fa6207fbae42869108a28f0383/kiwisolver-1.5.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:95a02752aa032eef4aed01cda6d9b687c669bd0396bf4519eef8bba22a286720", size = 1438206, upload-time = "2026-08-28T10:25:08.254Z" }, + { url = "https://files.pythonhosted.org/packages/c0/05/c941a139f27438c1910d630fdc3ccfdab7c8407c72052299ead12ece086e/kiwisolver-1.5.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:719a35fa1156db3640555f95ebb94f60a444e64d1c69626b0edef5df78eba225", size = 1248975, upload-time = "2026-08-28T10:25:10.053Z" }, + { url = "https://files.pythonhosted.org/packages/58/a1/2669ee5512e39b9d4de25faacaedf788c957f93730c5f7c63993ec4f5933/kiwisolver-1.5.1-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:febcce10f2bcdbb80b4ea919238a6a4ac13dbc4c7cadbe8d5d75c3682f8b5404", size = 1266301, upload-time = "2026-08-28T10:25:11.754Z" }, + { url = "https://files.pythonhosted.org/packages/28/b8/353f52f2c7f861a9e90cd2e8f90f85b3ad03060835f823e08298d094c463/kiwisolver-1.5.1-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1d852545c4d0e35a72728d072cbaa59e2fa7dd84bdf01e068d670dd0ceb58eb6", size = 1319708, upload-time = "2026-08-28T10:25:13.559Z" }, + { url = "https://files.pythonhosted.org/packages/21/0e/14b83200eadc2c1d63b76bac01c1813bf072aecf567429f303e00b70258e/kiwisolver-1.5.1-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:2e10ae1bba1899188b33557c10d73affcc12033edd18adddb57d209039976a4c", size = 971720, upload-time = "2026-08-28T10:25:14.934Z" }, + { url = "https://files.pythonhosted.org/packages/86/91/9d43d84d23b1cbff72a142d387ead1ea03db0cba8ff86ed5335addad3cc9/kiwisolver-1.5.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b69602970994a2ed8bbfa78c2f0394a7435226c6040489702d9f0a0ad0c07052", size = 2200119, upload-time = "2026-08-28T10:25:16.636Z" }, + { url = "https://files.pythonhosted.org/packages/ff/7c/f2bd9616f27ffb5e17cecc0baa5d0bbcee7e55aeddc0ccc871d69e2fc3ee/kiwisolver-1.5.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d50de98e8d807dc31822fff96f50293163a62418eb65487a21b42713d72ed0b7", size = 2295005, upload-time = "2026-08-28T10:25:18.374Z" }, + { url = "https://files.pythonhosted.org/packages/ba/17/ee671b72bf8f46a08379d4392c65582541759a542428197562f2898294ad/kiwisolver-1.5.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3221f78211074f561c44ca42eac0619828171bec15a2c4cf6f7747d07df76e8e", size = 1960982, upload-time = "2026-08-28T10:25:19.893Z" }, + { url = "https://files.pythonhosted.org/packages/2e/f7/0e26b4c05bee3bdb0f048dfa305e4fe701999ea17b51e9c616ef91035bbe/kiwisolver-1.5.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0ba9527afc80ae3d7814ed98b6572d02bf85eaf48065678342c5f0c6dab7a8c7", size = 2464918, upload-time = "2026-08-28T10:25:21.65Z" }, + { url = "https://files.pythonhosted.org/packages/ae/62/6eb431133d30ce656ac1e5ff72fac70dd34d54c3984f4011b9ac8bf77d54/kiwisolver-1.5.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e12dfea7f5fc2a34a9080efbf79c4c44eb380ec5b9c6fea09407e08f0d1e941d", size = 2270967, upload-time = "2026-08-28T10:25:23.643Z" }, + { url = "https://files.pythonhosted.org/packages/bf/9f/6f9e489c188200e6fb3193935501894811e8c97577c8ffe9033589bf3521/kiwisolver-1.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:1a7587dc335f2c0f5bd577fd0540bd16c66006bdb60f759a1059f025e6c4f071", size = 70744, upload-time = "2026-08-28T10:25:25.061Z" }, + { url = "https://files.pythonhosted.org/packages/c6/6d/dfc430d1d43957061599adea3f08ea982bb6f4ab601a8c974bedcf2ba850/kiwisolver-1.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:e4e4523d6f336708d732516e6cfca7796cf3d96c9474eb5aecf6165f2f1fefc3", size = 68404, upload-time = "2026-08-28T10:25:26.186Z" }, + { url = "https://files.pythonhosted.org/packages/6b/9b/65b302742389c6f96f2956bef5decf26011309feb2fc5d79613af18adea4/kiwisolver-1.5.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:63fb7294b768f444eb4b068965f2662f28c2fd4161e23bd60fcf3ff27b74c046", size = 123876, upload-time = "2026-08-28T10:25:27.44Z" }, + { url = "https://files.pythonhosted.org/packages/71/74/c21f339956f6f691b2ed7e31d5f3ae767304df6c460192739fc830853051/kiwisolver-1.5.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0ebdef3eae5336568147c39a55be6a2036ffde53faa9ca2d978989ae7c2da12c", size = 66487, upload-time = "2026-08-28T10:25:28.728Z" }, + { url = "https://files.pythonhosted.org/packages/84/e5/bdb34e21523e01dceda064d63713f3bdec91388af24fba1eca7ea5e85864/kiwisolver-1.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1798e83840c3f627246104c4d8a9639c60fa068adf9ce92b61791781fa8a68c1", size = 64660, upload-time = "2026-08-28T10:25:30.071Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f4/dadfec469313c7f428efa7e84b4aba9732f813c13ea7131a24b7b008ef57/kiwisolver-1.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:34633ecf50d16187ab8e5528b7a2530f2feb4e23f300db4672538b51cfc5cd38", size = 1477929, upload-time = "2026-08-28T10:25:31.495Z" }, + { url = "https://files.pythonhosted.org/packages/6f/35/09c58daac34e6f6ea5c6dee0094b422118e5a7c265586008a95fd135ac5f/kiwisolver-1.5.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d27c2123977cb9269c30a49ba45f03a4323017ef693e19db4ec9dbe1299a3002", size = 1278499, upload-time = "2026-08-28T10:25:33.375Z" }, + { url = "https://files.pythonhosted.org/packages/19/32/739765e24fbad29d13f83e546ea4abc215a78cea9d677ca09025b027724d/kiwisolver-1.5.1-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6a797a1cefc8b9c93170db580337e1fe3d011ad18b1299943231279406342048", size = 1296677, upload-time = "2026-08-28T10:25:35.059Z" }, + { url = "https://files.pythonhosted.org/packages/df/32/03304d1010e2cc45e5b3b52cef7e43fed3a2a5cd6c87a89b4a88e1d85b5d/kiwisolver-1.5.1-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2551cf9917af48ee7c4b29cc82320489508cf96fd26a51f6fc124de661cd44c7", size = 1346037, upload-time = "2026-08-28T10:25:36.705Z" }, + { url = "https://files.pythonhosted.org/packages/3e/57/4c49377bfd274450dd72ecaa13eaac32ea804a03363e4d1db0c5aa999ceb/kiwisolver-1.5.1-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:38f6e0deb4d0a4615efe0c4efc5990b06ae450ab50a0b321c0b078b6d238c083", size = 988248, upload-time = "2026-08-28T10:25:38.299Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c3/38df144a08b6c5d75ca4504e5cc3141bb3bfef64c04f4ef48204f42711b6/kiwisolver-1.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bfd1de989b3330420e29de39352f5c049905c9e3ee67233a50d550e3d652c148", size = 2228722, upload-time = "2026-08-28T10:25:40.038Z" }, + { url = "https://files.pythonhosted.org/packages/e7/11/3221838a89cd64d9b386353e000cd8a296069a20fbe3584507fdfd5bebae/kiwisolver-1.5.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1209042a623ddfda5497e4066c7b77651dde8e1d3a9dd97599dc7e97f3b9b78c", size = 2325216, upload-time = "2026-08-28T10:25:41.699Z" }, + { url = "https://files.pythonhosted.org/packages/83/d4/075c219230697bb5db910d37262b9bacf880f92b4811a02ab81ed073a253/kiwisolver-1.5.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:26e8268480be5061d509e29669d59103c067a26377a56491630ece11762e3858", size = 1977689, upload-time = "2026-08-28T10:25:43.559Z" }, + { url = "https://files.pythonhosted.org/packages/bb/08/1d219c3c2dd960983d0d4da623d916e9de6385df2b0bab3d1af0e9b8fccc/kiwisolver-1.5.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:d79308fa689fac89cbcfbd4dbfc80b5f95c54c5a7fd4d194be221f9d33d026e6", size = 2491443, upload-time = "2026-08-28T10:25:45.242Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d3/024208ec1079d273f1047468d1bdffbf38bb75b7b268090fd3a0301b9d9a/kiwisolver-1.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b03af77d77e50edba2030fd5f7c352ff209314b09030a3cba7c14edf9a09a444", size = 2295200, upload-time = "2026-08-28T10:25:46.984Z" }, + { url = "https://files.pythonhosted.org/packages/6e/7c/7b210498f9f92e1cd7855f260fa69ef056881087b199ee20c208f0e4189a/kiwisolver-1.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:06a6917674de9e0fe3f66f5430787f59a9f2ddb64af9b714eaec547e29ef5c19", size = 70748, upload-time = "2026-08-28T10:25:48.444Z" }, + { url = "https://files.pythonhosted.org/packages/94/61/ef0daa157c8bb23672f7423e0d14c39db1dc6ef8ed47e6bc54c9c1bef3bf/kiwisolver-1.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:ad8b9671348d7c8716715652ae11f85ed0eb99e265a2df2ca490577d69860b2c", size = 68324, upload-time = "2026-08-28T10:25:49.81Z" }, + { url = "https://files.pythonhosted.org/packages/08/c1/88018321d976f53c421e379c43bc6993e70ce0c8a3ec5edc4bfe102257f6/kiwisolver-1.5.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b6ae6a0328f0bc035741820fdeecdcd67bf4694eee03972e843663107122f450", size = 62272, upload-time = "2026-08-28T10:25:51.02Z" }, + { url = "https://files.pythonhosted.org/packages/85/d2/712bc17ea4f1d216034928069d612defbc6c95a471c55a7203a39faecb1a/kiwisolver-1.5.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:886fc26012f0e8b5f69d1cfe6d711f6b11f194621539bf8e6bb1c25c5dc82724", size = 64481, upload-time = "2026-08-28T10:25:52.22Z" }, + { url = "https://files.pythonhosted.org/packages/1b/e6/6c5380d676f43b6d918033962ea5e72360ca69e5a404154bc496b598ffdb/kiwisolver-1.5.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:aefe930d113798330e9462f7874542977869c0613cba3262e2de3a8d5dee8f3a", size = 66260, upload-time = "2026-08-28T10:25:53.387Z" }, + { url = "https://files.pythonhosted.org/packages/92/6a/7087f5822cc8bb272679641404b1966a42504dac9ee74e2b33840475a0aa/kiwisolver-1.5.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5ca5aebae78a0bc13c1943af4af615d4966c5b650b05d5aa83b50e427196fee", size = 123876, upload-time = "2026-08-28T10:25:54.644Z" }, + { url = "https://files.pythonhosted.org/packages/5c/4b/9f385087ca09ee5ab9c09c6832561a7d2f7c78d3e5661d511e669f70e439/kiwisolver-1.5.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1ed0f5e49d0ceff8b72190824d9e59c062fbbc02c231b853112c78474b3f5ec2", size = 66487, upload-time = "2026-08-28T10:25:55.899Z" }, + { url = "https://files.pythonhosted.org/packages/47/8b/40d33ffd2f378094ed462e9a9a0907e59d4de9845e65a59561272da350d4/kiwisolver-1.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:77a4c8187a5948d7f8795adb765a3c7b553d07d86d88e43038fc32fc1fb9a3f3", size = 64673, upload-time = "2026-08-28T10:25:57.048Z" }, + { url = "https://files.pythonhosted.org/packages/ab/43/86aacc027959108b4c66eeae8b73cedb057dfa6eb3a335d05ad65197081c/kiwisolver-1.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:74ad5c3dad54a4641b4c28cd15ded70899d04459c6c7aeacafea716be97cce6d", size = 1477992, upload-time = "2026-08-28T10:25:58.479Z" }, + { url = "https://files.pythonhosted.org/packages/ed/40/b1d0369048c79733a32c8abb0f2718532e6630641368e33a81384246e844/kiwisolver-1.5.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21e46b23a2da695c364124817bc01d970effd5483147f8d66a6a7167e3f6b851", size = 1278821, upload-time = "2026-08-28T10:26:00.121Z" }, + { url = "https://files.pythonhosted.org/packages/61/a1/fa71c1792272ff9461432715ae60ffb7e11a4d7ac3bf68961b9cab6c60cf/kiwisolver-1.5.1-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75d9b1cf8258462dbdc1eeda718c96ea7f079324c09067f6daabfcf37712b7fe", size = 1296805, upload-time = "2026-08-28T10:26:01.868Z" }, + { url = "https://files.pythonhosted.org/packages/a3/49/3f0bd94af8e06ecc47eb834b195a06f05d48711ceb2352c56d6835160f0e/kiwisolver-1.5.1-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fca690b00c4c48f6c2a547b0160ed511357093a4e4c9b47e0fadf3128066d89", size = 1346109, upload-time = "2026-08-28T10:26:03.59Z" }, + { url = "https://files.pythonhosted.org/packages/a4/44/3afe6ef9cf06d61220953a8963e94eca978491be1d9547cb01d82a1efa08/kiwisolver-1.5.1-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:876bbfd276473d3daffe30e8c975df4ed9429967b41a6cb362dbb5155b6f13ad", size = 988252, upload-time = "2026-08-28T10:26:05.306Z" }, + { url = "https://files.pythonhosted.org/packages/e4/30/a12bd7a7285a211e1747c3eec77b8c614dbfcc1dad942f7611a1a6921ae5/kiwisolver-1.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f942903fde7363d1d879057ec5de01310efda2597161784d752fa9953a01a71a", size = 2228846, upload-time = "2026-08-28T10:26:07.312Z" }, + { url = "https://files.pythonhosted.org/packages/a1/6b/233e2958abf0dab7b18d07e52f286e02e519d7651bfbbe97af9347564109/kiwisolver-1.5.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c90d3022d8a94778939cda8638c6c8da8fa757b8958dad7ec868ce29c87681b8", size = 2325583, upload-time = "2026-08-28T10:26:09.093Z" }, + { url = "https://files.pythonhosted.org/packages/42/8e/7673060a27b01405b580058510adef34687069d229800239f5e44682d4d0/kiwisolver-1.5.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8a34616dc2521cc8dc1d7d081734da63539f021ac0450ce950908340c6e7aa2f", size = 1978221, upload-time = "2026-08-28T10:26:11.127Z" }, + { url = "https://files.pythonhosted.org/packages/3f/7b/1b882fc1a8b4a0bb8084e7d1d85004116d08c92c0705d31f2928dec607f2/kiwisolver-1.5.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:8bf4df63592c2a66b4f8edc5df2544998c288aa02f96ce0acd880cd1de8c8127", size = 2491819, upload-time = "2026-08-28T10:26:13.348Z" }, + { url = "https://files.pythonhosted.org/packages/39/9c/426deb49e62c5f69464b64bbeca064d3b758a7506b8913d986ef34f4619c/kiwisolver-1.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d09037ca068d784ebc4aec290ef952ca27ac15dd9c0b5801a88c6e1096b83e6b", size = 2295520, upload-time = "2026-08-28T10:26:15.042Z" }, + { url = "https://files.pythonhosted.org/packages/f5/22/deabbb3ad6d918d74b7831b2d8ae7151b09d21c87974e5ee8a456f58c94c/kiwisolver-1.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:dc23390afe9f4ef9ac3bcc72a03a56eebbde03f4c571a32cb38f859cff9a6524", size = 70758, upload-time = "2026-08-28T10:26:16.504Z" }, + { url = "https://files.pythonhosted.org/packages/b9/71/02fa5c2fd92068bb8952847e70ee6c5cb280e7febe11653d17812acc53dd/kiwisolver-1.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:186884a58486651e3c217b6acea0a53eaa9498fdd472057c46f2f0fb5c25aad5", size = 68329, upload-time = "2026-08-28T10:26:17.658Z" }, + { url = "https://files.pythonhosted.org/packages/dd/87/2d5dfad0daf17dcc18d98c48ed2332fc3f051cf599e60be6182a30dd4cf1/kiwisolver-1.5.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0324cd2567259b7a095f6cf18a52b0ffc6f3de9e69528ff1bc0e7a37bd43ff1a", size = 62337, upload-time = "2026-08-28T10:26:18.778Z" }, + { url = "https://files.pythonhosted.org/packages/08/c8/83e1624f15d6262b470dbcc80b09979fd4d5b2ea3ddfc6b6e3327e235726/kiwisolver-1.5.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:74ea337e0ec3f6f342a36a4f1b5cd94dd9affddcd28ba9aae2905af932ee8c6b", size = 64513, upload-time = "2026-08-28T10:26:19.909Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2d/827ec30eb07f528c08d8459ffb318ae91a56d793ee8acbea8b491f0ff906/kiwisolver-1.5.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ee9df1f0d77b9c6e94f4ac0fec533fbddd5ea3a327807f18d7b069ae019ded80", size = 66287, upload-time = "2026-08-28T10:26:21.078Z" }, + { url = "https://files.pythonhosted.org/packages/53/11/5c43a562529dad8def4b81e5e1877c612a7e0298105a5939b3b409d2079c/kiwisolver-1.5.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fc271a6f0a2126958f4090e5507b9da5848927dae331f8f763bd4aa642b3d2cd", size = 123940, upload-time = "2026-08-28T10:26:22.475Z" }, + { url = "https://files.pythonhosted.org/packages/64/db/9bd6c505c95128c258a55236bfbb3a7a3fb6023f863316b6d7d9f3c69052/kiwisolver-1.5.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9b3092d8992a1d69b7a59c3e39f35e1b9be327a17f68a7c35fc17329e337d6f2", size = 66493, upload-time = "2026-08-28T10:26:23.743Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f4/b3007a3ed5c9be73f81161140684cf7d9bdb9c4b632f5f484d2a1c713fb9/kiwisolver-1.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c2306e8bb53601979fcb3fa09cc65e031876d9ae01eff2fcbcd7a84ef94d5bc1", size = 64720, upload-time = "2026-08-28T10:26:24.95Z" }, + { url = "https://files.pythonhosted.org/packages/a1/13/08188f0cafa3a800403e4ff62b9aad4e7a17f9c4c7e080dc8f18c64794cf/kiwisolver-1.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:18a0cfb124546a4c2e6087c5f3029c7f44b37c85b142e0ced71f73a7599ac208", size = 1475867, upload-time = "2026-08-28T10:26:26.393Z" }, + { url = "https://files.pythonhosted.org/packages/8a/3e/053bdc3c9abdb8f2606225eda398adca25c0c91ab90add8222a69db65ee0/kiwisolver-1.5.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34ec467940442c9943016fb2d4c81d1ba84351eeca2f1a78f8bc87f1ba0d414c", size = 1282865, upload-time = "2026-08-28T10:26:28.118Z" }, + { url = "https://files.pythonhosted.org/packages/5e/64/a44c341b36b610588cc2f1e89b3cae072a3119aa8be578e90987cd640751/kiwisolver-1.5.1-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a83ee7107df13abe42a54a6654670eef9bb39425cf2e27f65e0007465e1286ab", size = 1300865, upload-time = "2026-08-28T10:26:30.125Z" }, + { url = "https://files.pythonhosted.org/packages/60/5e/7e7d716dca38c714478b741257a5b4a321d9932b8d851551a136dcaf3984/kiwisolver-1.5.1-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bebb89489b279b2f5661bbbb2abcc87bcd4a46607bb4a5c966f04f1db6b8df9a", size = 1348071, upload-time = "2026-08-28T10:26:31.829Z" }, + { url = "https://files.pythonhosted.org/packages/10/1a/2b98fdda8bf45b7be317e48ed12393d44334394d315c74b81f4a14c0e31b/kiwisolver-1.5.1-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:509735237ae0d849e8a843551d423d2500d2e0a9ac1611a145658b29c0fb9f85", size = 992191, upload-time = "2026-08-28T10:26:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/b6/55/d893f5ede0e50f9e3fcf01f6015f42ec7d9cd221e26772701fe4a98745f9/kiwisolver-1.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:824c3d763a05ea9e9003610145186b0e9848c7584a5575c79bac5a8e7cd80bad", size = 2233854, upload-time = "2026-08-28T10:26:35.282Z" }, + { url = "https://files.pythonhosted.org/packages/b1/82/f85f6279555a6ee1639fef7bfe83adb037a03e11a6fc9eaa54b8d0380339/kiwisolver-1.5.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:1fff05e239575b1481b6ed1a782f6fad616efbf1f0b1f44e6e85c4dfe426e483", size = 2330621, upload-time = "2026-08-28T10:26:36.9Z" }, + { url = "https://files.pythonhosted.org/packages/56/31/e11aea078f66fc2fffcc179d38ca90d9da97652a241b64519169742ba46a/kiwisolver-1.5.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0627b9bceb9c3cdcf12b8a18655eedfed2692b038df27423383c120d0b7dc2d6", size = 1982848, upload-time = "2026-08-28T10:26:39.01Z" }, + { url = "https://files.pythonhosted.org/packages/af/ea/2956b63bf5140ca46aa2c2818e6aa03e2d5754dd2fa41db1c6b28922940c/kiwisolver-1.5.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:8a708a47ade1fe19e8371d5da076bac0dd4b0a5a7985ad6c637f7f7e361b6baa", size = 2494850, upload-time = "2026-08-28T10:26:40.837Z" }, + { url = "https://files.pythonhosted.org/packages/11/d1/3829542258d8b3fc0898d221e7ef0e2c83eca0d348709bb8dbe54f3d4005/kiwisolver-1.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:007a5553dfc4f4e8d184f588a0200e2cd4b63a59cc8796df3c39909e679dc7a0", size = 2298067, upload-time = "2026-08-28T10:26:42.803Z" }, + { url = "https://files.pythonhosted.org/packages/4e/0e/49522e1ab5788cbaf63a26fbd3b851f9028616828c961b8a31b35cb96df8/kiwisolver-1.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:f4167e87b397f273dc2356fcf1eaf50a6bac51e6105f45103ef7129c8efb0255", size = 72282, upload-time = "2026-08-28T10:26:44.268Z" }, + { url = "https://files.pythonhosted.org/packages/f5/b6/22e7ca5315d363e6f81c9f37c9472e12e7b298731e77c0428e6a911a2c39/kiwisolver-1.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:5c490db2168a508088f59140dd392556a54b8bd1048fc6383c8baff13c359673", size = 69855, upload-time = "2026-08-28T10:26:45.725Z" }, + { url = "https://files.pythonhosted.org/packages/30/8c/03a9cfbe871964c8758a816eb03ac96c806da2795a9a7cd9bf9648bfb594/kiwisolver-1.5.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4d4ca09bf13cff792b1884f64b98ee6c2467930d632233be25c56b442d99f10e", size = 126289, upload-time = "2026-08-28T10:26:47.023Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e3/14ce3041ca79dff9c9d884ca00c7bf32374e76028a865a9ecd99b4f5a517/kiwisolver-1.5.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:44b8faef94f1857e77fa0238f3390ff1ac51d2ea20a487e2e452a59fd2b5f5ca", size = 67709, upload-time = "2026-08-28T10:26:48.268Z" }, + { url = "https://files.pythonhosted.org/packages/af/c4/45030471a66ec8ef042e9f96ffe1d522c9ab12da180186a0898966fc1385/kiwisolver-1.5.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2ae70bc59790d2af72a3f76f24b272403e135070340281108b447cb77ea70819", size = 65909, upload-time = "2026-08-28T10:26:49.523Z" }, + { url = "https://files.pythonhosted.org/packages/42/73/17dce073a6ae259bb32cf9d686c4079d2e538868bc45967462bf33df914a/kiwisolver-1.5.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:43844c1a7ad6d723d5b5b4c4fc7f5bd399c40e288120d16257c7c9e8765c6e85", size = 1584907, upload-time = "2026-08-28T10:26:50.933Z" }, + { url = "https://files.pythonhosted.org/packages/8c/84/ae3c75909f507283cbfcc7e916c7e822579ef962020b97e6882b27b4478f/kiwisolver-1.5.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22d5e5aaad6be121f2515765e3b1c444352cb8eb4c86510801db8f2e50757316", size = 1392474, upload-time = "2026-08-28T10:26:52.638Z" }, + { url = "https://files.pythonhosted.org/packages/34/31/8bcc83caad5bce8fa4577152389848bf6bc110e51e573a2b4e7c2aa34c89/kiwisolver-1.5.1-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3fa5855898f6d3d01b72ccd48a2d65cbdee301251603fefe34e2025bddba219c", size = 1405246, upload-time = "2026-08-28T10:26:54.248Z" }, + { url = "https://files.pythonhosted.org/packages/55/72/220345537d790cf4ae54f8acfff4b5cc2468e0702a384d651cf7a771c63e/kiwisolver-1.5.1-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d66a64dd5dec136040ec2ae94aa026a912ee60fdd45bc28d3db30037fd809e88", size = 1456099, upload-time = "2026-08-28T10:26:56.042Z" }, + { url = "https://files.pythonhosted.org/packages/cd/10/3725fd2398f66d18c34b4e0f81a8d03764cd4f4f089f58a527f0b4428086/kiwisolver-1.5.1-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:9e51c119992ea8820706871c30a4642ec76de20ae82f9b50b9a45517d8e9f810", size = 1073695, upload-time = "2026-08-28T10:26:57.658Z" }, + { url = "https://files.pythonhosted.org/packages/86/cb/28d6e09e66b93e4588b2e6b7d84d020ccefea09e2f4de788510a07efeab7/kiwisolver-1.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:70ed9a45c7484d2b30cdacf60d220f494a1763b9fec1ad03285c6553fa0889f2", size = 2335355, upload-time = "2026-08-28T10:26:59.202Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b3/a0f31d5e4e40af7dc97c36b8a74fdd3a36cf3c8bbd098da9a23466ff6a94/kiwisolver-1.5.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:98b208a7cc42c803445ef551d6753cc42a5ea13e9cab1ee66cd8b9cb70195330", size = 2426524, upload-time = "2026-08-28T10:27:01.181Z" }, + { url = "https://files.pythonhosted.org/packages/2f/c9/728f63bd58c72cafdc79fc306abeeac7391bec03b757a48dadeb30906521/kiwisolver-1.5.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c6834b92dd2428e2dd85ef3d85f723d3c12f20aaf43a2ddd4f944ca25d833408", size = 2063430, upload-time = "2026-08-28T10:27:03.06Z" }, + { url = "https://files.pythonhosted.org/packages/84/df/ce188b96f92f9a2c958231da140768918cba53c9713dc887b82f85462118/kiwisolver-1.5.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:5d142e352eb13facc7dd047489aebdff6ba78576c239f1ea04931979caaf0567", size = 2597513, upload-time = "2026-08-28T10:27:05.072Z" }, + { url = "https://files.pythonhosted.org/packages/69/d6/76947c8203768968382e5bd74d9cc95654746703a61ea53015f2c74a2e06/kiwisolver-1.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9b1c4900736e489a812c529100de4b8fb617d4db075e931e213c57424b83d9b", size = 2394488, upload-time = "2026-08-28T10:27:07.423Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d4/14b21e4eb203c4d15425e8b6a2c625a320b4a1f2f7557eead63ffc30ffb7/kiwisolver-1.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5978c3340f16a35c30f8ab2fa7bcf559973c55f1a5ef6970e1f621acf3c4db13", size = 75404, upload-time = "2026-08-28T10:27:08.892Z" }, + { url = "https://files.pythonhosted.org/packages/cb/f5/53157899fc7f45f76421b77b99eb1639dd0f83f26ff9d76300c96bb4a3b0/kiwisolver-1.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ca307d6c259e5c98d3cb9ade55342b47a6839762caf2536f3d7b46ee660cc82e", size = 72946, upload-time = "2026-08-28T10:27:10.944Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c4/1407df7512a5b36cc79840e01710dc575733c461b13ab866cae77eaf87f3/kiwisolver-1.5.1-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:482676e5bd48d70ac99d9fc78863469845421e01184fa83f1f9366dc49f7e974", size = 134002, upload-time = "2026-08-28T10:28:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/16/45/c37a21ad5c0ab581a93c55ad544721aaa1f0ae94edb29c6a678a23d013e6/kiwisolver-1.5.1-graalpy312-graalpy250_312_native-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:072bdb15a3c19a5b5dbc8f8fb1f4e1884bf4f3507eeb4cc6334401274d37a5c0", size = 194292, upload-time = "2026-08-28T10:28:11.06Z" }, + { url = "https://files.pythonhosted.org/packages/a6/c1/69f00d627949580e43d57af0aa465df46868d7c29801c137a55374101294/kiwisolver-1.5.1-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:a5a00665d1a0e26763a7338d7e911d4598fbc1d50dd0d6b7919b7dc6c5d6569f", size = 73362, upload-time = "2026-08-28T10:28:12.449Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f1/8bae9fac0f1837679ed59a5dd7e97e7eb943b738defa7cc0117ea0d107dc/kiwisolver-1.5.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:a5716a33bfabb2c6ce27b6cf03253467b3804f83e215f4d202685cf93c6c9874", size = 59543, upload-time = "2026-08-28T10:28:13.794Z" }, + { url = "https://files.pythonhosted.org/packages/c3/3e/42e3639d32ef3ac6ede80c39dc1d1df6224927409960d8f762b6cb504efe/kiwisolver-1.5.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:685929988b208a911f1285e2f8ed54210b0d681a3dc0f03e00d599d291986e7e", size = 57504, upload-time = "2026-08-28T10:28:15.074Z" }, + { url = "https://files.pythonhosted.org/packages/e1/12/f4beaaceb740b96c363a8fa6f34dbdf37a58d9e5f15416427d7bf89c552a/kiwisolver-1.5.1-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e4e49f7e1a4e7191bdf9dc67a974db714501b1fc52c24324103d06a86abd5c08", size = 79885, upload-time = "2026-08-28T10:28:16.345Z" }, + { url = "https://files.pythonhosted.org/packages/02/24/18d8a755acdae79c37ca2f1a925795aab67b9456c30c8569c377ea3abc77/kiwisolver-1.5.1-pp310-pypy310_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f0a887b6565bbfe80efde2b7f6e8890d7d9bbdb11bdb17028a3690c32fe0621f", size = 77582, upload-time = "2026-08-28T10:28:17.719Z" }, + { url = "https://files.pythonhosted.org/packages/df/a9/b86dde4f553ef74db0f9c32398614e24b2e8ec1d29a6ca04ae080d7dd29b/kiwisolver-1.5.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:1983f0974a750a6f6556f368ba11105d1d8369c735b944747c9f12ae5aea7aae", size = 98321, upload-time = "2026-08-28T10:28:19.087Z" }, + { url = "https://files.pythonhosted.org/packages/b5/1d/59ba570b1774e95e97fde3a0981b2e22118a7a495f73bf74cedc538566a0/kiwisolver-1.5.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:416ba7ff9f233b7036689bb5a3783537e838ad483f63558d2a800f75afe738b1", size = 59450, upload-time = "2026-08-28T10:28:20.383Z" }, + { url = "https://files.pythonhosted.org/packages/22/98/a6849f04dc18b5400e8b98affa2cd8fd86ed583085f036e57b32e571f4fa/kiwisolver-1.5.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:8af9b142ad719ae3a911ebf616bc4b78b32bbab84d6a40d3ad2f129670509957", size = 57400, upload-time = "2026-08-28T10:28:21.632Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ce/a7dc71353dd06a4cbe02222773f52d4a28c81e5a452a75797f8ed113dc99/kiwisolver-1.5.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5daa1f19e097050b9c4d9a78fcc9263cb96c9dfae08037ddc1b7c4ad1889f2a2", size = 79891, upload-time = "2026-08-28T10:28:22.936Z" }, + { url = "https://files.pythonhosted.org/packages/10/b1/d61c61a84ff85d1a36a99df2c152b59ffedb1d356c598902aba44abcdb60/kiwisolver-1.5.1-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cdaeeb6c350106df6bf9d873395973e5f066a9713200b72cd64f55d0a3eafab6", size = 77605, upload-time = "2026-08-28T10:28:24.322Z" }, + { url = "https://files.pythonhosted.org/packages/d3/52/5aef56f21a460a6e43ab3cdfc7697d59d7b87deb0ec97a0f7b91aa4a521b/kiwisolver-1.5.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:17851e5dad4484be0cbccbde3b15331deae036de9aebd45eed964487802b172f", size = 98465, upload-time = "2026-08-28T10:28:25.696Z" }, ] [[package]] @@ -891,7 +1044,7 @@ resolution-markers = [ dependencies = [ { name = "contourpy", version = "1.3.2", source = { registry = "https://pypi.org/simple" } }, { name = "cycler" }, - { name = "fonttools" }, + { name = "fonttools", version = "4.65.0", source = { registry = "https://pypi.org/simple" } }, { name = "kiwisolver" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } }, { name = "packaging" }, @@ -959,7 +1112,7 @@ wheels = [ [[package]] name = "matplotlib" -version = "3.11.1" +version = "3.11.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", @@ -973,64 +1126,58 @@ resolution-markers = [ "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "contourpy", version = "1.3.3", source = { registry = "https://pypi.org/simple" } }, + { name = "contourpy", version = "1.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "contourpy", version = "1.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "cycler" }, - { name = "fonttools" }, + { name = "fonttools", version = "4.66.0", source = { registry = "https://pypi.org/simple" } }, { name = "kiwisolver" }, { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy", version = "2.5.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "packaging" }, { name = "pillow" }, { name = "pyparsing" }, { name = "python-dateutil" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/49/64/f9a391af28f518b11ad45a8a712353c94a0aefce09d3703200e5c54b610a/matplotlib-3.11.1.tar.gz", hash = "sha256:69647db5746941c793d6e445a4cd349323ffb87d9cc958c2ad84a659b4832d30", size = 32612045, upload-time = "2026-07-18T03:39:46.63Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6e/d0/791aa183dd88491555cf7d4be0b52b0bcf6c3c2a2c22c815a2e819bf53e2/matplotlib-3.11.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:b7cf158e7add54a8d51ac9b5a84abd6d4e13ed4951b4f25f1c5139f41c2addb2", size = 9440302, upload-time = "2026-07-18T03:38:03.844Z" }, - { url = "https://files.pythonhosted.org/packages/35/74/82bbdf683a301f4478384c8aaba6903631a2ca18294b2d7655c9a542bffb/matplotlib-3.11.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d2ace7273b9a5061a3b420918a16fae1f2dc5dfee1abcc13aba71b5d94b1820c", size = 9268549, upload-time = "2026-07-18T03:38:06.144Z" }, - { url = "https://files.pythonhosted.org/packages/f0/f0/9b4298911303f74e6d83e64a81d996c0616405ec95046fac7f17e4258b9e/matplotlib-3.11.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee55e9041211bf84302ab55ec3965df18dd90ae19f8b58332a7feaf208bfe83", size = 10024922, upload-time = "2026-07-18T03:38:08.236Z" }, - { url = "https://files.pythonhosted.org/packages/84/6f/0bc3c3d05b021db44c14bc379a7c0df7d57302aa15380c16fd4e63fd6a9b/matplotlib-3.11.1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f4bdeea33a8d15a071dbfe6d119451b1d719c733ac666d65357082901a9099", size = 10832170, upload-time = "2026-07-18T03:38:10.276Z" }, - { url = "https://files.pythonhosted.org/packages/db/4d/e375f39acdb2af5a9342730618608e39790ec842e6f1b392863028781459/matplotlib-3.11.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b4c78ceb2f11bcac7389d305cda17aeb1f4586a857854ab5780bd3dd8dbfc407", size = 10916701, upload-time = "2026-07-18T03:38:12.512Z" }, - { url = "https://files.pythonhosted.org/packages/bc/be/fa26ed085b41298f64a8f9b7592c671bbf1acc8b0df124c1c5de96b859f8/matplotlib-3.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:7f33a781e12b1e53b278deb2f5373c2e55ec4f10727be3440c0cfb5cda9f944f", size = 9315331, upload-time = "2026-07-18T03:38:14.949Z" }, - { url = "https://files.pythonhosted.org/packages/b6/f3/eb5bdf3b6e191b200db298b08bbc1638b7f3c82cdc8680f9d88bf72559ae/matplotlib-3.11.1-cp311-cp311-win_arm64.whl", hash = "sha256:67e4c3cd578c65ebd81bdc09a1b6592ceafee6dfafe116dc85dfcb647b5bbb18", size = 9003475, upload-time = "2026-07-18T03:38:17.205Z" }, - { url = "https://files.pythonhosted.org/packages/f2/6c/7ef7ebcb2bd9739b2b66b18b076e077f44bb46fdbe28ca0506edb3c62c79/matplotlib-3.11.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e15ef41507f3d525f46154ac9e3ae785dacde9f20e593a25de8986267892ef74", size = 9453849, upload-time = "2026-07-18T03:38:19.593Z" }, - { url = "https://files.pythonhosted.org/packages/eb/f8/6d0c312c8d9738e7d9677f09fe5c986b3239e651a7b73a2deb38b65e4a71/matplotlib-3.11.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:21a67b961a6d597bca54fae826cd20695ba4a6e4d05424a08da6e13e3176fd6b", size = 9283113, upload-time = "2026-07-18T03:38:21.95Z" }, - { url = "https://files.pythonhosted.org/packages/c9/cf/b4ad2cc81b6672ea29ea04e64e350a9f9b493b0908ccd884c67eeff8f7b2/matplotlib-3.11.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ba8f811b8ddfac493734d6af0b2dff96919d0c28ca0d641858dab4262777c6ea", size = 10035615, upload-time = "2026-07-18T03:38:24.315Z" }, - { url = "https://files.pythonhosted.org/packages/88/90/4e10e033d9b66589d8ed98b84c95cdbb57033d57c1f41339d7393dbd2f2e/matplotlib-3.11.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c52f7ad20ef476806ed212380b1d54d20310c8b86bdc2c9a68b51f0024a44472", size = 10842559, upload-time = "2026-07-18T03:38:26.285Z" }, - { url = "https://files.pythonhosted.org/packages/88/eb/799612d0f8cd3e816a10fec59329fca52cd2353264df80378dfc541ae855/matplotlib-3.11.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8b14eb22961fe865efb0e4ff167e333e428908b00115a8d800ccb65ee108e481", size = 10927532, upload-time = "2026-07-18T03:38:28.532Z" }, - { url = "https://files.pythonhosted.org/packages/88/89/56649bbaa2fd12e20f3be03dbcc135b0c8676d88bac17977599e3eb442a0/matplotlib-3.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:88a2a27dd9691ae448dfae4b26f59036be90c3c28757edd3553a29559d00859f", size = 9333886, upload-time = "2026-07-18T03:38:30.477Z" }, - { url = "https://files.pythonhosted.org/packages/c1/11/4d124efbbad677b7b7552f6f85a3bd432d4232f95400cea98fcd2ae36ef3/matplotlib-3.11.1-cp312-cp312-win_arm64.whl", hash = "sha256:480194afceca4df2f137c2721227d3cba67121fbf4397b69cee7f83714b0a58a", size = 9007545, upload-time = "2026-07-18T03:38:32.833Z" }, - { url = "https://files.pythonhosted.org/packages/04/6c/4798363b7fb5644e309fe1fac30216e9146c9f70859d80d588c18caf5317/matplotlib-3.11.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6771b0cd7838c6a857a7209814158c0ad09bfef878db3033dd82d70ad101f191", size = 9454341, upload-time = "2026-07-18T03:38:35.001Z" }, - { url = "https://files.pythonhosted.org/packages/59/98/6acadbe7f98df19d274bc107ac58bb439fa75df82c33dc110d71a4a8501f/matplotlib-3.11.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2abdee5ffa2fe11b2d19f7a5c63b785fb7c28cc46c7bc1814156341d9d1a33e1", size = 9283627, upload-time = "2026-07-18T03:38:37.061Z" }, - { url = "https://files.pythonhosted.org/packages/24/ea/65cec46fe241390ccea1b1754207ee28eb71c5ab866bd5f22fe47e538fa4/matplotlib-3.11.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0a19dcf73406d3746d25a5ed42d713604c9a3e024d129b102852b0d941cb9f3", size = 10035860, upload-time = "2026-07-18T03:38:39.663Z" }, - { url = "https://files.pythonhosted.org/packages/c7/10/63fdccccbabe002fb0960876baabc5e3f24d9c1bb4cfb25651457f74b3a0/matplotlib-3.11.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7389b77ed2ab0552f46d9a90b81b7b8e6dfcdc42adc36c37a0865799843e0e3e", size = 10843594, upload-time = "2026-07-18T03:38:42.144Z" }, - { url = "https://files.pythonhosted.org/packages/98/51/a1155945bff7b91381875022ac1522c5dfdac0d006be8e7df389b3134eae/matplotlib-3.11.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c90be0b73568da4f662afac580956a76e308437e641b4a45aa08925eeb67d95f", size = 10927962, upload-time = "2026-07-18T03:38:44.302Z" }, - { url = "https://files.pythonhosted.org/packages/0d/3a/3d5e1f42dc761bf53401a62a83ff93389b37de9d2c093b2a3aa49ac34f1b/matplotlib-3.11.1-cp313-cp313-win_amd64.whl", hash = "sha256:68408341f2312836fbbdf6b3c78047f65b2d8752f5fd221c3e72d348f5b34f8b", size = 9334074, upload-time = "2026-07-18T03:38:46.616Z" }, - { url = "https://files.pythonhosted.org/packages/e2/db/3f5ea5a5b64060ef5e1ff60a19170423e41ce21b8497a6fe15a36e0b43e3/matplotlib-3.11.1-cp313-cp313-win_arm64.whl", hash = "sha256:0c1f44890d435c1b4ef52f701ad5828cb450ea97bcc83918fda6be74965d6cd2", size = 9007662, upload-time = "2026-07-18T03:38:49.112Z" }, - { url = "https://files.pythonhosted.org/packages/98/6e/c7ae5e0531425b69c0826b00ebbc264c85cab853f1cd6e096c9983c2cdc1/matplotlib-3.11.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:5e510088c27a89d53580a752f959146893563e63c330e161d159b0fee652af6f", size = 9503790, upload-time = "2026-07-18T03:38:51.527Z" }, - { url = "https://files.pythonhosted.org/packages/92/79/15be162e0a2ed546939674e2e97d0e33ec2447d86d4d4e611fa295bb178c/matplotlib-3.11.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:1524e2bdd48a93557aa47ddcfe9c225dfdd57d5a01a5c49128c20f0632980ee1", size = 9336148, upload-time = "2026-07-18T03:38:53.564Z" }, - { url = "https://files.pythonhosted.org/packages/6a/7f/36ffe144fc4aacfe0e3ed2318f72b6755d1e73b041d619b4d393e60f5a66/matplotlib-3.11.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:11664c551345553db92e61cae6cf1376f138f8c47cafdf13b64b18f3e3e9e464", size = 10049244, upload-time = "2026-07-18T03:38:55.911Z" }, - { url = "https://files.pythonhosted.org/packages/ab/5f/55812d68c0a840d3a463638f48c00ab1fe338518ec49a640cb6473b444af/matplotlib-3.11.1-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e1f8922ba31959cf6a9dfb51be64b7f7bc582801a3957dc0c2f3afcd3537adf", size = 10860798, upload-time = "2026-07-18T03:38:58.282Z" }, - { url = "https://files.pythonhosted.org/packages/7a/64/cca444b4eb5e6c768c44fc5e1f0b5211f20ca2b282778051996e996a2bdf/matplotlib-3.11.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83235693abde86e5e0129998f80ee39fc7f58e6d56a88fafb28a9278833e9d5f", size = 10943282, upload-time = "2026-07-18T03:39:00.465Z" }, - { url = "https://files.pythonhosted.org/packages/e5/0f/a49c329d394f2e9ef38506982107e8b04ecf94dd41a9d8423ff82cc737c7/matplotlib-3.11.1-cp313-cp313t-win_amd64.whl", hash = "sha256:9a076f4fc5cdc43fdf510f5981418d25c2db4973418d9f22d8bb3dc8045ada78", size = 9383532, upload-time = "2026-07-18T03:39:02.468Z" }, - { url = "https://files.pythonhosted.org/packages/e4/50/103e86afb806d8f64d04ede14e4cfc09dbfc25f512421ff85fdd6ebd59cf/matplotlib-3.11.1-cp313-cp313t-win_arm64.whl", hash = "sha256:216fbb93a74add02ddb4cb38ef5348f59ac00b3e84567eaf16598772d40e150a", size = 9059665, upload-time = "2026-07-18T03:39:04.607Z" }, - { url = "https://files.pythonhosted.org/packages/35/04/3079499fa8cb661ea66d13d6439d5a3ae6710a7afd5c7f72e08914f275f8/matplotlib-3.11.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:30c492d4ba9448595b6fd8708c6725963f8148e25c0d8842948da5b05f0ee8d3", size = 9456022, upload-time = "2026-07-18T03:39:07.041Z" }, - { url = "https://files.pythonhosted.org/packages/53/a2/69acfe84ec1f32930e801a5782a07fc5c79c8c6599a507b806d859d5da8e/matplotlib-3.11.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ac104be2768ffdd8655db9e71b768cbb45f2b9aa7b450cf1595e8f65d3822319", size = 9285475, upload-time = "2026-07-18T03:39:09.562Z" }, - { url = "https://files.pythonhosted.org/packages/d3/b3/31b15a2ca56d4ddd6aaa1c884c2f51cf9a61cfaf5ca6f6fbd6343d38e6df/matplotlib-3.11.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6be943cb68bc6660ead58c55b3aa6366cba2ef7feb06460fbcce32360376f19f", size = 10847102, upload-time = "2026-07-18T03:39:11.532Z" }, - { url = "https://files.pythonhosted.org/packages/64/0d/a17e966e620545c1548125af0b29ac812dd17b197a18a7462ac12fa859ee/matplotlib-3.11.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5af0dcda57d471440a7b5b623e70e0a61003518443d9098f211a96ecfbbc25be", size = 11131087, upload-time = "2026-07-18T03:39:13.764Z" }, - { url = "https://files.pythonhosted.org/packages/97/c5/5e100efdd67abb7de20befaa333612ef9bfc63417fb71398f904f25d083c/matplotlib-3.11.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3d3fd84082b1afbd9398466c81309e20045be20d48fe0fb18c43504d164cbbb2", size = 10929036, upload-time = "2026-07-18T03:39:16.888Z" }, - { url = "https://files.pythonhosted.org/packages/ce/04/d719a0a36930ecc8dfc801ff340f9dcfc4223f8ca5d39d06b4020032fff8/matplotlib-3.11.1-cp314-cp314-win_amd64.whl", hash = "sha256:9601a1e90be21e4884c53b4f3dc3ee0544654946f9975258d691f1c2e2f119c6", size = 9489571, upload-time = "2026-07-18T03:39:19.449Z" }, - { url = "https://files.pythonhosted.org/packages/48/65/facabdc2f1f6caba7e856db64dfedddca25f7608df07d96a1c8fd114fd3b/matplotlib-3.11.1-cp314-cp314-win_arm64.whl", hash = "sha256:ae30c6109848ac0f9fa36c5d6270938487614c47ba31860bd5361266dabc5685", size = 9164486, upload-time = "2026-07-18T03:39:21.424Z" }, - { url = "https://files.pythonhosted.org/packages/88/dd/18da6cd01cf96354534f98c468a25380c68ce582a2c9dd0cae12b04af4f2/matplotlib-3.11.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:dadfe80797174e2984aae3be0b77594a3c72d2c0a40fbd4a0de48d2728caf3ae", size = 9504876, upload-time = "2026-07-18T03:39:23.633Z" }, - { url = "https://files.pythonhosted.org/packages/79/b0/f0b63555a18b79d038c81fd6126f35fc4dfce0eaff48d96103348c7cf935/matplotlib-3.11.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:89b193b255f4f6f7948dbcee3691f4f341ab05d9a8874a67b45ddb4182922eda", size = 9336120, upload-time = "2026-07-18T03:39:25.797Z" }, - { url = "https://files.pythonhosted.org/packages/c6/dd/f210ec7c4a6f198d5567237048a93d0811fb5a1f1691f13320e592f95b41/matplotlib-3.11.1-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:191163532cdefcb1571ca38a6d7e6474baccde64495783e6ba47aa07ec4b9bbb", size = 10858033, upload-time = "2026-07-18T03:39:27.999Z" }, - { url = "https://files.pythonhosted.org/packages/ec/d2/d6d5324507c5fbb316db48e258c09c2807f3de03d9af47017e120070926f/matplotlib-3.11.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9fdf1c818ab05d0e74002091ddaf414478a3a449ec9d51c8976d45be7e3a01e2", size = 11141827, upload-time = "2026-07-18T03:39:30.092Z" }, - { url = "https://files.pythonhosted.org/packages/0f/68/3c22e9320bdce2c4d2f1320643ef706db7a24cb7420eea28b97a2d67f5a8/matplotlib-3.11.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b937b9dba5f5f6c1e31c47abe2186c865c0914fd18f2ce0dfc39c9adcef5951d", size = 10943061, upload-time = "2026-07-18T03:39:32.356Z" }, - { url = "https://files.pythonhosted.org/packages/f6/4a/907ed190ee81a9df581e0ed5456134fc0f7cb55ffcfda2f9e54ca900761c/matplotlib-3.11.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f2912f647f3fbe1ccf085f91e213936f9101bead81a5e670565b1f1b3712f4fb", size = 9540074, upload-time = "2026-07-18T03:39:34.789Z" }, - { url = "https://files.pythonhosted.org/packages/23/d4/97c19b77e0a6e3b48581185bb65088f431cd20186076cc0f650a1757ea46/matplotlib-3.11.1-cp314-cp314t-win_arm64.whl", hash = "sha256:54d47b8ae8b579633a3902ca5b4ad6c1e132a5626d64447b2e22a66394e79987", size = 9213472, upload-time = "2026-07-18T03:39:37.141Z" }, - { url = "https://files.pythonhosted.org/packages/ee/38/ceb1d637c4db6d06141f3739e93af3321e7caaabe69b57ae48ffe3ee95b1/matplotlib-3.11.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:427258425f9a3fc4ed79a91f9e9b9aaf5a82cb6571e85dc14063cc6fbb993741", size = 9438045, upload-time = "2026-07-18T03:39:39.491Z" }, - { url = "https://files.pythonhosted.org/packages/89/25/72ad8b58602d3a6ef1dfc4b65ecd01634ab65a2bdf494c9fe0e966dbf081/matplotlib-3.11.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:1ac697e591c11b6ad04679a73c2d2f9980fe9d9f0311fb414a2e329706343dfb", size = 9266127, upload-time = "2026-07-18T03:39:41.597Z" }, - { url = "https://files.pythonhosted.org/packages/8a/6d/69552382fcc8e93d1f2763ef2665980a900a48b7f3a4c57ed290726d1cbc/matplotlib-3.11.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e4b9ac2f1f607ecda2af90a5232beee2af7582fce1cc30c4b6a1b012dc21ee99", size = 10019439, upload-time = "2026-07-18T03:39:43.78Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/e7/c8/9aa712a0afb882649424dd8de8ad9aa6235e796e84c6052e8f6dc1598d0d/matplotlib-3.11.2.tar.gz", hash = "sha256:cec596316640f2b394b8f0daa0ea61a8eae82d017b620b9f202befb972a59ea4", size = 32660610, upload-time = "2026-09-11T19:05:31.214Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/a9/a7fc5a63c427aeb9a4b99d6a0449f891be0952d3aee2b85229099f7b9f54/matplotlib-3.11.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:5e1e923a3fc3326b99ec0a6ff1ab1338ac6c6cc62ad9d8a9c944197c7f8c6221", size = 9461046, upload-time = "2026-09-11T19:02:59.277Z" }, + { url = "https://files.pythonhosted.org/packages/0b/d7/59d8ee63e4dee845d953515c50d67e6b8cefa953b453c6b803f197ae556e/matplotlib-3.11.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c27e577ece613ea12a0790e00b4eb80d901c59a3e16cad474f31d8b1529690b9", size = 9290872, upload-time = "2026-09-11T19:03:02.295Z" }, + { url = "https://files.pythonhosted.org/packages/2e/a4/1ca765c63a60e3096ae2ad132eb25f6f7eab0377418cd0472808befd9665/matplotlib-3.11.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:07d9b9fa60cd4c393692f50d0bb03123242ddf61c99bb0e95e75feb354e7c1a8", size = 9854405, upload-time = "2026-09-11T19:03:04.796Z" }, + { url = "https://files.pythonhosted.org/packages/d7/3d/f28ca971a4ad0ed83ebdd60230b09d99de8b91aba47964fac3a81eb2e2ba/matplotlib-3.11.2-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:30ec15d7eefee71de16b7c689b42ba49715a4644650b76a6c7c70d79daf24e91", size = 10660507, upload-time = "2026-09-11T19:03:07.402Z" }, + { url = "https://files.pythonhosted.org/packages/f7/0c/a743556bc64a9b57d2246b14d91b713078adfd2df4199f542df1861737a9/matplotlib-3.11.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:116cdb0eb0eb5644fc98eb2975d4b4dd4ad35e5c8e6b851c22e6976f371f7ab5", size = 10801141, upload-time = "2026-09-11T19:03:11.357Z" }, + { url = "https://files.pythonhosted.org/packages/66/54/91fe3fdfa29100b4668cbf9721577a10acde6e009694618230e4e7a97527/matplotlib-3.11.2-cp311-cp311-win_amd64.whl", hash = "sha256:a24d5fd36e4f0e742c3851dcd20810e56a95633a342e4bf6cb591c678e8fe61f", size = 9329982, upload-time = "2026-09-11T19:03:14.25Z" }, + { url = "https://files.pythonhosted.org/packages/49/c1/13b70871c98d7e93f0d2ed71f226b8d6f873f1372124046068a6b4902fb5/matplotlib-3.11.2-cp311-cp311-win_arm64.whl", hash = "sha256:57b9ea60a835937c2012861923cbb91f47db8565775d326d1c42fc926aa10351", size = 9023542, upload-time = "2026-09-11T19:03:16.878Z" }, + { url = "https://files.pythonhosted.org/packages/cb/ce/1bfcc4873b121597791ad74032943b123218c0613af0b97e8dd05e916fb1/matplotlib-3.11.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ef752769cd962f39ea0b6ffc82d1ea43a0012c5a6157c7a075212fa509cfcff2", size = 9476466, upload-time = "2026-09-11T19:03:19.45Z" }, + { url = "https://files.pythonhosted.org/packages/a6/c4/7f5f3601ee69baf072c0c7d3ce60c03e0618621c5a56c34a62a460e29d11/matplotlib-3.11.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ef31985c4dedb5f1424e1aec6849a47dd37689cb7fa3c20b1b82187f26806261", size = 9305583, upload-time = "2026-09-11T19:03:22.39Z" }, + { url = "https://files.pythonhosted.org/packages/b8/90/2b3fd67ee273163faeda6d514be70b5596eaa0fd77b60ffc294ad0b34f5f/matplotlib-3.11.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:df4f7784aca81a94f254c0a2767d592ee25f407e488f5fa7203e51093fb6ca27", size = 9860353, upload-time = "2026-09-11T19:03:25.049Z" }, + { url = "https://files.pythonhosted.org/packages/f4/84/32549e7a462dc311aed2ab62e5d2538028840b5c53a8be0195c789937c3d/matplotlib-3.11.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b9a7ad579856284135e401ecc918c5f8a017ee30539298862a109f51b971710", size = 10670348, upload-time = "2026-09-11T19:03:28.108Z" }, + { url = "https://files.pythonhosted.org/packages/84/39/02e21b74f7439bd643d717ea846006d46e309e6d29b632b57751265311d6/matplotlib-3.11.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3aa4b8516fd26659e4363abbf317c703d9116496c5db2e9d0609a2866dd39dd2", size = 10810580, upload-time = "2026-09-11T19:03:31.402Z" }, + { url = "https://files.pythonhosted.org/packages/04/93/0d239bde12b308262265c2d98909b2f0ecad2b5deeae96241642e822e9c7/matplotlib-3.11.2-cp312-cp312-win_amd64.whl", hash = "sha256:c5c1c68ee401fc98271263410f0e5ce88285abacf7627132914e8adf3d70ff43", size = 9349409, upload-time = "2026-09-11T19:03:34.195Z" }, + { url = "https://files.pythonhosted.org/packages/49/a8/06baf901c02246c8a222b655cc4540ef9c15e1549a04e07927f9cde716c3/matplotlib-3.11.2-cp312-cp312-win_arm64.whl", hash = "sha256:643ff850d8e0f5b8319337f87ed3cb59506afb3df3cc48de777d85871233be7b", size = 9028084, upload-time = "2026-09-11T19:03:37.032Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ce/fc58855a3d17f6cb8a87b6f9ccd100f65432a3a7ffed57309cbf14c78987/matplotlib-3.11.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7d43ff8cebb50840648cb6429b2228621dbd709010f3117b3740abb20abf21c0", size = 9476987, upload-time = "2026-09-11T19:03:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/a9/29/2a47b700545aca050e0ed73c89ba21e08407e11f5f42a567421b220de6c1/matplotlib-3.11.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2a8285cea8ef4d92aa041d1c33788bcae82248503300f93f1ca2136b9049452f", size = 9306050, upload-time = "2026-09-11T19:03:43.103Z" }, + { url = "https://files.pythonhosted.org/packages/a9/df/e7c10292f348cf55178a5b3c0092a38ea68891a3945f199ae8b7b8ac9dc1/matplotlib-3.11.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1944895967f87c84c9b4bad29a707b31f5b36df4a3a2339ea1f8ea3ce5105539", size = 9861060, upload-time = "2026-09-11T19:03:46.044Z" }, + { url = "https://files.pythonhosted.org/packages/e4/b9/b978de6d43d47f1d4d8e653fac32c16d8d2e5f46bbbb822e26d7cb1adece/matplotlib-3.11.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af2661f6ac6bbd1d081996f54fdd9385715c625ace8cec0869055c0cfbf38981", size = 10670885, upload-time = "2026-09-11T19:03:48.942Z" }, + { url = "https://files.pythonhosted.org/packages/a3/80/d72f61bb2631ac15d17fada1f17e0f99fecba69d8376d34f17cf93b7eddd/matplotlib-3.11.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7e5a90f8a707ebb6004a713b1cff091a40cc5df4c7c1cb165e5a505ebc11c292", size = 10811222, upload-time = "2026-09-11T19:03:51.665Z" }, + { url = "https://files.pythonhosted.org/packages/94/2e/6d1d33f242c705b5c6c6fb3f0fc141add51cb1abb62912e59de0835d28c4/matplotlib-3.11.2-cp313-cp313-win_amd64.whl", hash = "sha256:bc067c462a86f0e57bf52fc6e058d90171a5420007dac00c46e90153050c69a7", size = 9349539, upload-time = "2026-09-11T19:03:54.534Z" }, + { url = "https://files.pythonhosted.org/packages/bb/bb/c0aa86a7f4c0e62f1c3c0711e361f6f23f87eb9018732a5e11994bd1b7ec/matplotlib-3.11.2-cp313-cp313-win_arm64.whl", hash = "sha256:3e8576f7c47e02fd4f21f44171302d2d1d58d4471d46da3d71fe8899d19539d9", size = 9028295, upload-time = "2026-09-11T19:03:57.261Z" }, + { url = "https://files.pythonhosted.org/packages/54/b4/4facdb700bc236e4c676495ca3798d8134ab8668cbfe0f6b2e9ebf962a38/matplotlib-3.11.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c9721f81275499da1feeb36a2cf8192ea086283b3bd16b7dc4c9d7aedb7396d6", size = 9478976, upload-time = "2026-09-11T19:04:00.12Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a0/bba05f0a25bfeb0475c2f2a87958b31103aa6b7a5386017afb1a96212825/matplotlib-3.11.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1a3040b209f3968b4e84161df7b174f07a9fad33b0f2d7e48ea3bbd3075e2863", size = 9307932, upload-time = "2026-09-11T19:04:02.976Z" }, + { url = "https://files.pythonhosted.org/packages/d4/42/81d5cba4bb39b41b0998efc880e129f2d7cdb94256ca49678fdcd9c21be5/matplotlib-3.11.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf3fe71fbfb8ec0e310e0bc8537c3405a01f38f25f9394ed2135e6202fed542b", size = 10674370, upload-time = "2026-09-11T19:04:06.912Z" }, + { url = "https://files.pythonhosted.org/packages/29/5e/52f56f93d5b20815ee0ae352f7a38eff25580edb180b4fa7df87cfeba68f/matplotlib-3.11.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8c8255de28f986d935a64c9ca71c0ec2d2f41d355691f5ea684725dc91413f71", size = 10954597, upload-time = "2026-09-11T19:04:09.948Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ae/f8918a1b564db49c53cd603eb3c8748c5767e4ce0fb2cf1d7c38653e4c5d/matplotlib-3.11.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a8756cc73d9af9a7fe0deb54ea2e75ef73b01d9e575877e72acad5458e660943", size = 10812583, upload-time = "2026-09-11T19:04:12.887Z" }, + { url = "https://files.pythonhosted.org/packages/ea/61/4315ef0d63937fe56f451887b4a0be3b8e8b22cf1d92a8d157caac54570d/matplotlib-3.11.2-cp314-cp314-win_amd64.whl", hash = "sha256:ecea603dd2fbf8242fd31a305a8b12a4ece2de28096870c65fdd0d1e35b8d9a6", size = 9505972, upload-time = "2026-09-11T19:04:15.788Z" }, + { url = "https://files.pythonhosted.org/packages/d5/00/02398b0a1a62ef73d9af0a957fe1773363eec9a6ca7e433c72ae2790fbce/matplotlib-3.11.2-cp314-cp314-win_arm64.whl", hash = "sha256:01dc8eaaab5a9fce9ff615eca82345728f289e4715b186ee10c6d85272fc26bb", size = 9184534, upload-time = "2026-09-11T19:04:18.691Z" }, + { url = "https://files.pythonhosted.org/packages/ec/c5/1e6ca10baa7e04f113c37856aadb4adc3dbb1c55fc7da6dc830bba1292d7/matplotlib-3.11.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:79a258f58253dfa025af80a9e9bb228d75fced007f0e93ae7423fefbde81a74d", size = 9526415, upload-time = "2026-09-11T19:04:21.39Z" }, + { url = "https://files.pythonhosted.org/packages/e0/93/7561727af07ccc84747953c6a33eee3981cc436d04f465032e707ca0e429/matplotlib-3.11.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c0b83f044ce10a98027b105b3931548719a6e8c7ef986b4362651e0b5367c8dc", size = 9361064, upload-time = "2026-09-11T19:04:24.107Z" }, + { url = "https://files.pythonhosted.org/packages/c1/40/ce270ef2a6794d94a409fe28b8dabedffc07c07f9e5dbb89e0f605bd157d/matplotlib-3.11.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e43b188f0a5b75447bcc197728258166aa64365770ed1caa36595a5e1ca4bbba", size = 10684626, upload-time = "2026-09-11T19:04:27.201Z" }, + { url = "https://files.pythonhosted.org/packages/45/46/396e0307dbf1c1f2d0a3654caa6dbc780dd5379f5f11257369cf8d21d9fe/matplotlib-3.11.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3304eb5a59442a8867f6920d484591c0fa09ffc29e9260be2feec3351e25869", size = 10960602, upload-time = "2026-09-11T19:04:30.254Z" }, + { url = "https://files.pythonhosted.org/packages/1c/97/4defb84695477340aa88b27c9482cc1ca4de11f36f66b6c8c0ddd685def1/matplotlib-3.11.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:02329432ae5c6af87cf208ee575b701d698bdf0b1a3bb28cc6d53c36e967e575", size = 10822603, upload-time = "2026-09-11T19:04:32.964Z" }, + { url = "https://files.pythonhosted.org/packages/7d/e7/eab54551c1ec6b72dcd678a078b6c6da8d4c85ec1ff0016e81fe52866304/matplotlib-3.11.2-cp314-cp314t-win_amd64.whl", hash = "sha256:f2ac30cf5eb5dff1b584627ae0b0e1186551a4f69ae3c75073911da497a29170", size = 9551206, upload-time = "2026-09-11T19:04:35.811Z" }, + { url = "https://files.pythonhosted.org/packages/4e/0e/04698032cb4d8fb1e30d8ffec5ded20f6f582b03c8c58b2385c95245f158/matplotlib-3.11.2-cp314-cp314t-win_arm64.whl", hash = "sha256:cf41ecd1b0c0b6f7177ed965a54c2afbe888715c7cf6054dc12d53bc1494002c", size = 9234977, upload-time = "2026-09-11T19:04:38.972Z" }, + { url = "https://files.pythonhosted.org/packages/0f/d9/a1218a31fa2d8082eb893548990ad09a2005ebda1b2b89dad88a8308e51a/matplotlib-3.11.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:854df8d7dfe9fdffcbaa6f39e44a6c24b409cb4d7561fbc09213b157d833f6a6", size = 9457702, upload-time = "2026-09-11T19:05:22.086Z" }, + { url = "https://files.pythonhosted.org/packages/68/d7/a9fde05d87c3c853f233ec20c5b5c3a1c86035e1ed4bfa397cd54d46ec90/matplotlib-3.11.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:eac4b07d4e3743b172451e122ea964f72f152879d4f9adcf3f3d33e518f12ead", size = 9287753, upload-time = "2026-09-11T19:05:24.677Z" }, + { url = "https://files.pythonhosted.org/packages/ec/79/ea2ae4eab84f011c1dc8145864f78e5a63adda8efb95557ef36cc38e6da2/matplotlib-3.11.2-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ef7a53b66780e5d942923724f08577fbc5be1f7322da0f0f3f9dcaa45dd803d", size = 9848259, upload-time = "2026-09-11T19:05:27.455Z" }, ] [[package]] @@ -1044,84 +1191,84 @@ wheels = [ [[package]] name = "msgpack" -version = "1.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/31/f9/c0a1c127f9049db9155afc316952ea571720dd01833ff5e4d7e8e6352dbb/msgpack-1.2.1.tar.gz", hash = "sha256:04c721c2c7448767e9e3f2520a475663d8ee0f09c31890f6d2bd70fd636a9647", size = 183960, upload-time = "2026-06-18T16:13:52.594Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/16/f70100614b69feb3ade7285f08c9c52d6cda0a5c03f3f5e2facd63acb211/msgpack-1.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8c7b398c56ff125feae96c2737abfec5595f1fa0aa186df60c56040b8accb95c", size = 82926, upload-time = "2026-06-18T16:12:31.531Z" }, - { url = "https://files.pythonhosted.org/packages/e4/3c/08ecd5cdfe4e2de43aec79062028ad0f7b2d9b1fea5430068c198ba570da/msgpack-1.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1548006a91aa93c5da81f3bdcebc1a0d10cea2d25969754fbe848da622b2b895", size = 82730, upload-time = "2026-06-18T16:12:32.894Z" }, - { url = "https://files.pythonhosted.org/packages/19/9f/a70c9cb1a04ecc134005149367dcfe35d167284e8f65035a1e4156ad17b5/msgpack-1.2.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1dabedcd0f23559f3596428c6589c1cd8c6eaed3a0d720795b07b0225d769203", size = 400729, upload-time = "2026-06-18T16:12:34.052Z" }, - { url = "https://files.pythonhosted.org/packages/fa/7f/5ce020168cf0439041526e95aa068c722c016aee21624e331aeabeee2e8e/msgpack-1.2.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83efa1c898e0fc5380fc0cabbf75164c52e3b5cbb45973710d75821928380c73", size = 407625, upload-time = "2026-06-18T16:12:35.239Z" }, - { url = "https://files.pythonhosted.org/packages/79/70/fb7668ce0386819303047057aef6fc1da73b584291d9cff82b821744e2ef/msgpack-1.2.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01e2dd6c9b19d333a00282330cc8a73d38d8dabc306dc5b42cd668c3ac82e833", size = 377891, upload-time = "2026-06-18T16:12:36.684Z" }, - { url = "https://files.pythonhosted.org/packages/3d/dc/9ebe654a73c3aed2e40aa6b52e3c2a02b5f53ef0085fa235a45d5b367f87/msgpack-1.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:350cb813d0af6e65d2f7ef0d729f7ff5be5a8bce03665892f43e5883d4ecc1b8", size = 391987, upload-time = "2026-06-18T16:12:37.839Z" }, - { url = "https://files.pythonhosted.org/packages/42/eb/b67cf64218a2fa25e1c671fe1d3dbb06cbeb973e71bc4b822da079862d0b/msgpack-1.2.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:ee1d9ed27d0497b848923746cf762ed2e7db24f4be7eec8e5cbe8c766aa707b7", size = 374603, upload-time = "2026-06-18T16:12:39.221Z" }, - { url = "https://files.pythonhosted.org/packages/a2/2e/9ee200cde32fd1a0101b4006202fde554c1860adfb9bf7bff31ea4c08df8/msgpack-1.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:633727297ed063441fd1cda2288865487f33ad14eeb8831afb5f0c396a62cfce", size = 405121, upload-time = "2026-06-18T16:12:40.524Z" }, - { url = "https://files.pythonhosted.org/packages/43/b6/f10117be7ca7a51e8feed699a907b8e663a8cd66e115ae6b4fb30cc7945c/msgpack-1.2.1-cp310-cp310-win32.whl", hash = "sha256:298872ecf9e61950f1c6af4ca969b859ee91783bb920ef6e6172697d0c8aad74", size = 64088, upload-time = "2026-06-18T16:12:41.762Z" }, - { url = "https://files.pythonhosted.org/packages/ba/93/89976c696fb0224662239d952c47b4d1661b34d79a332ef5584facaa8579/msgpack-1.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:2ff164c1b0bcb740b073b99e945234d0212852fa378e44a208c425379140dbeb", size = 70113, upload-time = "2026-06-18T16:12:42.78Z" }, - { url = "https://files.pythonhosted.org/packages/f4/6b/e9b1cdc042c4458801d2545ed782a95f3d6ba8e270cce8745b8603c7f748/msgpack-1.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:29a3f6e9667868429d8240dfd063ea5ffdc1321c13d783aa23827a38de0dcb22", size = 82812, upload-time = "2026-06-18T16:12:45.022Z" }, - { url = "https://files.pythonhosted.org/packages/0c/3a/dd518a1bf78ed1e9ad8afe57307c079a00eafe4b3068932a27ca1ea56b4f/msgpack-1.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aded5bdf32609dc7987a49bbbd15a8ef096193f96dd8bbeb791de729e650acf5", size = 82739, upload-time = "2026-06-18T16:12:46.025Z" }, - { url = "https://files.pythonhosted.org/packages/70/e0/7ba9e1542bf0771a27b8b37c1316e3f95ae9d748fd765284655c476ad4ef/msgpack-1.2.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:146ee4e9ce80b365c6d4c47073da9da7bcec473e58194ceee5dd7620ace77e06", size = 414233, upload-time = "2026-06-18T16:12:47.029Z" }, - { url = "https://files.pythonhosted.org/packages/03/8d/671d81534ea0e2b0e8a121be100020da09eb78861fe3aa8f3ef7dcd3bed1/msgpack-1.2.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a28d076ca7c82b9c8728ad90b7147489449557038bed50e4241eb832395169b4", size = 423843, upload-time = "2026-06-18T16:12:48.19Z" }, - { url = "https://files.pythonhosted.org/packages/d2/b6/e5c737515ed1f166664b87601b532f58cbb73d8aa6a90b99f7c2c5037e8e/msgpack-1.2.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7d31c0ac0c640f877804c67cb2bc9f4e23dc2db97e96c2e67fa27d38283b41f8", size = 390772, upload-time = "2026-06-18T16:12:49.624Z" }, - { url = "https://files.pythonhosted.org/packages/a8/46/62ed8c2e87d7021eab19921594d961ef3aa3794eec76c716dc30f3bfd433/msgpack-1.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8ff92d7feeaf5bc26c51495b69e2f99ed97ab79346fb6555f44be7dd2ac6503b", size = 409559, upload-time = "2026-06-18T16:12:50.936Z" }, - { url = "https://files.pythonhosted.org/packages/70/ff/59aa3887b860bbf43532835e192b1c388a17590d6068ae4f8b2bc74c906e/msgpack-1.2.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:779197a6513bab3c3632265e3d0f7cb3227e62510841a6f34f1eaa37efbb345e", size = 387838, upload-time = "2026-06-18T16:12:52.161Z" }, - { url = "https://files.pythonhosted.org/packages/09/11/f8563e471093420cf6478cb3271a0175d8402b82d879783d4035d2d03360/msgpack-1.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:67f6dd22fa72a93752643f07889796d62739a13415ee630169a8ce764f86cf9f", size = 421732, upload-time = "2026-06-18T16:12:53.556Z" }, - { url = "https://files.pythonhosted.org/packages/57/cf/e673683c4c6c90c1022b24c65af4b03eda72b182a1176ef6449069d66acc/msgpack-1.2.1-cp311-cp311-win32.whl", hash = "sha256:91054a783328e0ea7954b8771095705c8d2243b814743fbaadf14552c9c52c5d", size = 64091, upload-time = "2026-06-18T16:12:54.821Z" }, - { url = "https://files.pythonhosted.org/packages/3f/07/ca212739d179f9083bff2c7c08c24101c3555a334fadc2b876b18768a3ae/msgpack-1.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2eda0b7ebb1283a98d3e4492ac933c8af6aff59fd3df1c3ed024f536af4b1dc8", size = 70462, upload-time = "2026-06-18T16:12:55.898Z" }, - { url = "https://files.pythonhosted.org/packages/6d/be/6798347b425e26f35db82e69dd83c09716c856a3714e7bffc4c0860fd830/msgpack-1.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:6ee967f7c7e1df2890c671ff2ee51a28ded0efc95da3e507176dee881ce36c66", size = 65059, upload-time = "2026-06-18T16:12:57.053Z" }, - { url = "https://files.pythonhosted.org/packages/bc/dd/9e8cbd8f5582ca4b590336f2b91ee5662f6a6ca562b565abaf696a0f81ff/msgpack-1.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2ef59c659f289eddf8aa6623823f19fa2f40a4029266889eac7a2505dd210c35", size = 83531, upload-time = "2026-06-18T16:12:58.249Z" }, - { url = "https://files.pythonhosted.org/packages/50/2e/ebdb85a8da151397a2790363676b7ed7c125924fe618e4c6d8befb0cc62c/msgpack-1.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d3567748a5107cb40cdf66a275430c2f87c07777698f4bfd25c35f44d533258c", size = 82657, upload-time = "2026-06-18T16:12:59.396Z" }, - { url = "https://files.pythonhosted.org/packages/26/aa/753ad8b007b464e1d8aa0c8e650b9c5f4f725e658fc5ac8a7635c55b7f6e/msgpack-1.2.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60926b75d00c8e816ef98f3034f484a8bc64242d66839cef4cf7e503142316a0", size = 410634, upload-time = "2026-06-18T16:13:00.383Z" }, - { url = "https://files.pythonhosted.org/packages/6a/fd/6adabd4f6d5e686f97dd02ce7fce3fe4cf672cbac36b8f67ff4040e8ad8b/msgpack-1.2.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:020e881a764b20d8d7ca1a54fc01b8175519d108e3c3f194fddc200bda95951a", size = 419989, upload-time = "2026-06-18T16:13:01.776Z" }, - { url = "https://files.pythonhosted.org/packages/5a/cc/85039b7b0eb168aaad7383a23c97e291a11f08351cb45a606ce865e4e3f1/msgpack-1.2.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4202c74688ca06591f78cb18988228bd4cca2cc75d57b60008372892d2f1e6e6", size = 377544, upload-time = "2026-06-18T16:13:03.637Z" }, - { url = "https://files.pythonhosted.org/packages/ed/bf/35963899493b32030c85fc513b723ae66144ac70c11ebc52e889e16e3d99/msgpack-1.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8b267ce94efb76fbd1b3373511420074ee3187f0f7811bf394531de13294735a", size = 400842, upload-time = "2026-06-18T16:13:05.012Z" }, - { url = "https://files.pythonhosted.org/packages/a6/df/8e2ac970c8f99264cd9997d1c73df5466bc19da3301d7dc5500862a9b089/msgpack-1.2.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f1d0f8f98ade9634e01fb704a408f9336c0a8f1117b369f5db83dc7551d8b1", size = 374108, upload-time = "2026-06-18T16:13:06.232Z" }, - { url = "https://files.pythonhosted.org/packages/17/dd/fa8bd265110dfa51c20cb529f9e6d240a16fafe7e645004c6af2d01353ba/msgpack-1.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f02cf17a6ca1abe29b5f980644f7551f94d71f2011509b26d8625ce038f0df64", size = 414939, upload-time = "2026-06-18T16:13:07.478Z" }, - { url = "https://files.pythonhosted.org/packages/2e/b9/8377a5ad8953fc0437c70cc98d9ae29f27fe5ac5109fbec0812085865735/msgpack-1.2.1-cp312-cp312-win32.whl", hash = "sha256:0c0d9802354507bcba62af19c17918e3eb437cc25e6f50657d511b5856a77aac", size = 64504, upload-time = "2026-06-18T16:13:08.822Z" }, - { url = "https://files.pythonhosted.org/packages/57/7f/ce1e377df7e62461fefd9eb23bfb93a4a523f40a517b377b8f844d836828/msgpack-1.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:5c24aa15d5963051e1a5c62b12c50cd705992502b5ec1f3bece6046f33c9fc24", size = 71421, upload-time = "2026-06-18T16:13:09.828Z" }, - { url = "https://files.pythonhosted.org/packages/8f/32/ebfe84c9929f08f188d56c7a2fd913406a9ddad76a634697c1c43b8112e6/msgpack-1.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:4227224aaec8f7fbcbfbd4272319347b2bb4030366502600f8c45588c5187b07", size = 64775, upload-time = "2026-06-18T16:13:11.056Z" }, - { url = "https://files.pythonhosted.org/packages/b0/ac/dcddcab6f6c20ecb387ca5e980371cdb3f87ff69aeca388be97eebc4c074/msgpack-1.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0a70e3cf2804a300d921bb0940426e35f4e489a23adfb77a808892241db0a064", size = 83151, upload-time = "2026-06-18T16:13:12.173Z" }, - { url = "https://files.pythonhosted.org/packages/64/71/fbcfa83a1d6a9c6091942d1cfd070962244664b87427a9a49a6897b1b219/msgpack-1.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:491cc39455ca765fad51fb451bf2915eb2cf41192ab5801ce8d67c1d614fe056", size = 82351, upload-time = "2026-06-18T16:13:13.194Z" }, - { url = "https://files.pythonhosted.org/packages/e3/10/ddf7b06db879e8792d13934ddda09ff20bd2a583fd84c9b59aae9b0e650b/msgpack-1.2.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f310233ef7fb9c14e201c93639fe5f5260b005f56f0b29048e999c30935596cc", size = 407518, upload-time = "2026-06-18T16:13:14.233Z" }, - { url = "https://files.pythonhosted.org/packages/79/d3/36a46a8ed992b781acbc05928bd5bee3c810cb0c3563bf81a7b0c04a1a76/msgpack-1.2.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:787c9bebb5833e8f6fc8abca3c0597683d8d87f56a8842b6b89c75a5f3176e2d", size = 416405, upload-time = "2026-06-18T16:13:15.435Z" }, - { url = "https://files.pythonhosted.org/packages/f9/84/e8e9598b557c0ba6ddae901a73780a4c75ac667dddf59414b1e56a42fb34/msgpack-1.2.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dc871b997a9370d855b7394465f2f350e847a5b806dd38dcc9c989e7d87da155", size = 376257, upload-time = "2026-06-18T16:13:17.022Z" }, - { url = "https://files.pythonhosted.org/packages/40/16/738fe6d875ad7e2a9429c165322a4ec088f4f273cdfae63d96a89c467961/msgpack-1.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:85f57e960d877f2977f6430896191b04a21f8901b3b4baf2e4604329f4db5402", size = 397469, upload-time = "2026-06-18T16:13:18.287Z" }, - { url = "https://files.pythonhosted.org/packages/ca/be/6d5952df75a7f24f35833af764c3a6860780364cb3a0030beb8099e1b2b4/msgpack-1.2.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:1233ee2dd0cefba127583de50ea654677277047d238303521db35def3d7b2e7c", size = 372802, upload-time = "2026-06-18T16:13:19.685Z" }, - { url = "https://files.pythonhosted.org/packages/e1/39/e2ef7dbf0473bcb8dc7c50bf782a892d67414877b63e47fc88eb189ef5e6/msgpack-1.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e3dc2feb0876209d9c38aa56cb1de169bd6c4348f1aa48271f241226590993e6", size = 411273, upload-time = "2026-06-18T16:13:21.028Z" }, - { url = "https://files.pythonhosted.org/packages/ef/c5/133f4512a56e983a93445c836c9d94d88f3bc2e0980ff4b9e577bd8416ce/msgpack-1.2.1-cp313-cp313-win32.whl", hash = "sha256:6d09badf350af2be9d189184e04e64cf54ad93569ab3d96fca58bd3e84aad707", size = 64471, upload-time = "2026-06-18T16:13:22.293Z" }, - { url = "https://files.pythonhosted.org/packages/e2/98/577e10b055096a7dd40732358cabaf7180a20c79ed1dcdbb618e4b9deac7/msgpack-1.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:33f14fba63278b714efe6ad07e50ea5f03d91537aa6a1c5f1ceca4cf44013ca9", size = 71274, upload-time = "2026-06-18T16:13:23.455Z" }, - { url = "https://files.pythonhosted.org/packages/ba/ee/0c0048e7cfbef23c6a94791b8959ab28155232e7956de8a305b5ff588f05/msgpack-1.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:afc5febcd4c99effbc02b528e49d6fd0760b2b7d48c05239e345a5fa6e743d9a", size = 64795, upload-time = "2026-06-18T16:13:24.687Z" }, - { url = "https://files.pythonhosted.org/packages/77/58/cce442852c6b9e1639c7c8ac8fd9143121cb32dab0f308df4d1426a8eb9c/msgpack-1.2.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:05f340e47e7e47d2da8db9b53e1bb1d294369e9ef45a747441309f6650b8351d", size = 83610, upload-time = "2026-06-18T16:13:25.724Z" }, - { url = "https://files.pythonhosted.org/packages/60/5c/15b4c7a0182f75ffa90751958ba36a9c01cafee367d49a3edc10ed140b01/msgpack-1.2.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:810b916696c86ef0deb3b74588480224df4c1b071136c34183e4a2a4284d7ac7", size = 83138, upload-time = "2026-06-18T16:13:26.781Z" }, - { url = "https://files.pythonhosted.org/packages/b8/a6/99e58722feaffc5f2fbcc0c8c0d1451ab9f84097f7af87291b46af2390f4/msgpack-1.2.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ca0dacff965c47afdc3749a8469d7302a8f801d6a28758d55120d75e66ce6889", size = 406090, upload-time = "2026-06-18T16:13:28.072Z" }, - { url = "https://files.pythonhosted.org/packages/19/03/8c63e8cf52958534ef688625965ab04c269a6cadd8caef16758b380a821a/msgpack-1.2.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e2bf9280bceb5efca998435904b5d3e9fdbcc11d90dc9df30aec7973252b720", size = 412106, upload-time = "2026-06-18T16:13:29.427Z" }, - { url = "https://files.pythonhosted.org/packages/63/d2/155d9e71b40e41fd934bc0c48b9b2770f22263e1ac20aad8e29fdca7be3f/msgpack-1.2.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa6c4be5d1c02a42b066ca6ddb71adf36432868fdcdb6ee87e634e86e0674190", size = 374851, upload-time = "2026-06-18T16:13:30.631Z" }, - { url = "https://files.pythonhosted.org/packages/98/48/deaf2326262a8d5ea3295ce9649912ecd3f551ba7ec8e33c665d2ba583f3/msgpack-1.2.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec0e675d59150a6269ddc9139087c722292664a37d071a849c05c473350f1f2d", size = 396168, upload-time = "2026-06-18T16:13:31.977Z" }, - { url = "https://files.pythonhosted.org/packages/10/2a/b4410f906c2ec0008f1608d3ab5143afc3ad3f4e6da0fed3ea2231d0bef4/msgpack-1.2.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:dd3bfe82d53edfe4b7fc9a7ec9761e23a7a5b1dac22264505af428253c29ed24", size = 371959, upload-time = "2026-06-18T16:13:33.282Z" }, - { url = "https://files.pythonhosted.org/packages/59/86/1edc67270099a528fa2093ea60fe191233cd238e4bd30cfacf7db79fc959/msgpack-1.2.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5ad5467fc3f68b5468e06c5f788d712e9f8ffc8b0cd1bcb160c105c1ee92dae7", size = 408457, upload-time = "2026-06-18T16:13:34.567Z" }, - { url = "https://files.pythonhosted.org/packages/82/90/8b630fef07d8c5ab457b71ff2c217910c83d333c7a68472c186e87cc504a/msgpack-1.2.1-cp314-cp314-win32.whl", hash = "sha256:98b58bdb89c46190e4609bb36abe17c6d4105ad13f9c5f8f6f64d320f8ced3fb", size = 65942, upload-time = "2026-06-18T16:13:36.056Z" }, - { url = "https://files.pythonhosted.org/packages/16/f1/467b81e98b24dd3885d7b1857728797b4ffc76a7a7483af4fb321a07de3c/msgpack-1.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:74847557e28ce71bd3c438a447ca90e4b507e997ddbdef8a12a7b283b86c156b", size = 72627, upload-time = "2026-06-18T16:13:37.079Z" }, - { url = "https://files.pythonhosted.org/packages/a7/1d/5d8c4c89985feb6acefb82a09e501c60392261856d2408d20bfe4f0360b1/msgpack-1.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:b50b727bd652bdc37d950336c848ef20ec54a4cafc38dce19b1cd86ad625d0f7", size = 66908, upload-time = "2026-06-18T16:13:38.23Z" }, - { url = "https://files.pythonhosted.org/packages/1b/02/ad2afb678b4de94496cd432b581759b756a92c1192d8c767edd6b132efdc/msgpack-1.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8d00f177ca88a77c1cf848d204a38f249751650b601cb6532acc68805d8a8273", size = 86000, upload-time = "2026-06-18T16:13:39.44Z" }, - { url = "https://files.pythonhosted.org/packages/54/74/0b797484013128837f3b1cbb6cea019277c4de4e377dc512b4d9a0f92940/msgpack-1.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5bb9c386f0a329c035ddbab4b72d1028bf9627add8dda41070288563d57ed1b1", size = 86544, upload-time = "2026-06-18T16:13:40.447Z" }, - { url = "https://files.pythonhosted.org/packages/a9/b4/b774d7eb95561739907fec675582f83203cf41c597a418c2589b4bfb8e9d/msgpack-1.2.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20466cca18c49c7292a8984bc15d65857b171e7264bdcb5f96baf8be238791fc", size = 427661, upload-time = "2026-06-18T16:13:41.574Z" }, - { url = "https://files.pythonhosted.org/packages/b2/f9/3243191dc9937e00756c8bc1b0272fed8f23758e43df2a3b46f533e5090f/msgpack-1.2.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:196300e7e5d6e74d50f1607ab9c06c4a1484c383cd22defd727902591f7e8dde", size = 426375, upload-time = "2026-06-18T16:13:42.936Z" }, - { url = "https://files.pythonhosted.org/packages/23/c7/1693111db9944ba4ad4b67a1e788400d78a0b6af7a6523dc7e4e58f8274b/msgpack-1.2.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575957e79cd51903a4e8495a242442949641e08f1efd5197b43bebd3ea7682b4", size = 380495, upload-time = "2026-06-18T16:13:44.306Z" }, - { url = "https://files.pythonhosted.org/packages/3e/2b/92f86956a0c13e8662f7e2ad630c4eb4db07497b967589bd5245e018b2c1/msgpack-1.2.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8c2ed1e48cc0f460bf3c7780e7137ff21a4e18433451916f2442c1b21036cd7d", size = 410897, upload-time = "2026-06-18T16:13:45.629Z" }, - { url = "https://files.pythonhosted.org/packages/da/ea/1479f72d200313a76fc2f823a79d1e07ed052ab7b8a0280640aa7b95de42/msgpack-1.2.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5f6277e5f783c36786a145e0247fc189a03f35f84b251646e53592d2bc12b355", size = 378519, upload-time = "2026-06-18T16:13:46.998Z" }, - { url = "https://files.pythonhosted.org/packages/f5/4d/fa006060ffa1011d32bfae826fe766fe73e02982183601633b7121058ab3/msgpack-1.2.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9389552ecf4784886345ead0647e4edc96bee37cbab05b75540f542f766c48c", size = 419815, upload-time = "2026-06-18T16:13:48.205Z" }, - { url = "https://files.pythonhosted.org/packages/2f/e1/aab6c946570496b78e67804721f3d5e2d62a93081b9b37df77764ef56347/msgpack-1.2.1-cp314-cp314t-win32.whl", hash = "sha256:c1c79a604a2969a868a78b6ebd27a887e00c624f14f66b3038e0590cb23332d1", size = 70914, upload-time = "2026-06-18T16:13:49.385Z" }, - { url = "https://files.pythonhosted.org/packages/13/0a/e608956488a2af014cfe6e3d665e090b8ee42aa14b07f8f95b8880d66b09/msgpack-1.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f12038a35fabd52e56a3547bab42401af49a45caa6dd00b34c44de235bc93ee2", size = 77999, upload-time = "2026-06-18T16:13:50.467Z" }, - { url = "https://files.pythonhosted.org/packages/d2/8a/27e2e57055176e366a46b85d02d68e7a5bcfbdd8474c9706375d965f24d3/msgpack-1.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:0adcf06ffde0777c0e1a9b771a2b1c4226ba1bbf748c8efcc02fcdeca3299107", size = 71160, upload-time = "2026-06-18T16:13:51.498Z" }, +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/44/ea2100ec54d30c46ee9dba10a3bfb79b655e96c6df237238a3234c75869b/msgpack-1.2.2.tar.gz", hash = "sha256:9eb0b0e602064527a045ea28c4f174ed69383587e29cebe28947e3b84106eb2a", size = 187025, upload-time = "2026-08-27T10:03:47.793Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/40/181c8944b28f779ed0b2587d24cc0ccf1bc87248204105327140aa20d63d/msgpack-1.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7afa5431f6f3487c584187ca6c8e2a34e9b106529893b3e720eabb068f6ac970", size = 83554, upload-time = "2026-08-27T10:01:34.182Z" }, + { url = "https://files.pythonhosted.org/packages/03/f7/a6bf145f7d3eb734b3d97fa295f8007a586799ef56b456c8b27bff62caac/msgpack-1.2.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a9b4cf3685a135666d27d0d7a73fece74e2fad01d9b508fded89e843512f0e90", size = 83857, upload-time = "2026-08-27T10:01:35.816Z" }, + { url = "https://files.pythonhosted.org/packages/93/1a/77d32a60a80ee67016e77b13bec07e85f1929a92f046da044591c7eb01c9/msgpack-1.2.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4710d881d8fb047deed2485707409116722af2b992d3fefd73c7667c4e350839", size = 398390, upload-time = "2026-08-27T10:01:37.693Z" }, + { url = "https://files.pythonhosted.org/packages/3f/f5/ae93f85b063d744731cb285528210cd950333b167cacc7b2f96e1420a475/msgpack-1.2.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:58ce37a4a54577115922385d37201d9a44d66d0167dfbbf4770a2e9bf8ea7ba3", size = 407183, upload-time = "2026-08-27T10:01:39.499Z" }, + { url = "https://files.pythonhosted.org/packages/17/92/d91a08a913a3bbafbede5b9dbf48e4e517f7d92510877b6b02730060ea85/msgpack-1.2.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:86f173a584f72f6164801f31866d22a581f60c991572cf922aed9ab8eb422b77", size = 375978, upload-time = "2026-08-27T10:01:41.234Z" }, + { url = "https://files.pythonhosted.org/packages/b5/a4/27400c101a96115fe8484cad57b6c5eb4f9ffba080a1d9be62ec9174bf67/msgpack-1.2.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e05a94a0442de86818a30281c6cc2cb9cc7aa148386fd3541c4d4774b73cb3a9", size = 389566, upload-time = "2026-08-27T10:01:42.779Z" }, + { url = "https://files.pythonhosted.org/packages/44/92/541e9fa4623587767623788b38d11fc78d402acb1421962a13d3ace48bef/msgpack-1.2.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:9bd3d1557c3fe1a095068210708a03e3e4795973392af6f4047060e70abd9a6c", size = 372583, upload-time = "2026-08-27T10:01:44.572Z" }, + { url = "https://files.pythonhosted.org/packages/0e/03/c1e0035e1f923f548b7016a2fef5afea431cdae95d397c5aa52ed75dde05/msgpack-1.2.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:46ec851571d8f1b6e29794ebb9dd36f785008da6d14f57c702e60781d6caf648", size = 404507, upload-time = "2026-08-27T10:01:46.167Z" }, + { url = "https://files.pythonhosted.org/packages/ca/49/7efba0fd604059f0dc8f57ba2867bda4d57bab6921a12e975ecfbd49284c/msgpack-1.2.2-cp310-cp310-win32.whl", hash = "sha256:1f3af0baafd184436501004828bb3df64eeb2fc49dfe9d89abcf604956094563", size = 64848, upload-time = "2026-08-27T10:01:47.797Z" }, + { url = "https://files.pythonhosted.org/packages/57/1d/b41e96ff441d46890b9d5982959aea4d15c5d322f92740dbef16e9dfa908/msgpack-1.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:82b1bdf293267afaadcc608b125e7fc6576bb0785a60c4fa7d07c7ab76ed76ec", size = 70927, upload-time = "2026-08-27T10:01:49.199Z" }, + { url = "https://files.pythonhosted.org/packages/53/fd/a05ba8f84c5951c9aec2a19c1c81f6c4a67b8bec80af604ac5b23ccfa019/msgpack-1.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9d7fb25b4442fae0cb2590272d06ab4f6caa526ee36a994edb81e946b874813e", size = 83498, upload-time = "2026-08-27T10:01:50.62Z" }, + { url = "https://files.pythonhosted.org/packages/0f/df/e20bcf5c149890545334743b212eb4b82e1a25fe0a34f99753a1755bfab5/msgpack-1.2.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7fe374ba76eb0ecca13a1703daa8fa85825a6ddddbb52d4c1a732fa524194683", size = 83896, upload-time = "2026-08-27T10:01:51.898Z" }, + { url = "https://files.pythonhosted.org/packages/ac/c3/00dcd902d66a641b9ba350783feb482ea5c1ca4a7ff6629db0c10c0ea982/msgpack-1.2.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9b0c1f2aa7b0026b4bd50718100e8b04175e4f36e160aa852502377b5e572e7", size = 413259, upload-time = "2026-08-27T10:01:53.296Z" }, + { url = "https://files.pythonhosted.org/packages/93/15/17374efe9793f5332c7d4727ab40539f95a1dc9df653531795daca8c4281/msgpack-1.2.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f11e09f10210a91c169e39c7a5a1f9090eaa73ad75555fafad5023c3053c47ba", size = 422907, upload-time = "2026-08-27T10:01:54.786Z" }, + { url = "https://files.pythonhosted.org/packages/d3/af/2b567d684f912fedcefe3f7c37de604716ffa99336bd432688f9f040df92/msgpack-1.2.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8b1415d02e9bf722672af8a90f90813265a0cd0b14163187261e54a5592bc949", size = 389248, upload-time = "2026-08-27T10:01:56.55Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fd/d8533fed473cc3e309a701e851d0e5fe36ada5552a3899025f5c69fbe877/msgpack-1.2.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:42fd9260416885b4815caca5bdd14dfd5dda6cdade732d6c09104ef8f6228761", size = 407099, upload-time = "2026-08-27T10:01:58.357Z" }, + { url = "https://files.pythonhosted.org/packages/d6/1b/57906337bfee0ead554571dc203ea17c3fad26d51e5eca6271ecd983f73b/msgpack-1.2.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:336525cc2688e43ea77dfb1a4ce012c8cde561835913801dbfcfdcf4111d8abb", size = 387201, upload-time = "2026-08-27T10:02:00.109Z" }, + { url = "https://files.pythonhosted.org/packages/de/0f/5d1e6d68e516621697a9262b24917d678793e838cf3f331ed4656b3e959d/msgpack-1.2.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cdb6cc6e1127d15879c47a8b3270716243da82d3e7feab1f5946872c75b3d60f", size = 420765, upload-time = "2026-08-27T10:02:01.573Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a6/07f9a4f3324d55c3567ab2a7e8d5325291bc95a31a374bb390a21b7c4e24/msgpack-1.2.2-cp311-cp311-win32.whl", hash = "sha256:cf66fb38703e61a486b01b56d43bb1f50698fbe99b6bd90feba10f24fab60b3b", size = 64785, upload-time = "2026-08-27T10:02:03.01Z" }, + { url = "https://files.pythonhosted.org/packages/84/13/f748f0d59f355d196e71a0b32d48d386a9bd311f94d954e666cf7e5b2572/msgpack-1.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:0883a1578168929fd1640fbbc4614773f1a130e419a8a817dc2918d9af1b651c", size = 71258, upload-time = "2026-08-27T10:02:04.375Z" }, + { url = "https://files.pythonhosted.org/packages/46/a6/10d979c4e76b18a9b9ebbd6499ff863474ffe5955028ea27e09b66f6833c/msgpack-1.2.2-cp311-cp311-win_arm64.whl", hash = "sha256:4955accbd87f27beebef5f3ecc27503aa74cb016fb4f640868e749fd93194a35", size = 65860, upload-time = "2026-08-27T10:02:05.735Z" }, + { url = "https://files.pythonhosted.org/packages/31/78/90c15bebb1a72667349ca62d4507e9d9369e7f8f76b95f490b823d3622e5/msgpack-1.2.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a4348705be86e029d04e741cf9ed0dfe03e942d7d3b92e838fa80d3aa2c3ebc", size = 84275, upload-time = "2026-08-27T10:02:07.106Z" }, + { url = "https://files.pythonhosted.org/packages/88/88/c2b6d8e81571da87aa232c0e34a3f3a0e618e6235892065ec82d1d81fc7a/msgpack-1.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0a652ceeededf71d3fa40c303a02a149d42338d310162367b91c539d4bd6e0a3", size = 83970, upload-time = "2026-08-27T10:02:08.488Z" }, + { url = "https://files.pythonhosted.org/packages/da/c0/d3ede9f5d16acb4c05a9281859f1e99ef9f877a928eb78454c37f70db001/msgpack-1.2.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90986cc9aab9d7d1d8f38bcbf65d3f7ac83bdd90c35765db7d691b4829698cba", size = 409401, upload-time = "2026-08-27T10:02:09.877Z" }, + { url = "https://files.pythonhosted.org/packages/41/f0/29f591bea185616cf417645ac03bd3ad9b317483ad8572160e325f7fe777/msgpack-1.2.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:77c2e018417dc1d66f235e383877ee885b60ade9d29e494dd581e08af2cb1923", size = 420619, upload-time = "2026-08-27T10:02:11.526Z" }, + { url = "https://files.pythonhosted.org/packages/4e/8e/c70c8c9180c5ddf4440eb8658ebead98e22e7686fbf84f6b165031430750/msgpack-1.2.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0e91332144f69bc3018c91232fac26da580ef748fb8eaddd7914d4458001cc4f", size = 379747, upload-time = "2026-08-27T10:02:13.345Z" }, + { url = "https://files.pythonhosted.org/packages/50/9a/f10ce11fa62700c9ab87a22e65b9ca272f7f673ddd31aeb2de6ae272ad35/msgpack-1.2.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e915d390d7068b257ca8b62f3fc59fad135c8631d1017ab03b0b924b07c5367", size = 398944, upload-time = "2026-08-27T10:02:15.006Z" }, + { url = "https://files.pythonhosted.org/packages/82/fe/d7be978456ff8552e69a8e270d882e7530e01513c096b293d83df03753ea/msgpack-1.2.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c522420d78db2431887d45b518e304d86e27b9ad0b30f24e3806a6ad5d8bdbfc", size = 373979, upload-time = "2026-08-27T10:02:16.618Z" }, + { url = "https://files.pythonhosted.org/packages/be/af/91b0d8d3fb3063e259daee3ea8515cea6282f68f4b0e5f0b6fea25762c6e/msgpack-1.2.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4b554d8164ebb526892194f71dcd96ef1fefe0c250087498785d3ffc04a80be3", size = 417781, upload-time = "2026-08-27T10:02:18.293Z" }, + { url = "https://files.pythonhosted.org/packages/8e/3c/ce8e9efe1fd9e95c78b3705e4300ba7feba3dc6c00fb76259895db155518/msgpack-1.2.2-cp312-cp312-win32.whl", hash = "sha256:0e3315de5a4b2920ccef48d96b4448025e064a10d0f5a250f6584477d839c8d4", size = 65267, upload-time = "2026-08-27T10:02:19.869Z" }, + { url = "https://files.pythonhosted.org/packages/85/98/a33b8b4af14e3476bb0da1b8c36ef7a0f28dcf95db1c5e68ff88cb89d591/msgpack-1.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:b68614fba0570349833b7dd999ff0aed4e5cc8d9eb6e3a7d4527be33c65e33d3", size = 72275, upload-time = "2026-08-27T10:02:21.141Z" }, + { url = "https://files.pythonhosted.org/packages/df/5e/2f323a33a6aba5bd4b2d8b430e4fab21d92cd91c093b49ee287bc166ee54/msgpack-1.2.2-cp312-cp312-win_arm64.whl", hash = "sha256:59d5b93efa45fd09f620d0c9ba81cde339a2c9937af3eea42ee9653094ce6640", size = 65488, upload-time = "2026-08-27T10:02:22.575Z" }, + { url = "https://files.pythonhosted.org/packages/1f/eb/42f31c5a48811787ff59a9869721f70a49654d65ab6c455f4463c39b044e/msgpack-1.2.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8b2a281b556f120a43e591ea39915741b7ad54d4727b9c4350a0a11692252533", size = 83911, upload-time = "2026-08-27T10:02:24.06Z" }, + { url = "https://files.pythonhosted.org/packages/33/54/10c6c16ddba8a5112e3680176b838e3694e4aad7284f9daa6d6d70d98817/msgpack-1.2.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1e8cdd1f3e7cc52c751092a9bf740e81e6919ab109cd376ae2d965dad0bbae34", size = 83734, upload-time = "2026-08-27T10:02:25.613Z" }, + { url = "https://files.pythonhosted.org/packages/d7/75/35823e4419df8792191b2a17ae3fe71b41d02c162b2c491c94d1a87f0caa/msgpack-1.2.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1814f92306ae7862908e9ece7cfd90e0dc87ded3e89b6ae7ffdd1175d6376fdc", size = 405635, upload-time = "2026-08-27T10:02:27.012Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d3/6592e4064619b04f2dd0054c5fa13e37e3d55eb26044483d871fadb2f46b/msgpack-1.2.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d24b38a825bcca41bb956de50eb98451ef291304a8607fad99e619043d3e79b9", size = 417332, upload-time = "2026-08-27T10:02:28.776Z" }, + { url = "https://files.pythonhosted.org/packages/e3/a1/b21c6818a545e9a4a976ac954a5c250eecde9a02e0ec82f415473dab1324/msgpack-1.2.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34e83e345194a2a51d8bd447dea9de2104f91e75b247f4735f14f04529f0746b", size = 374378, upload-time = "2026-08-27T10:02:30.678Z" }, + { url = "https://files.pythonhosted.org/packages/03/8b/7ada15c7b64151d6dbb562d1b091520efb2c37acf2403b1d4ae13797b27d/msgpack-1.2.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:682804bf31e43d46e51a9a33bd575b51e839d715ce6bd5612c055f7b28ad637b", size = 395809, upload-time = "2026-08-27T10:02:32.322Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f7/96283e50f7020df4dfeacc55612b7a210c8cdf0dda48bc262f1f9b3e4c49/msgpack-1.2.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:9b659d77f8726fa5e7038967dda6b68d53cf34472c094cfa5b845454713b90d5", size = 373495, upload-time = "2026-08-27T10:02:33.832Z" }, + { url = "https://files.pythonhosted.org/packages/cc/fe/1548dede9d9ca482f2d424a2e110a9705d4e02627a16b8bc8d10ce0208a2/msgpack-1.2.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4d9a562aec0a92fe536da2e533d313b3d2a6b929157b1dec7ff623446dc0a8ab", size = 414360, upload-time = "2026-08-27T10:02:35.396Z" }, + { url = "https://files.pythonhosted.org/packages/77/9d/4419b8f86c219174b1fb8bbd7faaf84a548935f7b1916d028401b9433417/msgpack-1.2.2-cp313-cp313-win32.whl", hash = "sha256:a4161eee7799863aee237c35c90427861f7b994416dd81ae829f560b0a81bdcd", size = 65196, upload-time = "2026-08-27T10:02:37.007Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f8/593f5caf0dacab41cde1564c5f0419e61af55ec9628006205e8fd5eb5e03/msgpack-1.2.2-cp313-cp313-win_amd64.whl", hash = "sha256:b07c03f0da7e5279170df7745ddc732d526c8a198208936ec1a95c11ed2b2d5f", size = 72203, upload-time = "2026-08-27T10:02:38.28Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9e/c6ef92046b4a2bbb9d3aa0cb581cbf4a4051afccf6e5fb301a1bd3086f39/msgpack-1.2.2-cp313-cp313-win_arm64.whl", hash = "sha256:d13d07efbf655f9ae7a2352b630c52727b359005b21ba08a507585c9ac8c0896", size = 65435, upload-time = "2026-08-27T10:02:39.534Z" }, + { url = "https://files.pythonhosted.org/packages/5e/50/3e92c403346652cabd08cb8faceef847bae917ea3b3c81b64a5b6d09ed41/msgpack-1.2.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e497ee34e8a3342bbde51b27c22d8db05a651df3361dd3daef5b3ab0d66f3e04", size = 84315, upload-time = "2026-08-27T10:02:41.181Z" }, + { url = "https://files.pythonhosted.org/packages/b6/dc/8efe6dd96a12ab043930cb4cffb40b6e7f061491d6ec7a3d2b75ef1fda42/msgpack-1.2.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0dd9173c5ebaf5ecc5ca86e7ae1db92934e1d57b856f3dd90698941431f4fd77", size = 84634, upload-time = "2026-08-27T10:02:42.621Z" }, + { url = "https://files.pythonhosted.org/packages/1b/89/996573095bf7b038c04dd65ddbc4f1a4d381b0f7a44ff9186f3c7b8325c2/msgpack-1.2.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8dc4487097571f7311188c3eca2a3e86cd1f1db4c37c7a017bcc3fd38486cbfe", size = 404194, upload-time = "2026-08-27T10:02:44.096Z" }, + { url = "https://files.pythonhosted.org/packages/b6/4e/46f5a5d949dbd054dab60cb15aac7ac6ae6774c134532893414689bf2f53/msgpack-1.2.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:73b0e05c32c3cfc3cd84994908e57430c0ebc6813abf905d3f18ff115d54df3f", size = 412343, upload-time = "2026-08-27T10:02:45.747Z" }, + { url = "https://files.pythonhosted.org/packages/da/e8/739a94197358a313307e6e9e7d8d22ef66add39222de911a44161aa96920/msgpack-1.2.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa1120c653b76d8eafa50423b5eba06b5c9737f8692c74fa3afe03e84b8978ea", size = 372620, upload-time = "2026-08-27T10:02:47.578Z" }, + { url = "https://files.pythonhosted.org/packages/03/d4/09b92e1fcdccea9466bfae45455367ac52362ae445d96a602e51b7a8df73/msgpack-1.2.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccfd880988f8438d1c91c77d7edc58e70f4d2012e999167bc154c64c6f06ea6b", size = 394603, upload-time = "2026-08-27T10:02:49.172Z" }, + { url = "https://files.pythonhosted.org/packages/47/db/d11bd6f258a60703dcdc7a3772818ad0c2f602ee4c2acfb24088c6c3ebc3/msgpack-1.2.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:6195257a107bf25872ef84aab7295078271eea3ac6413f0506b631f6c9586ed5", size = 372666, upload-time = "2026-08-27T10:02:50.886Z" }, + { url = "https://files.pythonhosted.org/packages/71/cf/fbbbac0c6e5fbb9d51abc23e3b5fe8620f5c01e0588797cf664a623bb9e1/msgpack-1.2.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b8dd6c71d20c28d2d0eb0c51e7cccf3584afde3b1364f6629596186c9025bd54", size = 410889, upload-time = "2026-08-27T10:02:52.51Z" }, + { url = "https://files.pythonhosted.org/packages/94/60/8366558da954095e04e7fbc351f9387d87a682feaee9a235ceda966f794b/msgpack-1.2.2-cp314-cp314-win32.whl", hash = "sha256:d242f3c4ccf55b056e6cf901720dccde58f1df117898f2bbf3bcd6e38ec7c248", size = 66774, upload-time = "2026-08-27T10:02:53.984Z" }, + { url = "https://files.pythonhosted.org/packages/4b/3d/1ce873c8057c65e4fbb076ffe1c99c9ae39d90a00a2540d7b06c652a292f/msgpack-1.2.2-cp314-cp314-win_amd64.whl", hash = "sha256:1510f24612d4b983dff6935d9273e02c320cfd525727fbcb58836a75f589fdbc", size = 73424, upload-time = "2026-08-27T10:02:55.277Z" }, + { url = "https://files.pythonhosted.org/packages/d5/55/e36f2a33e38657f33850d74e0bf256838a0d45802c298cc501a32bffcc08/msgpack-1.2.2-cp314-cp314-win_arm64.whl", hash = "sha256:7826f16edc763e768404f55605ef85dfcf5857e729c1ed29e0d7c180be4fe6d8", size = 67657, upload-time = "2026-08-27T10:02:56.493Z" }, + { url = "https://files.pythonhosted.org/packages/64/58/7e764b957bae80ae281a9cb28761068c8bae8d5c6ac0873e43cc69d176c7/msgpack-1.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f466049b8e1ec0854287bbe9a074316826fe0e08dcf707245f98b1ae49e92650", size = 86594, upload-time = "2026-08-27T10:02:57.796Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f0/250f5985b6ee533e60d357571a808aaae03c54118294dc3db7158e27feb1/msgpack-1.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f6b6f8deb07d49090e1808c6ef9cb7d23ca17bef3aa6ed3e5e03df16606e60c", size = 87374, upload-time = "2026-08-27T10:02:59.256Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2c/126ec8f187877c5f688631c543d1d3a3d75b2e66b83fb9de3ed7c13a39b6/msgpack-1.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b542ffc0a5c531eedc40419f291f1bd659aa8d4223408a5b51c88a2796083fd3", size = 428157, upload-time = "2026-08-27T10:03:00.9Z" }, + { url = "https://files.pythonhosted.org/packages/95/21/d2d81d50aaedb14147d01f22094185794db3ad8a8791b60afacba0627c89/msgpack-1.2.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d095df2627e5dd59ac7b0c5ad627a671c76e6020171e03cbe4621a61f0562c3", size = 426669, upload-time = "2026-08-27T10:03:02.457Z" }, + { url = "https://files.pythonhosted.org/packages/c6/fc/f7d484ee5b572719608e7ffad569bea22ff11309a96ca2fae85eec94226b/msgpack-1.2.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ffdd2f4950daf7815490f23087963e3420175b9609520b7ff5df64d351159c22", size = 380625, upload-time = "2026-08-27T10:03:04.244Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c4/b924cbd5516676f4e612329f18602a833bd055ffbe27f808eeba0f01bfea/msgpack-1.2.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:652d1bf13d01bac8fd569def0fe76745e55bcda01e30aa6332d5947ea3788839", size = 411328, upload-time = "2026-08-27T10:03:05.869Z" }, + { url = "https://files.pythonhosted.org/packages/27/9d/0c1d9683a951a80f270c3b7dac1022c18b9307617344dd44d904135d5e12/msgpack-1.2.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9bf452ff4d4981f25a18e9476e002bcc9263e7928024aa4d7148e25f7be3f929", size = 377892, upload-time = "2026-08-27T10:03:07.37Z" }, + { url = "https://files.pythonhosted.org/packages/06/bb/bf22338cdd22e0b40c8f28468cea5f3d9c320244c095d8303364bc012c41/msgpack-1.2.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:55faa6f8395e23b848c535ad5dcb96b3462f37f5e7f4ac500d500434f7345da7", size = 419426, upload-time = "2026-08-27T10:03:09Z" }, + { url = "https://files.pythonhosted.org/packages/7d/42/6d02c19a01abd8d7ce817c321d2ee6af1a8e24d584dca619d1b6576a83bf/msgpack-1.2.2-cp314-cp314t-win32.whl", hash = "sha256:419a45c67a5c04213172a14b1864657e014665b77d7081b107a51707923dd39e", size = 71810, upload-time = "2026-08-27T10:03:10.498Z" }, + { url = "https://files.pythonhosted.org/packages/af/df/fda3a204415dab0a8c0db5461ef7205416ea52bd8581c5cafd361be07f3b/msgpack-1.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:935b1cfad9b908b0fa845010f4271df4c2f04e1cd26e3f18acd61a45f93c9e36", size = 78919, upload-time = "2026-08-27T10:03:12.016Z" }, + { url = "https://files.pythonhosted.org/packages/63/d4/4b4b0ef25a86deca91feaf7252ca885ba4f2ada40461379120122a04fe96/msgpack-1.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11e8c421e117d1c36728b423d0402555cccbf0c6f53e288f0e75b6b12100d70f", size = 71925, upload-time = "2026-08-27T10:03:13.332Z" }, ] [[package]] name = "narwhals" -version = "2.24.0" +version = "2.26.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2b/1d/58946e5aab18393e793bd4add6985b95d0e01c3a2d832f38f54468b10dcd/narwhals-2.24.0.tar.gz", hash = "sha256:b5c0f684ccd9d7475b564111e319a4964abcf2baf79d3cf6b1003d06ac9b828d", size = 661143, upload-time = "2026-07-13T10:49:19.086Z" } +sdist = { url = "https://files.pythonhosted.org/packages/35/68/5351e34623d253423240ea7de3f8fc74fa8ab14b1ab3c0ec4ac8997413c9/narwhals-2.26.0.tar.gz", hash = "sha256:6b9cadca82f375c7e4cf584fdc86ca25da54827307a9c58f94547ee6104b82dd", size = 686970, upload-time = "2026-09-08T13:32:08.964Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/85/a5bfaebfd305ac18b57b0854d74e37e586809061a91fda62f0bd50c8518e/narwhals-2.24.0-py3-none-any.whl", hash = "sha256:42fdedf44e5b2ca7505630d45b4ac3058f38d8485cba9fe1652ca23152df7489", size = 461030, upload-time = "2026-07-13T10:49:17.571Z" }, + { url = "https://files.pythonhosted.org/packages/40/b5/1b84b2c784db76d69442334bc8b8748c840f13ca53be086f4f250ad4a0bc/narwhals-2.26.0-py3-none-any.whl", hash = "sha256:29326d74f107c347fd1009bd58e38d9f7c7c5b51e6de97bc93dbc325d9038b54", size = 474034, upload-time = "2026-09-08T13:32:07.159Z" }, ] [[package]] @@ -1275,7 +1422,7 @@ wheels = [ [[package]] name = "numpy" -version = "2.5.2" +version = "2.5.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", @@ -1285,51 +1432,51 @@ resolution-markers = [ "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] -sdist = { url = "https://files.pythonhosted.org/packages/9a/80/db0b4559e57ec36362bedbb05530a87fafbcb6067708c946967a41d449e7/numpy-2.5.2.tar.gz", hash = "sha256:d482d171c406ae88c5b19cad3b6a1c4c5209f886ab74bc44c2c865c23f52d860", size = 20773161, upload-time = "2026-08-09T13:48:27.962Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/69/72/dccb0aaf40972777283303919f613964227266d0c13adebb79ac124f1c3e/numpy-2.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:14e373cfc6387177e8409dac3c7159be8eb05cd77096cd7c950268b86f62831c", size = 16891693, upload-time = "2026-08-09T13:44:51.702Z" }, - { url = "https://files.pythonhosted.org/packages/60/2e/b5aee50a1f74ac815cf8331812cb8251e29024025de462e0c047641c614c/numpy-2.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4bbd96c833ecc8cc069ce518078fc8c60cb9cbfb0fea5b7a803ad65035596d03", size = 11903109, upload-time = "2026-08-09T13:44:55.501Z" }, - { url = "https://files.pythonhosted.org/packages/f3/f4/29e78102a80601cf034d4e9767022cffeca2c3b4c926e1754572ca95593d/numpy-2.5.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:6e8172ddfcf5cf74b811d372b570b83c60bd2de87a6fbfbebdadb4a9bd9c6cbb", size = 5350202, upload-time = "2026-08-09T13:44:58.401Z" }, - { url = "https://files.pythonhosted.org/packages/11/4b/dcd3b7eadaf4035d2c7a4289d232523a6964f602598ef7674e4bd7291f93/numpy-2.5.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:65f188481f1669e26f62b701e8205d19e460fa4a9b52a1414ba382330e4a3414", size = 6687736, upload-time = "2026-08-09T13:45:00.813Z" }, - { url = "https://files.pythonhosted.org/packages/e5/21/4947e0e9d6c9fc2e2ff15b8949049ee44f63adb9cacc729ab8793f97e712/numpy-2.5.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ee9c4eeb8454b3660a8b53493563c3e121c2fc94fbd72b848ef814ed7b676a9", size = 15612696, upload-time = "2026-08-09T13:45:04.151Z" }, - { url = "https://files.pythonhosted.org/packages/3a/5f/62d28cf019460c7f1394105b4d49d9911a9c444cb77ab0bd95a204c5a6de/numpy-2.5.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3cdec01fa790a186d430433fdd4d4ffb70eed6f0eeb4bf05c8dbe2dce0a9bcb8", size = 16722264, upload-time = "2026-08-09T13:45:07.714Z" }, - { url = "https://files.pythonhosted.org/packages/14/25/3f0be4c1b9fdf5dd5e708a6806978564d7c46a055c000496309ff2a2f8af/numpy-2.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7999d4ddb0c4025018373fd787510d46e04c769467af22869707b3c1cfd459ab", size = 16974396, upload-time = "2026-08-09T13:45:11.316Z" }, - { url = "https://files.pythonhosted.org/packages/22/72/6262cbdeeb45da9d971e40715f579d791603ba8ec0b5e2db1ac55454421d/numpy-2.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c1f017dc0875c9209d219f97feceb7d54c2661bb243deb4114478e1295808af7", size = 18476044, upload-time = "2026-08-09T13:45:14.869Z" }, - { url = "https://files.pythonhosted.org/packages/36/33/29208b8b075bde62d26a81d14b358c42b0f69b6cabd98d4ff97f37f22b05/numpy-2.5.2-cp312-cp312-win32.whl", hash = "sha256:d6a48072864e3324e194a8fbb3c657bcc5b5c869dbc64c9537b1d5c862572c0a", size = 6072817, upload-time = "2026-08-09T13:45:17.867Z" }, - { url = "https://files.pythonhosted.org/packages/7f/b9/87fea2769fe1c47c1b5b01d8310772c9d1a85d485de7cf386ef7a3332b02/numpy-2.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:28ac63476ec7651484215ee7fa15a1f78b57c14621f01e392afe17b9a1390ce4", size = 12464674, upload-time = "2026-08-09T13:45:20.734Z" }, - { url = "https://files.pythonhosted.org/packages/14/52/032b97e00461ab0809bbe4c588b035620e5a14b8cdee47ecddefc7b17d33/numpy-2.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:27650bb0e7140fa3d37b9923b4803645e0b125d190f326eecfd3f4dad8e8ade1", size = 10397131, upload-time = "2026-08-09T13:45:23.73Z" }, - { url = "https://files.pythonhosted.org/packages/f5/d2/6b24738a0ef4557d189b150046cd07823c50e4273e8aebd651222e24306f/numpy-2.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8e4cb9a754c8a0c62eaa88273a5fba3391f4a610d1dee893c0755da31c083f15", size = 16886595, upload-time = "2026-08-09T13:45:27.323Z" }, - { url = "https://files.pythonhosted.org/packages/65/60/f2d208d366f263f39c6e69ed309290717aab41078b6d04c9be2a84fa2a07/numpy-2.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:52c808f96484f5571a5cc863775ce50247c17dfb3b0361f8ed6b4b0456f80080", size = 11896845, upload-time = "2026-08-09T13:45:31.638Z" }, - { url = "https://files.pythonhosted.org/packages/3c/79/81e0bf24f4d020a2b1d5cd297a9f60c3f24eeb116f9bba5870443f7b6a4a/numpy-2.5.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:29d81e97f668489cba8ebfd796b9bdd453525d35dd9e162e2daec94bf3fc7740", size = 5343880, upload-time = "2026-08-09T13:45:34.373Z" }, - { url = "https://files.pythonhosted.org/packages/ba/cc/e3141cf06d1a8a2c7e107543fe1269c1d1af760d4d683c0794a4ee1127c2/numpy-2.5.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:afb3f0632d6b2e3ba04dbce8d1e48d321b369138b73830b5ca371a0e8d479d56", size = 6682264, upload-time = "2026-08-09T13:45:36.7Z" }, - { url = "https://files.pythonhosted.org/packages/29/f1/2a64a307d92c5d98f5255a4014eb43bb6103ee477087b61ecae44a3aa9b9/numpy-2.5.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0aadf13b60048d501e05fa699efaf7734e2494f3498a4c2a5521d822640324f3", size = 15609566, upload-time = "2026-08-09T13:45:39.518Z" }, - { url = "https://files.pythonhosted.org/packages/7b/44/59a1eb68e773c4098d107ef34a0dbdeca501d72ffcfbff9a7707343921ce/numpy-2.5.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29b86ff8a6cc556b47ec6b64b194815cc80e6bf5eedcc6cddfd65318cb0b4eee", size = 16709995, upload-time = "2026-08-09T13:45:43.661Z" }, - { url = "https://files.pythonhosted.org/packages/8a/4c/3e54d4ddbc359a1295f8b633e8106bcd4d7d4a206e82df051bdfb3058755/numpy-2.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6950c4b7dd562453090548ba7f5da7e59f57f85663f15d5dcc60e249192f7e59", size = 16972511, upload-time = "2026-08-09T13:45:47.094Z" }, - { url = "https://files.pythonhosted.org/packages/f2/9f/02e371638ebf19b66d46231e4be52999e87f32d1961b113bc45656608b22/numpy-2.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b9727f472d2f3888053b8a75ab0cb94745a9de224bb5846dbadc0092101bc71d", size = 18465609, upload-time = "2026-08-09T13:45:50.808Z" }, - { url = "https://files.pythonhosted.org/packages/eb/ae/ad6645abc7a3510fe48e8ea1ab4598166f500057ef4ebf38bfad4f1577de/numpy-2.5.2-cp313-cp313-win32.whl", hash = "sha256:4f9744f9fbdcea0bc552e8f19e1f141f811a3f9bc2be2cc6e86d982cab23e3f4", size = 6070204, upload-time = "2026-08-09T13:45:54.111Z" }, - { url = "https://files.pythonhosted.org/packages/15/20/f3489f86d81ea460b2bcdceaed094142ca6579f6be0ec527b781d39afe68/numpy-2.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:85aaccb24182c25df891ad0ec333585967e115269d5f1b17f2c9ae005bc96657", size = 12460532, upload-time = "2026-08-09T13:45:57.167Z" }, - { url = "https://files.pythonhosted.org/packages/d5/21/35b31dde1b283b79de828b80f876afd8c94e28fe1e9c375f89e261cc4c0d/numpy-2.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:bd68ece1553d2023c09a4226d9e41c586ad2d20594d1a456186c33513d2cb3f2", size = 10396725, upload-time = "2026-08-09T13:46:00.478Z" }, - { url = "https://files.pythonhosted.org/packages/ac/f8/c3b222bf075b50afd8e949a07a15c4b312a4a84bd8102a332bcd953cbbb4/numpy-2.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d787cf769c3baeb5f6235e778edb52c08dfa923789b5958f28e6450f96107cb1", size = 16885180, upload-time = "2026-08-09T13:46:03.939Z" }, - { url = "https://files.pythonhosted.org/packages/17/e1/2c1d4b1987795a92b5bbf7c24fe249ab96aa2573ab0d7604802c189d7b86/numpy-2.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:24b9dc2e3d84aa58523798805194e23e736f3f6ce2d1a5b92583ae734e6dbda8", size = 11907878, upload-time = "2026-08-09T13:46:07.045Z" }, - { url = "https://files.pythonhosted.org/packages/b9/ee/d08226fc858044355983a6e5b94f08ff6f3969e0a2b160a4a89f0ddb3445/numpy-2.5.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:9e9413326d726c2545bfa65d2c0876871e8d8386e77f992c1d426e180bbd4323", size = 5354922, upload-time = "2026-08-09T13:46:10.04Z" }, - { url = "https://files.pythonhosted.org/packages/94/f0/6d3d933056440ebbc5e6bad92065fc6c26a48a84a36b1208580e94eea76c/numpy-2.5.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:60e902ac295855348a5ca2ea4c89108989a9f5fddfad3dfc0a8f36b10358567e", size = 6679168, upload-time = "2026-08-09T13:46:12.275Z" }, - { url = "https://files.pythonhosted.org/packages/c4/3b/ecd49dd90033cceb2704d88ca905d4d7d89b0e8c739608754ffd325fa820/numpy-2.5.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50e500dc868e9313530ce12ba470fe50ff3afe3d62993ed6eff652dacd555b65", size = 15624501, upload-time = "2026-08-09T13:46:15.322Z" }, - { url = "https://files.pythonhosted.org/packages/c7/99/461bd36dbdfac6c1c53efa370bd55a83227542d0d118f1677dbf1a3dacd5/numpy-2.5.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318b9a4c845dbea06708a29c84ee429cc3065048db34cdb799047643492050ee", size = 16713701, upload-time = "2026-08-09T13:46:18.949Z" }, - { url = "https://files.pythonhosted.org/packages/f9/9c/2b251df9e8a5d647b62b0cbc1b90a91850c1cf4859ecb532fd0b4eacff6c/numpy-2.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:34c319e2963be042673fb46570501b2f06c41924e17e3563d58646b4380dfb68", size = 16986065, upload-time = "2026-08-09T13:46:23.006Z" }, - { url = "https://files.pythonhosted.org/packages/8f/25/20de43f53ff1390534a124475055a19f01fe10c920a0fd11b8e18d6d6052/numpy-2.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f06571a052127dc1b4e8b83029b4d1b20daa2b64a31cdd181fc6bc774e9000eb", size = 18470031, upload-time = "2026-08-09T13:46:27.102Z" }, - { url = "https://files.pythonhosted.org/packages/56/5e/0c577ca308d6da5eb79b546ba10bbe5b60148192194e2da060913b1de4f1/numpy-2.5.2-cp314-cp314-win32.whl", hash = "sha256:2cc779226e476d1e1f08c74068c419e60f41a9e0e069c92f6671d31d5c985e98", size = 6121028, upload-time = "2026-08-09T13:46:30.046Z" }, - { url = "https://files.pythonhosted.org/packages/15/5c/7bcbd5b11f94199073320410cddcbb80cee62415bfeb540874b265c2d922/numpy-2.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:7587f53dfbd5edc0f7b87c6217b4c6d2d1f2ef9c3da70bc1315e7db5f8d7ec9d", size = 12597627, upload-time = "2026-08-09T13:46:32.886Z" }, - { url = "https://files.pythonhosted.org/packages/87/bc/4d0b06fba0da90ccc75af62823cb9dcedb6c9ea0cffa058cb2c9ee773a77/numpy-2.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:3e4c367352d3747784248a227fbec218e193b56f7e6692e3b64fc805478ecfdf", size = 10680414, upload-time = "2026-08-09T13:46:36.036Z" }, - { url = "https://files.pythonhosted.org/packages/cd/17/f429aac9dc08833a0d0f188eba38c532a751b1a1f2ca6018a37b455cb321/numpy-2.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b879fb674276e331513fb136b78dbc6bd3c848309e0d841cfd63be3896c4cfc1", size = 12026967, upload-time = "2026-08-09T13:46:39.084Z" }, - { url = "https://files.pythonhosted.org/packages/ca/9f/d0849de96a2a4ceaa16662f18ee13eaa9c0aa418269fdc8c4857c56b11da/numpy-2.5.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:fd0d703772bba096843785bd38371e31bb4a0c1151497ad5739d182114a73f7f", size = 5473874, upload-time = "2026-08-09T13:46:42.075Z" }, - { url = "https://files.pythonhosted.org/packages/89/3c/8df216d4a4a5422a3de045301cf7df8ea47286d76f5cb7160b0128ac26b7/numpy-2.5.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:3a2f061cebd9e3d23bdcfaaded5e2293a4c6a5b60fa42df85d410a725ce621bf", size = 6789276, upload-time = "2026-08-09T13:46:44.387Z" }, - { url = "https://files.pythonhosted.org/packages/e6/3a/20d7e9891c4ddfadd6ff8d95bf4b29f353d8e1770553de2099880551dfb9/numpy-2.5.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6df895598c0edcb41030126c89e0f353b07d93238116143b7405e937359736c4", size = 15659154, upload-time = "2026-08-09T13:46:47.538Z" }, - { url = "https://files.pythonhosted.org/packages/aa/d6/f3aa3d2688bf501b858835c6bd087ae9b51a56ae6fca8e2b0990abd177af/numpy-2.5.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1ab3d4a901f844ea836c3e80bf463c6a27d7f3c14e8e292fcf28d348b25b9bce", size = 16748909, upload-time = "2026-08-09T13:46:51.442Z" }, - { url = "https://files.pythonhosted.org/packages/7d/8f/1c5cae8d2baf86ab802ae97a00be55bc7e21ebc11b12bbc33376c5f05342/numpy-2.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:cebc2d6dbb605a7703d59751dea4bd6b0ab127a5a4338a6f432df1936fef8b26", size = 17027685, upload-time = "2026-08-09T13:46:55.095Z" }, - { url = "https://files.pythonhosted.org/packages/5c/27/71d3467404aedc1c24ce79610f91b52b0b0f466c43a701aa56fc75c145ab/numpy-2.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eaca7ff36f0f52e2111ec71f169d8fd3e889e7ddc0d2592e0d703fd8d3ce8fac", size = 18501181, upload-time = "2026-08-09T13:46:59.09Z" }, - { url = "https://files.pythonhosted.org/packages/14/2f/42921d27c40aea7e077f4a423ae509fd9220b028cd787bafefd8ab2b3a5f/numpy-2.5.2-cp314-cp314t-win32.whl", hash = "sha256:ddf47472af2e4280d79bac82304f5e80150211f1b9e614b760061d5fdfbb6eba", size = 6271085, upload-time = "2026-08-09T13:47:01.903Z" }, - { url = "https://files.pythonhosted.org/packages/75/e6/bad5f5d56de9b1971bac959963dda276d35c40f1854475005434bbe08692/numpy-2.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:44ef9675d908e65f9953063837c3277730f3f4437615a4cdab67b366cabaf884", size = 12787971, upload-time = "2026-08-09T13:47:04.963Z" }, - { url = "https://files.pythonhosted.org/packages/df/05/f608795cb34391acd67e38d94a3c36abd8d8576293a3a80727d7595c372c/numpy-2.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:eaa088384c46f519dacb93b7ec483a6d6b19a4a2085ae4f25ab9b1c43d387d1e", size = 10750306, upload-time = "2026-08-09T13:47:07.976Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/13/01/11703282db468b85f6f7b8c7f22d058de5970d5c7e60a3a8aaa313c3de36/numpy-2.5.3.tar.gz", hash = "sha256:df2d5874ff183595a4ba404edd04f6bd9b5505c1d7708573f6a6c17489a67563", size = 20791231, upload-time = "2026-09-06T16:27:47.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/50/8fdbb16af64895706a45f06a4068e29db732ec180f3c1375f14123359138/numpy-2.5.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cb189f09db39283b26bfd061ec16189e14f71c6755207f72a0f7540867afe5b9", size = 16994982, upload-time = "2026-09-06T16:24:29.244Z" }, + { url = "https://files.pythonhosted.org/packages/60/39/789131c1188c078dcb3a1692e72e1e050c68b88ffe72c9ccaac9bcd7a9cd/numpy-2.5.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f59a878c33d6b88122d80d239bb3b845d58708750b0cb06a09aebb9b18ec696c", size = 12009327, upload-time = "2026-09-06T16:24:32.491Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/a312e95696e5f601914dd8b6dd844692ba61670807417e24b68e337b5c70/numpy-2.5.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:a72f874bc9e10e4b8f80426fb49716d5141f64442a0c8418065093ec8017fbb0", size = 5445405, upload-time = "2026-09-06T16:24:35.071Z" }, + { url = "https://files.pythonhosted.org/packages/30/d0/5623a1707ed4fe16e3909fe3cf5ee3da004ae677ad23d83bbf3adf1a6faf/numpy-2.5.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:fc36dc566135b5eceec4cf89758fcb719266a019ef07dae1754ae7c9f617ef3e", size = 6783213, upload-time = "2026-09-06T16:24:37.253Z" }, + { url = "https://files.pythonhosted.org/packages/f1/32/84146fc020ad3c25f805f70ab60da46fe3c540a21369754a7e4369754b6f/numpy-2.5.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76c2c1e6bfa5c84adc6434dfbf013aa92096a7985221762c8f11fedfd20fff58", size = 15687872, upload-time = "2026-09-06T16:24:39.751Z" }, + { url = "https://files.pythonhosted.org/packages/65/af/aa78d1a88805456e212b65461354cd943197fb9acecc4c90fd12295123a3/numpy-2.5.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7e18c623bb5c95acb3b3328861272816ba199fb531921c5d6d0b675f1fde9e3", size = 16717410, upload-time = "2026-09-06T16:24:42.745Z" }, + { url = "https://files.pythonhosted.org/packages/3b/24/faa79d865e69a97ba17473b23a1b74094b2259c03e820c70297293b9ea49/numpy-2.5.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4f8929ee6c96bfbd7b4ed2032e0c03af86fe1826740ab61ddabf9072d06e57ff", size = 17040975, upload-time = "2026-09-06T16:24:45.961Z" }, + { url = "https://files.pythonhosted.org/packages/62/4a/8877e629445a7176297dffcaf9c485faa96a95d81728a62521ad55bd4c0f/numpy-2.5.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b5d93cf48f687479941d12b69c873ad2cc76bbd487f0091c2200636497f34034", size = 18476479, upload-time = "2026-09-06T16:24:49.35Z" }, + { url = "https://files.pythonhosted.org/packages/c8/db/35e1c2d38b04cbd5b731f9d71495e055e813197669d22b612f11748d2ff9/numpy-2.5.3-cp312-cp312-win32.whl", hash = "sha256:bf63afbe037eb5d2fe87fbcc7778e61da53ebaf21d938a4515aa73b62532a5d4", size = 6133378, upload-time = "2026-09-06T16:24:51.915Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/accf6d4f0c80c5d9ba9735d6b1550e444180599f34dec69ca01360f717ad/numpy-2.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:0a59a421a32580a009e8a1751345bf829631b990dc1794b80514ab722b435def", size = 12567828, upload-time = "2026-09-06T16:24:54.255Z" }, + { url = "https://files.pythonhosted.org/packages/22/43/1764aff32e4652526ae2f71fa8b3efd8d25c8a3d6926914454e47138ed1e/numpy-2.5.3-cp312-cp312-win_arm64.whl", hash = "sha256:ccb32e0525d29e8b0572eb84c9a57af0e7a4e615726927506f55063c62414034", size = 10485432, upload-time = "2026-09-06T16:24:57.278Z" }, + { url = "https://files.pythonhosted.org/packages/79/e5/8fb89cd46d14e35699d13bf943a5f5f441ecee8667120a1f6105ab89e349/numpy-2.5.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:66a78fe4556c60aceda5916f9eacd638b18e9e681016ec302dcb4682d6d4d034", size = 16991061, upload-time = "2026-09-06T16:25:00.411Z" }, + { url = "https://files.pythonhosted.org/packages/2f/06/9dc9e48b5e5e941c8b10350c5ff2d721da42a20517d911d15544246775ff/numpy-2.5.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92f30e89b8ee0ecf363033576c422b2f58fed6a80bed0aa48dff6d14c654663e", size = 12003676, upload-time = "2026-09-06T16:25:03.475Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2a/98282aa5b8f58b1157d440bb6282eed47e3632a5de53a714fbab17e659fe/numpy-2.5.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f9a2353b37a1a9e78fd82b27ad7e2a32a2d036604d18f02b05e3136c62ca3b09", size = 5439695, upload-time = "2026-09-06T16:25:05.978Z" }, + { url = "https://files.pythonhosted.org/packages/a1/f9/b6533d777be9d6ffd29dc1be0867e563e6e8cc9a220ff1b716adc317f060/numpy-2.5.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:ccbc4665079665c3cf3bab4db9f6b095370cd6437d66be549b6c2a1fd19e1958", size = 6779395, upload-time = "2026-09-06T16:25:08.599Z" }, + { url = "https://files.pythonhosted.org/packages/73/85/735720d04ec197c5dcfacdfc9922667c7f1f5f496a279b7ba4d7c74c4cc7/numpy-2.5.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c76d5dde9f445058f83d0c02af00557a4db91de9a9a57c0df87d1535001d654b", size = 15681750, upload-time = "2026-09-06T16:25:11.173Z" }, + { url = "https://files.pythonhosted.org/packages/3a/1b/3b16a9bc514a440a7a0883684111dcb1ef1aee960af2ca95da8fc775f124/numpy-2.5.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5fa86b80fd24bcd1aff83ad23be44ea323de3f787be8f8b15d4a65621e25321", size = 16708577, upload-time = "2026-09-06T16:25:14.171Z" }, + { url = "https://files.pythonhosted.org/packages/69/c4/386f397831b07328b639c96c5b62719346cf4baf07c68d927239752b1534/numpy-2.5.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd4cb9ad3c7889b9b3fe0a9a9fb5d2ed26f9879bff2608d9f01aed147a20d231", size = 17042047, upload-time = "2026-09-06T16:25:17.582Z" }, + { url = "https://files.pythonhosted.org/packages/5f/3e/a700ecbf36e85ae8328fd3b0e12eeddc22ed6358a64cb2bd913e0d195d65/numpy-2.5.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1302b90c0e52281681b2975adfe8a860cb7b12216a27b4b0b4207c44bf7bccf0", size = 18465724, upload-time = "2026-09-06T16:25:20.949Z" }, + { url = "https://files.pythonhosted.org/packages/41/ee/38e785e88a4045f6ad1d1f2808dcdfafdca48c760260c0587bf171e29fc9/numpy-2.5.3-cp313-cp313-win32.whl", hash = "sha256:1c80eabb4035ecf4ca9cd49cde8a9fdd69a729e63e6474887d1523ade7aa277f", size = 6129003, upload-time = "2026-09-06T16:25:23.664Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ec/100f2b1794ede74a9b3d7ec6b9736927f56713414c1dfe19ab6c383494bf/numpy-2.5.3-cp313-cp313-win_amd64.whl", hash = "sha256:71cad2b2a7451ab79d8f5e71b453485b6775963d5cf794179144a7463fe6e8ec", size = 12560965, upload-time = "2026-09-06T16:25:26.602Z" }, + { url = "https://files.pythonhosted.org/packages/80/b1/7dc825ca94c12acebbce4c37caa5e198695eb31424bc579679f32b1bb49d/numpy-2.5.3-cp313-cp313-win_arm64.whl", hash = "sha256:8e4dd766076855b5ff7ea52fa5f07ce26286726e0f8bff446b7739d02e6ea204", size = 10482343, upload-time = "2026-09-06T16:25:29.772Z" }, + { url = "https://files.pythonhosted.org/packages/70/78/cf416f15dc29375a229d9dfebf8db6e313f291580b39fa1a568b6052bb07/numpy-2.5.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:350ba9783ce969cf9f7ce6e6a9a58e1a6e2a19ca025b7ee448c4db727706212a", size = 16998686, upload-time = "2026-09-06T16:25:33.171Z" }, + { url = "https://files.pythonhosted.org/packages/9e/59/abcc2d8def4fd60eec7d87f92d27c13448ffd9ab14339bcc63a0d7a2fdea/numpy-2.5.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:012e66aca395d795496446e52aeeb5866312a5d4d3f27da270e5a0b43f70dc5c", size = 12013862, upload-time = "2026-09-06T16:25:36.748Z" }, + { url = "https://files.pythonhosted.org/packages/94/75/4640d2d6e4b64a049e48425a82728a41ef4adb61332d2cba68055774878b/numpy-2.5.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:adc1ada2662f8a5f960b8a10d9986897e7499ef07e06d4cfe7197f8cce923c07", size = 5449793, upload-time = "2026-09-06T16:25:39.476Z" }, + { url = "https://files.pythonhosted.org/packages/96/cd/625b57ae33d4ca560f32cc0b47b4a5922146d9beb998ddf773900d440a73/numpy-2.5.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:54a115e5a73b8fc44f0cebef486365a1894b5c9760685d4558b72b7c3eb846e0", size = 6785176, upload-time = "2026-09-06T16:25:42.069Z" }, + { url = "https://files.pythonhosted.org/packages/9c/72/12918652e7912ef9751e8694c88820fcd1908e0618cb23f5f3caa6004b7b/numpy-2.5.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be5a8381859b6da607c84f4f7d6847725f1cf1853ef8a2c9e115b7d58bef47dc", size = 15703377, upload-time = "2026-09-06T16:25:45.135Z" }, + { url = "https://files.pythonhosted.org/packages/45/8f/9beacf79ca7c650688ad0baa80931adb988fe6e6e5d5903c23cc3dbd70eb/numpy-2.5.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b0521d0f4aebb6e06189451025fa17a913287b13c03d5fe05c017333b654ea5b", size = 16711928, upload-time = "2026-09-06T16:25:48.461Z" }, + { url = "https://files.pythonhosted.org/packages/09/8d/41d0a56e1ac4c87495c897a211b1368691b7237aadabec8b3b8f3a74d48f/numpy-2.5.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9deb49575e5b0b94ed72c8a64ec4d033381adc27e9060ae842971f697ba96104", size = 17059507, upload-time = "2026-09-06T16:25:51.873Z" }, + { url = "https://files.pythonhosted.org/packages/08/1e/0dfbc5cc251d54e2af790f254d24ec38637fa97ec7d5d11de7ffed787098/numpy-2.5.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b00eefbcf0f292945c4b4dec2ae845389ef5bcdcd596e6e4328051db5b5ba694", size = 18471002, upload-time = "2026-09-06T16:25:55.233Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2c/dfa40f6991f8185c8c30ffd023dfcbb11888e823cfab9557b920f3bb7bed/numpy-2.5.3-cp314-cp314-win32.whl", hash = "sha256:c2381f82999704f818e2c987a865050e285ec3621262c66d40f5a96c8f899f8e", size = 6180485, upload-time = "2026-09-06T16:25:58.157Z" }, + { url = "https://files.pythonhosted.org/packages/a4/73/d2c08231e4fde7e415501fd02c715d96e98599b2d8384445933944152984/numpy-2.5.3-cp314-cp314-win_amd64.whl", hash = "sha256:2c25dfa72943e4336ddb6b0ee4277b47a0c85bede0807530ec68103bf58e2c10", size = 12698179, upload-time = "2026-09-06T16:26:00.789Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e9/dcdcc9b95cf5f49815055573aee1b11cfbf5299f38a180e437ded050810f/numpy-2.5.3-cp314-cp314-win_arm64.whl", hash = "sha256:15aa985ac73a8db02db7663381aa109510449d3819d37206caed27b33a65a8a6", size = 10769383, upload-time = "2026-09-06T16:26:04.011Z" }, + { url = "https://files.pythonhosted.org/packages/49/c4/af8bc08a7ef4e1529a7c0cf24969accce316b783999802089a581ec99272/numpy-2.5.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ac7bb1c52d445bd4f8f7f97fefe6abc3a084dc4d63df50d79b17fa2b78e89297", size = 12132668, upload-time = "2026-09-06T16:26:07.138Z" }, + { url = "https://files.pythonhosted.org/packages/c5/ae/0f15eb56d4ec5e13c1f7ff04ff407f997d1acbadb45d3e1f2e2645a8f43c/numpy-2.5.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e6ab667ba76450084eb64013762c438ea76d9d29cc676dcd6c2e9892ba37f841", size = 5568580, upload-time = "2026-09-06T16:26:09.828Z" }, + { url = "https://files.pythonhosted.org/packages/23/fb/c72a8f25d4b6e96c354e7ab45ace3b27dc11e5d6a13b6c7d0cd6b08bf112/numpy-2.5.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:f7fabeb6cea87d65f3b926de33d03fb016cfdc29314c90974383b5582ae72891", size = 6882634, upload-time = "2026-09-06T16:26:12.524Z" }, + { url = "https://files.pythonhosted.org/packages/07/a9/968c90ed2ab15060c338e8137f1215b5a60756ae07328e0a60d1c6734df4/numpy-2.5.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fb6f8fb9ff0b3a69f52c66ce397b0246583e9f28616231b0e32ca49259a5fa6", size = 15748923, upload-time = "2026-09-06T16:26:15.092Z" }, + { url = "https://files.pythonhosted.org/packages/59/08/9df04103947b95e3b6b1f2ed1a70521f325647a31b82da6a2aae3a485508/numpy-2.5.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93e1f5447e2b1e479d7bd74701e84746b86450cff1fc368b132d195e2b8f8211", size = 16746748, upload-time = "2026-09-06T16:26:18.43Z" }, + { url = "https://files.pythonhosted.org/packages/41/a0/14c8d5fe5b53a334aabb653deb391c0fef49558f491880ea300ed6785224/numpy-2.5.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c00abe94c1a69d75d827dcf1c025b25c8a45d230b3bcd77a9020883a1b047653", size = 17111561, upload-time = "2026-09-06T16:26:22.113Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a6/d7e96e42f01522e154c32489640f16dfc4f6181d165d05fc3bec8c2c4999/numpy-2.5.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:536f963710a4e63934d80ac0dc4f478804a83e9a84b6828018f25d09953ada33", size = 18513945, upload-time = "2026-09-06T16:26:25.401Z" }, + { url = "https://files.pythonhosted.org/packages/25/39/3453afb7119d0449ef11c886874120ff180e2c337760e0e2d88f70f1a945/numpy-2.5.3-cp314-cp314t-win32.whl", hash = "sha256:4c8a6d2ebce6305fd82fbefca827775437147052a976ee7c94b36a0c1b52ac6c", size = 6335421, upload-time = "2026-09-06T16:26:28.175Z" }, + { url = "https://files.pythonhosted.org/packages/99/01/22815d2b19a1a746b1d45205cffebb3fe511a18acb75fba6c88491fc9894/numpy-2.5.3-cp314-cp314t-win_amd64.whl", hash = "sha256:9a37475425b431b4d060f23b4f52cd2f3aef6bc7c654bd760adf0040eec9d435", size = 12896420, upload-time = "2026-09-06T16:26:31.265Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ee/a7cbba67eeaff038dc29ca8b98a88396c8b0cc9c89d4924f4a27a5c9150b/numpy-2.5.3-cp314-cp314t-win_arm64.whl", hash = "sha256:2d8240cb4c16fd831074aa2b2cf9fc54664d826341d61c372245b96a74a49a9a", size = 10857177, upload-time = "2026-09-06T16:26:34.167Z" }, ] [[package]] @@ -1407,7 +1554,7 @@ wheels = [ [[package]] name = "pandas" -version = "3.0.5" +version = "3.0.6" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", @@ -1422,53 +1569,53 @@ resolution-markers = [ ] dependencies = [ { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy", version = "2.5.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "python-dateutil" }, { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/be/4f/5f3422a2afec5ffc46308b79e53291365a93748b498ac2e58bead0197916/pandas-3.0.5.tar.gz", hash = "sha256:dca3734d6ab7c906e6730f0788b0a1dbb9f2467731f9711f77995c8e9d62d712", size = 4658219, upload-time = "2026-07-22T22:19:28.819Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/48/ef/f1fd7431d635bf20015489bf0bd69c17fff1018de773540f651455a3916b/pandas-3.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2946e77e4a53cd248cbde631a12f0e51c8324ce354c3eba4d20147c1ad6f4282", size = 10397178, upload-time = "2026-07-22T22:17:48.274Z" }, - { url = "https://files.pythonhosted.org/packages/31/b4/0eafac990a431561187694126de01f9b12559549b4d86360c0c4bd870fde/pandas-3.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71ecc8fb7ed1a7aa4392316b5309a6347e8e7f832f38fd897846b3a1457a9298", size = 9990736, upload-time = "2026-07-22T22:17:52.388Z" }, - { url = "https://files.pythonhosted.org/packages/de/21/359880af3ea9b7cb23bea5b51e8e70ef3866c03be09da9a2787e18e330a8/pandas-3.0.5-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b173f5951ff6b8b0ec7675e20dff3c97b7e7a57dfcce387c2d7c5afe87cb7899", size = 10814438, upload-time = "2026-07-22T22:17:54.708Z" }, - { url = "https://files.pythonhosted.org/packages/d1/50/d6cc4d7e508bbccf5d6027314a8312bc7ac73d0ec7f195f53838daafab40/pandas-3.0.5-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2c0cf1dd9b55a22d105fc46c1b489af3bd42264fcba7c66297bf47a9a1d9c78a", size = 11323634, upload-time = "2026-07-22T22:17:56.858Z" }, - { url = "https://files.pythonhosted.org/packages/70/2b/d5f0a8c90dd0ae04e64ba53b871afb796ec026b615086d382ddc2ade729b/pandas-3.0.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0fac0010c75e4efb6b99e249c183a8993ce0dc95c240f9b120a5e67c727b7928", size = 11850860, upload-time = "2026-07-22T22:17:59.1Z" }, - { url = "https://files.pythonhosted.org/packages/5c/30/183aec2e19adf778a98d29b5729a0a68f4cc4ebf9b9c3b70d0297355bcb1/pandas-3.0.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:08d24fe11a17dc33bd6e937dc9c665f9cba08fbdc9f657f405713515febe300d", size = 12411100, upload-time = "2026-07-22T22:18:01.485Z" }, - { url = "https://files.pythonhosted.org/packages/fa/9a/31f4983f191af51ab2a8f2d0c7b33dff3a84da26533f982fff02c2f9e28b/pandas-3.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:b1261758dfb6cf12c3cff8300e21cefad30e7ec709abb4c24ac7318e6a52462a", size = 9968804, upload-time = "2026-07-22T22:18:03.903Z" }, - { url = "https://files.pythonhosted.org/packages/49/97/7886c89a39045c69ad82cbceaf3343810480c8ef49a216319ce8183860a6/pandas-3.0.5-cp311-cp311-win_arm64.whl", hash = "sha256:679f4e85b30ddb1515458ab1e788d3e260eae369b1f78da7a3aa4cac8ebf4a2a", size = 9205447, upload-time = "2026-07-22T22:18:06.134Z" }, - { url = "https://files.pythonhosted.org/packages/1c/54/1dc810ea558d1320b597aa140a514f2fdf1d2ea09c38cf556f13ea712ec9/pandas-3.0.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fa290c16964d4963fbfbc358928239cf3bd755b20e988ce944877def2f44471d", size = 10411717, upload-time = "2026-07-22T22:18:08.307Z" }, - { url = "https://files.pythonhosted.org/packages/68/56/fbe81c09195924d8b7b8d4461a20458fe80a6a5ed6b24f0314da684277e1/pandas-3.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c2e26bb46934b8a2ca0c3de1d3d606fc5f6746584791b2db264d58cf370e08dc", size = 9957095, upload-time = "2026-07-22T22:18:10.6Z" }, - { url = "https://files.pythonhosted.org/packages/e0/51/fac252f4a913ed5eabf3c11b880a9e8d5a6c10f0b2129d0462212d238b4d/pandas-3.0.5-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:73fa87b08a7ef706f8aafda39ddaccf2a99047bea62d8c88a0361bcafb2237bc", size = 10485458, upload-time = "2026-07-22T22:18:12.834Z" }, - { url = "https://files.pythonhosted.org/packages/12/98/e976540c1addf70442be7842a18cf70884a964abbf69442504f4d2939989/pandas-3.0.5-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d373ce03ffd84010ed9839fa73672a9c8256990532e158440c0085db7d914b34", size = 10998091, upload-time = "2026-07-22T22:18:15.209Z" }, - { url = "https://files.pythonhosted.org/packages/a4/8c/1f29b5be8d3fc47dd7567eb167fabba2085879b31e0287ce7cba6d3d2ff4/pandas-3.0.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2a29c53d85ea98c5e792c59ef82ee9fbe6ca902c0d0adb6b23f45ef894cd7bf6", size = 11499501, upload-time = "2026-07-22T22:18:17.689Z" }, - { url = "https://files.pythonhosted.org/packages/9d/e2/bd9c98ad2df7b38bde002adde4cdf353519da51881634323b126c55997f9/pandas-3.0.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a5ad3b02ed6bc7d7ae9b70804b2c6aa31827489d150f8e623ce82491b82085d7", size = 12060559, upload-time = "2026-07-22T22:18:20.147Z" }, - { url = "https://files.pythonhosted.org/packages/f3/9a/ffbd852d58bd74a617fe2f8ee6a58a96982271ce41cf981eab22190b4a4b/pandas-3.0.5-cp312-cp312-pyemscripten_2024_0_wasm32.whl", hash = "sha256:b2acb4650527eec6822c3dadb2b771277b65e7dae7a267d4bccf65fd1bb3fbce", size = 7197652, upload-time = "2026-07-22T22:18:22.502Z" }, - { url = "https://files.pythonhosted.org/packages/70/b5/d2d3e9ae73362ba4229651b0ee1455cf78073a1ce585f6ff693782ce263e/pandas-3.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:80a611068e8a3ac23f7398c6c14eb46dc974e5cc9997f653e2dcfd1da74edd41", size = 9831691, upload-time = "2026-07-22T22:18:24.534Z" }, - { url = "https://files.pythonhosted.org/packages/52/51/dea1e89d6a6796b9c43f85a09b484ee03edb8a4c4842e73e200a8c11301c/pandas-3.0.5-cp312-cp312-win_arm64.whl", hash = "sha256:25ff585b972a18ef1fe9ffa3ac6544d9950508aa76832e5147640b6022821e49", size = 9105796, upload-time = "2026-07-22T22:18:27.064Z" }, - { url = "https://files.pythonhosted.org/packages/bf/09/7b95c4a0025227d6f118c4039b423412ac6a982db02864166185d812fbc7/pandas-3.0.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c1c05a767fe8e5b4fe9e1c29806829c582052eaedb9120a3da83ba3f69e24a5b", size = 10385742, upload-time = "2026-07-22T22:18:29.346Z" }, - { url = "https://files.pythonhosted.org/packages/8d/0c/dc78fd8c4da477b4b5e8ad37295af352190d21ef63a9ee1bc071753074cc/pandas-3.0.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b86765f268b56f7e665b93bce9d5df69dee7f99e595cf8fb839483ab315942a3", size = 9932067, upload-time = "2026-07-22T22:18:31.833Z" }, - { url = "https://files.pythonhosted.org/packages/3e/71/3592c055cf44df9808550f9368ceda80ff2b224d355ef73fe251dcda1802/pandas-3.0.5-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c597ecf5616b5c420372c1d4d4c00dbbfba7398bea857dcc984347e1ea48417b", size = 10466756, upload-time = "2026-07-22T22:18:34.195Z" }, - { url = "https://files.pythonhosted.org/packages/e3/70/4363150359f95b4cb4bcbb34ca23572bb5495749a621a8f3d5a1ddfd293c/pandas-3.0.5-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b11c36e218331d0387cbe3a0a5f75162357a1d92d57b2b08a336ff94b19b2be", size = 10938525, upload-time = "2026-07-22T22:18:36.81Z" }, - { url = "https://files.pythonhosted.org/packages/f7/d0/317e7a0c67c0e69fa905a0161409397a7dc2d46ff611f6ca4803352c042b/pandas-3.0.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cf52e1f61d229496da17dc7ab54acdee627357e7008fd4fecba3d0ba2937fa58", size = 11489303, upload-time = "2026-07-22T22:18:39.287Z" }, - { url = "https://files.pythonhosted.org/packages/f1/8d/36dade89b49e4f9d5cbdbe863772581f98c0c6d78fc39ad4c557f6f2e17e/pandas-3.0.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:db172144bb56422bd157812f3b021eacc255451470b31e2c633c349490a1cfee", size = 11989004, upload-time = "2026-07-22T22:18:42.208Z" }, - { url = "https://files.pythonhosted.org/packages/9c/ba/18c4ec8a746e177da05a9e7a7963781d8ea195780724f854601b6ebd6b78/pandas-3.0.5-cp313-cp313-win_amd64.whl", hash = "sha256:0d298e951f23016ce4699951d044ae6418dbc91bf68cefca0f77666fcbb4e5c6", size = 9826896, upload-time = "2026-07-22T22:18:44.539Z" }, - { url = "https://files.pythonhosted.org/packages/de/ec/28a57266b753799a87b8bc79e7887ac6fd981b8c6d2978a0b7e7b6bd708c/pandas-3.0.5-cp313-cp313-win_arm64.whl", hash = "sha256:66266d3442a5e8b3c90274c2b8b230bee42dd1c286bc822cc2f9f2c7e12b883e", size = 9094790, upload-time = "2026-07-22T22:18:47.468Z" }, - { url = "https://files.pythonhosted.org/packages/51/2f/cf6aae281264f4463f0875bcbb15fd2bb6d291cc535187dad1732475e4a9/pandas-3.0.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2f264fc46911cc8131a7322a16199bbf8e353d27c10bb211f5bd0c814324dc36", size = 10390034, upload-time = "2026-07-22T22:18:49.818Z" }, - { url = "https://files.pythonhosted.org/packages/06/ec/5189518c7a7659c4bdcc6b1eb32c46c6f3c86b0661ffd84143d1112c7732/pandas-3.0.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:53730687fcd161883b24e10411c06d6a4c0f2275d2faf3bb2bc25deb4ba8007c", size = 9980065, upload-time = "2026-07-22T22:18:52.249Z" }, - { url = "https://files.pythonhosted.org/packages/ea/f1/598503ce8d7e3c35601e0747ba288c7864baae66380725bc12f13f884dfe/pandas-3.0.5-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:960d3ebcf249f75206899fcd2c6de53f736b7265759ced0d3e559df0b8b709b0", size = 10545532, upload-time = "2026-07-22T22:18:54.813Z" }, - { url = "https://files.pythonhosted.org/packages/fa/de/ceae2adf7034e07e9910299fe412e1819c4f0dd520700a888bcb03625448/pandas-3.0.5-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e94c2c5ca43bd3ca32bf64d32308887b65e5f9bfd8023ea52755107a999f93b", size = 10963120, upload-time = "2026-07-22T22:18:57.42Z" }, - { url = "https://files.pythonhosted.org/packages/66/25/86e0f4451874eb79e688deeebe3c451fec4557f8952005818d800ee8ac7e/pandas-3.0.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e819dd5f62966b481a8cb649d3299ebd886a1ea91ed5a99bf7ce77c98d18ab94", size = 11563178, upload-time = "2026-07-22T22:18:59.729Z" }, - { url = "https://files.pythonhosted.org/packages/f3/45/8643daa3b4147e433adfcccefdd0380d3aad79d86b15d8999730fe1944d5/pandas-3.0.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3c5ed2e7c06e91d340dfd091d7934f9bc82e4a36b95f647f090b9d1c9ac649da", size = 12028708, upload-time = "2026-07-22T22:19:02.164Z" }, - { url = "https://files.pythonhosted.org/packages/96/58/ad979ae617615576e8aafd569c9d4b62f1191d896e38f51d66ba06f3b89a/pandas-3.0.5-cp314-cp314-win_amd64.whl", hash = "sha256:cd8f7c6dc98527058ee6264219343f5392240a6f1bfa654fc5d79023020d0c92", size = 9951806, upload-time = "2026-07-22T22:19:04.596Z" }, - { url = "https://files.pythonhosted.org/packages/69/32/7ac03886b304049a9d2625ee88f59af760d8a93bd30ed9239bce7b9869a8/pandas-3.0.5-cp314-cp314-win_arm64.whl", hash = "sha256:5183427f5a8156d480f30333777bc978be93650a49a7c01db26adffe95b31e85", size = 9238297, upload-time = "2026-07-22T22:19:06.836Z" }, - { url = "https://files.pythonhosted.org/packages/be/ed/1d1f2ee5547d5167face2376d11c8b2a4c7bfff5a416ee7a9046891fab1e/pandas-3.0.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:303da736987d481074ca720ada325f8bd80c64ebc2d45ed79b29df3aaa4a26ca", size = 10849690, upload-time = "2026-07-22T22:19:09.391Z" }, - { url = "https://files.pythonhosted.org/packages/57/55/17e17152e98fbb0c4b1e562bc65387a2f20a80db0f4a86bf8d3a0e4248d4/pandas-3.0.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3b2801bbb049d0136f6c213eae02b5fca969384fc2064dd728d8620552aa49da", size = 10509945, upload-time = "2026-07-22T22:19:11.773Z" }, - { url = "https://files.pythonhosted.org/packages/88/90/817d44dbf83facf9556f33576d9af0a241981e7bb5c00606c0bcb5df8dda/pandas-3.0.5-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cce3a9d11d2b1f82c69a27ec1f4948a170e2c403c4bbfa8cca62e3fdebe2ef3a", size = 10392197, upload-time = "2026-07-22T22:19:14.024Z" }, - { url = "https://files.pythonhosted.org/packages/f1/da/889f00c0a6f5aa1545add70abbf01502dff87ab577adb855bd631c54d2f2/pandas-3.0.5-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef01af4d8dc6cd2c8d6c7736f149574ef93fe043811eeb5e445f2647154b5040", size = 10862726, upload-time = "2026-07-22T22:19:16.351Z" }, - { url = "https://files.pythonhosted.org/packages/bc/98/f1e934fb3c98fce859c6147c6785816c7b5b9ab7821115c5d8c4de9842b9/pandas-3.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e2759e890db96dfcffdbd9b86c3c2cb6afaf58def482820317e06163ec1066cd", size = 11414864, upload-time = "2026-07-22T22:19:18.981Z" }, - { url = "https://files.pythonhosted.org/packages/fe/be/d448af7d657d82e1888dd8551f79c6d6fb161080b5b9752d84d910ec2319/pandas-3.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b58b1b39d46a5862e3fb18f50d1a201398619d16a0f9f73f57eea5583cf0e63c", size = 11925105, upload-time = "2026-07-22T22:19:21.515Z" }, - { url = "https://files.pythonhosted.org/packages/29/c1/ccb4238212c8c4f496c584f3044d94e0c030ed8e1d68999db46c91c2242f/pandas-3.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:1c10461f6eeb35d8f05b6184c65c8b9991663b66c46b1d559b682cb34ae7c6ea", size = 10387612, upload-time = "2026-07-22T22:19:24.257Z" }, - { url = "https://files.pythonhosted.org/packages/d2/cf/6a51b2c38980e04c279fd2fa908a1b0982064e860444acfca4ec2e2c8359/pandas-3.0.5-cp314-cp314t-win_arm64.whl", hash = "sha256:3c5015fd1730fbf883647e88068176c839c102cea883ba1769a6f4593bfc1f8c", size = 9509776, upload-time = "2026-07-22T22:19:26.694Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/e2/17/d7b106e05bfa642e8694451e7d3d759c6a241c5386a5d962e4f66c047e06/pandas-3.0.6.tar.gz", hash = "sha256:66b07ef7315a31bfe1089cd3d71a7de781c9dca986762d0b4fe7c0ef17465d10", size = 4667686, upload-time = "2026-09-17T23:23:18.345Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/48/88e8d250d28efa8163294f6809a71683c7ee67f63ac7c33021c0503b3547/pandas-3.0.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:085e3786ae6b2e82b406266bce36690f72b9dc1421903ba9296b2981a9fcf586", size = 10391798, upload-time = "2026-09-17T23:20:20.96Z" }, + { url = "https://files.pythonhosted.org/packages/55/a6/39db5d41f3eb5d7846626312cb30e0cea48e988fc47a3725698bc931ad3c/pandas-3.0.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d7564d86a94c2eb8ab290b07f63ddaae5c032fa53897c29a2ff2197d43aee8af", size = 10023718, upload-time = "2026-09-17T23:20:25.094Z" }, + { url = "https://files.pythonhosted.org/packages/54/b7/707e966129f77ee8a39d41a41bbd989b790b3fef581c6e26b821315cba6b/pandas-3.0.6-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e7c0afdcaf6661d795fcefc2f647ddd1136f62cdc153fba177c685d97a87808", size = 10612800, upload-time = "2026-09-17T23:20:27.99Z" }, + { url = "https://files.pythonhosted.org/packages/63/be/dfb6cc9329d0bbe76dadda8a1113d026bb996ec76c509221368dc68349f4/pandas-3.0.6-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47121f9571503f724c9b93e297ab6254ac99c77adf5e9ed085ea419fd585c258", size = 11108900, upload-time = "2026-09-17T23:20:30.65Z" }, + { url = "https://files.pythonhosted.org/packages/1a/6f/3d58f15bbe972f3d7bfa13ee06d731785a76fe8d232c92b2655a8de14127/pandas-3.0.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:994a79608263fe1c14cc48ffa7300e2b834b7d1cb406ffe96a08828cb0cdd79b", size = 11630226, upload-time = "2026-09-17T23:20:33.582Z" }, + { url = "https://files.pythonhosted.org/packages/58/54/9b494de4a3dd92fc6eb19187db1b21afb50fdc21962f32ea1aca9f270cb2/pandas-3.0.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a3a22e07fe75347eaacc75b0e85297947af4fba6b4aae23916bd8b6828d0bba3", size = 12146942, upload-time = "2026-09-17T23:20:36.792Z" }, + { url = "https://files.pythonhosted.org/packages/d3/dc/d2df02854aec5d47659acfb2be352eecc691845b2f86e99c84f1010a8671/pandas-3.0.6-cp311-cp311-win_amd64.whl", hash = "sha256:2e5fa32ff162dfdbc280157d664f44d23049ae414725af9676df339c501d82cd", size = 9859246, upload-time = "2026-09-17T23:20:43.976Z" }, + { url = "https://files.pythonhosted.org/packages/79/1e/2a30df0d7dede5c195300a1820b0bc21cea3aa24e4c8b6c4431565ecc79a/pandas-3.0.6-cp311-cp311-win_arm64.whl", hash = "sha256:5e75072773c1b2f7cb63faa3a6f562aede11f3976f68ed34cb538bc091a28171", size = 9106923, upload-time = "2026-09-17T23:20:46.611Z" }, + { url = "https://files.pythonhosted.org/packages/77/4c/597d588c055d4373cff19cbbc32d4dd046c7be8fadee957585b5ba9e5b24/pandas-3.0.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7dac2d65e9087e8e7b5a45fe15c4920911a221df061ab629943ce016489145c7", size = 10397411, upload-time = "2026-09-17T23:20:49.465Z" }, + { url = "https://files.pythonhosted.org/packages/18/8f/48907c7c707b61a8e5018c32e1a3f70623209bfb59020a2f6196159d3aa7/pandas-3.0.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9dab635a549e58a053c7b0fa054dc0bd7be22f0ed9a720f4a85d5fb993276172", size = 10051279, upload-time = "2026-09-17T23:20:52.409Z" }, + { url = "https://files.pythonhosted.org/packages/67/fa/613d867c3d9554a61bafdec6f79565c8a3e73235feb52cc4a72ad2e0fa6a/pandas-3.0.6-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e3dccb584123b399c07562ac4d62543e90ede49ddf8ce3c13ffc64cbe828c281", size = 10303429, upload-time = "2026-09-17T23:20:55.597Z" }, + { url = "https://files.pythonhosted.org/packages/cb/67/0c0f18e38d7f2d2af8c24b3315bc4046e73bbdd4a5540405506671ad0c0d/pandas-3.0.6-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0704044b676496b8350e023b09f174a26772456c974a2b11c36bebb558c9490d", size = 10788193, upload-time = "2026-09-17T23:20:58.617Z" }, + { url = "https://files.pythonhosted.org/packages/39/53/1b57f3162501fe36687e4870e1b918a6458ca7386b173af75663ace95857/pandas-3.0.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e7c1905ef02c3d6d43d9dbd5b6ccb4da4870a0b0c821bbc103fbdb6f3ad2707b", size = 11323501, upload-time = "2026-09-17T23:21:01.911Z" }, + { url = "https://files.pythonhosted.org/packages/f2/d2/b1182e8d39100369d7f13f4a125a3fb6b096fef112c46d0566c25781ff68/pandas-3.0.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:569e114072b24fc4970c12e2b4bab252671668a40b324318903380cab0254c0c", size = 11838173, upload-time = "2026-09-17T23:21:04.85Z" }, + { url = "https://files.pythonhosted.org/packages/c7/33/5b717af24d2f27995e51e0875a269dddd373045216e34cf62e3aa764eaa1/pandas-3.0.6-cp312-cp312-pyemscripten_2024_0_wasm32.whl", hash = "sha256:2a8fc94be2ee5f1d86f97aacd8cc566f81680b6498e76f3007421bb5d98151bf", size = 7118499, upload-time = "2026-09-17T23:21:07.661Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2a/14b3b17cd75cef4a1ee1af4234eb41cc1afe4103b98c2d80e8916abfd42b/pandas-3.0.6-cp312-cp312-win_amd64.whl", hash = "sha256:3ef908d28590b3f42d7070e7ad8f9b34b442b260b7f3c1afb57e0040c58cdb1b", size = 9661439, upload-time = "2026-09-17T23:21:10.959Z" }, + { url = "https://files.pythonhosted.org/packages/3b/11/3d580a604a1e35d69f6676847bd12db7d14344bc677b717f4413e79c5d0d/pandas-3.0.6-cp312-cp312-win_arm64.whl", hash = "sha256:f4e7c52eb108d752e7592268108fd3e98efd76d83a3125cdd06c621c2e44359b", size = 8959806, upload-time = "2026-09-17T23:21:13.851Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1c/143605a1f6443ad50ebda78a31e5a3a10147fec2590e931584aaa5ff0a09/pandas-3.0.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9ae8073aed8e21d1a7fe263dcdc6840743549722a6738198a0a46000fa9476f2", size = 10418900, upload-time = "2026-09-17T23:21:16.594Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ca/87f8548f73d452aab35e4a90f8b39ae303295e0f2ef0b4055c44d6b3f1be/pandas-3.0.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:60d81f9e1799b36f3739e7fff44d1fbb2e8fd5a271b3863e03de9715fccda0fa", size = 10064785, upload-time = "2026-09-17T23:21:19.677Z" }, + { url = "https://files.pythonhosted.org/packages/43/1a/d951442e5607c6e3b2462eff8f420797d428aa74b87c6ecfe4f48553626e/pandas-3.0.6-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:097090508a1dd335013d39106fc10b20f4fd4a171638e47b77d55798ed9dab6c", size = 10245290, upload-time = "2026-09-17T23:21:22.797Z" }, + { url = "https://files.pythonhosted.org/packages/50/fa/96d50e1e6cd0b08b5e2b7c838f65ae644940f75a124063380b5ef73b6866/pandas-3.0.6-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e92d9fa834c7d877130027cddc0cad8dcff97c1f6cca26bd6310f847228b658", size = 10757657, upload-time = "2026-09-17T23:21:25.673Z" }, + { url = "https://files.pythonhosted.org/packages/7b/12/f82d13a2cb703e1a8acee7e01fdc2b898d9cd0c00f07d1dfce63af43e350/pandas-3.0.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b27c8d890e4aa2171437ae2a39de1d215e674158e4865c4023a8b31c932513b2", size = 11249114, upload-time = "2026-09-17T23:21:28.898Z" }, + { url = "https://files.pythonhosted.org/packages/1a/ce/8aef2e561a2f2c8b38c913c67373c65ba6748174e763d27c80271b24bd17/pandas-3.0.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8029ec0f1f89e4f985929ce1f6626dabf3140d61a4e9c1215afdab34eaf9a5d", size = 11820511, upload-time = "2026-09-17T23:21:32.11Z" }, + { url = "https://files.pythonhosted.org/packages/c0/bd/63cb67e6903ef6d9c2871916dbcbc09d254da0fe8b870cf62e16b21945f2/pandas-3.0.6-cp313-cp313-win_amd64.whl", hash = "sha256:f3ce8a6968045481e91a3990e797e348ce13db45ee164a7095bbc824e26c09dd", size = 9638092, upload-time = "2026-09-17T23:21:34.883Z" }, + { url = "https://files.pythonhosted.org/packages/75/2e/e7b35b712edb068d382ddc8b2bea8a04974100515ba2daa22b478b265842/pandas-3.0.6-cp313-cp313-win_arm64.whl", hash = "sha256:cc39303913e2ea129915670de5d1c9fbd647f543bb72e5543bac8baa94e9e42f", size = 8952032, upload-time = "2026-09-17T23:21:37.729Z" }, + { url = "https://files.pythonhosted.org/packages/75/55/1a8875395b05ccd572cbca0b9255dcd2db6e6508e632a558c1a6884b39ad/pandas-3.0.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ee913a91669056c1de1a6b733fbfeab711de9e54e3bee2dfa5fe79d9457247d1", size = 10492213, upload-time = "2026-09-17T23:21:40.746Z" }, + { url = "https://files.pythonhosted.org/packages/35/61/47ae13476995cc8a40cd609e93e7cf11f273d8692925c2903cb6d38aa0d1/pandas-3.0.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ff51a4459ed036e93d1eb1bb5e6e7b28685d3cb6b7c12b91c05b31024e234729", size = 10156618, upload-time = "2026-09-17T23:21:44.142Z" }, + { url = "https://files.pythonhosted.org/packages/bc/f2/cc5f2adb8d6e86a85d9fb5128f8cf205a61189336f70d1f7faf0d1b53ec9/pandas-3.0.6-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:654aae059295dbba6ecd2328ca12712a2cf1676214c8699f1c29213f7ccf9c34", size = 10375479, upload-time = "2026-09-17T23:21:47.159Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ba/ffdcb19be4ff6bfe7d969e7cef2c567c633df5a3a1cc1053394ad053bca8/pandas-3.0.6-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f51d7f651c8054c5e82a69265c98082e795d1442df7ca6edc3a545d61214b1", size = 10783244, upload-time = "2026-09-17T23:21:50.367Z" }, + { url = "https://files.pythonhosted.org/packages/77/5b/e150075b2c6eb69fae896f2d9239bc6ed07db97735971d53d66de6553460/pandas-3.0.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:22172a92e7ee678ec0140c7af4fc9366b55413834a1cd86af78b3caa0b0574de", size = 11382223, upload-time = "2026-09-17T23:21:53.355Z" }, + { url = "https://files.pythonhosted.org/packages/d6/8a/b441c587dc7355bf6e1f68a91b4f76a6c29740f0c23be3acc5d4ebbeea6d/pandas-3.0.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:583be68728a31d0d750d5b8d9e00f02b153df0d4655f858bde93cb84cfc4227c", size = 11854881, upload-time = "2026-09-17T23:21:56.342Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e9/f43410fada510b43fec09993c08f552086c3d247d3ee801a678f3cb10ea5/pandas-3.0.6-cp314-cp314-win_amd64.whl", hash = "sha256:77ccbe5057aece6fc172b9b77f19c04335af6882bc2e10c8f3ee4e6bfb3da553", size = 9791672, upload-time = "2026-09-17T23:21:59.332Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9e/db14c059c21f9baa1907d436f8bf30e0c76c6288225c5e8b79a08ba8b2c5/pandas-3.0.6-cp314-cp314-win_arm64.whl", hash = "sha256:fb625f426b375bcc96e3a04c5d5d266cd7be6ae5d6866e0e703382ab5164068c", size = 9121246, upload-time = "2026-09-17T23:22:02.123Z" }, + { url = "https://files.pythonhosted.org/packages/67/ba/bad0f8dac020ab38a8637fddab01a57a82da7a496a6e6f19590aad53ab62/pandas-3.0.6-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9e492cd4bdba6778de4fe0df7f4590c012161ebcf9902dce01b01dc683105514", size = 10920612, upload-time = "2026-09-17T23:22:05.404Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a9/b500982e9aac6d52a58da4ad3f11e14168a315b06906b3f397c427878065/pandas-3.0.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d7dcd21238cbb4828ff148481ba01cac8946dc5121457b5aeba28636f8f99a60", size = 10570213, upload-time = "2026-09-17T23:22:08.44Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fa/e6ecd0073c98be8f840ac3125b955272835d7d9fd69f5944383b164deb5e/pandas-3.0.6-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ff482fa91fa2bafd92e8fe66ce3645c851824310f295c1f0a2f96e928fc4541", size = 10252356, upload-time = "2026-09-17T23:22:11.302Z" }, + { url = "https://files.pythonhosted.org/packages/04/f5/001e230a7a7803590d9275a1a3f7e1bb605e3a495cfe5e8d3a532090621b/pandas-3.0.6-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:db7ec631f26223beee8e5c9e0b8f23c24d8197bbd1d982421d4e3188bea51965", size = 10655952, upload-time = "2026-09-17T23:22:14.283Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ab/bab587148a3852c96aae26c4b5f9e04ce2221801ad94e166b4fbf969ede0/pandas-3.0.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd75ed0c840f709fc2ae26ddd9534ac77ca1a48ac0cce521a74acaa85f3340a7", size = 11274177, upload-time = "2026-09-17T23:22:17.352Z" }, + { url = "https://files.pythonhosted.org/packages/f3/32/74b48d87df2b80892d713c149abfe36d5db4de41b4eccb042a2bc07dafc1/pandas-3.0.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ef738d71d1059245b6bb03e312be06d8b3821326a83486c1ad03b9aba3710e44", size = 11719041, upload-time = "2026-09-17T23:22:20.227Z" }, + { url = "https://files.pythonhosted.org/packages/f6/c6/d64b72d64d7eb0fad9fe424d34138e45dee70ddbea1360dcd0adf30e28f6/pandas-3.0.6-cp314-cp314t-win_amd64.whl", hash = "sha256:429d9df32731ab01383ed98f2baa7a60368090d1a94fc06019a12062510e8630", size = 10191388, upload-time = "2026-09-17T23:22:23.524Z" }, + { url = "https://files.pythonhosted.org/packages/7a/30/5e5b2ccabeca73ae2b03fc82bca3eabb7466cf43737f05ac08d591665d47/pandas-3.0.6-cp314-cp314t-win_arm64.whl", hash = "sha256:a4dbd4dc65cbe645b92b8785d0f96dd7311010dc6606cf620e51b07b8788a12a", size = 9387796, upload-time = "2026-09-17T23:22:26.64Z" }, ] [[package]] @@ -1555,15 +1702,15 @@ wheels = [ [[package]] name = "plotly" -version = "6.9.0" +version = "7.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "narwhals" }, { name = "packaging" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/96/07/795c79dbce40c39bece88e69d049babbd23ffa95b5d117f248db8ea03abb/plotly-6.9.0.tar.gz", hash = "sha256:967ad33e8c704fed051800d11d985eb206a9c795c14206b30a6f463ed9c67d0d", size = 6919903, upload-time = "2026-07-09T14:55:59.982Z" } +sdist = { url = "https://files.pythonhosted.org/packages/49/c3/72b369f5ed7701b04ab0ea3dcf83e9bbce71c0b3bc6f07f87568550d09ea/plotly-7.1.0.tar.gz", hash = "sha256:f860166a4a3d78c69cb1f4a15f28a5c8283eade98a282a698f3bb853a449ace5", size = 6689315, upload-time = "2026-09-15T19:21:21.75Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/24/18/d8544811ab076f876c4892b3714f5b0dad335e1dc33aef826df431b8325d/plotly-6.9.0-py3-none-any.whl", hash = "sha256:36bebe2f1bb13884774fe61689c329071446f6ce4a8927fb1f0d6fb24f581236", size = 9909646, upload-time = "2026-07-09T14:55:55.421Z" }, + { url = "https://files.pythonhosted.org/packages/e5/7d/905a3a3d51087515719058c94cfbda2ff0fc14417c20d557ae3e82d8b250/plotly-7.1.0-py3-none-any.whl", hash = "sha256:dbb7fa18afce40d0a8e80d1bf162eceb3faa0ce5a77fe741ad09a74cf78f53f3", size = 9692368, upload-time = "2026-09-15T19:21:18.331Z" }, ] [[package]] @@ -1614,7 +1761,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.13.4" +version = "2.13.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -1622,125 +1769,125 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/ef/fc4f868f4e2cee79f863883abffceff107875f569b848507319842d2a681/pydantic-2.13.5.tar.gz", hash = "sha256:51a9c5f7b2f8e636f04c6cada605d9b6a3bf1348fdf945a3d8869b19bba0ee08", size = 845750, upload-time = "2026-08-28T14:04:00.916Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, + { url = "https://files.pythonhosted.org/packages/eb/47/c95ffc2009878c7aac0c5e08528022dcb885933252a88b5f170058014464/pydantic-2.13.5-py3-none-any.whl", hash = "sha256:346a034f080da3755d8e9cb5e00e8b07de1d39e4f6e2c87d8ab7cafa0b269a73", size = 472589, upload-time = "2026-08-28T14:03:59.136Z" }, ] [[package]] name = "pydantic-core" -version = "2.46.4" +version = "2.46.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/08/f1ba952f1c8ae5581c70fa9c6da89f247b83e3dd8c09c035d5d7931fc23d/pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4", size = 2113146, upload-time = "2026-05-06T13:37:36.537Z" }, - { url = "https://files.pythonhosted.org/packages/56/c6/65f646c7ff09bd257f660434adb45c4dfcbbcebcc030562fecf6f5bf887d/pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5", size = 1949769, upload-time = "2026-05-06T13:37:46.365Z" }, - { url = "https://files.pythonhosted.org/packages/64/ba/bfb1d928fd5b49e1258935ff104ae356e9fd89384a55bf9f847e9193ad40/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba", size = 1974958, upload-time = "2026-05-06T13:37:28.611Z" }, - { url = "https://files.pythonhosted.org/packages/4e/74/76223bfb117b64af743c9b6670d1364516f5c0604f96b48f3272f6af6cc6/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b", size = 2042118, upload-time = "2026-05-06T13:36:55.216Z" }, - { url = "https://files.pythonhosted.org/packages/cb/7b/848732968bc8f48f3187542f08358b9d842db564147b256669426ebb1652/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c", size = 2222876, upload-time = "2026-05-06T13:38:25.455Z" }, - { url = "https://files.pythonhosted.org/packages/b5/2f/e90b63ee2e14bd8d3db8f705a6d75d64e6ee1b7c2c8833747ce706e1e0ce/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50", size = 2286703, upload-time = "2026-05-06T13:37:53.304Z" }, - { url = "https://files.pythonhosted.org/packages/ba/1e/acc4d70f88a0a277e4a1fa77ebb985ceabaf900430f875bf9338e11c9420/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd", size = 2092042, upload-time = "2026-05-06T13:38:46.981Z" }, - { url = "https://files.pythonhosted.org/packages/a9/da/0a422b57bf8504102bf3c4ccea9c41bab5a5cee6a54650acf8faf67f5a24/pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01", size = 2117231, upload-time = "2026-05-06T13:39:23.146Z" }, - { url = "https://files.pythonhosted.org/packages/bd/2a/2ac13c3af305843e23c5078c53d135656b3f05a2fd78cb7bbbb12e97b473/pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d", size = 2168388, upload-time = "2026-05-06T13:40:08.06Z" }, - { url = "https://files.pythonhosted.org/packages/72/04/2beacf7e1607e93eefe4aed1b4709f079b905fb77530179d4f7c71745f22/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4", size = 2184769, upload-time = "2026-05-06T13:38:13.901Z" }, - { url = "https://files.pythonhosted.org/packages/9e/29/d2b9fd9f539133548eaf622c06a4ce176cb46ac59f32d0359c4abc0de047/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f", size = 2319312, upload-time = "2026-05-06T13:39:08.24Z" }, - { url = "https://files.pythonhosted.org/packages/7c/af/0f7a5b85fec6075bea96e3ef9187de38fccced0de92c1e7feda8d5cc7bb9/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39", size = 2361817, upload-time = "2026-05-06T13:38:43.2Z" }, - { url = "https://files.pythonhosted.org/packages/25/a4/73363fec545fd3ec025490bdda2743c56d0dd5b6266b1a53bbe9e4265375/pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d", size = 1987085, upload-time = "2026-05-06T13:39:25.497Z" }, - { url = "https://files.pythonhosted.org/packages/01/aa/62f082da2c91fac1c234bc9ee0066257ce83f0604abd72e4c9d5991f2d84/pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf", size = 2074311, upload-time = "2026-05-06T13:39:59.922Z" }, - { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" }, - { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" }, - { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" }, - { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" }, - { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" }, - { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" }, - { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" }, - { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" }, - { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" }, - { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" }, - { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" }, - { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" }, - { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" }, - { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" }, - { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" }, - { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, - { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, - { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, - { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, - { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, - { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, - { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, - { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, - { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, - { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, - { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, - { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, - { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, - { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, - { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, - { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, - { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, - { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, - { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, - { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, - { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, - { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, - { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, - { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, - { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, - { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, - { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, - { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, - { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, - { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, - { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, - { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, - { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, - { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, - { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, - { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, - { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, - { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, - { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, - { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, - { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, - { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, - { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, - { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, - { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, - { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, - { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, - { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, - { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, - { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, - { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, - { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, - { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, - { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, - { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, - { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, - { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, - { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, - { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, - { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, - { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" }, - { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" }, - { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" }, - { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" }, - { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, - { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, - { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, - { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" }, - { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" }, - { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" }, - { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" }, - { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" }, - { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" }, - { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" }, - { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/af/f9/8a06bea35ef8daf588f707784c973a7046e0034c8d8cfb08828eeffb8b75/pydantic_core-2.46.5.tar.gz", hash = "sha256:10416c15b8839ecc4ef4d0885da76da6fd0f67333a0eb8aff6d93c4b8f2910fc", size = 472262, upload-time = "2026-08-28T10:01:31.677Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/6b/8f79692844269427abb3e4dd9e68edfcbe65ae25527d99183214de716c59/pydantic_core-2.46.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:657b40d6240c0a7b6a64b30f22d1e3aa631c7e846c621b0c0f6d1d75e2e15ea6", size = 2076533, upload-time = "2026-08-28T09:57:35.421Z" }, + { url = "https://files.pythonhosted.org/packages/bd/d0/c787604c71c2bdcda1a5656942fc822cd0f9cd879b9484bb84fc42172703/pydantic_core-2.46.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ecb42011e12ee19cafbc312887cbf3546959fe02fbad44f272d4be5baa997615", size = 1924650, upload-time = "2026-08-28T09:57:37.944Z" }, + { url = "https://files.pythonhosted.org/packages/4a/77/ca2f8e997d9bfdb32205297aff38f210f398822d895b1af1b59fd9df9c13/pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4dedce55295becb61921e386b99d4f2706045306e7fa52249a33004c837379fb", size = 1951261, upload-time = "2026-08-28T09:57:39.339Z" }, + { url = "https://files.pythonhosted.org/packages/a0/53/bd12e1a9255df4edee00353778e2614b5346265d51e1567ab72153e803a2/pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9f47b8a949e60f027f0aa0a6f6c7b7e9c55cbf4380d10b344e282fa4e7ab1e1b", size = 2021808, upload-time = "2026-08-28T09:57:40.69Z" }, + { url = "https://files.pythonhosted.org/packages/d7/41/f7f312751ebc6d6767da91964a9c7954c18e226a1720ab234e3dfb9d6c17/pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:200aa3dc9f8d54f0754f43247c0bad0999fdcfbfd2488384dd44f37279271fe6", size = 2196184, upload-time = "2026-08-28T09:57:42.275Z" }, + { url = "https://files.pythonhosted.org/packages/3d/93/ce93aa030ab6bac4683ba8861e7baad89dd24b02e66b8801a0e4f6a00311/pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6d30e1a4f138b8951063e9a394752a9179b51da288ffa507b1e659222f4c1793", size = 2238212, upload-time = "2026-08-28T09:57:44.122Z" }, + { url = "https://files.pythonhosted.org/packages/34/a1/c8e6b66f499f510752c07a092dfe27621f9c255635e59d38704b5681c35a/pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:850a08d167dde16db8702c274f320c7be9d7da6f6dff2b58b18f9e815bd94f5b", size = 2064073, upload-time = "2026-08-28T09:57:45.613Z" }, + { url = "https://files.pythonhosted.org/packages/5c/fa/605e2b127ee30dbf4b1da9da4843587cf2b2d16486c241cc7a5be2d2c1bd/pydantic_core-2.46.5-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:c3471e5c4a949c26ec00a77f01df59096aa9495877de76fd60a980f8ee6be461", size = 2093102, upload-time = "2026-08-28T09:57:46.953Z" }, + { url = "https://files.pythonhosted.org/packages/4a/f7/1ab28093c09032ddce7c92c7a55d503b6ecd70f42c32492946c1cb5477b1/pydantic_core-2.46.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3a3e26b6a8274211bddee2d0e4d0d42778f17a34510f49d2ec44b58abfc41736", size = 2133452, upload-time = "2026-08-28T09:57:48.362Z" }, + { url = "https://files.pythonhosted.org/packages/30/c8/47c79b756f12f85e8b0fbdb2b495f6b6eb32e6c98a4beae7a570a0b7c63c/pydantic_core-2.46.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:fc5d783bd4a2387e97b8a2d5ec781cfb92b3d893bf82370548e99db5915935d3", size = 2146477, upload-time = "2026-08-28T09:57:49.74Z" }, + { url = "https://files.pythonhosted.org/packages/13/5c/79fc00cb8f651d6061991de8d7cedf1c78c73cbd4862c42ef418f03b8bfa/pydantic_core-2.46.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:356c8368cbc321050b169595683a2e1d63413b1e0e2868b330af9fc14c616d3f", size = 2300832, upload-time = "2026-08-28T09:57:51.639Z" }, + { url = "https://files.pythonhosted.org/packages/b4/72/dd1a29853cf6d22a1ebd9e3baf0239cbc57d2d16caff36a89e38eb9b1db3/pydantic_core-2.46.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:eb7d8d0e5886a89a55d2eef490e272fa965a9d57c6b29a5b5088a7997ec2cad1", size = 2320505, upload-time = "2026-08-28T09:57:53.236Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d1/ba4a8e06a9ddad0b4caf69cfaeecc0fbfcec20473bd808f5127fd16491c4/pydantic_core-2.46.5-cp310-cp310-win32.whl", hash = "sha256:4d44cf99ddebf875f9b68cc267aa684c99b7b44fe63ee1cac4ec163807290069", size = 1956853, upload-time = "2026-08-28T09:57:54.592Z" }, + { url = "https://files.pythonhosted.org/packages/f2/94/205ed9d7ddaf44acd489889708ea124a3f41bdb42c141c8684d528ad0e7a/pydantic_core-2.46.5-cp310-cp310-win_amd64.whl", hash = "sha256:1e5aad1220a1192c42341c8fd4a8686657e73ab2a920c970bdc4de334fe3193d", size = 2042551, upload-time = "2026-08-28T09:57:56.017Z" }, + { url = "https://files.pythonhosted.org/packages/a2/b6/81d2d19ea0be2c03664381b59f65fa72fc7969decedae00bc2c4ad835708/pydantic_core-2.46.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a1dee1b804ff4d11c663636cf15d2ea47e9f79cd56c033fb1cbf08924842a48f", size = 2074737, upload-time = "2026-08-28T09:57:57.711Z" }, + { url = "https://files.pythonhosted.org/packages/0c/18/b70da8300e292df4099684ea11b1958043580d2f50d2dc8bf7e542bdd84a/pydantic_core-2.46.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d625a186a65201c23a9e3b8ed9c47e90a026e03256608cc91851c6709096844f", size = 1921751, upload-time = "2026-08-28T09:57:59.265Z" }, + { url = "https://files.pythonhosted.org/packages/e7/1a/0d590341b6ffa4b4aca83508e6b8db4761aaeacfc15a25ca3815876d4797/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f8507560a9284e1370bb048ed4282012fbef4e8d109875b95e884d228552061", size = 1948231, upload-time = "2026-08-28T09:58:00.678Z" }, + { url = "https://files.pythonhosted.org/packages/7d/1d/02eb35761c51f2f7b1b042d6ab4cda6600f0c8c88a2243b3f734376201e5/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f93c5fe914d75fbec9a49209b00da5f08e9e467d69da2b1510c81940cfd10be", size = 2020708, upload-time = "2026-08-28T09:58:02.267Z" }, + { url = "https://files.pythonhosted.org/packages/4a/ea/f86073830e35d508cc8ddf9c3d9e6e6840fcb88d34bf726b0b4710186f27/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c767f552b21b10f774aeac128e828eafb796adfa1b666a18bf6321453c3a", size = 2194914, upload-time = "2026-08-28T09:58:03.934Z" }, + { url = "https://files.pythonhosted.org/packages/bb/d7/fc36240d7791ce90939e51608568c33bfdae26202016f9770c229a487d86/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:701b2e04b560eeb4bddf7a25ab8ca476176e34fdbd9a0e18196f0d12d4685f0b", size = 2235622, upload-time = "2026-08-28T09:58:05.516Z" }, + { url = "https://files.pythonhosted.org/packages/cf/bc/3fa2d76b83162820a17da7f645b28d1cba99fc8e1e5fc6517067ec450fa1/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49776eab08766a08dfff7012f8b422dcd7e25e43b316eedf0477c24fcfa84b7c", size = 2062091, upload-time = "2026-08-28T09:58:07.135Z" }, + { url = "https://files.pythonhosted.org/packages/ab/9a/095d557bb492c90cd8a70a6dd048bf793d433d03d86c81c11e912e4cd049/pydantic_core-2.46.5-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:a2468d93d181667a7abd66e1b64bb9f76f361b0fef8faddf687456453576f5ee", size = 2089904, upload-time = "2026-08-28T09:58:08.814Z" }, + { url = "https://files.pythonhosted.org/packages/24/98/7b76b1ad10a19a617a52aaa1d80e159115af939b095e86f8e756fd52e0df/pydantic_core-2.46.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:53feb344243bb9510a9dec7bf3cf1b64d88a98af5dc7872a5160465f8b198c8e", size = 2132244, upload-time = "2026-08-28T09:58:10.435Z" }, + { url = "https://files.pythonhosted.org/packages/20/32/7d6ca365fadba186a0c8f85de1a701663bce81efd309d9479be58687622f/pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:cd5214352ae68f3b5e9af7768bdc5253695ee069675db3480518420b3be881f2", size = 2143901, upload-time = "2026-08-28T09:58:12.033Z" }, + { url = "https://files.pythonhosted.org/packages/f8/09/eb9a6aa57f22fd1541a9c0aa2a1f3aeef3ec65347d33e10a6da2f43e0ee9/pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:9432f3598db432cb51c5b37fdbf29a60fcccc79e30d37a05022776a6bc4ab689", size = 2299425, upload-time = "2026-08-28T09:58:13.614Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f9/548a5bb9d4ba8cd26e26daf48052236f6b38bb61e7b7241fbc3c995719eb/pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8feeac04b5794e513e710af2f9c87d49f31a6dc47967bb264a1fed61a8989bec", size = 2318566, upload-time = "2026-08-28T09:58:15.199Z" }, + { url = "https://files.pythonhosted.org/packages/4a/20/06454d18834c02c406c9133f1a3b485305fd9ee984f9636c2f730bef6a9d/pydantic_core-2.46.5-cp311-cp311-win32.whl", hash = "sha256:892a881d5f68c2b9ea304b7a6c2c60d9343df578a311b0f86b94bc8f1ffe8129", size = 1954258, upload-time = "2026-08-28T09:58:16.813Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c2/718b9deb4b72453b5d8c7447a3b14cb77bef36917ef5f514e0948a4096a0/pydantic_core-2.46.5-cp311-cp311-win_amd64.whl", hash = "sha256:40375c2d05acec10323e45dfe2077ac44bc74659008614af5069034e2cfc781c", size = 2041030, upload-time = "2026-08-28T09:58:18.288Z" }, + { url = "https://files.pythonhosted.org/packages/67/ea/c1d1a5b72d6e1ff7f377a4d9199f6591f095beb5b409a8a5d89f7238d939/pydantic_core-2.46.5-cp311-cp311-win_arm64.whl", hash = "sha256:28a6a556cd3b6066bea827857f9d9cce027c96f776e512f544a581f9e42161f8", size = 2009234, upload-time = "2026-08-28T09:58:19.929Z" }, + { url = "https://files.pythonhosted.org/packages/82/3f/76358795aa7a8c6d4f36e2cb828ad1c90ee118e1393a9281664f5aade9d4/pydantic_core-2.46.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b9fe6fb92520e3fd61f2e49000b6911b188824f089b75973ea06d6267f0b476d", size = 2076516, upload-time = "2026-08-28T09:58:21.576Z" }, + { url = "https://files.pythonhosted.org/packages/db/50/26b091836076ce4cb2fac264186936acc069e0595772cfd02a563bc4761a/pydantic_core-2.46.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a39ac25a9a2fa4072efdb429833c4a4c8009a51ff9eea3eeae131713cd27991e", size = 1922874, upload-time = "2026-08-28T09:58:23.766Z" }, + { url = "https://files.pythonhosted.org/packages/09/f0/2a8ce3849e299d44e2d2c196b6082643a3235565a735cb51db7a6261f614/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fdc8b93a41521988916eeaa271173fcca7fa0803d62f87675aac8dcec1c8e29", size = 1951772, upload-time = "2026-08-28T09:58:25.435Z" }, + { url = "https://files.pythonhosted.org/packages/87/46/ac0dc8bdd9e6048183a14eb127764e7ad9240021c17513074a4711b0e31e/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b98134087d9de723658d17a42c7d0da8d6e2ef08015dee7dc93889047315f5e4", size = 2031832, upload-time = "2026-08-28T09:58:27.102Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c2/339de5bef7be36301a2231eaa52e62163742c2281f11b5f4892bc79785cd/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e652ab17569c94bff5475520f907b7148b8c24036a8ebbe5cf7cf7493d28579a", size = 2208645, upload-time = "2026-08-28T09:58:28.948Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a0/9ff22b797724262da14427abaed4dd1d864a139693fc5e7809114376a716/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d925f3d9afd05a8c0fb3a1031463a8d59ebe5e2afad297e29c78be19e13b4e62", size = 2265935, upload-time = "2026-08-28T09:58:30.625Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a4/eb9409ec0736e50aa70a412f16c204ed149516846912f7e6724d4c73ee53/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fc5be0abd4a407e200d844b404e33639a554e7bd0d448e7b9ae181be4789ac2", size = 2066284, upload-time = "2026-08-28T09:58:32.289Z" }, + { url = "https://files.pythonhosted.org/packages/c0/02/7f6156ffc926857f1c37c07d9a388682865a81830ab6a1b637082c25e399/pydantic_core-2.46.5-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:816ff0a6550ffc06c098ccd2e0698600f9aa7da192a79eaa6f9af504a35db869", size = 2105889, upload-time = "2026-08-28T09:58:33.986Z" }, + { url = "https://files.pythonhosted.org/packages/92/b1/e781d357ebe09fc929f995700f1b3503e8897f1cece183ecb1300d4d67e9/pydantic_core-2.46.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c7ea57fc63aa7da93a1bd2d644e6577befae10c52c4e36377635eea1056a74f5", size = 2158006, upload-time = "2026-08-28T09:58:35.647Z" }, + { url = "https://files.pythonhosted.org/packages/70/0a/644597d84ab400e50609c192120b85c9681c22d3a20461b9060a79be0a7a/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:efd62a42486f1bda5d24cb4f63d15a3c7768375fe83d36f9417b4ad7a2fb20b3", size = 2158408, upload-time = "2026-08-28T09:58:37.38Z" }, + { url = "https://files.pythonhosted.org/packages/1e/ee/ca3b7b3a4b3769ffe9ce9432a7c9be755de9593a46d3b0d54d0409323e44/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:2bc9419666990c06d7397831f2126a1ecc3594aaa3ff7de5bf2d066802f4e07b", size = 2309609, upload-time = "2026-08-28T09:58:39.22Z" }, + { url = "https://files.pythonhosted.org/packages/ce/52/39fa1f451486019524ca685020390e7ca351832fd874530ba30c8628e6dc/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:18a09e1e1011b462f2e32774f25859ef1223d5c2b0546a633cf56654710721e0", size = 2342618, upload-time = "2026-08-28T09:58:40.89Z" }, + { url = "https://files.pythonhosted.org/packages/81/5e/468fc630568c61dcef3cd47ad32ffbeed9af643f49208d1ea86ab4f890c4/pydantic_core-2.46.5-cp312-cp312-win32.whl", hash = "sha256:5cb482e9e84c851f4e623fe4acc1ced89168cf1fe18f7089db4548c8f5bbb65b", size = 1939475, upload-time = "2026-08-28T09:58:42.591Z" }, + { url = "https://files.pythonhosted.org/packages/cf/c9/4c19f41b84cf6b622a72fbeed7665b25d47a187d68d47d0d430c07f23268/pydantic_core-2.46.5-cp312-cp312-win_amd64.whl", hash = "sha256:5e81740c09e310f5aa5cbd3e434a01c154d4bef93241c7877b39f211d2b78ba8", size = 2043140, upload-time = "2026-08-28T09:58:44.272Z" }, + { url = "https://files.pythonhosted.org/packages/af/dd/0c1a050299147c746e5256db16d645ab5efd4f78c59937d581a0524e74a2/pydantic_core-2.46.5-cp312-cp312-win_arm64.whl", hash = "sha256:f7b0ec93a2893de856652154d73b7ba622f26fa97726487dcac373de5f4c6084", size = 1997729, upload-time = "2026-08-28T09:58:46.13Z" }, + { url = "https://files.pythonhosted.org/packages/f5/37/5abe39a8372a61d3dc3c1338fc504281c01b32fdb3169cd7187153b56d3e/pydantic_core-2.46.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:b7ca9034437b6022f941f4857459562ee00a560b97e7cce8a0ec5a74fc6766e0", size = 2075885, upload-time = "2026-08-28T09:58:47.856Z" }, + { url = "https://files.pythonhosted.org/packages/21/43/6323b1f8b217780454c61304bcd2b38ae4762f50754414124603ccc90bb2/pydantic_core-2.46.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f332f0e72a5a0400141f830744e141bf9f97917878dbe968669e8a7fefea78ff", size = 1922768, upload-time = "2026-08-28T09:58:49.58Z" }, + { url = "https://files.pythonhosted.org/packages/0f/a3/c05ca796e1197618a774b01e596aeedfefc2f7d8c01ae3054e910b120e8a/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:193375f3548919d3f0b60936ca113ada3e38f264f91b9b8e0508efaad57be931", size = 1951241, upload-time = "2026-08-28T09:58:51.511Z" }, + { url = "https://files.pythonhosted.org/packages/68/32/33bc39ac705c52cffc908e8389f9754fdb208aea5c69cceddf4eb3ce99af/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:79bdfa52f843137045b2d081cc05c120ba6665d29b7559c2c47690906f39279f", size = 2031975, upload-time = "2026-08-28T09:58:53.166Z" }, + { url = "https://files.pythonhosted.org/packages/b0/70/2333e885c0f6a67bc105c5916965dac9b57f2718ee20d81d1a06a4ebdc13/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:24922243639cbdac66c75fcb6fd6495a9cb52b213d62f9a0d16f0310b1ff8038", size = 2208542, upload-time = "2026-08-28T09:58:55.017Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ea/296debfb4264207bbda5936133892e027c0a58875ad53ebd512fba8ec3a2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c76fe65e607be28c7fd4d56fc3c42b1583aa058ce3408b7ad0fd540171d31f9f", size = 2264692, upload-time = "2026-08-28T09:58:56.767Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f2/9e4de77a6271e07a76d2d58b11c091a979c191ed2939bf80067568b369d2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f7b393a8b3da82f5c1fc0751e6d01ac6c55b93c18226a60bdfba4a724efafd1", size = 2066633, upload-time = "2026-08-28T09:58:58.531Z" }, + { url = "https://files.pythonhosted.org/packages/8d/db/f9e9d0c97445987b2084823d5c240de88087338f04fc2cfaa2df186b8049/pydantic_core-2.46.5-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:7ac031912d54f3d83ef3b3eb98dfabc1608802e2202263d25957eeed40b94761", size = 2105235, upload-time = "2026-08-28T09:59:00.421Z" }, + { url = "https://files.pythonhosted.org/packages/07/c5/79169b047b3b2c3e99e04bc76372af9637e0bf6db638274fa927df96369e/pydantic_core-2.46.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:837b396ca3d7b74091ca623f6cbd8351bd42d670a79c2683e79fb089f06a2de5", size = 2157367, upload-time = "2026-08-28T09:59:02.442Z" }, + { url = "https://files.pythonhosted.org/packages/26/b5/ba6057afb7c291bd449f51b867f95aef2072941c4ce4e5c31d6ffd132d3b/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:5ee239d575f80b08eca11f6e20f90c4c695de7825c67eefe6091fbf20dda648e", size = 2158420, upload-time = "2026-08-28T09:59:04.2Z" }, + { url = "https://files.pythonhosted.org/packages/6e/28/2057abecaafdc22912afa819603a51f0a62d40643b7c4871c51721fea9be/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:e80675d75ae2cd14372cb65cad5400d9347a3d3f6c13000183f22dfd027283ed", size = 2309588, upload-time = "2026-08-28T09:59:06.048Z" }, + { url = "https://files.pythonhosted.org/packages/71/9d/881156dc404e27479c4246128d73538464cab4a239bec61995e227644c30/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:9c4b71f10dd532fb7a5cbc8f58707779e64f03a258c2bf8bfbaecfcd9970b519", size = 2341866, upload-time = "2026-08-28T09:59:08.539Z" }, + { url = "https://files.pythonhosted.org/packages/5a/38/d66f443a259f84d13babdceae568e572b0ed26da17ca5d0a649ebb110a67/pydantic_core-2.46.5-cp313-cp313-win32.whl", hash = "sha256:97bf8de4d541598c94a59344eeb988a94c08ff76b5723c41f6567ec18c7892ea", size = 1938580, upload-time = "2026-08-28T09:59:10.402Z" }, + { url = "https://files.pythonhosted.org/packages/2c/1e/1d5371213f4cc9a7ed70c0bfcc7911de22311ee99a662a56077d7292d2ac/pydantic_core-2.46.5-cp313-cp313-win_amd64.whl", hash = "sha256:15f4a94963c95accac15b7b657bb177d3ad82bb90b0d0526d9a9b85079925db5", size = 2041980, upload-time = "2026-08-28T09:59:12.396Z" }, + { url = "https://files.pythonhosted.org/packages/5a/48/4222d90b1c67568bace4dec6dca6271449c66de3595d72b6d098f5fde597/pydantic_core-2.46.5-cp313-cp313-win_arm64.whl", hash = "sha256:d22a945598fb91236b4dd793a6e42e4f3dd7740bb5aace5ebd7d4c08d13bb575", size = 1997213, upload-time = "2026-08-28T09:59:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/8e/8a/14596f2a8367da50cf7cbac48169ee5d9c8e11d486a3b527082384630c72/pydantic_core-2.46.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c1c43ad4339643d70ebb8124e1305a7dab423001eff58bb41a0f731adbc98355", size = 2074081, upload-time = "2026-08-28T09:59:16.141Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d5/d8a4eb6d6c7f66b91dd37c576d76e9e60fba900caf5372c17bcf949febc2/pydantic_core-2.46.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1a353f84de772f423b5ffb11d7ae352fbbef0f446f3c0b0af0f8236d7233606e", size = 1920497, upload-time = "2026-08-28T09:59:18.065Z" }, + { url = "https://files.pythonhosted.org/packages/8e/26/092079428f86e927e030b2c0ced87df69dbb1c875cdeaa67bf42ea2be746/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5086029a57366b8cf81b130a43908738095c270c21a8d7f0e8bdfdb89718e2f3", size = 1952130, upload-time = "2026-08-28T09:59:20.476Z" }, + { url = "https://files.pythonhosted.org/packages/08/c3/8ec0e290a9ebaebd64047bf5fda94be835c6b1551b02437e4b76778fbcd7/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46c25dda9d092a06c08db76ffe0a197107904d0dfac653f7d5306bbcd6d6119c", size = 2026371, upload-time = "2026-08-28T09:59:22.227Z" }, + { url = "https://files.pythonhosted.org/packages/01/72/4fd20ad520fb8da0157f95b27a7eb05a72790ef08138e7701ac972c342ea/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:37ea7b83c935e5b0d68c9449b82651accf78a10828b2c02b2f2d9e9496446c21", size = 2202822, upload-time = "2026-08-28T09:59:24.277Z" }, + { url = "https://files.pythonhosted.org/packages/31/b0/d16e0771206b29314f0d52198b720be21e8a99ab2bf11e3bc0d7c9cebdff/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e64e88d5585bea9ce95861079de72006c7fa6d3df4e3a3b65ba31eb979c15c9f", size = 2262756, upload-time = "2026-08-28T09:59:26.608Z" }, + { url = "https://files.pythonhosted.org/packages/2c/9b/59634b7ac631c63b2a37760eb6943af3e29573d6b59a4abc5e7f019d4cee/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d510bac3ee52247af28ed4bb18a1e799f040ac60fd2bf5ccd4c92f1fbe786f", size = 2068352, upload-time = "2026-08-28T09:59:29.044Z" }, + { url = "https://files.pythonhosted.org/packages/08/7c/570abb1ad2155348dc754ea91be22e5aaa18eb6d69a6068f7c6f2679a6ed/pydantic_core-2.46.5-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:a2a5e1d0ff29adddc9f6d6821a66302e4493f8ca898b715b6b1182c2c201ea0a", size = 2104777, upload-time = "2026-08-28T09:59:30.95Z" }, + { url = "https://files.pythonhosted.org/packages/8e/25/5bf74adc65a1ac5b7be3f6cb0bcb5433615c1598a801c19d830d84c98ded/pydantic_core-2.46.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:03b9666e41e35d8909852ba191a0607520f81b74eaf12ccf8737005dbb313821", size = 2156312, upload-time = "2026-08-28T09:59:32.604Z" }, + { url = "https://files.pythonhosted.org/packages/90/6a/2ef38830675e050121040618135564ed56b860b45433b02d9b4ebece46f3/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:a91c17edf6eea2402cb5457b4c89e99bc5ed1004aa34c4adf1d4258c1a5c22c2", size = 2150067, upload-time = "2026-08-28T09:59:34.453Z" }, + { url = "https://files.pythonhosted.org/packages/90/ef/a7dbb03a14a64c2a4621f989c615ed9a892535a6cad938fc27079f919d80/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b49924c73a235e969511bf2aabdff3beebf9820931f646c80274d5d780010c47", size = 2304516, upload-time = "2026-08-28T09:59:36.194Z" }, + { url = "https://files.pythonhosted.org/packages/68/f8/6bb4c4b80e8a6fde1904c64a51c62a1d04fcdfa3ea521a66b2ddefa1d885/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:2cbd9a5eff05e51c447c34dfa4632145b26b09120cf04bd0c871e44c1a5e1c9a", size = 2335223, upload-time = "2026-08-28T09:59:37.931Z" }, + { url = "https://files.pythonhosted.org/packages/2a/80/f46b8c681195190b2c1f1c7c0a81abce60663e987613e09ef64d433dd96b/pydantic_core-2.46.5-cp314-cp314-win32.whl", hash = "sha256:2d5d76654becf5efd62c9e51c3756c67b49498b0c9a40884934c40807adbd074", size = 1934827, upload-time = "2026-08-28T09:59:39.836Z" }, + { url = "https://files.pythonhosted.org/packages/f7/3c/60674207246bc0a4009d2391b7c7251c7159f279c8d2ab8aae8ef46f3dee/pydantic_core-2.46.5-cp314-cp314-win_amd64.whl", hash = "sha256:fa10ef4112775900e7a0661068635eb67b2ab824fbde764de6e0e21982a93db0", size = 2042648, upload-time = "2026-08-28T09:59:41.792Z" }, + { url = "https://files.pythonhosted.org/packages/69/0c/117c562c7c1babdf44576b72a5e496906506c93690387ecfbca7c729ae2e/pydantic_core-2.46.5-cp314-cp314-win_arm64.whl", hash = "sha256:045ab3b6d308439e32b81cc173bba5b9018bc6ed896afd0c65b3b009b1699af5", size = 1989652, upload-time = "2026-08-28T09:59:43.702Z" }, + { url = "https://files.pythonhosted.org/packages/e8/66/9336ae58f9eb68c41d121894e52c4c89eccb07eb8f602a04ee9c3f37736a/pydantic_core-2.46.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8816f3d218beb4b787de5c9759c259b8fa61f9dec42dc7811f320a33771778b7", size = 2065829, upload-time = "2026-08-28T09:59:45.364Z" }, + { url = "https://files.pythonhosted.org/packages/c5/02/bc19b47a96c2d3109760711acf22369e56bd7e405ca52f7ade164d2ead57/pydantic_core-2.46.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bce57638e08ac148e5778cce7feb968307a727d66f8e2274a543d0cf0c9ad6a3", size = 1905716, upload-time = "2026-08-28T09:59:47.18Z" }, + { url = "https://files.pythonhosted.org/packages/52/a4/70b47c0509923dd98ccfed04fb3e32ea3849c82a0ff2205bb41009b43c00/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:976e1128455aa595ea04c79ccfedff1aaeab96ee013fcc916bed120c4f0ad94f", size = 1934216, upload-time = "2026-08-28T09:59:49.241Z" }, + { url = "https://files.pythonhosted.org/packages/52/ab/aa03b65f7bb198585edf806b906c3223ecf1795543e39e23aec4cce27ad2/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b891faeedeafba41b2983e5001a81b6a915b69544c7e7570d1989ce1c36ac7", size = 2010635, upload-time = "2026-08-28T09:59:51.692Z" }, + { url = "https://files.pythonhosted.org/packages/3c/8b/0da06343f30b84ec549aafd309c6456223d5dc8bd36af504c573faad561d/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f194189415698233dd1114a093a9b56e61e2c57e11b469be3b0506f46f0771c", size = 2209369, upload-time = "2026-08-28T09:59:53.582Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5b/844c4defaa34a3df66eb9257087d121d70c201298b96abdf9f492fc2f1bf/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82a36973cf8a2ef5406f4fe2edbf8ed0c99629535d959e0b100c76a32535a111", size = 2253238, upload-time = "2026-08-28T09:59:55.484Z" }, + { url = "https://files.pythonhosted.org/packages/f4/64/a4e536cb16d7f61a7fd3120b46c577fc7fa7325992f69c4f52bc786d77d8/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdbb78909f52b981d3b2d56b97328d71eb0b974c36bd77c920123a7ebb192829", size = 2065740, upload-time = "2026-08-28T09:59:58.038Z" }, + { url = "https://files.pythonhosted.org/packages/5f/75/aaa38c6bc2d085f6605b34eabdc6a8a4e0b2e61fc9c8e6e52b28e97b3125/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:52e24eacdb536cade636aa90fb851835222becff8484b7001fdc78cb0290f2aa", size = 2087425, upload-time = "2026-08-28T09:59:59.898Z" }, + { url = "https://files.pythonhosted.org/packages/55/ae/fcab4cfc39aba3689e1d20c8b5250ad280957022c09af2ed9cd585602a5e/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:37ae34309d7bd8c0d61ab839668058f2a7962ea1fc51d105d2db228fe0618034", size = 2139306, upload-time = "2026-08-28T10:00:03.057Z" }, + { url = "https://files.pythonhosted.org/packages/2d/f4/f1d03a4bc9d9acbc62f4d742b8a319af52f71885079868b2ff8e48a651ee/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:0cdbada856a1c69a7624a64d3d9aefe79300bd6ef827b43a4f265010b9b55184", size = 2144589, upload-time = "2026-08-28T10:00:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/83/f3/7a53bb1356de514a4cd295f25b6ac39237895620c0462d2592b76c16e114/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:545f26c504b27c3758439a5e6d9349931f0a04f855668d5fe323c89e82300a38", size = 2288882, upload-time = "2026-08-28T10:00:07.931Z" }, + { url = "https://files.pythonhosted.org/packages/cd/94/5a81583660c175c59d49ffb09f4b3a44debeaf86a19fca664ae1cdd9ee32/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:ff218293c9c806138dca139765e3b067621be52bcd93cdc14c7711be7ddc90a9", size = 2335210, upload-time = "2026-08-28T10:00:10.177Z" }, + { url = "https://files.pythonhosted.org/packages/5a/9f/5d685c2693b972d1a59c998586e8823712b66603aeff47ee60a4bdaafd37/pydantic_core-2.46.5-cp314-cp314t-win32.whl", hash = "sha256:97cf3eb53a8cccacf9d46686a0926186c9bfb5574f2ed66d3639d5fe117cd3a9", size = 1921180, upload-time = "2026-08-28T10:00:12.35Z" }, + { url = "https://files.pythonhosted.org/packages/70/12/5c94ee16d65a37a15f9e869f5e6256df111154491173801a4c5e800ab548/pydantic_core-2.46.5-cp314-cp314t-win_amd64.whl", hash = "sha256:d2f9fc07a8042a8f95925b35c4f04f469707c981fc33245b6ca187cf5d2dd290", size = 2020515, upload-time = "2026-08-28T10:00:14.774Z" }, + { url = "https://files.pythonhosted.org/packages/63/19/67830dda664e6bdf9285ee2e40f355d0d7d6b92aa0c42e8d217bb8d33d36/pydantic_core-2.46.5-cp314-cp314t-win_arm64.whl", hash = "sha256:acf8a67ba51f4ca9ddbd0e6b3000a65ac51ab734661778b3e7ba64d99a710f2f", size = 1989276, upload-time = "2026-08-28T10:00:16.984Z" }, + { url = "https://files.pythonhosted.org/packages/af/1e/ecca01fce348f7e8afa9572441ff6f7d1cc70d21e4859f33944d10877e1e/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:c14ad3bdc85ee7f318742c457ca3968a92126d144b15721c759033bfb06296c2", size = 2075342, upload-time = "2026-08-28T10:00:51.353Z" }, + { url = "https://files.pythonhosted.org/packages/1f/4c/af80c7a8032dfc897040ad5cb772bebde529a381186499e6e29987f23f8c/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0bddb4020d8f04175865ccd17eff3040874fc11fb593f424edb452653b4b947c", size = 1907219, upload-time = "2026-08-28T10:00:53.438Z" }, + { url = "https://files.pythonhosted.org/packages/be/3e/54d89e2b092e778716bf6153634ef479e955f48c261090be23aa1e0fb0b5/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2471fd51c61c610e1dcf7de44d7299283661654d11264ab4802b303368d69c47", size = 1953393, upload-time = "2026-08-28T10:00:55.58Z" }, + { url = "https://files.pythonhosted.org/packages/ea/89/828ee90cda28ce17bdefaa3a6eaf74fe430e113295a10e6126beca559d6c/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b10ec717381bdbfafef34607824db4c91de69ff085e4fca3b2af91b4fa17e68a", size = 2099024, upload-time = "2026-08-28T10:00:57.794Z" }, + { url = "https://files.pythonhosted.org/packages/df/dd/053c2e4303f791f3b8f8a14ab0b22008e8eb21d868c0c90b4f9be705b76a/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:013d6f3483d81e02e7c328831808f336c8596ee33b4bd4026b9ffb1e960b8942", size = 2062540, upload-time = "2026-08-28T10:01:00.318Z" }, + { url = "https://files.pythonhosted.org/packages/d7/dd/a18df751a5e37dd51bfad7f68e766999125bebe68c9e1d10a493ad01bd63/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:e9c134bb666dd54b778b9fc0d2b50cbb7f979b9e3716f26a88c9ab3b6fc1dd0f", size = 1902040, upload-time = "2026-08-28T10:01:02.529Z" }, + { url = "https://files.pythonhosted.org/packages/b7/13/01d40f9d07ce8a779fd6e0bd8ad4fba91309500dd67b869e2e219d261a6d/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:347ec774390c87326a2e4929d58d3f7e8763a104d5d35f4cd595a4c952366433", size = 1967479, upload-time = "2026-08-28T10:01:05.004Z" }, + { url = "https://files.pythonhosted.org/packages/fa/04/c81d4841331c2178b6fb09ae225425e110ed72d990c9fe556c4ec03d1013/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e24d8f05fa2d28513d94e877e9c75ad66175376209b3977f916e240e623193c", size = 2111034, upload-time = "2026-08-28T10:01:07.345Z" }, + { url = "https://files.pythonhosted.org/packages/20/21/22102e9950b3049526d20e811b95396508377d87651edd2b80d2b3d28659/pydantic_core-2.46.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ab4b66edffb32d9e951efb3814bd104b8367a7501b81b955cacb5726d897389f", size = 2071333, upload-time = "2026-08-28T10:01:09.636Z" }, + { url = "https://files.pythonhosted.org/packages/d8/18/87aefa427d191e6d3ab1447f1efc1cdcac86af1069239b133e8a0fd7f7c9/pydantic_core-2.46.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:337639ba62a11acde6ef3aeb08c8ea755f8ef1fe5e513356c0f36a2b0d7568b0", size = 1912713, upload-time = "2026-08-28T10:01:12.285Z" }, + { url = "https://files.pythonhosted.org/packages/1f/93/fd89e9ad49b1805ca94d24ce1088b7d305f05c35ffafcedb9819d03588a0/pydantic_core-2.46.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:413a717a410d0c817ef5b786a059415550b3794e1d0c2abffd9efb93a3d9f7b4", size = 2090926, upload-time = "2026-08-28T10:01:15.19Z" }, + { url = "https://files.pythonhosted.org/packages/6f/45/8e59dab6acf8d35f02f0a958980074f31038968bdb2c983fcae9d1efee03/pydantic_core-2.46.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1e449def1945a462c464331254e5a44fca7c3b4f9aedf59ec2f50f8066dd8e25", size = 2131303, upload-time = "2026-08-28T10:01:17.937Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a5/e1d4dc5180dd887a9522efc1f8716b8692b7606b1d3273d7862eaf66be44/pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:a445486499897b88a7d6c310c88ed64dd37b1b59bfd7ae9107490bbb362f47d6", size = 2145128, upload-time = "2026-08-28T10:01:20.694Z" }, + { url = "https://files.pythonhosted.org/packages/c2/d7/ad493864a7fb21c0c4df98f965e2db430cb25a9d7369b5778d5016c09fd9/pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:2d330aaba8621b1edcec8ae2c4050f63b84ccf6d98723a8f212e9684713abf0e", size = 2294560, upload-time = "2026-08-28T10:01:23.495Z" }, + { url = "https://files.pythonhosted.org/packages/02/8e/b41c84c913f29973a268e6c2b5bbf13c95adb9956c126d10da11ba3b2bef/pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b6acfb46a814762367fb7ba0828b0a17d441b92ce249a0e007474c9072662dda", size = 2317531, upload-time = "2026-08-28T10:01:26.334Z" }, + { url = "https://files.pythonhosted.org/packages/db/1d/068464f23075f66a8f1b806935e9cd9363ee446636ea70d2c22ee8659dbf/pydantic_core-2.46.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d0a24b40877af2de4950252be9d21eaf7fb07660f3c2cae1f56c6b599ada5266", size = 2140686, upload-time = "2026-08-28T10:01:28.947Z" }, ] [[package]] @@ -1758,32 +1905,32 @@ wheels = [ [[package]] name = "pygments" -version = "2.20.0" +version = "2.21.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, + { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, ] [[package]] name = "pyjwt" -version = "2.13.0" +version = "2.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3b/81/58d0ac84e1ef3a3843791d6954d94c0b33d526c75eeb1efbce9d0a4c4077/pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423", size = 107515, upload-time = "2026-05-21T19:54:36.618Z" } +sdist = { url = "https://files.pythonhosted.org/packages/02/a5/5197bfd06417837ac079921c66fa6393f1dea3557272a263cebfef69e432/pyjwt-2.15.0.tar.gz", hash = "sha256:b11c5f9791d7bf51c2b39a81ed669f6b2dbbd669df2942f6c60167e9e3d1abe4", size = 120513, upload-time = "2026-09-23T16:56:00.689Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728", size = 31274, upload-time = "2026-05-21T19:54:35.362Z" }, + { url = "https://files.pythonhosted.org/packages/e8/55/40e45bf052ee8ee12a4dfd785519660f8effa7b065442b91646ec6828619/pyjwt-2.15.0-py3-none-any.whl", hash = "sha256:7a3742debf6b879e912dbb9819ceec1594be812452b78c5f2e2dfc56564954f8", size = 33680, upload-time = "2026-09-23T16:55:59.241Z" }, ] [[package]] name = "pyparsing" -version = "3.3.2" +version = "3.3.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/11/b213bebff182584360cb8d17c72c1677fec5c5c228de439e63bcf8ab1c8f/pyparsing-3.3.3.tar.gz", hash = "sha256:928ae7e20211f3b6f3915a72f06a0cfd29ab9d24279dd6346b6b1a7146397d36", size = 1050487, upload-time = "2026-09-20T20:59:05.609Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, + { url = "https://files.pythonhosted.org/packages/38/bb/d215ee7c73b61497b28a5503f9f53523f294fcc936762b7caf90e0c1c2b5/pyparsing-3.3.3-py3-none-any.whl", hash = "sha256:ece8c00a69cf01b45d0b1dedabb469c90d8caf996d4fda40f147627a122849a4", size = 126420, upload-time = "2026-09-20T20:59:04.025Z" }, ] [[package]] @@ -1891,11 +2038,11 @@ wheels = [ [[package]] name = "pytz" -version = "2026.3.post1" +version = "2026.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fb/48/fb042503b6ca6cd271261dc559fd6432f7d8c713153e9ec5c591af4dfc1c/pytz-2026.3.post1.tar.gz", hash = "sha256:2211d3fcf9a797d3405cac96ac7f61d80e6a644f72a3309607282fe8a2010c5d", size = 319745, upload-time = "2026-07-25T15:12:07.385Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/ed/fa23d28713004418bbf2407127f80f385bfb0bf4e677bef02c25c27b00ee/pytz-2026.4.tar.gz", hash = "sha256:464303645bafafd72418898368b2429458f709cf1eb6a15372fbcc396b64da63", size = 318444, upload-time = "2026-09-24T03:58:40.443Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/7b/39c34ca613b0b198cb866466651b26b045e2009864c5183c979a3b83f383/pytz-2026.3.post1-py2.py3-none-any.whl", hash = "sha256:dd95840dd199baea12d9cc096a1d452caa6596a1c1e4b5f3dbd1541855d5e815", size = 508283, upload-time = "2026-07-25T15:12:05.782Z" }, + { url = "https://files.pythonhosted.org/packages/d5/11/e3df59614bebe0ca50c905442603eda64ae7ac19811d4e4f74aa2d51ca85/pytz-2026.4-py2.py3-none-any.whl", hash = "sha256:9d514388fbc89ca0833203464272ac485b8828568ab73532f5020f17e892a0ff", size = 506747, upload-time = "2026-09-24T03:58:38.629Z" }, ] [[package]] @@ -2000,36 +2147,36 @@ wheels = [ [[package]] name = "ruff" -version = "0.16.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/73/e1/4508a569211b35599016e84ba65c1a992b7a4004b4b6c4bea02a851cba1b/ruff-0.16.2.tar.gz", hash = "sha256:c3d7828d12e8927a6fc65fe38e2c2541b9e762d360a1786d752cb1b8883b3c9c", size = 4885811, upload-time = "2026-08-07T13:31:01.432Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/14/57/db19951540f98859c956b50bdb4d31089b4d91e9f15e2968e7d5193806d5/ruff-0.16.2-py3-none-linux_armv6l.whl", hash = "sha256:3c8de4cf2181f01d57946d87d777aa52916976fc09942aed89938fab5e013318", size = 10847925, upload-time = "2026-08-07T13:30:14.468Z" }, - { url = "https://files.pythonhosted.org/packages/13/5a/995fe85a8470d3e391ac0f7fa8054bb454eaf33ee138196d6172ed1079c0/ruff-0.16.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9a48cc05c6fbc811ca81b5d7ba95375affea6582d1b8024e455e41afbbf55344", size = 11072662, upload-time = "2026-08-07T13:30:18.143Z" }, - { url = "https://files.pythonhosted.org/packages/32/53/370d767c61c71a971a4ace36703a7ecd8c393956349a7325d7fab2b56827/ruff-0.16.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a2c0d14fcbb26c91f0f867a6dc9bd71bbc30b1b6151829c884f23faeab2e5700", size = 10566771, upload-time = "2026-08-07T13:30:20.899Z" }, - { url = "https://files.pythonhosted.org/packages/85/d6/9d96948caf5a632be62d62202d5ec914d6856f204fd79eb036e5915e79ea/ruff-0.16.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:335c621622c4650330be50842561c6586ac6971bb8ab5407fe34dcc9efb16bbe", size = 10975825, upload-time = "2026-08-07T13:30:23.517Z" }, - { url = "https://files.pythonhosted.org/packages/3b/92/ea87129b3414acb0b5770563779c51804d37ac67675c7ba35447ddb14773/ruff-0.16.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20e66910f2c37cc753f9ef6580c914a621b80c4fa3549d3e3521e29d0f5bfc3f", size = 10649437, upload-time = "2026-08-07T13:30:26.097Z" }, - { url = "https://files.pythonhosted.org/packages/ac/43/f8f291dcd4af5bb7872b74fdfa41a7cd7c856ca1d4069670971cf1b9f5cb/ruff-0.16.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7e36fbfba65510548156902bcf1350a979a958ce0347ce0f90d73894036b39f", size = 11446761, upload-time = "2026-08-07T13:30:28.752Z" }, - { url = "https://files.pythonhosted.org/packages/71/4a/ef991fb2fcf516ab71f0808adcdd8da5e18c8cde447f4ceaf5f47a5132a5/ruff-0.16.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f0eab35f80df8f134aae5d1630e751901321d317cc8e50dc39e36fa3ed34cd12", size = 12336364, upload-time = "2026-08-07T13:30:31.468Z" }, - { url = "https://files.pythonhosted.org/packages/f3/24/f615e74f307e6ca0e56a482872477b856c70d530aa356abfb6dfe5ca8a80/ruff-0.16.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40ea8c0594feb894e89c8c61ab9c103d38b0ea72dfde6c594107147ca31b1140", size = 11630720, upload-time = "2026-08-07T13:30:34.426Z" }, - { url = "https://files.pythonhosted.org/packages/c5/d3/8ef50149e8412a77f7ab409efdef0e2b23803707a3863da4fc64cb23d459/ruff-0.16.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab3d62dde0b19facdd632008cc4827fc28ada7736c6bd35ab6f1050f0bfed53f", size = 11466130, upload-time = "2026-08-07T13:30:36.958Z" }, - { url = "https://files.pythonhosted.org/packages/dd/a7/a19334985c4dea8c381981fa252cd854c7ee52dc4b1686dc16f4a911c702/ruff-0.16.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:e43e1f5b8388da9eca1b9e88328d47a5cec794633ccf6f7484ac2dd15eee92c0", size = 11523634, upload-time = "2026-08-07T13:30:39.822Z" }, - { url = "https://files.pythonhosted.org/packages/6e/6c/96d192b0e742412ceda08c0a50f9669b253dde9fd6a60ea1a10c9fa79a63/ruff-0.16.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c24788a980581e1d7ea3a0cbe4344c4fbeb0a6a9b1f4713aa46bb104f8294690", size = 10949807, upload-time = "2026-08-07T13:30:42.745Z" }, - { url = "https://files.pythonhosted.org/packages/fa/51/e26599ceca11e79ee255c7df515995561edf87e9ca1893284e44d98f5a86/ruff-0.16.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:81806b08329130005dd4a8a8394a0c9da8c6f4cafb16ba438d2a2ee6a18bedf1", size = 10646891, upload-time = "2026-08-07T13:30:45.522Z" }, - { url = "https://files.pythonhosted.org/packages/68/01/800c4b1f97bc8d7c6029e06b1f20473a3cf1e13c4933d8f3342add83fc55/ruff-0.16.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:4ce4e02bad779bef557f541a1b31f20d6abeae1cc05ed1b1ac019d4ffd1044c8", size = 11162063, upload-time = "2026-08-07T13:30:48.131Z" }, - { url = "https://files.pythonhosted.org/packages/e4/d0/1477ea50fc5a0d4b0b71d1d63d50770bdd794d90b43e37a7618e63ec9894/ruff-0.16.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e0422abdf70070255fc4073ce9dfc814cc03db577013761ddd09bc1e4a9a4fbd", size = 11556038, upload-time = "2026-08-07T13:30:50.686Z" }, - { url = "https://files.pythonhosted.org/packages/b8/76/a7776f32048d991e16d4fa8ff91790b877342d3596cc3ed04acdbf1aaedc/ruff-0.16.2-py3-none-win32.whl", hash = "sha256:bf3a63d78fb39f4bf5ac8ae52051c5520505301abe19ba4e204c453b3f09bb0b", size = 10872850, upload-time = "2026-08-07T13:30:53.471Z" }, - { url = "https://files.pythonhosted.org/packages/00/0d/929c800d920e61397d82a01b60bffc68da3052c17d31de59efaad2e4ed75/ruff-0.16.2-py3-none-win_amd64.whl", hash = "sha256:bcabe2f6d0fc7819f1431793005af4e4de7371927d037345bf941252b195b9fa", size = 12023338, upload-time = "2026-08-07T13:30:56.193Z" }, - { url = "https://files.pythonhosted.org/packages/5b/6c/93e26c22c5f78ff87363e07da49c84955affbeb1098bd1936bf3b3f293bf/ruff-0.16.2-py3-none-win_arm64.whl", hash = "sha256:d614e95cedf38a2053fd351c55b103ba30d017d61688fdbfd40ee0412852a99f", size = 11374065, upload-time = "2026-08-07T13:30:58.775Z" }, +version = "0.16.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/78/449cb84790bd5cc3823b2652ee405a4558856e5c4195aee3a16bf7b3eb5d/ruff-0.16.8.tar.gz", hash = "sha256:9247bf92b5f04d825c8639a4fe423ec2e4222acd9222e58412b0dab7e442798b", size = 4938814, upload-time = "2026-09-16T15:54:46.688Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/25/6071aabc530e9be7e2c195e8fe3f7aea2735405b6cf447212832d7811831/ruff-0.16.8-py3-none-linux_armv6l.whl", hash = "sha256:6ffbd6d87383c1edf5f6fa890f10200950240d7c1a16052a19a09d3a2307dd38", size = 10048966, upload-time = "2026-09-16T15:53:57.605Z" }, + { url = "https://files.pythonhosted.org/packages/54/98/07f90ecbc74dd5fb5764f11f2bc774d6a7cffef92d2ff5f5b4e9e23c754e/ruff-0.16.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:42ed6b878ed61e3acca92f2730a17acff39286944ea82398544696366a6f925e", size = 10165498, upload-time = "2026-09-16T15:54:01.14Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1f/e6a712e3b47cad4a40600134105ed193cb773f618a42eb7ba323cb812cc0/ruff-0.16.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7ea781c7f2afba8c6a505ea0fb3f994020249e0c450635f5381286fea6b46170", size = 9830004, upload-time = "2026-09-16T15:54:03.998Z" }, + { url = "https://files.pythonhosted.org/packages/23/f2/311a08776d75d81c7676e20b6b020ae63cbe881fcdc7a8dd64e6e18bdd93/ruff-0.16.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8efeae3bbe414a5efefda11a792dfb51ef90ac48d50c4830de2f644caf3e8659", size = 9986558, upload-time = "2026-09-16T15:54:06.804Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ed/37b6cb3d3ba8c73e68ae3eb1d502383beb5aa05a582bb7bb3a922f929f54/ruff-0.16.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a79b795469fef7fc6e908b218eed2eb17332afd85031db6480dc864560e69b2", size = 9877332, upload-time = "2026-09-16T15:54:09.552Z" }, + { url = "https://files.pythonhosted.org/packages/22/cc/40873a8f36ad084cc540d55fcca7077264d5b13b24659e9180c176fb2b08/ruff-0.16.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fdc5563cdc50555e6fba39322850860e9267c1b3d12c26a74729d8604c3c812", size = 10507125, upload-time = "2026-09-16T15:54:12.152Z" }, + { url = "https://files.pythonhosted.org/packages/c3/e4/fc91a642b78ccbab6b9477720f3644ae7a10a9bcce69a934679cd64f62bc/ruff-0.16.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:34508983c70665578dab88f5223d8e6228307e1135398ca8bfc8b7e9501e282b", size = 11336694, upload-time = "2026-09-16T15:54:15.489Z" }, + { url = "https://files.pythonhosted.org/packages/c2/3d/bbd2a9a600a4e73dc3e7548a249c8d1671273464b55822c6fae50f602dff/ruff-0.16.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:644bb578569e0ffc575741232bd385dacdd6fbe123f1a729e7a225f54aa3957f", size = 10774448, upload-time = "2026-09-16T15:54:18.16Z" }, + { url = "https://files.pythonhosted.org/packages/1a/41/d83af9879a7b6e8bf5fe16b1da0b134049d2f5d3afac12defb0897cb84bd/ruff-0.16.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15e7d226246961db9235098333caa13063906d3851136b84c2900b82f5daa1df", size = 10323796, upload-time = "2026-09-16T15:54:20.743Z" }, + { url = "https://files.pythonhosted.org/packages/f5/2c/cefd07bfe914b84943ea769ade8d607bd22750b965d3228eefd7cebd15d0/ruff-0.16.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a2bf6bc3e9ebdd4449abc6f06cf64b98051a2c61cf94d2fe9596518c881f1a1e", size = 10514115, upload-time = "2026-09-16T15:54:23.497Z" }, + { url = "https://files.pythonhosted.org/packages/f3/9d/76a2e26c79a23be6e6e3664c57bec9e9fc8de155cfb9e4b67ea91b64f9d7/ruff-0.16.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6ca111ba0849539165e9e59d2b442542f3c1e8060ebbdea82494f1ffbccb1e1f", size = 10072582, upload-time = "2026-09-16T15:54:26.185Z" }, + { url = "https://files.pythonhosted.org/packages/2e/d4/f42edddb39668af1a559ceafa3823aedd65633a48dc9768e775485faa2c1/ruff-0.16.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:359a1e5b495448ee1e91018064382ebc86f90e8aac2fed222c7d0e4e8df85fd2", size = 9879644, upload-time = "2026-09-16T15:54:29.278Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/913e3195d95e0378786c6656945c865f534a3560e29139da4882aff630d1/ruff-0.16.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:59e8f5681349474110b24d62e93cfda6593f5fa3473446ca3705200cac1a08b9", size = 10231569, upload-time = "2026-09-16T15:54:32.036Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c4/8aa6ea0bdcedbd1bf87397e2fc4ed8406448ea5842f8660bc6e5f163039d/ruff-0.16.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:efa3e7a16d1baaa79957888dfdf8be9ef2e44db81cb032af06d76632ab59e773", size = 10663666, upload-time = "2026-09-16T15:54:34.838Z" }, + { url = "https://files.pythonhosted.org/packages/3d/02/7f10ef4700bc223c30a3fdd10631a29830c45524b810a3c7ed947af64591/ruff-0.16.8-py3-none-win32.whl", hash = "sha256:55793ba85c69921e89be061426d91a78652d6e50317c962240922747a4eb713f", size = 10093472, upload-time = "2026-09-16T15:54:37.47Z" }, + { url = "https://files.pythonhosted.org/packages/1e/5d/a509c07d714b6da88f2c518b4637cf6f1d46b074be8f0f1e5fb9ff5126fe/ruff-0.16.8-py3-none-win_amd64.whl", hash = "sha256:a6b85621fd3c81e31fc5f5add09c9c078b430db3595ca632efafdec9e64ebfaa", size = 10586899, upload-time = "2026-09-16T15:54:40.488Z" }, + { url = "https://files.pythonhosted.org/packages/fe/a0/50787329e4f20bf9dc9f6230015d46ec69c51a97ace5bc202dae4755365d/ruff-0.16.8-py3-none-win_arm64.whl", hash = "sha256:d075e820af612102ce217f07cc93e69f9490b10ec13ea85fa87bd03d996cef8a", size = 10386316, upload-time = "2026-09-16T15:54:43.332Z" }, ] [[package]] name = "semver" -version = "3.0.4" +version = "3.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/d1/d3159231aec234a59dd7d601e9dd9fe96f3afff15efd33c1070019b26132/semver-3.0.4.tar.gz", hash = "sha256:afc7d8c584a5ed0a11033af086e8af226a9c0b206f313e0301f8dd7b6b589602", size = 269730, upload-time = "2025-01-24T13:19:27.617Z" } +sdist = { url = "https://files.pythonhosted.org/packages/92/f5/e1dfe8e1d91c54ce212fd93916eb01fd1c590f413be0a0978c39a97aa1bb/semver-3.1.0.tar.gz", hash = "sha256:14bc073439513d7773662a338f4db9829cf16c12b74b9568e2d2689975fbd7fc", size = 304162, upload-time = "2026-09-12T18:38:14.045Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a6/24/4d91e05817e92e3a61c8a21e08fd0f390f5301f1c448b137c57c4bc6e543/semver-3.0.4-py3-none-any.whl", hash = "sha256:9c824d87ba7f7ab4a1890799cec8596f15c1241cb473404ea1cb0c55e4b04746", size = 17912, upload-time = "2025-01-24T13:19:24.949Z" }, + { url = "https://files.pythonhosted.org/packages/be/02/bce7a1d70e9d0f6ea8623c16386138c251b5855af6cd3a11433d3fba4e66/semver-3.1.0-py3-none-any.whl", hash = "sha256:6e4998f8b4762acdfea85f9bfd5b4032e0bb50038dc04bf82011b42d5d1145c5", size = 18783, upload-time = "2026-09-12T18:38:12.457Z" }, ] [[package]] @@ -2047,9 +2194,9 @@ dependencies = [ { name = "msgpack" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, - { name = "numpy", version = "2.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy", version = "2.5.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "pandas", version = "3.0.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pandas", version = "3.0.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "pip" }, { name = "psutil" }, { name = "pydantic" }, @@ -2070,7 +2217,7 @@ dependencies = [ [package.optional-dependencies] plot = [ { name = "matplotlib", version = "3.10.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "matplotlib", version = "3.11.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "matplotlib", version = "3.11.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "plotly" }, ] @@ -2099,9 +2246,9 @@ requires-dist = [ { name = "matplotlib", marker = "extra == 'plot'", specifier = ">=3.10.0,<4.0.0" }, { name = "msgpack", specifier = ">=1.2.1" }, { name = "numpy", specifier = ">=2.0.0,<3.0.0" }, - { name = "pandas", specifier = ">=2.2.3,<3.0.6" }, + { name = "pandas", specifier = ">=2.2.3,<3.0.7" }, { name = "pip", specifier = ">=26.1.2" }, - { name = "plotly", marker = "extra == 'plot'", specifier = ">=6.0.0,<7.0.0" }, + { name = "plotly", marker = "extra == 'plot'", specifier = ">=6.0.0,<8.0.0" }, { name = "psutil", specifier = ">=6.1.1,<8.0.0" }, { name = "pydantic", specifier = ">=2.11,<3.0.0" }, { name = "pydantic-extra-types", specifier = ">=2.10.5,<3.0.0" }, @@ -2128,7 +2275,7 @@ dev = [ { name = "pytest-sugar", specifier = ">=1.1.1" }, { name = "pytest-timeout", specifier = ">=2.4.0" }, { name = "pytest-xdist", specifier = ">=3.8.0" }, - { name = "ruff", specifier = ">=0.15.14" }, + { name = "ruff", specifier = ">=0.16.8" }, { name = "types-requests", specifier = ">=2.33.0.20260518" }, ] @@ -2254,14 +2401,14 @@ wheels = [ [[package]] name = "types-requests" -version = "2.33.0.20260712" +version = "2.33.0.20260906" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/db/51/703318f7b7be8bee126ec13bf615050f932d0179b8784420f3a0199cc769/types_requests-2.33.0.20260712.tar.gz", hash = "sha256:2141b67ab534a5c5cd2dac5034f2a35f42e699c5bf185eee608c5246a069d7fb", size = 25084, upload-time = "2026-07-12T05:14:20.455Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/18/4c2c0290953f8b3b9612adfcb07b57f144ade3ad32a76764fca42b77c5f3/types_requests-2.33.0.20260906.tar.gz", hash = "sha256:76ab8a0fb736744a0c3deee7aa57b2927e301f078d9e61f5391b3e92002416b9", size = 25263, upload-time = "2026-09-06T06:35:47.707Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/62/e7/010c87f559e216d83f9dc51e939633fd0d0ead3377340181ab0e223cd3b5/types_requests-2.33.0.20260712-py3-none-any.whl", hash = "sha256:de027e28c171d3da529689cbfa023b0b4eab188c8dfa22fd834eebd2cee6e7bb", size = 21392, upload-time = "2026-07-12T05:14:19.616Z" }, + { url = "https://files.pythonhosted.org/packages/60/4c/51ec821d22a45b4162fa3f3e9e94ea4c0f8c49e82363b10955baa5438391/types_requests-2.33.0.20260906-py3-none-any.whl", hash = "sha256:9f53622652bd921ead7a54d665be1b8d518165c8b51cc9d68d944cd6b6bfa8fb", size = 21461, upload-time = "2026-09-06T06:35:45.856Z" }, ] [[package]] @@ -2275,23 +2422,23 @@ wheels = [ [[package]] name = "typing-inspection" -version = "0.4.3" +version = "0.4.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6d/bc/4eae18cd40c65798a16267572ba346c11f599d44b01603dbd843342042bc/typing_inspection-0.4.3.tar.gz", hash = "sha256:c5f9ec1530b5c1e2c9bc34a84d9a3466ed1b2f3f2fa9f901368d9c5596210e4d", size = 76711, upload-time = "2026-08-10T09:39:18.063Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/26/b09b8010994eccc3c09092e6b34058f36a460eea2d4c3e8b910c695975a0/typing_inspection-0.4.4.tar.gz", hash = "sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47", size = 76928, upload-time = "2026-08-12T12:37:25.997Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/42/f7/7a3935abdebd5cf18705a5f0335dd6a3a18bef3baa7cb9edc3b6b9922cc8/typing_inspection-0.4.3-py3-none-any.whl", hash = "sha256:5f42b23858a91e0b4ef521f5418f03a0da3c9216fd2995ef5e73463100e676cd", size = 14693, upload-time = "2026-08-10T09:39:16.693Z" }, + { url = "https://files.pythonhosted.org/packages/67/81/4add07e5172b7ac40d8ed5ff580409a7801a4fe26d529bdd915401dabfbe/typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147", size = 14750, upload-time = "2026-08-12T12:37:24.648Z" }, ] [[package]] name = "tzdata" -version = "2026.3" +version = "2026.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/92/ff/5a28bdfd8c3ebec42564ac7d0e54ca3db65044a9314a97f9564fa7a1e926/tzdata-2026.3.tar.gz", hash = "sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415", size = 198674, upload-time = "2026-07-10T08:50:37.887Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/31/3d74fa778a63b98b7374323befcc0be5ab3bd94afd4096a0124e7379152c/tzdata-2026.4.tar.gz", hash = "sha256:f1b8bd365d8d210c55353f4d7f8d6d8561c0ba50d704b700d195a9424bba0d79", size = 199350, upload-time = "2026-09-12T12:56:03.251Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl", hash = "sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931", size = 348168, upload-time = "2026-07-10T08:50:36.46Z" }, + { url = "https://files.pythonhosted.org/packages/f9/bc/8737e8d54cf51106118039b83f485a4783112fab49ea9d044b234978a46e/tzdata-2026.4-py2.py3-none-any.whl", hash = "sha256:c2169a8b0a7a5e9674da5a135ccdfb2b3e671b333ed9fed17b41f73c34476e81", size = 347494, upload-time = "2026-09-12T12:56:01.67Z" }, ] [[package]] @@ -2301,7 +2448,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, - { name = "numpy", version = "2.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy", version = "2.5.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "packaging" }, { name = "sympy" }, ] @@ -2312,9 +2459,9 @@ wheels = [ [[package]] name = "urllib3" -version = "2.7.0" +version = "2.8.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/05/b17359e1cefb4f909b5e40b1b90a496d987258916dbbf88e842c729f510e/urllib3-2.8.0.tar.gz", hash = "sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63", size = 458972, upload-time = "2026-09-15T19:29:36.253Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, + { url = "https://files.pythonhosted.org/packages/92/9d/c4e665119135114480843e7ab388fa94d8480650450e6f8e26b70d323a4c/urllib3-2.8.0-py3-none-any.whl", hash = "sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3", size = 135717, upload-time = "2026-09-15T19:29:34.577Z" }, ] From fa06cf5c65da7b17b4b6cb65bac25cf9f756c952 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Zar=C4=99bski?= Date: Thu, 24 Sep 2026 09:28:21 +0100 Subject: [PATCH 14/23] Re-fix linting --- pyproject.toml | 1 + simvue/api/objects/base.py | 7 +++---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 12377e5f..135589f1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -102,6 +102,7 @@ ignore = [ "S404", "S603", "PLW0717", + "TC003", ] dummy-variable-rgx = "^_+$" extend-select = [ diff --git a/simvue/api/objects/base.py b/simvue/api/objects/base.py index 59e4082a..84eb1c2f 100644 --- a/simvue/api/objects/base.py +++ b/simvue/api/objects/base.py @@ -13,9 +13,11 @@ import types import typing import uuid +from collections.abc import Generator if typing.TYPE_CHECKING: - from collections.abc import Callable, Generator + import pathlib + from collections.abc import Callable import msgpack import pydantic @@ -41,9 +43,6 @@ from simvue.exception import ObjectNotFoundError from simvue.utilities import staging_merger -if typing.TYPE_CHECKING: - import pathlib - try: from typing import Self, override except ImportError: From 91c36085bfa5877311f631d2810be4cdf3d9831d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Zar=C4=99bski?= Date: Fri, 25 Sep 2026 14:40:10 +0100 Subject: [PATCH 15/23] =?UTF-8?q?=F0=9F=90=9B=20Fixed=20bugs=20arising=20f?= =?UTF-8?q?rom=20merge=20and=20linting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- simvue/api/objects/base.py | 7 +++++-- simvue/api/request.py | 43 +++++++++++++++----------------------- tests/unit/test_request.py | 4 ++-- 3 files changed, 24 insertions(+), 30 deletions(-) diff --git a/simvue/api/objects/base.py b/simvue/api/objects/base.py index 84eb1c2f..96ace600 100644 --- a/simvue/api/objects/base.py +++ b/simvue/api/objects/base.py @@ -696,6 +696,7 @@ def commit(self) -> list[dict[str, str]] | dict[str, str] | None: self.id, self._staging, ) + _response = self._post_single(**self._staging) elif self._staging: self._logger.debug( "Pushing updates from staged data for %s '%s': %s", @@ -790,11 +791,13 @@ def _post_single( data: list[dict[str, object]] | dict[str, object] | None = None, **kwargs, ) -> dict[str, typing.Any] | list[dict[str, typing.Any]]: - _data = kwargs if is_json else msgpack.packb(data or kwargs, use_bin_type=True) # Remove any extra keys for key in self._local_only_args: - _ = _data.pop(key, None) + _ = (data or kwargs).pop(key, None) + + if not is_json: + kwargs = msgpack.packb(data or kwargs, use_bin_type=True) _response = sv_post( url=f"{self.base_url}", diff --git a/simvue/api/request.py b/simvue/api/request.py index cc67713b..807166a5 100644 --- a/simvue/api/request.py +++ b/simvue/api/request.py @@ -10,7 +10,7 @@ import json as json_module import logging import typing -from collections.abc import Callable, Generator +from collections.abc import Generator import requests from tenacity import ( @@ -59,21 +59,15 @@ def _rewind_request_streams(retry_state: RetryCallState) -> None: """Rewind file-like request bodies before retrying.""" _kwargs = typing.cast("dict[str, typing.Any]", retry_state.kwargs) _files = typing.cast("dict[str, FileValue]", _kwargs.get("files") or {}) - _file_values: list[FileValue] = list(_files.values()) - - if _data := typing.cast("FileValue", _kwargs.get("data")): - _file_values.append(_data) + streams: tuple[FileValue | None, ...] = ( + *_files.values(), + retry_state.kwargs.get("data"), + ) - for value in _file_values: - if isinstance(value, tuple): - _stream, *_ = value - else: - _stream = value - _seek = typing.cast( - "Callable[[int], object] | None", getattr(_stream, "seek", None) - ) - if _seek: - _ = _seek(0) + for value in streams: + stream = value[1] if isinstance(value, tuple) else value + if callable(seek := getattr(stream, "seek", None)): + seek(0) @retry( @@ -358,31 +352,28 @@ def get_json_from_response( expected_type: type[dict[str, object] | list[object]] = dict, ) -> dict[str, object] | list[dict[str, object]]: try: - json_response: list[dict[str, object]] | dict[str, str | object] | None = ( - typing.cast( - "list[dict[str, object]] | dict[str, str | object] | None", - response.json(), - ) - ) + json_response: ( + list[dict[str, object]] | dict[str, str | object] | typing.Any | None + ) = response.json() json_response = json_response or ({} if expected_type is dict else []) decode_error = "" except requests.exceptions.JSONDecodeError as e: json_response = {} if allow_parse_failure else None - decode_error = f"{e}" + decode_error = f": {e}" error_str = f"{scenario} failed for url '{response.url}'" details: str | None = None if (_status_code := response.status_code) in expected_status: - if not isinstance(json_response, expected_type): + if json_response and not isinstance(json_response, expected_type): details = ( f"expected type '{expected_type.__name__}' " f"but got '{type(json_response).__name__}'" ) - elif not json_response: - details = f"could not request JSON response: {decode_error}" - else: + elif json_response is not None: return json_response + else: + details = f"could not request JSON response{decode_error}" elif isinstance(json_response, dict): error_str += f" with status {_status_code}" details = typing.cast("str", (json_response or {}).get("detail")) diff --git a/tests/unit/test_request.py b/tests/unit/test_request.py index 019513b5..a3bda893 100644 --- a/tests/unit/test_request.py +++ b/tests/unit/test_request.py @@ -20,7 +20,7 @@ def test_post_retry_rewinds_file_stream(mocker, tuple_form: bool) -> None: stream = io.BytesIO(_PAYLOAD) file_value = ("artifact.bin", stream) if tuple_form else stream - def fake_post(*args, **kwargs): + def fake_post(*_, **kwargs): file = kwargs["files"]["file"] file = file[1] if isinstance(file, tuple) else file payloads.append(file.read()) @@ -47,7 +47,7 @@ def test_put_retry_rewinds_data_stream(mocker) -> None: payloads = [] stream = io.BytesIO(_PAYLOAD) - def fake_put(*args, **kwargs): + def fake_put(*_, **kwargs): payloads.append(kwargs["data"].read()) return _Response(503 if len(payloads) == 1 else 200) From c6a06aa344c4ee9881be0ef1e3e7f2b35c063701 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Zar=C4=99bski?= Date: Fri, 25 Sep 2026 15:22:13 +0100 Subject: [PATCH 16/23] Redo (Again) linting --- pyproject.toml | 22 ++++++++++++---------- simvue/api/objects/alert/metrics.py | 2 +- simvue/api/objects/storage/base.py | 5 ++--- simvue/eco/api_client.py | 7 +++++-- simvue/handler.py | 2 +- simvue/serialization.py | 4 ++-- tests/unit/test_sender.py | 14 +++++++++++--- 7 files changed, 34 insertions(+), 22 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 135589f1..9ca0d68c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -92,17 +92,19 @@ dev = [ extend-exclude = ["tests", "examples", "notebooks", "simvue/pynvml.py"] [tool.ruff.lint] +preview = true ignore = [ - "COM812", - "PLR0904", - "PLR6301", - "D203", - "D212", - "D417", - "S404", - "S603", - "PLW0717", - "TC003", + "missing-trailing-comma", + "too-many-public-methods", + "no-self-use", + "incorrect-blank-line-before-class", + "multi-line-summary-first-line", + "undocumented-param", + "suspicious-subprocess-import", + "subprocess-without-shell-equals-true", + "too-many-statements-in-try-clause", + "suspicious-pickle-import", + "typing-only-standard-library-import", ] dummy-variable-rgx = "^_+$" extend-select = [ diff --git a/simvue/api/objects/alert/metrics.py b/simvue/api/objects/alert/metrics.py index 1bb68781..f4f3c48e 100644 --- a/simvue/api/objects/alert/metrics.py +++ b/simvue/api/objects/alert/metrics.py @@ -12,7 +12,7 @@ try: from typing import Self, override except ImportError: - from typing import Self, override + from typing_extensions import Self, override from simvue.api.objects.base import write_only from simvue.models import NAME_REGEX diff --git a/simvue/api/objects/storage/base.py b/simvue/api/objects/storage/base.py index ec789dff..ae26666a 100644 --- a/simvue/api/objects/storage/base.py +++ b/simvue/api/objects/storage/base.py @@ -59,10 +59,9 @@ def new( """Create a new instance of a storage type.""" @property - @classmethod - def type(cls) -> str: + def type(self) -> str: """Return storage type.""" - return cls._type + return self._type @property @staging_check diff --git a/simvue/eco/api_client.py b/simvue/eco/api_client.py index a605ca6e..dbd9f0e5 100644 --- a/simvue/eco/api_client.py +++ b/simvue/eco/api_client.py @@ -11,9 +11,9 @@ import functools import http import logging +import typing import geocoder -import geocoder.ipinfo import pydantic import requests @@ -24,6 +24,9 @@ except ImportError: from typing_extensions import Self +if typing.TYPE_CHECKING: + from geocoder.ipinfo import IpinfoQuery + CO2_SIGNAL_API_ENDPOINT: str = ( "https://api.electricitymap.org/v3/carbon-intensity/latest" ) @@ -57,7 +60,7 @@ def from_json_response(cls, json_response: dict[str, str]) -> "CO2SignalResponse @functools.lru_cache -def _call_geocoder_query() -> geocoder.ipinfo.IpinfoQuery: +def _call_geocoder_query() -> "IpinfoQuery": """Call GeoCoder API for IP location. Cached so this API is only called once per session as required. diff --git a/simvue/handler.py b/simvue/handler.py index a1963497..46dffa68 100644 --- a/simvue/handler.py +++ b/simvue/handler.py @@ -9,7 +9,7 @@ try: from typing import override except ImportError: - from typing import override + from typing_extensions import override class Handler(logging.Handler): diff --git a/simvue/serialization.py b/simvue/serialization.py index 4b4155ff..8e1f3446 100644 --- a/simvue/serialization.py +++ b/simvue/serialization.py @@ -186,7 +186,7 @@ def _serialize_json(data: typing.Any) -> tuple[str, str] | None: def _serialize_pickle(data: typing.Any) -> tuple[str, str] | None: - import pickle # ruff: ignore[import-outside-top-level,suspicious-pickle-import] + import pickle # ruff: ignore[import-outside-top-level] mimetype = "application/octet-stream" data = pickle.dumps(data) @@ -250,7 +250,7 @@ def _deserialize_torch_tensor(data: "Buffer") -> "Tensor | None": def _deserialize_pickle(data) -> typing.Any | None: - import pickle # ruff: ignore[import-outside-top-level,suspicious-pickle-import] + import pickle # ruff: ignore[import-outside-top-level] return pickle.loads(data) # ruff: ignore[suspicious-pickle-usage] diff --git a/tests/unit/test_sender.py b/tests/unit/test_sender.py index 4cccd8e7..89a33fd3 100644 --- a/tests/unit/test_sender.py +++ b/tests/unit/test_sender.py @@ -205,7 +205,15 @@ def test_send_heartbeat(offline_cache_setup, parallel, mocker): # Check requests.put() endpoint called 50 times - once for each of the 5 runs, on all 10 iterations assert spy_put.call_count == 50 + + _all_running: bool = False + _counter: int = 0 - # Get online runs and check all running - [_online_run.refresh() for _online_run in _online_runs] - assert all([_online_run.status == "running" for _online_run in _online_runs]) + while not _all_running and _counter < 100: + # Get online runs and check all running + [_online_run.refresh() for _online_run in _online_runs] + _all_running = all([_online_run.status == "running" for _online_run in _online_runs]) + time.sleep(0.1) + + if not _all_running and _counter >= 100: + raise AssertionError("Failed to launch all runs from sender") From 206cb7bed275793a9fd0ba5b16ae8190f4fc3097 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Zar=C4=99bski?= Date: Fri, 25 Sep 2026 15:33:33 +0100 Subject: [PATCH 17/23] Redo (Again) linting --- simvue/config/user.py | 2 +- simvue/simvue_types.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/simvue/config/user.py b/simvue/config/user.py index 5bfeebd4..bed41753 100644 --- a/simvue/config/user.py +++ b/simvue/config/user.py @@ -20,7 +20,7 @@ try: from typing import Self except ImportError: - from typing import Self + from typing_extensions import Self import simvue.utilities as sv_util from simvue.api.request import get as sv_get diff --git a/simvue/simvue_types.py b/simvue/simvue_types.py index 8bac3445..bd274355 100644 --- a/simvue/simvue_types.py +++ b/simvue/simvue_types.py @@ -5,7 +5,7 @@ try: from typing import TypeAlias except ImportError: - from typing import TypeAlias + from typing_extensions import TypeAlias # ruff: ignore[deprecated-import] if typing.TYPE_CHECKING: From 635d49795a9005d60c094ca67739464ecce9b041 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Zar=C4=99bski?= Date: Fri, 25 Sep 2026 15:43:33 +0100 Subject: [PATCH 18/23] Fix overrides --- simvue/api/objects/events.py | 17 ++++++----------- simvue/api/objects/folder.py | 6 +++--- tests/unit/test_sender.py | 33 ++++++++++++++++++++++----------- 3 files changed, 31 insertions(+), 25 deletions(-) diff --git a/simvue/api/objects/events.py b/simvue/api/objects/events.py index 1f5981de..1a156baa 100644 --- a/simvue/api/objects/events.py +++ b/simvue/api/objects/events.py @@ -19,16 +19,11 @@ from .base import SimvueObject try: - from typing import Self + from typing import Self, override except ImportError: - from typing_extensions import Self + from typing_extensions import Self, override -try: - from typing import override -except ImportError: - from typing_extensions import override - if typing.TYPE_CHECKING: from simvue.api.url import URL @@ -81,7 +76,7 @@ def __init__( @classmethod @pydantic.validate_call - @typing.override + @override def get( cls, run_id: str, @@ -150,7 +145,7 @@ def get( @classmethod @pydantic.validate_call - @typing.override + @override def new( cls, *, @@ -268,7 +263,7 @@ def histogram( return _data - @typing.override + @override def delete(self, **kwargs: object) -> dict[str, typing.Any]: """Event set deletion not implemented. @@ -280,7 +275,7 @@ def delete(self, **kwargs: object) -> dict[str, typing.Any]: """ raise NotImplementedError("Cannot delete event set") - @typing.override + @override def on_reconnect(self, id_mapping: dict[str, str]) -> None: if online_run_id := id_mapping.get(self._staging["run"]): self._staging["run"] = online_run_id diff --git a/simvue/api/objects/folder.py b/simvue/api/objects/folder.py index 1afdc36f..c0aa9a80 100644 --- a/simvue/api/objects/folder.py +++ b/simvue/api/objects/folder.py @@ -92,7 +92,7 @@ def __init__( @classmethod @pydantic.validate_call - @typing.override + @override def new( cls, *, @@ -114,7 +114,7 @@ def new( @classmethod @pydantic.validate_call - @typing.override + @override def get( cls, *, @@ -289,7 +289,7 @@ def ttl(self, time_seconds: int) -> None: """Update the retention period for this folder.""" self._staging["ttl"] = time_seconds - @typing.override + @override def delete( self, *, diff --git a/tests/unit/test_sender.py b/tests/unit/test_sender.py index 89a33fd3..0d1fd16a 100644 --- a/tests/unit/test_sender.py +++ b/tests/unit/test_sender.py @@ -180,35 +180,46 @@ def test_send_heartbeat(offline_cache_setup, parallel, mocker): _path = f"/simvue_unit_testing/objects/folder/{_uuid}" _folder = Folder.new(path=_path, offline=True) _folder.commit() - + _offline_runs = [] - + for i in range(5): _name = f"test_sender_server_ids-{_uuid}-{i}" _run = Run.new(name=_name, folder=_path, offline=True, heartbeat_timeout=1, status="running") _run.commit() - + _offline_runs.append(_run) - + _sender = Sender(threading_threshold=1 if parallel else 10) _sender.upload() _online_runs = [Run(identifier=_sender.id_mapping.get(_offline_run.id)) for _offline_run in _offline_runs] assert all([_online_run.status == "running" for _online_run in _online_runs]) - + + _all_running: bool = False + _counter: int = 0 + + while not _all_running and _counter < 100: + # Get online runs and check all running + [_online_run.refresh() for _online_run in _online_runs] + _all_running = all([_online_run.status == "running" for _online_run in _online_runs]) + time.sleep(0.1) + if not _all_running and _counter >= 100: + raise AssertionError("Failed to launch all runs from sender") + spy_put = mocker.spy(requests, "put") - + # Create heartbeat and send every 0.5s for 5s for i in range(10): time.sleep(0.5) [_offline_run.send_heartbeat() for _offline_run in _offline_runs] Sender(threading_threshold=1 if parallel else 10).upload() - + # Check requests.put() endpoint called 50 times - once for each of the 5 runs, on all 10 iterations assert spy_put.call_count == 50 - _all_running: bool = False - _counter: int = 0 - + _all_running = False + _counter = 0 + while not _all_running and _counter < 100: # Get online runs and check all running [_online_run.refresh() for _online_run in _online_runs] @@ -216,4 +227,4 @@ def test_send_heartbeat(offline_cache_setup, parallel, mocker): time.sleep(0.1) if not _all_running and _counter >= 100: - raise AssertionError("Failed to launch all runs from sender") + raise AssertionError("Failed to keep all runs running from sender") From a4d5b513d8dc1c87bf2e0dbd17fcb99b19252b02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Zar=C4=99bski?= Date: Fri, 25 Sep 2026 15:53:17 +0100 Subject: [PATCH 19/23] Updated prek action version --- .github/workflows/prek.yml | 4 ++-- poetry.lock | 36 +++++++++++++++++++++++++++++++++++- pyproject.toml | 3 ++- simvue/api/objects/stats.py | 3 ++- uv.lock | 30 ++++++++++++++++++++++++++++++ 5 files changed, 71 insertions(+), 5 deletions(-) diff --git a/.github/workflows/prek.yml b/.github/workflows/prek.yml index 6b83b631..cbe51b27 100644 --- a/.github/workflows/prek.yml +++ b/.github/workflows/prek.yml @@ -23,6 +23,6 @@ jobs: - name: Checkout Code uses: actions/checkout@v6 - name: Run Prek - uses: j178/prek-action@v2 + uses: j178/prek-action@v3.0.0 with: - extra-args: '--all-files --skip no-commit-to-branch' + extra-args: "--all-files --skip no-commit-to-branch" diff --git a/poetry.lock b/poetry.lock index d4b80634..ac207b59 100644 --- a/poetry.lock +++ b/poetry.lock @@ -24,6 +24,21 @@ files = [ {file = "attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32"}, ] +[[package]] +name = "basedpyright" +version = "1.40.1" +description = "static type checking for Python (but based)" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "basedpyright-1.40.1-py3-none-any.whl", hash = "sha256:222dc0382caf9816eb23a27cb7059fa96356ddc500b3b7b306072848fd650244"}, + {file = "basedpyright-1.40.1.tar.gz", hash = "sha256:da1c9913b6d169340a0dbb6df76ea97f476ccb697da8f92659ac46032f6d2ce8"}, +] + +[package.dependencies] +nodejs-wheel-binaries = ">=20.13.1" + [[package]] name = "certifi" version = "2026.7.22" @@ -1577,6 +1592,25 @@ pyspark-connect = ["pyspark[connect] (>=3.5.0)"] sql = ["narwhals[duckdb]", "sqlparse (>=0.5.5)"] sqlframe = ["sqlframe (>=3.22.0,!=3.39.3)"] +[[package]] +name = "nodejs-wheel-binaries" +version = "24.19.0" +description = "unoffical Node.js package" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "nodejs_wheel_binaries-24.19.0-py2.py3-none-macosx_13_0_arm64.whl", hash = "sha256:e12cbfd69089504e42fb14194ce734a9dcf3eb38c820ca63dd511d36fb964e9c"}, + {file = "nodejs_wheel_binaries-24.19.0-py2.py3-none-macosx_13_0_x86_64.whl", hash = "sha256:1c890adf4b7e6556ccc1ca66c866bb81884b6c9a581dee4e530dc7f78fb9d514"}, + {file = "nodejs_wheel_binaries-24.19.0-py2.py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:4e029dadfae1295876063c96b236f673487e8c27379fe146c1e2250283520227"}, + {file = "nodejs_wheel_binaries-24.19.0-py2.py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:4196a947bcc883f2003ab101762d729f3e99b5e86b75bd09151563403e2eceb8"}, + {file = "nodejs_wheel_binaries-24.19.0-py2.py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:352e048ab4dd35e7de5f338d1cc4fcbf77a0e93da30bf7336a8217ee246b31d7"}, + {file = "nodejs_wheel_binaries-24.19.0-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:28d078b2ced9e2069516e652dc4b1380e7a1a7f2d3934eccd1611586d283ba4c"}, + {file = "nodejs_wheel_binaries-24.19.0-py2.py3-none-win_amd64.whl", hash = "sha256:67e3abeb9c3830cae8c8487ae8a2af7cc27dfa75af06145cee5ca7d1857c81bd"}, + {file = "nodejs_wheel_binaries-24.19.0-py2.py3-none-win_arm64.whl", hash = "sha256:d9074c665ea68b04e183d82482c86dc907d3a9bd15eb6cf85542cb785266bb36"}, + {file = "nodejs_wheel_binaries-24.19.0.tar.gz", hash = "sha256:db217eef8cab8551667863379b08db4d9067403f6cbbe87481eb40edceb8aa9b"}, +] + [[package]] name = "numpy" version = "2.2.6" @@ -3032,4 +3066,4 @@ plot = ["matplotlib", "plotly"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<3.15" -content-hash = "49cb62d64368313661e6fceec273dfd2ccb3d9aee2e0fdd4f43ddbeae32774c5" +content-hash = "62ea4b12c962321e7faf670d9e2c9278e2a12364f9637b681c3b9f05cb0ba327" diff --git a/pyproject.toml b/pyproject.toml index 9ca0d68c..ceab4529 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,6 +77,7 @@ build-backend = "hatchling.build" [dependency-groups] dev = [ + "basedpyright>=1.40.1", "interrogate>=1.7.0", "jinja2>=3.1.6", "pytest-cov>=7.1.0", @@ -188,7 +189,7 @@ exclude = ["docs", "tests"] [tool.basedpyright] include = ["src", "examples"] -pythonVersion = "3.13" +pythonVersion = "3.14" typeCheckingMode = "off" deprecateTypingAliases = true reportAttributeAccessIssue = "error" diff --git a/simvue/api/objects/stats.py b/simvue/api/objects/stats.py index dca4677f..4f1e13ab 100644 --- a/simvue/api/objects/stats.py +++ b/simvue/api/objects/stats.py @@ -121,8 +121,9 @@ def id(self) -> None: return @override - def on_reconnect(self, **_: object) -> None: + def on_reconnect(self, id_mapping: dict[str, str]) -> None: """No offline to online reconnect functionality for statistics.""" + _ = id_mapping @override @classmethod diff --git a/uv.lock b/uv.lock index 94043246..369f3c88 100644 --- a/uv.lock +++ b/uv.lock @@ -32,6 +32,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, ] +[[package]] +name = "basedpyright" +version = "1.40.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodejs-wheel-binaries" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/ee/8d0b6806338b13526303cf72754351221a32cdb80ab56dcf2c91d6b1ea57/basedpyright-1.40.1.tar.gz", hash = "sha256:da1c9913b6d169340a0dbb6df76ea97f476ccb697da8f92659ac46032f6d2ce8", size = 25131254, upload-time = "2026-09-10T23:17:24.559Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/84/c1e1e845d0453253a98d1ba188d68597f0ba7e552eeabea514d9395d418a/basedpyright-1.40.1-py3-none-any.whl", hash = "sha256:222dc0382caf9816eb23a27cb7059fa96356ddc500b3b7b306072848fd650244", size = 13689276, upload-time = "2026-09-10T23:17:20.322Z" }, +] + [[package]] name = "certifi" version = "2026.7.22" @@ -1271,6 +1283,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/40/b5/1b84b2c784db76d69442334bc8b8748c840f13ca53be086f4f250ad4a0bc/narwhals-2.26.0-py3-none-any.whl", hash = "sha256:29326d74f107c347fd1009bd58e38d9f7c7c5b51e6de97bc93dbc325d9038b54", size = 474034, upload-time = "2026-09-08T13:32:07.159Z" }, ] +[[package]] +name = "nodejs-wheel-binaries" +version = "24.19.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/76/7e97195e14346598565a0de4ca8bdbd5e634b3fb5b1ba590b7b1b89f8a63/nodejs_wheel_binaries-24.19.0.tar.gz", hash = "sha256:db217eef8cab8551667863379b08db4d9067403f6cbbe87481eb40edceb8aa9b", size = 8058, upload-time = "2026-08-19T21:47:19.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/52/0774b52c7be8151ad9d5aff44edc100c3f13d6d8eb3765f63ffa40e69fe8/nodejs_wheel_binaries-24.19.0-py2.py3-none-macosx_13_0_arm64.whl", hash = "sha256:e12cbfd69089504e42fb14194ce734a9dcf3eb38c820ca63dd511d36fb964e9c", size = 56047203, upload-time = "2026-08-19T21:46:43.448Z" }, + { url = "https://files.pythonhosted.org/packages/67/3a/4fdbbfecf2c23d52c0e3f68de7f7c1b3c97a26d328c69c5f6c49c48e340e/nodejs_wheel_binaries-24.19.0-py2.py3-none-macosx_13_0_x86_64.whl", hash = "sha256:1c890adf4b7e6556ccc1ca66c866bb81884b6c9a581dee4e530dc7f78fb9d514", size = 56219459, upload-time = "2026-08-19T21:46:48.45Z" }, + { url = "https://files.pythonhosted.org/packages/5f/a8/0147149415195c59b8a72a594916bfb80d6be4d586f9fbfda313889e0efc/nodejs_wheel_binaries-24.19.0-py2.py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:4e029dadfae1295876063c96b236f673487e8c27379fe146c1e2250283520227", size = 60588256, upload-time = "2026-08-19T21:46:53.298Z" }, + { url = "https://files.pythonhosted.org/packages/f4/89/6631d0982353da1bb7bc00bb1988f702822c62b42634f57999ee53b5c337/nodejs_wheel_binaries-24.19.0-py2.py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:4196a947bcc883f2003ab101762d729f3e99b5e86b75bd09151563403e2eceb8", size = 61123607, upload-time = "2026-08-19T21:46:58.117Z" }, + { url = "https://files.pythonhosted.org/packages/32/a2/fa30f0841e4602995782e124359f9b910c7b481d98decf61ef0b2fc3ebfb/nodejs_wheel_binaries-24.19.0-py2.py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:352e048ab4dd35e7de5f338d1cc4fcbf77a0e93da30bf7336a8217ee246b31d7", size = 62632842, upload-time = "2026-08-19T21:47:03.42Z" }, + { url = "https://files.pythonhosted.org/packages/18/01/22d97ca72213f66cc386ee638db30c2e62757fdf761c6029074bced83d1c/nodejs_wheel_binaries-24.19.0-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:28d078b2ced9e2069516e652dc4b1380e7a1a7f2d3934eccd1611586d283ba4c", size = 63250653, upload-time = "2026-08-19T21:47:07.938Z" }, + { url = "https://files.pythonhosted.org/packages/88/d1/e3be8fa327a795bcaf7a19cd84299e338a7bce32ff0665fdce9cfa22573c/nodejs_wheel_binaries-24.19.0-py2.py3-none-win_amd64.whl", hash = "sha256:67e3abeb9c3830cae8c8487ae8a2af7cc27dfa75af06145cee5ca7d1857c81bd", size = 42448503, upload-time = "2026-08-19T21:47:12.093Z" }, + { url = "https://files.pythonhosted.org/packages/1d/37/34cf28ba1691a060174948a9927fe61091982d6048b2e403071a9acce443/nodejs_wheel_binaries-24.19.0-py2.py3-none-win_arm64.whl", hash = "sha256:d9074c665ea68b04e183d82482c86dc907d3a9bd15eb6cf85542cb785266bb36", size = 40090155, upload-time = "2026-08-19T21:47:16.032Z" }, +] + [[package]] name = "numpy" version = "2.2.6" @@ -2223,6 +2251,7 @@ plot = [ [package.dev-dependencies] dev = [ + { name = "basedpyright" }, { name = "interrogate" }, { name = "jinja2" }, { name = "pytest-cov" }, @@ -2268,6 +2297,7 @@ provides-extras = ["plot"] [package.metadata.requires-dev] dev = [ + { name = "basedpyright", specifier = ">=1.40.1" }, { name = "interrogate", specifier = ">=1.7.0" }, { name = "jinja2", specifier = ">=3.1.6" }, { name = "pytest-cov", specifier = ">=7.1.0" }, From 04ba7227e139346ef7099258ef43edd0e85d4fbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Zar=C4=99bski?= Date: Fri, 25 Sep 2026 16:04:41 +0100 Subject: [PATCH 20/23] Bumped dependencies --- poetry.lock | 58 ++++++++++++++++++++++++++----------------- pyproject.toml | 55 +++++++++++++++++++++-------------------- uv.lock | 67 +++++++++++++++++++++++++++++--------------------- 3 files changed, 102 insertions(+), 78 deletions(-) diff --git a/poetry.lock b/poetry.lock index ac207b59..69be527b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2441,14 +2441,14 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pyjwt" -version = "2.14.0" +version = "2.15.0" description = "JSON Web Token implementation in Python" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "pyjwt-2.14.0-py3-none-any.whl", hash = "sha256:ad0cef71c756a56e74863c2919cf0985f72decbcfcb550ee2f422e7c62b5eedc"}, - {file = "pyjwt-2.14.0.tar.gz", hash = "sha256:77283c83fb56ecf566a886c757a714bc83668e38156de2cce8263302f42e0b86"}, + {file = "pyjwt-2.15.0-py3-none-any.whl", hash = "sha256:7a3742debf6b879e912dbb9819ceec1594be812452b78c5f2e2dfc56564954f8"}, + {file = "pyjwt-2.15.0.tar.gz", hash = "sha256:b11c5f9791d7bf51c2b39a81ed669f6b2dbbd669df2942f6c60167e9e3d1abe4"}, ] [package.dependencies] @@ -2621,6 +2621,18 @@ files = [ [package.dependencies] six = ">=1.5" +[[package]] +name = "python-version" +version = "0.0.2" +description = "Provides a simple utility for checking the python version." +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "python_version-0.0.2-py2.py3-none-any.whl", hash = "sha256:2c7429f3375cee19873892e5db4d10d2b42172d8851a8bb5111729e028470580"}, + {file = "python_version-0.0.2.tar.gz", hash = "sha256:5c16de57c7f2d614621cf468e8f5a20bb6b3cec665c9c7f5e9f9f000bf04fe67"}, +] + [[package]] name = "pytz" version = "2026.3.post1" @@ -2770,30 +2782,30 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<8)"] [[package]] name = "ruff" -version = "0.16.8" +version = "0.16.9" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" groups = ["dev"] files = [ - {file = "ruff-0.16.8-py3-none-linux_armv6l.whl", hash = "sha256:6ffbd6d87383c1edf5f6fa890f10200950240d7c1a16052a19a09d3a2307dd38"}, - {file = "ruff-0.16.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:42ed6b878ed61e3acca92f2730a17acff39286944ea82398544696366a6f925e"}, - {file = "ruff-0.16.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7ea781c7f2afba8c6a505ea0fb3f994020249e0c450635f5381286fea6b46170"}, - {file = "ruff-0.16.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8efeae3bbe414a5efefda11a792dfb51ef90ac48d50c4830de2f644caf3e8659"}, - {file = "ruff-0.16.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a79b795469fef7fc6e908b218eed2eb17332afd85031db6480dc864560e69b2"}, - {file = "ruff-0.16.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fdc5563cdc50555e6fba39322850860e9267c1b3d12c26a74729d8604c3c812"}, - {file = "ruff-0.16.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:34508983c70665578dab88f5223d8e6228307e1135398ca8bfc8b7e9501e282b"}, - {file = "ruff-0.16.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:644bb578569e0ffc575741232bd385dacdd6fbe123f1a729e7a225f54aa3957f"}, - {file = "ruff-0.16.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15e7d226246961db9235098333caa13063906d3851136b84c2900b82f5daa1df"}, - {file = "ruff-0.16.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a2bf6bc3e9ebdd4449abc6f06cf64b98051a2c61cf94d2fe9596518c881f1a1e"}, - {file = "ruff-0.16.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6ca111ba0849539165e9e59d2b442542f3c1e8060ebbdea82494f1ffbccb1e1f"}, - {file = "ruff-0.16.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:359a1e5b495448ee1e91018064382ebc86f90e8aac2fed222c7d0e4e8df85fd2"}, - {file = "ruff-0.16.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:59e8f5681349474110b24d62e93cfda6593f5fa3473446ca3705200cac1a08b9"}, - {file = "ruff-0.16.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:efa3e7a16d1baaa79957888dfdf8be9ef2e44db81cb032af06d76632ab59e773"}, - {file = "ruff-0.16.8-py3-none-win32.whl", hash = "sha256:55793ba85c69921e89be061426d91a78652d6e50317c962240922747a4eb713f"}, - {file = "ruff-0.16.8-py3-none-win_amd64.whl", hash = "sha256:a6b85621fd3c81e31fc5f5add09c9c078b430db3595ca632efafdec9e64ebfaa"}, - {file = "ruff-0.16.8-py3-none-win_arm64.whl", hash = "sha256:d075e820af612102ce217f07cc93e69f9490b10ec13ea85fa87bd03d996cef8a"}, - {file = "ruff-0.16.8.tar.gz", hash = "sha256:9247bf92b5f04d825c8639a4fe423ec2e4222acd9222e58412b0dab7e442798b"}, + {file = "ruff-0.16.9-py3-none-linux_armv6l.whl", hash = "sha256:95e6f022090368ab3b824c36276839c53b2adf1a3f4c09fefc33dfc400f6da96"}, + {file = "ruff-0.16.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a5f27be168556594a86d2f415db0cf43f5291917849318f873c7e2791f7a8c67"}, + {file = "ruff-0.16.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1632eb1d6197f33bd00b1acbc5b71009e89a8895c158e2d2b03a834fac964ab6"}, + {file = "ruff-0.16.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b3f951b14d865d5952c89d40a5ca07e87abe24fa5453299878411e127748fb1c"}, + {file = "ruff-0.16.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:447fc07e1573afff7cb02803462b12b6c8ece7cf10e2cd78565fa6d7a1c0bf8d"}, + {file = "ruff-0.16.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a3e039a6a40ed976c491722b60e0ae4a4aa1a86057f540ee7a37a5d19ae9120"}, + {file = "ruff-0.16.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4684dded7db60aa57cb118fa158630f5feade4af5782903b6053484bdf9bd129"}, + {file = "ruff-0.16.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d29c934357e45642fda2f34c0b1f4025b4a6c01e15e4bf0016879d60078a142c"}, + {file = "ruff-0.16.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a21713e629d3e5bdb2f5c2def1cc7f04f47fa8e1a7eb0571b4a28e1da64bc728"}, + {file = "ruff-0.16.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:7baa24ef5fc8e77aa93879e1d3f43754a01ae488e869f1ae30cf431afd4d2452"}, + {file = "ruff-0.16.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a41aac6230aadfaa133bdfa1614488531ffa3e0837567ae04c0da2058a9c0f9e"}, + {file = "ruff-0.16.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:c2529fb5896d49115b0e9aa8f887490b34bbe76baf879ec2264ac59406869ce7"}, + {file = "ruff-0.16.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:41e3870277694177429b56406d65dfbdb2c2802c52b715edaf6a0b829c69d4ee"}, + {file = "ruff-0.16.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8adbe4e58af167f767d7b2ba5e83c42e878350796cf78c2f5e14ab9903a92588"}, + {file = "ruff-0.16.9-py3-none-win32.whl", hash = "sha256:0e1dbc2073624dee6618d41d0098690a7244654af746704b64759e12b6b6b385"}, + {file = "ruff-0.16.9-py3-none-win_amd64.whl", hash = "sha256:6bd40fec8cd4c8a3d4dd589bd8ad4e6320c13c29234159bfd959a40d529d597b"}, + {file = "ruff-0.16.9-py3-none-win_arm64.whl", hash = "sha256:ed1a252039200f57a59eebc063b54beabea67bfbaaca0eeaa7f54b5fbcda2284"}, + {file = "ruff-0.16.9.tar.gz", hash = "sha256:12b625c6cfba78d285d9f48eda5f053374f1e53cb10ef17342a383750db99161"}, ] [[package]] @@ -3066,4 +3078,4 @@ plot = ["matplotlib", "plotly"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<3.15" -content-hash = "62ea4b12c962321e7faf670d9e2c9278e2a12364f9637b681c3b9f05cb0ba327" +content-hash = "249acf99e47d757f0fb08b45a8201272656a5b01e18f3683423fd4e9899670de" diff --git a/pyproject.toml b/pyproject.toml index ceab4529..731e96b1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,31 +33,32 @@ keywords = [ "metrics-gathering", ] dependencies = [ - "requests (>=2.32.3,<3.0.0)", - "pydantic (>=2.11,<3.0.0)", - "tabulate (>=0.9.0,<0.11.0)", - "pyjwt (>=2.13.0,<3.0.0)", - "pandas>=2.2.3,<3.0.7", - "toml (>=0.10.2,<0.11.0)", - "click (>=8.4.1,<9.0.0)", - "gitpython (>=3.1.59,<4.0.0)", - "humanfriendly (>=10.0,<11.0)", - "randomname (>=0.2.1,<0.3.0)", - "numpy (>=2.0.0,<3.0.0)", - "semver (>=3.0.4,<4.0.0)", - "email-validator (>=2.2.0,<3.0.0)", - "psutil (>=6.1.1,<8.0.0)", - "tenacity (>=9.0.0,<10.0.0)", - "typing-extensions (>=4.12.2,<5.0.0) ; python_version < \"3.11\"", - "deepmerge (>=2.0,<4.0)", - "geocoder (>=1.38.1,<2.0.0)", - "pydantic-extra-types (>=2.10.5,<3.0.0)", - "pyyaml (>=6.0.2,<7.0.0)", - "flatdict (==4.1.0)", - "pytest (>=9.0.3,<10.0.0)", - "unyt (>=3.1.0,<4.0.0)", - "pip>=26.1.2", - "msgpack>=1.2.1", + "requests>=2.34.2", + "pydantic>=2.13.5", + "tabulate>=0.10.0", + "pyjwt>=2.15.0", + "pandas>=2.3.3", + "toml>=0.10.2", + "click>=8.5.0", + "gitpython>=3.1.62", + "humanfriendly>=10.0", + "randomname>=0.2.1", + "numpy>=2.2.6", + "semver>=3.1.0", + "email-validator>=2.3.0", + "psutil>=7.2.2", + "tenacity>=9.1.4", + "typing-extensions>=4.16.0", + "python-version>=0.0.2", + "deepmerge>=3.0.1", + "geocoder>=1.38.1", + "pydantic-extra-types>=2.11.1", + "pyyaml>=6.0.3", + "flatdict>=4.1.0", + "pytest>=9.1.1", + "unyt>=3.1.0", + "pip>=26.2.1", + "msgpack>=1.2.2", ] [project.urls] @@ -85,8 +86,8 @@ dev = [ "pytest-sugar>=1.1.1", "pytest-timeout>=2.4.0", "pytest-xdist>=3.8.0", - "ruff>=0.16.8", - "types-requests>=2.33.0.20260518", + "ruff>=0.16.9", + "types-requests>=2.33.0.20260906", ] [tool.ruff] diff --git a/uv.lock b/uv.lock index 369f3c88..71b1fbd6 100644 --- a/uv.lock +++ b/uv.lock @@ -2064,6 +2064,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] +[[package]] +name = "python-version" +version = "0.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/74/8ed1d7f895b33a89565be4b1c8651d503fcb2cdda951fef4ec6f6906432c/python_version-0.0.2.tar.gz", hash = "sha256:5c16de57c7f2d614621cf468e8f5a20bb6b3cec665c9c7f5e9f9f000bf04fe67", size = 1888, upload-time = "2017-12-01T17:28:30.993Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/e3/06841b7e2b5dab99f6e5ece60db926ce35b9cf351da87ecaf2d75e5d9009/python_version-0.0.2-py2.py3-none-any.whl", hash = "sha256:2c7429f3375cee19873892e5db4d10d2b42172d8851a8bb5111729e028470580", size = 3411, upload-time = "2017-12-01T17:28:32.686Z" }, +] + [[package]] name = "pytz" version = "2026.4" @@ -2231,6 +2240,7 @@ dependencies = [ { name = "pydantic-extra-types" }, { name = "pyjwt" }, { name = "pytest" }, + { name = "python-version" }, { name = "pyyaml" }, { name = "randomname" }, { name = "requests" }, @@ -2238,7 +2248,7 @@ dependencies = [ { name = "tabulate" }, { name = "tenacity" }, { name = "toml" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, { name = "unyt" }, ] @@ -2265,33 +2275,34 @@ dev = [ [package.metadata] requires-dist = [ - { name = "click", specifier = ">=8.4.1,<9.0.0" }, - { name = "deepmerge", specifier = ">=2.0,<4.0" }, - { name = "email-validator", specifier = ">=2.2.0,<3.0.0" }, - { name = "flatdict", specifier = "==4.1.0" }, - { name = "geocoder", specifier = ">=1.38.1,<2.0.0" }, - { name = "gitpython", specifier = ">=3.1.59,<4.0.0" }, - { name = "humanfriendly", specifier = ">=10.0,<11.0" }, + { name = "click", specifier = ">=8.5.0" }, + { name = "deepmerge", specifier = ">=3.0.1" }, + { name = "email-validator", specifier = ">=2.3.0" }, + { name = "flatdict", specifier = ">=4.1.0" }, + { name = "geocoder", specifier = ">=1.38.1" }, + { name = "gitpython", specifier = ">=3.1.62" }, + { name = "humanfriendly", specifier = ">=10.0" }, { name = "matplotlib", marker = "extra == 'plot'", specifier = ">=3.10.0,<4.0.0" }, - { name = "msgpack", specifier = ">=1.2.1" }, - { name = "numpy", specifier = ">=2.0.0,<3.0.0" }, - { name = "pandas", specifier = ">=2.2.3,<3.0.7" }, - { name = "pip", specifier = ">=26.1.2" }, + { name = "msgpack", specifier = ">=1.2.2" }, + { name = "numpy", specifier = ">=2.2.6" }, + { name = "pandas", specifier = ">=2.3.3" }, + { name = "pip", specifier = ">=26.2.1" }, { name = "plotly", marker = "extra == 'plot'", specifier = ">=6.0.0,<8.0.0" }, - { name = "psutil", specifier = ">=6.1.1,<8.0.0" }, - { name = "pydantic", specifier = ">=2.11,<3.0.0" }, - { name = "pydantic-extra-types", specifier = ">=2.10.5,<3.0.0" }, - { name = "pyjwt", specifier = ">=2.13.0,<3.0.0" }, - { name = "pytest", specifier = ">=9.0.3,<10.0.0" }, - { name = "pyyaml", specifier = ">=6.0.2,<7.0.0" }, - { name = "randomname", specifier = ">=0.2.1,<0.3.0" }, - { name = "requests", specifier = ">=2.32.3,<3.0.0" }, - { name = "semver", specifier = ">=3.0.4,<4.0.0" }, - { name = "tabulate", specifier = ">=0.9.0,<0.11.0" }, - { name = "tenacity", specifier = ">=9.0.0,<10.0.0" }, - { name = "toml", specifier = ">=0.10.2,<0.11.0" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'", specifier = ">=4.12.2,<5.0.0" }, - { name = "unyt", specifier = ">=3.1.0,<4.0.0" }, + { name = "psutil", specifier = ">=7.2.2" }, + { name = "pydantic", specifier = ">=2.13.5" }, + { name = "pydantic-extra-types", specifier = ">=2.11.1" }, + { name = "pyjwt", specifier = ">=2.15.0" }, + { name = "pytest", specifier = ">=9.1.1" }, + { name = "python-version", specifier = ">=0.0.2" }, + { name = "pyyaml", specifier = ">=6.0.3" }, + { name = "randomname", specifier = ">=0.2.1" }, + { name = "requests", specifier = ">=2.34.2" }, + { name = "semver", specifier = ">=3.1.0" }, + { name = "tabulate", specifier = ">=0.10.0" }, + { name = "tenacity", specifier = ">=9.1.4" }, + { name = "toml", specifier = ">=0.10.2" }, + { name = "typing-extensions", specifier = ">=4.16.0" }, + { name = "unyt", specifier = ">=3.1.0" }, ] provides-extras = ["plot"] @@ -2305,8 +2316,8 @@ dev = [ { name = "pytest-sugar", specifier = ">=1.1.1" }, { name = "pytest-timeout", specifier = ">=2.4.0" }, { name = "pytest-xdist", specifier = ">=3.8.0" }, - { name = "ruff", specifier = ">=0.16.8" }, - { name = "types-requests", specifier = ">=2.33.0.20260518" }, + { name = "ruff", specifier = ">=0.16.9" }, + { name = "types-requests", specifier = ">=2.33.0.20260906" }, ] [[package]] From fbd2d5981b0d810e675af086394c7665a6d9e113 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Zar=C4=99bski?= Date: Fri, 25 Sep 2026 16:13:09 +0100 Subject: [PATCH 21/23] Run Prek manually and update CI actions --- .github/workflows/citation.yml | 18 ++++----- .github/workflows/examples.yml | 12 +++--- .github/workflows/prek.yml | 12 ++++-- .../workflows/test_client_macos_nightlies.yml | 40 +++++++++---------- .github/workflows/test_client_ubuntu.yml | 40 +++++++++---------- .../test_client_ubuntu_nightlies.yml | 40 +++++++++---------- .../test_client_windows_nightlies.yml | 40 +++++++++---------- .github/workflows/test_multiple_python.yml | 40 +++++++++---------- 8 files changed, 123 insertions(+), 119 deletions(-) diff --git a/.github/workflows/citation.yml b/.github/workflows/citation.yml index ed95d956..bd1190af 100644 --- a/.github/workflows/citation.yml +++ b/.github/workflows/citation.yml @@ -2,9 +2,9 @@ name: Citation File Validation on: push: - branches: [ "main", "dev", "hotfix/update-ci" ] + branches: ["main", "dev", "hotfix/update-ci"] pull_request: - branches: [ "main", "dev", "hotfix/update-ci" ] + branches: ["main", "dev", "hotfix/update-ci"] permissions: contents: read @@ -18,10 +18,10 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v5 - - name: Set up Python - uses: actions/setup-python@v6 - - name: Install Checker - run: python3 -m pip install cffconvert - - name: Check Citation File - run: cffconvert --validate + - uses: actions/checkout@v7 + - name: Set up Python + uses: actions/setup-python@v7 + - name: Install Checker + run: python3 -m pip install cffconvert + - name: Check Citation File + run: cffconvert --validate diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index 45c7349c..1159a6e8 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -50,7 +50,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Setup Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "3.11" - name: Install Simvue @@ -66,7 +66,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Setup Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "3.11" - name: Install Simvue @@ -86,7 +86,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Setup Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "3.11" - name: Install Simvue @@ -106,7 +106,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Setup Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "3.11" - name: Download SU2 @@ -128,7 +128,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Setup Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "3.11" - name: Install Simvue @@ -150,7 +150,7 @@ jobs: with: submodules: recursive - name: Setup Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "3.11" - name: Install Simvue diff --git a/.github/workflows/prek.yml b/.github/workflows/prek.yml index cbe51b27..d9409bb9 100644 --- a/.github/workflows/prek.yml +++ b/.github/workflows/prek.yml @@ -21,8 +21,12 @@ jobs: timeout-minutes: 30 steps: - name: Checkout Code - uses: actions/checkout@v6 - - name: Run Prek - uses: j178/prek-action@v3.0.0 + uses: actions/checkout@v7 + - name: Set up Python 3.14 + uses: actions/setup-python@v7 with: - extra-args: "--all-files --skip no-commit-to-branch" + python-version: "3.14" + - name: Install Prek + run: python -m pip install prek + - name: Run Prek on Repository + run: prek run --all-files --skip no-commit-to-branch diff --git a/.github/workflows/test_client_macos_nightlies.yml b/.github/workflows/test_client_macos_nightlies.yml index 2a9f5f24..366ad9be 100644 --- a/.github/workflows/test_client_macos_nightlies.yml +++ b/.github/workflows/test_client_macos_nightlies.yml @@ -17,9 +17,9 @@ jobs: runs-on: macos-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Set up Python 3.13 - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "3.13" - name: Install dependencies @@ -42,9 +42,9 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Set up Python 3.13 - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "3.13" - name: Install dependencies @@ -67,9 +67,9 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Set up Python 3.13 - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "3.13" - name: Install dependencies @@ -91,9 +91,9 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Set up Python 3.13 - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "3.13" - name: Install dependencies @@ -116,9 +116,9 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Set up Python 3.13 - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "3.13" - name: Install dependencies @@ -141,9 +141,9 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Set up Python 3.13 - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "3.13" - name: Install dependencies @@ -165,9 +165,9 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Set up Python 3.14 - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "3.14" - name: Install dependencies @@ -189,9 +189,9 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Set up Python 3.13 - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "3.13" - name: Install dependencies @@ -213,9 +213,9 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Set up Python 3.13 - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "3.13" - name: Install dependencies @@ -237,9 +237,9 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Set up Python 3.13 - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "3.13" - name: Install dependencies diff --git a/.github/workflows/test_client_ubuntu.yml b/.github/workflows/test_client_ubuntu.yml index 9b99fb88..352a3409 100644 --- a/.github/workflows/test_client_ubuntu.yml +++ b/.github/workflows/test_client_ubuntu.yml @@ -22,9 +22,9 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Set up Python 3.14 - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "3.14" - name: Install dependencies @@ -46,9 +46,9 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Set up Python 3.14 - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "3.14" - name: Install dependencies @@ -70,9 +70,9 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Set up Python 3.14 - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "3.14" - name: Install dependencies @@ -93,9 +93,9 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Set up Python 3.14 - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "3.14" - name: Install dependencies @@ -117,9 +117,9 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Set up Python 3.14 - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "3.14" - name: Install dependencies @@ -141,9 +141,9 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Set up Python 3.14 - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "3.14" - name: Install dependencies @@ -164,9 +164,9 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Set up Python 3.14 - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "3.14" - name: Install dependencies @@ -187,9 +187,9 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Set up Python 3.14 - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "3.14" - name: Install dependencies @@ -210,9 +210,9 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Set up Python 3.14 - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "3.14" - name: Install dependencies @@ -233,9 +233,9 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Set up Python 3.14 - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "3.14" - name: Install dependencies diff --git a/.github/workflows/test_client_ubuntu_nightlies.yml b/.github/workflows/test_client_ubuntu_nightlies.yml index 578fc7d5..dce507e5 100644 --- a/.github/workflows/test_client_ubuntu_nightlies.yml +++ b/.github/workflows/test_client_ubuntu_nightlies.yml @@ -20,9 +20,9 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Set up Python 3.14 - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "3.14" - name: Install dependencies @@ -45,9 +45,9 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Set up Python 3.14 - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "3.14" - name: Install dependencies @@ -70,9 +70,9 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Set up Python 3.14 - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "3.14" - name: Install dependencies @@ -94,9 +94,9 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Set up Python 3.14 - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "3.14" - name: Install dependencies @@ -119,9 +119,9 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Set up Python 3.14 - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "3.14" - name: Install dependencies @@ -144,9 +144,9 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Set up Python 3.14 - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "3.14" - name: Install dependencies @@ -168,9 +168,9 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Set up Python 3.14 - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "3.14" - name: Install dependencies @@ -192,9 +192,9 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Set up Python 3.14 - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "3.14" - name: Install dependencies @@ -216,9 +216,9 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Set up Python 3.14 - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "3.14" - name: Install dependencies @@ -240,9 +240,9 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Set up Python 3.14 - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "3.14" - name: Install dependencies diff --git a/.github/workflows/test_client_windows_nightlies.yml b/.github/workflows/test_client_windows_nightlies.yml index b8bbb710..b383f08c 100644 --- a/.github/workflows/test_client_windows_nightlies.yml +++ b/.github/workflows/test_client_windows_nightlies.yml @@ -17,9 +17,9 @@ jobs: runs-on: windows-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Set up Python 3.13 - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "3.13" - name: Install dependencies @@ -43,9 +43,9 @@ jobs: runs-on: windows-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Set up Python 3.13 - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "3.13" - name: Install dependencies @@ -69,9 +69,9 @@ jobs: runs-on: windows-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Set up Python 3.13 - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "3.13" - name: Install dependencies @@ -94,9 +94,9 @@ jobs: runs-on: windows-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Set up Python 3.13 - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "3.13" - name: Install dependencies @@ -120,9 +120,9 @@ jobs: runs-on: windows-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Set up Python 3.13 - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "3.13" - name: Install dependencies @@ -146,9 +146,9 @@ jobs: runs-on: windows-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Set up Python 3.13 - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "3.13" - name: Install dependencies @@ -171,9 +171,9 @@ jobs: runs-on: windows-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Set up Python 3.14 - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "3.14" - name: Install dependencies @@ -195,9 +195,9 @@ jobs: runs-on: windows-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Set up Python 3.13 - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "3.13" - name: Install dependencies @@ -220,9 +220,9 @@ jobs: runs-on: windows-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Set up Python 3.13 - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "3.13" - name: Install dependencies @@ -245,9 +245,9 @@ jobs: runs-on: windows-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Set up Python 3.13 - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "3.13" - name: Install dependencies diff --git a/.github/workflows/test_multiple_python.yml b/.github/workflows/test_multiple_python.yml index dc1b5f1e..ed3da0c1 100644 --- a/.github/workflows/test_multiple_python.yml +++ b/.github/workflows/test_multiple_python.yml @@ -25,9 +25,9 @@ jobs: matrix: python-version: ["3.11", "3.12", "3.13"] steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "${{ matrix.python-version }}" - name: Install dependencies @@ -55,9 +55,9 @@ jobs: matrix: python-version: ["3.10", "3.11", "3.12"] steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "${{ matrix.python-version }}" - name: Install dependencies @@ -85,9 +85,9 @@ jobs: matrix: python-version: ["3.11", "3.12", "3.13"] steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "${{ matrix.python-version }}" - name: Install dependencies @@ -114,9 +114,9 @@ jobs: matrix: python-version: ["3.11", "3.12", "3.13"] steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "${{ matrix.python-version }}" - name: Install dependencies @@ -144,9 +144,9 @@ jobs: matrix: python-version: ["3.11", "3.12", "3.13"] steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "${{ matrix.python-version }}" - name: Install dependencies @@ -174,9 +174,9 @@ jobs: matrix: python-version: ["3.11", "3.12", "3.13"] steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "${{ matrix.python-version }}" - name: Install dependencies @@ -198,9 +198,9 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Set up Python 3.14 - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "3.14" - name: Install dependencies @@ -227,9 +227,9 @@ jobs: matrix: python-version: ["3.11", "3.12", "3.13"] steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "${{ matrix.python-version }}" - name: Install dependencies @@ -256,9 +256,9 @@ jobs: matrix: python-version: ["3.11", "3.12", "3.13"] steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "${{ matrix.python-version }}" - name: Install dependencies @@ -285,9 +285,9 @@ jobs: matrix: python-version: ["3.11", "3.12", "3.13"] steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "${{ matrix.python-version }}" - name: Install dependencies From f8d077dae2b734f3fa1eabc8ddd6476084c29928 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Zar=C4=99bski?= Date: Fri, 25 Sep 2026 16:14:26 +0100 Subject: [PATCH 22/23] Run Prek manually and update CI actions --- .github/workflows/prek.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/prek.yml b/.github/workflows/prek.yml index d9409bb9..196b6cdb 100644 --- a/.github/workflows/prek.yml +++ b/.github/workflows/prek.yml @@ -28,5 +28,7 @@ jobs: python-version: "3.14" - name: Install Prek run: python -m pip install prek + - name: Get BasedPyright Version + run: prek exec basedpyright -- basedpyright --version - name: Run Prek on Repository run: prek run --all-files --skip no-commit-to-branch From 65406ca87e2bc0cb01b4e611d8c3b28c2aa1e931 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Zar=C4=99bski?= Date: Fri, 25 Sep 2026 16:20:25 +0100 Subject: [PATCH 23/23] Try reversing the logic for tolist --- .github/workflows/prek.yml | 12 +++--------- simvue/api/objects/grids.py | 2 +- simvue/run.py | 6 ++++-- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/.github/workflows/prek.yml b/.github/workflows/prek.yml index 196b6cdb..6a299fb2 100644 --- a/.github/workflows/prek.yml +++ b/.github/workflows/prek.yml @@ -22,13 +22,7 @@ jobs: steps: - name: Checkout Code uses: actions/checkout@v7 - - name: Set up Python 3.14 - uses: actions/setup-python@v7 + - name: Run Prek + uses: j178/prek-action@v3.0.0 with: - python-version: "3.14" - - name: Install Prek - run: python -m pip install prek - - name: Get BasedPyright Version - run: prek exec basedpyright -- basedpyright --version - - name: Run Prek on Repository - run: prek run --all-files --skip no-commit-to-branch + extra-args: "--all-files --skip no-commit-to-branch" diff --git a/simvue/api/objects/grids.py b/simvue/api/objects/grids.py index 9995e4cb..86475b29 100644 --- a/simvue/api/objects/grids.py +++ b/simvue/api/objects/grids.py @@ -58,7 +58,7 @@ def check_ordered_array( ValueError if the values are unordered """ - if isinstance(array, np.ndarray): + if not isinstance(array, list): array = array.tolist() for i, element in enumerate(array): _array = np.array(element) diff --git a/simvue/run.py b/simvue/run.py index 93473f53..12ae9b33 100644 --- a/simvue/run.py +++ b/simvue/run.py @@ -1665,7 +1665,9 @@ def assign_metric_to_grid( """ _axes_ticks = ( - axes_ticks.tolist() if isinstance(axes_ticks, np.ndarray) else axes_ticks + axes_ticks.tolist() + if axes_ticks and not isinstance(axes_ticks, list) + else axes_ticks ) grid_name = grid_name or metric_name @@ -1790,7 +1792,7 @@ def log_metrics( # Classify metrics into regular and tensor based for label, metric in metrics.items(): - if isinstance(metric, np.ndarray): + if not isinstance(metric, (int, float)): if metric.size > MAXIMUM_GRID_METRIC_SIZE: logger.warning( "Cannot log grid metric %s, size %d exceeds limit of %d",