diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..68a36c8 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,13 @@ +version: 2 +updates: + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 5 + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 5 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..6554dcd --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,50 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + lint-and-test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.12", "3.13"] + + container: astral/uv:python${{ matrix.python-version }}-bookworm-slim + + steps: + - uses: actions/checkout@v6 + + # If you have uv.lock → prefer: uv sync --locked + - name: Install deps + run: uv pip install --system -e ".[dev]" + + - name: Ruff + run: uv run ruff check src test + + - name: Mypy + run: uv run mypy src/kinexon_handball_api + + - name: Pytest + run: uv run pytest + + build: + runs-on: ubuntu-latest + needs: lint-and-test + container: astral/uv:python3.13-bookworm-slim + + steps: + - uses: actions/checkout@v6 + + - name: Build package + run: uv build + + - name: Upload dist artifacts + uses: actions/upload-artifact@v7 + with: + name: kinexon-dist + path: dist/ diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..432fcac --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,32 @@ +name: CodeQL + +on: + push: + branches: [main] + pull_request: + branches: [main] + paths-ignore: + - "src/_vendor/**" + schedule: + - cron: "23 3 * * 1" + workflow_dispatch: + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + steps: + - uses: actions/checkout@v6 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: python + + - name: Perform CodeQL analysis + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..696c08a --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,85 @@ +name: Release + +on: + push: + tags: + - "v*.*.*" + +jobs: + publish-release: + runs-on: ubuntu-latest + permissions: + contents: write + id-token: write + attestations: write + + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-python@v6 + with: + python-version: "3.13" + + - uses: astral-sh/setup-uv@v5 + + - name: Verify tag matches version + run: | + python - <<'PY' + import os + import sys + import tomllib + + tag = os.environ.get("GITHUB_REF_NAME", "") + with open("pyproject.toml", "rb") as handle: + version = tomllib.load(handle)["project"]["version"] + expected = f"v{version}" + if tag != expected: + print(f"Tag {tag} does not match version {version}") + sys.exit(1) + PY + + - name: Install deps + run: uv pip install --system -e ".[dev]" + + - name: Ruff + run: uv run ruff check src test + + - name: Mypy + run: uv run mypy src/sportradar_datacore_api + + - name: Pytest + run: uv run pytest + + - name: Build artifacts + run: | + uv build + + - name: Generate SBOM + uses: anchore/sbom-action@v0 + with: + path: dist + artifact-name: sportradar-sbom + + - name: Upload release artifacts + uses: actions/upload-artifact@v7 + with: + name: sportradar-dist + path: dist/* + + - name: Attest build provenance + uses: actions/attest-build-provenance@v2 + with: + subject-path: dist/* + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + generate_release_notes: true + files: dist/* + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: dist diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7ab641c..44aa41e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,37 +1,46 @@ repos: + # Generic hygiene - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v5.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer + exclude: ^log/ - id: check-yaml - id: check-merge-conflict - id: debug-statements - - repo: https://github.com/psf/black - rev: 25.9.0 + # Ruff: lint + format + import sort + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.5.7 hooks: - - id: black - language_version: python3 - exclude: ^src/_vendor/kinexon_client/ + - id: ruff + args: ["--fix"] + exclude: ^src/_vendor/ + - id: ruff-format + exclude: ^src/_vendor/ - - repo: https://github.com/pycqa/isort - rev: 5.13.2 + # mypy: static typing + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.10.0 hooks: - - id: isort - name: isort (python) - args: ["--profile", "black", "--line-length", "88"] - exclude: ^src/_vendor/kinexon_client/ - - - repo: https://github.com/pycqa/flake8 - rev: 7.0.0 - hooks: - - id: flake8 - args: ["--max-line-length=88", "--extend-ignore=E203,W503,D200,D103,E501"] - exclude: ^src/_vendor/kinexon_client/ + - id: mypy + exclude: ^src/_vendor/ + additional_dependencies: + - pydantic>=2.6 + - types-PyYAML>=6.0.12.20250915 + # Strip notebook outputs - repo: https://github.com/kynan/nbstripout rev: 0.7.1 hooks: - id: nbstripout - name: Strip output from Jupyter notebooks + + # Tests (via uv-managed env) + - repo: local + hooks: + - id: pytest + name: pytest + entry: uv run pytest test + language: system + pass_filenames: false diff --git a/README.md b/README.md index ffea4b4..1d9c741 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,8 @@ [![Python](https://img.shields.io/badge/python-3.13+-blue.svg)](https://www.python.org/downloads/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -[![Code Style: Black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) +[![Code style: ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) + A Python wrapper for the Kinexon Handball API. @@ -22,22 +23,19 @@ This library simplifies interaction with the Kinexon API by handling complex two This project requires **Python 3.13+**. -### Standard Installation -Clone the repository and install using pip: +### Using uv (Recommended) +If you use [uv](https://github.com/astral-sh/uv) for fast package management: ```bash git clone https://github.com/mad4ms/KinexonHandballAPI.git cd KinexonHandballAPI -pip install . +uv sync ``` -### Using uv (Recommended) -If you use [uv](https://github.com/astral-sh/uv) for fast package management: +If you prefer an editable install instead of syncing a lockfile: ```bash -git clone https://github.com/mad4ms/KinexonHandballAPI.git -cd KinexonHandballAPI -uv pip install . +uv pip install -e "." ``` ## Configuration @@ -60,11 +58,11 @@ USERNAME_KINEXON_MAIN=your_main_username PASSWORD_KINEXON_MAIN=your_main_password ENDPOINT_KINEXON_MAIN=https://hbl-cloud.kinexon.com/checklogin -# API Base Configuration -ENDPOINT_KINEXON_API=https://hbl-cloud.kinexon.com/public/v1/ +# API Base Configuration (no trailing /public/v1) +ENDPOINT_KINEXON_API=https://hbl-cloud.kinexon.com ``` -yeah i know it's confusing, but the two-step authentication requires separate credentials for each step. Don't shoot the messenger! +Note: The two-step authentication requires separate credentials for each step. ## Usage @@ -83,12 +81,14 @@ load_dotenv() # 2. Initialize the API api = HandballAPI( - base_url=os.getenv("ENDPOINT_KINEXON_API"), - api_key=os.getenv("API_KEY_KINEXON"), - username_basic=os.getenv("USERNAME_KINEXON_SESSION"), - password_basic=os.getenv("PASSWORD_KINEXON_SESSION"), - username_main=os.getenv("USERNAME_KINEXON_MAIN"), - password_main=os.getenv("PASSWORD_KINEXON_MAIN"), + base_url=os.getenv("ENDPOINT_KINEXON_API", ""), + api_key=os.getenv("API_KEY_KINEXON", ""), + username_basic=os.getenv("USERNAME_KINEXON_SESSION", ""), + password_basic=os.getenv("PASSWORD_KINEXON_SESSION", ""), + username_main=os.getenv("USERNAME_KINEXON_MAIN", ""), + password_main=os.getenv("PASSWORD_KINEXON_MAIN", ""), + endpoint_session=os.getenv("ENDPOINT_KINEXON_SESSION", ""), + endpoint_main=os.getenv("ENDPOINT_KINEXON_MAIN", ""), ) # 3. Use high-level helpers @@ -109,11 +109,11 @@ if teams: For endpoints not covered by high-level helpers, accessing the generated client directly is supported and encouraged. The generated client resides in `kinexon_client`. ```python -from kinexon_client.api.players import get_team_players +from kinexon_client.api.players import get_public_v1_teams_by_team_id_players from kinexon_client.models import PlayerModel # You can access the authenticated low-level client via `api.client` -response = get_team_players.sync_detailed( +response = get_public_v1_teams_by_team_id_players.sync_detailed( client=api.client, team_id=12345 ) @@ -125,7 +125,7 @@ if response.status_code == 200: ``` ### Advanced: Adding new teams -You can add new teams by modifying the `config/teams.json`. Somehow there is no API endpoint to fetch all teams, so this is a manual step for now. +You can add new teams by modifying `config/teams.yaml`. Somehow there is no API endpoint to fetch all teams, so this is a manual step for now. ## Architecture @@ -136,6 +136,18 @@ This project uses a **Wrapper Pattern** around a generated OpenAPI client. - *Note*: This directory allows us to ship the generated code without external dependencies or versioning conflicts. - **Do not edit files in `_vendor` manually.** They are overwritten during code generation. +## Repository Layout + +- **`src/kinexon_handball_api/`**: Hand-written wrapper and helper APIs. +- **`src/_vendor/kinexon_client/`**: Generated OpenAPI client (do not edit by hand). +- **`scripts/`**: Code generation helpers for the OpenAPI client. +- **`test/`**: Test suite executed with `pytest`. + +## AI Assistance + +If you are using GitHub Copilot in this repo, see the project-specific guidance in +`.github/copilot-instructions.md`. + ## Development ### Setup @@ -148,8 +160,10 @@ uv pip install -e ".[dev]" ### Running Tests ```bash -# Run unit tests -uv run pytest -m "not integration" +pytest + +Note: The integration tests use live API calls and will be skipped if required +environment variables are not set. ``` ### Code Generation diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 0000000..4c376ae --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,21 @@ +# Releasing (KinexonHandballAPI) + +## 1) Pre-flight +- Ensure `main` is green in CI +- Confirm version in `pyproject.toml` is updated +- Update changelog/release notes if used + +## 2) Tag release +```bash +git checkout main +git pull + +git tag vX.Y.Z +git push origin vX.Y.Z +``` + +## 3) What happens automatically +- GitHub Action `release.yml` verifies tag matches `pyproject.toml` version +- Builds `sdist` + `wheel` +- Publishes to PyPI via trusted publishing +- Artifacts are attached to a GitHub Release for the tag diff --git a/pyproject.toml b/pyproject.toml index a2fbadf..a38bebd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ authors = [ ] license = "MIT" readme = "README.md" -requires-python = ">=3.13" +requires-python = ">=3.12" # Core dependencies dependencies = [ @@ -16,9 +16,7 @@ dependencies = [ "typing-extensions>=4.8", # safety on older 3.10 "pandas>=2.2.2", # your wrapper/processing "python-dotenv>=1.1.0", - # Keep 'requests' ONLY if your own wrapper uses it (else remove): - # "requests>=2.31.0", - "requests>=2.32.5", + # requests no longer required (httpx covers auth handling) "pytest>=8.4.2", "pytest-order>=1.3.0", "attrs>=25.3.0", @@ -32,6 +30,7 @@ classifiers = [ "Programming Language :: Python :: 3", "Development Status :: 4 - Beta", "Intended Audience :: Developers", + "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Topic :: Software Development :: Libraries :: Python Modules", ] @@ -42,16 +41,17 @@ dev = [ "pytest>=8.4.1", "pytest-cov>=5.0", "requests-mock>=1.12", - "black>=24.0.0", - "isort>=5.13.0", + "ruff>=0.12.5", "mypy>=1.8.0", + "types-PyYAML>=6.0.12.20250915", "types-requests>=2.31.0", + "pre-commit>=4.5.1", ] lint = [ - "black>=24.0.0", - "isort>=5.13.0", + "ruff>=0.12.5", "mypy>=1.8.0", + "types-PyYAML>=6.0.12.20250915", "types-requests>=2.31.0", ] @@ -75,54 +75,56 @@ where = ["src", "src/_vendor"] # Code Quality Tools Configuration # ============================================================================= -# Black - Code formatter -[tool.black] +# Ruff - Linting/formatting +[tool.ruff] line-length = 88 -target-version = ['py313'] -include = '\.pyi?$' -extend-exclude = ''' -/( - \.eggs - | \.git - | \.hg - | \.mypy_cache - | \.tox - | \.venv - | build - | dist -)/ -''' - -# isort - Import sorting -[tool.isort] -profile = "black" -line_length = 88 -multi_line_output = 3 -include_trailing_comma = true -force_grid_wrap = 0 -use_parentheses = true -ensure_newline_before_comments = true - -# flake8 - Linting -[tool.flake8] -max-line-length = 88 -extend-ignore = ["E203", "W503", "D200", "D103", "E501"] +target-version = "py312" +extend-exclude = [ + ".eggs", + ".git", + ".hg", + ".mypy_cache", + ".tox", + ".venv", + "build", + "dist", + "src/_vendor/kinexon_client", +] + + +[tool.ruff.lint] +select = ["E","F","I","UP","B","SIM","PGH","PL","RUF"] +ignore = ["E203"] # formatter compatibility + +[tool.ruff.lint.isort] +known-first-party = ["kinexon_handball_api"] + +[tool.ruff.format] # mypy - Type checking +# --- mypy --- [tool.mypy] -python_version = "3.13" -warn_return_any = true -warn_unused_configs = true -disallow_untyped_defs = true -disallow_incomplete_defs = true -check_untyped_defs = true -disallow_untyped_decorators = true -no_implicit_optional = true -warn_redundant_casts = true +python_version = "3.12" +plugins = ["pydantic.mypy"] + +mypy_path = ["src", "src/_vendor"] + +exclude = ["^src/_vendor/"] + warn_unused_ignores = true -warn_no_return = true -warn_unreachable = true -strict_equality = true +warn_redundant_casts = true +strict_optional = true +check_untyped_defs = true +disallow_untyped_defs = true + +[[tool.mypy.overrides]] +module = "kinexon_client.*" +follow_imports = "skip" +ignore_errors = true + +[[tool.mypy.overrides]] +module = ["yaml", "yaml.*", "tqdm", "tqdm.*"] +ignore_missing_imports = true # pytest - Testing [tool.pytest.ini_options] diff --git a/scripts/rename_operation_ids.py b/scripts/rename_operation_ids.py index dbd549a..8a1d2ef 100644 --- a/scripts/rename_operation_ids.py +++ b/scripts/rename_operation_ids.py @@ -1,31 +1,32 @@ import json +from typing import Any # Mapping from ugly hashed operationId -> meaningful name -OPID_MAP = { # noqa - "566d29114a11605d95737f60abd4cfd0": "GetStatisticsListDeprecated", # noqa - "d54d2f11c25c494177d090ea3b95f3db": "GetPublicV1StatisticsList", # noqa - "78177200cc2b9fff7579ada0b1ecbc78": "GetTeamsByTeamIdPlayersDeprecated", # noqa - "e3bcaeeece5bd8b95151ff7669d53994": "GetPublicV1TeamsByTeamIdPlayers", # noqa - "49a583357f101cc1006c15dea3efa105": "GetTeamsByTeamIdPlayersByPlayerIdDeprecated", # noqa +OPID_MAP = { + "566d29114a11605d95737f60abd4cfd0": "GetStatisticsListDeprecated", + "d54d2f11c25c494177d090ea3b95f3db": "GetPublicV1StatisticsList", + "78177200cc2b9fff7579ada0b1ecbc78": "GetTeamsByTeamIdPlayersDeprecated", + "e3bcaeeece5bd8b95151ff7669d53994": "GetPublicV1TeamsByTeamIdPlayers", + "49a583357f101cc1006c15dea3efa105": "GetTeamsByTeamIdPlayersByPlayerIdDeprecated", "be90c35c61275ac6baef43ab359c001f": "GetPublicV1TeamsByTeamIdPlayersByPlayerId", - "0ff053da47c2c9aff6a630969537dd41": "GetStatisticsByTypeByPlayerIdByTimeEntityTypeByTimeEntityIdentifierDeprecated", # noqa - "a7cad42141c2fb49dd1c3b67e1c968c2": "GetPublicV1StatisticsByTypeByPlayerIdByTimeEntityTypeByTimeEntityIdentifier", # noqa - "4c360929b311565c84813123eea34d15": "GetStatisticsByTypeByPlayerIdByTimeEntityRangeTypeDeprecated", # noqa - "6140e44b435c3a7a5abb242b29b6e3ac": "GetPublicV1StatisticsByTypeByPlayerIdByTimeEntityRangeType", # noqa - "2f0b7602d8727d1554e6117288066419": "GetExportPositionsSessionByTimeEntityIdentifierDeprecated", # noqa - "518b3d20ddb1e4d6c4c1e1e5d7c8c280": "GetPublicV1ExportPositionsSessionByTimeEntityIdentifier", # noqa - "fcf0afd2f23dde1f15154cc98707c6a1": "GetExportInertialSessionByTimeEntityIdentifierDeprecated", # noqa - "2bd187efb4692eb25dfe220c2ca43e61": "GetPublicV1ExportInertialSessionByTimeEntityIdentifier", # noqa - "45398dc6e355095495ff3d7b3af976cd": "GetSensorAssignmentByTimeEntityIdentifierDeprecated", # noqa - "01b59cb8a2cb10b8a311829c5f4a8b9b": "GetPublicV1SensorAssignmentByTimeEntityIdentifier", # noqa - "4b000d72fdb374144e95fc4c2cf75b1a": "GetTeamsByTeamIdSessionsAndPhasesDeprecated", # noqa - "3709a95cd3000e6377d69b3a55d048e2": "GetPublicV1TeamsByTeamIdSessionsAndPhases", # noqa - "2a9ca44b0734126f65503a9cb9a0260b": "GetStatisticsBySessionIdCategoriesDeprecated", # noqa - "7e09029201135344482130236d7d3626": "GetPublicV1StatisticsBySessionIdCategories", # noqa + "0ff053da47c2c9aff6a630969537dd41": "GetStatisticsByTypeByPlayerIdByTimeEntityTypeByTimeEntityIdentifierDeprecated", # noqa: E501 + "a7cad42141c2fb49dd1c3b67e1c968c2": "GetPublicV1StatisticsByTypeByPlayerIdByTimeEntityTypeByTimeEntityIdentifier", # noqa: E501 + "4c360929b311565c84813123eea34d15": "GetStatisticsByTypeByPlayerIdByTimeEntityRangeTypeDeprecated", # noqa: E501 + "6140e44b435c3a7a5abb242b29b6e3ac": "GetPublicV1StatisticsByTypeByPlayerIdByTimeEntityRangeType", # noqa: E501 + "2f0b7602d8727d1554e6117288066419": "GetExportPositionsSessionByTimeEntityIdentifierDeprecated", # noqa: E501 + "518b3d20ddb1e4d6c4c1e1e5d7c8c280": "GetPublicV1ExportPositionsSessionByTimeEntityIdentifier", # noqa: E501 + "fcf0afd2f23dde1f15154cc98707c6a1": "GetExportInertialSessionByTimeEntityIdentifierDeprecated", # noqa: E501 + "2bd187efb4692eb25dfe220c2ca43e61": "GetPublicV1ExportInertialSessionByTimeEntityIdentifier", # noqa: E501 + "45398dc6e355095495ff3d7b3af976cd": "GetSensorAssignmentByTimeEntityIdentifierDeprecated", # noqa: E501 + "01b59cb8a2cb10b8a311829c5f4a8b9b": "GetPublicV1SensorAssignmentByTimeEntityIdentifier", # noqa: E501 + "4b000d72fdb374144e95fc4c2cf75b1a": "GetTeamsByTeamIdSessionsAndPhasesDeprecated", + "3709a95cd3000e6377d69b3a55d048e2": "GetPublicV1TeamsByTeamIdSessionsAndPhases", + "2a9ca44b0734126f65503a9cb9a0260b": "GetStatisticsBySessionIdCategoriesDeprecated", + "7e09029201135344482130236d7d3626": "GetPublicV1StatisticsBySessionIdCategories", } -def replace_operation_ids(data: dict) -> dict: +def replace_operation_ids(data: Any) -> Any: """Recursively traverse OpenAPI spec and replace operationId values.""" if isinstance(data, dict): if "operationId" in data and data["operationId"] in OPID_MAP: @@ -33,7 +34,7 @@ def replace_operation_ids(data: dict) -> dict: new = OPID_MAP[old] data["operationId"] = new print(f"Replaced {old} -> {new}") - for k, v in data.items(): + for _k, v in data.items(): replace_operation_ids(v) elif isinstance(data, list): for item in data: @@ -41,8 +42,8 @@ def replace_operation_ids(data: dict) -> dict: return data -def main(): - with open("openapi/sport_app.json", "r", encoding="utf-8") as f: +def main() -> None: + with open("openapi/sport_app.json", encoding="utf-8") as f: spec = json.load(f) updated = replace_operation_ids(spec) diff --git a/src/kinexon_handball_api/__init__.py b/src/kinexon_handball_api/__init__.py new file mode 100644 index 0000000..3d4d48c --- /dev/null +++ b/src/kinexon_handball_api/__init__.py @@ -0,0 +1 @@ +"""Kinexon Handball API wrapper package.""" diff --git a/src/kinexon_handball_api/api.py b/src/kinexon_handball_api/api.py index d2e679b..140043c 100644 --- a/src/kinexon_handball_api/api.py +++ b/src/kinexon_handball_api/api.py @@ -1,16 +1,11 @@ -""" -Kinexon API Client -API-key authenticated wrapper, integrating with generated kinexon_client functions. - -Author: Michael Adams, 2025 -""" +"""Kinexon API client wrapper with two-step authentication.""" import logging -from typing import Any, Dict, Optional +from types import TracebackType +from typing import Any import httpx from kinexon_client import Client -from requests.auth import HTTPBasicAuth class APIRequestError(Exception): @@ -23,7 +18,7 @@ class KinexonAPI: Provides low-level setup for generated kinexon_client.* functions. """ - def __init__( + def __init__( # noqa: PLR0913 self, base_url: str, username_basic: str, @@ -49,7 +44,7 @@ def __init__( self.endpoint_session = endpoint_session self.endpoint_main = endpoint_main - self.client = None + self.client: Client | None = None if connect_on_init: self.connect() @@ -57,9 +52,7 @@ def __init__( # Note: We bypass Authorization headers; # API key is injected via query by generated funcs. - def connect( - self, - ) -> None: + def connect(self) -> None: self.client = Client( base_url=self.base_url, # token="", # not used @@ -73,27 +66,32 @@ def connect( self.authenticate() - def authenticate( - self, - ) -> bool: + def _get_httpx_client(self) -> httpx.Client: + if self.client is None: + raise APIRequestError("Client not initialized. Call connect() first.") + return self.client.get_httpx_client() + + def authenticate(self) -> bool: """ Authenticate with Kinexon API using a two-step login: 1) Persist Basic Auth on the session and GET the session endpoint. 2) POST JSON (nested 'login') to the main endpoint. """ logger = logging.getLogger(__name__) + ok_status = 200 # Use a temporary httpx.Client for authentication - self.client.get_httpx_client().auth = HTTPBasicAuth( + client = self._get_httpx_client() + client.auth = httpx.BasicAuth( self.username_basic, self.password_basic, ) # Step 1: Session-level Basic Auth - resp = self.client.get_httpx_client().get( + resp = client.get( self.endpoint_session, # auth=(self.username_basic, self.password_basic), ) - if not resp.is_redirect: + if resp.status_code not in {200, 204} and not resp.is_redirect: raise APIRequestError( f"Session login failed: {resp.status_code} {resp.text}" ) @@ -106,40 +104,47 @@ def authenticate( "password": self.password_main, } } - resp = self.client.get_httpx_client().post(self.endpoint_main, json=payload) - if resp.status_code != 200: + resp = client.post(self.endpoint_main, json=payload) + if resp.status_code != ok_status: raise APIRequestError(f"Main login failed: {resp.status_code} {resp.text}") logger.info("Main authentication successful.") return True def close(self) -> None: + if self.client is None: + return self.client.get_httpx_client().close() def __enter__(self) -> "KinexonAPI": return self - def __exit__(self, exc_type, exc, tb) -> None: + def __exit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + tb: TracebackType | None, + ) -> None: self.close() - def make_custom_request( + def make_custom_request( # noqa: PLR0913 self, method: str, url: str, *, - params: Optional[Dict[str, Any]] = None, - headers: Optional[Dict[str, str]] = None, - data: Optional[Dict[str, Any]] = None, - json: Optional[Dict[str, Any]] = None, + params: dict[str, Any] | None = None, + headers: dict[str, str] | None = None, + data: dict[str, Any] | None = None, + json: dict[str, Any] | None = None, stream: bool = False, - timeout: Optional[float] = None, + timeout: float | None = None, ) -> httpx.Response: """ Low-level custom request via the authenticated httpx.Client. If stream=True, returns an open Response with streaming enabled. Caller is responsible for closing the response (resp.close()). """ - client = self.client.get_httpx_client() + client = self._get_httpx_client() # Prepend base URL if relative if not (url.startswith("http://") or url.startswith("https://")): diff --git a/src/kinexon_handball_api/config/teams.yaml b/src/kinexon_handball_api/config/teams.yaml index 3354fca..190a5a4 100644 --- a/src/kinexon_handball_api/config/teams.yaml +++ b/src/kinexon_handball_api/config/teams.yaml @@ -9,7 +9,7 @@ # 5. Copy IDs next to team names # 6. Update this file for new seasons -# Current season (2025/26) +# Current season (2025/26). Used when fetchers receive no season input. current_season: "2025-26" seasons: diff --git a/src/kinexon_handball_api/fetchers.py b/src/kinexon_handball_api/fetchers.py index f0fae17..a7198cc 100644 --- a/src/kinexon_handball_api/fetchers.py +++ b/src/kinexon_handball_api/fetchers.py @@ -1,21 +1,63 @@ +from functools import lru_cache from pathlib import Path -from typing import Dict, List, Optional +from typing import Any, TypedDict import yaml +DEFAULT_SEASON = "2025-26" -def _load_teams_config() -> Dict: + +class TeamEntry(TypedDict): + id: int + name: str + + +@lru_cache(maxsize=1) +def _load_teams_config() -> dict[str, Any]: """Load teams configuration from YAML file.""" config_path = Path(__file__).parent / "config" / "teams.yaml" if not config_path.exists(): raise FileNotFoundError(f"Teams config file not found: {config_path}") - with open(config_path, "r", encoding="utf-8") as f: - return yaml.safe_load(f) + with open(config_path, encoding="utf-8") as f: + data = yaml.safe_load(f) + + if not isinstance(data, dict): + raise ValueError("Teams config must be a mapping at the top level.") + + return data -def fetch_team_ids(season: Optional[str] = None) -> List[Dict[str, int]]: +def _get_current_season(config: dict[str, Any]) -> str: + """Return configured current season with a stable fallback.""" + value = config.get("current_season") + return value if isinstance(value, str) and value else DEFAULT_SEASON + + +def _validate_teams(season: str, teams: Any) -> list[TeamEntry]: + if not isinstance(teams, list): + raise ValueError( + f"Season '{season}' teams must be a list, got {type(teams).__name__}." + ) + normalized: list[TeamEntry] = [] + for entry in teams: + if not isinstance(entry, dict): + raise ValueError( + f"Season '{season}' team entries must be mappings, got " + f"{type(entry).__name__}." + ) + team_id = entry.get("id") + name = entry.get("name") + if not isinstance(team_id, int) or not isinstance(name, str): + raise ValueError( + f"Season '{season}' team entries must include int 'id' and str 'name'." + ) + normalized.append({"id": team_id, "name": name}) + return normalized + + +def fetch_team_ids(season: str | None = None) -> list[TeamEntry]: """ Return the list of Kinexon team IDs for a specific season. @@ -37,7 +79,7 @@ def fetch_team_ids(season: Optional[str] = None) -> List[Dict[str, int]]: config = _load_teams_config() if season is None: - season = config.get("current_season", "2025-26") + season = _get_current_season(config) teams = config.get("seasons", {}).get(season) if teams is None: @@ -45,11 +87,10 @@ def fetch_team_ids(season: Optional[str] = None) -> List[Dict[str, int]]: raise ValueError( f"Season '{season}' not found. Available seasons: {available_seasons}" ) - - return teams + return _validate_teams(season, teams) -def get_available_seasons() -> List[str]: +def get_available_seasons() -> list[str]: """Get list of available seasons.""" config = _load_teams_config() return list(config.get("seasons", {}).keys()) @@ -58,4 +99,4 @@ def get_available_seasons() -> List[str]: def get_current_season() -> str: """Get the current season from config.""" config = _load_teams_config() - return config.get("current_season", "2025-26") + return _get_current_season(config) diff --git a/src/kinexon_handball_api/handball.py b/src/kinexon_handball_api/handball.py index b7c1a4e..ed229ad 100644 --- a/src/kinexon_handball_api/handball.py +++ b/src/kinexon_handball_api/handball.py @@ -1,24 +1,21 @@ -""" -Handball API wrapper for Kinexon API. +"""Handball API wrapper for Kinexon API. Provides convenience methods using generated kinexon_client functions. - -Author: Michael Adams, 2025 """ -from typing import Any, Dict, List, Optional +from __future__ import annotations + +import contextlib +import logging +from datetime import datetime +from typing import Any, TypeVar, cast import httpx from kinexon_client.api.available_metrics_and_events import ( get_public_v1_statistics_list, ) - -# events -# noqa: E501 from kinexon_client.api.events import ( get_public_v_1_events_event_type_player_players_time_entity_type_time_entity_identifier, ) - -# Import generated API functions from kinexon_client.api.exports import ( get_public_v1_export_positions_session_by_time_entity_identifier, ) @@ -30,24 +27,48 @@ from tqdm import tqdm from kinexon_handball_api.api import KinexonAPI -from kinexon_handball_api.fetchers import fetch_team_ids +from kinexon_handball_api.fetchers import TeamEntry, fetch_team_ids +logger = logging.getLogger(__name__) -def _bool_str(v: bool) -> str: - return "true" if v else "false" +T = TypeVar("T") -class HandballAPI(KinexonAPI): - """ - High-level wrapper around Kinexon handball endpoints. - """ +def _bool_str(value: bool) -> str: + return "true" if value else "false" - def fetch_team_ids(self, season: Optional[str] = None) -> List[Dict[str, int]]: - """ - Fetch the list of team IDs from the Kinexon API. - Returns: - List[Dict[str, int]]: List of team IDs and names. - """ + +class HandballAPI(KinexonAPI): + """High-level wrapper around Kinexon handball endpoints.""" + + @staticmethod + def _require_value(name: str, value: Any) -> None: + if value is None or value == "": + raise ValueError(f"{name} must be provided.") + + @staticmethod + def _handle_response( + response: Any, + context: str, + default: T, + *, + ok_statuses: set[int] | None = None, + ) -> T: + """Validate a generated response and return parsed payload or fallback.""" + allowed = ok_statuses or {200} + if response.status_code not in allowed: + content = getattr(response, "content", None) + raise RuntimeError(f"{context}: HTTP {response.status_code}: {content!r}") + if response.parsed is not None: + return cast(T, response.parsed) + return default + + @staticmethod + def _to_iso(value: datetime | str) -> str: + return value.isoformat() if isinstance(value, datetime) else value + + def get_team_ids(self, season: str | None = None) -> list[TeamEntry]: + """Return team IDs for a season (or default season behavior).""" return fetch_team_ids(season) def get_events_for_session( @@ -56,69 +77,61 @@ def get_events_for_session( players: str = "in-entity", session_id: str = "latest", ) -> Any: - resp = get_public_v_1_events_event_type_player_players_time_entity_type_time_entity_identifier.sync_detailed( # noqa + self._require_value("session_id", session_id) + resp = get_public_v_1_events_event_type_player_players_time_entity_type_time_entity_identifier.sync_detailed( # noqa: E501 event_type=event_type, players=players, time_entity_type="session", time_entity_identifier=session_id, client=self.client, - ) # noqa - if resp.status_code != 200: - raise RuntimeError(f"HTTP {resp.status_code}: {resp.content!r}") - return resp.parsed or {} - - def get_available_metrics_and_events(self) -> Any: - resp = get_public_v1_statistics_list.sync_detailed( - client=self.client, ) - if resp.status_code != 200: - raise RuntimeError(f"HTTP {resp.status_code}: {resp.content!r}") - return resp.parsed or {} + return self._handle_response(resp, "events_for_session", {}) - def get_team_ids(self, season: Optional[str] = None) -> List[Dict[str, int]]: - """ - Fetch the list of team IDs from the Kinexon API. - Returns: - List[Dict[str, int]]: List of team IDs and names. - """ - return fetch_team_ids(season) + def get_available_metrics_and_events(self) -> Any: + resp = get_public_v1_statistics_list.sync_detailed(client=self.client) + return self._handle_response(resp, "available_metrics_and_events", {}) - def get_sessions_for_team(self, team_id: int, start: str, end: str) -> Any: + def get_sessions_for_team( + self, + team_id: int, + start: datetime | str, + end: datetime | str, + ) -> Any: + self._require_value("team_id", team_id) resp = get_public_v1_teams_by_team_id_sessions_and_phases.sync_detailed( team_id=team_id, - min_=start, - max_=end, + min_=self._to_iso(start), + max_=self._to_iso(end), client=self.client, ) - if resp.status_code != 200: - raise RuntimeError(f"HTTP {resp.status_code}: {resp.content!r}") - return resp.parsed or {} + return self._handle_response(resp, "sessions_for_team", {}) def get_team_players(self, team_id: int) -> Any: + self._require_value("team_id", team_id) resp = get_public_v1_teams_by_team_id_players.sync_detailed( team_id=team_id, client=self.client, ) - if resp.status_code != 200: - raise RuntimeError(f"HTTP {resp.status_code}: {resp.content!r}") - return resp.parsed or {} + return self._handle_response(resp, "team_players", {}) def get_positions_csv( self, session_id: str, update_rate: int = 20, group_by_ts: bool = True, - players: Optional[str] = None, + players: str | None = None, ) -> str: - return get_public_v1_export_positions_session_by_time_entity_identifier.sync( + self._require_value("session_id", session_id) + resp = get_public_v1_export_positions_session_by_time_entity_identifier.sync_detailed( # noqa: E501 time_entity_identifier=session_id, client=self.client, update_rate=update_rate, group_by_timestamp=group_by_ts, players=players or UNSET, ) + return self._handle_response(resp, "positions_csv", "") - def download_positions_csv_via_custom( + def download_positions_csv_via_custom( # noqa: PLR0913, PLR0912 self, session_id: str, *, @@ -127,150 +140,89 @@ def download_positions_csv_via_custom( use_local_frame_imu: bool = False, center_origin: bool = False, group_by_timestamp: bool = False, - players: Optional[str] = None, + players: str | None = None, + max_bytes: int | None = None, chunk_size: int = 1024 * 1024, show_progress: bool = True, + timeout: float | None = None, ) -> bytes: - """ - Thread-safe download of positions CSV via custom request with streaming. - This allows downloading large CSV files with streaming (there is no configuration flag for this in the generated client). + """Download positions CSV via streaming custom request. - Returns the CSV bytes (optionally compressed depending on API settings). + If max_bytes is set, the download stops after that many bytes. """ + self._require_value("session_id", session_id) - # Legacy semantics: booleans as lowercase strings - def b(x: bool) -> str: - return "true" if x else "false" + max_bytes_remaining = max_bytes if max_bytes and max_bytes > 0 else None - params: Dict[str, Any] = { + params: dict[str, Any] = { "updateRate": update_rate, - "compressOutput": b(compress_output), - "useLocalFrameIMU": b(use_local_frame_imu), - "centerOrigin": b(center_origin), - "groupByTimestamp": b(group_by_timestamp), + "compressOutput": _bool_str(compress_output), + "useLocalFrameIMU": _bool_str(use_local_frame_imu), + "centerOrigin": _bool_str(center_origin), + "groupByTimestamp": _bool_str(group_by_timestamp), } if players: params["players"] = players headers = {"Accept": "text/csv"} - # optional: add gzip, zip to Accept-Encoding if compress_output if compress_output: headers["Accept-Encoding"] = "gzip, zip" url = f"/public/v1/export/positions/session/{session_id}" - - # Request with streaming enabled resp = self.make_custom_request( "GET", url, params=params, headers=headers, stream=True, + timeout=timeout, ) - # Raise early for non-200 try: resp.raise_for_status() - except httpx.HTTPStatusError as e: - text = "" - try: - text = resp.text - except Exception: - pass - raise RuntimeError(f"HTTP {resp.status_code}: {text}") from e - - # Progress: use Content-Length when available - total = int(resp.headers.get("Content-Length", "0")) or None - buf = bytearray() - - if show_progress and tqdm is not None: - with tqdm( - total=total, - unit="B", - unit_scale=True, - unit_divisor=1024, - desc=f"Downloading file for session {session_id}", - ) as bar: + total = int(resp.headers.get("Content-Length", "0")) or None + buf = bytearray() + + if show_progress: + with tqdm( + total=total, + unit="B", + unit_scale=True, + unit_divisor=1024, + desc=f"Downloading file for session {session_id}", + ) as bar: + for chunk in resp.iter_bytes(chunk_size=chunk_size): + if chunk: + if max_bytes_remaining is None: + buf.extend(chunk) + bar.update(len(chunk)) + else: + take = min(len(chunk), max_bytes_remaining) + if take: + buf.extend(chunk[:take]) + bar.update(take) + max_bytes_remaining -= take + if max_bytes_remaining == 0: + break + else: for chunk in resp.iter_bytes(chunk_size=chunk_size): if chunk: - buf.extend(chunk) - bar.update(len(chunk)) - else: - for chunk in resp.iter_bytes(chunk_size=chunk_size): - if chunk: - buf.extend(chunk) + if max_bytes_remaining is None: + buf.extend(chunk) + else: + take = min(len(chunk), max_bytes_remaining) + if take: + buf.extend(chunk[:take]) + max_bytes_remaining -= take + if max_bytes_remaining == 0: + break + except httpx.HTTPStatusError as exc: + text = "" + with contextlib.suppress(Exception): + text = resp.text + raise RuntimeError(f"HTTP {resp.status_code}: {text}") from exc + finally: + resp.close() - resp.close() + logger.debug("Downloaded %d bytes for session %s", len(buf), session_id) return bytes(buf) - - -if __name__ == "__main__": - import os - from datetime import datetime - - from dotenv import load_dotenv - - load_dotenv() - - api = HandballAPI( - base_url=os.getenv( - "ENDPOINT_KINEXON_SESSION", "https://hbl-cloud.kinexon.com/api" - ), - api_key=os.getenv("API_KEY_KINEXON", "your_api_key_here"), - username_basic=os.getenv("USERNAME_KINEXON_SESSION", "your_username_here"), - password_basic=os.getenv("PASSWORD_KINEXON_SESSION", "your_password_here"), - username_main=os.getenv("USERNAME_KINEXON_MAIN", "your_username_here"), - password_main=os.getenv("PASSWORD_KINEXON_MAIN", "your_password_here"), - endpoint_session=os.getenv( - "ENDPOINT_KINEXON_SESSION", - "https://hbl-cloud.kinexon.com/api/session", - ), - endpoint_main=os.getenv( - "ENDPOINT_KINEXON_MAIN", - "https://hbl-cloud.kinexon.com/api", - ), - timeout=10000, - ) - avail = api.get_available_metrics_and_events() - # print("Available metrics and events:", len(avail)) - - ids_team = fetch_team_ids() - - print("Team IDs:", len(ids_team)) - - print( - f"Team Players for team {ids_team[0]['id']}:", - api.get_team_players(team_id=ids_team[0]["id"]), - ) - - if ids_team: - team_id = ids_team[0]["id"] - sessions = api.get_sessions_for_team( - team_id=team_id, - start=datetime.fromisoformat("2024-08-01T00:00:00+00:00"), - end=datetime.fromisoformat("2025-08-01T00:00:00+00:00"), - ) - # print session ids and names - print( - f"Sessions for team {team_id}:", - len(sessions), - ) - - # 2) call the custom downloader - csv_bytes = api.download_positions_csv_via_custom( - session_id=sessions[0].session_id, - update_rate=20, - compress_output=True, - use_local_frame_imu=False, - center_origin=False, - group_by_timestamp=False, - players=None, # or "123,456,789" - chunk_size=512 * 1024, # 512 KB chunks - show_progress=True, # requires tqdm installed; otherwise ignored - ) - - # 3) write to disk (name accordingly if compressed) - out_path = "positions.csv.gz" if True else "positions.csv" - with open(out_path, "wb") as f: - f.write(csv_bytes) - print(f"Wrote {len(csv_bytes):,} bytes to {out_path}") diff --git a/test/test_auth.py b/test/test_auth.py new file mode 100644 index 0000000..376484f --- /dev/null +++ b/test/test_auth.py @@ -0,0 +1,85 @@ +import base64 + +import httpx +import pytest +from kinexon_client import Client + +from kinexon_handball_api.api import APIRequestError, KinexonAPI + + +def _make_api(transport: httpx.BaseTransport) -> KinexonAPI: + client = Client( + base_url="https://example.test", + headers={"api-key": "test-key"}, + raise_on_unexpected_status=True, + ) + httpx_client = httpx.Client( + base_url="https://example.test", + transport=transport, + ) + client.set_httpx_client(httpx_client) + + api = KinexonAPI( + base_url="https://example.test", + username_basic="basic-user", + password_basic="basic-pass", + username_main="main-user", + password_main="main-pass", + endpoint_session="/session", + endpoint_main="/login", + api_key="test-key", + connect_on_init=False, + ) + api.client = client + return api + + +def _basic_auth_header(username: str, password: str) -> str: + raw = f"{username}:{password}".encode() + return "Basic " + base64.b64encode(raw).decode("ascii") + + +def test_authenticate_success_sets_basic_auth() -> None: + requests: list[httpx.Request] = [] + + def handler(request: httpx.Request) -> httpx.Response: + requests.append(request) + if request.url.path == "/session": + return httpx.Response(302, headers={"Location": "/"}) + if request.url.path == "/login": + return httpx.Response(200, json={"ok": True}) + return httpx.Response(404) + + api = _make_api(httpx.MockTransport(handler)) + + assert api.authenticate() is True + assert [r.url.path for r in requests] == ["/session", "/login"] + + expected = _basic_auth_header("basic-user", "basic-pass") + assert requests[0].headers.get("Authorization") == expected + + +def test_authenticate_raises_on_session_failure() -> None: + def handler(request: httpx.Request) -> httpx.Response: + if request.url.path == "/session": + return httpx.Response(401, text="nope") + return httpx.Response(404) + + api = _make_api(httpx.MockTransport(handler)) + + with pytest.raises(APIRequestError): + api.authenticate() + + +def test_authenticate_raises_on_main_login_failure() -> None: + def handler(request: httpx.Request) -> httpx.Response: + if request.url.path == "/session": + return httpx.Response(302, headers={"Location": "/"}) + if request.url.path == "/login": + return httpx.Response(403, text="nope") + return httpx.Response(404) + + api = _make_api(httpx.MockTransport(handler)) + + with pytest.raises(APIRequestError): + api.authenticate() diff --git a/test/test_auth_integration.py b/test/test_auth_integration.py new file mode 100644 index 0000000..ef94b3a --- /dev/null +++ b/test/test_auth_integration.py @@ -0,0 +1,37 @@ +import os +from typing import cast + +import pytest +from dotenv import load_dotenv + +from kinexon_handball_api.api import KinexonAPI + +load_dotenv() # Load .env file for integration test credentials + + +def _require_env(name: str) -> str: + value = os.getenv(name) + if not value: + pytest.skip(f"Missing env var: {name}") + return cast(str, value) + + +@pytest.mark.integration +def test_authenticate_live() -> None: + api = KinexonAPI( + base_url=_require_env("ENDPOINT_KINEXON_API"), + username_basic=_require_env("USERNAME_KINEXON_SESSION"), + password_basic=_require_env("PASSWORD_KINEXON_SESSION"), + username_main=_require_env("USERNAME_KINEXON_MAIN"), + password_main=_require_env("PASSWORD_KINEXON_MAIN"), + endpoint_session=_require_env("ENDPOINT_KINEXON_SESSION"), + endpoint_main=_require_env("ENDPOINT_KINEXON_MAIN"), + api_key=_require_env("API_KEY_KINEXON"), + connect_on_init=False, + ) + + try: + api.connect() + assert api.client is not None + finally: + api.close() diff --git a/test/test_handball_integration.py b/test/test_handball_integration.py new file mode 100644 index 0000000..8788c39 --- /dev/null +++ b/test/test_handball_integration.py @@ -0,0 +1,98 @@ +import os +from typing import cast + +import httpx +import pytest +from dotenv import load_dotenv + +from kinexon_handball_api.handball import HandballAPI + +load_dotenv() + +REQUIRED_ENV_VARS = ( + "ENDPOINT_KINEXON_API", + "USERNAME_KINEXON_SESSION", + "PASSWORD_KINEXON_SESSION", + "USERNAME_KINEXON_MAIN", + "PASSWORD_KINEXON_MAIN", + "ENDPOINT_KINEXON_SESSION", + "ENDPOINT_KINEXON_MAIN", + "API_KEY_KINEXON", +) + + +def _missing_env_vars() -> list[str]: + return [name for name in REQUIRED_ENV_VARS if not os.getenv(name)] + + +pytestmark = pytest.mark.skipif( + _missing_env_vars(), + reason="Missing integration env vars.", +) + + +def _require_env(name: str) -> str: + value = os.getenv(name) + if not value: + pytest.skip(f"Missing env var: {name}") + return cast(str, value) + + +def _require_int_env(name: str) -> int: + return int(_require_env(name)) + + +def _make_api() -> HandballAPI: + api = HandballAPI( + base_url=_require_env("ENDPOINT_KINEXON_API"), + username_basic=_require_env("USERNAME_KINEXON_SESSION"), + password_basic=_require_env("PASSWORD_KINEXON_SESSION"), + username_main=_require_env("USERNAME_KINEXON_MAIN"), + password_main=_require_env("PASSWORD_KINEXON_MAIN"), + endpoint_session=_require_env("ENDPOINT_KINEXON_SESSION"), + endpoint_main=_require_env("ENDPOINT_KINEXON_MAIN"), + api_key=_require_env("API_KEY_KINEXON"), + connect_on_init=False, + ) + api.connect() + return api + + +@pytest.mark.integration +def test_get_available_metrics_and_events_live() -> None: + api = _make_api() + try: + result = api.get_available_metrics_and_events() + assert result is not None + finally: + api.close() + + +@pytest.mark.integration +def test_get_team_players_live() -> None: + api = _make_api() + try: + team_id = _require_int_env("KINEXON_TEAM_ID") + result = api.get_team_players(team_id) + assert result is not None + finally: + api.close() + + +@pytest.mark.integration +def test_download_positions_csv_partial_live() -> None: + api = _make_api() + try: + session_id = _require_env("KINEXON_SESSION_ID") + try: + data = api.download_positions_csv_via_custom( + session_id, + show_progress=False, + max_bytes=1024, + timeout=60.0, + ) + assert len(data) <= 1024 # noqa: PLR2004 + except httpx.TimeoutException: + pytest.skip("Download timed out before first bytes arrived.") + finally: + api.close() diff --git a/uv.lock b/uv.lock index 626b978..354c7b3 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,6 @@ version = 1 revision = 3 -requires-python = ">=3.13" +requires-python = ">=3.12" [[package]] name = "annotated-types" @@ -18,6 +18,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, { name = "sniffio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f1/b4/636b3b65173d3ce9a38ef5f0522789614e590dab6a8d505340a4efe4c567/anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6", size = 213252, upload-time = "2025-08-04T08:54:26.451Z" } wheels = [ @@ -34,33 +35,21 @@ wheels = [ ] [[package]] -name = "black" -version = "25.9.0" +name = "certifi" +version = "2025.8.3" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "mypy-extensions" }, - { name = "packaging" }, - { name = "pathspec" }, - { name = "platformdirs" }, - { name = "pytokens" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/4b/43/20b5c90612d7bdb2bdbcceeb53d588acca3bb8f0e4c5d5c751a2c8fdd55a/black-25.9.0.tar.gz", hash = "sha256:0474bca9a0dd1b51791fcc507a4e02078a1c63f6d4e4ae5544b9848c7adfb619", size = 648393, upload-time = "2025-09-19T00:27:37.758Z" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/48/99/3acfea65f5e79f45472c45f87ec13037b506522719cd9d4ac86484ff51ac/black-25.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0172a012f725b792c358d57fe7b6b6e8e67375dd157f64fa7a3097b3ed3e2175", size = 1742165, upload-time = "2025-09-19T00:34:10.402Z" }, - { url = "https://files.pythonhosted.org/packages/3a/18/799285282c8236a79f25d590f0222dbd6850e14b060dfaa3e720241fd772/black-25.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3bec74ee60f8dfef564b573a96b8930f7b6a538e846123d5ad77ba14a8d7a64f", size = 1581259, upload-time = "2025-09-19T00:32:49.685Z" }, - { url = "https://files.pythonhosted.org/packages/f1/ce/883ec4b6303acdeca93ee06b7622f1fa383c6b3765294824165d49b1a86b/black-25.9.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b756fc75871cb1bcac5499552d771822fd9db5a2bb8db2a7247936ca48f39831", size = 1655583, upload-time = "2025-09-19T00:30:44.505Z" }, - { url = "https://files.pythonhosted.org/packages/21/17/5c253aa80a0639ccc427a5c7144534b661505ae2b5a10b77ebe13fa25334/black-25.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:846d58e3ce7879ec1ffe816bb9df6d006cd9590515ed5d17db14e17666b2b357", size = 1343428, upload-time = "2025-09-19T00:32:13.839Z" }, - { url = "https://files.pythonhosted.org/packages/1b/46/863c90dcd3f9d41b109b7f19032ae0db021f0b2a81482ba0a1e28c84de86/black-25.9.0-py3-none-any.whl", hash = "sha256:474b34c1342cdc157d307b56c4c65bce916480c4a8f6551fdc6bf9b486a7c4ae", size = 203363, upload-time = "2025-09-19T00:27:35.724Z" }, + { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" }, ] [[package]] -name = "certifi" -version = "2025.8.3" +name = "cfgv" +version = "3.5.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" }, + { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, ] [[package]] @@ -69,6 +58,17 @@ version = "3.4.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/5e/14c94999e418d9b87682734589404a25854d5f5d0408df68bc15b6ff54bb/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", size = 205655, upload-time = "2025-08-09T07:56:08.475Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a8/c6ec5d389672521f644505a257f50544c074cf5fc292d5390331cd6fc9c3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", size = 146223, upload-time = "2025-08-09T07:56:09.708Z" }, + { url = "https://files.pythonhosted.org/packages/fc/eb/a2ffb08547f4e1e5415fb69eb7db25932c52a52bed371429648db4d84fb1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018", size = 159366, upload-time = "2025-08-09T07:56:11.326Z" }, + { url = "https://files.pythonhosted.org/packages/82/10/0fd19f20c624b278dddaf83b8464dcddc2456cb4b02bb902a6da126b87a1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392", size = 157104, upload-time = "2025-08-09T07:56:13.014Z" }, + { url = "https://files.pythonhosted.org/packages/16/ab/0233c3231af734f5dfcf0844aa9582d5a1466c985bbed6cedab85af9bfe3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f", size = 151830, upload-time = "2025-08-09T07:56:14.428Z" }, + { url = "https://files.pythonhosted.org/packages/ae/02/e29e22b4e02839a0e4a06557b1999d0a47db3567e82989b5bb21f3fbbd9f/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154", size = 148854, upload-time = "2025-08-09T07:56:16.051Z" }, + { url = "https://files.pythonhosted.org/packages/05/6b/e2539a0a4be302b481e8cafb5af8792da8093b486885a1ae4d15d452bcec/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491", size = 160670, upload-time = "2025-08-09T07:56:17.314Z" }, + { url = "https://files.pythonhosted.org/packages/31/e7/883ee5676a2ef217a40ce0bffcc3d0dfbf9e64cbcfbdf822c52981c3304b/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93", size = 158501, upload-time = "2025-08-09T07:56:18.641Z" }, + { url = "https://files.pythonhosted.org/packages/c1/35/6525b21aa0db614cf8b5792d232021dca3df7f90a1944db934efa5d20bb1/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f", size = 153173, upload-time = "2025-08-09T07:56:20.289Z" }, + { url = "https://files.pythonhosted.org/packages/50/ee/f4704bad8201de513fdc8aac1cabc87e38c5818c93857140e06e772b5892/charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37", size = 99822, upload-time = "2025-08-09T07:56:21.551Z" }, + { url = "https://files.pythonhosted.org/packages/39/f5/3b3836ca6064d0992c58c7561c6b6eee1b3892e9665d650c803bd5614522/charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc", size = 107543, upload-time = "2025-08-09T07:56:23.115Z" }, { url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326, upload-time = "2025-08-09T07:56:24.721Z" }, { url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008, upload-time = "2025-08-09T07:56:26.004Z" }, { url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196, upload-time = "2025-08-09T07:56:27.25Z" }, @@ -94,18 +94,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" }, ] -[[package]] -name = "click" -version = "8.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943, upload-time = "2025-09-18T17:32:23.696Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" }, -] - [[package]] name = "colorama" version = "0.4.6" @@ -121,6 +109,17 @@ version = "7.10.6" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/14/70/025b179c993f019105b79575ac6edb5e084fb0f0e63f15cdebef4e454fb5/coverage-7.10.6.tar.gz", hash = "sha256:f644a3ae5933a552a29dbb9aa2f90c677a875f80ebea028e5a52a4f429044b90", size = 823736, upload-time = "2025-08-29T15:35:16.668Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/26/06/263f3305c97ad78aab066d116b52250dd316e74fcc20c197b61e07eb391a/coverage-7.10.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5b2dd6059938063a2c9fee1af729d4f2af28fd1a545e9b7652861f0d752ebcea", size = 217324, upload-time = "2025-08-29T15:33:29.06Z" }, + { url = "https://files.pythonhosted.org/packages/e9/60/1e1ded9a4fe80d843d7d53b3e395c1db3ff32d6c301e501f393b2e6c1c1f/coverage-7.10.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:388d80e56191bf846c485c14ae2bc8898aa3124d9d35903fef7d907780477634", size = 217560, upload-time = "2025-08-29T15:33:30.748Z" }, + { url = "https://files.pythonhosted.org/packages/b8/25/52136173c14e26dfed8b106ed725811bb53c30b896d04d28d74cb64318b3/coverage-7.10.6-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:90cb5b1a4670662719591aa92d0095bb41714970c0b065b02a2610172dbf0af6", size = 249053, upload-time = "2025-08-29T15:33:32.041Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1d/ae25a7dc58fcce8b172d42ffe5313fc267afe61c97fa872b80ee72d9515a/coverage-7.10.6-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:961834e2f2b863a0e14260a9a273aff07ff7818ab6e66d2addf5628590c628f9", size = 251802, upload-time = "2025-08-29T15:33:33.625Z" }, + { url = "https://files.pythonhosted.org/packages/f5/7a/1f561d47743710fe996957ed7c124b421320f150f1d38523d8d9102d3e2a/coverage-7.10.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf9a19f5012dab774628491659646335b1928cfc931bf8d97b0d5918dd58033c", size = 252935, upload-time = "2025-08-29T15:33:34.909Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ad/8b97cd5d28aecdfde792dcbf646bac141167a5cacae2cd775998b45fabb5/coverage-7.10.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:99c4283e2a0e147b9c9cc6bc9c96124de9419d6044837e9799763a0e29a7321a", size = 250855, upload-time = "2025-08-29T15:33:36.922Z" }, + { url = "https://files.pythonhosted.org/packages/33/6a/95c32b558d9a61858ff9d79580d3877df3eb5bc9eed0941b1f187c89e143/coverage-7.10.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:282b1b20f45df57cc508c1e033403f02283adfb67d4c9c35a90281d81e5c52c5", size = 248974, upload-time = "2025-08-29T15:33:38.175Z" }, + { url = "https://files.pythonhosted.org/packages/0d/9c/8ce95dee640a38e760d5b747c10913e7a06554704d60b41e73fdea6a1ffd/coverage-7.10.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8cdbe264f11afd69841bd8c0d83ca10b5b32853263ee62e6ac6a0ab63895f972", size = 250409, upload-time = "2025-08-29T15:33:39.447Z" }, + { url = "https://files.pythonhosted.org/packages/04/12/7a55b0bdde78a98e2eb2356771fd2dcddb96579e8342bb52aa5bc52e96f0/coverage-7.10.6-cp312-cp312-win32.whl", hash = "sha256:a517feaf3a0a3eca1ee985d8373135cfdedfbba3882a5eab4362bda7c7cf518d", size = 219724, upload-time = "2025-08-29T15:33:41.172Z" }, + { url = "https://files.pythonhosted.org/packages/36/4a/32b185b8b8e327802c9efce3d3108d2fe2d9d31f153a0f7ecfd59c773705/coverage-7.10.6-cp312-cp312-win_amd64.whl", hash = "sha256:856986eadf41f52b214176d894a7de05331117f6035a28ac0016c0f63d887629", size = 220536, upload-time = "2025-08-29T15:33:42.524Z" }, + { url = "https://files.pythonhosted.org/packages/08/3a/d5d8dc703e4998038c3099eaf77adddb00536a3cec08c8dcd556a36a3eb4/coverage-7.10.6-cp312-cp312-win_arm64.whl", hash = "sha256:acf36b8268785aad739443fa2780c16260ee3fa09d12b3a70f772ef100939d80", size = 219171, upload-time = "2025-08-29T15:33:43.974Z" }, { url = "https://files.pythonhosted.org/packages/bd/e7/917e5953ea29a28c1057729c1d5af9084ab6d9c66217523fd0e10f14d8f6/coverage-7.10.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ffea0575345e9ee0144dfe5701aa17f3ba546f8c3bb48db62ae101afb740e7d6", size = 217351, upload-time = "2025-08-29T15:33:45.438Z" }, { url = "https://files.pythonhosted.org/packages/eb/86/2e161b93a4f11d0ea93f9bebb6a53f113d5d6e416d7561ca41bb0a29996b/coverage-7.10.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:95d91d7317cde40a1c249d6b7382750b7e6d86fad9d8eaf4fa3f8f44cf171e80", size = 217600, upload-time = "2025-08-29T15:33:47.269Z" }, { url = "https://files.pythonhosted.org/packages/0e/66/d03348fdd8df262b3a7fb4ee5727e6e4936e39e2f3a842e803196946f200/coverage-7.10.6-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e23dd5408fe71a356b41baa82892772a4cefcf758f2ca3383d2aa39e1b7a003", size = 248600, upload-time = "2025-08-29T15:33:48.953Z" }, @@ -168,6 +167,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/44/0c/50db5379b615854b5cf89146f8f5bd1d5a9693d7f3a987e269693521c404/coverage-7.10.6-py3-none-any.whl", hash = "sha256:92c4ecf6bf11b2e85fd4d8204814dc26e6a19f0c9d938c207c5cb0eadfcabbe3", size = 208986, upload-time = "2025-08-29T15:35:14.506Z" }, ] +[[package]] +name = "distlib" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, +] + +[[package]] +name = "filelock" +version = "3.24.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/73/92/a8e2479937ff39185d20dd6a851c1a63e55849e447a55e798cc2e1f49c65/filelock-3.24.3.tar.gz", hash = "sha256:011a5644dc937c22699943ebbfc46e969cdde3e171470a6e40b9533e5a72affa", size = 37935, upload-time = "2026-02-19T00:48:20.543Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/0f/5d0c71a1aefeb08efff26272149e07ab922b64f46c63363756224bd6872e/filelock-3.24.3-py3-none-any.whl", hash = "sha256:426e9a4660391f7f8a810d71b0555bce9008b0a1cc342ab1f6947d37639e002d", size = 24331, upload-time = "2026-02-19T00:48:18.465Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -205,6 +222,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] +[[package]] +name = "identify" +version = "2.6.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/8d/e8b97e6bd3fb6fb271346f7981362f1e04d6a7463abd0de79e1fda17c067/identify-2.6.16.tar.gz", hash = "sha256:846857203b5511bbe94d5a352a48ef2359532bc8f6727b5544077a0dcfb24980", size = 99360, upload-time = "2026-01-12T18:58:58.201Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/58/40fbbcefeda82364720eba5cf2270f98496bdfa19ea75b4cccae79c698e6/identify-2.6.16-py2.py3-none-any.whl", hash = "sha256:391ee4d77741d994189522896270b787aed8670389bfd60f326d677d64a6dfb0", size = 99202, upload-time = "2026-01-12T18:58:56.627Z" }, +] + [[package]] name = "idna" version = "3.10" @@ -223,15 +249,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, ] -[[package]] -name = "isort" -version = "6.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b8/21/1e2a441f74a653a144224d7d21afe8f4169e6c7c20bb13aec3a2dc3815e0/isort-6.0.1.tar.gz", hash = "sha256:1cb5df28dfbc742e490c5e41bad6da41b805b0a8be7bc93cd0fb2a8a890ac450", size = 821955, upload-time = "2025-02-26T21:13:16.955Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/11/114d0a5f4dabbdcedc1125dee0888514c3c3b16d3e9facad87ed96fad97c/isort-6.0.1-py3-none-any.whl", hash = "sha256:2dc5d7f65c9678d94c88dfc29161a320eec67328bc97aad576874cb4be1e9615", size = 94186, upload-time = "2025-02-26T21:13:14.911Z" }, -] - [[package]] name = "kinexon-handball-api" version = "0.1.0" @@ -245,39 +262,36 @@ dependencies = [ { name = "pytest-order" }, { name = "python-dotenv" }, { name = "pyyaml" }, - { name = "requests" }, { name = "tqdm" }, { name = "typing-extensions" }, ] [package.optional-dependencies] dev = [ - { name = "black" }, - { name = "isort" }, { name = "mypy" }, + { name = "pre-commit" }, { name = "pytest" }, { name = "pytest-cov" }, { name = "requests-mock" }, + { name = "ruff" }, + { name = "types-pyyaml" }, { name = "types-requests" }, ] lint = [ - { name = "black" }, - { name = "isort" }, { name = "mypy" }, + { name = "ruff" }, + { name = "types-pyyaml" }, { name = "types-requests" }, ] [package.metadata] requires-dist = [ { name = "attrs", specifier = ">=25.3.0" }, - { name = "black", marker = "extra == 'dev'", specifier = ">=24.0.0" }, - { name = "black", marker = "extra == 'lint'", specifier = ">=24.0.0" }, { name = "httpx", specifier = ">=0.27.0" }, - { name = "isort", marker = "extra == 'dev'", specifier = ">=5.13.0" }, - { name = "isort", marker = "extra == 'lint'", specifier = ">=5.13.0" }, { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.8.0" }, { name = "mypy", marker = "extra == 'lint'", specifier = ">=1.8.0" }, { name = "pandas", specifier = ">=2.2.2" }, + { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=4.5.1" }, { name = "pydantic", specifier = ">=2.6" }, { name = "pytest", specifier = ">=8.4.2" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.4.1" }, @@ -285,9 +299,12 @@ requires-dist = [ { name = "pytest-order", specifier = ">=1.3.0" }, { name = "python-dotenv", specifier = ">=1.1.0" }, { name = "pyyaml", specifier = ">=6.0.2" }, - { name = "requests", specifier = ">=2.32.5" }, { name = "requests-mock", marker = "extra == 'dev'", specifier = ">=1.12" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.12.5" }, + { name = "ruff", marker = "extra == 'lint'", specifier = ">=0.12.5" }, { name = "tqdm", specifier = ">=4.67.1" }, + { name = "types-pyyaml", marker = "extra == 'dev'", specifier = ">=6.0.12.20250915" }, + { name = "types-pyyaml", marker = "extra == 'lint'", specifier = ">=6.0.12.20250915" }, { name = "types-requests", marker = "extra == 'dev'", specifier = ">=2.31.0" }, { name = "types-requests", marker = "extra == 'lint'", specifier = ">=2.31.0" }, { name = "typing-extensions", specifier = ">=4.8" }, @@ -305,6 +322,12 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/c0/77/8f0d0001ffad290cef2f7f216f96c814866248a0b92a722365ed54648e7e/mypy-1.18.2.tar.gz", hash = "sha256:06a398102a5f203d7477b2923dda3634c36727fa5c237d8f859ef90c42a9924b", size = 3448846, upload-time = "2025-09-19T00:11:10.519Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/07/06/dfdd2bc60c66611dd8335f463818514733bc763e4760dee289dcc33df709/mypy-1.18.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:33eca32dd124b29400c31d7cf784e795b050ace0e1f91b8dc035672725617e34", size = 12908273, upload-time = "2025-09-19T00:10:58.321Z" }, + { url = "https://files.pythonhosted.org/packages/81/14/6a9de6d13a122d5608e1a04130724caf9170333ac5a924e10f670687d3eb/mypy-1.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a3c47adf30d65e89b2dcd2fa32f3aeb5e94ca970d2c15fcb25e297871c8e4764", size = 11920910, upload-time = "2025-09-19T00:10:20.043Z" }, + { url = "https://files.pythonhosted.org/packages/5f/a9/b29de53e42f18e8cc547e38daa9dfa132ffdc64f7250e353f5c8cdd44bee/mypy-1.18.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d6c838e831a062f5f29d11c9057c6009f60cb294fea33a98422688181fe2893", size = 12465585, upload-time = "2025-09-19T00:10:33.005Z" }, + { url = "https://files.pythonhosted.org/packages/77/ae/6c3d2c7c61ff21f2bee938c917616c92ebf852f015fb55917fd6e2811db2/mypy-1.18.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01199871b6110a2ce984bde85acd481232d17413868c9807e95c1b0739a58914", size = 13348562, upload-time = "2025-09-19T00:10:11.51Z" }, + { url = "https://files.pythonhosted.org/packages/4d/31/aec68ab3b4aebdf8f36d191b0685d99faa899ab990753ca0fee60fb99511/mypy-1.18.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a2afc0fa0b0e91b4599ddfe0f91e2c26c2b5a5ab263737e998d6817874c5f7c8", size = 13533296, upload-time = "2025-09-19T00:10:06.568Z" }, + { url = "https://files.pythonhosted.org/packages/9f/83/abcb3ad9478fca3ebeb6a5358bb0b22c95ea42b43b7789c7fb1297ca44f4/mypy-1.18.2-cp312-cp312-win_amd64.whl", hash = "sha256:d8068d0afe682c7c4897c0f7ce84ea77f6de953262b12d07038f4d296d547074", size = 9828828, upload-time = "2025-09-19T00:10:28.203Z" }, { url = "https://files.pythonhosted.org/packages/5f/04/7f462e6fbba87a72bc8097b93f6842499c428a6ff0c81dd46948d175afe8/mypy-1.18.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:07b8b0f580ca6d289e69209ec9d3911b4a26e5abfde32228a288eb79df129fcc", size = 12898728, upload-time = "2025-09-19T00:10:01.33Z" }, { url = "https://files.pythonhosted.org/packages/99/5b/61ed4efb64f1871b41fd0b82d29a64640f3516078f6c7905b68ab1ad8b13/mypy-1.18.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ed4482847168439651d3feee5833ccedbf6657e964572706a2adb1f7fa4dfe2e", size = 11910758, upload-time = "2025-09-19T00:10:42.607Z" }, { url = "https://files.pythonhosted.org/packages/3c/46/d297d4b683cc89a6e4108c4250a6a6b717f5fa96e1a30a7944a6da44da35/mypy-1.18.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3ad2afadd1e9fea5cf99a45a822346971ede8685cc581ed9cd4d42eaf940986", size = 12475342, upload-time = "2025-09-19T00:11:00.371Z" }, @@ -329,12 +352,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] +[[package]] +name = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + [[package]] name = "numpy" version = "2.3.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d0/19/95b3d357407220ed24c139018d2518fab0a61a948e68286a25f1a4d049ff/numpy-2.3.3.tar.gz", hash = "sha256:ddc7c39727ba62b80dfdbedf400d1c10ddfa8eefbd7ec8dcb118be8b56d31029", size = 20576648, upload-time = "2025-09-09T16:54:12.543Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/51/5d/bb7fc075b762c96329147799e1bcc9176ab07ca6375ea976c475482ad5b3/numpy-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cfdd09f9c84a1a934cde1eec2267f0a43a7cd44b2cca4ff95b7c0d14d144b0bf", size = 20957014, upload-time = "2025-09-09T15:56:29.966Z" }, + { url = "https://files.pythonhosted.org/packages/6b/0e/c6211bb92af26517acd52125a237a92afe9c3124c6a68d3b9f81b62a0568/numpy-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb32e3cf0f762aee47ad1ddc6672988f7f27045b0783c887190545baba73aa25", size = 14185220, upload-time = "2025-09-09T15:56:32.175Z" }, + { url = "https://files.pythonhosted.org/packages/22/f2/07bb754eb2ede9073f4054f7c0286b0d9d2e23982e090a80d478b26d35ca/numpy-2.3.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:396b254daeb0a57b1fe0ecb5e3cff6fa79a380fa97c8f7781a6d08cd429418fe", size = 5113918, upload-time = "2025-09-09T15:56:34.175Z" }, + { url = "https://files.pythonhosted.org/packages/81/0a/afa51697e9fb74642f231ea36aca80fa17c8fb89f7a82abd5174023c3960/numpy-2.3.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:067e3d7159a5d8f8a0b46ee11148fc35ca9b21f61e3c49fbd0a027450e65a33b", size = 6647922, upload-time = "2025-09-09T15:56:36.149Z" }, + { url = "https://files.pythonhosted.org/packages/5d/f5/122d9cdb3f51c520d150fef6e87df9279e33d19a9611a87c0d2cf78a89f4/numpy-2.3.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c02d0629d25d426585fb2e45a66154081b9fa677bc92a881ff1d216bc9919a8", size = 14281991, upload-time = "2025-09-09T15:56:40.548Z" }, + { url = "https://files.pythonhosted.org/packages/51/64/7de3c91e821a2debf77c92962ea3fe6ac2bc45d0778c1cbe15d4fce2fd94/numpy-2.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9192da52b9745f7f0766531dcfa978b7763916f158bb63bdb8a1eca0068ab20", size = 16641643, upload-time = "2025-09-09T15:56:43.343Z" }, + { url = "https://files.pythonhosted.org/packages/30/e4/961a5fa681502cd0d68907818b69f67542695b74e3ceaa513918103b7e80/numpy-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cd7de500a5b66319db419dc3c345244404a164beae0d0937283b907d8152e6ea", size = 16056787, upload-time = "2025-09-09T15:56:46.141Z" }, + { url = "https://files.pythonhosted.org/packages/99/26/92c912b966e47fbbdf2ad556cb17e3a3088e2e1292b9833be1dfa5361a1a/numpy-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:93d4962d8f82af58f0b2eb85daaf1b3ca23fe0a85d0be8f1f2b7bb46034e56d7", size = 18579598, upload-time = "2025-09-09T15:56:49.844Z" }, + { url = "https://files.pythonhosted.org/packages/17/b6/fc8f82cb3520768718834f310c37d96380d9dc61bfdaf05fe5c0b7653e01/numpy-2.3.3-cp312-cp312-win32.whl", hash = "sha256:5534ed6b92f9b7dca6c0a19d6df12d41c68b991cef051d108f6dbff3babc4ebf", size = 6320800, upload-time = "2025-09-09T15:56:52.499Z" }, + { url = "https://files.pythonhosted.org/packages/32/ee/de999f2625b80d043d6d2d628c07d0d5555a677a3cf78fdf868d409b8766/numpy-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:497d7cad08e7092dba36e3d296fe4c97708c93daf26643a1ae4b03f6294d30eb", size = 12786615, upload-time = "2025-09-09T15:56:54.422Z" }, + { url = "https://files.pythonhosted.org/packages/49/6e/b479032f8a43559c383acb20816644f5f91c88f633d9271ee84f3b3a996c/numpy-2.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:ca0309a18d4dfea6fc6262a66d06c26cfe4640c3926ceec90e57791a82b6eee5", size = 10195936, upload-time = "2025-09-09T15:56:56.541Z" }, { url = "https://files.pythonhosted.org/packages/7d/b9/984c2b1ee61a8b803bf63582b4ac4242cf76e2dbd663efeafcb620cc0ccb/numpy-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f5415fb78995644253370985342cd03572ef8620b934da27d77377a2285955bf", size = 20949588, upload-time = "2025-09-09T15:56:59.087Z" }, { url = "https://files.pythonhosted.org/packages/a6/e4/07970e3bed0b1384d22af1e9912527ecbeb47d3b26e9b6a3bced068b3bea/numpy-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d00de139a3324e26ed5b95870ce63be7ec7352171bc69a4cf1f157a48e3eb6b7", size = 14177802, upload-time = "2025-09-09T15:57:01.73Z" }, { url = "https://files.pythonhosted.org/packages/35/c7/477a83887f9de61f1203bad89cf208b7c19cc9fef0cebef65d5a1a0619f2/numpy-2.3.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:9dc13c6a5829610cc07422bc74d3ac083bd8323f14e2827d992f9e52e22cd6a6", size = 5106537, upload-time = "2025-09-09T15:57:03.765Z" }, @@ -402,6 +445,13 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/79/8e/0e90233ac205ad182bd6b422532695d2b9414944a280488105d598c70023/pandas-2.3.2.tar.gz", hash = "sha256:ab7b58f8f82706890924ccdfb5f48002b83d2b5a3845976a9fb705d36c34dcdb", size = 4488684, upload-time = "2025-08-21T10:28:29.257Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/db/614c20fb7a85a14828edd23f1c02db58a30abf3ce76f38806155d160313c/pandas-2.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fbb977f802156e7a3f829e9d1d5398f6192375a3e2d1a9ee0803e35fe70a2b9", size = 11587652, upload-time = "2025-08-21T10:27:15.888Z" }, + { url = "https://files.pythonhosted.org/packages/99/b0/756e52f6582cade5e746f19bad0517ff27ba9c73404607c0306585c201b3/pandas-2.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1b9b52693123dd234b7c985c68b709b0b009f4521000d0525f2b95c22f15944b", size = 10717686, upload-time = "2025-08-21T10:27:18.486Z" }, + { url = "https://files.pythonhosted.org/packages/37/4c/dd5ccc1e357abfeee8353123282de17997f90ff67855f86154e5a13b81e5/pandas-2.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bd281310d4f412733f319a5bc552f86d62cddc5f51d2e392c8787335c994175", size = 11278722, upload-time = "2025-08-21T10:27:21.149Z" }, + { url = "https://files.pythonhosted.org/packages/d3/a4/f7edcfa47e0a88cda0be8b068a5bae710bf264f867edfdf7b71584ace362/pandas-2.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96d31a6b4354e3b9b8a2c848af75d31da390657e3ac6f30c05c82068b9ed79b9", size = 11987803, upload-time = "2025-08-21T10:27:23.767Z" }, + { url = "https://files.pythonhosted.org/packages/f6/61/1bce4129f93ab66f1c68b7ed1c12bac6a70b1b56c5dab359c6bbcd480b52/pandas-2.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:df4df0b9d02bb873a106971bb85d448378ef14b86ba96f035f50bbd3688456b4", size = 12766345, upload-time = "2025-08-21T10:27:26.6Z" }, + { url = "https://files.pythonhosted.org/packages/8e/46/80d53de70fee835531da3a1dae827a1e76e77a43ad22a8cd0f8142b61587/pandas-2.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:213a5adf93d020b74327cb2c1b842884dbdd37f895f42dcc2f09d451d949f811", size = 13439314, upload-time = "2025-08-21T10:27:29.213Z" }, + { url = "https://files.pythonhosted.org/packages/28/30/8114832daff7489f179971dbc1d854109b7f4365a546e3ea75b6516cea95/pandas-2.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c13b81a9347eb8c7548f53fd9a4f08d4dfe996836543f805c987bafa03317ae", size = 10983326, upload-time = "2025-08-21T10:27:31.901Z" }, { url = "https://files.pythonhosted.org/packages/27/64/a2f7bf678af502e16b472527735d168b22b7824e45a4d7e96a4fbb634b59/pandas-2.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0c6ecbac99a354a051ef21c5307601093cb9e0f4b1855984a084bfec9302699e", size = 11531061, upload-time = "2025-08-21T10:27:34.647Z" }, { url = "https://files.pythonhosted.org/packages/54/4c/c3d21b2b7769ef2f4c2b9299fcadd601efa6729f1357a8dbce8dd949ed70/pandas-2.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c6f048aa0fd080d6a06cc7e7537c09b53be6642d330ac6f54a600c3ace857ee9", size = 10668666, upload-time = "2025-08-21T10:27:37.203Z" }, { url = "https://files.pythonhosted.org/packages/50/e2/f775ba76ecfb3424d7f5862620841cf0edb592e9abd2d2a5387d305fe7a8/pandas-2.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0064187b80a5be6f2f9c9d6bdde29372468751dfa89f4211a3c5871854cfbf7a", size = 11332835, upload-time = "2025-08-21T10:27:40.188Z" }, @@ -444,6 +494,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] +[[package]] +name = "pre-commit" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/f1/6d86a29246dfd2e9b6237f0b5823717f60cad94d47ddc26afa916d21f525/pre_commit-4.5.1.tar.gz", hash = "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61", size = 198232, upload-time = "2025-12-16T21:14:33.552Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", size = 226437, upload-time = "2025-12-16T21:14:32.409Z" }, +] + [[package]] name = "pydantic" version = "2.11.9" @@ -468,6 +534,20 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" }, + { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" }, + { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" }, + { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" }, + { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" }, + { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" }, + { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" }, + { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" }, + { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" }, + { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" }, { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, @@ -551,21 +631,25 @@ wheels = [ ] [[package]] -name = "python-dotenv" -version = "1.1.1" +name = "python-discovery" +version = "1.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" } +dependencies = [ + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/bb/93a3e83bdf9322c7e21cafd092e56a4a17c4d8ef4277b6eb01af1a540a6f/python_discovery-1.1.0.tar.gz", hash = "sha256:447941ba1aed8cc2ab7ee3cb91be5fc137c5bdbb05b7e6ea62fbdcb66e50b268", size = 55674, upload-time = "2026-02-26T09:42:49.668Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" }, + { url = "https://files.pythonhosted.org/packages/06/54/82a6e2ef37f0f23dccac604b9585bdcbd0698604feb64807dcb72853693e/python_discovery-1.1.0-py3-none-any.whl", hash = "sha256:a162893b8809727f54594a99ad2179d2ede4bf953e12d4c7abc3cc9cdbd1437b", size = 30687, upload-time = "2026-02-26T09:42:48.548Z" }, ] [[package]] -name = "pytokens" -version = "0.1.10" +name = "python-dotenv" +version = "1.1.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/30/5f/e959a442435e24f6fb5a01aec6c657079ceaca1b3baf18561c3728d681da/pytokens-0.1.10.tar.gz", hash = "sha256:c9a4bfa0be1d26aebce03e6884ba454e842f186a59ea43a6d3b25af58223c044", size = 12171, upload-time = "2025-02-19T14:51:22.001Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/60/e5/63bed382f6a7a5ba70e7e132b8b7b8abbcf4888ffa6be4877698dcfbed7d/pytokens-0.1.10-py3-none-any.whl", hash = "sha256:db7b72284e480e69fb085d9f251f66b3d2df8b7166059261258ff35f50fb711b", size = 12046, upload-time = "2025-02-19T14:51:18.694Z" }, + { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" }, ] [[package]] @@ -583,6 +667,15 @@ version = "6.0.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, @@ -621,6 +714,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/97/ec/889fbc557727da0c34a33850950310240f2040f3b1955175fdb2b36a8910/requests_mock-1.12.1-py2.py3-none-any.whl", hash = "sha256:b1e37054004cdd5e56c84454cc7df12b25f90f382159087f4b6915aaeef39563", size = 27695, upload-time = "2024-03-29T03:54:27.64Z" }, ] +[[package]] +name = "ruff" +version = "0.15.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/31/d6e536cdebb6568ae75a7f00e4b4819ae0ad2640c3604c305a0428680b0c/ruff-0.15.4.tar.gz", hash = "sha256:3412195319e42d634470cc97aa9803d07e9d5c9223b99bcb1518f0c725f26ae1", size = 4569550, upload-time = "2026-02-26T20:04:14.959Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/82/c11a03cfec3a4d26a0ea1e571f0f44be5993b923f905eeddfc397c13d360/ruff-0.15.4-py3-none-linux_armv6l.whl", hash = "sha256:a1810931c41606c686bae8b5b9a8072adac2f611bb433c0ba476acba17a332e0", size = 10453333, upload-time = "2026-02-26T20:04:20.093Z" }, + { url = "https://files.pythonhosted.org/packages/ce/5d/6a1f271f6e31dffb31855996493641edc3eef8077b883eaf007a2f1c2976/ruff-0.15.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5a1632c66672b8b4d3e1d1782859e98d6e0b4e70829530666644286600a33992", size = 10853356, upload-time = "2026-02-26T20:04:05.808Z" }, + { url = "https://files.pythonhosted.org/packages/b1/d8/0fab9f8842b83b1a9c2bf81b85063f65e93fb512e60effa95b0be49bfc54/ruff-0.15.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a4386ba2cd6c0f4ff75252845906acc7c7c8e1ac567b7bc3d373686ac8c222ba", size = 10187434, upload-time = "2026-02-26T20:03:54.656Z" }, + { url = "https://files.pythonhosted.org/packages/85/cc/cc220fd9394eff5db8d94dec199eec56dd6c9f3651d8869d024867a91030/ruff-0.15.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2496488bdfd3732747558b6f95ae427ff066d1fcd054daf75f5a50674411e75", size = 10535456, upload-time = "2026-02-26T20:03:52.738Z" }, + { url = "https://files.pythonhosted.org/packages/fa/0f/bced38fa5cf24373ec767713c8e4cadc90247f3863605fb030e597878661/ruff-0.15.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3f1c4893841ff2d54cbda1b2860fa3260173df5ddd7b95d370186f8a5e66a4ac", size = 10287772, upload-time = "2026-02-26T20:04:08.138Z" }, + { url = "https://files.pythonhosted.org/packages/2b/90/58a1802d84fed15f8f281925b21ab3cecd813bde52a8ca033a4de8ab0e7a/ruff-0.15.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:820b8766bd65503b6c30aaa6331e8ef3a6e564f7999c844e9a547c40179e440a", size = 11049051, upload-time = "2026-02-26T20:04:03.53Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ac/b7ad36703c35f3866584564dc15f12f91cb1a26a897dc2fd13d7cb3ae1af/ruff-0.15.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9fb74bab47139c1751f900f857fa503987253c3ef89129b24ed375e72873e85", size = 11890494, upload-time = "2026-02-26T20:04:10.497Z" }, + { url = "https://files.pythonhosted.org/packages/93/3d/3eb2f47a39a8b0da99faf9c54d3eb24720add1e886a5309d4d1be73a6380/ruff-0.15.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f80c98765949c518142b3a50a5db89343aa90f2c2bf7799de9986498ae6176db", size = 11326221, upload-time = "2026-02-26T20:04:12.84Z" }, + { url = "https://files.pythonhosted.org/packages/ff/90/bf134f4c1e5243e62690e09d63c55df948a74084c8ac3e48a88468314da6/ruff-0.15.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:451a2e224151729b3b6c9ffb36aed9091b2996fe4bdbd11f47e27d8f2e8888ec", size = 11168459, upload-time = "2026-02-26T20:04:00.969Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e5/a64d27688789b06b5d55162aafc32059bb8c989c61a5139a36e1368285eb/ruff-0.15.4-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a8f157f2e583c513c4f5f896163a93198297371f34c04220daf40d133fdd4f7f", size = 11104366, upload-time = "2026-02-26T20:03:48.099Z" }, + { url = "https://files.pythonhosted.org/packages/f1/f6/32d1dcb66a2559763fc3027bdd65836cad9eb09d90f2ed6a63d8e9252b02/ruff-0.15.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:917cc68503357021f541e69b35361c99387cdbbf99bd0ea4aa6f28ca99ff5338", size = 10510887, upload-time = "2026-02-26T20:03:45.771Z" }, + { url = "https://files.pythonhosted.org/packages/ff/92/22d1ced50971c5b6433aed166fcef8c9343f567a94cf2b9d9089f6aa80fe/ruff-0.15.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e9737c8161da79fd7cfec19f1e35620375bd8b2a50c3e77fa3d2c16f574105cc", size = 10285939, upload-time = "2026-02-26T20:04:22.42Z" }, + { url = "https://files.pythonhosted.org/packages/e6/f4/7c20aec3143837641a02509a4668fb146a642fd1211846634edc17eb5563/ruff-0.15.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:291258c917539e18f6ba40482fe31d6f5ac023994ee11d7bdafd716f2aab8a68", size = 10765471, upload-time = "2026-02-26T20:03:58.924Z" }, + { url = "https://files.pythonhosted.org/packages/d0/09/6d2f7586f09a16120aebdff8f64d962d7c4348313c77ebb29c566cefc357/ruff-0.15.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3f83c45911da6f2cd5936c436cf86b9f09f09165f033a99dcf7477e34041cbc3", size = 11263382, upload-time = "2026-02-26T20:04:24.424Z" }, + { url = "https://files.pythonhosted.org/packages/1b/fa/2ef715a1cd329ef47c1a050e10dee91a9054b7ce2fcfdd6a06d139afb7ec/ruff-0.15.4-py3-none-win32.whl", hash = "sha256:65594a2d557d4ee9f02834fcdf0a28daa8b3b9f6cb2cb93846025a36db47ef22", size = 10506664, upload-time = "2026-02-26T20:03:50.56Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a8/c688ef7e29983976820d18710f955751d9f4d4eb69df658af3d006e2ba3e/ruff-0.15.4-py3-none-win_amd64.whl", hash = "sha256:04196ad44f0df220c2ece5b0e959c2f37c777375ec744397d21d15b50a75264f", size = 11651048, upload-time = "2026-02-26T20:04:17.191Z" }, + { url = "https://files.pythonhosted.org/packages/3e/0a/9e1be9035b37448ce2e68c978f0591da94389ade5a5abafa4cf99985d1b2/ruff-0.15.4-py3-none-win_arm64.whl", hash = "sha256:60d5177e8cfc70e51b9c5fad936c634872a74209f934c1e79107d11787ad5453", size = 10966776, upload-time = "2026-02-26T20:03:56.908Z" }, +] + [[package]] name = "six" version = "1.17.0" @@ -651,6 +769,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, ] +[[package]] +name = "types-pyyaml" +version = "6.0.12.20250915" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/69/3c51b36d04da19b92f9e815be12753125bd8bc247ba0470a982e6979e71c/types_pyyaml-6.0.12.20250915.tar.gz", hash = "sha256:0f8b54a528c303f0e6f7165687dd33fafa81c807fcac23f632b63aa624ced1d3", size = 17522, upload-time = "2025-09-15T03:01:00.728Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/e0/1eed384f02555dde685fff1a1ac805c1c7dcb6dd019c916fe659b1c1f9ec/types_pyyaml-6.0.12.20250915-py3-none-any.whl", hash = "sha256:e7d4d9e064e89a3b3cae120b4990cd370874d2bf12fa5f46c97018dd5d3c9ab6", size = 20338, upload-time = "2025-09-15T03:00:59.218Z" }, +] + [[package]] name = "types-requests" version = "2.32.4.20250913" @@ -701,3 +828,18 @@ sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599 wheels = [ { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, ] + +[[package]] +name = "virtualenv" +version = "21.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, + { name = "python-discovery" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/c9/18d4b36606d6091844daa3bd93cf7dc78e6f5da21d9f21d06c221104b684/virtualenv-21.1.0.tar.gz", hash = "sha256:1990a0188c8f16b6b9cf65c9183049007375b26aad415514d377ccacf1e4fb44", size = 5840471, upload-time = "2026-02-27T08:49:29.702Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/55/896b06bf93a49bec0f4ae2a6f1ed12bd05c8860744ac3a70eda041064e4d/virtualenv-21.1.0-py3-none-any.whl", hash = "sha256:164f5e14c5587d170cf98e60378eb91ea35bf037be313811905d3a24ea33cc07", size = 5825072, upload-time = "2026-02-27T08:49:27.516Z" }, +]