diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..57d5a07 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,37 @@ +name: Test + +on: + pull_request: + push: + branches: + - main + +permissions: + contents: read + +jobs: + test: + name: Python ${{ matrix.python-version }} + runs-on: ubuntu-latest + timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + python-version: + - "3.9" + - "3.10" + - "3.11" + - "3.12" + - "3.13" + - "3.14" + steps: + - name: Check out repository + uses: actions/checkout@v6.0.2 + + - name: Set up uv and Python + uses: astral-sh/setup-uv@v10.0.1 + with: + python-version: ${{ matrix.python-version }} + + - name: Run tests + run: uv run --frozen -- python -W error -m unittest discover -s tests -v diff --git a/README.md b/README.md index 8c26da5..34ee4bd 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,13 @@ # sanka-sdk -Python SDK for the Sanka API. +Python SDK for Sanka's hosted API and local migration lifecycle. This package is generated from Sanka's OpenAPI spec using Fern, then packaged locally for `uv` and PyPI. ## Install +Python 3.9 or newer is required. CI tests every minor from Python 3.9 through Python 3.14. + ```bash uv add sanka-sdk ``` @@ -20,6 +22,89 @@ response = client.public_auth.whoami() print(response) ``` +## Local migration + +The hosted API client and local migration adapter are separate surfaces: + +| Import | What runs | Authentication | +|---|---|---| +| `from sanka_sdk import SankaClient` | Sanka's hosted HTTP API | API token | +| `SankaMigrate` or `AsyncSankaMigrate` from `sanka_sdk.migrate` | A local `sanka-migrate` subprocess | None | + +Install the migration runtime separately, then use the tokenless adapter: + +```bash +uv tool install sanka-migrate +``` + +### Synchronous + +```python +from sanka_sdk.migrate import SankaMigrate + +migrate = SankaMigrate(cwd="./django-app") + +scan = migrate.scan() +plan = migrate.plan( + to="fastapi", + generation="full", + strategy="native", + package_manager="uv", +) +applied = migrate.apply(plan_hash=plan.data["plan_hash"]) +tested = migrate.test() +verified = migrate.verify() +``` + +### Asynchronous + +Use `AsyncSankaMigrate` to run the same commands without blocking the event +loop. Cancelling an awaited command also terminates its local CLI process. + +```python +import asyncio + +from sanka_sdk.migrate import AsyncSankaMigrate + + +async def main() -> None: + migrate = AsyncSankaMigrate(cwd="./django-app") + + await migrate.scan() + plan = await migrate.plan(to="fastapi", generation="full") + await migrate.apply(plan_hash=plan.data["plan_hash"]) + await migrate.test() + await migrate.verify() + + +asyncio.run(main()) +``` + +Each method maps directly to the local runtime: + +| Python method | Runtime command | Purpose | +|---|---|---| +| `scan()` | `sanka-migrate scan ... --json` | Inspect the source and write the scan artifact | +| `plan()` | `sanka-migrate plan ... --json` | Create a reviewable plan and plan hash | +| `apply()` | `sanka-migrate apply ... --json` | Generate only from the supplied reviewed plan hash | +| `test()` | `sanka-migrate test ... --json` | Prepare the generated target environment and run its tests | +| `verify()` | `sanka-migrate verify ... --json` | Verify integrity and configured behavior | + +Both adapters invoke an argument vector without a shell and never call Sanka's +hosted API. They forward only parameters you provide; defaults, validation, +framework detection, generated-target environments, and plan-hash safety remain +owned by `sanka-migrate`. Every call returns a typed `SankaMigrateResult` with +the `sanka-cli/v1` fields `data`, `artifacts`, `limitations`, and +`next_actions`. + +Failures raise `SankaMigrateError`. Its `command`, `exit_code`, `parsed_error`, +and `stderr` attributes distinguish a migration failure (exit `1`), invalid +usage (exit `2`), a missing executable, and an invalid protocol response. The +public classes and methods include docstrings for IDE hover and `help()`. + +See the [CLI execution model](https://github.com/sankaHQ/sanka/blob/main/docs/django-to-fastapi.md#cli-and-sdk-execution-model) +and [Sanka developer documentation](https://sanka.com/docs/developers/). + ## Regenerate ```bash diff --git a/handwritten/sanka_sdk/migrate.py b/handwritten/sanka_sdk/migrate.py new file mode 100644 index 0000000..49cebe3 --- /dev/null +++ b/handwritten/sanka_sdk/migrate.py @@ -0,0 +1,940 @@ +"""Local Sanka migration commands for Python applications. + +``SankaMigrate`` and ``AsyncSankaMigrate`` expose the same generic lifecycle as +the ``sanka-migrate`` CLI: ``scan -> plan -> apply -> test -> verify``. Both run +the separately installed CLI in non-interactive JSON mode; neither calls +Sanka's hosted API. +""" + +from __future__ import annotations + +import asyncio +import json +import os +import subprocess +from dataclasses import dataclass +from typing import Any, Dict, Generic, List, Literal, Mapping, Optional, Sequence, Tuple, TypedDict, TypeVar, Union + +PathValue = Union[str, "os.PathLike[str]"] +CLI_SCHEMA_VERSION = "sanka-cli/v1" + +__all__ = [ + "ApplyData", + "AsyncSankaMigrate", + "PlanData", + "SankaMigrate", + "SankaMigrateError", + "SankaMigrateResult", + "ScanData", + "TestData", + "VerifyData", +] + +TData = TypeVar("TData") + + +class ScanData(TypedDict, total=False): + """Core semantic scan fields; additional CLI fields remain available.""" + + scan_hash: str + + +class PlanData(TypedDict): + """Core plan fields required by the next lifecycle command.""" + + plan_hash: str + + +class ApplyData(TypedDict): + """Core apply fields identifying the reviewed plan that was written.""" + + plan_hash: str + + +class TestData(TypedDict): + """Core generated-target test verdict.""" + + ok: bool + + +class VerifyData(TypedDict): + """Core migration verification verdict.""" + + ok: bool + + +@dataclass(frozen=True) +class SankaMigrateResult(Generic[TData]): + """A successful ``sanka-cli/v1`` command result. + + Attributes: + schema_version: Version of the CLI-to-SDK protocol. + command: Generic command that produced this result. + outcome: CLI verdict, normally ``"success"``. + migration_state: Lifecycle state after the command completed. + data: Command-specific machine-readable data. + artifacts: Files or directories written by the command. + limitations: Scope limits or known migration gaps. + next_actions: Deterministic suggested follow-up commands. + """ + + schema_version: str + command: str + outcome: str + migration_state: str + data: TData + artifacts: List[str] + limitations: List[str] + next_actions: List[str] + + +class SankaMigrateError(RuntimeError): + """A local migration command or protocol failure. + + Attributes: + command: Generic command that failed. + exit_code: Process exit code, or ``None`` when the CLI could not start. + parsed_error: Structured CLI error from ``data.error``, when available. + stderr: Diagnostic text written by the CLI. + """ + + def __init__( + self, + message: str, + *, + command: str, + exit_code: Optional[int] = None, + parsed_error: Optional[Dict[str, Any]] = None, + stderr: str = "", + ) -> None: + super().__init__(message) + self.command = command + self.exit_code = exit_code + self.parsed_error = parsed_error + self.stderr = stderr + + +class SankaMigrate: + """Run local Sanka migration commands without a Sanka API token. + + Args: + cwd: Working directory used by ``sanka-migrate``. Relative command + paths and default artifacts resolve from this directory. + executable: CLI executable name or path. Install it separately with + ``uv tool install sanka-migrate``. + env: Environment variables merged over the current process environment. + + The adapter is non-interactive and always requests one ``sanka-cli/v1`` + JSON document. Framework detection, defaults, validation, and migration + execution remain owned by the CLI. + """ + + def __init__( + self, + *, + cwd: Optional[PathValue] = None, + executable: PathValue = "sanka-migrate", + env: Optional[Mapping[str, str]] = None, + ) -> None: + executable_value = os.fspath(executable) + if not executable_value.strip(): + raise ValueError("executable must not be empty") + self.cwd = os.fspath(cwd) if cwd is not None else None + self.executable = executable_value + self.env = dict(env or {}) + + def scan( + self, + *, + root: Optional[PathValue] = None, + settings: Optional[str] = None, + artifact_dir: Optional[PathValue] = None, + ) -> SankaMigrateResult[ScanData]: + """Inspect a source application with ``sanka-migrate scan``. + + Args: + root: Source repository root. Omit it to use ``cwd``. + settings: Explicit Django settings module; otherwise the CLI detects it. + artifact_dir: Directory for the semantic scan artifact. + + Returns: + The scan result, discovered application data, risks, and artifact paths. + + Raises: + SankaMigrateError: If scanning fails or the CLI violates its JSON protocol. + + Side effects: + Reads the source and writes only the scan artifact. + + Example: + ``scan = SankaMigrate(cwd="./app").scan()`` + """ + + return self._run("scan", _scan_args(root, settings, artifact_dir)) + + def plan( + self, + *, + root: Optional[PathValue] = None, + file: Optional[PathValue] = None, + state: Optional[PathValue] = None, + to: Optional[Literal["fastapi"]] = None, + strategy: Optional[Literal["native", "compatibility"]] = None, + artifact_dir: Optional[PathValue] = None, + output: Optional[PathValue] = None, + generation: Optional[Literal["full", "update", "minimal"]] = None, + package_manager: Optional[Literal["uv", "pip"]] = None, + orm: Optional[Literal["tortoise", "sqlalchemy", "psycopg"]] = None, + ) -> SankaMigrateResult[PlanData]: + """Create a reviewable, hash-bound plan with ``sanka-migrate plan``. + + Args: + root: Source repository root. Omit it to use ``cwd``. + file: Migration spec passed as ``--file``. + state: Run-state SQLite file passed as ``--state``. + to: Target framework, currently ``"fastapi"`` for application migration. + strategy: Runtime strategy, currently ``"native"`` or ``"compatibility"``. + artifact_dir: Directory containing scan and plan artifacts. + output: Planned generated target directory. + generation: Generation mode: ``"full"``, ``"update"``, or ``"minimal"``. + package_manager: Generated environment manager: ``"uv"`` or ``"pip"``. + orm: ORM selected when the scan detects database-backed routes. + + Returns: + The plan result. Use ``result.data["plan_hash"]`` for ``apply()``. + + Raises: + SankaMigrateError: If planning fails or required non-interactive choices + are missing. + + Side effects: + Writes plan and run-state artifacts but does not modify the target. + + Example: + ``plan = migrate.plan(to="fastapi", generation="full", output="./api")`` + """ + + return self._run( + "plan", + _plan_args( + root, + file, + state, + to, + strategy, + artifact_dir, + output, + generation, + package_manager, + orm, + ), + ) + + def apply( + self, + *, + plan_hash: str, + root: Optional[PathValue] = None, + file: Optional[PathValue] = None, + state: Optional[PathValue] = None, + to: Optional[Literal["fastapi"]] = None, + artifact_dir: Optional[PathValue] = None, + output: Optional[PathValue] = None, + force: bool = False, + orm: Optional[Literal["tortoise", "sqlalchemy", "psycopg"]] = None, + min_readiness: Optional[float] = None, + gap_report_only: bool = False, + bench_candidate: Optional[PathValue] = None, + ) -> SankaMigrateResult[ApplyData]: + """Apply exactly one reviewed plan with ``sanka-migrate apply``. + + Args: + plan_hash: Non-empty hash returned by ``plan()``; writes are bound to it. + root: Source repository root passed as ``--root``. + file: Migration spec passed as ``--file``. + state: Run-state SQLite file passed as ``--state``. + to: Target framework selector. + artifact_dir: Directory containing the reviewed plan. + output: Generated target directory reviewed by the plan. + force: Replace conflicting generated files only when explicitly true. + orm: Assert the reviewed ORM without changing it. + min_readiness: Minimum native readiness percentage from 0 through 100. + gap_report_only: Write a gap report instead of generating an application. + bench_candidate: Also write a Migration Bench candidate here. + + Returns: + The apply result and paths written from the reviewed plan. + + Raises: + ValueError: If ``plan_hash`` is empty. + SankaMigrateError: If the hash, target safety checks, or generation fail. + + Side effects: + Mutates only the target and artifacts authorized by the reviewed plan. + + Example: + ``applied = migrate.apply(plan_hash=plan.data["plan_hash"])`` + """ + + return self._run( + "apply", + _apply_args( + plan_hash, + root, + file, + state, + to, + artifact_dir, + output, + force, + orm, + min_readiness, + gap_report_only, + bench_candidate, + ), + ) + + def test( + self, + *, + root: Optional[PathValue] = None, + file: Optional[PathValue] = None, + state: Optional[PathValue] = None, + to: Optional[Literal["fastapi"]] = None, + artifact_dir: Optional[PathValue] = None, + output: Optional[PathValue] = None, + ) -> SankaMigrateResult[TestData]: + """Run generated-target tests with ``sanka-migrate test``. + + Args: + root: Source repository root. Omit it to use ``cwd``. + file: Migration spec passed as ``--file``. + state: Run-state SQLite file passed as ``--state``. + to: Target framework selector. + artifact_dir: Directory containing the applied plan. + output: Generated target directory. + + Returns: + Test verdict, target interpreter, dependencies, and generated test artifact. + + Raises: + SankaMigrateError: If environment setup, dependency installation, or tests fail. + + Side effects: + Prepares and uses the generated target's own environment, then writes + generated-target tests. It never borrows SDK or Sanka dependencies. + + Example: + ``tested = SankaMigrate(cwd="./source").test()`` + """ + + return self._run("test", _test_args(root, file, state, to, artifact_dir, output)) + + def verify( + self, + *, + root: Optional[PathValue] = None, + file: Optional[PathValue] = None, + state: Optional[PathValue] = None, + to: Optional[Literal["fastapi"]] = None, + artifact_dir: Optional[PathValue] = None, + output: Optional[PathValue] = None, + cases: Optional[PathValue] = None, + no_http: bool = False, + ) -> SankaMigrateResult[VerifyData]: + """Verify the selected migration with ``sanka-migrate verify``. + + Args: + root: Source repository root. Omit it to use ``cwd``. + file: Migration spec passed as ``--file``. + state: Run-state SQLite file passed as ``--state``. + to: Target framework selector. + artifact_dir: Directory containing the applied plan. + output: Generated target directory. + cases: JSON file containing additional read-only HTTP verification cases. + no_http: Skip HTTP probes when structural verification is sufficient. + + Returns: + Verification verdict, checked scope, artifacts, and limitations. + + Raises: + SankaMigrateError: If verification fails or its evidence is malformed. + + Side effects: + Performs structural checks and configured read-only probes using the + generated target environment. + + Example: + ``verified = SankaMigrate(cwd="./source").verify(no_http=True)`` + """ + + return self._run( + "verify", + _verify_args(root, file, state, to, artifact_dir, output, cases, no_http), + ) + + def _run(self, command: str, args: Sequence[str]) -> SankaMigrateResult[Any]: + argv, environment = self._prepare(command, args) + try: + completed = subprocess.run( + argv, + cwd=self.cwd, + env=environment, + capture_output=True, + text=True, + check=False, + ) + except FileNotFoundError as error: + raise _missing_executable(command) from error + except OSError as error: + raise SankaMigrateError( + "could not execute sanka-migrate: {}".format(error), + command=command, + ) from error + + return _finish_result( + completed.stdout, + command=command, + exit_code=completed.returncode, + stderr=completed.stderr, + ) + + def _prepare(self, command: str, args: Sequence[str]) -> Tuple[List[str], Dict[str, str]]: + if self.cwd is not None and not os.path.isdir(self.cwd): + raise SankaMigrateError( + "sanka-migrate working directory was not found: {}".format(self.cwd), + command=command, + ) + environment = os.environ.copy() + environment.update(self.env) + return [self.executable, *args, "--json"], environment + + +class AsyncSankaMigrate(SankaMigrate): + """Run local Sanka migration commands without blocking the event loop. + + Args: + cwd: Working directory used by ``sanka-migrate``. + executable: Separately installed CLI executable name or path. + env: Environment variables merged over the current process environment. + + The async adapter has the same options, results, and errors as + :class:`SankaMigrate`. Cancelling a command kills and reaps its child process. + """ + + async def scan( + self, + *, + root: Optional[PathValue] = None, + settings: Optional[str] = None, + artifact_dir: Optional[PathValue] = None, + ) -> SankaMigrateResult[ScanData]: + """Asynchronously inspect a source with ``sanka-migrate scan``. + + Args: + root: Source repository root. Omit it to use ``cwd``. + settings: Explicit Django settings module. + artifact_dir: Directory for the semantic scan artifact. + + Returns: + The scan result, discovered application data, risks, and artifacts. + + Raises: + SankaMigrateError: If scanning or the CLI protocol fails. + + Side effects: + Reads the source and writes only the scan artifact. + + Example: + ``scan = await AsyncSankaMigrate(cwd="./app").scan()`` + """ + + return await self._run_async("scan", _scan_args(root, settings, artifact_dir)) + + async def plan( + self, + *, + root: Optional[PathValue] = None, + file: Optional[PathValue] = None, + state: Optional[PathValue] = None, + to: Optional[Literal["fastapi"]] = None, + strategy: Optional[Literal["native", "compatibility"]] = None, + artifact_dir: Optional[PathValue] = None, + output: Optional[PathValue] = None, + generation: Optional[Literal["full", "update", "minimal"]] = None, + package_manager: Optional[Literal["uv", "pip"]] = None, + orm: Optional[Literal["tortoise", "sqlalchemy", "psycopg"]] = None, + ) -> SankaMigrateResult[PlanData]: + """Asynchronously create a hash-bound plan with ``sanka-migrate plan``. + + Args: + root: Source repository root. Omit it to use ``cwd``. + file: Migration spec passed as ``--file``. + state: Run-state SQLite file passed as ``--state``. + to: Target framework, currently ``"fastapi"``. + strategy: ``"native"`` or ``"compatibility"``. + artifact_dir: Directory containing scan and plan artifacts. + output: Planned generated target directory. + generation: ``"full"``, ``"update"``, or ``"minimal"``. + package_manager: ``"uv"`` or ``"pip"``. + orm: ORM selected for database-backed routes. + + Returns: + The plan result whose ``data["plan_hash"]`` is required by ``apply``. + + Raises: + SankaMigrateError: If planning or required choices fail. + + Side effects: + Writes plan and run-state artifacts without modifying the target. + + Example: + ``plan = await migrate.plan(to="fastapi", generation="full")`` + """ + + return await self._run_async( + "plan", + _plan_args( + root, + file, + state, + to, + strategy, + artifact_dir, + output, + generation, + package_manager, + orm, + ), + ) + + async def apply( + self, + *, + plan_hash: str, + root: Optional[PathValue] = None, + file: Optional[PathValue] = None, + state: Optional[PathValue] = None, + to: Optional[Literal["fastapi"]] = None, + artifact_dir: Optional[PathValue] = None, + output: Optional[PathValue] = None, + force: bool = False, + orm: Optional[Literal["tortoise", "sqlalchemy", "psycopg"]] = None, + min_readiness: Optional[float] = None, + gap_report_only: bool = False, + bench_candidate: Optional[PathValue] = None, + ) -> SankaMigrateResult[ApplyData]: + """Asynchronously apply one reviewed plan with ``sanka-migrate apply``. + + Args: + plan_hash: Non-empty hash returned by ``plan``. + root: Source repository root passed as ``--root``. + file: Migration spec passed as ``--file``. + state: Run-state SQLite file passed as ``--state``. + to: Target framework selector. + artifact_dir: Directory containing the reviewed plan. + output: Generated target directory reviewed by the plan. + force: Replace conflicting generated files only when true. + orm: Assert the reviewed ORM without changing it. + min_readiness: Minimum native readiness percentage. + gap_report_only: Write a gap report instead of an application. + bench_candidate: Also write a Migration Bench candidate here. + + Returns: + The apply result and paths written from the reviewed plan. + + Raises: + ValueError: If ``plan_hash`` is empty. + SankaMigrateError: If plan safety or generation fails. + + Side effects: + Mutates only the target and artifacts authorized by the plan. + + Example: + ``applied = await migrate.apply(plan_hash=plan.data["plan_hash"])`` + """ + + return await self._run_async( + "apply", + _apply_args( + plan_hash, + root, + file, + state, + to, + artifact_dir, + output, + force, + orm, + min_readiness, + gap_report_only, + bench_candidate, + ), + ) + + async def test( + self, + *, + root: Optional[PathValue] = None, + file: Optional[PathValue] = None, + state: Optional[PathValue] = None, + to: Optional[Literal["fastapi"]] = None, + artifact_dir: Optional[PathValue] = None, + output: Optional[PathValue] = None, + ) -> SankaMigrateResult[TestData]: + """Asynchronously run generated tests with ``sanka-migrate test``. + + Args: + root: Source repository root. Omit it to use ``cwd``. + file: Migration spec passed as ``--file``. + state: Run-state SQLite file passed as ``--state``. + to: Target framework selector. + artifact_dir: Directory containing the applied plan. + output: Generated target directory. + + Returns: + Test verdict, target interpreter, dependencies, and test artifact. + + Raises: + SankaMigrateError: If environment setup or tests fail. + + Side effects: + Uses the generated target environment and writes generated tests. + + Example: + ``tested = await AsyncSankaMigrate(cwd="./source").test()`` + """ + + return await self._run_async("test", _test_args(root, file, state, to, artifact_dir, output)) + + async def verify( + self, + *, + root: Optional[PathValue] = None, + file: Optional[PathValue] = None, + state: Optional[PathValue] = None, + to: Optional[Literal["fastapi"]] = None, + artifact_dir: Optional[PathValue] = None, + output: Optional[PathValue] = None, + cases: Optional[PathValue] = None, + no_http: bool = False, + ) -> SankaMigrateResult[VerifyData]: + """Asynchronously verify with ``sanka-migrate verify``. + + Args: + root: Source repository root. Omit it to use ``cwd``. + file: Migration spec passed as ``--file``. + state: Run-state SQLite file passed as ``--state``. + to: Target framework selector. + artifact_dir: Directory containing the applied plan. + output: Generated target directory. + cases: JSON file with additional read-only HTTP cases. + no_http: Skip HTTP probes when structural checks are sufficient. + + Returns: + Verification verdict, checked scope, artifacts, and limitations. + + Raises: + SankaMigrateError: If verification or its evidence fails. + + Side effects: + Performs checks and read-only probes in the target environment. + + Example: + ``verified = await migrate.verify(no_http=True)`` + """ + + return await self._run_async( + "verify", + _verify_args(root, file, state, to, artifact_dir, output, cases, no_http), + ) + + async def _run_async(self, command: str, args: Sequence[str]) -> SankaMigrateResult[Any]: + argv, environment = self._prepare(command, args) + try: + process = await asyncio.create_subprocess_exec( + *argv, + cwd=self.cwd, + env=environment, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + except FileNotFoundError as error: + raise _missing_executable(command) from error + except OSError as error: + raise SankaMigrateError( + "could not execute sanka-migrate: {}".format(error), + command=command, + ) from error + + try: + stdout_bytes, stderr_bytes = await process.communicate() + except asyncio.CancelledError: + if process.returncode is None: + try: + process.kill() + except ProcessLookupError: + pass + await process.communicate() + raise + + exit_code = process.returncode + if exit_code is None: + raise SankaMigrateError("sanka-migrate {} did not exit".format(command), command=command) + return _finish_result( + stdout_bytes.decode("utf-8", errors="replace"), + command=command, + exit_code=exit_code, + stderr=stderr_bytes.decode("utf-8", errors="replace"), + ) + + +def _scan_args(root: Optional[PathValue], settings: Optional[str], artifact_dir: Optional[PathValue]) -> List[str]: + args = ["scan"] + _positional(args, root) + _option(args, "--settings", settings) + _option(args, "--artifact-dir", artifact_dir) + return args + + +def _plan_args( + root: Optional[PathValue], + file: Optional[PathValue], + state: Optional[PathValue], + to: Optional[str], + strategy: Optional[str], + artifact_dir: Optional[PathValue], + output: Optional[PathValue], + generation: Optional[str], + package_manager: Optional[str], + orm: Optional[str], +) -> List[str]: + args = ["plan"] + _positional(args, root) + for flag, value in ( + ("--file", file), + ("--state", state), + ("--to", to), + ("--strategy", strategy), + ("--artifact-dir", artifact_dir), + ("--output", output), + ("--generation", generation), + ("--package-manager", package_manager), + ("--orm", orm), + ): + _option(args, flag, value) + return args + + +def _apply_args( + plan_hash: str, + root: Optional[PathValue], + file: Optional[PathValue], + state: Optional[PathValue], + to: Optional[str], + artifact_dir: Optional[PathValue], + output: Optional[PathValue], + force: bool, + orm: Optional[str], + min_readiness: Optional[float], + gap_report_only: bool, + bench_candidate: Optional[PathValue], +) -> List[str]: + if not plan_hash.strip(): + raise ValueError("plan_hash must not be empty") + args = ["apply", "--plan-hash", plan_hash] + for flag, value in ( + ("--root", root), + ("--file", file), + ("--state", state), + ("--to", to), + ("--artifact-dir", artifact_dir), + ("--output", output), + ): + _option(args, flag, value) + _flag(args, "--force", force) + _option(args, "--orm", orm) + _option(args, "--min-readiness", min_readiness) + _flag(args, "--gap-report-only", gap_report_only) + _option(args, "--bench-candidate", bench_candidate) + return args + + +def _test_args( + root: Optional[PathValue], + file: Optional[PathValue], + state: Optional[PathValue], + to: Optional[str], + artifact_dir: Optional[PathValue], + output: Optional[PathValue], +) -> List[str]: + args = ["test"] + _positional(args, root) + for flag, value in ( + ("--file", file), + ("--state", state), + ("--to", to), + ("--artifact-dir", artifact_dir), + ("--output", output), + ): + _option(args, flag, value) + return args + + +def _verify_args( + root: Optional[PathValue], + file: Optional[PathValue], + state: Optional[PathValue], + to: Optional[str], + artifact_dir: Optional[PathValue], + output: Optional[PathValue], + cases: Optional[PathValue], + no_http: bool, +) -> List[str]: + args = ["verify"] + _positional(args, root) + for flag, value in ( + ("--file", file), + ("--state", state), + ("--to", to), + ("--artifact-dir", artifact_dir), + ("--output", output), + ("--cases", cases), + ): + _option(args, flag, value) + _flag(args, "--no-http", no_http) + return args + + +def _positional(args: List[str], value: Optional[PathValue]) -> None: + if value is not None: + args.append(os.fspath(value)) + + +def _option(args: List[str], flag: str, value: Any) -> None: + if value is not None: + args.extend((flag, os.fspath(value) if isinstance(value, os.PathLike) else str(value))) + + +def _flag(args: List[str], flag: str, enabled: bool) -> None: + if enabled: + args.append(flag) + + +def _finish_result(stdout: str, *, command: str, exit_code: int, stderr: str) -> SankaMigrateResult[Any]: + result = _decode_result( + stdout, + command=command, + exit_code=exit_code, + stderr=stderr, + ) + if exit_code != 0 or result.outcome == "error": + parsed_error = result.data.get("error") + error_data = parsed_error if isinstance(parsed_error, dict) else None + message = ( + str(error_data.get("message")) + if error_data and error_data.get("message") + else "sanka-migrate {} failed with exit code {}".format(command, exit_code) + ) + raise SankaMigrateError( + message, + command=command, + exit_code=exit_code, + parsed_error=error_data, + stderr=stderr, + ) + return result + + +def _missing_executable(command: str) -> SankaMigrateError: + return SankaMigrateError( + "sanka-migrate executable was not found; install it with " + "`uv tool install sanka-migrate` or pass executable=...", + command=command, + ) + + +def _decode_result( + stdout: str, + *, + command: str, + exit_code: int, + stderr: str, +) -> SankaMigrateResult[Dict[str, Any]]: + try: + payload = json.loads(stdout) + except (json.JSONDecodeError, TypeError) as error: + raise SankaMigrateError( + "sanka-migrate {} did not return one valid JSON document".format(command), + command=command, + exit_code=exit_code, + stderr=stderr, + ) from error + if not isinstance(payload, dict): + raise SankaMigrateError( + "sanka-migrate {} returned a non-object JSON document".format(command), + command=command, + exit_code=exit_code, + stderr=stderr, + ) + + schema_version = payload.get("schema_version") + payload_command = payload.get("command") + if schema_version != CLI_SCHEMA_VERSION: + raise SankaMigrateError( + "unsupported sanka-migrate protocol: {!r}".format(schema_version), + command=command, + exit_code=exit_code, + stderr=stderr, + ) + if payload_command != command: + raise SankaMigrateError( + "sanka-migrate returned command {!r}, expected {!r}".format(payload_command, command), + command=command, + exit_code=exit_code, + stderr=stderr, + ) + + data = payload.get("data") + artifacts = payload.get("artifacts") + limitations = payload.get("limitations") + next_actions = payload.get("next_actions") + if not isinstance(data, dict): + raise _invalid_field(command, exit_code, stderr, "data", "an object") + for name, value in ( + ("artifacts", artifacts), + ("limitations", limitations), + ("next_actions", next_actions), + ): + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise _invalid_field(command, exit_code, stderr, name, "a string array") + if not isinstance(payload.get("outcome"), str): + raise _invalid_field(command, exit_code, stderr, "outcome", "a string") + if not isinstance(payload.get("migration_state"), str): + raise _invalid_field(command, exit_code, stderr, "migration_state", "a string") + + return SankaMigrateResult( + schema_version=schema_version, + command=payload_command, + outcome=payload["outcome"], + migration_state=payload["migration_state"], + data=data, + artifacts=artifacts, + limitations=limitations, + next_actions=next_actions, + ) + + +def _invalid_field( + command: str, + exit_code: int, + stderr: str, + name: str, + expected: str, +) -> SankaMigrateError: + return SankaMigrateError( + "invalid sanka-cli/v1 field {!r}; expected {}".format(name, expected), + command=command, + exit_code=exit_code, + stderr=stderr, + ) diff --git a/scripts/generate_sdk.sh b/scripts/generate_sdk.sh index b357273..969a643 100755 --- a/scripts/generate_sdk.sh +++ b/scripts/generate_sdk.sh @@ -105,6 +105,7 @@ docker run --rm \ /workspace/config.manual.json >/dev/null cp -R "$GENERATOR_OUTPUT_DIR"/. "$OUTPUT_DIR"/ +cp "$ROOT/handwritten/sanka_sdk/migrate.py" "$OUTPUT_DIR/migrate.py" touch "$OUTPUT_DIR/py.typed" python3 -m compileall "$OUTPUT_DIR" >/dev/null diff --git a/src/sanka_sdk/migrate.py b/src/sanka_sdk/migrate.py new file mode 100644 index 0000000..49cebe3 --- /dev/null +++ b/src/sanka_sdk/migrate.py @@ -0,0 +1,940 @@ +"""Local Sanka migration commands for Python applications. + +``SankaMigrate`` and ``AsyncSankaMigrate`` expose the same generic lifecycle as +the ``sanka-migrate`` CLI: ``scan -> plan -> apply -> test -> verify``. Both run +the separately installed CLI in non-interactive JSON mode; neither calls +Sanka's hosted API. +""" + +from __future__ import annotations + +import asyncio +import json +import os +import subprocess +from dataclasses import dataclass +from typing import Any, Dict, Generic, List, Literal, Mapping, Optional, Sequence, Tuple, TypedDict, TypeVar, Union + +PathValue = Union[str, "os.PathLike[str]"] +CLI_SCHEMA_VERSION = "sanka-cli/v1" + +__all__ = [ + "ApplyData", + "AsyncSankaMigrate", + "PlanData", + "SankaMigrate", + "SankaMigrateError", + "SankaMigrateResult", + "ScanData", + "TestData", + "VerifyData", +] + +TData = TypeVar("TData") + + +class ScanData(TypedDict, total=False): + """Core semantic scan fields; additional CLI fields remain available.""" + + scan_hash: str + + +class PlanData(TypedDict): + """Core plan fields required by the next lifecycle command.""" + + plan_hash: str + + +class ApplyData(TypedDict): + """Core apply fields identifying the reviewed plan that was written.""" + + plan_hash: str + + +class TestData(TypedDict): + """Core generated-target test verdict.""" + + ok: bool + + +class VerifyData(TypedDict): + """Core migration verification verdict.""" + + ok: bool + + +@dataclass(frozen=True) +class SankaMigrateResult(Generic[TData]): + """A successful ``sanka-cli/v1`` command result. + + Attributes: + schema_version: Version of the CLI-to-SDK protocol. + command: Generic command that produced this result. + outcome: CLI verdict, normally ``"success"``. + migration_state: Lifecycle state after the command completed. + data: Command-specific machine-readable data. + artifacts: Files or directories written by the command. + limitations: Scope limits or known migration gaps. + next_actions: Deterministic suggested follow-up commands. + """ + + schema_version: str + command: str + outcome: str + migration_state: str + data: TData + artifacts: List[str] + limitations: List[str] + next_actions: List[str] + + +class SankaMigrateError(RuntimeError): + """A local migration command or protocol failure. + + Attributes: + command: Generic command that failed. + exit_code: Process exit code, or ``None`` when the CLI could not start. + parsed_error: Structured CLI error from ``data.error``, when available. + stderr: Diagnostic text written by the CLI. + """ + + def __init__( + self, + message: str, + *, + command: str, + exit_code: Optional[int] = None, + parsed_error: Optional[Dict[str, Any]] = None, + stderr: str = "", + ) -> None: + super().__init__(message) + self.command = command + self.exit_code = exit_code + self.parsed_error = parsed_error + self.stderr = stderr + + +class SankaMigrate: + """Run local Sanka migration commands without a Sanka API token. + + Args: + cwd: Working directory used by ``sanka-migrate``. Relative command + paths and default artifacts resolve from this directory. + executable: CLI executable name or path. Install it separately with + ``uv tool install sanka-migrate``. + env: Environment variables merged over the current process environment. + + The adapter is non-interactive and always requests one ``sanka-cli/v1`` + JSON document. Framework detection, defaults, validation, and migration + execution remain owned by the CLI. + """ + + def __init__( + self, + *, + cwd: Optional[PathValue] = None, + executable: PathValue = "sanka-migrate", + env: Optional[Mapping[str, str]] = None, + ) -> None: + executable_value = os.fspath(executable) + if not executable_value.strip(): + raise ValueError("executable must not be empty") + self.cwd = os.fspath(cwd) if cwd is not None else None + self.executable = executable_value + self.env = dict(env or {}) + + def scan( + self, + *, + root: Optional[PathValue] = None, + settings: Optional[str] = None, + artifact_dir: Optional[PathValue] = None, + ) -> SankaMigrateResult[ScanData]: + """Inspect a source application with ``sanka-migrate scan``. + + Args: + root: Source repository root. Omit it to use ``cwd``. + settings: Explicit Django settings module; otherwise the CLI detects it. + artifact_dir: Directory for the semantic scan artifact. + + Returns: + The scan result, discovered application data, risks, and artifact paths. + + Raises: + SankaMigrateError: If scanning fails or the CLI violates its JSON protocol. + + Side effects: + Reads the source and writes only the scan artifact. + + Example: + ``scan = SankaMigrate(cwd="./app").scan()`` + """ + + return self._run("scan", _scan_args(root, settings, artifact_dir)) + + def plan( + self, + *, + root: Optional[PathValue] = None, + file: Optional[PathValue] = None, + state: Optional[PathValue] = None, + to: Optional[Literal["fastapi"]] = None, + strategy: Optional[Literal["native", "compatibility"]] = None, + artifact_dir: Optional[PathValue] = None, + output: Optional[PathValue] = None, + generation: Optional[Literal["full", "update", "minimal"]] = None, + package_manager: Optional[Literal["uv", "pip"]] = None, + orm: Optional[Literal["tortoise", "sqlalchemy", "psycopg"]] = None, + ) -> SankaMigrateResult[PlanData]: + """Create a reviewable, hash-bound plan with ``sanka-migrate plan``. + + Args: + root: Source repository root. Omit it to use ``cwd``. + file: Migration spec passed as ``--file``. + state: Run-state SQLite file passed as ``--state``. + to: Target framework, currently ``"fastapi"`` for application migration. + strategy: Runtime strategy, currently ``"native"`` or ``"compatibility"``. + artifact_dir: Directory containing scan and plan artifacts. + output: Planned generated target directory. + generation: Generation mode: ``"full"``, ``"update"``, or ``"minimal"``. + package_manager: Generated environment manager: ``"uv"`` or ``"pip"``. + orm: ORM selected when the scan detects database-backed routes. + + Returns: + The plan result. Use ``result.data["plan_hash"]`` for ``apply()``. + + Raises: + SankaMigrateError: If planning fails or required non-interactive choices + are missing. + + Side effects: + Writes plan and run-state artifacts but does not modify the target. + + Example: + ``plan = migrate.plan(to="fastapi", generation="full", output="./api")`` + """ + + return self._run( + "plan", + _plan_args( + root, + file, + state, + to, + strategy, + artifact_dir, + output, + generation, + package_manager, + orm, + ), + ) + + def apply( + self, + *, + plan_hash: str, + root: Optional[PathValue] = None, + file: Optional[PathValue] = None, + state: Optional[PathValue] = None, + to: Optional[Literal["fastapi"]] = None, + artifact_dir: Optional[PathValue] = None, + output: Optional[PathValue] = None, + force: bool = False, + orm: Optional[Literal["tortoise", "sqlalchemy", "psycopg"]] = None, + min_readiness: Optional[float] = None, + gap_report_only: bool = False, + bench_candidate: Optional[PathValue] = None, + ) -> SankaMigrateResult[ApplyData]: + """Apply exactly one reviewed plan with ``sanka-migrate apply``. + + Args: + plan_hash: Non-empty hash returned by ``plan()``; writes are bound to it. + root: Source repository root passed as ``--root``. + file: Migration spec passed as ``--file``. + state: Run-state SQLite file passed as ``--state``. + to: Target framework selector. + artifact_dir: Directory containing the reviewed plan. + output: Generated target directory reviewed by the plan. + force: Replace conflicting generated files only when explicitly true. + orm: Assert the reviewed ORM without changing it. + min_readiness: Minimum native readiness percentage from 0 through 100. + gap_report_only: Write a gap report instead of generating an application. + bench_candidate: Also write a Migration Bench candidate here. + + Returns: + The apply result and paths written from the reviewed plan. + + Raises: + ValueError: If ``plan_hash`` is empty. + SankaMigrateError: If the hash, target safety checks, or generation fail. + + Side effects: + Mutates only the target and artifacts authorized by the reviewed plan. + + Example: + ``applied = migrate.apply(plan_hash=plan.data["plan_hash"])`` + """ + + return self._run( + "apply", + _apply_args( + plan_hash, + root, + file, + state, + to, + artifact_dir, + output, + force, + orm, + min_readiness, + gap_report_only, + bench_candidate, + ), + ) + + def test( + self, + *, + root: Optional[PathValue] = None, + file: Optional[PathValue] = None, + state: Optional[PathValue] = None, + to: Optional[Literal["fastapi"]] = None, + artifact_dir: Optional[PathValue] = None, + output: Optional[PathValue] = None, + ) -> SankaMigrateResult[TestData]: + """Run generated-target tests with ``sanka-migrate test``. + + Args: + root: Source repository root. Omit it to use ``cwd``. + file: Migration spec passed as ``--file``. + state: Run-state SQLite file passed as ``--state``. + to: Target framework selector. + artifact_dir: Directory containing the applied plan. + output: Generated target directory. + + Returns: + Test verdict, target interpreter, dependencies, and generated test artifact. + + Raises: + SankaMigrateError: If environment setup, dependency installation, or tests fail. + + Side effects: + Prepares and uses the generated target's own environment, then writes + generated-target tests. It never borrows SDK or Sanka dependencies. + + Example: + ``tested = SankaMigrate(cwd="./source").test()`` + """ + + return self._run("test", _test_args(root, file, state, to, artifact_dir, output)) + + def verify( + self, + *, + root: Optional[PathValue] = None, + file: Optional[PathValue] = None, + state: Optional[PathValue] = None, + to: Optional[Literal["fastapi"]] = None, + artifact_dir: Optional[PathValue] = None, + output: Optional[PathValue] = None, + cases: Optional[PathValue] = None, + no_http: bool = False, + ) -> SankaMigrateResult[VerifyData]: + """Verify the selected migration with ``sanka-migrate verify``. + + Args: + root: Source repository root. Omit it to use ``cwd``. + file: Migration spec passed as ``--file``. + state: Run-state SQLite file passed as ``--state``. + to: Target framework selector. + artifact_dir: Directory containing the applied plan. + output: Generated target directory. + cases: JSON file containing additional read-only HTTP verification cases. + no_http: Skip HTTP probes when structural verification is sufficient. + + Returns: + Verification verdict, checked scope, artifacts, and limitations. + + Raises: + SankaMigrateError: If verification fails or its evidence is malformed. + + Side effects: + Performs structural checks and configured read-only probes using the + generated target environment. + + Example: + ``verified = SankaMigrate(cwd="./source").verify(no_http=True)`` + """ + + return self._run( + "verify", + _verify_args(root, file, state, to, artifact_dir, output, cases, no_http), + ) + + def _run(self, command: str, args: Sequence[str]) -> SankaMigrateResult[Any]: + argv, environment = self._prepare(command, args) + try: + completed = subprocess.run( + argv, + cwd=self.cwd, + env=environment, + capture_output=True, + text=True, + check=False, + ) + except FileNotFoundError as error: + raise _missing_executable(command) from error + except OSError as error: + raise SankaMigrateError( + "could not execute sanka-migrate: {}".format(error), + command=command, + ) from error + + return _finish_result( + completed.stdout, + command=command, + exit_code=completed.returncode, + stderr=completed.stderr, + ) + + def _prepare(self, command: str, args: Sequence[str]) -> Tuple[List[str], Dict[str, str]]: + if self.cwd is not None and not os.path.isdir(self.cwd): + raise SankaMigrateError( + "sanka-migrate working directory was not found: {}".format(self.cwd), + command=command, + ) + environment = os.environ.copy() + environment.update(self.env) + return [self.executable, *args, "--json"], environment + + +class AsyncSankaMigrate(SankaMigrate): + """Run local Sanka migration commands without blocking the event loop. + + Args: + cwd: Working directory used by ``sanka-migrate``. + executable: Separately installed CLI executable name or path. + env: Environment variables merged over the current process environment. + + The async adapter has the same options, results, and errors as + :class:`SankaMigrate`. Cancelling a command kills and reaps its child process. + """ + + async def scan( + self, + *, + root: Optional[PathValue] = None, + settings: Optional[str] = None, + artifact_dir: Optional[PathValue] = None, + ) -> SankaMigrateResult[ScanData]: + """Asynchronously inspect a source with ``sanka-migrate scan``. + + Args: + root: Source repository root. Omit it to use ``cwd``. + settings: Explicit Django settings module. + artifact_dir: Directory for the semantic scan artifact. + + Returns: + The scan result, discovered application data, risks, and artifacts. + + Raises: + SankaMigrateError: If scanning or the CLI protocol fails. + + Side effects: + Reads the source and writes only the scan artifact. + + Example: + ``scan = await AsyncSankaMigrate(cwd="./app").scan()`` + """ + + return await self._run_async("scan", _scan_args(root, settings, artifact_dir)) + + async def plan( + self, + *, + root: Optional[PathValue] = None, + file: Optional[PathValue] = None, + state: Optional[PathValue] = None, + to: Optional[Literal["fastapi"]] = None, + strategy: Optional[Literal["native", "compatibility"]] = None, + artifact_dir: Optional[PathValue] = None, + output: Optional[PathValue] = None, + generation: Optional[Literal["full", "update", "minimal"]] = None, + package_manager: Optional[Literal["uv", "pip"]] = None, + orm: Optional[Literal["tortoise", "sqlalchemy", "psycopg"]] = None, + ) -> SankaMigrateResult[PlanData]: + """Asynchronously create a hash-bound plan with ``sanka-migrate plan``. + + Args: + root: Source repository root. Omit it to use ``cwd``. + file: Migration spec passed as ``--file``. + state: Run-state SQLite file passed as ``--state``. + to: Target framework, currently ``"fastapi"``. + strategy: ``"native"`` or ``"compatibility"``. + artifact_dir: Directory containing scan and plan artifacts. + output: Planned generated target directory. + generation: ``"full"``, ``"update"``, or ``"minimal"``. + package_manager: ``"uv"`` or ``"pip"``. + orm: ORM selected for database-backed routes. + + Returns: + The plan result whose ``data["plan_hash"]`` is required by ``apply``. + + Raises: + SankaMigrateError: If planning or required choices fail. + + Side effects: + Writes plan and run-state artifacts without modifying the target. + + Example: + ``plan = await migrate.plan(to="fastapi", generation="full")`` + """ + + return await self._run_async( + "plan", + _plan_args( + root, + file, + state, + to, + strategy, + artifact_dir, + output, + generation, + package_manager, + orm, + ), + ) + + async def apply( + self, + *, + plan_hash: str, + root: Optional[PathValue] = None, + file: Optional[PathValue] = None, + state: Optional[PathValue] = None, + to: Optional[Literal["fastapi"]] = None, + artifact_dir: Optional[PathValue] = None, + output: Optional[PathValue] = None, + force: bool = False, + orm: Optional[Literal["tortoise", "sqlalchemy", "psycopg"]] = None, + min_readiness: Optional[float] = None, + gap_report_only: bool = False, + bench_candidate: Optional[PathValue] = None, + ) -> SankaMigrateResult[ApplyData]: + """Asynchronously apply one reviewed plan with ``sanka-migrate apply``. + + Args: + plan_hash: Non-empty hash returned by ``plan``. + root: Source repository root passed as ``--root``. + file: Migration spec passed as ``--file``. + state: Run-state SQLite file passed as ``--state``. + to: Target framework selector. + artifact_dir: Directory containing the reviewed plan. + output: Generated target directory reviewed by the plan. + force: Replace conflicting generated files only when true. + orm: Assert the reviewed ORM without changing it. + min_readiness: Minimum native readiness percentage. + gap_report_only: Write a gap report instead of an application. + bench_candidate: Also write a Migration Bench candidate here. + + Returns: + The apply result and paths written from the reviewed plan. + + Raises: + ValueError: If ``plan_hash`` is empty. + SankaMigrateError: If plan safety or generation fails. + + Side effects: + Mutates only the target and artifacts authorized by the plan. + + Example: + ``applied = await migrate.apply(plan_hash=plan.data["plan_hash"])`` + """ + + return await self._run_async( + "apply", + _apply_args( + plan_hash, + root, + file, + state, + to, + artifact_dir, + output, + force, + orm, + min_readiness, + gap_report_only, + bench_candidate, + ), + ) + + async def test( + self, + *, + root: Optional[PathValue] = None, + file: Optional[PathValue] = None, + state: Optional[PathValue] = None, + to: Optional[Literal["fastapi"]] = None, + artifact_dir: Optional[PathValue] = None, + output: Optional[PathValue] = None, + ) -> SankaMigrateResult[TestData]: + """Asynchronously run generated tests with ``sanka-migrate test``. + + Args: + root: Source repository root. Omit it to use ``cwd``. + file: Migration spec passed as ``--file``. + state: Run-state SQLite file passed as ``--state``. + to: Target framework selector. + artifact_dir: Directory containing the applied plan. + output: Generated target directory. + + Returns: + Test verdict, target interpreter, dependencies, and test artifact. + + Raises: + SankaMigrateError: If environment setup or tests fail. + + Side effects: + Uses the generated target environment and writes generated tests. + + Example: + ``tested = await AsyncSankaMigrate(cwd="./source").test()`` + """ + + return await self._run_async("test", _test_args(root, file, state, to, artifact_dir, output)) + + async def verify( + self, + *, + root: Optional[PathValue] = None, + file: Optional[PathValue] = None, + state: Optional[PathValue] = None, + to: Optional[Literal["fastapi"]] = None, + artifact_dir: Optional[PathValue] = None, + output: Optional[PathValue] = None, + cases: Optional[PathValue] = None, + no_http: bool = False, + ) -> SankaMigrateResult[VerifyData]: + """Asynchronously verify with ``sanka-migrate verify``. + + Args: + root: Source repository root. Omit it to use ``cwd``. + file: Migration spec passed as ``--file``. + state: Run-state SQLite file passed as ``--state``. + to: Target framework selector. + artifact_dir: Directory containing the applied plan. + output: Generated target directory. + cases: JSON file with additional read-only HTTP cases. + no_http: Skip HTTP probes when structural checks are sufficient. + + Returns: + Verification verdict, checked scope, artifacts, and limitations. + + Raises: + SankaMigrateError: If verification or its evidence fails. + + Side effects: + Performs checks and read-only probes in the target environment. + + Example: + ``verified = await migrate.verify(no_http=True)`` + """ + + return await self._run_async( + "verify", + _verify_args(root, file, state, to, artifact_dir, output, cases, no_http), + ) + + async def _run_async(self, command: str, args: Sequence[str]) -> SankaMigrateResult[Any]: + argv, environment = self._prepare(command, args) + try: + process = await asyncio.create_subprocess_exec( + *argv, + cwd=self.cwd, + env=environment, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + except FileNotFoundError as error: + raise _missing_executable(command) from error + except OSError as error: + raise SankaMigrateError( + "could not execute sanka-migrate: {}".format(error), + command=command, + ) from error + + try: + stdout_bytes, stderr_bytes = await process.communicate() + except asyncio.CancelledError: + if process.returncode is None: + try: + process.kill() + except ProcessLookupError: + pass + await process.communicate() + raise + + exit_code = process.returncode + if exit_code is None: + raise SankaMigrateError("sanka-migrate {} did not exit".format(command), command=command) + return _finish_result( + stdout_bytes.decode("utf-8", errors="replace"), + command=command, + exit_code=exit_code, + stderr=stderr_bytes.decode("utf-8", errors="replace"), + ) + + +def _scan_args(root: Optional[PathValue], settings: Optional[str], artifact_dir: Optional[PathValue]) -> List[str]: + args = ["scan"] + _positional(args, root) + _option(args, "--settings", settings) + _option(args, "--artifact-dir", artifact_dir) + return args + + +def _plan_args( + root: Optional[PathValue], + file: Optional[PathValue], + state: Optional[PathValue], + to: Optional[str], + strategy: Optional[str], + artifact_dir: Optional[PathValue], + output: Optional[PathValue], + generation: Optional[str], + package_manager: Optional[str], + orm: Optional[str], +) -> List[str]: + args = ["plan"] + _positional(args, root) + for flag, value in ( + ("--file", file), + ("--state", state), + ("--to", to), + ("--strategy", strategy), + ("--artifact-dir", artifact_dir), + ("--output", output), + ("--generation", generation), + ("--package-manager", package_manager), + ("--orm", orm), + ): + _option(args, flag, value) + return args + + +def _apply_args( + plan_hash: str, + root: Optional[PathValue], + file: Optional[PathValue], + state: Optional[PathValue], + to: Optional[str], + artifact_dir: Optional[PathValue], + output: Optional[PathValue], + force: bool, + orm: Optional[str], + min_readiness: Optional[float], + gap_report_only: bool, + bench_candidate: Optional[PathValue], +) -> List[str]: + if not plan_hash.strip(): + raise ValueError("plan_hash must not be empty") + args = ["apply", "--plan-hash", plan_hash] + for flag, value in ( + ("--root", root), + ("--file", file), + ("--state", state), + ("--to", to), + ("--artifact-dir", artifact_dir), + ("--output", output), + ): + _option(args, flag, value) + _flag(args, "--force", force) + _option(args, "--orm", orm) + _option(args, "--min-readiness", min_readiness) + _flag(args, "--gap-report-only", gap_report_only) + _option(args, "--bench-candidate", bench_candidate) + return args + + +def _test_args( + root: Optional[PathValue], + file: Optional[PathValue], + state: Optional[PathValue], + to: Optional[str], + artifact_dir: Optional[PathValue], + output: Optional[PathValue], +) -> List[str]: + args = ["test"] + _positional(args, root) + for flag, value in ( + ("--file", file), + ("--state", state), + ("--to", to), + ("--artifact-dir", artifact_dir), + ("--output", output), + ): + _option(args, flag, value) + return args + + +def _verify_args( + root: Optional[PathValue], + file: Optional[PathValue], + state: Optional[PathValue], + to: Optional[str], + artifact_dir: Optional[PathValue], + output: Optional[PathValue], + cases: Optional[PathValue], + no_http: bool, +) -> List[str]: + args = ["verify"] + _positional(args, root) + for flag, value in ( + ("--file", file), + ("--state", state), + ("--to", to), + ("--artifact-dir", artifact_dir), + ("--output", output), + ("--cases", cases), + ): + _option(args, flag, value) + _flag(args, "--no-http", no_http) + return args + + +def _positional(args: List[str], value: Optional[PathValue]) -> None: + if value is not None: + args.append(os.fspath(value)) + + +def _option(args: List[str], flag: str, value: Any) -> None: + if value is not None: + args.extend((flag, os.fspath(value) if isinstance(value, os.PathLike) else str(value))) + + +def _flag(args: List[str], flag: str, enabled: bool) -> None: + if enabled: + args.append(flag) + + +def _finish_result(stdout: str, *, command: str, exit_code: int, stderr: str) -> SankaMigrateResult[Any]: + result = _decode_result( + stdout, + command=command, + exit_code=exit_code, + stderr=stderr, + ) + if exit_code != 0 or result.outcome == "error": + parsed_error = result.data.get("error") + error_data = parsed_error if isinstance(parsed_error, dict) else None + message = ( + str(error_data.get("message")) + if error_data and error_data.get("message") + else "sanka-migrate {} failed with exit code {}".format(command, exit_code) + ) + raise SankaMigrateError( + message, + command=command, + exit_code=exit_code, + parsed_error=error_data, + stderr=stderr, + ) + return result + + +def _missing_executable(command: str) -> SankaMigrateError: + return SankaMigrateError( + "sanka-migrate executable was not found; install it with " + "`uv tool install sanka-migrate` or pass executable=...", + command=command, + ) + + +def _decode_result( + stdout: str, + *, + command: str, + exit_code: int, + stderr: str, +) -> SankaMigrateResult[Dict[str, Any]]: + try: + payload = json.loads(stdout) + except (json.JSONDecodeError, TypeError) as error: + raise SankaMigrateError( + "sanka-migrate {} did not return one valid JSON document".format(command), + command=command, + exit_code=exit_code, + stderr=stderr, + ) from error + if not isinstance(payload, dict): + raise SankaMigrateError( + "sanka-migrate {} returned a non-object JSON document".format(command), + command=command, + exit_code=exit_code, + stderr=stderr, + ) + + schema_version = payload.get("schema_version") + payload_command = payload.get("command") + if schema_version != CLI_SCHEMA_VERSION: + raise SankaMigrateError( + "unsupported sanka-migrate protocol: {!r}".format(schema_version), + command=command, + exit_code=exit_code, + stderr=stderr, + ) + if payload_command != command: + raise SankaMigrateError( + "sanka-migrate returned command {!r}, expected {!r}".format(payload_command, command), + command=command, + exit_code=exit_code, + stderr=stderr, + ) + + data = payload.get("data") + artifacts = payload.get("artifacts") + limitations = payload.get("limitations") + next_actions = payload.get("next_actions") + if not isinstance(data, dict): + raise _invalid_field(command, exit_code, stderr, "data", "an object") + for name, value in ( + ("artifacts", artifacts), + ("limitations", limitations), + ("next_actions", next_actions), + ): + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise _invalid_field(command, exit_code, stderr, name, "a string array") + if not isinstance(payload.get("outcome"), str): + raise _invalid_field(command, exit_code, stderr, "outcome", "a string") + if not isinstance(payload.get("migration_state"), str): + raise _invalid_field(command, exit_code, stderr, "migration_state", "a string") + + return SankaMigrateResult( + schema_version=schema_version, + command=payload_command, + outcome=payload["outcome"], + migration_state=payload["migration_state"], + data=data, + artifacts=artifacts, + limitations=limitations, + next_actions=next_actions, + ) + + +def _invalid_field( + command: str, + exit_code: int, + stderr: str, + name: str, + expected: str, +) -> SankaMigrateError: + return SankaMigrateError( + "invalid sanka-cli/v1 field {!r}; expected {}".format(name, expected), + command=command, + exit_code=exit_code, + stderr=stderr, + ) diff --git a/tests/test_migrate.py b/tests/test_migrate.py new file mode 100644 index 0000000..5fa7a87 --- /dev/null +++ b/tests/test_migrate.py @@ -0,0 +1,384 @@ +from __future__ import annotations + +import asyncio +import inspect +import os +import tempfile +import textwrap +import unittest +from pathlib import Path + +import sanka_sdk.migrate as migrate_module +from sanka_sdk.migrate import AsyncSankaMigrate, SankaMigrate, SankaMigrateError + + +_FAKE_CLI = """\ +#!/usr/bin/env python3 +import json +import os +import sys +import time + +command = sys.argv[1] +mode = os.environ.get("FAKE_SANKA_MODE", "success") +pid_file = os.environ.get("FAKE_SANKA_PID_FILE") +if pid_file: + with open(pid_file, "w", encoding="utf-8") as file: + file.write(str(os.getpid())) +delay = float(os.environ.get("FAKE_SANKA_DELAY", "0")) +if delay: + time.sleep(delay) +if mode == "malformed": + print("progress before json") + print("{}") + raise SystemExit(0) + +payload = { + "schema_version": "wrong/v1" if mode == "wrong-schema" else "sanka-cli/v1", + "command": "wrong" if mode == "wrong-command" else command, + "outcome": "error" if mode == "error" else "success", + "migration_state": "failed" if mode == "error" else "complete", + "data": { + "argv": sys.argv[1:], + **( + {"error": {"code": "SANKA_USAGE", "message": "bad option"}} + if mode == "error" + else {} + ), + }, + "artifacts": [], + "limitations": [], + "next_actions": [], +} +print(json.dumps(payload)) +raise SystemExit(int(os.environ.get("FAKE_SANKA_EXIT", "2" if mode == "error" else "0"))) +""" + + +class SankaMigrateTests(unittest.TestCase): + def test_public_clients_import(self) -> None: + from sanka_sdk import AsyncSankaClient, SankaClient + + self.assertTrue(callable(SankaClient)) + self.assertTrue(callable(AsyncSankaClient)) + + def setUp(self) -> None: + self.temporary = tempfile.TemporaryDirectory() + self.addCleanup(self.temporary.cleanup) + self.root = Path(self.temporary.name) + self.executable = self.root / "fake-sanka-migrate" + self.executable.write_text(textwrap.dedent(_FAKE_CLI), encoding="utf-8") + self.executable.chmod(0o755) + self.migrate = SankaMigrate(cwd=self.root, executable=self.executable) + + def argv(self, result: object) -> list[str]: + return result.data["argv"] # type: ignore[attr-defined, no-any-return] + + def test_every_command_forwards_every_functional_option(self) -> None: + self.assertEqual( + self.argv( + self.migrate.scan( + root="source root", + settings="project.settings", + artifact_dir=".artifacts", + ) + ), + [ + "scan", + "source root", + "--settings", + "project.settings", + "--artifact-dir", + ".artifacts", + "--json", + ], + ) + self.assertEqual( + self.argv( + self.migrate.plan( + root="source", + file="migration.yaml", + state="state.db", + to="fastapi", + strategy="native", + artifact_dir=".artifacts", + output="target", + generation="full", + package_manager="uv", + orm="tortoise", + ) + ), + [ + "plan", + "source", + "--file", + "migration.yaml", + "--state", + "state.db", + "--to", + "fastapi", + "--strategy", + "native", + "--artifact-dir", + ".artifacts", + "--output", + "target", + "--generation", + "full", + "--package-manager", + "uv", + "--orm", + "tortoise", + "--json", + ], + ) + self.assertEqual( + self.argv( + self.migrate.apply( + plan_hash="sha256:reviewed", + root="source", + file="migration.yaml", + state="state.db", + to="fastapi", + artifact_dir=".artifacts", + output="target", + force=True, + orm="sqlalchemy", + min_readiness=75, + gap_report_only=True, + bench_candidate="candidate", + ) + ), + [ + "apply", + "--plan-hash", + "sha256:reviewed", + "--root", + "source", + "--file", + "migration.yaml", + "--state", + "state.db", + "--to", + "fastapi", + "--artifact-dir", + ".artifacts", + "--output", + "target", + "--force", + "--orm", + "sqlalchemy", + "--min-readiness", + "75", + "--gap-report-only", + "--bench-candidate", + "candidate", + "--json", + ], + ) + self.assertEqual( + self.argv( + self.migrate.test( + root="source", + file="migration.yaml", + state="state.db", + to="fastapi", + artifact_dir=".artifacts", + output="target", + ) + ), + [ + "test", + "source", + "--file", + "migration.yaml", + "--state", + "state.db", + "--to", + "fastapi", + "--artifact-dir", + ".artifacts", + "--output", + "target", + "--json", + ], + ) + self.assertEqual( + self.argv( + self.migrate.verify( + root="source", + file="migration.yaml", + state="state.db", + to="fastapi", + artifact_dir=".artifacts", + output="target", + cases="cases.json", + no_http=True, + ) + ), + [ + "verify", + "source", + "--file", + "migration.yaml", + "--state", + "state.db", + "--to", + "fastapi", + "--artifact-dir", + ".artifacts", + "--output", + "target", + "--cases", + "cases.json", + "--no-http", + "--json", + ], + ) + + def test_arguments_are_not_interpreted_by_a_shell(self) -> None: + marker = self.root / "unexpected" + root = "$(touch {})".format(marker) + result = self.migrate.scan(root=root) + + self.assertIn(root, self.argv(result)) + self.assertFalse(marker.exists()) + + def test_structured_errors_keep_exit_and_cli_details(self) -> None: + migrate = SankaMigrate( + cwd=self.root, + executable=self.executable, + env={"FAKE_SANKA_MODE": "error", "FAKE_SANKA_EXIT": "2"}, + ) + + with self.assertRaises(SankaMigrateError) as raised: + migrate.apply(plan_hash="sha256:reviewed") + + self.assertEqual(raised.exception.command, "apply") + self.assertEqual(raised.exception.exit_code, 2) + self.assertEqual(raised.exception.parsed_error["code"], "SANKA_USAGE") + self.assertEqual(str(raised.exception), "bad option") + + def test_protocol_rejects_malformed_schema_and_command(self) -> None: + for mode, message in ( + ("malformed", "one valid JSON document"), + ("wrong-schema", "unsupported sanka-migrate protocol"), + ("wrong-command", "expected 'scan'"), + ): + with self.subTest(mode=mode): + migrate = SankaMigrate( + cwd=self.root, + executable=self.executable, + env={"FAKE_SANKA_MODE": mode}, + ) + with self.assertRaisesRegex(SankaMigrateError, message): + migrate.scan() + + def test_apply_requires_a_reviewed_plan_hash(self) -> None: + with self.assertRaisesRegex(ValueError, "plan_hash"): + self.migrate.apply(plan_hash="") + + def test_missing_executable_has_an_install_hint(self) -> None: + migrate = SankaMigrate(cwd=self.root, executable=self.root / "missing") + with self.assertRaisesRegex(SankaMigrateError, "uv tool install sanka-migrate"): + migrate.scan() + + def test_every_public_symbol_and_method_has_hover_documentation(self) -> None: + for name in migrate_module.__all__: + self.assertTrue(inspect.getdoc(getattr(migrate_module, name)), name) + for client in (SankaMigrate, AsyncSankaMigrate): + for name in ("scan", "plan", "apply", "test", "verify"): + documentation = inspect.getdoc(getattr(client, name)) + self.assertTrue(documentation, "{}.{}".format(client.__name__, name)) + self.assertIn("Args:", documentation) + self.assertIn("Returns:", documentation) + self.assertIn("Raises:", documentation) + self.assertIn("Side effects:", documentation) + self.assertEqual( + inspect.iscoroutinefunction(getattr(client, name)), + client is AsyncSankaMigrate, + ) + self.assertEqual( + inspect.signature(getattr(client, name)), + inspect.signature(getattr(SankaMigrate, name)), + ) + + def test_packaged_module_matches_the_regeneration_source(self) -> None: + repository = Path(__file__).resolve().parents[1] + self.assertEqual( + (repository / "src/sanka_sdk/migrate.py").read_text(encoding="utf-8"), + (repository / "handwritten/sanka_sdk/migrate.py").read_text(encoding="utf-8"), + ) + + +class AsyncSankaMigrateTests(unittest.IsolatedAsyncioTestCase): + def setUp(self) -> None: + self.temporary = tempfile.TemporaryDirectory() + self.addCleanup(self.temporary.cleanup) + self.root = Path(self.temporary.name) + self.executable = self.root / "fake-sanka-migrate" + self.executable.write_text(textwrap.dedent(_FAKE_CLI), encoding="utf-8") + self.executable.chmod(0o755) + self.migrate = AsyncSankaMigrate(cwd=self.root, executable=self.executable) + + def argv(self, result: object) -> list[str]: + return result.data["argv"] # type: ignore[attr-defined, no-any-return] + + async def test_every_async_lifecycle_method_uses_the_shared_cli_contract(self) -> None: + results = [ + await self.migrate.scan(settings="project.settings"), + await self.migrate.plan(to="fastapi", generation="full"), + await self.migrate.apply(plan_hash="sha256:reviewed", force=True), + await self.migrate.test(output="target"), + await self.migrate.verify(cases="cases.json", no_http=True), + ] + + self.assertEqual( + [self.argv(result)[0] for result in results], + ["scan", "plan", "apply", "test", "verify"], + ) + for result in results: + self.assertEqual(self.argv(result)[-1], "--json") + + async def test_async_execution_does_not_block_the_event_loop(self) -> None: + migrate = AsyncSankaMigrate( + cwd=self.root, + executable=self.executable, + env={"FAKE_SANKA_DELAY": "0.2"}, + ) + task = asyncio.create_task(migrate.scan()) + + await asyncio.sleep(0.02) + self.assertFalse(task.done()) + await task + + @unittest.skipIf(os.name == "nt", "POSIX process liveness check") + async def test_cancellation_kills_and_reaps_the_cli_process(self) -> None: + pid_file = self.root / "child.pid" + migrate = AsyncSankaMigrate( + cwd=self.root, + executable=self.executable, + env={"FAKE_SANKA_DELAY": "2", "FAKE_SANKA_PID_FILE": str(pid_file)}, + ) + task = asyncio.create_task(migrate.scan()) + for _ in range(100): + if pid_file.exists(): + break + await asyncio.sleep(0.01) + self.assertTrue(pid_file.exists()) + + task.cancel() + with self.assertRaises(asyncio.CancelledError): + await task + + with self.assertRaises(ProcessLookupError): + os.kill(int(pid_file.read_text(encoding="utf-8")), 0) + + async def test_missing_executable_has_an_async_install_hint(self) -> None: + migrate = AsyncSankaMigrate(cwd=self.root, executable=self.root / "missing") + with self.assertRaisesRegex(SankaMigrateError, "uv tool install sanka-migrate"): + await migrate.scan() + + +if __name__ == "__main__": + unittest.main() diff --git a/uv.lock b/uv.lock index 8ca0d9c..ed49b20 100644 --- a/uv.lock +++ b/uv.lock @@ -119,7 +119,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.12.5" +version = "2.13.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -127,140 +127,139 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/ef/fc4f868f4e2cee79f863883abffceff107875f569b848507319842d2a681/pydantic-2.13.5.tar.gz", hash = "sha256:51a9c5f7b2f8e636f04c6cada605d9b6a3bf1348fdf945a3d8869b19bba0ee08", size = 845750, upload-time = "2026-08-28T14:04:00.916Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, + { url = "https://files.pythonhosted.org/packages/eb/47/c95ffc2009878c7aac0c5e08528022dcb885933252a88b5f170058014464/pydantic-2.13.5-py3-none-any.whl", hash = "sha256:346a034f080da3755d8e9cb5e00e8b07de1d39e4f6e2c87d8ab7cafa0b269a73", size = 472589, upload-time = "2026-08-28T14:03:59.136Z" }, ] [[package]] name = "pydantic-core" -version = "2.41.5" +version = "2.46.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +sdist = { url = "https://files.pythonhosted.org/packages/af/f9/8a06bea35ef8daf588f707784c973a7046e0034c8d8cfb08828eeffb8b75/pydantic_core-2.46.5.tar.gz", hash = "sha256:10416c15b8839ecc4ef4d0885da76da6fd0f67333a0eb8aff6d93c4b8f2910fc", size = 472262, upload-time = "2026-08-28T10:01:31.677Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, - { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, - { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, - { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, - { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, - { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, - { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, - { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, - { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, - { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, - { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, - { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, - { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, - { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, - { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, - { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, - { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, - { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, - { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, - { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, - { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, - { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, - { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, - { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, - { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, - { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, - { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, - { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, - { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, - { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, - { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, - { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, - { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, - { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, - { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, - { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, - { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, - { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, - { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, - { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, - { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, - { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, - { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, - { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, - { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, - { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, - { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, - { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, - { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, - { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, - { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, - { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, - { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, - { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, - { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, - { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, - { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, - { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, - { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, - { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, - { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, - { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, - { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, - { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, - { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, - { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, - { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, - { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, - { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, - { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, - { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, - { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, - { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, - { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, - { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, - { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, - { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, - { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, - { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, - { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, - { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, - { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, - { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, - { url = "https://files.pythonhosted.org/packages/54/db/160dffb57ed9a3705c4cbcbff0ac03bdae45f1ca7d58ab74645550df3fbd/pydantic_core-2.41.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:8bfeaf8735be79f225f3fefab7f941c712aaca36f1128c9d7e2352ee1aa87bdf", size = 2107999, upload-time = "2025-11-04T13:42:03.885Z" }, - { url = "https://files.pythonhosted.org/packages/a3/7d/88e7de946f60d9263cc84819f32513520b85c0f8322f9b8f6e4afc938383/pydantic_core-2.41.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:346285d28e4c8017da95144c7f3acd42740d637ff41946af5ce6e5e420502dd5", size = 1929745, upload-time = "2025-11-04T13:42:06.075Z" }, - { url = "https://files.pythonhosted.org/packages/d5/c2/aef51e5b283780e85e99ff19db0f05842d2d4a8a8cd15e63b0280029b08f/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a75dafbf87d6276ddc5b2bf6fae5254e3d0876b626eb24969a574fff9149ee5d", size = 1920220, upload-time = "2025-11-04T13:42:08.457Z" }, - { url = "https://files.pythonhosted.org/packages/c7/97/492ab10f9ac8695cd76b2fdb24e9e61f394051df71594e9bcc891c9f586e/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7b93a4d08587e2b7e7882de461e82b6ed76d9026ce91ca7915e740ecc7855f60", size = 2067296, upload-time = "2025-11-04T13:42:10.817Z" }, - { url = "https://files.pythonhosted.org/packages/ec/23/984149650e5269c59a2a4c41d234a9570adc68ab29981825cfaf4cfad8f4/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8465ab91a4bd96d36dde3263f06caa6a8a6019e4113f24dc753d79a8b3a3f82", size = 2231548, upload-time = "2025-11-04T13:42:13.843Z" }, - { url = "https://files.pythonhosted.org/packages/71/0c/85bcbb885b9732c28bec67a222dbed5ed2d77baee1f8bba2002e8cd00c5c/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:299e0a22e7ae2b85c1a57f104538b2656e8ab1873511fd718a1c1c6f149b77b5", size = 2362571, upload-time = "2025-11-04T13:42:16.208Z" }, - { url = "https://files.pythonhosted.org/packages/c0/4a/412d2048be12c334003e9b823a3fa3d038e46cc2d64dd8aab50b31b65499/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:707625ef0983fcfb461acfaf14de2067c5942c6bb0f3b4c99158bed6fedd3cf3", size = 2068175, upload-time = "2025-11-04T13:42:18.911Z" }, - { url = "https://files.pythonhosted.org/packages/73/f4/c58b6a776b502d0a5540ad02e232514285513572060f0d78f7832ca3c98b/pydantic_core-2.41.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f41eb9797986d6ebac5e8edff36d5cef9de40def462311b3eb3eeded1431e425", size = 2177203, upload-time = "2025-11-04T13:42:22.578Z" }, - { url = "https://files.pythonhosted.org/packages/ed/ae/f06ea4c7e7a9eead3d165e7623cd2ea0cb788e277e4f935af63fc98fa4e6/pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0384e2e1021894b1ff5a786dbf94771e2986ebe2869533874d7e43bc79c6f504", size = 2148191, upload-time = "2025-11-04T13:42:24.89Z" }, - { url = "https://files.pythonhosted.org/packages/c1/57/25a11dcdc656bf5f8b05902c3c2934ac3ea296257cc4a3f79a6319e61856/pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:f0cd744688278965817fd0839c4a4116add48d23890d468bc436f78beb28abf5", size = 2343907, upload-time = "2025-11-04T13:42:27.683Z" }, - { url = "https://files.pythonhosted.org/packages/96/82/e33d5f4933d7a03327c0c43c65d575e5919d4974ffc026bc917a5f7b9f61/pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:753e230374206729bf0a807954bcc6c150d3743928a73faffee51ac6557a03c3", size = 2322174, upload-time = "2025-11-04T13:42:30.776Z" }, - { url = "https://files.pythonhosted.org/packages/81/45/4091be67ce9f469e81656f880f3506f6a5624121ec5eb3eab37d7581897d/pydantic_core-2.41.5-cp39-cp39-win32.whl", hash = "sha256:873e0d5b4fb9b89ef7c2d2a963ea7d02879d9da0da8d9d4933dee8ee86a8b460", size = 1990353, upload-time = "2025-11-04T13:42:33.111Z" }, - { url = "https://files.pythonhosted.org/packages/44/8a/a98aede18db6e9cd5d66bcacd8a409fcf8134204cdede2e7de35c5a2c5ef/pydantic_core-2.41.5-cp39-cp39-win_amd64.whl", hash = "sha256:e4f4a984405e91527a0d62649ee21138f8e3d0ef103be488c1dc11a80d7f184b", size = 2015698, upload-time = "2025-11-04T13:42:35.484Z" }, - { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, - { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, - { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, - { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, - { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, - { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, - { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, - { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, - { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, - { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, - { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, - { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, - { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, - { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, - { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, - { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" }, - { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, - { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, - { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, - { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, - { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, - { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, - { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, + { url = "https://files.pythonhosted.org/packages/74/6b/8f79692844269427abb3e4dd9e68edfcbe65ae25527d99183214de716c59/pydantic_core-2.46.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:657b40d6240c0a7b6a64b30f22d1e3aa631c7e846c621b0c0f6d1d75e2e15ea6", size = 2076533, upload-time = "2026-08-28T09:57:35.421Z" }, + { url = "https://files.pythonhosted.org/packages/bd/d0/c787604c71c2bdcda1a5656942fc822cd0f9cd879b9484bb84fc42172703/pydantic_core-2.46.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ecb42011e12ee19cafbc312887cbf3546959fe02fbad44f272d4be5baa997615", size = 1924650, upload-time = "2026-08-28T09:57:37.944Z" }, + { url = "https://files.pythonhosted.org/packages/4a/77/ca2f8e997d9bfdb32205297aff38f210f398822d895b1af1b59fd9df9c13/pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4dedce55295becb61921e386b99d4f2706045306e7fa52249a33004c837379fb", size = 1951261, upload-time = "2026-08-28T09:57:39.339Z" }, + { url = "https://files.pythonhosted.org/packages/a0/53/bd12e1a9255df4edee00353778e2614b5346265d51e1567ab72153e803a2/pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9f47b8a949e60f027f0aa0a6f6c7b7e9c55cbf4380d10b344e282fa4e7ab1e1b", size = 2021808, upload-time = "2026-08-28T09:57:40.69Z" }, + { url = "https://files.pythonhosted.org/packages/d7/41/f7f312751ebc6d6767da91964a9c7954c18e226a1720ab234e3dfb9d6c17/pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:200aa3dc9f8d54f0754f43247c0bad0999fdcfbfd2488384dd44f37279271fe6", size = 2196184, upload-time = "2026-08-28T09:57:42.275Z" }, + { url = "https://files.pythonhosted.org/packages/3d/93/ce93aa030ab6bac4683ba8861e7baad89dd24b02e66b8801a0e4f6a00311/pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6d30e1a4f138b8951063e9a394752a9179b51da288ffa507b1e659222f4c1793", size = 2238212, upload-time = "2026-08-28T09:57:44.122Z" }, + { url = "https://files.pythonhosted.org/packages/34/a1/c8e6b66f499f510752c07a092dfe27621f9c255635e59d38704b5681c35a/pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:850a08d167dde16db8702c274f320c7be9d7da6f6dff2b58b18f9e815bd94f5b", size = 2064073, upload-time = "2026-08-28T09:57:45.613Z" }, + { url = "https://files.pythonhosted.org/packages/5c/fa/605e2b127ee30dbf4b1da9da4843587cf2b2d16486c241cc7a5be2d2c1bd/pydantic_core-2.46.5-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:c3471e5c4a949c26ec00a77f01df59096aa9495877de76fd60a980f8ee6be461", size = 2093102, upload-time = "2026-08-28T09:57:46.953Z" }, + { url = "https://files.pythonhosted.org/packages/4a/f7/1ab28093c09032ddce7c92c7a55d503b6ecd70f42c32492946c1cb5477b1/pydantic_core-2.46.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3a3e26b6a8274211bddee2d0e4d0d42778f17a34510f49d2ec44b58abfc41736", size = 2133452, upload-time = "2026-08-28T09:57:48.362Z" }, + { url = "https://files.pythonhosted.org/packages/30/c8/47c79b756f12f85e8b0fbdb2b495f6b6eb32e6c98a4beae7a570a0b7c63c/pydantic_core-2.46.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:fc5d783bd4a2387e97b8a2d5ec781cfb92b3d893bf82370548e99db5915935d3", size = 2146477, upload-time = "2026-08-28T09:57:49.74Z" }, + { url = "https://files.pythonhosted.org/packages/13/5c/79fc00cb8f651d6061991de8d7cedf1c78c73cbd4862c42ef418f03b8bfa/pydantic_core-2.46.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:356c8368cbc321050b169595683a2e1d63413b1e0e2868b330af9fc14c616d3f", size = 2300832, upload-time = "2026-08-28T09:57:51.639Z" }, + { url = "https://files.pythonhosted.org/packages/b4/72/dd1a29853cf6d22a1ebd9e3baf0239cbc57d2d16caff36a89e38eb9b1db3/pydantic_core-2.46.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:eb7d8d0e5886a89a55d2eef490e272fa965a9d57c6b29a5b5088a7997ec2cad1", size = 2320505, upload-time = "2026-08-28T09:57:53.236Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d1/ba4a8e06a9ddad0b4caf69cfaeecc0fbfcec20473bd808f5127fd16491c4/pydantic_core-2.46.5-cp310-cp310-win32.whl", hash = "sha256:4d44cf99ddebf875f9b68cc267aa684c99b7b44fe63ee1cac4ec163807290069", size = 1956853, upload-time = "2026-08-28T09:57:54.592Z" }, + { url = "https://files.pythonhosted.org/packages/f2/94/205ed9d7ddaf44acd489889708ea124a3f41bdb42c141c8684d528ad0e7a/pydantic_core-2.46.5-cp310-cp310-win_amd64.whl", hash = "sha256:1e5aad1220a1192c42341c8fd4a8686657e73ab2a920c970bdc4de334fe3193d", size = 2042551, upload-time = "2026-08-28T09:57:56.017Z" }, + { url = "https://files.pythonhosted.org/packages/a2/b6/81d2d19ea0be2c03664381b59f65fa72fc7969decedae00bc2c4ad835708/pydantic_core-2.46.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a1dee1b804ff4d11c663636cf15d2ea47e9f79cd56c033fb1cbf08924842a48f", size = 2074737, upload-time = "2026-08-28T09:57:57.711Z" }, + { url = "https://files.pythonhosted.org/packages/0c/18/b70da8300e292df4099684ea11b1958043580d2f50d2dc8bf7e542bdd84a/pydantic_core-2.46.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d625a186a65201c23a9e3b8ed9c47e90a026e03256608cc91851c6709096844f", size = 1921751, upload-time = "2026-08-28T09:57:59.265Z" }, + { url = "https://files.pythonhosted.org/packages/e7/1a/0d590341b6ffa4b4aca83508e6b8db4761aaeacfc15a25ca3815876d4797/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f8507560a9284e1370bb048ed4282012fbef4e8d109875b95e884d228552061", size = 1948231, upload-time = "2026-08-28T09:58:00.678Z" }, + { url = "https://files.pythonhosted.org/packages/7d/1d/02eb35761c51f2f7b1b042d6ab4cda6600f0c8c88a2243b3f734376201e5/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f93c5fe914d75fbec9a49209b00da5f08e9e467d69da2b1510c81940cfd10be", size = 2020708, upload-time = "2026-08-28T09:58:02.267Z" }, + { url = "https://files.pythonhosted.org/packages/4a/ea/f86073830e35d508cc8ddf9c3d9e6e6840fcb88d34bf726b0b4710186f27/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c767f552b21b10f774aeac128e828eafb796adfa1b666a18bf6321453c3a", size = 2194914, upload-time = "2026-08-28T09:58:03.934Z" }, + { url = "https://files.pythonhosted.org/packages/bb/d7/fc36240d7791ce90939e51608568c33bfdae26202016f9770c229a487d86/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:701b2e04b560eeb4bddf7a25ab8ca476176e34fdbd9a0e18196f0d12d4685f0b", size = 2235622, upload-time = "2026-08-28T09:58:05.516Z" }, + { url = "https://files.pythonhosted.org/packages/cf/bc/3fa2d76b83162820a17da7f645b28d1cba99fc8e1e5fc6517067ec450fa1/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49776eab08766a08dfff7012f8b422dcd7e25e43b316eedf0477c24fcfa84b7c", size = 2062091, upload-time = "2026-08-28T09:58:07.135Z" }, + { url = "https://files.pythonhosted.org/packages/ab/9a/095d557bb492c90cd8a70a6dd048bf793d433d03d86c81c11e912e4cd049/pydantic_core-2.46.5-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:a2468d93d181667a7abd66e1b64bb9f76f361b0fef8faddf687456453576f5ee", size = 2089904, upload-time = "2026-08-28T09:58:08.814Z" }, + { url = "https://files.pythonhosted.org/packages/24/98/7b76b1ad10a19a617a52aaa1d80e159115af939b095e86f8e756fd52e0df/pydantic_core-2.46.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:53feb344243bb9510a9dec7bf3cf1b64d88a98af5dc7872a5160465f8b198c8e", size = 2132244, upload-time = "2026-08-28T09:58:10.435Z" }, + { url = "https://files.pythonhosted.org/packages/20/32/7d6ca365fadba186a0c8f85de1a701663bce81efd309d9479be58687622f/pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:cd5214352ae68f3b5e9af7768bdc5253695ee069675db3480518420b3be881f2", size = 2143901, upload-time = "2026-08-28T09:58:12.033Z" }, + { url = "https://files.pythonhosted.org/packages/f8/09/eb9a6aa57f22fd1541a9c0aa2a1f3aeef3ec65347d33e10a6da2f43e0ee9/pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:9432f3598db432cb51c5b37fdbf29a60fcccc79e30d37a05022776a6bc4ab689", size = 2299425, upload-time = "2026-08-28T09:58:13.614Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f9/548a5bb9d4ba8cd26e26daf48052236f6b38bb61e7b7241fbc3c995719eb/pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8feeac04b5794e513e710af2f9c87d49f31a6dc47967bb264a1fed61a8989bec", size = 2318566, upload-time = "2026-08-28T09:58:15.199Z" }, + { url = "https://files.pythonhosted.org/packages/4a/20/06454d18834c02c406c9133f1a3b485305fd9ee984f9636c2f730bef6a9d/pydantic_core-2.46.5-cp311-cp311-win32.whl", hash = "sha256:892a881d5f68c2b9ea304b7a6c2c60d9343df578a311b0f86b94bc8f1ffe8129", size = 1954258, upload-time = "2026-08-28T09:58:16.813Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c2/718b9deb4b72453b5d8c7447a3b14cb77bef36917ef5f514e0948a4096a0/pydantic_core-2.46.5-cp311-cp311-win_amd64.whl", hash = "sha256:40375c2d05acec10323e45dfe2077ac44bc74659008614af5069034e2cfc781c", size = 2041030, upload-time = "2026-08-28T09:58:18.288Z" }, + { url = "https://files.pythonhosted.org/packages/67/ea/c1d1a5b72d6e1ff7f377a4d9199f6591f095beb5b409a8a5d89f7238d939/pydantic_core-2.46.5-cp311-cp311-win_arm64.whl", hash = "sha256:28a6a556cd3b6066bea827857f9d9cce027c96f776e512f544a581f9e42161f8", size = 2009234, upload-time = "2026-08-28T09:58:19.929Z" }, + { url = "https://files.pythonhosted.org/packages/82/3f/76358795aa7a8c6d4f36e2cb828ad1c90ee118e1393a9281664f5aade9d4/pydantic_core-2.46.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b9fe6fb92520e3fd61f2e49000b6911b188824f089b75973ea06d6267f0b476d", size = 2076516, upload-time = "2026-08-28T09:58:21.576Z" }, + { url = "https://files.pythonhosted.org/packages/db/50/26b091836076ce4cb2fac264186936acc069e0595772cfd02a563bc4761a/pydantic_core-2.46.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a39ac25a9a2fa4072efdb429833c4a4c8009a51ff9eea3eeae131713cd27991e", size = 1922874, upload-time = "2026-08-28T09:58:23.766Z" }, + { url = "https://files.pythonhosted.org/packages/09/f0/2a8ce3849e299d44e2d2c196b6082643a3235565a735cb51db7a6261f614/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fdc8b93a41521988916eeaa271173fcca7fa0803d62f87675aac8dcec1c8e29", size = 1951772, upload-time = "2026-08-28T09:58:25.435Z" }, + { url = "https://files.pythonhosted.org/packages/87/46/ac0dc8bdd9e6048183a14eb127764e7ad9240021c17513074a4711b0e31e/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b98134087d9de723658d17a42c7d0da8d6e2ef08015dee7dc93889047315f5e4", size = 2031832, upload-time = "2026-08-28T09:58:27.102Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c2/339de5bef7be36301a2231eaa52e62163742c2281f11b5f4892bc79785cd/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e652ab17569c94bff5475520f907b7148b8c24036a8ebbe5cf7cf7493d28579a", size = 2208645, upload-time = "2026-08-28T09:58:28.948Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a0/9ff22b797724262da14427abaed4dd1d864a139693fc5e7809114376a716/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d925f3d9afd05a8c0fb3a1031463a8d59ebe5e2afad297e29c78be19e13b4e62", size = 2265935, upload-time = "2026-08-28T09:58:30.625Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a4/eb9409ec0736e50aa70a412f16c204ed149516846912f7e6724d4c73ee53/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fc5be0abd4a407e200d844b404e33639a554e7bd0d448e7b9ae181be4789ac2", size = 2066284, upload-time = "2026-08-28T09:58:32.289Z" }, + { url = "https://files.pythonhosted.org/packages/c0/02/7f6156ffc926857f1c37c07d9a388682865a81830ab6a1b637082c25e399/pydantic_core-2.46.5-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:816ff0a6550ffc06c098ccd2e0698600f9aa7da192a79eaa6f9af504a35db869", size = 2105889, upload-time = "2026-08-28T09:58:33.986Z" }, + { url = "https://files.pythonhosted.org/packages/92/b1/e781d357ebe09fc929f995700f1b3503e8897f1cece183ecb1300d4d67e9/pydantic_core-2.46.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c7ea57fc63aa7da93a1bd2d644e6577befae10c52c4e36377635eea1056a74f5", size = 2158006, upload-time = "2026-08-28T09:58:35.647Z" }, + { url = "https://files.pythonhosted.org/packages/70/0a/644597d84ab400e50609c192120b85c9681c22d3a20461b9060a79be0a7a/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:efd62a42486f1bda5d24cb4f63d15a3c7768375fe83d36f9417b4ad7a2fb20b3", size = 2158408, upload-time = "2026-08-28T09:58:37.38Z" }, + { url = "https://files.pythonhosted.org/packages/1e/ee/ca3b7b3a4b3769ffe9ce9432a7c9be755de9593a46d3b0d54d0409323e44/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:2bc9419666990c06d7397831f2126a1ecc3594aaa3ff7de5bf2d066802f4e07b", size = 2309609, upload-time = "2026-08-28T09:58:39.22Z" }, + { url = "https://files.pythonhosted.org/packages/ce/52/39fa1f451486019524ca685020390e7ca351832fd874530ba30c8628e6dc/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:18a09e1e1011b462f2e32774f25859ef1223d5c2b0546a633cf56654710721e0", size = 2342618, upload-time = "2026-08-28T09:58:40.89Z" }, + { url = "https://files.pythonhosted.org/packages/81/5e/468fc630568c61dcef3cd47ad32ffbeed9af643f49208d1ea86ab4f890c4/pydantic_core-2.46.5-cp312-cp312-win32.whl", hash = "sha256:5cb482e9e84c851f4e623fe4acc1ced89168cf1fe18f7089db4548c8f5bbb65b", size = 1939475, upload-time = "2026-08-28T09:58:42.591Z" }, + { url = "https://files.pythonhosted.org/packages/cf/c9/4c19f41b84cf6b622a72fbeed7665b25d47a187d68d47d0d430c07f23268/pydantic_core-2.46.5-cp312-cp312-win_amd64.whl", hash = "sha256:5e81740c09e310f5aa5cbd3e434a01c154d4bef93241c7877b39f211d2b78ba8", size = 2043140, upload-time = "2026-08-28T09:58:44.272Z" }, + { url = "https://files.pythonhosted.org/packages/af/dd/0c1a050299147c746e5256db16d645ab5efd4f78c59937d581a0524e74a2/pydantic_core-2.46.5-cp312-cp312-win_arm64.whl", hash = "sha256:f7b0ec93a2893de856652154d73b7ba622f26fa97726487dcac373de5f4c6084", size = 1997729, upload-time = "2026-08-28T09:58:46.13Z" }, + { url = "https://files.pythonhosted.org/packages/f5/37/5abe39a8372a61d3dc3c1338fc504281c01b32fdb3169cd7187153b56d3e/pydantic_core-2.46.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:b7ca9034437b6022f941f4857459562ee00a560b97e7cce8a0ec5a74fc6766e0", size = 2075885, upload-time = "2026-08-28T09:58:47.856Z" }, + { url = "https://files.pythonhosted.org/packages/21/43/6323b1f8b217780454c61304bcd2b38ae4762f50754414124603ccc90bb2/pydantic_core-2.46.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f332f0e72a5a0400141f830744e141bf9f97917878dbe968669e8a7fefea78ff", size = 1922768, upload-time = "2026-08-28T09:58:49.58Z" }, + { url = "https://files.pythonhosted.org/packages/0f/a3/c05ca796e1197618a774b01e596aeedfefc2f7d8c01ae3054e910b120e8a/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:193375f3548919d3f0b60936ca113ada3e38f264f91b9b8e0508efaad57be931", size = 1951241, upload-time = "2026-08-28T09:58:51.511Z" }, + { url = "https://files.pythonhosted.org/packages/68/32/33bc39ac705c52cffc908e8389f9754fdb208aea5c69cceddf4eb3ce99af/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:79bdfa52f843137045b2d081cc05c120ba6665d29b7559c2c47690906f39279f", size = 2031975, upload-time = "2026-08-28T09:58:53.166Z" }, + { url = "https://files.pythonhosted.org/packages/b0/70/2333e885c0f6a67bc105c5916965dac9b57f2718ee20d81d1a06a4ebdc13/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:24922243639cbdac66c75fcb6fd6495a9cb52b213d62f9a0d16f0310b1ff8038", size = 2208542, upload-time = "2026-08-28T09:58:55.017Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ea/296debfb4264207bbda5936133892e027c0a58875ad53ebd512fba8ec3a2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c76fe65e607be28c7fd4d56fc3c42b1583aa058ce3408b7ad0fd540171d31f9f", size = 2264692, upload-time = "2026-08-28T09:58:56.767Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f2/9e4de77a6271e07a76d2d58b11c091a979c191ed2939bf80067568b369d2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f7b393a8b3da82f5c1fc0751e6d01ac6c55b93c18226a60bdfba4a724efafd1", size = 2066633, upload-time = "2026-08-28T09:58:58.531Z" }, + { url = "https://files.pythonhosted.org/packages/8d/db/f9e9d0c97445987b2084823d5c240de88087338f04fc2cfaa2df186b8049/pydantic_core-2.46.5-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:7ac031912d54f3d83ef3b3eb98dfabc1608802e2202263d25957eeed40b94761", size = 2105235, upload-time = "2026-08-28T09:59:00.421Z" }, + { url = "https://files.pythonhosted.org/packages/07/c5/79169b047b3b2c3e99e04bc76372af9637e0bf6db638274fa927df96369e/pydantic_core-2.46.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:837b396ca3d7b74091ca623f6cbd8351bd42d670a79c2683e79fb089f06a2de5", size = 2157367, upload-time = "2026-08-28T09:59:02.442Z" }, + { url = "https://files.pythonhosted.org/packages/26/b5/ba6057afb7c291bd449f51b867f95aef2072941c4ce4e5c31d6ffd132d3b/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:5ee239d575f80b08eca11f6e20f90c4c695de7825c67eefe6091fbf20dda648e", size = 2158420, upload-time = "2026-08-28T09:59:04.2Z" }, + { url = "https://files.pythonhosted.org/packages/6e/28/2057abecaafdc22912afa819603a51f0a62d40643b7c4871c51721fea9be/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:e80675d75ae2cd14372cb65cad5400d9347a3d3f6c13000183f22dfd027283ed", size = 2309588, upload-time = "2026-08-28T09:59:06.048Z" }, + { url = "https://files.pythonhosted.org/packages/71/9d/881156dc404e27479c4246128d73538464cab4a239bec61995e227644c30/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:9c4b71f10dd532fb7a5cbc8f58707779e64f03a258c2bf8bfbaecfcd9970b519", size = 2341866, upload-time = "2026-08-28T09:59:08.539Z" }, + { url = "https://files.pythonhosted.org/packages/5a/38/d66f443a259f84d13babdceae568e572b0ed26da17ca5d0a649ebb110a67/pydantic_core-2.46.5-cp313-cp313-win32.whl", hash = "sha256:97bf8de4d541598c94a59344eeb988a94c08ff76b5723c41f6567ec18c7892ea", size = 1938580, upload-time = "2026-08-28T09:59:10.402Z" }, + { url = "https://files.pythonhosted.org/packages/2c/1e/1d5371213f4cc9a7ed70c0bfcc7911de22311ee99a662a56077d7292d2ac/pydantic_core-2.46.5-cp313-cp313-win_amd64.whl", hash = "sha256:15f4a94963c95accac15b7b657bb177d3ad82bb90b0d0526d9a9b85079925db5", size = 2041980, upload-time = "2026-08-28T09:59:12.396Z" }, + { url = "https://files.pythonhosted.org/packages/5a/48/4222d90b1c67568bace4dec6dca6271449c66de3595d72b6d098f5fde597/pydantic_core-2.46.5-cp313-cp313-win_arm64.whl", hash = "sha256:d22a945598fb91236b4dd793a6e42e4f3dd7740bb5aace5ebd7d4c08d13bb575", size = 1997213, upload-time = "2026-08-28T09:59:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/8e/8a/14596f2a8367da50cf7cbac48169ee5d9c8e11d486a3b527082384630c72/pydantic_core-2.46.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c1c43ad4339643d70ebb8124e1305a7dab423001eff58bb41a0f731adbc98355", size = 2074081, upload-time = "2026-08-28T09:59:16.141Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d5/d8a4eb6d6c7f66b91dd37c576d76e9e60fba900caf5372c17bcf949febc2/pydantic_core-2.46.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1a353f84de772f423b5ffb11d7ae352fbbef0f446f3c0b0af0f8236d7233606e", size = 1920497, upload-time = "2026-08-28T09:59:18.065Z" }, + { url = "https://files.pythonhosted.org/packages/8e/26/092079428f86e927e030b2c0ced87df69dbb1c875cdeaa67bf42ea2be746/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5086029a57366b8cf81b130a43908738095c270c21a8d7f0e8bdfdb89718e2f3", size = 1952130, upload-time = "2026-08-28T09:59:20.476Z" }, + { url = "https://files.pythonhosted.org/packages/08/c3/8ec0e290a9ebaebd64047bf5fda94be835c6b1551b02437e4b76778fbcd7/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46c25dda9d092a06c08db76ffe0a197107904d0dfac653f7d5306bbcd6d6119c", size = 2026371, upload-time = "2026-08-28T09:59:22.227Z" }, + { url = "https://files.pythonhosted.org/packages/01/72/4fd20ad520fb8da0157f95b27a7eb05a72790ef08138e7701ac972c342ea/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:37ea7b83c935e5b0d68c9449b82651accf78a10828b2c02b2f2d9e9496446c21", size = 2202822, upload-time = "2026-08-28T09:59:24.277Z" }, + { url = "https://files.pythonhosted.org/packages/31/b0/d16e0771206b29314f0d52198b720be21e8a99ab2bf11e3bc0d7c9cebdff/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e64e88d5585bea9ce95861079de72006c7fa6d3df4e3a3b65ba31eb979c15c9f", size = 2262756, upload-time = "2026-08-28T09:59:26.608Z" }, + { url = "https://files.pythonhosted.org/packages/2c/9b/59634b7ac631c63b2a37760eb6943af3e29573d6b59a4abc5e7f019d4cee/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d510bac3ee52247af28ed4bb18a1e799f040ac60fd2bf5ccd4c92f1fbe786f", size = 2068352, upload-time = "2026-08-28T09:59:29.044Z" }, + { url = "https://files.pythonhosted.org/packages/08/7c/570abb1ad2155348dc754ea91be22e5aaa18eb6d69a6068f7c6f2679a6ed/pydantic_core-2.46.5-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:a2a5e1d0ff29adddc9f6d6821a66302e4493f8ca898b715b6b1182c2c201ea0a", size = 2104777, upload-time = "2026-08-28T09:59:30.95Z" }, + { url = "https://files.pythonhosted.org/packages/8e/25/5bf74adc65a1ac5b7be3f6cb0bcb5433615c1598a801c19d830d84c98ded/pydantic_core-2.46.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:03b9666e41e35d8909852ba191a0607520f81b74eaf12ccf8737005dbb313821", size = 2156312, upload-time = "2026-08-28T09:59:32.604Z" }, + { url = "https://files.pythonhosted.org/packages/90/6a/2ef38830675e050121040618135564ed56b860b45433b02d9b4ebece46f3/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:a91c17edf6eea2402cb5457b4c89e99bc5ed1004aa34c4adf1d4258c1a5c22c2", size = 2150067, upload-time = "2026-08-28T09:59:34.453Z" }, + { url = "https://files.pythonhosted.org/packages/90/ef/a7dbb03a14a64c2a4621f989c615ed9a892535a6cad938fc27079f919d80/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b49924c73a235e969511bf2aabdff3beebf9820931f646c80274d5d780010c47", size = 2304516, upload-time = "2026-08-28T09:59:36.194Z" }, + { url = "https://files.pythonhosted.org/packages/68/f8/6bb4c4b80e8a6fde1904c64a51c62a1d04fcdfa3ea521a66b2ddefa1d885/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:2cbd9a5eff05e51c447c34dfa4632145b26b09120cf04bd0c871e44c1a5e1c9a", size = 2335223, upload-time = "2026-08-28T09:59:37.931Z" }, + { url = "https://files.pythonhosted.org/packages/2a/80/f46b8c681195190b2c1f1c7c0a81abce60663e987613e09ef64d433dd96b/pydantic_core-2.46.5-cp314-cp314-win32.whl", hash = "sha256:2d5d76654becf5efd62c9e51c3756c67b49498b0c9a40884934c40807adbd074", size = 1934827, upload-time = "2026-08-28T09:59:39.836Z" }, + { url = "https://files.pythonhosted.org/packages/f7/3c/60674207246bc0a4009d2391b7c7251c7159f279c8d2ab8aae8ef46f3dee/pydantic_core-2.46.5-cp314-cp314-win_amd64.whl", hash = "sha256:fa10ef4112775900e7a0661068635eb67b2ab824fbde764de6e0e21982a93db0", size = 2042648, upload-time = "2026-08-28T09:59:41.792Z" }, + { url = "https://files.pythonhosted.org/packages/69/0c/117c562c7c1babdf44576b72a5e496906506c93690387ecfbca7c729ae2e/pydantic_core-2.46.5-cp314-cp314-win_arm64.whl", hash = "sha256:045ab3b6d308439e32b81cc173bba5b9018bc6ed896afd0c65b3b009b1699af5", size = 1989652, upload-time = "2026-08-28T09:59:43.702Z" }, + { url = "https://files.pythonhosted.org/packages/e8/66/9336ae58f9eb68c41d121894e52c4c89eccb07eb8f602a04ee9c3f37736a/pydantic_core-2.46.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8816f3d218beb4b787de5c9759c259b8fa61f9dec42dc7811f320a33771778b7", size = 2065829, upload-time = "2026-08-28T09:59:45.364Z" }, + { url = "https://files.pythonhosted.org/packages/c5/02/bc19b47a96c2d3109760711acf22369e56bd7e405ca52f7ade164d2ead57/pydantic_core-2.46.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bce57638e08ac148e5778cce7feb968307a727d66f8e2274a543d0cf0c9ad6a3", size = 1905716, upload-time = "2026-08-28T09:59:47.18Z" }, + { url = "https://files.pythonhosted.org/packages/52/a4/70b47c0509923dd98ccfed04fb3e32ea3849c82a0ff2205bb41009b43c00/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:976e1128455aa595ea04c79ccfedff1aaeab96ee013fcc916bed120c4f0ad94f", size = 1934216, upload-time = "2026-08-28T09:59:49.241Z" }, + { url = "https://files.pythonhosted.org/packages/52/ab/aa03b65f7bb198585edf806b906c3223ecf1795543e39e23aec4cce27ad2/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b891faeedeafba41b2983e5001a81b6a915b69544c7e7570d1989ce1c36ac7", size = 2010635, upload-time = "2026-08-28T09:59:51.692Z" }, + { url = "https://files.pythonhosted.org/packages/3c/8b/0da06343f30b84ec549aafd309c6456223d5dc8bd36af504c573faad561d/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f194189415698233dd1114a093a9b56e61e2c57e11b469be3b0506f46f0771c", size = 2209369, upload-time = "2026-08-28T09:59:53.582Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5b/844c4defaa34a3df66eb9257087d121d70c201298b96abdf9f492fc2f1bf/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82a36973cf8a2ef5406f4fe2edbf8ed0c99629535d959e0b100c76a32535a111", size = 2253238, upload-time = "2026-08-28T09:59:55.484Z" }, + { url = "https://files.pythonhosted.org/packages/f4/64/a4e536cb16d7f61a7fd3120b46c577fc7fa7325992f69c4f52bc786d77d8/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdbb78909f52b981d3b2d56b97328d71eb0b974c36bd77c920123a7ebb192829", size = 2065740, upload-time = "2026-08-28T09:59:58.038Z" }, + { url = "https://files.pythonhosted.org/packages/5f/75/aaa38c6bc2d085f6605b34eabdc6a8a4e0b2e61fc9c8e6e52b28e97b3125/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:52e24eacdb536cade636aa90fb851835222becff8484b7001fdc78cb0290f2aa", size = 2087425, upload-time = "2026-08-28T09:59:59.898Z" }, + { url = "https://files.pythonhosted.org/packages/55/ae/fcab4cfc39aba3689e1d20c8b5250ad280957022c09af2ed9cd585602a5e/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:37ae34309d7bd8c0d61ab839668058f2a7962ea1fc51d105d2db228fe0618034", size = 2139306, upload-time = "2026-08-28T10:00:03.057Z" }, + { url = "https://files.pythonhosted.org/packages/2d/f4/f1d03a4bc9d9acbc62f4d742b8a319af52f71885079868b2ff8e48a651ee/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:0cdbada856a1c69a7624a64d3d9aefe79300bd6ef827b43a4f265010b9b55184", size = 2144589, upload-time = "2026-08-28T10:00:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/83/f3/7a53bb1356de514a4cd295f25b6ac39237895620c0462d2592b76c16e114/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:545f26c504b27c3758439a5e6d9349931f0a04f855668d5fe323c89e82300a38", size = 2288882, upload-time = "2026-08-28T10:00:07.931Z" }, + { url = "https://files.pythonhosted.org/packages/cd/94/5a81583660c175c59d49ffb09f4b3a44debeaf86a19fca664ae1cdd9ee32/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:ff218293c9c806138dca139765e3b067621be52bcd93cdc14c7711be7ddc90a9", size = 2335210, upload-time = "2026-08-28T10:00:10.177Z" }, + { url = "https://files.pythonhosted.org/packages/5a/9f/5d685c2693b972d1a59c998586e8823712b66603aeff47ee60a4bdaafd37/pydantic_core-2.46.5-cp314-cp314t-win32.whl", hash = "sha256:97cf3eb53a8cccacf9d46686a0926186c9bfb5574f2ed66d3639d5fe117cd3a9", size = 1921180, upload-time = "2026-08-28T10:00:12.35Z" }, + { url = "https://files.pythonhosted.org/packages/70/12/5c94ee16d65a37a15f9e869f5e6256df111154491173801a4c5e800ab548/pydantic_core-2.46.5-cp314-cp314t-win_amd64.whl", hash = "sha256:d2f9fc07a8042a8f95925b35c4f04f469707c981fc33245b6ca187cf5d2dd290", size = 2020515, upload-time = "2026-08-28T10:00:14.774Z" }, + { url = "https://files.pythonhosted.org/packages/63/19/67830dda664e6bdf9285ee2e40f355d0d7d6b92aa0c42e8d217bb8d33d36/pydantic_core-2.46.5-cp314-cp314t-win_arm64.whl", hash = "sha256:acf8a67ba51f4ca9ddbd0e6b3000a65ac51ab734661778b3e7ba64d99a710f2f", size = 1989276, upload-time = "2026-08-28T10:00:16.984Z" }, + { url = "https://files.pythonhosted.org/packages/96/cc/4c88abc035cc0d8b2646a715d8c4145fad7d95817eb5f18297066b21e20e/pydantic_core-2.46.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c583b927a8838dab890706a6fa7573fbb8b70e24000ef9f7238e2d6f6435a5ed", size = 2078970, upload-time = "2026-08-28T10:00:18.938Z" }, + { url = "https://files.pythonhosted.org/packages/b4/59/fa3ef009cc1b2ca3753fd6869ee461b0b5b67c420cf659e32a12be027a6a/pydantic_core-2.46.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cdc8b74ecc48c0cb1e9607a05ec4e9e88db60a19ffcc9a1d5f9088ede40c8dc0", size = 1917185, upload-time = "2026-08-28T10:00:20.891Z" }, + { url = "https://files.pythonhosted.org/packages/15/5e/b3d8901f9775ad928077c3155c36f56fc1c813285e3986ed736a5fbf538e/pydantic_core-2.46.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b10e3e8fd7ddc2bd915848a2768e44c15b22936f1cc54c462ad1164deb02655", size = 1955266, upload-time = "2026-08-28T10:00:23.135Z" }, + { url = "https://files.pythonhosted.org/packages/aa/9e/5522b09d12e8720013f2e4ac174999f40a05501bd15ad2bfa197bc136198/pydantic_core-2.46.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f077d0b97ab11fa7dcc633fca53515f290bca8a8a633e966d5b6d1879d9ed01a", size = 2023466, upload-time = "2026-08-28T10:00:25.489Z" }, + { url = "https://files.pythonhosted.org/packages/89/2a/a5267bf2c6c7ded3f282b315e5f0cf2c58008c15b917a652fc32f92d6775/pydantic_core-2.46.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7b0fc826b16c55e561e5d2a0c5c77b051ba1d92808118c4e4b5390f5e0cf191d", size = 2198448, upload-time = "2026-08-28T10:00:27.676Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a5/f72c192aba23924065946728e4fba96f73939b90e5aa4f7d41e728aea8d4/pydantic_core-2.46.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef3fbbf161dc9351a2fe0422e51b129f9e97e42385bd0320b309c15f7d287dd8", size = 2240121, upload-time = "2026-08-28T10:00:30.168Z" }, + { url = "https://files.pythonhosted.org/packages/a8/9e/0c0cc24149429c030bef1a5c1776150e7e61fcbbfc068c6d1f9de90eb259/pydantic_core-2.46.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:978e7b97d4824b5be09c69fb70507cbde3b0323fc147332ca40a94d9a6a0ebbf", size = 2067262, upload-time = "2026-08-28T10:00:32.285Z" }, + { url = "https://files.pythonhosted.org/packages/1c/00/a2b8690a11d909d9ec9c4eb4d084b4d2e1b227e9b2e74f5926cd39096245/pydantic_core-2.46.5-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:9b68938dd5b0c783d88ff8e2dcc69451b5eb936fe212d516b21b9d5567f6d464", size = 2095116, upload-time = "2026-08-28T10:00:35.556Z" }, + { url = "https://files.pythonhosted.org/packages/bf/7e/d3088a2717b7bb316d8d0e64a4b0caf994769e88c56df79df547d75c1dc0/pydantic_core-2.46.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:771cf63ae0b1b50dd22e5f3e3549fab5f3f4ff1635d352a9e1a97fe01c7b2e64", size = 2134727, upload-time = "2026-08-28T10:00:37.829Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a4/55a9e0ef61cfd1cbf4289059eb68a3eab765fca8ead6f9991d7de027d42e/pydantic_core-2.46.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7c6be839a5a8312626b32029a415644a0846b420bc8b52b95b28cd92da162168", size = 2147932, upload-time = "2026-08-28T10:00:40.123Z" }, + { url = "https://files.pythonhosted.org/packages/2d/25/d2fbc9d59f91f6c50c0d2ec032041c5e3295d68325ade06ec93fa82da43c/pydantic_core-2.46.5-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:895395f8918627b04efb1ad2a4cf605387143300ba03304cd1dfa6d03f5e095e", size = 2301528, upload-time = "2026-08-28T10:00:42.339Z" }, + { url = "https://files.pythonhosted.org/packages/9d/76/eccc0528d1421e298b42f85650cf021f0f7c42f502c7e58808db4a672bdb/pydantic_core-2.46.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:fc8515076c11f3cfdf4fb142dcca0fe384b1230a3b5415458ac84f3e0903ec13", size = 2322431, upload-time = "2026-08-28T10:00:44.399Z" }, + { url = "https://files.pythonhosted.org/packages/6f/0c/ffab5a9a0fb82825c44f00dea8ec9d540d2e1e4ab2f1c4f0c32bb8b37fd9/pydantic_core-2.46.5-cp39-cp39-win32.whl", hash = "sha256:3d2652072b2d774947ba5cf78a9e59644ac62ee572daf6dd2e1dfe905e15b2b7", size = 1958681, upload-time = "2026-08-28T10:00:46.786Z" }, + { url = "https://files.pythonhosted.org/packages/86/89/8bb47660fed8c16adf1aae301ba149442e8fd220c126bbea2d24b987abb8/pydantic_core-2.46.5-cp39-cp39-win_amd64.whl", hash = "sha256:3aa166e99c4f2985407fb8714aebede877ecb5455cf321b606adca926d30d5a0", size = 2046649, upload-time = "2026-08-28T10:00:48.99Z" }, + { url = "https://files.pythonhosted.org/packages/af/1e/ecca01fce348f7e8afa9572441ff6f7d1cc70d21e4859f33944d10877e1e/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:c14ad3bdc85ee7f318742c457ca3968a92126d144b15721c759033bfb06296c2", size = 2075342, upload-time = "2026-08-28T10:00:51.353Z" }, + { url = "https://files.pythonhosted.org/packages/1f/4c/af80c7a8032dfc897040ad5cb772bebde529a381186499e6e29987f23f8c/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0bddb4020d8f04175865ccd17eff3040874fc11fb593f424edb452653b4b947c", size = 1907219, upload-time = "2026-08-28T10:00:53.438Z" }, + { url = "https://files.pythonhosted.org/packages/be/3e/54d89e2b092e778716bf6153634ef479e955f48c261090be23aa1e0fb0b5/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2471fd51c61c610e1dcf7de44d7299283661654d11264ab4802b303368d69c47", size = 1953393, upload-time = "2026-08-28T10:00:55.58Z" }, + { url = "https://files.pythonhosted.org/packages/ea/89/828ee90cda28ce17bdefaa3a6eaf74fe430e113295a10e6126beca559d6c/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b10ec717381bdbfafef34607824db4c91de69ff085e4fca3b2af91b4fa17e68a", size = 2099024, upload-time = "2026-08-28T10:00:57.794Z" }, + { url = "https://files.pythonhosted.org/packages/df/dd/053c2e4303f791f3b8f8a14ab0b22008e8eb21d868c0c90b4f9be705b76a/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:013d6f3483d81e02e7c328831808f336c8596ee33b4bd4026b9ffb1e960b8942", size = 2062540, upload-time = "2026-08-28T10:01:00.318Z" }, + { url = "https://files.pythonhosted.org/packages/d7/dd/a18df751a5e37dd51bfad7f68e766999125bebe68c9e1d10a493ad01bd63/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:e9c134bb666dd54b778b9fc0d2b50cbb7f979b9e3716f26a88c9ab3b6fc1dd0f", size = 1902040, upload-time = "2026-08-28T10:01:02.529Z" }, + { url = "https://files.pythonhosted.org/packages/b7/13/01d40f9d07ce8a779fd6e0bd8ad4fba91309500dd67b869e2e219d261a6d/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:347ec774390c87326a2e4929d58d3f7e8763a104d5d35f4cd595a4c952366433", size = 1967479, upload-time = "2026-08-28T10:01:05.004Z" }, + { url = "https://files.pythonhosted.org/packages/fa/04/c81d4841331c2178b6fb09ae225425e110ed72d990c9fe556c4ec03d1013/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e24d8f05fa2d28513d94e877e9c75ad66175376209b3977f916e240e623193c", size = 2111034, upload-time = "2026-08-28T10:01:07.345Z" }, + { url = "https://files.pythonhosted.org/packages/20/21/22102e9950b3049526d20e811b95396508377d87651edd2b80d2b3d28659/pydantic_core-2.46.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ab4b66edffb32d9e951efb3814bd104b8367a7501b81b955cacb5726d897389f", size = 2071333, upload-time = "2026-08-28T10:01:09.636Z" }, + { url = "https://files.pythonhosted.org/packages/d8/18/87aefa427d191e6d3ab1447f1efc1cdcac86af1069239b133e8a0fd7f7c9/pydantic_core-2.46.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:337639ba62a11acde6ef3aeb08c8ea755f8ef1fe5e513356c0f36a2b0d7568b0", size = 1912713, upload-time = "2026-08-28T10:01:12.285Z" }, + { url = "https://files.pythonhosted.org/packages/1f/93/fd89e9ad49b1805ca94d24ce1088b7d305f05c35ffafcedb9819d03588a0/pydantic_core-2.46.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:413a717a410d0c817ef5b786a059415550b3794e1d0c2abffd9efb93a3d9f7b4", size = 2090926, upload-time = "2026-08-28T10:01:15.19Z" }, + { url = "https://files.pythonhosted.org/packages/6f/45/8e59dab6acf8d35f02f0a958980074f31038968bdb2c983fcae9d1efee03/pydantic_core-2.46.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1e449def1945a462c464331254e5a44fca7c3b4f9aedf59ec2f50f8066dd8e25", size = 2131303, upload-time = "2026-08-28T10:01:17.937Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a5/e1d4dc5180dd887a9522efc1f8716b8692b7606b1d3273d7862eaf66be44/pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:a445486499897b88a7d6c310c88ed64dd37b1b59bfd7ae9107490bbb362f47d6", size = 2145128, upload-time = "2026-08-28T10:01:20.694Z" }, + { url = "https://files.pythonhosted.org/packages/c2/d7/ad493864a7fb21c0c4df98f965e2db430cb25a9d7369b5778d5016c09fd9/pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:2d330aaba8621b1edcec8ae2c4050f63b84ccf6d98723a8f212e9684713abf0e", size = 2294560, upload-time = "2026-08-28T10:01:23.495Z" }, + { url = "https://files.pythonhosted.org/packages/02/8e/b41c84c913f29973a268e6c2b5bbf13c95adb9956c126d10da11ba3b2bef/pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b6acfb46a814762367fb7ba0828b0a17d441b92ce249a0e007474c9072662dda", size = 2317531, upload-time = "2026-08-28T10:01:26.334Z" }, + { url = "https://files.pythonhosted.org/packages/db/1d/068464f23075f66a8f1b806935e9cd9363ee446636ea70d2c22ee8659dbf/pydantic_core-2.46.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d0a24b40877af2de4950252be9d21eaf7fb07660f3c2cae1f56c6b599ada5266", size = 2140686, upload-time = "2026-08-28T10:01:28.947Z" }, ] [[package]]