diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 4842bd3..b4efbd6 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -4,11 +4,11 @@ on: pull_request: branches: - main - - release/0.9.0 + - release/1.0.0 push: branches: - main - - release/0.9.0 + - release/1.0.0 workflow_dispatch: permissions: @@ -23,11 +23,18 @@ jobs: name: Offline tests - Python ${{ matrix.python-version }} runs-on: ubuntu-latest strategy: + # Report every interpreter result instead of cancelling the matrix on the + # first failure, so a single-version incompatibility is easy to isolate. + fail-fast: false + # Python 3.15 is intentionally absent: it is still a prerelease during the + # 1.0 release work and is not claimed as a supported version. matrix: python-version: - "3.10" - "3.11" - "3.12" + - "3.13" + - "3.14" steps: - uses: actions/checkout@v4 @@ -55,10 +62,10 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Set up Python 3.12 + - name: Set up Python 3.14 uses: actions/setup-python@v5 with: - python-version: "3.12" + python-version: "3.14" - name: Install Poetry uses: snok/install-poetry@v1 with: @@ -70,8 +77,12 @@ jobs: run: | rm -rf dist poetry build - # Validates the built artifacts and a clean wheel installation. - # Runs outside the Poetry environment so the smoke test cannot import - # the repository checkout instead of the installed distribution. + # Validates artifact metadata, source-distribution contents, and a clean + # install of both the wheel and the source distribution. Runs outside the + # Poetry environment so the smoke tests cannot import the repository + # checkout instead of the installed distribution artifact. - name: Validate release artifacts run: python scripts/validate_release.py + # Metadata rendering check only; nothing is ever uploaded from CI. + - name: Twine check artifacts + run: poetry run twine check dist/* diff --git a/README.md b/README.md index 1e10bbc..da9fb7a 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,17 @@ For detailed documentation, check out the [Wiki](https://github.com/zero-sum-sea python3 -m pip install python-mlb-statsapi ``` +### Python support + +| Claim | Value | +| --- | --- | +| Minimum declared Python version (`Requires-Python`) | `>=3.10` | +| CI-validated versions | 3.10, 3.11, 3.12, 3.13, 3.14 | + +The minimum declared Python version is 3.10 and the CI-validated versions are +3.10 through 3.14. There is no upper Python bound. Prerelease interpreters are +excluded from the required test matrix and are not claimed as supported. + ## Quick Start ```python >>> import mlbstatsapi @@ -67,73 +78,70 @@ Seattle Mariners Seattle ## HTTP Sessions, Timeouts, Retries, and Error Behavior -Version 0.8.0 added shared HTTP Sessions, explicit timeouts, optional Session injection, bounded retries, and structured transport exceptions. Version 0.9.0 builds on that transport with configurable HTTP behavior: a public retry policy, richer `MlbHttpError` context, an optional strict mode, compatibility warnings, and a versioned User-Agent. +Version 0.8.0 added shared HTTP Sessions, explicit timeouts, optional Session injection, bounded retries, and structured transport exceptions. Version 0.9.0 made that transport configurable with a public retry policy, richer `MlbHttpError` context, compatibility warnings, and a versioned User-Agent. Version 1.0.0 makes strict HTTP handling the default and documents the stable public API contract. The `Mlb` client remains synchronous. Shared Sessions pool reusable connections; they do not cache MLB response bodies, and the client does not enable response caching by default. -For the complete reference see the [HTTP transport documentation](docs/http-transport.md). For what changed in this release see the [0.9.0 release notes](docs/releases/0.9.0.md). - -### Recommended context-manager usage +For the complete reference see the [HTTP transport documentation](docs/http-transport.md). For what changed in this release see the [1.0.0 release notes](docs/releases/1.0.0.md). For the stable public API boundary see the [public API contract](docs/public-api.md). -Prefer a context manager so library-owned HTTP resources are closed when the block exits, including when the block exits because of an exception: +### Upgrading to version 1.0 -```python -import mlbstatsapi +`Mlb()` now uses strict HTTP handling by default. It is equivalent to `Mlb(strict_http=True)`. -with mlbstatsapi.Mlb() as mlb: - player = mlb.get_person(664034) - team = mlb.get_team(136) +```text +Mlb() now uses strict HTTP handling by default +Final non-404 4xx responses raise MlbHttpError +404 keeps endpoint-specific None / [] / {} behavior +Final 5xx still raises MlbHttpError +Timeouts still raise MlbTimeoutError +Transport failures still raise MlbTransportError +Successful invalid JSON still raises MlbDecodeError ``` -One `Mlb` client uses one shared `requests.Session`. The v1 and v1.1 adapters share that Session, so repeated requests can reuse pooled connections. A Session manages a pool of reusable connections; it is not one permanent network connection. - -Callers who do not use a context manager may call `mlb.close()` instead. Repeated `close()` calls are safe. Closing a client only closes a Session the library created; a caller-injected Session is left open for its owner. - -### Compatibility mode is the default - -Existing construction continues to work unchanged: +Recommended version 1.0 usage: ```python import mlbstatsapi -with mlbstatsapi.Mlb() as mlb: - player = mlb.get_person(664034) +try: + with mlbstatsapi.Mlb() as mlb: + player = mlb.get_person(664034) +except mlbstatsapi.MlbHttpError as exc: + print(exc.status_code) + print(exc.reason) + print(exc.url) ``` -That is equivalent to: +Temporary compatibility opt-out while migrating: ```python -mlb = mlbstatsapi.Mlb( - strict_http=False, -) +import mlbstatsapi + +with mlbstatsapi.Mlb(strict_http=False) as mlb: + player = mlb.get_person(664034) ``` -Compatibility mode remains the default in version 0.9.0. A final non-404 4xx response still returns the historical empty result instead of raising, so existing applications keep working after upgrading. +`strict_http=False` is a temporary migration opt-out and an explicit request for historical 0.9 behavior. It is not the recommended long-term 1.0 configuration. See [Migrating from 0.9.x to 1.0](docs/http-transport.md#migrating-from-09x-to-10) for the full process, warning-as-error guidance, and before-and-after examples. -### Optional strict HTTP mode +### Recommended context-manager usage -Applications that would rather fail loudly can opt in to strict mode: +Prefer a context manager so library-owned HTTP resources are closed when the block exits, including when the block exits because of an exception: ```python import mlbstatsapi -with mlbstatsapi.Mlb( - strict_http=True, -) as mlb: +with mlbstatsapi.Mlb() as mlb: player = mlb.get_person(664034) + team = mlb.get_team(136) ``` -In strict mode: - -* A final non-404 4xx response raises `MlbHttpError` -* A final 5xx response raises `MlbHttpError`, as it already did in compatibility mode -* A 404 keeps the existing endpoint-specific behavior and does not raise +One `Mlb` client uses one shared `requests.Session`. The v1 and v1.1 adapters share that Session, so repeated requests can reuse pooled connections. A Session manages a pool of reusable connections; it is not one permanent network connection. -"Final" means after the bounded retry policy has been exhausted. Strict mode is opt-in; it is not the default. +Callers who do not use a context manager may call `mlb.close()` instead. Repeated `close()` calls are safe. Closing a client only closes a Session the library created; a caller-injected Session is left open for its owner. -### Compatibility warnings +### Compatibility mode -When compatibility mode converts a final non-404 4xx response into the historical empty result, the library emits `MlbHttpCompatibilityWarning`. The warning marks a response that strict mode would have raised on, so it doubles as migration guidance. +Callers who need historical 0.9 empty-result behavior for final non-404 4xx responses can pass `strict_http=False`. That path emits `MlbHttpCompatibilityWarning` exactly once per suppressed final response, does not change 404 handling, and does not suppress final 5xx, timeout, transport, or decode failures. The category inherits from `FutureWarning`, so it stays visible under default Python warning filters. Applications can promote only this package category to an error: @@ -147,7 +155,7 @@ warnings.filterwarnings( ) ``` -Filter on `mlbstatsapi.MlbHttpCompatibilityWarning` specifically rather than disabling all warnings or all `FutureWarning` instances, which would also hide unrelated notices from other libraries. No warning is emitted for successful responses, 404 responses, intermediate retries, final 5xx responses, or in strict mode. +Filter on `mlbstatsapi.MlbHttpCompatibilityWarning` specifically rather than disabling all warnings or all `FutureWarning` instances, which would also hide unrelated notices from other libraries. Prefer removing `strict_http=False` and catching `MlbHttpError` over permanently ignoring the warning. ### Custom timeouts @@ -218,7 +226,7 @@ Caller-injected Session ### Reusing the retry policy on a caller-managed Session -`create_retry_policy()` is public in version 0.9.0. It returns a new instance of the same tested policy the library mounts on Sessions it creates, so a caller-managed Session can opt in to identical retry behavior: +`create_retry_policy()` remains public. It returns a new instance of the same tested policy the library mounts on Sessions it creates, so a caller-managed Session can opt in to identical retry behavior: ```python import requests @@ -250,7 +258,7 @@ A Session created by the library sends a package-specific User-Agent: python-mlb-statsapi/ ``` -For this release that resolves to `python-mlb-statsapi/0.9.0`. The version is read from the installed distribution metadata, so it always matches the installed release. Only the `User-Agent` header is set; other Requests defaults such as `Accept-Encoding` remain intact, and the header carries no identifiers beyond the package name and version. +For this release's currently declared package metadata that resolves to `python-mlb-statsapi/1.0.0`. The version is read from the installed distribution metadata, so it always matches the installed release. Only the `User-Agent` header is set; other Requests defaults such as `Accept-Encoding` remain intact, and the header carries no identifiers beyond the package name and version. Headers on a caller-injected Session are left untouched, so applications that set their own User-Agent keep it. @@ -282,7 +290,7 @@ except mlbstatsapi.MlbDecodeError: * `MlbHttpError` represents an unexpected final HTTP response * `MlbDecodeError` represents invalid JSON in a successful response -Version 0.9.0 adds `method`, `response_data`, and `body_excerpt` to `MlbHttpError` alongside the existing `status_code`, `reason`, and `url`. `response_data` holds the decoded JSON dictionary or list when the error body contains one, and is `None` otherwise. `body_excerpt` is a bounded excerpt of the response text, capped at 500 characters. Complete response bodies are never automatically logged, and `str(exc)` stays concise. +`MlbHttpError` exposes `method`, `status_code`, `reason`, `url`, `response_data`, and `body_excerpt`. `response_data` holds the decoded JSON dictionary or list when the error body contains one, and is `None` otherwise. `body_excerpt` is a bounded excerpt of the response text, capped at 500 characters. Complete response bodies are never automatically logged, and `str(exc)` stays concise. ### Backward-compatible exception handling @@ -318,11 +326,11 @@ Backoff factor: 0.5 Retry-After respected: yes ``` -Only GET requests are retried, and retries are bounded. Ordinary client errors such as 400, 401, 403, and 404 are not retried. Invalid JSON and Pydantic validation failures are not retried. Retries improve resilience for transient failures, but they do not guarantee success. The retry values are unchanged from version 0.8.0. +Only GET requests are retried, and retries are bounded. Ordinary client errors such as 400, 401, 403, and 404 are not retried. Invalid JSON and Pydantic validation failures are not retried. Retries improve resilience for transient failures, but they do not guarantee success. The retry values are unchanged from versions 0.8.0 and 0.9.0. The version 1.0 strict default does not change retry or Session behavior. ### Existing 404 compatibility -Version 0.9.0 preserves existing endpoint-specific not-found behavior in both compatibility mode and strict mode. Depending on the endpoint, a 404 may still produce: +Version 1.0.0 preserves existing endpoint-specific not-found behavior under both the default and `strict_http=False`. Depending on the endpoint, a 404 may still produce: ```text None @@ -330,19 +338,19 @@ None {} ``` -Not every 404 raises `MlbHttpError`, and strict mode does not change that. +Not every 404 raises `MlbHttpError`, and the strict default does not change that. ### HTTP behavior at a glance -| Final response | Compatibility mode (default) | Strict mode | -| -------------- | ---------------------------- | ----------- | +| Final response | Default 1.0 behavior | Explicit compatibility mode | +| -------------- | -------------------- | --------------------------- | | Successful 2xx | Normal result | Normal result | -| Non-404 4xx | Warning and historical empty result | `MlbHttpError` | +| Non-404 4xx | `MlbHttpError` | Warning and historical empty result | | 404 | Existing endpoint behavior | Existing endpoint behavior | -| Final 429 | Warning and historical empty result | `MlbHttpError` | +| Final 429 | `MlbHttpError` after retries | Warning and historical empty result after retries | | Final 5xx | `MlbHttpError` | `MlbHttpError` | -See the [HTTP transport documentation](docs/http-transport.md) for the complete retry policy, Session ownership rules, warning behavior, cleanup behavior, and exception hierarchy, and the [0.9.0 release notes](docs/releases/0.9.0.md) for the release summary and migration guidance. +See the [HTTP transport documentation](docs/http-transport.md) for the complete retry policy, Session ownership rules, warning behavior, cleanup behavior, and migration guidance, and the [1.0.0 release notes](docs/releases/1.0.0.md) for the release summary. ## Working with Pydantic Models @@ -483,9 +491,10 @@ poetry run pytest tests/ rm -rf dist poetry build python3 scripts/validate_release.py +poetry run twine check dist/* ``` -`scripts/validate_release.py` is the same release check offline CI runs. It inspects the built wheel and source distribution, installs the wheel into a temporary virtual environment, and runs a public-import smoke test against the installed package. It never contacts the MLB API. +`scripts/validate_release.py` is the same release check offline CI runs. It inspects the built wheel and source distribution, clean-installs each artifact into its own temporary virtual environment, and runs the same public-API smoke test against both installed artifacts. The smoke test verifies the declared metadata, the supported package-root imports, the strict HTTP default, explicit strict and compatibility modes, the versioned `User-Agent`, and injected-Session ownership. Every response it observes comes from an injected fake Session, so it never contacts the MLB API. Offline CI is the normal pull-request gate. External tests are available manually, on a weekly schedule, and before releases. diff --git a/docs/http-transport.md b/docs/http-transport.md index df04dc0..371ca8a 100644 --- a/docs/http-transport.md +++ b/docs/http-transport.md @@ -1,12 +1,19 @@ # HTTP Transport -This document describes the HTTP transport behavior of the current release, version 0.9.0. +This document describes the HTTP transport behavior of the current release, +version 1.0.0. -Version 0.8.0 introduced shared Sessions, explicit timeouts, bounded retries, and structured exceptions. Version 0.9.0 keeps all of that and adds configurable HTTP behavior: a public retry policy, richer `MlbHttpError` context, an optional strict mode, compatibility warnings, and a versioned User-Agent. +Version 0.8.0 introduced shared Sessions, explicit timeouts, bounded retries, +and structured exceptions. Version 0.9.0 introduced configurable strict +behavior and compatibility warnings. Version 1.0.0 makes strict handling the +default and defines the stable public contract. -The public client remains synchronous. Ordinary usage does not need to configure sessions or retries. +The public client remains synchronous. Ordinary usage does not need to +configure sessions or retries. -See [the 0.9.0 release notes](releases/0.9.0.md) for a shorter summary of what changed. +See [the 1.0.0 release notes](releases/1.0.0.md) for a shorter summary of what +changed. For the authoritative public API boundary see +[the public API contract](public-api.md). ## Public transport API @@ -26,8 +33,9 @@ from mlbstatsapi import ( ) ``` -Names that are not exported from `mlbstatsapi` are internal and may change without a -deprecation cycle. +Names that are not exported from `mlbstatsapi` are internal and may change +without a deprecation cycle. See [public-api.md](public-api.md) for the +complete stability classification. ## Existing usage @@ -40,11 +48,13 @@ mlb = mlbstatsapi.Mlb() player = mlb.get_person(664034) ``` -The client remains synchronous. Async support is not part of version 0.9.0. +In version 1.0.0 that construction uses strict HTTP handling by default. The +client remains synchronous. Async support is not part of version 1.0.0. ## Context manager -Prefer a context manager when you want automatic cleanup of a library-created Session: +Prefer a context manager when you want automatic cleanup of a library-created +Session: ```python import mlbstatsapi @@ -89,7 +99,8 @@ That means: 30 seconds: read timeout ``` -The read timeout is the maximum wait while reading response data. It is not one total wall-clock duration for the complete request. +The read timeout is the maximum wait while reading response data. It is not +one total wall-clock duration for the complete request. ## Custom timeout @@ -132,8 +143,8 @@ A Session is not: ## Session ownership -Session ownership is the single most important rule in this document. Whoever creates the -Session configures it and closes it. +Session ownership is the single most important rule in this document. Whoever +creates the Session configures it and closes it. ```text Library-created Session @@ -147,9 +158,12 @@ Caller-injected Session Existing headers remain untouched ``` -The library never installs adapters, replaces headers, or closes a Session it did not -create. `Mlb.close()` and exiting `with Mlb(session=session)` both leave an injected -Session open. +The library never installs adapters, replaces headers, or closes a Session it +did not create. `Mlb.close()` and exiting `with Mlb(session=session)` both +leave an injected Session open. + +The version 1.0 strict default does not change Session ownership, injection, +or cleanup behavior. ## Session injection @@ -168,10 +182,10 @@ finally: session.close() ``` -Callers who inject a Session control its retry, TLS, proxy, header, and adapter -configuration. See [Reusing the retry policy on a caller-managed -Session](#reusing-the-retry-policy-on-a-caller-managed-session) for opting in to the -library's tested retry policy. +Callers who inject a Session control its retry, TLS, proxy, header, and +adapter configuration. See [Reusing the retry policy on a caller-managed +Session](#reusing-the-retry-policy-on-a-caller-managed-session) for opting in +to the library's tested retry policy. ## User-Agent @@ -181,14 +195,14 @@ Library-created Sessions send a package-specific User-Agent: python-mlb-statsapi/ ``` -For this release that resolves to: +With the package version currently declared in project metadata that resolves to: ```text -python-mlb-statsapi/0.9.0 +python-mlb-statsapi/1.0.0 ``` -The version comes from the installed package metadata, so it always matches the -installed release without a separately maintained version string. +The version comes from the installed package metadata, so it always matches +the installed release without a separately maintained version string. Notes: @@ -229,10 +243,12 @@ finally: ## Default retry policy -Library-created Sessions mount a bounded retry policy for GET requests automatically. +Library-created Sessions mount a bounded retry policy for GET requests +automatically. -Caller-injected Sessions are never automatically reconfigured. Retry settings on an -injected Session remain under the caller's control unless the caller opts in. +Caller-injected Sessions are never automatically reconfigured. Retry settings +on an injected Session remain under the caller's control unless the caller +opts in. ```text Initial request: 1 @@ -270,68 +286,81 @@ Additional rules: * Pydantic validation failures are not retried * Application parsing failures are not retried * A final 404 preserves existing not-found behavior -* A final 429 preserves existing 4xx compatibility by default and warns +* A final non-404 4xx, including a final 429, raises `MlbHttpError` under the default * A final 5xx raises `MlbHttpError` -* In strict mode, a final non-404 4xx (including a final 429) raises `MlbHttpError` +* Explicit `strict_http=False` preserves the historical empty result for final non-404 4xx and warns -Retries improve resilience for transient failures. They do not guarantee success. +Retries improve resilience for transient failures. They do not guarantee +success. The version 1.0 strict default does not change retry values or which +statuses are retried. -## HTTP compatibility modes +## Default HTTP behavior -Compatibility mode remains the default: +Version 1.0.0 defaults to strict HTTP handling. These constructions are +equivalent: ```python mlb = mlbstatsapi.Mlb() +mlb = mlbstatsapi.Mlb(strict_http=True) ``` -or: +Default behavior: -```python -mlb = mlbstatsapi.Mlb( - strict_http=False, -) -``` +```text +Successful 2xx + Return the normal endpoint result -Callers may explicitly enable strict mode: +Final non-404 4xx + Raise MlbHttpError by default -```python -mlb = mlbstatsapi.Mlb( - strict_http=True, -) +404 + Preserve endpoint-specific None, [], or {} behavior + +Final 429 + Retry first, then raise MlbHttpError under the default + +Final 5xx + Retry where configured, then raise MlbHttpError + +Timeout + Raise MlbTimeoutError + +Transport failure + Raise MlbTransportError + +Successful malformed JSON + Raise MlbDecodeError ``` -Behavior: +"Final" means the response remaining after the bounded retry policy has +completed. Intermediate retried responses neither raise nor warn. -| Final response | Compatibility mode | Strict mode | -| -------------- | ----------------------------------- | -------------------------- | -| Successful 2xx | Normal result | Normal result | -| Non-404 4xx | Warning and historical empty result | `MlbHttpError` | -| 404 | Existing endpoint behavior | Existing endpoint behavior | -| Final 429 | Warning and historical empty result | `MlbHttpError` | -| Final 5xx | `MlbHttpError` | `MlbHttpError` | +## Behavior table -Every row describes the *final* response. A retryable status such as 429, 500, 502, 503, -or 504 is evaluated against this table only after the bounded retry policy has been -exhausted; intermediate retried responses neither raise nor warn. +| Final response | Default 1.0 behavior | Explicit `strict_http=False` | +| -------------- | ---------------------------- | ------------------------------------------------- | +| Successful 2xx | Normal result | Normal result | +| Non-404 4xx | `MlbHttpError` | Warning and historical empty result | +| 404 | Existing endpoint behavior | Existing endpoint behavior | +| Final 429 | `MlbHttpError` after retries | Warning and historical empty result after retries | +| Final 5xx | `MlbHttpError` | `MlbHttpError` | Notes: -* Compatibility mode remains the default -* Strict mode is explicitly opt-in -* Strict mode applies only after retries are exhausted -* Strict mode does not make 404 raise -* Strict mode does not change transport or decode exceptions -* Strict-mode exceptions include the richer context from `MlbHttpError` -* Existing constructor usage remains valid -* Compatibility mode emits `MlbHttpCompatibilityWarning` for a suppressed non-404 4xx +* `Mlb()` and `Mlb(strict_http=True)` are equivalent spellings of the default +* `strict_http=False` is an explicit compatibility opt-out, not the preferred long-term configuration +* Strict handling applies only after retries are exhausted +* Strict handling does not make 404 raise +* Strict handling does not change transport or decode exceptions +* Raised `MlbHttpError` instances include the richer response context attributes -Example: +Recommended default usage: ```python import mlbstatsapi try: - with mlbstatsapi.Mlb(strict_http=True) as mlb: + with mlbstatsapi.Mlb() as mlb: player = mlb.get_person(664034) except mlbstatsapi.MlbHttpError as exc: print(exc.method) @@ -342,44 +371,66 @@ except mlbstatsapi.MlbHttpError as exc: print(exc.body_excerpt) ``` -## Compatibility warnings +## Compatibility mode -Version 0.9.0 emits `MlbHttpCompatibilityWarning` when compatibility mode returns the -historical empty result for a final non-404 4xx response. +```python +mlb = mlbstatsapi.Mlb(strict_http=False) +``` -The warning means strict mode would have raised `MlbHttpError` for the same response. +is an explicit compatibility opt-out. It: -```python -import mlbstatsapi +* Preserves the historical empty result for final non-404 4xx responses +* Emits `MlbHttpCompatibilityWarning` exactly once per suppressed final response +* Does not change 404 behavior +* Does not suppress final 5xx errors +* Does not alter timeout, transport, or decode failures +* Runs only after retry exhaustion -mlb = mlbstatsapi.Mlb() -sports = mlb.get_sports() -``` +Compatibility mode is a temporary migration path and an explicit request for +historical 0.9 behavior. It is not the recommended long-term 1.0 +configuration. + +## Compatibility warnings + +When `strict_http=False` converts a final non-404 4xx response into the +historical empty result, the library emits `MlbHttpCompatibilityWarning`. -A representative warning looks like: +The warning means the default strict path would have raised `MlbHttpError` for +the same response. It is emitted once per suppressed final response and is +attributed to the public caller frame outside the `mlbstatsapi` package +namespace. + +The warning's semantic content includes: ```text -HTTP 403 for https://statsapi.mlb.com/api/v1/sports was handled through -compatibility mode and returned the historical empty result. Pass -strict_http=True to raise MlbHttpError. This compatibility behavior may -change in version 1.0. +Status code +Request URL +strict_http=False selected compatibility mode +Historical empty result was returned +Strict handling is the version 1.0 default +How to receive MlbHttpError instead ``` -The category inherits from `FutureWarning` so the migration notice stays visible under -default Python warning filters. +Warning messages contain only the status code and request URL from the +response. Response bodies, headers, credentials, cookies, and tokens are never +included. Do not treat the complete prose string as a stable public contract; +filter and handle the warning by category. + +The category inherits from `FutureWarning` so the migration notice stays +visible under default Python warning filters. A warning is emitted only when all three of the following are true: -* Compatibility mode is active +* Compatibility mode is active (`strict_http=False`) * The final response is a non-404 4xx -* Strict mode would have raised `MlbHttpError` for the same response +* The default strict path would have raised `MlbHttpError` for the same response No warning is emitted for: ```text Successful responses 404 -Strict mode +Default strict handling Intermediate retries Final 5xx Timeouts @@ -390,38 +441,26 @@ Pydantic validation failures When the warning is emitted: -| Response | Warning | -| ------------------------- | ------- | -| Non-404 4xx, compatibility mode | Yes | -| Final 429, compatibility mode, after retries | Yes | -| Non-404 4xx, strict mode | No, `MlbHttpError` is raised instead | -| 404, either mode | No | -| Successful 2xx | No | -| Final 5xx | No, `MlbHttpError` is raised in both modes | -| Timeout, transport, decode, or validation failure | No | +| Response | Warning | +| -------------------------------------------------- | ------- | +| Non-404 4xx, `strict_http=False` | Yes | +| Final 429, `strict_http=False`, after retries | Yes | +| Non-404 4xx, default / `strict_http=True` | No, `MlbHttpError` is raised instead | +| 404, either mode | No | +| Successful 2xx | No | +| Final 5xx | No, `MlbHttpError` is raised in both modes | +| Timeout, transport, decode, or validation failure | No | Additional rules: -* The warning never changes the return value; compatibility mode still returns the - historical empty result in version 0.9.0 +* The warning never changes the return value; compatibility mode still returns the historical empty result * A final 404 remains warning-free and keeps existing `None` / `[]` / `{}` behavior * Warnings are emitted only after retries are exhausted, so a retried 429 warns once -* Strict mode does not warn because it raises `MlbHttpError` directly -* Warning messages contain only the status code and request URL, never response - bodies, headers, or credentials -* Stricter defaults may be introduced in version 1.0 +* Default strict handling does not warn because it raises `MlbHttpError` directly -Enabling strict mode is the recommended migration: - -```python -import mlbstatsapi +## Warning-as-error environments -mlb = mlbstatsapi.Mlb( - strict_http=True, -) -``` - -Applications may also turn only this package warning into an exception: +Applications may treat warnings as exceptions: ```python import warnings @@ -433,7 +472,11 @@ warnings.filterwarnings( ) ``` -Or silence only this category: +Under `strict_http=False`, this can turn a suppressed 4xx into a warning +exception. The preferred migration is to remove `strict_http=False` and catch +`MlbHttpError`. A temporary targeted warning filter is acceptable. + +Temporary targeted ignore, labeled as migration-only behavior: ```python import warnings @@ -445,21 +488,75 @@ warnings.filterwarnings( ) ``` -Prefer enabling strict mode over permanently ignoring the warning when the application -wants explicit HTTP failures. Filter by `mlbstatsapi.MlbHttpCompatibilityWarning` rather -than disabling all `FutureWarning` or all warnings, which would also hide unrelated -notices from other libraries. +Ignoring the warning is temporary migration behavior. Disabling every warning +or every `FutureWarning` is not recommended; that would also hide unrelated +notices from other libraries. Filter by +`mlbstatsapi.MlbHttpCompatibilityWarning` specifically. + +## Migrating from 0.9.x to 1.0 + +Recommended process: + +1. Identify code that relied on empty results for failed non-404 4xx responses +2. Add handling for `MlbHttpError` +3. Distinguish 404 domain results from other HTTP failures +4. Use `strict_http=False` only where migration cannot happen immediately +5. Test warning-as-error configurations +6. Remove `strict_http=False` +7. Confirm injected Session and retry behavior remain correct + +Version 0.9-style compatibility: + +```python +import mlbstatsapi + +with mlbstatsapi.Mlb(strict_http=False) as mlb: + player = mlb.get_person(664034) +``` + +Recommended 1.0 state: + +```python +import mlbstatsapi + +try: + with mlbstatsapi.Mlb() as mlb: + player = mlb.get_person(664034) +except mlbstatsapi.MlbHttpError as exc: + print(exc.status_code) + print(exc.url) +``` + +A missing person may still return `None` on a 404 and is not necessarily an +exception. Catch `MlbHttpError` for unexpected HTTP failures; continue treating +endpoint-specific 404 empty results as domain-level not-found outcomes. + +## Public API stability + +Version 1.0.0 documents the stable public API in +[public-api.md](public-api.md). That contract covers package-root imports, +constructor signatures, the exception hierarchy, Session ownership, documented +endpoint methods, Python support, and the boundary between public and +internal APIs. + +This transport guide does not duplicate that contract. In particular, version +1.0 does not promise that: + +* Every upstream MLB response field is frozen +* Every class in `mlbstatsapi.models` is permanently stable +* The unofficial MLB API itself will never change +* Private underscore-prefixed names are public ## Reusing the retry policy on a caller-managed Session -`create_retry_policy()` returns a new instance of the same tested retry policy used -internally for library-created Sessions. +`create_retry_policy()` returns a new instance of the same tested retry policy +used internally for library-created Sessions. -Callers who inject a Session must mount the policy themselves. The library does not -install or replace adapters on caller-injected Sessions. +Callers who inject a Session must mount the policy themselves. The library +does not install or replace adapters on caller-injected Sessions. -Callers retain control over connection-pool sizes and other `HTTPAdapter` options. -The caller remains responsible for closing an injected Session. +Callers retain control over connection-pool sizes and other `HTTPAdapter` +options. The caller remains responsible for closing an injected Session. ```python import requests @@ -481,8 +578,8 @@ finally: session.close() ``` -Mounting the same adapter instance for both schemes is valid. Callers may also mount -separate adapters when they need different settings for HTTP and HTTPS. +Mounting the same adapter instance for both schemes is valid. Callers may also +mount separate adapters when they need different settings for HTTP and HTTPS. ## Structured exceptions @@ -525,7 +622,8 @@ except MlbDecodeError: print("The MLB API returned invalid JSON") ``` -Backward-compatible handling remains valid because all new errors inherit from `TheMlbStatsApiException`: +Backward-compatible handling remains valid because all new errors inherit from +`TheMlbStatsApiException`: ```python try: @@ -591,8 +689,8 @@ except mlbstatsapi.MlbHttpError as exc: ## Existing 404 behavior -Version 0.9.0 preserves endpoint-specific not-found behavior in both compatibility mode -and strict mode. +Version 1.0.0 preserves endpoint-specific not-found behavior under both the +default and `strict_http=False`. Depending on the endpoint, a 404 may become: @@ -602,27 +700,13 @@ None {} ``` -Not every 404 raises `MlbHttpError`. Strict mode does not change this, and a 404 never -emits `MlbHttpCompatibilityWarning`. - -## Version 1.0 migration direction - -Version 0.9.0 keeps compatibility mode as the default. - -The `MlbHttpCompatibilityWarning` notices exist to give applications advance migration -guidance: each warning marks a response that strict mode would already have raised on. - -A future 1.0 release may make stricter non-404 4xx behavior the default. No final 1.0 -decision is implemented here, and nothing about the current return shapes changes in -version 0.9.0. - -Applications that want the future-facing behavior today can enable strict mode, and -applications that want to find affected call sites early can turn -`MlbHttpCompatibilityWarning` into an error. +Not every 404 raises `MlbHttpError`. The strict default does not change this, +and a 404 never emits `MlbHttpCompatibilityWarning`. ## No response caching -Shared Sessions pool network connections. They do not cache MLB response bodies. +Shared Sessions pool network connections. They do not cache MLB response +bodies. The client has no default response cache. @@ -630,4 +714,4 @@ The client has no default response cache. The client remains synchronous. -Async support is not part of version 0.9.0. +Async support is not part of version 1.0.0. diff --git a/docs/public-api.md b/docs/public-api.md new file mode 100644 index 0000000..12ee217 --- /dev/null +++ b/docs/public-api.md @@ -0,0 +1,482 @@ +# Public API Contract (1.x) + +This document is the authoritative public API contract for the +`python-mlb-statsapi` **1.x** series. + +It defines which package-root symbols, constructor signatures, exception and +warning relationships, Session ownership rules, and `Mlb` endpoint methods are +supported after version 1.0. Maintainers should use this document when deciding +whether a change is a patch, a minor release, or a major release. + +This package is an unofficial wrapper for the MLB Stats API and is not +affiliated with Major League Baseball. + +Related documents: + +* [HTTP transport](http-transport.md) — timeouts, retries, strict mode, and Session details +* Issue #286 — define the stable 1.0 public API +* Issue #282 — parent 1.0 release tracking + +## Stability policy + +During the 1.x series: + +* Existing supported package-root symbols will not be removed or renamed +* Required constructor parameters will not be added without compatibility handling +* Positional and keyword-only parameter boundaries are part of the API +* Structured exception inheritance will remain compatible +* Documented Session ownership behavior will remain compatible +* Documented endpoint-level 404 return shapes will remain compatible + +The following may still evolve in a compatible way: + +* New optional parameters +* New endpoint methods +* New model fields +* New exception subclasses under `TheMlbStatsApiException` +* New documented public helpers +* Bug fixes +* Additional supported Python versions + +Semantic versioning expectations after 1.0: + +| Change | Typical release | +| --- | --- | +| Bug fix that preserves documented contracts | patch | +| Compatible addition (optional arg, new endpoint, new model field) | minor | +| Removal or rename of a supported symbol | major | +| Breaking change to a documented constructor signature | major | +| Breaking change to documented exception inheritance | major | +| Breaking change to documented Session ownership | major | +| Breaking change to a documented 404 return shape | major | + +## Package-root imports + +Supported symbols are importable as: + +```python +import mlbstatsapi +from mlbstatsapi import Mlb +``` + +and via: + +```python +from mlbstatsapi import ( + Mlb, + MlbDataAdapter, + MlbResult, + create_retry_policy, + TheMlbStatsApiException, + MlbTransportError, + MlbTimeoutError, + MlbHttpError, + MlbDecodeError, + MlbHttpCompatibilityWarning, + return_splits, + get_stat_attributes, +) +``` + +### Classification of package-root symbols + +| Symbol | Status | +| --- | --- | +| `Mlb` | Public and stable in 1.x | +| `MlbDataAdapter` | Public and stable in 1.x | +| `MlbResult` | Public and stable in 1.x | +| `create_retry_policy` | Public and stable in 1.x | +| `TheMlbStatsApiException` | Public and stable in 1.x | +| `MlbTransportError` | Public and stable in 1.x | +| `MlbTimeoutError` | Public and stable in 1.x | +| `MlbHttpError` | Public and stable in 1.x | +| `MlbDecodeError` | Public and stable in 1.x | +| `MlbHttpCompatibilityWarning` | Public and stable in 1.x | +| `return_splits` | Public legacy helper, stable in 1.x but not preferred for new code | +| `get_stat_attributes` | Public legacy helper, stable in 1.x but not preferred for new code | + +No package-root symbol is marked deprecated in version 1.0. Deprecation requires +a documented replacement, a warning strategy, a removal timeline, and a +separate focused issue. + +### Accidentally exposed submodule names + +Python attaches imported submodules to the package namespace. The following +names may appear via `dir(mlbstatsapi)` and `from mlbstatsapi import *`, but +they are **not** part of the supported public API: + +| Name | Where exposed | Recommended 1.0 status | Follow-up | +| --- | --- | --- | --- | +| `exceptions` | package attribute / star import | Accidentally exposed | Yes — dedicated cleanup issue | +| `warnings` | package attribute / star import | Accidentally exposed | Yes — dedicated cleanup issue | +| `mlb_api` | package attribute / star import | Accidentally exposed | Yes — dedicated cleanup issue | +| `mlb_dataadapter` | package attribute / star import | Accidentally exposed | Yes — dedicated cleanup issue | +| `mlb_module` | package attribute / star import | Accidentally exposed | Yes — dedicated cleanup issue | +| `models` | package attribute / star import | Accidentally exposed | Yes — dedicated cleanup issue | + +These names are not documented as public imports. Prefer the explicit +package-root symbols above. Do not remove them from star imports in a patch +release without an approved issue; wildcard callers may currently receive them. + +### Why `__all__` is not defined + +Version 1.0 intentionally omits `__all__`. + +Without `__all__`, `from mlbstatsapi import *` currently includes both the +supported symbols and the accidentally exposed submodule names listed above. + +Adding `__all__` that lists only the supported symbols would silently change +wildcard-import behavior by removing those submodule names. Adding them to +`__all__` would incorrectly promote accidental exposure into the supported +surface. + +A future focused issue may introduce `__all__` after deciding how to treat the +accidental submodule names (for example, a documented deprecation period). + +## Primary client + +`Mlb` is the primary synchronous client. + +### Constructor + +```text +Mlb( + hostname="statsapi.mlb.com", + logger=None, + timeout=(3.05, 30.0), + session=None, + *, + strict_http=True, +) +``` + +Stable constructor rules: + +* Parameter order above is part of the API +* Default values above are part of the API +* `strict_http` is keyword-only +* Version 1.0 defaults `strict_http` to `True` +* Pass `strict_http=False` for the historical empty-result compatibility path + on final non-404 4xx responses + +Private attributes such as `_session`, `_owns_session`, `_mlb_adapter_v1`, and +`_mlb_adapter_v1_1` are **not** public API. + +### Context-manager behavior + +```python +with mlbstatsapi.Mlb() as mlb: + person = mlb.get_person(664034) +``` + +* `Mlb.__enter__` returns `self` +* `Mlb.__exit__` calls `close()` +* Repeated `close()` calls are safe +* Library-owned Sessions are closed +* Caller-injected Sessions are not closed + +### API versions used by `Mlb` + +`Mlb` constructs internal adapters for both `v1` and `v1.1` that share one +Session. Most endpoint methods use `v1`. `get_game` uses the `v1.1` live feed +endpoint. Standalone `MlbDataAdapter(ver="v1")` and +`MlbDataAdapter(ver="v1.1")` remain supported. + +## Low-level adapter + +`MlbDataAdapter` is the public low-level HTTP adapter. + +### Constructor + +```text +MlbDataAdapter( + hostname="statsapi.mlb.com", + ver="v1", + logger=None, + timeout=(3.05, 30.0), + session=None, + *, + strict_http=True, +) +``` + +Stable constructor rules: + +* Parameter order above is part of the API +* Default values above are part of the API +* `strict_http` is keyword-only +* Version 1.0 defaults `strict_http` to `True` +* `ver` supports the library's documented API versions, including `v1` and + `v1.1` + +`MlbDataAdapter` exposes `get()` and `close()`. It does not implement the +context-manager protocol in version 1.0; callers should call `close()` +explicitly when they own a standalone adapter. + +## Result object + +```text +MlbResult( + status_code, + message, + data=None, +) +``` + +Stable public attributes: + +* `status_code` — coerced to `int` +* `message` — coerced to `str` +* `data` — a dictionary; defaults to `{}` when `data` is omitted or `None` + +Stable behaviors already covered by offline tests: + +* Caller-provided dictionaries are not mutated +* Each instance gets an independent `data` dictionary +* A top-level `"copyright"` key is removed from the stored `data` copy + +## Retry policy + +```text +create_retry_policy() +``` + +Stable factory contract: + +* Takes no arguments +* Returns `urllib3.util.retry.Retry` +* Each call returns a new instance +* Callers may mount the returned policy on their own `requests.Session` +* The library does not automatically modify injected Sessions + +Current numeric configuration (also asserted by the HTTP contract tests and +treated as stable for 1.x unless a future major release documents otherwise): + +```text +total=3 +connect=3 +read=2 +status=3 +backoff_factor=0.5 +status_forcelist={429, 500, 502, 503, 504} +allowed_methods={"GET"} +respect_retry_after_header=True +raise_on_status=False +``` + +## Exception hierarchy + +```text +Exception +└── TheMlbStatsApiException + ├── MlbTransportError + │ └── MlbTimeoutError + ├── MlbHttpError + └── MlbDecodeError +``` + +Supported catch patterns: + +* Broad package failures: `except TheMlbStatsApiException` +* Transport failures: `except MlbTransportError` +* Timeouts: `except MlbTimeoutError` +* HTTP failures: `except MlbHttpError` +* JSON decode failures: `except MlbDecodeError` + +### `MlbHttpError` stable attributes + +* `status_code` +* `reason` +* `url` +* `method` +* `response_data` +* `body_excerpt` + +Exact `str(exc)` formatting beyond the currently tested +`"{status_code}: {reason}"` shape for `MlbHttpError` is not frozen as a broader +string-formatting promise for every exception type. + +## Compatibility warning + +```text +issubclass(MlbHttpCompatibilityWarning, FutureWarning) +``` + +remains true. + +`MlbHttpCompatibilityWarning` is emitted when `strict_http=False` suppresses a +final non-404 4xx response that strict mode would raise. Warning message text +may be refined for clarity within 1.x as long as the warning class and +filtering behavior remain compatible. See [HTTP transport](http-transport.md). + +## Session ownership + +These ownership rules are stable public API: + +```text +Library-created Session + Owned by the library + Receives library retry adapters + Receives the package User-Agent + Closed by Mlb.close(), adapter.close(), or Mlb context-manager exit + +Caller-injected Session + Owned by the caller + Existing headers remain untouched + Existing adapters remain untouched + Not closed by the library +``` + +## Python support + +| Claim | Value | +| --- | --- | +| Minimum declared Python version (`Requires-Python`) | `>=3.10` | +| CI-validated versions | 3.10, 3.11, 3.12, 3.13, 3.14 | +| Later Python versions | May work, but are not claimed as CI-validated unless added to the matrix | + +The minimum declared Python version is 3.10 and the CI-validated versions are +3.10 through 3.14. Every version in that range runs the deterministic offline +suite on each pull request and push to a watched branch. Prerelease +interpreters are deliberately excluded from the required matrix and are not +claimed as supported until they reach a stable release. + +Version 1.0 does not add an upper Python bound, and the declared runtime +requirement stays `>=3.10`. Adding a new interpreter is a compatible change: +extend the CI matrix and update this table in the same pull request. + +## Mlb endpoint methods + +The following public methods are defined directly on `Mlb`. Newly exposed +methods require an intentional update to the public API tests. + +Lifecycle and context managers: + +| Method | Signature notes | +| --- | --- | +| `close` | no parameters | +| `__enter__` | returns `self` | +| `__exit__` | closes only library-owned Sessions | + +Endpoint methods (parameter order and defaults are part of the API): + +| Method | Parameters | Top-level return shape | 404 / client-empty shape | +| --- | --- | --- | --- | +| `get_people` | `sport_id=1, **params` | `list[Person]` | `[]` | +| `get_person` | `player_id, **params` | `Person \| None` | `None` | +| `get_persons` | `person_ids, **params` | `list[Person]` | `[]` | +| `get_people_id` | `fullname, sport_id=1, search_key='fullName', **params` | `list[int]` | `[]` | +| `get_teams` | `sport_id=1, **params` | `list[Team]` | `[]` | +| `get_team` | `team_id, **params` | `Team \| None` | `None` | +| `get_team_id` | `team_name, search_key='name', **params` | `list[int]` | `[]` | +| `get_team_roster` | `team_id, **params` | `list[Player]` | `[]` | +| `get_team_coaches` | `team_id, **params` | `list[Coach]` | `[]` | +| `get_schedule` | `date=None, start_date=None, end_date=None, sport_id=1, team_id=None, **params` | `Schedule \| None` | `None` | +| `get_scheduled_games_by_date` | `date=None, start_date=None, end_date=None, sport_id=1, **params` | `list[ScheduleGames]` | `[]` | +| `get_game` | `game_id, **params` | `Game \| None` | `None` (uses `v1.1`) | +| `get_game_play_by_play` | `game_id, **params` | `Plays \| None` | `None` | +| `get_game_line_score` | `game_id, **params` | `Linescore \| None` | see notes | +| `get_game_box_score` | `game_id, **params` | `BoxScore \| None` | `None` | +| `get_game_ids` | `date=None, start_date=None, end_date=None, sport_id=1, **params` | `list[int]` | `[]` | +| `get_gamepace` | `season, sport_id=1, **params` | `GamePace \| None` | `None` | +| `get_venue` | `venue_id, **params` | annotated `Venue \| None` | returns `[]` today; see notes | +| `get_venues` | `**params` | `list[Venue]` | `[]` | +| `get_venue_id` | `venue_name, search_key='name', **params` | `list[int]` | `[]` | +| `get_sport` | `sport_id, **params` | `Sport \| None` | `None` | +| `get_sports` | `**params` | `list[Sport]` | `[]` | +| `get_sport_id` | `sport_name, search_key='name', **params` | `list[int]` | `[]` | +| `get_league` | `league_id, **params` | `League \| None` | `None` | +| `get_leagues` | `**params` | `list[League]` | `[]` | +| `get_league_id` | `league_name, search_key='name', **params` | `list[int]` | `[]` | +| `get_division` | `division_id, **params` | `Division \| None` | `None` | +| `get_divisions` | `**params` | `list[Division]` | `[]` | +| `get_division_id` | `division_name, search_key='name', **params` | `list[int]` | `[]` | +| `get_season` | `season_id, sport_id=1, **params` | annotated `Season`; may return `None` | `None` | +| `get_seasons` | `sport_id=1, **params` | `list[Season]` | `[]` | +| `get_standings` | `league_id, season, **params` | `list[Standings]` | `[]` | +| `get_attendance` | `team_id=None, league_id=None, league_list_id=None, **params` | `Attendance \| None` | `None` | +| `get_draft` | `year_id, **params` | `list[Round]` | `[]` | +| `get_awards` | `award_id, **params` | `list[Award]` | `[]` | +| `get_homerun_derby` | `game_id, **params` | `HomeRunDerby \| None` | see notes | +| `get_team_stats` | `team_id, stats, groups, **params` | `dict` | `{}` | +| `get_players_stats_for_game` | `person_id, game_id, **params` | `dict` | `{}` | +| `get_player_stats` | `person_id, stats, groups, **params` | `dict` | `{}` | +| `get_stats` | `stats, groups, **params` | `dict` | `{}` | + +Notes and known conflicts (documented, not redesigned by this contract): + +* Under the version 1.0 strict default, final non-404 4xx responses raise + `MlbHttpError` before endpoint empty-shape logic runs. The empty shapes above + remain the documented domain-level not-found / empty results for **404** + responses (and for compatibility mode where applicable). +* `get_game_line_score` does not currently short-circuit on a 400–499 status + the same way as sibling game helpers; missing linescore data falls through to + an implicit `None`. +* `get_venue` is annotated to return `Venue | None` but currently returns `[]` + on 400–499 statuses. Treat the implementation shape as the observed behavior + until a focused fix lands. +* `get_homerun_derby` currently executes a bare `None` expression on 400–499 + instead of `return None`, so execution may continue. A focused bugfix is + recommended. +* Nested Pydantic model fields are not frozen by this contract. + +## Return-contract boundaries + +Version 1.0 guarantees endpoint method availability, parameter order and +defaults, top-level return types or shapes listed above, and documented 404 +empty shapes. + +Version 1.0 does **not** guarantee: + +* Every nested model field +* Every upstream JSON property +* Undocumented behavior caused by malformed upstream data +* Exact log messages +* Exact exception string formatting beyond documented attributes +* The availability or stability of the unofficial MLB Stats API itself + +## Internal APIs + +The following are outside the 1.0 stability promise: + +* Private names beginning with an underscore +* Internal adapter helpers such as `_configure_library_session`, + `_build_http_error`, and `_warn_http_compatibility` +* Private `Mlb` attributes such as `_session` or `_mlb_adapter_v1` +* Exact log messages +* Exact exception string formatting beyond documented attributes +* Undocumented upstream MLB response fields +* The availability or stability of the unofficial MLB API itself +* Every symbol located in `mlbstatsapi.models` unless separately documented +* Accidentally exposed package-root submodule names listed above + +Do not treat every Pydantic model field as permanently frozen. + +## Legacy helpers + +`return_splits` and `get_stat_attributes` remain importable from the package +root and are stable in 1.x for existing callers. + +They are not the preferred entry point for new application code. Prefer the +`Mlb` statistics endpoint methods. These helpers are **not** deprecated in +version 1.0. + +## Deprecation policy + +No new deprecations are introduced by the version 1.0 public API audit. + +A future deprecation must include: + +1. A documented replacement +2. A warning strategy +3. A removal timeline +4. A separate focused issue + +## Semantic-versioning expectations + +After 1.0.0: + +* Preserve supported package-root imports across minor and patch releases +* Prefer additive changes for new endpoints and optional parameters +* Use a major version for removals, renames, or incompatible contract changes +* Update this document when the supported surface intentionally changes diff --git a/docs/releases/1.0.0.md b/docs/releases/1.0.0.md new file mode 100644 index 0000000..7a6ae61 --- /dev/null +++ b/docs/releases/1.0.0.md @@ -0,0 +1,283 @@ +# python-mlb-statsapi 1.0.0 + +Version 1.0.0 is the stable HTTP contract release. + +Version 0.8.0 made the network layer reliable. Version 0.9.0 made HTTP behavior +configurable and introduced a warning-backed migration path toward stricter +failures. Version 1.0.0 completes that migration: strict HTTP handling is now +the default, while an explicit compatibility opt-out remains available for +callers who need more time to migrate. + +The primary breaking change is that `Mlb()` and `MlbDataAdapter()` now default +to `strict_http=True`. A final non-404 4xx response raises `MlbHttpError` +instead of returning the historical empty result. + +Endpoint-specific 404 behavior, Session ownership, retry values, structured +exceptions, and the synchronous public client remain unchanged. + +## Breaking change + +### Strict HTTP behavior is now the default + +In version 0.9.x, these constructions were equivalent and kept compatibility +mode: + +```python +import mlbstatsapi + +mlb = mlbstatsapi.Mlb() +mlb = mlbstatsapi.Mlb(strict_http=False) +``` + +In version 1.0.0, the default matches explicit strict handling: + +```python +import mlbstatsapi + +mlb = mlbstatsapi.Mlb() +mlb = mlbstatsapi.Mlb(strict_http=True) +``` + +Default behavior for a final response: + +```text +Successful 2xx + Return the normal endpoint result + +Final non-404 4xx + Raise MlbHttpError + +404 + Preserve endpoint-specific None, [], or {} behavior + +Final 5xx + Raise MlbHttpError + +Timeout + Raise MlbTimeoutError + +Transport failure + Raise MlbTransportError + +Successful invalid JSON + Raise MlbDecodeError +``` + +"Final" means the response remaining after the bounded retry policy has +completed. + +## Highlights + +### Endpoint-specific 404 behavior is preserved + +A 404 still follows the existing per-endpoint contract. Depending on the +endpoint, a missing resource may return: + +```text +None +[] +{} +``` + +Not every 404 raises `MlbHttpError`. The strict default does not change that. + +### Explicit compatibility mode remains available + +Callers who need the historical empty-result path can opt out temporarily: + +```python +import mlbstatsapi + +with mlbstatsapi.Mlb(strict_http=False) as mlb: + player = mlb.get_person(664034) +``` + +Compatibility mode: + +* Preserves the historical empty result for final non-404 4xx responses +* Emits `MlbHttpCompatibilityWarning` exactly once per suppressed final response +* Does not change 404 behavior +* Does not suppress final 5xx errors +* Does not alter timeout, transport, or decode failures +* Runs only after retry exhaustion + +It is a temporary migration opt-out and an explicit request for historical 0.9 +behavior. It is not the recommended long-term 1.0 configuration. + +Compatibility warnings point to the public caller frame outside the package +namespace. Messages include the status code and request URL, state that +`strict_http=False` selected compatibility mode, note that the historical empty +result was returned, identify strict handling as the version 1.0 default, and +explain how to receive `MlbHttpError` instead. Response bodies, headers, +credentials, cookies, and tokens are never included. + +### Stable public API contract + +Version 1.0.0 establishes the stable public API contract documented in +[public-api.md](../public-api.md). That document is the authoritative +classification for package-root imports, constructor signatures, the exception +hierarchy, Session ownership, documented endpoint methods, Python support, and +internal or private APIs. + +This release does not freeze every upstream MLB response field, every class +under `mlbstatsapi.models`, or private underscore-prefixed names, and it does +not promise that the unofficial MLB API itself will never change. + +### Session ownership remains explicit + +```text +Library-created Session + Configured and closed by the library + Receives retry adapters + Receives the package User-Agent + +Caller-injected Session + Configured and closed by the caller + Existing adapters remain untouched + Existing headers remain untouched +``` + +Ownership rules are unchanged from versions 0.8.0 and 0.9.0. The new strict +default does not change Session creation, injection, or cleanup behavior. + +### Retry behavior remains bounded + +Library-created Sessions continue to retry temporary GET failures for: + +```text +429 +500 +502 +503 +504 +``` + +```text +Initial request: 1 +Maximum retries: 3 +Maximum total attempts: 4 +Backoff factor: 0.5 +Retry-After respected: yes +``` + +Retry values are unchanged. Ordinary client errors such as 400, 401, 403, and +404 are not retried. Invalid JSON and Pydantic validation failures are not +retried. The new strict default is evaluated only after retries are exhausted. + +### Structured exceptions + +The exception hierarchy is unchanged: + +```text +TheMlbStatsApiException +├── MlbTransportError +│ └── MlbTimeoutError +├── MlbHttpError +└── MlbDecodeError +``` + +`MlbHttpError` continues to expose `method`, `status_code`, `reason`, `url`, +`response_data`, and `body_excerpt`. Broad catches of +`TheMlbStatsApiException` remain valid. + +### Testing and release validation + +Deterministic offline coverage documents the version 1.0 HTTP contract, +including the strict default, explicit compatibility mode, warning behavior, +404 return shapes, Session ownership, and retry exhaustion. + +`scripts/validate_release.py` is the packaging check for the built artifacts. It +clean-installs the wheel and the source distribution into separate throwaway +virtual environments and runs the same installed-package smoke test against +each, so a broken sdist build, a missing runtime dependency, or an omitted +package file cannot hide behind a working wheel. + +Against the installed artifact the smoke test verifies: + +```text +Declared metadata matches the built version +Supported package-root imports resolve +Strict HTTP handling is the default for Mlb() and MlbDataAdapter() +A final 403 raises MlbHttpError with status, reason, method, URL, and payload +strict_http=False returns the historical empty result and warns exactly once +A library-created Session carries the versioned User-Agent and retry policy +A caller-injected Session keeps its headers, adapters, and ownership +``` + +Every response the smoke test observes is produced by an injected fake Session, +so release validation never contacts the MLB API. Continuous integration builds +the artifacts, runs the validator, and runs `twine check` on both artifacts. No +ordinary pull request or push path publishes anything. + +### Python support + +| Claim | Value | +| --- | --- | +| Minimum declared Python version (`Requires-Python`) | `>=3.10` | +| CI-validated versions | 3.10, 3.11, 3.12, 3.13, 3.14 | + +The minimum declared Python version is 3.10 and the CI-validated versions are +3.10 through 3.14. Version 1.0.0 adds no upper Python bound and does not change +the declared runtime requirement. Prerelease interpreters are excluded from the +required matrix and are not claimed as supported. + +## Migration guidance + +Recommended process when upgrading from 0.9.x: + +1. Identify code that relied on empty results for failed non-404 4xx responses +2. Add handling for `MlbHttpError` +3. Distinguish 404 domain results from other HTTP failures +4. Use `strict_http=False` only where migration cannot happen immediately +5. Test warning-as-error configurations +6. Remove `strict_http=False` +7. Confirm injected Session and retry behavior remain correct + +Version 0.9-style temporary compatibility: + +```python +import mlbstatsapi + +with mlbstatsapi.Mlb(strict_http=False) as mlb: + player = mlb.get_person(664034) +``` + +Recommended 1.0 state: + +```python +import mlbstatsapi + +try: + with mlbstatsapi.Mlb() as mlb: + player = mlb.get_person(664034) +except mlbstatsapi.MlbHttpError as exc: + print(exc.status_code) + print(exc.url) +``` + +A missing person may still return `None` on a 404 and is not necessarily an +exception. + +Applications that treat warnings as exceptions should prefer removing +`strict_http=False` and catching `MlbHttpError`. A temporary targeted filter +on `MlbHttpCompatibilityWarning` is acceptable during migration; disabling +every warning or every `FutureWarning` is not recommended. + +## Documentation + +* [HTTP transport documentation](../http-transport.md) +* [Public API contract](../public-api.md) +* README upgrading section and HTTP behavior summary + +## Not included + +Version 1.0.0 does not add: + +* Async support +* Response caching +* New MLB endpoints +* Global rate limiting +* Strict handling for endpoint-specific 404 responses +* Automatic modification of injected Sessions +* Telemetry +* New retry values diff --git a/mlbstatsapi/__init__.py b/mlbstatsapi/__init__.py index 3a046df..bb3c21c 100644 --- a/mlbstatsapi/__init__.py +++ b/mlbstatsapi/__init__.py @@ -1,3 +1,15 @@ +"""python-mlb-statsapi public package root. + +Supported package-root symbols for the 1.x series are documented in +``docs/public-api.md``. + +``__all__`` is intentionally omitted in version 1.0. Adding it today would +change ``from mlbstatsapi import *`` by excluding submodule names that appear +in the package namespace as an import side effect. Those submodules are not +part of the supported public API; cleaning them up requires a separate +focused issue. See ``docs/public-api.md``. +""" + from .mlb_api import Mlb from .mlb_dataadapter import MlbDataAdapter, MlbResult, create_retry_policy from .exceptions import ( diff --git a/mlbstatsapi/mlb_api.py b/mlbstatsapi/mlb_api.py index 1e8a6d5..0f3384f 100644 --- a/mlbstatsapi/mlb_api.py +++ b/mlbstatsapi/mlb_api.py @@ -52,12 +52,14 @@ def __init__( timeout: TimeoutType = DEFAULT_TIMEOUT, session: requests.Session | None = None, *, - strict_http: bool = False, + strict_http: bool = True, ): # One session is shared by the v1 and v1.1 adapters. The library closes # only sessions it creates; caller-injected sessions remain caller-owned. # The versioned User-Agent and retry adapters are applied only to # library-created Sessions. + # strict_http defaults to True in version 1.0; pass False for the + # historical empty-result compatibility path on final non-404 4xx. self._owns_session = session is None if session is None: self._session = requests.Session() @@ -1042,7 +1044,7 @@ def get_game_line_score(self, game_id: int, **params) -> Union[Linescore, None]: Examples -------- >>> mlb = Mlb() - >>> mlb.get_game_line_scrore(662242) + >>> mlb.get_game_line_score(662242) Linescore """ @@ -1682,10 +1684,10 @@ def get_divisions(self, **params) -> List[Division]: return divisions - def get_division_id(self, division_name: str, - search_key: str = 'name', **params) -> List[Division]: + def get_division_id(self, division_name: str, + search_key: str = 'name', **params) -> List[int]: """ - return divsion id + return division id Parameters ---------- diff --git a/mlbstatsapi/mlb_dataadapter.py b/mlbstatsapi/mlb_dataadapter.py index 4bc66d1..8082896 100644 --- a/mlbstatsapi/mlb_dataadapter.py +++ b/mlbstatsapi/mlb_dataadapter.py @@ -8,6 +8,7 @@ MlbTransportError, ) from .warnings import MlbHttpCompatibilityWarning +import inspect import logging import warnings @@ -28,9 +29,33 @@ # Bounded excerpt for error response bodies attached to MlbHttpError. HTTP_ERROR_BODY_EXCERPT_LIMIT = 500 -# Frames from warnings.warn() out to whoever called MlbDataAdapter.get(), so the -# warning points at application code rather than the helper below. -COMPATIBILITY_WARNING_STACKLEVEL = 3 + +def _is_mlbstatsapi_module(module_name: str) -> bool: + """Return True when *module_name* belongs to this package.""" + return module_name == "mlbstatsapi" or module_name.startswith("mlbstatsapi.") + + +def _compatibility_warning_stacklevel() -> int: + """Return a warnings.warn stacklevel for the first non-package caller. + + A fixed stack level cannot serve both direct MlbDataAdapter.get() calls and + public Mlb endpoint methods that wrap the adapter. Walk frames from the + caller of this helper outward and stop at the first module outside the + mlbstatsapi package namespace. + """ + frame = inspect.currentframe() + stacklevel = 1 + try: + frame = frame.f_back + while frame is not None: + module_name = frame.f_globals.get("__name__", "") + if not _is_mlbstatsapi_module(module_name): + return stacklevel + stacklevel += 1 + frame = frame.f_back + finally: + del frame + return 1 def _warn_http_compatibility( @@ -45,13 +70,14 @@ def _warn_http_compatibility( """ warnings.warn( ( - f"HTTP {status_code} for {url} was handled through compatibility mode " - "and returned the historical empty result. Pass strict_http=True to " - "raise MlbHttpError. This compatibility behavior may change in " - "version 1.0." + f"HTTP {status_code} for {url} was suppressed because " + "strict_http=False explicitly selected compatibility mode, so the " + "historical empty result was returned. Strict HTTP behavior is the " + "default in version 1.0. Remove strict_http=False or pass " + "strict_http=True to raise MlbHttpError." ), MlbHttpCompatibilityWarning, - stacklevel=COMPATIBILITY_WARNING_STACKLEVEL, + stacklevel=_compatibility_warning_stacklevel(), ) @@ -247,8 +273,10 @@ def __init__( timeout: TimeoutType = DEFAULT_TIMEOUT, session: requests.Session | None = None, *, - strict_http: bool = False, + strict_http: bool = True, ): + # strict_http defaults to True in version 1.0; pass False for the + # historical empty-result compatibility path on final non-404 4xx. self.url = f'https://{hostname}/api/{ver}/' self._logger = logger or logging.getLogger(__name__) self._timeout = timeout diff --git a/poetry.lock b/poetry.lock index 52de79e..ac3da90 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2,24 +2,39 @@ [[package]] name = "annotated-types" -version = "0.7.0" +version = "0.8.0" description = "Reusable constraint types to use with typing.Annotated" optional = false +python-versions = ">=3.10" +files = [ + {file = "annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0"}, + {file = "annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7"}, +] + +[[package]] +name = "backports-tarfile" +version = "1.2.0" +description = "Backport of CPython tarfile module" +optional = false python-versions = ">=3.8" files = [ - {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, - {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, + {file = "backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34"}, + {file = "backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991"}, ] +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["jaraco.test", "pytest (!=8.0.*)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)"] + [[package]] name = "build" -version = "1.4.0" +version = "1.5.0" description = "A simple, correct Python build frontend" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "build-1.4.0-py3-none-any.whl", hash = "sha256:6a07c1b8eb6f2b311b96fcbdbce5dab5fe637ffda0fd83c9cac622e927501596"}, - {file = "build-1.4.0.tar.gz", hash = "sha256:f1b91b925aa322be454f8330c6fb48b465da993d1e7e7e6fa35027ec49f3c936"}, + {file = "build-1.5.0-py3-none-any.whl", hash = "sha256:13f3eecb844759ab66efec90ca17639bbf14dc06cb2fdf37a9010322d9c50a6f"}, + {file = "build-1.5.0.tar.gz", hash = "sha256:302c22c3ba2a0fd5f3911918651341ebb3896176cbdec15bd421f80b1afc7647"}, ] [package.dependencies] @@ -30,140 +45,233 @@ pyproject_hooks = "*" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} [package.extras] +keyring = ["keyring"] uv = ["uv (>=0.1.18)"] -virtualenv = ["virtualenv (>=20.11)", "virtualenv (>=20.17)", "virtualenv (>=20.31)"] +virtualenv = ["virtualenv (>=20.17)", "virtualenv (>=20.31)"] [[package]] name = "certifi" -version = "2026.1.4" +version = "2026.7.22" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.7" files = [ - {file = "certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c"}, - {file = "certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120"}, + {file = "certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775"}, + {file = "certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55"}, ] +[[package]] +name = "cffi" +version = "2.1.1" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.10" +files = [ + {file = "cffi-2.1.1-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:baed1e86cc735622097354b9d1281406caf42ff42a886d29faa8e8d1630333be"}, + {file = "cffi-2.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ca82be1a1d406ecfe1d25dc16cb33488e5a16bf4438c9fb590484ea29d92478b"}, + {file = "cffi-2.1.1-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:42e2f76b9455f5a9a844f770bf3e200ed3da0e15f5df3db9c31fe80b04b3d004"}, + {file = "cffi-2.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5a59cc1c4442bc3d5c703bf720b51138d0bfc173618807c9ee2490a7541dd3d9"}, + {file = "cffi-2.1.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:9f8d177621de5cb38ee3e731eda45d421db093ec0739f46a5594babda7987a98"}, + {file = "cffi-2.1.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:75f80557d1389eddbd0de2681f6a390a0c5338c31ddaa821381c203fc3fd50d9"}, + {file = "cffi-2.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:194cffa889098ced9976c3fc6340305e43f6303657d298da55366907c05c22d6"}, + {file = "cffi-2.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5bb4e7ea95dcd6a014a6fef62e62467d67d8e582326443f3d68e71d6320a9fcf"}, + {file = "cffi-2.1.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3d22a20b1fb1632cc72c22f95f7b0d2961c3e1c235f245ba4c606c4771035659"}, + {file = "cffi-2.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1dea0e4d7d4f11f619fe8c1d76caf49e24405b4b5743c0e3be16a500ecd930c9"}, + {file = "cffi-2.1.1-cp310-cp310-win32.whl", hash = "sha256:7ce713ace7c0e4520535b42b77eaa742c16dab813978064913e5a3cf82973b41"}, + {file = "cffi-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a48d62ab9d6f4f98c983223a547af44be6ca3691074c31cecced6facd3ba2dc1"}, + {file = "cffi-2.1.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:c8d2c9fd1f2d16f780d15127abb050d13d1a76c03a4bd87d7e4980e45e511e12"}, + {file = "cffi-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:398aff33cee2767e3e781d2554c54bd0dff386bb437581e0d8011fde1a942ec1"}, + {file = "cffi-2.1.1-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:154852545011f779917b11c78db2358d095da62a9a172b78ad0a583ee5adc0d0"}, + {file = "cffi-2.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3311ed60d36f83378794e1009ac6258bafbf81f7888b4caa7b35a521e3f95813"}, + {file = "cffi-2.1.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6e192623c49c94421616a5778fba35cf0d5a8d000650c1967ef4448ee5cdd990"}, + {file = "cffi-2.1.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a6e721d4b0e45d5b65e87534470e67b18dcd092c83f68fba09f152b9cbc061af"}, + {file = "cffi-2.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:34e261f78cb6ceaaa36f42f2613f4380d94d9c759a9c73c769ee6e0247364632"}, + {file = "cffi-2.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7225e4514edb64eb6740324353e0da0711954fd8d7da4576755b1c6e09b697cd"}, + {file = "cffi-2.1.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:df913725b79db7bcf03448f36b7bf8815363417d5b58deecf9305e3e30f0f21a"}, + {file = "cffi-2.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f5cfbc5fe74540d335175b656c725d74d90e3730c626d92575eea35029d9afaa"}, + {file = "cffi-2.1.1-cp311-cp311-win32.whl", hash = "sha256:f8ec5e643a9a937f64e1999eb9f75d072263751912dc5cd06d3c85f8f44be7c3"}, + {file = "cffi-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:42f6930c31dc7f50732c9ae793c2786c7b6b044195967bbdde40bb9be81c4cc0"}, + {file = "cffi-2.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:c7659f22557c5a0bc4855cd635f55edec690cc008a40768527762cb9fb263455"}, + {file = "cffi-2.1.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0"}, + {file = "cffi-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf"}, + {file = "cffi-2.1.1-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a"}, + {file = "cffi-2.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890"}, + {file = "cffi-2.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50"}, + {file = "cffi-2.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e"}, + {file = "cffi-2.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf"}, + {file = "cffi-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517"}, + {file = "cffi-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735"}, + {file = "cffi-2.1.1-cp312-cp312-win32.whl", hash = "sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e"}, + {file = "cffi-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a"}, + {file = "cffi-2.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80"}, + {file = "cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e"}, + {file = "cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c"}, + {file = "cffi-2.1.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6"}, + {file = "cffi-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971"}, + {file = "cffi-2.1.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c"}, + {file = "cffi-2.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125"}, + {file = "cffi-2.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264"}, + {file = "cffi-2.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3"}, + {file = "cffi-2.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2"}, + {file = "cffi-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b"}, + {file = "cffi-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7"}, + {file = "cffi-2.1.1-cp313-cp313-win32.whl", hash = "sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac"}, + {file = "cffi-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d"}, + {file = "cffi-2.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973"}, + {file = "cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c"}, + {file = "cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb"}, + {file = "cffi-2.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54"}, + {file = "cffi-2.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72"}, + {file = "cffi-2.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1"}, + {file = "cffi-2.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062"}, + {file = "cffi-2.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03"}, + {file = "cffi-2.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96"}, + {file = "cffi-2.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527"}, + {file = "cffi-2.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13"}, + {file = "cffi-2.1.1-cp314-cp314-win32.whl", hash = "sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c"}, + {file = "cffi-2.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48"}, + {file = "cffi-2.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836"}, + {file = "cffi-2.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3"}, + {file = "cffi-2.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2"}, + {file = "cffi-2.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94"}, + {file = "cffi-2.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc"}, + {file = "cffi-2.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29"}, + {file = "cffi-2.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676"}, + {file = "cffi-2.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e"}, + {file = "cffi-2.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f"}, + {file = "cffi-2.1.1-cp314-cp314t-win32.whl", hash = "sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4"}, + {file = "cffi-2.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e"}, + {file = "cffi-2.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5"}, + {file = "cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d"}, + {file = "cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b"}, + {file = "cffi-2.1.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4"}, + {file = "cffi-2.1.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8"}, + {file = "cffi-2.1.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6"}, + {file = "cffi-2.1.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80"}, + {file = "cffi-2.1.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779"}, + {file = "cffi-2.1.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399"}, + {file = "cffi-2.1.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688"}, + {file = "cffi-2.1.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7"}, + {file = "cffi-2.1.1-cp315-cp315-win32.whl", hash = "sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac"}, + {file = "cffi-2.1.1-cp315-cp315-win_amd64.whl", hash = "sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960"}, + {file = "cffi-2.1.1-cp315-cp315-win_arm64.whl", hash = "sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1"}, + {file = "cffi-2.1.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc"}, + {file = "cffi-2.1.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab"}, + {file = "cffi-2.1.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e"}, + {file = "cffi-2.1.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358"}, + {file = "cffi-2.1.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231"}, + {file = "cffi-2.1.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6"}, + {file = "cffi-2.1.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94"}, + {file = "cffi-2.1.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5"}, + {file = "cffi-2.1.1-cp315-cp315t-win32.whl", hash = "sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66"}, + {file = "cffi-2.1.1-cp315-cp315t-win_amd64.whl", hash = "sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3"}, + {file = "cffi-2.1.1-cp315-cp315t-win_arm64.whl", hash = "sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692"}, + {file = "cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be"}, +] + +[package.dependencies] +pycparser = {version = "*", markers = "implementation_name != \"PyPy\""} + [[package]] name = "charset-normalizer" -version = "3.4.4" +version = "3.4.9" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7" files = [ - {file = "charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-win32.whl", hash = "sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-win32.whl", hash = "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-win_arm64.whl", hash = "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50"}, - {file = "charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f"}, - {file = "charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a"}, + {file = "charset_normalizer-3.4.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd6280cf040f233bd7d3407b743b4b4c74f70e8e1c4199cb112a62c941c0772a"}, + {file = "charset_normalizer-3.4.9-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616"}, + {file = "charset_normalizer-3.4.9-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c1225416b463483160e4af85d5fc3a9690ccb53fd4b1865a6437825f5ede3209"}, + {file = "charset_normalizer-3.4.9-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:16d10d789dd9bcca1173c95af82c58433122564b7bc39385124be735a35cbe99"}, + {file = "charset_normalizer-3.4.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bb41182d93ea91f60b4bc8fbf4c820c69ef8a12ab2d917f3f1834f1acad07e8"}, + {file = "charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:bcf74c1df76758a395bf0af608c04c82257523f55c9868b334f06270d0f2112b"}, + {file = "charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b5314963fce9b0b12743891de876e724997864ee22aa496f903f426c7e2fa5b2"}, + {file = "charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e9701d0049d92c16703a42771b98d560b95248949f23f8cf7b4eddd201814fb9"}, + {file = "charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:65a7ff3f705e57d392f7261b6d0550fe137c3019477431f1c355e0db0a7d3e15"}, + {file = "charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79580094b00d1789d1f93ea55bc43cb2f611910c72235b7657f3482ddcc1b22d"}, + {file = "charset_normalizer-3.4.9-cp310-cp310-win32.whl", hash = "sha256:432786d3561e69aeeae6c7e8648964ce0ad05736120135601f87ac26b9c83381"}, + {file = "charset_normalizer-3.4.9-cp310-cp310-win_amd64.whl", hash = "sha256:8c041122946b7ba21bb32c45b1aa57b1be35527690aeb3c5c234521085632eee"}, + {file = "charset_normalizer-3.4.9-cp310-cp310-win_arm64.whl", hash = "sha256:375b83ed0aecfce76c16d198fbc21f3b11b337d68662bea0a995046682a11419"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-win32.whl", hash = "sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-win32.whl", hash = "sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-win32.whl", hash = "sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-win32.whl", hash = "sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-win32.whl", hash = "sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:253a4a220747e8b5faf57ec320c4f5efb0cef05f647420bf267143ec15dba10a"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:68ce9f4d6b26d5ccbf7fd4459bf75f74a0a146677ebba80597df60cbdb20e6f4"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:58150c9f9b9a552505912d182ccdf26f6396fb6094816ceebcbb20eecabaed94"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:df7276909358e5635ae203673ab7e509ddd224225a8d6b0790bf13eb2bde1cc5"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3c09a49d6cde137258beb3d551994a2927fd35ad5cf96aed573f61bbd67c5f84"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:231ddcbb35e2ff8973e1365db41fe0572662893b99a05deb183b68ad4c0c8bd4"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:920079c3f7456fa213e0829ed2073aaa727fd39d889ead5b4f35d0de5460d04f"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0fa1aec2d32bcc03c8fa0f6f1712caad1adc38509f31142112e5c9daf5b9c833"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:ad41ba96094304aa090f5a30cb6e4fb3b3f1c264c523394b4c39bbacc4dc92ba"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:43b9e366a31fdd1c87d0eb08f579b4a82b723ea54338f040d6b4e518a026ea29"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-win32.whl", hash = "sha256:93d59d504b230e83c7a843251681959a0b6a9cd76f6e146ce1b8a80eb8739af9"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-win_amd64.whl", hash = "sha256:ddf4af30b417d9fe16481e9b81c27ab2a7cde1ff7ba3e85653b02db7d145dc7b"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-win_arm64.whl", hash = "sha256:476743fe6dfe14a2da12e3ac79125dc84a3b2cf8094369a47a1529b0cd8549fe"}, + {file = "charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5"}, + {file = "charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b"}, ] [[package]] @@ -177,6 +285,79 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "cryptography" +version = "50.0.0" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = "!=3.9.0,!=3.9.1,>=3.9" +files = [ + {file = "cryptography-50.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:031e2d5dd4bb9caa3ca9c82e5a197fd8ae680232cee62603d1a813f3f07e3d03"}, + {file = "cryptography-50.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645"}, + {file = "cryptography-50.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7"}, + {file = "cryptography-50.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3"}, + {file = "cryptography-50.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f"}, + {file = "cryptography-50.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae"}, + {file = "cryptography-50.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a"}, + {file = "cryptography-50.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987"}, + {file = "cryptography-50.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169"}, + {file = "cryptography-50.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f"}, + {file = "cryptography-50.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105"}, + {file = "cryptography-50.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef"}, + {file = "cryptography-50.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:bd1c592e4d5974f0d08d4888e432157adba757c66da0246918e43677fafa2d30"}, + {file = "cryptography-50.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:49e7d93abdbd2990caced757e5fade25302f719c3c8fb6e6fff2dde98999fc41"}, + {file = "cryptography-50.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:19736989797678c6af1e55cd49055cdbcb55d8f6b5583ac5335f933aba9101dc"}, + {file = "cryptography-50.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80b63928fa35083b33966ce1efb70e5b9607181e49dcd1c22c8c005e319f667f"}, + {file = "cryptography-50.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:d58c3db7cd6eed54e6c06744db55456b65ebd7492ddeae9c1e93cfca7aa857d3"}, + {file = "cryptography-50.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:df2a58a472f332225671c35b0a830208b86d004f82baa8530fa3782c85646533"}, + {file = "cryptography-50.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11b74db56cdbe3cdee6e3f6982ecb70334fa10dce99ed58bf7894aaaa3b2a037"}, + {file = "cryptography-50.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f59e38625469987d7ef6d495323c55e7db6c212eaf6112267e0d3b565a2e9c9f"}, + {file = "cryptography-50.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ecfed7367f965a0328cfbdd70da860f15441f002f613185668c6e6ebf5a0ac11"}, + {file = "cryptography-50.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9aa87839c383bdbab6ef865787a1fb877af8dd03464c4400322726feaaadfc6d"}, + {file = "cryptography-50.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:6ba6a53445bd3cfa809ef3ef5f1589aa6ba08784a1d962bf47d0940e871dab1c"}, + {file = "cryptography-50.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3f5735ffe4996d28b809371756219f5354864902a3b9e7c0b9ee87041209fc9c"}, + {file = "cryptography-50.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1b4a266766514614f8aa60416e71f2fc6e575d36e7bdc90f644fadb2f4b75b95"}, + {file = "cryptography-50.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:12b9c6996425c76ea6c457ace4f3073e715b8c545add07cd1a8f3a4f90691269"}, + {file = "cryptography-50.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ccdc4a71a4dabae05de219404f9f4abc38e3b58422177ff93d0da05967dafa07"}, + {file = "cryptography-50.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3"}, + {file = "cryptography-50.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f"}, + {file = "cryptography-50.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5"}, + {file = "cryptography-50.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f"}, + {file = "cryptography-50.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025"}, + {file = "cryptography-50.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a"}, + {file = "cryptography-50.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b"}, + {file = "cryptography-50.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708"}, + {file = "cryptography-50.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47"}, + {file = "cryptography-50.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9"}, + {file = "cryptography-50.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7"}, + {file = "cryptography-50.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:d24fead1d4d076e1bfb006dcec392074a3cd8d7b4fc8a595aa64073b2b7a96ba"}, + {file = "cryptography-50.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5e34edd123674534acd70147f0ca331eaa2c74e6325fb2028c886aa26ba0b68c"}, + {file = "cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:8eb5e1172eb569ea8a872796576e6a67c276351728b6455d5beb01242b027c6a"}, + {file = "cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:910d11e1a385c654bf738bf3e6b8e6ed5de0f5610fcae2be9e5b398d8081d20e"}, + {file = "cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:62598a8a57f815db4c6259a4e97d857dab56697e7de8e8ab02352ab74da1995d"}, + {file = "cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:07479a1cb08219ab719147e742e76090c9c773321959bb94946fffdd397a6437"}, + {file = "cryptography-50.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c99c003e088647b8a5b7c145d6f78c335f6348332b62e142d411c4b63d1460b9"}, + {file = "cryptography-50.0.0.tar.gz", hash = "sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9"}, +] + +[package.dependencies] +cffi = {version = ">=2.0.0", markers = "platform_python_implementation != \"PyPy\""} +typing-extensions = {version = ">=4.13.2", markers = "python_full_version < \"3.11\""} + +[package.extras] +ssh = ["bcrypt (>=3.1.5)"] + +[[package]] +name = "docutils" +version = "0.23" +description = "Docutils -- Python Documentation Utilities" +optional = false +python-versions = ">=3.9" +files = [ + {file = "docutils-0.23-py3-none-any.whl", hash = "sha256:25d013af9bf23bc1c7b2b093dff4208166c53a94786c9e447808335ef1185fea"}, + {file = "docutils-0.23.tar.gz", hash = "sha256:746f5060322511280a1e50eb76846ed6bf2342984b2ac04dc42caa1a8d78799e"}, +] + [[package]] name = "exceptiongroup" version = "1.3.1" @@ -194,42 +375,61 @@ typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} [package.extras] test = ["pytest (>=6)"] +[[package]] +name = "id" +version = "1.6.1" +description = "A tool for generating OIDC identities" +optional = false +python-versions = ">=3.9" +files = [ + {file = "id-1.6.1-py3-none-any.whl", hash = "sha256:f5ec41ed2629a508f5d0988eda142e190c9c6da971100612c4de9ad9f9b237ca"}, + {file = "id-1.6.1.tar.gz", hash = "sha256:d0732d624fb46fd4e7bc4e5152f00214450953b9e772c182c1c22964def1a069"}, +] + +[package.dependencies] +urllib3 = ">=2,<3" + +[package.extras] +dev = ["build", "bump (>=1.3.2)", "id[lint,test]"] +lint = ["bandit", "interrogate", "mypy", "ruff (<0.14.15)"] +test = ["coverage[toml]", "pretend", "pytest", "pytest-cov"] + [[package]] name = "idna" -version = "3.11" +version = "3.18" description = "Internationalized Domain Names in Applications (IDNA)" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"}, - {file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"}, + {file = "idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2"}, + {file = "idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848"}, ] [package.extras] -all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] +all = ["mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] [[package]] name = "importlib-metadata" -version = "8.7.1" +version = "9.0.0" description = "Read metadata from Python packages" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151"}, - {file = "importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb"}, + {file = "importlib_metadata-9.0.0-py3-none-any.whl", hash = "sha256:2d21d1cc5a017bd0559e36150c21c830ab1dc304dedd1b7ea85d20f45ef3edd7"}, + {file = "importlib_metadata-9.0.0.tar.gz", hash = "sha256:a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc"}, ] [package.dependencies] zipp = ">=3.20" [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +check = ["pytest-checkdocs (>=2.14)", "pytest-ruff (>=0.2.1)"] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] enabler = ["pytest-enabler (>=3.4)"] perf = ["ipython"] -test = ["flufl.flake8", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] -type = ["mypy (<1.19)", "pytest-mypy (>=1.0.1)"] +test = ["packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +type = ["pytest-mypy (>=1.0.1)"] [[package]] name = "iniconfig" @@ -242,15 +442,202 @@ files = [ {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, ] +[[package]] +name = "jaraco-classes" +version = "3.4.0" +description = "Utility functions for Python class constructs" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790"}, + {file = "jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd"}, +] + +[package.dependencies] +more-itertools = "*" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] + +[[package]] +name = "jaraco-context" +version = "6.1.2" +description = "Useful decorators and context managers" +optional = false +python-versions = ">=3.10" +files = [ + {file = "jaraco_context-6.1.2-py3-none-any.whl", hash = "sha256:bf8150b79a2d5d91ae48629d8b427a8f7ba0e1097dd6202a9059f29a36379535"}, + {file = "jaraco_context-6.1.2.tar.gz", hash = "sha256:f1a6c9d391e661cc5b8d39861ff077a7dc24dc23833ccee564b234b81c82dfe3"}, +] + +[package.dependencies] +"backports.tarfile" = {version = "*", markers = "python_version < \"3.12\""} + +[package.extras] +check = ["pytest-checkdocs (>=2.14)", "pytest-ruff (>=0.2.1)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=3.4)"] +test = ["jaraco.test (>=5.6.0)", "portend", "pytest (>=6,!=8.1.*)"] +type = ["pytest-mypy (>=1.0.1)"] + +[[package]] +name = "jaraco-functools" +version = "4.6.0" +description = "Functools like those found in stdlib" +optional = false +python-versions = ">=3.10" +files = [ + {file = "jaraco_functools-4.6.0-py3-none-any.whl", hash = "sha256:99e3dc0060c5cbe8fcd1cdb36258e2a65ca40f1566b2033b12abb1bb44dd3c30"}, + {file = "jaraco_functools-4.6.0.tar.gz", hash = "sha256:880c577ec9720b3a052d5bc611fb9f2269b3d87902ef42440df443b88e443280"}, +] + +[package.dependencies] +more_itertools = "*" + +[package.extras] +check = ["pytest-checkdocs (>=2.14)", "pytest-ruff (>=0.2.1)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=3.4)"] +test = ["jaraco.classes", "pytest (>=6,!=8.1.*)"] +type = ["pytest-mypy (>=1.0.1)"] + +[[package]] +name = "jeepney" +version = "0.9.0" +description = "Low-level, pure Python DBus protocol wrapper." +optional = false +python-versions = ">=3.7" +files = [ + {file = "jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683"}, + {file = "jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732"}, +] + +[package.extras] +test = ["async-timeout", "pytest", "pytest-asyncio (>=0.17)", "pytest-trio", "testpath", "trio"] +trio = ["trio"] + +[[package]] +name = "keyring" +version = "25.7.0" +description = "Store and access your passwords safely." +optional = false +python-versions = ">=3.9" +files = [ + {file = "keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f"}, + {file = "keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b"}, +] + +[package.dependencies] +importlib_metadata = {version = ">=4.11.4", markers = "python_version < \"3.12\""} +"jaraco.classes" = "*" +"jaraco.context" = "*" +"jaraco.functools" = "*" +jeepney = {version = ">=0.4.2", markers = "sys_platform == \"linux\""} +pywin32-ctypes = {version = ">=0.2.0", markers = "sys_platform == \"win32\""} +SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""} + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +completion = ["shtab (>=1.1.0)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=3.4)"] +test = ["pyfakefs", "pytest (>=6,!=8.1.*)"] +type = ["pygobject-stubs", "pytest-mypy (>=1.0.1)", "shtab", "types-pywin32"] + +[[package]] +name = "markdown-it-py" +version = "4.2.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.10" +files = [ + {file = "markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a"}, + {file = "markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "markdown-it-pyrs", "mistletoe (>=1.0,<2.0)", "mistune (>=3.0,<4.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins (>=0.5.0)"] +profiling = ["gprof2dot"] +rtd = ["ipykernel", "jupyter_sphinx", "mdit-py-plugins (>=0.5.0)", "myst-parser", "pyyaml", "sphinx", "sphinx-book-theme (>=1.0,<2.0)", "sphinx-copybutton", "sphinx-design"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions", "pytest-timeout", "requests"] + +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + +[[package]] +name = "more-itertools" +version = "11.1.0" +description = "More routines for operating on iterables, beyond itertools" +optional = false +python-versions = ">=3.10" +files = [ + {file = "more_itertools-11.1.0-py3-none-any.whl", hash = "sha256:4b65538ae22f6fed0ce4874efd317463a7489796a0939fa66824dd542125a192"}, + {file = "more_itertools-11.1.0.tar.gz", hash = "sha256:48e8f4d9e7e5878571ecf6f2b4e57634f93cd474cc8cfbd2376f2d11b396e30d"}, +] + +[[package]] +name = "nh3" +version = "0.3.6" +description = "Python binding to Ammonia HTML sanitizer Rust crate" +optional = false +python-versions = ">=3.8" +files = [ + {file = "nh3-0.3.6-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:2411e8c3cee81a1ddd62c2a5d50585c28aa5566d373ad1db92536b95ddb24ef2"}, + {file = "nh3-0.3.6-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e196fa70c2ff2eb4de7d3df3108f8f358c1d69dff20d45b11f20a5aa227ffb6d"}, + {file = "nh3-0.3.6-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:34d2b0d934156b87ee114f599a3ba9b8b9e17b5d79652ba3a13fa50903de965e"}, + {file = "nh3-0.3.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f2f14b7ae1fca99c4a66c981aac3974e7fbc1ca30a12673d223ae1df76680917"}, + {file = "nh3-0.3.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:889932a97fb4abb6f95fef1914c0d269ebfb60011e67121c1163059b9449dbb4"}, + {file = "nh3-0.3.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:edb2b4a1a27523e6cc7c417f8d21ce3d005243548b93e56b762b66b0c7f589f9"}, + {file = "nh3-0.3.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:43bc1ed3fa0716295fabee29ba42b2667e4a51d140b0a68e092170a765474fa6"}, + {file = "nh3-0.3.6-cp314-cp314t-win32.whl", hash = "sha256:597a8e843bea00b2eb5520658dc24a9bb032e7fc9e7c2c0c4cd29420220c9796"}, + {file = "nh3-0.3.6-cp314-cp314t-win_amd64.whl", hash = "sha256:4713502748f564fee0633b37b3403783ce0a3af3a3d148ad91025a5bdadb7bc6"}, + {file = "nh3-0.3.6-cp314-cp314t-win_arm64.whl", hash = "sha256:69bbb92865a693d909db3a700d3c01537533844d0948c1e9323561ce06ecda41"}, + {file = "nh3-0.3.6-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:a43ebd7543555c3ac1bc353023d0794e75cb76f6f18f19c32e95441496c0cc25"}, + {file = "nh3-0.3.6-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1b160831c9cdb06a6c79c2f9cdb11386602938f9af260d1c457a85add4f6f69"}, + {file = "nh3-0.3.6-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d14bf7982e7a77c0c775634c29c07ce08b38a046df73e1c1f139b3e82f18a38e"}, + {file = "nh3-0.3.6-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:44673b27010051ab5a5e438a86ec31bbda61d4a77d7e900af6b7be3037c1abae"}, + {file = "nh3-0.3.6-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6b7beece07525dc6e6b0fc2f104442de2ba328360ad00e50cbe2e1fd620447d"}, + {file = "nh3-0.3.6-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:455469a29951edc92bc48b47ac2281c3f2609e6c4f6a047056449f8c2c23facf"}, + {file = "nh3-0.3.6-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:905f877dc66dd7aea4a76e54bcb26acb5ff8216f720c0017ccf63e0e6035698e"}, + {file = "nh3-0.3.6-cp38-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:25c733bee928530556b1db0ea46c52cf5aa686146e38e60a6fc7cb801ef91cec"}, + {file = "nh3-0.3.6-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2f90d9a0cfdbee218994fdaaeeb5a0fde62d08f35e4eef0378ec1e2200172fd0"}, + {file = "nh3-0.3.6-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:82ca5bf427ad1b216b65ede1a2e2d87dc49bec417ceba0f297213107d3cd9d78"}, + {file = "nh3-0.3.6-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:f5ed5fe84aee7f39db95c214a7421bf0499fbf500fec6d86a4e29bfc37971438"}, + {file = "nh3-0.3.6-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:082675ff87b9385ec430ffe6d5847ba7456cc39b73720cd4add472f9f4cffd56"}, + {file = "nh3-0.3.6-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:36d06341bd501240d320f5942481ed5e6846136b666e1ba4faf802b78ebc875f"}, + {file = "nh3-0.3.6-cp38-abi3-win32.whl", hash = "sha256:5276ef17bdba9ad8040575c74072008b13aae429436e9d0429e718bb5f90f4da"}, + {file = "nh3-0.3.6-cp38-abi3-win_amd64.whl", hash = "sha256:f338ac7d594c067679f1e99b4f5ec3906842979560f9d8f15d6bdfa39a353b10"}, + {file = "nh3-0.3.6-cp38-abi3-win_arm64.whl", hash = "sha256:69f365963f63a1e9bff53bdbb3c542c7c2efed3e163c9d5d83a772a2ac468c21"}, + {file = "nh3-0.3.6.tar.gz", hash = "sha256:f3736c9dd3d1856f80cd031715b84ca75cda2bbb1ac802c3da26bfce590838d7"}, +] + [[package]] name = "packaging" -version = "25.0" +version = "26.3" description = "Core utilities for Python packages" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, - {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, + {file = "packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c"}, + {file = "packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79"}, ] [[package]] @@ -268,20 +655,31 @@ files = [ dev = ["pre-commit", "tox"] testing = ["coverage", "pytest", "pytest-benchmark"] +[[package]] +name = "pycparser" +version = "3.0" +description = "C parser in Python" +optional = false +python-versions = ">=3.10" +files = [ + {file = "pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992"}, + {file = "pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29"}, +] + [[package]] name = "pydantic" -version = "2.12.5" +version = "2.13.4" description = "Data validation using Python type hints" optional = false python-versions = ">=3.9" files = [ - {file = "pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d"}, - {file = "pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49"}, + {file = "pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba"}, + {file = "pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6"}, ] [package.dependencies] annotated-types = ">=0.6.0" -pydantic-core = "2.41.5" +pydantic-core = "2.46.4" typing-extensions = ">=4.14.1" typing-inspection = ">=0.4.2" @@ -291,132 +689,131 @@ timezone = ["tzdata"] [[package]] name = "pydantic-core" -version = "2.41.5" +version = "2.46.4" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.9" files = [ - {file = "pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146"}, - {file = "pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c"}, - {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2"}, - {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556"}, - {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49"}, - {file = "pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba"}, - {file = "pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9"}, - {file = "pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6"}, - {file = "pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b"}, - {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284"}, - {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594"}, - {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e"}, - {file = "pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b"}, - {file = "pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe"}, - {file = "pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f"}, - {file = "pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7"}, - {file = "pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5"}, - {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c"}, - {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294"}, - {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1"}, - {file = "pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d"}, - {file = "pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815"}, - {file = "pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3"}, - {file = "pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9"}, - {file = "pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d"}, - {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740"}, - {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e"}, - {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858"}, - {file = "pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36"}, - {file = "pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11"}, - {file = "pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd"}, - {file = "pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a"}, - {file = "pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553"}, - {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90"}, - {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07"}, - {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb"}, - {file = "pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23"}, - {file = "pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf"}, - {file = "pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008"}, - {file = "pydantic_core-2.41.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:8bfeaf8735be79f225f3fefab7f941c712aaca36f1128c9d7e2352ee1aa87bdf"}, - {file = "pydantic_core-2.41.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:346285d28e4c8017da95144c7f3acd42740d637ff41946af5ce6e5e420502dd5"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a75dafbf87d6276ddc5b2bf6fae5254e3d0876b626eb24969a574fff9149ee5d"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7b93a4d08587e2b7e7882de461e82b6ed76d9026ce91ca7915e740ecc7855f60"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8465ab91a4bd96d36dde3263f06caa6a8a6019e4113f24dc753d79a8b3a3f82"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:299e0a22e7ae2b85c1a57f104538b2656e8ab1873511fd718a1c1c6f149b77b5"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:707625ef0983fcfb461acfaf14de2067c5942c6bb0f3b4c99158bed6fedd3cf3"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f41eb9797986d6ebac5e8edff36d5cef9de40def462311b3eb3eeded1431e425"}, - {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0384e2e1021894b1ff5a786dbf94771e2986ebe2869533874d7e43bc79c6f504"}, - {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:f0cd744688278965817fd0839c4a4116add48d23890d468bc436f78beb28abf5"}, - {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:753e230374206729bf0a807954bcc6c150d3743928a73faffee51ac6557a03c3"}, - {file = "pydantic_core-2.41.5-cp39-cp39-win32.whl", hash = "sha256:873e0d5b4fb9b89ef7c2d2a963ea7d02879d9da0da8d9d4933dee8ee86a8b460"}, - {file = "pydantic_core-2.41.5-cp39-cp39-win_amd64.whl", hash = "sha256:e4f4a984405e91527a0d62649ee21138f8e3d0ef103be488c1dc11a80d7f184b"}, - {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034"}, - {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c"}, - {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2"}, - {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad"}, - {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd"}, - {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc"}, - {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56"}, - {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51"}, - {file = "pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e"}, + {file = "pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4"}, + {file = "pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d"}, + {file = "pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4"}, + {file = "pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f"}, + {file = "pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39"}, + {file = "pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d"}, + {file = "pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf"}, + {file = "pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594"}, + {file = "pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3"}, + {file = "pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848"}, + {file = "pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3"}, + {file = "pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109"}, + {file = "pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda"}, + {file = "pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33"}, + {file = "pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d"}, + {file = "pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2"}, + {file = "pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b"}, + {file = "pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458"}, + {file = "pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b"}, + {file = "pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c"}, + {file = "pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894"}, + {file = "pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89"}, + {file = "pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a"}, + {file = "pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008"}, + {file = "pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e"}, + {file = "pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd"}, + {file = "pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be"}, + {file = "pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d"}, + {file = "pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb"}, + {file = "pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292"}, + {file = "pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d"}, + {file = "pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb"}, + {file = "pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb"}, + {file = "pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898"}, + {file = "pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e"}, + {file = "pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519"}, + {file = "pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4"}, + {file = "pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac"}, + {file = "pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596"}, + {file = "pydantic_core-2.46.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:fd8b3d9fd264be37976686c7f65cd52a83f5e84f4bfd2adf9c1d469676bbb6ae"}, + {file = "pydantic_core-2.46.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9f444c499b3eefd3a92e348059471ea0c3a6e303d9c1cec09fa748fd9f895201"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3447661d99f75a3683a4cf5c87da72f2161964611864dbbeac7fbb118bb4bfc0"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b9bab013d1c7a79d3501ff86d0bc9c31bf587db4551677b96bec07df78c6b15"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d995260fdf4e1db774581b4900e0f832abe3c7c84996726bbc161b19c8f29e76"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f13a646d65d09fbf1bc6b3a9635d30095c8e7e5cc419ff35ecc563c5fd04cd49"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432c179df7874eeb73307aad2df0755e1ae0efa61ff0ea89b93e194411ae3928"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:e68b7a074f65a2fd746c52a7ce6142ab7006074ac269ace0c25cd8ba171f8066"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4a05d69cba51d852c5c3e92758653245a50c0b646ced0cf05bd793ed592839d6"}, + {file = "pydantic_core-2.46.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:228ee9bae8bef5b1e97ec58302f80357c37199e0d0a99174e138d28e6957b9d9"}, + {file = "pydantic_core-2.46.4-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:10e17cbb10a330363733efc4d7c4d0dd827ac0909b8f6a6542298fed1ea62f29"}, + {file = "pydantic_core-2.46.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:91a06d2e259ecfbd8c901d70c3c507900458498142b3026a296b7de4d1322cc9"}, + {file = "pydantic_core-2.46.4-cp39-cp39-win32.whl", hash = "sha256:d80ee3d731373b24cebbc10d689ca4ee1875caf0d5703a245db18efd4dd37fc1"}, + {file = "pydantic_core-2.46.4-cp39-cp39-win_amd64.whl", hash = "sha256:3be77f45df024d789a672ae34f8b06fb346c4f9f46ea714956660ea4862e89ac"}, + {file = "pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c"}, + {file = "pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b"}, + {file = "pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b"}, + {file = "pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea"}, + {file = "pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7"}, + {file = "pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df"}, + {file = "pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526"}, + {file = "pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983"}, + {file = "pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1"}, ] [package.dependencies] @@ -424,13 +821,13 @@ typing-extensions = ">=4.14.1" [[package]] name = "pygments" -version = "2.19.2" +version = "2.20.0" description = "Pygments is a syntax highlighting package written in Python." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, - {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, + {file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"}, + {file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"}, ] [package.extras] @@ -470,26 +867,56 @@ tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +description = "A (partial) reimplementation of pywin32 using ctypes/cffi" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755"}, + {file = "pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8"}, +] + +[[package]] +name = "readme-renderer" +version = "45.0" +description = "readme_renderer is a library for rendering readme descriptions for Warehouse" +optional = false +python-versions = ">=3.10" +files = [ + {file = "readme_renderer-45.0-py3-none-any.whl", hash = "sha256:3385ed220117104a2bceb4a9dac8c5fdf6d1f96890d7ea2a9c7174fd5c84091f"}, + {file = "readme_renderer-45.0.tar.gz", hash = "sha256:030a8fac74904f8fba11ad1bb6964e3f76e896dc7e5e71f16af190c9056696d1"}, +] + +[package.dependencies] +docutils = ">=0.21.2" +nh3 = ">=0.2.14" +Pygments = ">=2.5.1" + +[package.extras] +md = ["comrak (>=0.0.11)"] + [[package]] name = "requests" -version = "2.32.5" +version = "2.34.2" description = "Python HTTP for Humans." optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, - {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, + {file = "requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0"}, + {file = "requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed"}, ] [package.dependencies] -certifi = ">=2017.4.17" +certifi = ">=2023.5.7" charset_normalizer = ">=2,<4" idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<3" +urllib3 = ">=1.26,<3" [package.extras] socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<8)"] [[package]] name = "requests-mock" @@ -508,71 +935,157 @@ requests = ">=2.22,<3" [package.extras] fixture = ["fixtures"] +[[package]] +name = "requests-toolbelt" +version = "1.0.0" +description = "A utility belt for advanced users of python-requests" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6"}, + {file = "requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06"}, +] + +[package.dependencies] +requests = ">=2.0.1,<3.0.0" + +[[package]] +name = "rfc3986" +version = "2.0.0" +description = "Validating URI References per RFC 3986" +optional = false +python-versions = ">=3.7" +files = [ + {file = "rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd"}, + {file = "rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c"}, +] + +[package.extras] +idna2008 = ["idna"] + +[[package]] +name = "rich" +version = "15.0.0" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.9.0" +files = [ + {file = "rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb"}, + {file = "rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + +[[package]] +name = "secretstorage" +version = "3.5.0" +description = "Python bindings to FreeDesktop.org Secret Service API" +optional = false +python-versions = ">=3.10" +files = [ + {file = "secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137"}, + {file = "secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be"}, +] + +[package.dependencies] +cryptography = ">=2.0" +jeepney = ">=0.6" + [[package]] name = "tomli" -version = "2.4.0" +version = "2.4.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" files = [ - {file = "tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867"}, - {file = "tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9"}, - {file = "tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95"}, - {file = "tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76"}, - {file = "tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d"}, - {file = "tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576"}, - {file = "tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a"}, - {file = "tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa"}, - {file = "tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614"}, - {file = "tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1"}, - {file = "tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8"}, - {file = "tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a"}, - {file = "tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1"}, - {file = "tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b"}, - {file = "tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51"}, - {file = "tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729"}, - {file = "tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da"}, - {file = "tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3"}, - {file = "tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0"}, - {file = "tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e"}, - {file = "tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4"}, - {file = "tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e"}, - {file = "tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c"}, - {file = "tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f"}, - {file = "tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86"}, - {file = "tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87"}, - {file = "tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132"}, - {file = "tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6"}, - {file = "tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc"}, - {file = "tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66"}, - {file = "tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d"}, - {file = "tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702"}, - {file = "tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8"}, - {file = "tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776"}, - {file = "tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475"}, - {file = "tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2"}, - {file = "tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9"}, - {file = "tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0"}, - {file = "tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df"}, - {file = "tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d"}, - {file = "tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f"}, - {file = "tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b"}, - {file = "tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087"}, - {file = "tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd"}, - {file = "tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4"}, - {file = "tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a"}, - {file = "tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c"}, + {file = "tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30"}, + {file = "tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a"}, + {file = "tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076"}, + {file = "tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9"}, + {file = "tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c"}, + {file = "tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc"}, + {file = "tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049"}, + {file = "tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e"}, + {file = "tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece"}, + {file = "tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a"}, + {file = "tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085"}, + {file = "tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9"}, + {file = "tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5"}, + {file = "tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585"}, + {file = "tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1"}, + {file = "tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917"}, + {file = "tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9"}, + {file = "tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257"}, + {file = "tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54"}, + {file = "tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a"}, + {file = "tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897"}, + {file = "tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f"}, + {file = "tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d"}, + {file = "tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5"}, + {file = "tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd"}, + {file = "tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36"}, + {file = "tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd"}, + {file = "tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf"}, + {file = "tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac"}, + {file = "tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662"}, + {file = "tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853"}, + {file = "tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15"}, + {file = "tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba"}, + {file = "tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6"}, + {file = "tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7"}, + {file = "tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232"}, + {file = "tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4"}, + {file = "tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c"}, + {file = "tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d"}, + {file = "tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41"}, + {file = "tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c"}, + {file = "tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f"}, + {file = "tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8"}, + {file = "tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26"}, + {file = "tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396"}, + {file = "tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe"}, + {file = "tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f"}, +] + +[[package]] +name = "twine" +version = "6.2.0" +description = "Collection of utilities for publishing packages on PyPI" +optional = false +python-versions = ">=3.9" +files = [ + {file = "twine-6.2.0-py3-none-any.whl", hash = "sha256:418ebf08ccda9a8caaebe414433b0ba5e25eb5e4a927667122fbe8f829f985d8"}, + {file = "twine-6.2.0.tar.gz", hash = "sha256:e5ed0d2fd70c9959770dce51c8f39c8945c574e18173a7b81802dab51b4b75cf"}, ] +[package.dependencies] +id = "*" +keyring = {version = ">=21.2.0", markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\""} +packaging = ">=24.0" +readme-renderer = ">=35.0" +requests = ">=2.20" +requests-toolbelt = ">=0.8.0,<0.9.0 || >0.9.0" +rfc3986 = ">=1.4.0" +rich = ">=12.0.0" +urllib3 = ">=1.26.0" + +[package.extras] +keyring = ["keyring (>=21.2.0)"] + [[package]] name = "typing-extensions" -version = "4.15.0" +version = "4.16.0" description = "Backported and Experimental Type Hints for Python 3.9+" optional = false python-versions = ">=3.9" files = [ - {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, - {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, + {file = "typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8"}, + {file = "typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5"}, ] [[package]] @@ -591,13 +1104,13 @@ typing-extensions = ">=4.12.0" [[package]] name = "urllib3" -version = "2.6.3" +version = "2.7.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"}, - {file = "urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed"}, + {file = "urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897"}, + {file = "urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c"}, ] [package.extras] @@ -608,24 +1121,24 @@ zstd = ["backports-zstd (>=1.0.0)"] [[package]] name = "zipp" -version = "3.23.0" +version = "4.1.0" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e"}, - {file = "zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166"}, + {file = "zipp-4.1.0-py3-none-any.whl", hash = "sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f"}, + {file = "zipp-4.1.0.tar.gz", hash = "sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602"}, ] [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +check = ["pytest-checkdocs (>=2.14)", "pytest-ruff (>=0.2.1)"] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] +enabler = ["pytest-enabler (>=3.4)"] test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] -type = ["pytest-mypy"] +type = ["pytest-mypy (>=1.0.1)"] [metadata] lock-version = "2.0" python-versions = ">=3.10" -content-hash = "7413638efb3c23e0e04b32ca1cf4a49cb1838cd5c30a00ac987f79c2e688b77f" +content-hash = "a010df85afbd7110b3c9a8eef0bee07e69eaf5828d35fe29e1c7d71b161d4885" diff --git a/pyproject.toml b/pyproject.toml index ef962dc..d802765 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "python-mlb-statsapi" -version = "0.9.0" +version = "1.0.0" description = "mlbstatsapi python wrapper" authors = [ "Matthew Spah ", @@ -29,6 +29,7 @@ pydantic = "^2.0" pytest = "^8.0" requests-mock = "^1.10.0" build = "^1.0" +twine = "^6.2" [build-system] requires = ["poetry-core"] diff --git a/scripts/validate_release.py b/scripts/validate_release.py index ff45dfc..956d8ae 100644 --- a/scripts/validate_release.py +++ b/scripts/validate_release.py @@ -1,18 +1,28 @@ """Validate the built python-mlb-statsapi distributions before a release. -Checks the artifacts in ``dist/``, then installs the wheel into a throwaway -virtual environment and runs a public-import smoke test against the *installed* -package. +Checks the artifacts in ``dist/``, then clean-installs each distribution +artifact into its own throwaway virtual environment and runs a public-API +smoke test against the *installed* package. + +Both the wheel and the source distribution are installed separately so a +broken sdist build, a missing runtime dependency, or an omitted package file +cannot hide behind a working wheel. The smoke test deliberately runs from a temporary directory so the repository -checkout cannot shadow the installed distribution. +checkout cannot shadow the installed distribution artifact. -Nothing here contacts the MLB API. +Nothing here contacts the MLB API. Every HTTP response exercised by the smoke +test is produced by an injected fake Session. Usage:: python scripts/validate_release.py - python scripts/validate_release.py --dist dist --expected-version 0.9.0 + python scripts/validate_release.py --expected-version 1.0.0 + python scripts/validate_release.py --dist dist + +Without ``--expected-version`` the expected artifact version is read from the +version declared in ``pyproject.toml``, so the same validator follows the +project through a version bump without being edited. """ from __future__ import annotations @@ -32,23 +42,56 @@ NORMALIZED_DISTRIBUTION_NAME = "python_mlb_statsapi" EXPECTED_REQUIRES_PYTHON = ">=3.10" -# Paths every source distribution must carry so the project can be rebuilt and -# read from the sdist alone. +WHEEL_LABEL = "wheel" +SDIST_LABEL = "source distribution" + +# Paths every source distribution must carry so the project can be rebuilt, +# installed, and read from the sdist alone. Each entry was confirmed present in +# the archive Poetry actually generates; tests, docs, and scripts are +# intentionally excluded from the sdist and must not be listed here. REQUIRED_SDIST_PATHS = ( + "PKG-INFO", + "LICENSE", "README.md", "pyproject.toml", "mlbstatsapi/__init__.py", + "mlbstatsapi/exceptions.py", + "mlbstatsapi/warnings.py", + "mlbstatsapi/mlb_api.py", + "mlbstatsapi/mlb_dataadapter.py", + "mlbstatsapi/mlb_module.py", + "mlbstatsapi/models/__init__.py", +) + +# Explicit failure messages for the version 1.0 strict defaults. They are +# module-level constants so the offline validator tests can assert that the +# smoke-test contract still reports a reverted default in an understandable way. +MLB_STRICT_DEFAULT_MESSAGE = "Mlb.strict_http must default to True for the 1.0 contract" +ADAPTER_STRICT_DEFAULT_MESSAGE = ( + "MlbDataAdapter.strict_http must default to True for the 1.0 contract" ) SMOKE_TEST_SOURCE = ''' -"""Public import smoke test for an installed python-mlb-statsapi wheel.""" +"""Public API smoke test for an installed python-mlb-statsapi artifact. + +Runs inside a throwaway virtual environment against the installed +distribution, never against a repository checkout. + +Every HTTP response comes from an injected fake Session, so this test performs +no network I/O and never reaches the MLB API. +""" import importlib.metadata import inspect +import json +import logging import sys +import sysconfig +import warnings from pathlib import Path import requests +from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry import mlbstatsapi @@ -58,17 +101,49 @@ MlbDecodeError, MlbHttpCompatibilityWarning, MlbHttpError, + MlbResult, MlbTimeoutError, MlbTransportError, TheMlbStatsApiException, create_retry_policy, + get_stat_attributes, + return_splits, ) expected_version = sys.argv[1] +# The adapter logs an error for every fake 403, which is expected here. Silence +# it from the consumer side so the smoke-test output stays readable; the library +# itself must never configure logging for its callers. +package_logger = logging.getLogger("mlbstatsapi") +package_logger.addHandler(logging.NullHandler()) +package_logger.propagate = False + +MLB_STRICT_DEFAULT_MESSAGE = ( + "Mlb.strict_http must default to True for the 1.0 contract" +) +ADAPTER_STRICT_DEFAULT_MESSAGE = ( + "MlbDataAdapter.strict_http must default to True for the 1.0 contract" +) + +# Small deterministic error payload; MlbHttpError must expose it unchanged. +FORBIDDEN_PAYLOAD = {"messageNumber": 403, "message": "Forbidden"} +V1_SPORTS_URL = "https://statsapi.mlb.com/api/v1/sports" +V1_1_SPORTS_URL = "https://statsapi.mlb.com/api/v1.1/sports" +DOCUMENTED_RETRY_STATUSES = {429, 500, 502, 503, 504} + + +# --- The installed artifact, not the repository checkout --- + +assert sys.prefix != sys.base_prefix, ( + "the smoke test must run inside the throwaway virtual environment" +) + +site_packages = Path(sysconfig.get_paths()["purelib"]).resolve() package_file = Path(mlbstatsapi.__file__).resolve() -assert "site-packages" in package_file.parts, ( - f"mlbstatsapi was imported from {package_file}, not from the installed wheel" +assert package_file.is_relative_to(site_packages), ( + f"mlbstatsapi was imported from {package_file}, not from the installed " + f"distribution artifact under {site_packages}" ) installed_version = importlib.metadata.version("python-mlb-statsapi") @@ -76,51 +151,355 @@ f"installed metadata reports {installed_version}, expected {expected_version}" ) + +# --- Supported package-root surface --- + +supported_symbols = ( + "Mlb", + "MlbDataAdapter", + "MlbDecodeError", + "MlbHttpCompatibilityWarning", + "MlbHttpError", + "MlbResult", + "MlbTimeoutError", + "MlbTransportError", + "TheMlbStatsApiException", + "create_retry_policy", + "get_stat_attributes", + "return_splits", +) +for name in supported_symbols: + assert hasattr(mlbstatsapi, name), f"mlbstatsapi.{name} is not importable" + assert getattr(mlbstatsapi, name) is not None, f"mlbstatsapi.{name} is None" + +# Version 1.0 intentionally omits __all__; adding it would narrow star imports. +assert getattr(mlbstatsapi, "__all__", None) is None, ( + "version 1.0 must not define mlbstatsapi.__all__" +) + + +def assert_documented_retry_policy(retry, *, label): + """Assert the documented retry values without freezing Requests internals.""" + assert isinstance(retry, Retry), f"{label}: {type(retry)!r} is not a Retry" + assert retry.total == 3, f"{label}: total={retry.total}" + assert retry.connect == 3, f"{label}: connect={retry.connect}" + assert retry.read == 2, f"{label}: read={retry.read}" + assert retry.status == 3, f"{label}: status={retry.status}" + assert retry.backoff_factor == 0.5, f"{label}: backoff_factor={retry.backoff_factor}" + assert set(retry.status_forcelist) == DOCUMENTED_RETRY_STATUSES, ( + f"{label}: status_forcelist={sorted(retry.status_forcelist)}" + ) + assert retry.allowed_methods == frozenset({"GET"}), ( + f"{label}: allowed_methods={retry.allowed_methods}" + ) + assert retry.respect_retry_after_header is True, label + assert retry.raise_on_status is False, label + + assert callable(create_retry_policy) +assert inspect.signature(create_retry_policy).parameters == {} retry_policy = create_retry_policy() -assert isinstance(retry_policy, Retry), type(retry_policy) +assert_documented_retry_policy(retry_policy, label="create_retry_policy()") assert create_retry_policy() is not retry_policy, ( "create_retry_policy() must return a new Retry instance per call" ) assert issubclass(MlbHttpCompatibilityWarning, FutureWarning) +assert issubclass(TheMlbStatsApiException, Exception) assert issubclass(MlbHttpError, TheMlbStatsApiException) assert issubclass(MlbTimeoutError, MlbTransportError) assert issubclass(MlbTransportError, TheMlbStatsApiException) assert issubclass(MlbDecodeError, TheMlbStatsApiException) -# Compatibility mode is the default in this release. -assert ( - inspect.signature(Mlb.__init__).parameters["strict_http"].default is False -) -assert ( - inspect.signature(MlbDataAdapter.__init__).parameters["strict_http"].default - is False +mlb_init = inspect.signature(Mlb.__init__).parameters +adapter_init = inspect.signature(MlbDataAdapter.__init__).parameters +result_init = inspect.signature(MlbResult.__init__).parameters + +assert list(mlb_init) == [ + "self", + "hostname", + "logger", + "timeout", + "session", + "strict_http", +] +assert mlb_init["hostname"].default == "statsapi.mlb.com" +assert mlb_init["logger"].default is None +assert mlb_init["timeout"].default == (3.05, 30.0) +assert mlb_init["session"].default is None +assert mlb_init["strict_http"].default is True, MLB_STRICT_DEFAULT_MESSAGE +assert mlb_init["strict_http"].kind is inspect.Parameter.KEYWORD_ONLY + +assert list(adapter_init) == [ + "self", + "hostname", + "ver", + "logger", + "timeout", + "session", + "strict_http", +] +assert adapter_init["hostname"].default == "statsapi.mlb.com" +assert adapter_init["ver"].default == "v1" +assert adapter_init["logger"].default is None +assert adapter_init["timeout"].default == (3.05, 30.0) +assert adapter_init["session"].default is None +assert adapter_init["strict_http"].default is True, ADAPTER_STRICT_DEFAULT_MESSAGE +assert adapter_init["strict_http"].kind is inspect.Parameter.KEYWORD_ONLY + +assert list(result_init) == ["self", "status_code", "message", "data"] +assert result_init["data"].default is None + +result = MlbResult(200, "OK", {"copyright": "x", "ok": True}) +assert result.status_code == 200 +assert result.message == "OK" +assert result.data == {"ok": True} + +assert callable(return_splits) +assert callable(get_stat_attributes) +assert return_splits is mlbstatsapi.return_splits +assert get_stat_attributes is mlbstatsapi.get_stat_attributes + + +# --- Offline HTTP behavior --- + + +class ForbiddenSession: + """Injected Session stand-in that answers every GET with a final 403. + + A realistic requests.Response is built for the requested URL so the + installed adapter runs its real status handling. No network I/O happens, + so the smoke test never reaches the MLB API. + """ + + def __init__(self): + self.requested_urls = [] + + def get(self, url, params=None, timeout=None, **kwargs): + self.requested_urls.append(url) + response = requests.Response() + response.status_code = 403 + response.reason = "Forbidden" + response.url = url + response.headers["Content-Type"] = "application/json" + response.encoding = "utf-8" + # requests only exposes a body through Response._content; building it + # directly is the way to produce a realistic offline Response. + response._content = json.dumps(FORBIDDEN_PAYLOAD).encode("utf-8") + return response + + def close(self): + pass + + +def assert_forbidden_error(exc, *, expected_url, label): + assert exc.status_code == 403, f"{label}: status_code={exc.status_code}" + assert exc.reason == "Forbidden", f"{label}: reason={exc.reason!r}" + assert exc.method == "GET", f"{label}: method={exc.method!r}" + assert exc.url == expected_url, f"{label}: url={exc.url!r}" + assert isinstance(exc.response_data, dict), ( + f"{label}: response_data={exc.response_data!r}" + ) + for key, value in FORBIDDEN_PAYLOAD.items(): + assert exc.response_data.get(key) == value, ( + f"{label}: response_data={exc.response_data!r}" + ) + + +def assert_raises_forbidden(call, *, expected_url, label): + try: + call() + except MlbHttpError as exc: + assert_forbidden_error(exc, expected_url=expected_url, label=label) + return + raise AssertionError(f"{label}: a final 403 did not raise MlbHttpError") + + +def capture_compatibility_warnings(call): + """Run *call* and return (result, captured MlbHttpCompatibilityWarnings).""" + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + result = call() + compatibility = [ + record + for record in caught + if issubclass(record.category, MlbHttpCompatibilityWarning) + ] + return result, compatibility + + +def assert_single_compatibility_warning(captured, *, label): + assert len(captured) == 1, ( + f"{label}: expected exactly one MlbHttpCompatibilityWarning, " + f"captured {[str(record.message) for record in captured]}" + ) + record = captured[0] + assert record.category is MlbHttpCompatibilityWarning, ( + f"{label}: warning category is {record.category!r}" + ) + assert "strict_http=False" in str(record.message), ( + f"{label}: warning message does not mention strict_http=False: " + f"{str(record.message)!r}" + ) + + +# Constructed without strict_http so the real constructor default is exercised. +session = ForbiddenSession() +with Mlb(session=session) as mlb: + assert_raises_forbidden( + mlb.get_sports, + expected_url=V1_SPORTS_URL, + label=MLB_STRICT_DEFAULT_MESSAGE, + ) + +session = ForbiddenSession() +with Mlb(session=session, strict_http=True) as mlb: + assert_raises_forbidden( + mlb.get_sports, + expected_url=V1_SPORTS_URL, + label="Mlb(strict_http=True).get_sports()", + ) + +session = ForbiddenSession() +with Mlb(session=session, strict_http=False) as mlb: + sports, captured = capture_compatibility_warnings(mlb.get_sports) + +assert sports == [], f"Mlb(strict_http=False).get_sports() returned {sports!r}" +assert_single_compatibility_warning( + captured, + label="Mlb(strict_http=False).get_sports()", ) -# A library-created Session is library-owned, so reading its User-Agent through -# the private attribute is acceptable for internal release validation only. + +# --- Direct adapter construction, both documented API versions --- + +for api_version, sports_url in (("v1", V1_SPORTS_URL), ("v1.1", V1_1_SPORTS_URL)): + # Omitting strict_http exercises the real adapter default. + adapter = MlbDataAdapter(ver=api_version, session=ForbiddenSession()) + try: + assert_raises_forbidden( + lambda: adapter.get(endpoint="sports"), + expected_url=sports_url, + label=f"{ADAPTER_STRICT_DEFAULT_MESSAGE} (ver={api_version})", + ) + finally: + adapter.close() + + adapter = MlbDataAdapter( + ver=api_version, + session=ForbiddenSession(), + strict_http=True, + ) + try: + assert_raises_forbidden( + lambda: adapter.get(endpoint="sports"), + expected_url=sports_url, + label=f"MlbDataAdapter(ver={api_version}, strict_http=True).get()", + ) + finally: + adapter.close() + + adapter = MlbDataAdapter( + ver=api_version, + session=ForbiddenSession(), + strict_http=False, + ) + label = f"MlbDataAdapter(ver={api_version}, strict_http=False).get()" + try: + result, captured = capture_compatibility_warnings( + lambda: adapter.get(endpoint="sports"), + ) + finally: + adapter.close() + + assert isinstance(result, MlbResult), f"{label}: {type(result)!r}" + assert result.status_code == 403, f"{label}: status_code={result.status_code}" + assert result.message == "Forbidden", f"{label}: message={result.message!r}" + assert result.data == {}, f"{label}: data={result.data!r}" + assert_single_compatibility_warning(captured, label=label) + + +# --- Library-created Session --- + +# A library-created Session is library-owned, so reading its headers and +# adapters through private attributes is acceptable for release validation only. expected_user_agent = f"python-mlb-statsapi/{expected_version}" with Mlb() as mlb: user_agent = mlb._session.headers["User-Agent"] - assert user_agent == expected_user_agent, user_agent - -# Strict mode is constructible and injected Session headers stay untouched. -session = requests.Session() -session.headers.update( - { - "User-Agent": "release-smoke-test/1.0", - "X-Release-Test": "preserved", - } -) + assert user_agent == expected_user_agent, ( + f"library-created Session sends User-Agent {user_agent!r}, " + f"expected {expected_user_agent!r}" + ) + assert mlb._strict_http is True, MLB_STRICT_DEFAULT_MESSAGE + for scheme in ("https://", "http://"): + assert_documented_retry_policy( + mlb._session.get_adapter(scheme).max_retries, + label=f"library-created Session {scheme} adapter", + ) + +adapter = MlbDataAdapter() try: - with Mlb(session=session, strict_http=True): - pass + assert adapter._session.headers["User-Agent"] == expected_user_agent + assert adapter._strict_http is True, ADAPTER_STRICT_DEFAULT_MESSAGE +finally: + adapter.close() + + +# --- Injected Session stays caller-owned and unmodified --- + + +class OwnershipSession(requests.Session): + """Real Session that records close() so caller ownership is observable.""" + + def __init__(self): + super().__init__() + self.close_calls = 0 + + def close(self): + self.close_calls += 1 + super().close() + + +session = OwnershipSession() +session.headers["User-Agent"] = "release-smoke-test/1.0" +session.headers["X-Release-Test"] = "preserved" +# max_retries=0 so a library retry policy mounted here would be detectable. +injected_https_adapter = HTTPAdapter(max_retries=0) +injected_http_adapter = HTTPAdapter(max_retries=0) +session.mount("https://", injected_https_adapter) +session.mount("http://", injected_http_adapter) +headers_before = dict(session.headers) + +try: + with Mlb(session=session) as mlb: + assert mlb._session is session + + assert session.close_calls == 0, ( + "the library must not close a caller-injected Session" + ) + assert dict(session.headers) == headers_before, dict(session.headers) assert session.headers["User-Agent"] == "release-smoke-test/1.0" assert session.headers["X-Release-Test"] == "preserved" + assert session.get_adapter("https://") is injected_https_adapter, ( + "the injected https:// adapter was replaced" + ) + assert session.get_adapter("http://") is injected_http_adapter, ( + "the injected http:// adapter was replaced" + ) + for scheme in ("https://", "http://"): + mounted_retries = session.get_adapter(scheme).max_retries + assert mounted_retries.total == 0, ( + "the library must not mount its retry policy on an injected " + f"Session: {scheme} total={mounted_retries.total}" + ) finally: session.close() +assert session.close_calls == 1, ( + f"the smoke test must close its own Session exactly once, " + f"saw {session.close_calls}" +) + print(f"smoke test passed for python-mlb-statsapi {installed_version}") ''' @@ -166,13 +545,16 @@ def _read_expected_version(project_root: Path) -> str: def _find_single(dist_dir: Path, pattern: str, label: str) -> Path: matches = sorted(dist_dir.glob(pattern)) if not matches: + present = ", ".join(sorted(path.name for path in dist_dir.iterdir())) or "nothing" raise ValidationError( - f"no {label} matching {pattern!r} in {dist_dir}; run `poetry build` first" + f"{label}: no artifact matching {pattern!r} in {dist_dir}; " + f"found {present}. Run `poetry build` first." ) if len(matches) > 1: names = ", ".join(path.name for path in matches) raise ValidationError( - f"expected exactly one {label} in {dist_dir}, found: {names}. " + f"{label}: expected exactly one artifact matching {pattern!r} in " + f"{dist_dir}, found {len(matches)}: {names}. " "Remove stale artifacts and rebuild." ) return matches[0] @@ -187,7 +569,8 @@ def _check_wheel_metadata(wheel: Path, expected_version: str) -> None: ] if len(metadata_names) != 1: raise ValidationError( - f"expected one METADATA file in {wheel.name}, found {metadata_names}" + f"{WHEEL_LABEL} {wheel.name}: expected exactly one " + f".dist-info/METADATA file, found {metadata_names}" ) raw_metadata = archive.read(metadata_names[0]).decode("utf-8") @@ -195,19 +578,23 @@ def _check_wheel_metadata(wheel: Path, expected_version: str) -> None: name = metadata.get("Name") if name != DISTRIBUTION_NAME: - raise ValidationError(f"wheel Name is {name!r}, expected {DISTRIBUTION_NAME!r}") + raise ValidationError( + f"{WHEEL_LABEL} {wheel.name}: metadata Name is {name!r}, " + f"expected {DISTRIBUTION_NAME!r}" + ) version = metadata.get("Version") if version != expected_version: raise ValidationError( - f"wheel Version is {version!r}, expected {expected_version!r}" + f"{WHEEL_LABEL} {wheel.name}: metadata Version is {version!r}, " + f"expected {expected_version!r}" ) requires_python = metadata.get("Requires-Python") if requires_python != EXPECTED_REQUIRES_PYTHON: raise ValidationError( - f"wheel Requires-Python is {requires_python!r}, " - f"expected {EXPECTED_REQUIRES_PYTHON!r}" + f"{WHEEL_LABEL} {wheel.name}: metadata Requires-Python is " + f"{requires_python!r}, expected {EXPECTED_REQUIRES_PYTHON!r}" ) _log( @@ -226,7 +613,9 @@ def _check_sdist_contents(sdist: Path) -> None: missing = [path for path in REQUIRED_SDIST_PATHS if path not in relative_paths] if missing: raise ValidationError( - f"source distribution {sdist.name} is missing: {', '.join(missing)}" + f"{SDIST_LABEL} {sdist.name}: missing required path(s) " + f"{', '.join(missing)}; expected every path in " + f"{', '.join(REQUIRED_SDIST_PATHS)}" ) _log(f" sdist contains: {', '.join(REQUIRED_SDIST_PATHS)}") @@ -243,39 +632,58 @@ def _venv_python(venv_dir: Path) -> Path: raise ValidationError(f"no interpreter found in {venv_dir}") -def _run(command: list[str], *, cwd: Path) -> None: +def _run(command: list[str], *, cwd: Path, label: str) -> None: result = subprocess.run(command, cwd=cwd, check=False) if result.returncode != 0: printable = " ".join(command) - raise ValidationError(f"command failed ({result.returncode}): {printable}") + raise ValidationError( + f"{label} failed (exit code {result.returncode}): {printable}" + ) + + +def _create_clean_environment(venv_dir: Path) -> Path: + """Create an empty virtual environment and return its interpreter.""" + venv.EnvBuilder(with_pip=True, clear=True).create(venv_dir) + return _venv_python(venv_dir) + +def _check_clean_install(artifact: Path, expected_version: str, *, label: str) -> None: + """Clean-install one distribution artifact and smoke test the result. -def _check_clean_install(wheel: Path, expected_version: str) -> None: + Each artifact gets its own virtual environment so the wheel and the source + distribution are never validated against a shared install. + """ with tempfile.TemporaryDirectory(prefix="python-mlb-statsapi-release-") as tmp: workspace = Path(tmp) venv_dir = workspace / "venv" - _log(f" creating clean virtual environment in {venv_dir}") - venv.EnvBuilder(with_pip=True, clear=True).create(venv_dir) - python = _venv_python(venv_dir) + _log(f" creating clean virtual environment for the {label} in {venv_dir}") + python = _create_clean_environment(venv_dir) _run( [str(python), "-m", "pip", "install", "--upgrade", "--quiet", "pip"], cwd=workspace, + label=f"pip upgrade for the {label} environment", ) - _log(f" installing {wheel.name}") + + _log(f" installing {label}: {artifact.name}") _run( - [str(python), "-m", "pip", "install", "--quiet", str(wheel.resolve())], + [str(python), "-m", "pip", "install", "--quiet", str(artifact.resolve())], cwd=workspace, + label=f"{label} installation of {artifact.name}", ) smoke_test = workspace / "release_smoke_test.py" smoke_test.write_text(SMOKE_TEST_SOURCE, encoding="utf-8") - # Run from the temporary directory so the repository checkout is not on - # sys.path and cannot shadow the installed distribution. - _log(" running public import smoke test against the installed wheel") - _run([str(python), str(smoke_test), expected_version], cwd=workspace) + # Run from the temporary workspace so the repository checkout is not on + # sys.path and cannot shadow the installed distribution artifact. + _log(f" running {label} smoke test against the installed artifact") + _run( + [str(python), str(smoke_test), expected_version], + cwd=workspace, + label=f"{label} smoke test", + ) def validate(dist_dir: Path, expected_version: str) -> None: @@ -284,20 +692,24 @@ def validate(dist_dir: Path, expected_version: str) -> None: wheel = _find_single( dist_dir, f"{NORMALIZED_DISTRIBUTION_NAME}-{expected_version}-*.whl", - "wheel", + WHEEL_LABEL, ) _log(f" wheel: {wheel.name}") sdist = _find_single( dist_dir, f"{NORMALIZED_DISTRIBUTION_NAME}-{expected_version}.tar.gz", - "source distribution", + SDIST_LABEL, ) _log(f" source distribution: {sdist.name}") _check_wheel_metadata(wheel, expected_version) _check_sdist_contents(sdist) - _check_clean_install(wheel, expected_version) + + # Separate environments: an sdist that cannot build, omits package files, or + # loses a runtime dependency must not be masked by the wheel install. + _check_clean_install(wheel, expected_version, label=WHEEL_LABEL) + _check_clean_install(sdist, expected_version, label=SDIST_LABEL) _log(f"Release validation passed for {DISTRIBUTION_NAME} {expected_version}") diff --git a/tests/external_tests/homerunderby/test_homerunderby.py b/tests/external_tests/homerunderby/test_homerunderby.py index abbf2be..64053ba 100644 --- a/tests/external_tests/homerunderby/test_homerunderby.py +++ b/tests/external_tests/homerunderby/test_homerunderby.py @@ -1,6 +1,6 @@ import unittest from mlbstatsapi.models.homerunderby import HomeRunDerby, Round -from mlbstatsapi import Mlb +from mlbstatsapi import Mlb, MlbHttpError class TestHomerunderby(unittest.TestCase): @@ -10,7 +10,7 @@ def setUpClass(cls) -> None: @classmethod def tearDownClass(cls) -> None: - pass + cls.mlb.close() def test_get_homerunderby(self): """This test should return a 200 and Round""" @@ -32,14 +32,14 @@ def test_get_homerunderby(self): # items in list should be Round self.assertIsInstance(derby.rounds[0], Round) - def test_get_homerunderby_404(self): - """This test should return None for invalid game id""" + def test_get_homerunby_invalid_game_id(self): + """An invalid game ID currently produces a 400 from a live MLB API.""" - # set gameid to invalid id - game_id = '100394810242' + game_id = "100394810242" + + with self.assertRaises(MlbHttpError) as raised: + self.mlb.get_homerun_derby(game_id) + + self.assertEqual(raised.exception.status_code, 400) - # call get_homerun_derby return HomeRunDerby object - derby = self.mlb.get_homerun_derby(game_id) - # derby should be None - self.assertIsNone(derby) diff --git a/tests/http_contract_support.py b/tests/http_contract_support.py index 5b17318..63c3475 100644 --- a/tests/http_contract_support.py +++ b/tests/http_contract_support.py @@ -12,9 +12,9 @@ from urllib3.util.retry import Retry -# Final non-404 client errors that currently return an empty MlbResult by -# default rather than raising MlbHttpError. Later strict-mode work should -# reuse this matrix when asserting the opposite behavior. +# Final non-404 client errors that raise MlbHttpError under the version 1.0 +# strict default, and return an empty MlbResult with a compatibility warning +# when strict_http=False. COMPATIBILITY_CLIENT_ERRORS = ( 400, 401, @@ -93,17 +93,8 @@ def assert_library_retry_policy(retry: Retry) -> None: API_VERSIONS = ("v1", "v1.1") -# Pending version 1.0 default strict HTTP behavior (#284). -XFAIL_PENDING_STRICT_DEFAULT = pytest.mark.xfail( - strict=True, - reason="Pending #284: strict HTTP behavior becomes the 1.0 default", -) - -# Pending compatibility warning caller location via public Mlb endpoints (#285). -XFAIL_PENDING_WARNING_CALL_SITE = pytest.mark.xfail( - strict=True, - reason="Pending #285: compatibility warning must point to the public caller", -) +# Sentinel so helpers can omit strict_http and exercise the real constructor default. +_UNSET = object() def adapter_for_api_version(mlb, api_version: str): @@ -119,13 +110,19 @@ def standalone_adapter_for_version( session, api_version: str, *, - strict_http: bool = False, + strict_http=_UNSET, ) -> "MlbDataAdapter": - """Build a versioned MlbDataAdapter sharing a mocked session.""" + """Build a versioned MlbDataAdapter sharing a mocked session. + + Omitting ``strict_http`` leaves the constructor argument unset so tests + exercise the real production default rather than an explicit False. + """ from mlbstatsapi import MlbDataAdapter - return MlbDataAdapter( - session=session, - ver=api_version, - strict_http=strict_http, - ) + kwargs = { + "session": session, + "ver": api_version, + } + if strict_http is not _UNSET: + kwargs["strict_http"] = strict_http + return MlbDataAdapter(**kwargs) diff --git a/tests/test_http_contract.py b/tests/test_http_contract.py index 447dddb..2de72fe 100644 --- a/tests/test_http_contract.py +++ b/tests/test_http_contract.py @@ -1,7 +1,6 @@ """Offline HTTP contract tests for version 1.0 strict defaults and compatibility mode. -Documents the version 1.0 HTTP behavior in deterministic tests. Unimplemented -1.0 default wiring is marked xfail pending issue #284. +Documents the version 1.0 HTTP behavior in deterministic tests. """ from __future__ import annotations @@ -30,7 +29,6 @@ HTTP_REASON_BY_STATUS, NOT_FOUND_STATUS, SERVER_ERRORS, - XFAIL_PENDING_STRICT_DEFAULT, adapter_for_api_version, assert_library_retry_policy, standalone_adapter_for_version, @@ -114,10 +112,9 @@ def test_status_matrices_document_compatibility_baseline(): assert set(SERVER_ERRORS).isdisjoint(COMPATIBILITY_CLIENT_ERRORS) -# --- Version 1.0 default strict wiring (pending implementation #284) --- +# --- Version 1.0 default strict wiring --- -@XFAIL_PENDING_STRICT_DEFAULT def test_mlb_default_matches_explicit_strict_mode_wiring(): """Mlb() must default to strict mode on the client and both adapters.""" mlb = Mlb() @@ -129,7 +126,6 @@ def test_mlb_default_matches_explicit_strict_mode_wiring(): mlb.close() -@XFAIL_PENDING_STRICT_DEFAULT def test_mlb_data_adapter_default_is_strict(): """MlbDataAdapter() must default to strict HTTP in version 1.0.""" adapter = MlbDataAdapter() @@ -236,12 +232,11 @@ def test_compatibility_client_errors_do_not_raise_mlb_http_error( assert result.data == {} -# --- Version 1.0 default: final non-404 4xx raises (pending #284) --- +# --- Version 1.0 default: final non-404 4xx raises --- @pytest.mark.parametrize("api_version", API_VERSIONS) @pytest.mark.parametrize("status_code", COMPATIBILITY_CLIENT_ERRORS) -@XFAIL_PENDING_STRICT_DEFAULT def test_default_adapter_raises_on_final_non_404_client_error( api_version, status_code, @@ -275,7 +270,6 @@ def test_default_adapter_raises_on_final_non_404_client_error( @pytest.mark.parametrize("status_code", COMPATIBILITY_CLIENT_ERRORS) -@XFAIL_PENDING_STRICT_DEFAULT def test_default_mlb_raises_on_final_non_404_client_error(status_code): """Default Mlb() raises MlbHttpError for final non-404 4xx.""" reason = HTTP_REASON_BY_STATUS[status_code] @@ -665,7 +659,7 @@ def test_mlb_constructors_remain_compatible(): try: assert isinstance(mlb_default._session, requests.Session) assert mlb_default._timeout == DEFAULT_TIMEOUT - assert mlb_default._strict_http is False + assert mlb_default._strict_http is True finally: mlb_default.close() @@ -697,7 +691,7 @@ def test_mlb_positional_timeout_remains_third_argument(): assert session.calls[0]["timeout"] == 10 assert session.calls[1]["timeout"] == 10 assert mlb._session is session - assert mlb._strict_http is False + assert mlb._strict_http is True def test_mlb_strict_http_is_keyword_only(): @@ -709,7 +703,7 @@ def test_mlb_strict_http_is_keyword_only(): mlb = Mlb("statsapi.mlb.com", logger, 10, session) assert mlb._session is session assert mlb._timeout == 10 - assert mlb._strict_http is False + assert mlb._strict_http is True mlb_strict = Mlb( "statsapi.mlb.com", @@ -724,6 +718,17 @@ def test_mlb_strict_http_is_keyword_only(): assert mlb_strict._mlb_adapter_v1._strict_http is True assert mlb_strict._mlb_adapter_v1_1._strict_http is True + mlb_compat = Mlb( + "statsapi.mlb.com", + logger, + 10, + session, + strict_http=False, + ) + assert mlb_compat._strict_http is False + assert mlb_compat._mlb_adapter_v1._strict_http is False + assert mlb_compat._mlb_adapter_v1_1._strict_http is False + with pytest.raises(TypeError): Mlb("statsapi.mlb.com", logger, 10, session, True) @@ -738,7 +743,7 @@ def test_adapter_positional_construction_remains_compatible(): assert adapter._logger is logger assert adapter._timeout == (5.0, 60.0) assert adapter._session is session - assert adapter._strict_http is False + assert adapter._strict_http is True adapter.get(endpoint="game") assert session.calls[0]["timeout"] == (5.0, 60.0) diff --git a/tests/test_http_warnings.py b/tests/test_http_warnings.py index 92343d6..b7231d1 100644 --- a/tests/test_http_warnings.py +++ b/tests/test_http_warnings.py @@ -31,7 +31,6 @@ HTTP_REASON_BY_STATUS, NOT_FOUND_STATUS, SERVER_ERRORS, - XFAIL_PENDING_WARNING_CALL_SITE, adapter_for_api_version, standalone_adapter_for_version, ) @@ -171,10 +170,11 @@ def test_compatibility_warning_message_contains_migration_guidance(status_code): message = str(warning_info[0].message) assert str(status_code) in message assert SPORTS_URL in message + assert "strict_http=False" in message assert "compatibility mode" in message + assert "default in version 1.0" in message assert "strict_http=True" in message assert "MlbHttpError" in message - assert "version 1.0" in message def test_compatibility_warning_excludes_response_body(): @@ -490,7 +490,6 @@ def test_compatibility_warning_points_to_direct_adapter_caller_line(): assert warning.lineno == expected_lineno -@XFAIL_PENDING_WARNING_CALL_SITE def test_compatibility_warning_points_to_public_mlb_endpoint_caller_line(): """Public Mlb endpoint warnings must reference the application caller line.""" import inspect diff --git a/tests/test_mlb_retries.py b/tests/test_mlb_retries.py index a66eccf..0366808 100644 --- a/tests/test_mlb_retries.py +++ b/tests/test_mlb_retries.py @@ -27,10 +27,11 @@ NON_RETRYABLE_CLIENT_ERRORS, RETRYABLE_STATUS_CODES, SERVER_ERRORS, - XFAIL_PENDING_STRICT_DEFAULT, assert_library_retry_policy, ) +_UNSET = object() + def test_create_retry_policy_is_publicly_importable(): """create_retry_policy is available through the package public API.""" @@ -196,9 +197,12 @@ def no_retry_sleep(monkeypatch): def _adapter_against_local_server( port: int, *, - strict_http: bool = False, + strict_http=_UNSET, ) -> MlbDataAdapter: - adapter = MlbDataAdapter(strict_http=strict_http) + kwargs = {} + if strict_http is not _UNSET: + kwargs["strict_http"] = strict_http + adapter = MlbDataAdapter(**kwargs) adapter.url = f"http://127.0.0.1:{port}/api/v1/" return adapter @@ -250,18 +254,23 @@ def test_non_retryable_client_errors_are_not_retried( scripted_http_server, no_retry_sleep, ): - """Ordinary client errors are returned immediately without retries.""" + """Ordinary client errors complete after one attempt without retries.""" configure, port = scripted_http_server configure([status_code, 200]) adapter = _adapter_against_local_server(port) try: - result = adapter.get(endpoint="sports") + if status_code == 404: + result = adapter.get(endpoint="sports") + assert result.status_code == status_code + assert result.data == {} + else: + with pytest.raises(MlbHttpError) as exc_info: + adapter.get(endpoint="sports") + assert exc_info.value.status_code == status_code finally: adapter.close() - assert result.status_code == status_code - assert result.data == {} assert _ScriptedHandler.request_count == 1 @@ -306,7 +315,6 @@ def test_final_429_returns_empty_mlb_result_in_compatibility_mode( assert _ScriptedHandler.request_count == 4 -@XFAIL_PENDING_STRICT_DEFAULT def test_final_429_raises_mlb_http_error_after_retry_exhaustion_default_adapter( scripted_http_server, no_retry_sleep, @@ -327,7 +335,6 @@ def test_final_429_raises_mlb_http_error_after_retry_exhaustion_default_adapter( assert exc_info.value.method == "GET" -@XFAIL_PENDING_STRICT_DEFAULT def test_final_429_raises_via_default_mlb_client_after_retry_exhaustion( scripted_http_server, no_retry_sleep, diff --git a/tests/test_public_api.py b/tests/test_public_api.py new file mode 100644 index 0000000..6d2dc85 --- /dev/null +++ b/tests/test_public_api.py @@ -0,0 +1,534 @@ +"""Contract tests for the version 1.x public API surface. + +These tests freeze the supported package-root symbols, constructor signatures, +exception and warning inheritance, Session ownership guarantees, and the +explicit ``Mlb`` public-method manifest documented in ``docs/public-api.md``. + +They must not contact the live MLB API. +""" + +from __future__ import annotations + +import inspect +import warnings +from typing import Any + +import pytest +import requests +from urllib3.util.retry import Retry + +import mlbstatsapi +from mlbstatsapi import ( + Mlb, + MlbDataAdapter, + MlbDecodeError, + MlbHttpCompatibilityWarning, + MlbHttpError, + MlbResult, + MlbTimeoutError, + MlbTransportError, + TheMlbStatsApiException, + create_retry_policy, + get_stat_attributes, + return_splits, +) + +from http_contract_support import assert_library_retry_policy + + +# --------------------------------------------------------------------------- +# Package-root manifests +# --------------------------------------------------------------------------- + +# Intentionally supported package-root symbols for the 1.x series. +SUPPORTED_PACKAGE_ROOT_SYMBOLS: tuple[str, ...] = ( + "Mlb", + "MlbDataAdapter", + "MlbDecodeError", + "MlbHttpCompatibilityWarning", + "MlbHttpError", + "MlbResult", + "MlbTimeoutError", + "MlbTransportError", + "TheMlbStatsApiException", + "create_retry_policy", + "get_stat_attributes", + "return_splits", +) + +# Legacy helpers remain supported but are not preferred for new code. +LEGACY_PACKAGE_ROOT_HELPERS: tuple[str, ...] = ( + "get_stat_attributes", + "return_splits", +) + +# Submodules that appear on the package namespace as an import side effect. +# They are not part of the supported public API; see docs/public-api.md. +ACCIDENTAL_PACKAGE_ROOT_SUBMODULES: tuple[str, ...] = ( + "exceptions", + "mlb_api", + "mlb_dataadapter", + "mlb_module", + "models", + "warnings", +) + + +# Python 3.14 renders typing.Union[a, b] as "a | b" while Python 3.10-3.13 +# render "Union[a, b]". The annotation object itself is unchanged, so the legacy +# spelling is rewritten here and one manifest stays valid across the whole +# supported interpreter matrix. +LEGACY_UNION_RENDERINGS: dict[str, str] = { + "Union[str, List[int]]": "str | List[int]", +} + + +def _normalize_annotation(annotation: Any) -> str: + rendered = inspect.formatannotation(annotation) + for legacy, pep604 in LEGACY_UNION_RENDERINGS.items(): + rendered = rendered.replace(legacy, pep604) + return rendered + + +def _normalize_signature(fn: Any) -> str: + """Return a stable, readable signature string without the ``self`` parameter.""" + sig = inspect.signature(fn) + parts: list[str] = [] + for name, parameter in sig.parameters.items(): + if name == "self": + continue + if parameter.kind is inspect.Parameter.VAR_KEYWORD: + annotation = "" + if parameter.annotation is not inspect.Parameter.empty: + annotation = f": {_normalize_annotation(parameter.annotation)}" + parts.append(f"**{name}{annotation}") + continue + if parameter.kind is inspect.Parameter.VAR_POSITIONAL: + parts.append(f"*{name}") + continue + piece = name + if parameter.annotation is not inspect.Parameter.empty: + piece += f": {_normalize_annotation(parameter.annotation)}" + if parameter.default is not inspect.Parameter.empty: + piece += f"={parameter.default!r}" + parts.append(piece) + return "(" + ", ".join(parts) + ")" + + +# Explicit inventory of public methods defined directly on Mlb. +# A newly exposed method must update this manifest intentionally. +MLB_PUBLIC_METHOD_MANIFEST: dict[str, str] = { + "close": "()", + "__enter__": "()", + "__exit__": "(exc_type, exc, traceback)", + "get_people": "(sport_id: int=1, **params)", + "get_person": "(player_id: int, **params)", + "get_persons": "(person_ids: str | List[int], **params)", + "get_people_id": ( + "(fullname: str, sport_id: int=1, search_key: str='fullName', **params)" + ), + "get_teams": "(sport_id: int=1, **params)", + "get_team": "(team_id: int, **params)", + "get_team_id": "(team_name: str, search_key: str='name', **params)", + "get_team_roster": "(team_id: int, **params)", + "get_team_coaches": "(team_id: int, **params)", + "get_schedule": ( + "(date: str=None, start_date: str=None, end_date: str=None, " + "sport_id: int=1, team_id: int=None, **params)" + ), + "get_scheduled_games_by_date": ( + "(date: str=None, start_date: str=None, end_date: str=None, " + "sport_id: int=1, **params)" + ), + "get_game": "(game_id: int, **params)", + "get_game_play_by_play": "(game_id: int, **params)", + "get_game_line_score": "(game_id: int, **params)", + "get_game_box_score": "(game_id: int, **params)", + "get_game_ids": ( + "(date: str=None, start_date: str=None, end_date: str=None, " + "sport_id: int=1, **params)" + ), + "get_gamepace": "(season: str, sport_id=1, **params)", + "get_venue": "(venue_id: int, **params)", + "get_venues": "(**params)", + "get_venue_id": "(venue_name: str, search_key: str='name', **params)", + "get_sport": "(sport_id: int, **params)", + "get_sports": "(**params)", + "get_sport_id": "(sport_name: str, search_key: str='name', **params)", + "get_league": "(league_id: int, **params)", + "get_leagues": "(**params)", + "get_league_id": "(league_name: str, search_key: str='name', **params)", + "get_division": "(division_id: int, **params)", + "get_divisions": "(**params)", + "get_division_id": "(division_name: str, search_key: str='name', **params)", + "get_season": "(season_id: str, sport_id: int=1, **params)", + "get_seasons": "(sport_id: int=1, **params)", + "get_standings": "(league_id: int, season: str, **params)", + "get_attendance": ( + "(team_id: int=None, league_id: int=None, " + "league_list_id: str=None, **params)" + ), + "get_draft": "(year_id: int, **params)", + "get_awards": "(award_id: str, **params)", + "get_homerun_derby": "(game_id, **params)", + "get_team_stats": "(team_id: int, stats: list, groups: list, **params)", + "get_players_stats_for_game": "(person_id: int, game_id: int, **params)", + "get_player_stats": "(person_id: int, stats: list, groups: list, **params)", + "get_stats": "(stats: list, groups: list, **params: dict)", +} + + +# --------------------------------------------------------------------------- +# Package-root symbols +# --------------------------------------------------------------------------- + + +def test_supported_package_root_symbols_are_unique() -> None: + assert len(SUPPORTED_PACKAGE_ROOT_SYMBOLS) == len(set(SUPPORTED_PACKAGE_ROOT_SYMBOLS)) + + +def test_supported_package_root_symbols_are_importable_from_package() -> None: + for name in SUPPORTED_PACKAGE_ROOT_SYMBOLS: + assert hasattr(mlbstatsapi, name), name + assert getattr(mlbstatsapi, name) is not None + + +@pytest.mark.parametrize("name", SUPPORTED_PACKAGE_ROOT_SYMBOLS) +def test_supported_symbols_are_importable_by_name(name: str) -> None: + namespace: dict[str, Any] = {} + exec(f"from mlbstatsapi import {name}", namespace) + assert name in namespace + assert namespace[name] is getattr(mlbstatsapi, name) + + +def test_package_does_not_define_all_in_version_1_0() -> None: + """``__all__`` is omitted so star-import behavior is not silently narrowed.""" + assert getattr(mlbstatsapi, "__all__", None) is None + + +def test_star_import_includes_supported_symbols() -> None: + namespace: dict[str, Any] = {} + exec("from mlbstatsapi import *", namespace) + for name in SUPPORTED_PACKAGE_ROOT_SYMBOLS: + assert name in namespace, name + + +def test_star_import_currently_includes_accidental_submodules() -> None: + """Document current wildcard behavior without promoting it to supported API.""" + namespace: dict[str, Any] = {} + exec("from mlbstatsapi import *", namespace) + for name in ACCIDENTAL_PACKAGE_ROOT_SUBMODULES: + assert name in namespace, name + + +def test_legacy_helpers_remain_package_root_importable() -> None: + assert return_splits is mlbstatsapi.return_splits + assert get_stat_attributes is mlbstatsapi.get_stat_attributes + assert callable(return_splits) + assert callable(get_stat_attributes) + for name in LEGACY_PACKAGE_ROOT_HELPERS: + assert name in SUPPORTED_PACKAGE_ROOT_SYMBOLS + + +# --------------------------------------------------------------------------- +# Constructor signatures +# --------------------------------------------------------------------------- + + +def _parameter_names(fn: Any) -> list[str]: + return [ + name + for name in inspect.signature(fn).parameters + if name != "self" + ] + + +def test_mlb_constructor_parameter_order_and_defaults() -> None: + parameters = inspect.signature(Mlb.__init__).parameters + + assert _parameter_names(Mlb.__init__) == [ + "hostname", + "logger", + "timeout", + "session", + "strict_http", + ] + assert parameters["hostname"].default == "statsapi.mlb.com" + assert parameters["logger"].default is None + assert parameters["timeout"].default == (3.05, 30.0) + assert parameters["session"].default is None + assert parameters["strict_http"].default is True + assert parameters["strict_http"].kind is inspect.Parameter.KEYWORD_ONLY + + +def test_mlb_data_adapter_constructor_parameter_order_and_defaults() -> None: + parameters = inspect.signature(MlbDataAdapter.__init__).parameters + + assert _parameter_names(MlbDataAdapter.__init__) == [ + "hostname", + "ver", + "logger", + "timeout", + "session", + "strict_http", + ] + assert parameters["hostname"].default == "statsapi.mlb.com" + assert parameters["ver"].default == "v1" + assert parameters["logger"].default is None + assert parameters["timeout"].default == (3.05, 30.0) + assert parameters["session"].default is None + assert parameters["strict_http"].default is True + assert parameters["strict_http"].kind is inspect.Parameter.KEYWORD_ONLY + + +def test_mlb_result_constructor_parameter_order_and_defaults() -> None: + parameters = inspect.signature(MlbResult.__init__).parameters + + assert _parameter_names(MlbResult.__init__) == [ + "status_code", + "message", + "data", + ] + assert parameters["status_code"].default is inspect.Parameter.empty + assert parameters["message"].default is inspect.Parameter.empty + assert parameters["data"].default is None + + +def test_strict_http_rejects_positional_argument_for_mlb() -> None: + with pytest.raises(TypeError): + Mlb("statsapi.mlb.com", None, (3.05, 30.0), None, True) # type: ignore[misc] + + +def test_strict_http_rejects_positional_argument_for_adapter() -> None: + with pytest.raises(TypeError): + MlbDataAdapter( + "statsapi.mlb.com", + "v1", + None, + (3.05, 30.0), + None, + True, + ) # type: ignore[misc] + + +# --------------------------------------------------------------------------- +# Mlb public method manifest +# --------------------------------------------------------------------------- + + +def test_mlb_public_method_manifest_has_unique_names() -> None: + assert len(MLB_PUBLIC_METHOD_MANIFEST) == len(set(MLB_PUBLIC_METHOD_MANIFEST)) + + +def test_mlb_public_method_manifest_matches_class_dict() -> None: + discovered = { + name + for name, obj in Mlb.__dict__.items() + if inspect.isfunction(obj) + and (not name.startswith("_") or name in ("__enter__", "__exit__")) + and name != "__init__" + } + assert discovered == set(MLB_PUBLIC_METHOD_MANIFEST) + + +@pytest.mark.parametrize("method_name, expected", MLB_PUBLIC_METHOD_MANIFEST.items()) +def test_mlb_public_method_signature(method_name: str, expected: str) -> None: + method = getattr(Mlb, method_name) + actual = _normalize_signature(method) + assert actual == expected, f"{method_name}: {actual} != {expected}" + + +def test_mlb_public_endpoint_count() -> None: + endpoint_methods = [ + name + for name in MLB_PUBLIC_METHOD_MANIFEST + if not name.startswith("_") and name != "close" + ] + assert len(endpoint_methods) == 40 + assert len(MLB_PUBLIC_METHOD_MANIFEST) == 43 + + +# --------------------------------------------------------------------------- +# Exception and warning inheritance +# --------------------------------------------------------------------------- + + +def test_exception_hierarchy() -> None: + assert issubclass(TheMlbStatsApiException, Exception) + assert issubclass(MlbTransportError, TheMlbStatsApiException) + assert issubclass(MlbTimeoutError, MlbTransportError) + assert issubclass(MlbHttpError, TheMlbStatsApiException) + assert issubclass(MlbDecodeError, TheMlbStatsApiException) + + +def test_exceptions_are_publicly_imported() -> None: + assert mlbstatsapi.TheMlbStatsApiException is TheMlbStatsApiException + assert mlbstatsapi.MlbTransportError is MlbTransportError + assert mlbstatsapi.MlbTimeoutError is MlbTimeoutError + assert mlbstatsapi.MlbHttpError is MlbHttpError + assert mlbstatsapi.MlbDecodeError is MlbDecodeError + + +def test_broad_and_specific_exception_catches() -> None: + with pytest.raises(TheMlbStatsApiException): + raise MlbTransportError("transport") + with pytest.raises(MlbTransportError): + raise MlbTimeoutError("timeout") + with pytest.raises(MlbTimeoutError): + raise MlbTimeoutError("timeout") + with pytest.raises(MlbHttpError): + raise MlbHttpError(500, "Internal Server Error", "https://example.test") + with pytest.raises(MlbDecodeError): + raise MlbDecodeError("bad json") + with pytest.raises(TheMlbStatsApiException): + raise MlbDecodeError("bad json") + + +def test_mlb_http_error_stable_attributes() -> None: + exc = MlbHttpError( + status_code=502, + reason="Bad Gateway", + url="https://statsapi.mlb.com/api/v1/sports", + method="get", + response_data={"message": "nope"}, + body_excerpt="nope", + ) + assert exc.status_code == 502 + assert exc.reason == "Bad Gateway" + assert exc.url == "https://statsapi.mlb.com/api/v1/sports" + assert exc.method == "GET" + assert exc.response_data == {"message": "nope"} + assert exc.body_excerpt == "nope" + + +def test_compatibility_warning_inherits_from_future_warning() -> None: + assert issubclass(MlbHttpCompatibilityWarning, FutureWarning) + assert mlbstatsapi.MlbHttpCompatibilityWarning is MlbHttpCompatibilityWarning + + +# --------------------------------------------------------------------------- +# Retry policy +# --------------------------------------------------------------------------- + + +def test_create_retry_policy_contract() -> None: + assert callable(create_retry_policy) + assert inspect.signature(create_retry_policy).parameters == {} + + first = create_retry_policy() + second = create_retry_policy() + + assert isinstance(first, Retry) + assert first is not second + assert_library_retry_policy(first) + assert_library_retry_policy(second) + + +# --------------------------------------------------------------------------- +# MlbResult +# --------------------------------------------------------------------------- + + +def test_mlb_result_public_attributes_and_non_mutation() -> None: + payload = {"copyright": "MLB", "sports": [{"id": 1}]} + result = MlbResult(200, "OK", payload) + + assert result.status_code == 200 + assert result.message == "OK" + assert result.data == {"sports": [{"id": 1}]} + assert payload == {"copyright": "MLB", "sports": [{"id": 1}]} + + +def test_mlb_result_default_data_is_empty_dict() -> None: + result = MlbResult(404, "Not Found") + assert result.data == {} + + +# --------------------------------------------------------------------------- +# Context managers and Session ownership +# --------------------------------------------------------------------------- + + +def test_mlb_context_manager_returns_self_and_closes_library_session() -> None: + with Mlb() as mlb: + assert mlb is mlb.__enter__() + session = mlb._session + assert mlb._owns_session is True + assert mlb._closed is True + # Requests marks a closed Session; a second close must remain safe. + mlb.close() + assert mlb._closed is True + # The underlying Session object still exists but was closed by the client. + assert session is mlb._session + + +def test_mlb_context_manager_does_not_close_injected_session() -> None: + session = requests.Session() + session.headers.update( + { + "User-Agent": "public-api-test/1.0", + "X-Public-Api-Test": "preserved", + } + ) + try: + with Mlb(session=session) as mlb: + assert mlb._owns_session is False + assert mlb._session is session + mlb.close() + assert session.headers["User-Agent"] == "public-api-test/1.0" + assert session.headers["X-Public-Api-Test"] == "preserved" + # Injected Sessions remain usable after the client exits. + assert session.headers.get("X-Public-Api-Test") == "preserved" + finally: + session.close() + + +def test_library_created_session_receives_user_agent_and_retries() -> None: + with Mlb() as mlb: + assert "python-mlb-statsapi/" in mlb._session.headers["User-Agent"] + https_adapter = mlb._session.get_adapter("https://example.test") + assert_library_retry_policy(https_adapter.max_retries) + + +def test_injected_session_adapters_remain_untouched() -> None: + session = requests.Session() + original_adapters = dict(session.adapters) + try: + with Mlb(session=session): + assert session.adapters == original_adapters + finally: + session.close() + + +def test_adapter_close_owns_only_library_sessions() -> None: + adapter = MlbDataAdapter() + adapter.close() + adapter.close() + assert adapter._closed is True + + session = requests.Session() + try: + injected = MlbDataAdapter(session=session) + injected.close() + injected.close() + assert injected._owns_session is False + assert session.headers is not None + finally: + session.close() + + +def test_adapter_supports_documented_api_versions() -> None: + for version in ("v1", "v1.1"): + adapter = MlbDataAdapter(ver=version) + try: + assert adapter.url.endswith(f"/api/{version}/") + finally: + adapter.close() + + +def test_compatibility_warning_can_be_filtered_by_public_class() -> None: + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + warnings.warn("probe", MlbHttpCompatibilityWarning) + assert len(caught) == 1 + assert caught[0].category is MlbHttpCompatibilityWarning diff --git a/tests/test_release_validation.py b/tests/test_release_validation.py index a20f5de..a4533bb 100644 --- a/tests/test_release_validation.py +++ b/tests/test_release_validation.py @@ -1,11 +1,27 @@ -"""Offline checks that the release documentation stays consistent with the package. +"""Offline checks for the release validator, deterministic CI, and release docs. -These tests do not build or install anything. Packaging itself is validated by -``scripts/validate_release.py``, which runs against ``dist/`` and a clean -virtual environment. +Three groups of checks live here: + +* documentation consistency for the current release +* unit coverage for ``scripts/validate_release.py`` helpers and failure messages +* the deterministic CI contract (release branch triggers, Python matrix, twine) + +Nothing here builds the real package, creates a virtual environment, installs +an artifact, or makes a network request. Synthetic wheel ZIPs and +source-distribution tarballs stand in for real artifacts, and clean-install +steps are stubbed. Real packaging is validated by running +``scripts/validate_release.py`` against ``dist/``. """ +from __future__ import annotations + +import importlib.util +import io import re +import sys +import tarfile +import types +import zipfile from pathlib import Path import pytest @@ -13,21 +29,76 @@ PROJECT_ROOT = Path(__file__).resolve().parent.parent README = PROJECT_ROOT / "README.md" TRANSPORT_DOC = PROJECT_ROOT / "docs" / "http-transport.md" -RELEASE_NOTES = PROJECT_ROOT / "docs" / "releases" / "0.9.0.md" +PUBLIC_API_DOC = PROJECT_ROOT / "docs" / "public-api.md" +RELEASE_NOTES_DIR = PROJECT_ROOT / "docs" / "releases" +PYPROJECT = PROJECT_ROOT / "pyproject.toml" +POETRY_LOCK = PROJECT_ROOT / "poetry.lock" +VALIDATE_RELEASE = PROJECT_ROOT / "scripts" / "validate_release.py" +OFFLINE_WORKFLOW = PROJECT_ROOT / ".github" / "workflows" / "build-and-test.yml" +EXTERNAL_WORKFLOW = PROJECT_ROOT / ".github" / "workflows" / "external-tests.yml" + +# Release notes for the version this branch is preparing. Kept explicit so the +# current-document checks do not depend on the pyproject version bump, which is +# owned by a separate issue. +CURRENT_RELEASE_NOTES = RELEASE_NOTES_DIR / "1.0.0.md" + +# Historical notes keep their own version-specific statements and must not be +# rewritten to match the current release. +HISTORICAL_RELEASE_NOTES = ( + RELEASE_NOTES_DIR / "0.7.1.md", + RELEASE_NOTES_DIR / "0.8.0.md", + RELEASE_NOTES_DIR / "0.9.0.md", +) + +# Deterministic CI contract for the 1.0 release. +RELEASE_BRANCH = "release/1.0.0" +STALE_RELEASE_BRANCH = "release/0.9.0" +SUPPORTED_PYTHON_VERSIONS = ("3.10", "3.11", "3.12", "3.13", "3.14") +CI_VALIDATED_PYTHON_RANGE = "3.10 through 3.14" +# Prerelease during this work, so it is deliberately excluded from the matrix. +UNSUPPORTED_PRERELEASE_PYTHON = "3.15" +BUILD_JOB_PYTHON = "3.14" +DECLARED_PYTHON_REQUIREMENT = ">=3.10" PYTHON_BLOCK_PATTERN = re.compile( r"^```python\n(.*?)^```", re.MULTILINE | re.DOTALL, ) +USER_AGENT_PATTERN = re.compile(r"python-mlb-statsapi/[0-9][^\s`\"']*") + + +def _load_validator() -> types.ModuleType: + """Import scripts/validate_release.py, which is not an installable package.""" + spec = importlib.util.spec_from_file_location( + "validate_release_under_test", + VALIDATE_RELEASE, + ) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +validator = _load_validator() + +# Synthetic version used only as an expected-version fixture. The validator +# itself must keep reading the real expected version from pyproject.toml. +SYNTHETIC_VERSION = "1.0.0" + def _project_version() -> str: - text = (PROJECT_ROOT / "pyproject.toml").read_text(encoding="utf-8") + text = PYPROJECT.read_text(encoding="utf-8") match = re.search(r'^version\s*=\s*"([^"]+)"', text, flags=re.MULTILINE) assert match is not None, "no version found in pyproject.toml" return match.group(1) +# --------------------------------------------------------------------------- +# Documentation consistency +# --------------------------------------------------------------------------- + + def _python_blocks(path: Path) -> list[tuple[int, str]]: """Return (line number, source) for every non-REPL ```python block.""" text = path.read_text(encoding="utf-8") @@ -44,8 +115,28 @@ def _python_blocks(path: Path) -> list[tuple[int, str]]: return blocks +def _release_notes_paths() -> list[Path]: + return sorted(RELEASE_NOTES_DIR.glob("*.md")) + + +def _current_document_paths() -> list[Path]: + """Documents that must describe the current release, not history.""" + return [README, TRANSPORT_DOC, PUBLIC_API_DOC, CURRENT_RELEASE_NOTES] + + def _documented_paths() -> list[Path]: - return [README, TRANSPORT_DOC, RELEASE_NOTES] + """Every Markdown document whose Python examples must stay valid.""" + return [README, TRANSPORT_DOC, PUBLIC_API_DOC, *_release_notes_paths()] + + +def _documented_user_agents(path: Path) -> set[str]: + return set(USER_AGENT_PATTERN.findall(path.read_text(encoding="utf-8"))) + + +def test_release_notes_directory_is_fully_covered() -> None: + """Every release-notes file is classified as current or historical.""" + classified = {CURRENT_RELEASE_NOTES, *HISTORICAL_RELEASE_NOTES} + assert set(_release_notes_paths()) == classified @pytest.mark.parametrize( @@ -83,7 +174,7 @@ def test_documentation_examples_use_public_api_only(path: Path) -> None: def test_release_notes_exist_for_the_declared_version() -> None: version = _project_version() - notes = PROJECT_ROOT / "docs" / "releases" / f"{version}.md" + notes = RELEASE_NOTES_DIR / f"{version}.md" assert notes.is_file(), f"missing release notes for version {version}" assert notes.read_text(encoding="utf-8").startswith( f"# python-mlb-statsapi {version}" @@ -91,23 +182,845 @@ def test_release_notes_exist_for_the_declared_version() -> None: def test_documented_user_agent_matches_the_declared_version() -> None: - """The documented User-Agent must track the version the build will produce.""" - version = _project_version() - expected = f"python-mlb-statsapi/{version}" + """Current docs must track the version the build will produce.""" + expected = f"python-mlb-statsapi/{_project_version()}" - for path in (README, TRANSPORT_DOC, RELEASE_NOTES): - text = path.read_text(encoding="utf-8") - documented = set(re.findall(r"python-mlb-statsapi/[0-9][^\s`\"']*", text)) + for path in (README, TRANSPORT_DOC): + documented = _documented_user_agents(path) assert documented == {expected}, ( f"{path.name} documents User-Agent versions {sorted(documented)}, " f"expected only {expected!r}" ) + # The current release notes need not repeat a User-Agent example, but any + # example they do carry must match the declared version. + documented = _documented_user_agents(CURRENT_RELEASE_NOTES) + assert documented <= {expected}, ( + f"{CURRENT_RELEASE_NOTES.name} documents User-Agent versions " + f"{sorted(documented)}, expected only {expected!r}" + ) + + +@pytest.mark.parametrize( + "path", + HISTORICAL_RELEASE_NOTES, + ids=lambda path: path.name, +) +def test_historical_release_notes_keep_their_own_user_agent(path: Path) -> None: + """Historical notes document the version they shipped, not the current one.""" + documented = _documented_user_agents(path) + assert documented <= {f"python-mlb-statsapi/{path.stem}"}, ( + f"{path.name} documents User-Agent versions {sorted(documented)}" + ) + + +def test_public_api_contract_document_exists() -> None: + assert PUBLIC_API_DOC.is_file() + text = PUBLIC_API_DOC.read_text(encoding="utf-8") + assert text.startswith("# Public API Contract (1.x)") + assert "Stability policy" in text + assert "Session ownership" in text + assert "Python support" in text + + +@pytest.mark.parametrize( + "path", + (README, PUBLIC_API_DOC, CURRENT_RELEASE_NOTES), + ids=lambda path: path.name, +) +def test_current_documents_state_the_validated_python_versions(path: Path) -> None: + """Support wording must match the CI matrix this branch establishes.""" + text = path.read_text(encoding="utf-8") + + assert DECLARED_PYTHON_REQUIREMENT in text, ( + f"{path.name} does not state the declared Python requirement" + ) + assert CI_VALIDATED_PYTHON_RANGE in text, ( + f"{path.name} does not state the CI-validated Python range" + ) + for version in SUPPORTED_PYTHON_VERSIONS: + assert version in text, f"{path.name} does not mention Python {version}" + assert UNSUPPORTED_PRERELEASE_PYTHON not in text, ( + f"{path.name} must not mention Python {UNSUPPORTED_PRERELEASE_PYTHON}, " + "which is a prerelease and is not a supported version" + ) + + +# --------------------------------------------------------------------------- +# Synthetic artifacts +# --------------------------------------------------------------------------- + + +def _metadata_text( + *, + name: str = "python-mlb-statsapi", + version: str = SYNTHETIC_VERSION, + requires_python: str = DECLARED_PYTHON_REQUIREMENT, +) -> str: + return ( + "Metadata-Version: 2.1\n" + f"Name: {name}\n" + f"Version: {version}\n" + f"Requires-Python: {requires_python}\n" + "\n" + "Synthetic metadata for release-validator tests.\n" + ) + + +def _write_wheel( + dist_dir: Path, + *, + version: str = SYNTHETIC_VERSION, + tag: str = "py3-none-any", + metadata_name: str = "python-mlb-statsapi", + metadata_version: str | None = None, + requires_python: str = DECLARED_PYTHON_REQUIREMENT, + metadata_files: int = 1, +) -> Path: + """Write a synthetic wheel ZIP with controllable ``.dist-info`` metadata.""" + dist_dir.mkdir(parents=True, exist_ok=True) + wheel = dist_dir / f"python_mlb_statsapi-{version}-{tag}.whl" + raw_metadata = _metadata_text( + name=metadata_name, + version=metadata_version or version, + requires_python=requires_python, + ) + with zipfile.ZipFile(wheel, "w") as archive: + archive.writestr("mlbstatsapi/__init__.py", "") + for index in range(metadata_files): + suffix = "" if index == 0 else f".extra{index}" + dist_info = f"python_mlb_statsapi-{version}{suffix}.dist-info" + archive.writestr(f"{dist_info}/METADATA", raw_metadata) + archive.writestr(f"{dist_info}/WHEEL", "Wheel-Version: 1.0\n") + return wheel + + +def _write_sdist( + dist_dir: Path, + *, + version: str = SYNTHETIC_VERSION, + paths: tuple[str, ...] | None = None, +) -> Path: + """Write a synthetic source-distribution tarball with a versioned root.""" + dist_dir.mkdir(parents=True, exist_ok=True) + sdist = dist_dir / f"python_mlb_statsapi-{version}.tar.gz" + root = f"python_mlb_statsapi-{version}" + contents = validator.REQUIRED_SDIST_PATHS if paths is None else paths + with tarfile.open(sdist, "w:gz") as archive: + for relative in contents: + payload = b"synthetic\n" + info = tarfile.TarInfo(f"{root}/{relative}") + info.size = len(payload) + archive.addfile(info, io.BytesIO(payload)) + return sdist + + +def _classify_command(command) -> str: + parts = [str(part) for part in command] + joined = " ".join(parts) + if "release_smoke_test.py" in joined: + return "smoke" + if "--upgrade" in parts: + return "pip-upgrade" + if "install" in parts: + return "install" + return "other" + + +class _CompletedProcess: + def __init__(self, returncode: int): + self.returncode = returncode + + +def _stub_clean_install(monkeypatch, *, failing: str | None = None) -> list[list[str]]: + """Stub environment creation and subprocess execution for install tests. + + ``failing`` selects the step that returns a non-zero exit code: ``install`` + for the artifact installation or ``smoke`` for the installed-package smoke + test. Only the validator's own ``subprocess`` reference is replaced, so no + real interpreter, environment, or download is involved. + """ + commands: list[list[str]] = [] + + monkeypatch.setattr( + validator, + "_create_clean_environment", + lambda venv_dir: Path(sys.executable), + ) + + def fake_run(command, cwd=None, check=False, **kwargs): + commands.append([str(part) for part in command]) + returncode = 1 if _classify_command(command) == failing else 0 + return _CompletedProcess(returncode) + + monkeypatch.setattr(validator, "subprocess", types.SimpleNamespace(run=fake_run)) + return commands + + +# --------------------------------------------------------------------------- +# Expected-version handling +# --------------------------------------------------------------------------- + + +def test_expected_version_is_read_from_pyproject(tmp_path: Path) -> None: + """The validator stays version-aware instead of pinning one release.""" + (tmp_path / "pyproject.toml").write_text( + '[tool.poetry]\nname = "python-mlb-statsapi"\nversion = "2.3.4"\n', + encoding="utf-8", + ) + + assert validator._read_expected_version(tmp_path) == "2.3.4" + + +def test_declared_project_version_is_the_default_expected_version() -> None: + assert validator._read_expected_version(PROJECT_ROOT) == _project_version() + + +def test_missing_project_version_is_reported(tmp_path: Path) -> None: + (tmp_path / "pyproject.toml").write_text( + '[tool.poetry]\nname = "python-mlb-statsapi"\n', + encoding="utf-8", + ) + + with pytest.raises(validator.ValidationError, match="could not find a version"): + validator._read_expected_version(tmp_path) + + +def test_missing_dist_directory_is_reported(tmp_path: Path, capsys) -> None: + missing = tmp_path / "dist" + + exit_code = validator.main( + ["--dist", str(missing), "--expected-version", SYNTHETIC_VERSION] + ) + + assert exit_code == 1 + message = capsys.readouterr().err + assert str(missing) in message + assert "does not exist" in message + assert "poetry build" in message + + +# --------------------------------------------------------------------------- +# Artifact discovery failures +# --------------------------------------------------------------------------- + + +def test_missing_wheel_is_reported(tmp_path: Path) -> None: + _write_sdist(tmp_path) + + with pytest.raises(validator.ValidationError) as exc_info: + validator.validate(tmp_path, SYNTHETIC_VERSION) + + message = str(exc_info.value) + assert validator.WHEEL_LABEL in message + assert f"python_mlb_statsapi-{SYNTHETIC_VERSION}-*.whl" in message + assert "poetry build" in message + + +def test_missing_source_distribution_is_reported(tmp_path: Path) -> None: + wheel = _write_wheel(tmp_path) + + with pytest.raises(validator.ValidationError) as exc_info: + validator.validate(tmp_path, SYNTHETIC_VERSION) + + message = str(exc_info.value) + assert validator.SDIST_LABEL in message + assert f"python_mlb_statsapi-{SYNTHETIC_VERSION}.tar.gz" in message + # The wheel is present, so the actual directory contents are reported. + assert wheel.name in message + + +def test_multiple_stale_wheels_are_reported(tmp_path: Path) -> None: + first = _write_wheel(tmp_path, tag="py3-none-any") + second = _write_wheel(tmp_path, tag="py310-none-any") + _write_sdist(tmp_path) + + with pytest.raises(validator.ValidationError) as exc_info: + validator.validate(tmp_path, SYNTHETIC_VERSION) + + message = str(exc_info.value) + assert validator.WHEEL_LABEL in message + assert first.name in message + assert second.name in message + assert "Remove stale artifacts" in message + + +def test_multiple_stale_source_distributions_are_reported(tmp_path: Path) -> None: + """Two sdists matching one lookup pattern must be rejected, not guessed. + + ``validate()`` looks the sdist up by its exact versioned filename, so this + exercises the shared discovery helper directly with a wildcard pattern. + """ + first = _write_sdist(tmp_path, version=SYNTHETIC_VERSION) + second = _write_sdist(tmp_path, version=f"{SYNTHETIC_VERSION}rc1") + + with pytest.raises(validator.ValidationError) as exc_info: + validator._find_single( + tmp_path, + "python_mlb_statsapi-*.tar.gz", + validator.SDIST_LABEL, + ) + + message = str(exc_info.value) + assert validator.SDIST_LABEL in message + assert first.name in message + assert second.name in message + assert "Remove stale artifacts" in message + + +# --------------------------------------------------------------------------- +# Wheel metadata failures +# --------------------------------------------------------------------------- + + +def test_wheel_metadata_is_accepted_when_correct(tmp_path: Path) -> None: + wheel = _write_wheel(tmp_path) + + validator._check_wheel_metadata(wheel, SYNTHETIC_VERSION) + + +def test_incorrect_wheel_name_metadata_is_reported(tmp_path: Path) -> None: + wheel = _write_wheel(tmp_path, metadata_name="mlb-statsapi") + + with pytest.raises(validator.ValidationError) as exc_info: + validator._check_wheel_metadata(wheel, SYNTHETIC_VERSION) + + message = str(exc_info.value) + assert validator.WHEEL_LABEL in message + assert wheel.name in message + assert "Name" in message + assert "'mlb-statsapi'" in message + assert "'python-mlb-statsapi'" in message + + +def test_incorrect_wheel_version_metadata_is_reported(tmp_path: Path) -> None: + wheel = _write_wheel(tmp_path, metadata_version="0.9.0") + + with pytest.raises(validator.ValidationError) as exc_info: + validator._check_wheel_metadata(wheel, SYNTHETIC_VERSION) + + message = str(exc_info.value) + assert validator.WHEEL_LABEL in message + assert wheel.name in message + assert "Version" in message + assert "'0.9.0'" in message + assert f"'{SYNTHETIC_VERSION}'" in message + + +def test_incorrect_requires_python_metadata_is_reported(tmp_path: Path) -> None: + wheel = _write_wheel(tmp_path, requires_python=">=3.8") + + with pytest.raises(validator.ValidationError) as exc_info: + validator._check_wheel_metadata(wheel, SYNTHETIC_VERSION) + + message = str(exc_info.value) + assert validator.WHEEL_LABEL in message + assert wheel.name in message + assert "Requires-Python" in message + assert "'>=3.8'" in message + assert f"'{DECLARED_PYTHON_REQUIREMENT}'" in message + + +def test_ambiguous_wheel_metadata_is_reported(tmp_path: Path) -> None: + wheel = _write_wheel(tmp_path, metadata_files=2) + + with pytest.raises(validator.ValidationError) as exc_info: + validator._check_wheel_metadata(wheel, SYNTHETIC_VERSION) + + message = str(exc_info.value) + assert validator.WHEEL_LABEL in message + assert wheel.name in message + assert "METADATA" in message + + +def test_expected_requires_python_matches_pyproject() -> None: + assert validator.EXPECTED_REQUIRES_PYTHON == DECLARED_PYTHON_REQUIREMENT + assert ( + f'python = "{DECLARED_PYTHON_REQUIREMENT}"' + in PYPROJECT.read_text(encoding="utf-8") + ) + + +# --------------------------------------------------------------------------- +# Source-distribution content failures +# --------------------------------------------------------------------------- + + +def test_source_distribution_contents_are_accepted_when_complete( + tmp_path: Path, +) -> None: + sdist = _write_sdist(tmp_path) + + validator._check_sdist_contents(sdist) + + +@pytest.mark.parametrize("omitted", validator.REQUIRED_SDIST_PATHS) +def test_missing_required_source_distribution_path_is_reported( + tmp_path: Path, + omitted: str, +) -> None: + remaining = tuple( + path for path in validator.REQUIRED_SDIST_PATHS if path != omitted + ) + sdist = _write_sdist(tmp_path, paths=remaining) + + with pytest.raises(validator.ValidationError) as exc_info: + validator._check_sdist_contents(sdist) + + message = str(exc_info.value) + assert validator.SDIST_LABEL in message + assert sdist.name in message + assert omitted in message + + +def test_required_source_distribution_paths_cover_the_package_entry_points() -> None: + """The required list must include the files needed to rebuild and import.""" + required = set(validator.REQUIRED_SDIST_PATHS) + + assert {"README.md", "pyproject.toml", "mlbstatsapi/__init__.py"} <= required + assert "mlbstatsapi/mlb_api.py" in required + assert "mlbstatsapi/mlb_dataadapter.py" in required + # Tests, docs, and scripts are intentionally absent from the sdist. + assert not any(path.startswith(("tests/", "docs/", "scripts/")) for path in required) + + +# --------------------------------------------------------------------------- +# Clean-install and smoke-test failures +# --------------------------------------------------------------------------- + + +def test_wheel_installation_failure_identifies_the_artifact( + monkeypatch, + tmp_path: Path, +) -> None: + wheel = _write_wheel(tmp_path) + _stub_clean_install(monkeypatch, failing="install") + + with pytest.raises(validator.ValidationError) as exc_info: + validator._check_clean_install( + wheel, + SYNTHETIC_VERSION, + label=validator.WHEEL_LABEL, + ) + + message = str(exc_info.value) + assert f"{validator.WHEEL_LABEL} installation" in message + assert wheel.name in message + assert "exit code 1" in message + + +def test_source_distribution_installation_failure_identifies_the_artifact( + monkeypatch, + tmp_path: Path, +) -> None: + sdist = _write_sdist(tmp_path) + _stub_clean_install(monkeypatch, failing="install") + + with pytest.raises(validator.ValidationError) as exc_info: + validator._check_clean_install( + sdist, + SYNTHETIC_VERSION, + label=validator.SDIST_LABEL, + ) + + message = str(exc_info.value) + assert f"{validator.SDIST_LABEL} installation" in message + assert sdist.name in message + assert "exit code 1" in message + + +@pytest.mark.parametrize( + "label", + (validator.WHEEL_LABEL, validator.SDIST_LABEL), +) +def test_smoke_test_failure_identifies_the_artifact( + monkeypatch, + tmp_path: Path, + label: str, +) -> None: + artifact = ( + _write_wheel(tmp_path) + if label == validator.WHEEL_LABEL + else _write_sdist(tmp_path) + ) + _stub_clean_install(monkeypatch, failing="smoke") + + with pytest.raises(validator.ValidationError) as exc_info: + validator._check_clean_install(artifact, SYNTHETIC_VERSION, label=label) + + message = str(exc_info.value) + assert f"{label} smoke test" in message + assert "exit code 1" in message + + +def test_clean_install_runs_the_artifact_and_smoke_test_from_a_temp_workspace( + monkeypatch, + tmp_path: Path, +) -> None: + wheel = _write_wheel(tmp_path) + commands = _stub_clean_install(monkeypatch) + + validator._check_clean_install( + wheel, + SYNTHETIC_VERSION, + label=validator.WHEEL_LABEL, + ) + + steps = [_classify_command(command) for command in commands] + assert steps == ["pip-upgrade", "install", "smoke"] + + install_command = commands[steps.index("install")] + assert str(wheel.resolve()) in install_command + + smoke_command = commands[steps.index("smoke")] + assert smoke_command[-1] == SYNTHETIC_VERSION + smoke_script = Path(smoke_command[-2]) + # The script is written into a throwaway workspace, never the checkout. + assert smoke_script.name == "release_smoke_test.py" + assert PROJECT_ROOT not in smoke_script.parents + + +def test_each_artifact_is_installed_into_its_own_environment( + monkeypatch, + tmp_path: Path, +) -> None: + wheel = _write_wheel(tmp_path) + sdist = _write_sdist(tmp_path) + created: list[Path] = [] + + def record_environment(venv_dir: Path) -> Path: + created.append(venv_dir) + return Path(sys.executable) + + monkeypatch.setattr(validator, "_create_clean_environment", record_environment) + monkeypatch.setattr( + validator, + "subprocess", + types.SimpleNamespace(run=lambda *args, **kwargs: _CompletedProcess(0)), + ) + + validator._check_clean_install( + wheel, + SYNTHETIC_VERSION, + label=validator.WHEEL_LABEL, + ) + validator._check_clean_install( + sdist, + SYNTHETIC_VERSION, + label=validator.SDIST_LABEL, + ) + + assert len(created) == 2 + assert created[0] != created[1] + + +def test_validate_clean_installs_both_artifacts(monkeypatch, tmp_path: Path) -> None: + """validate() must clean-install the wheel and the source distribution.""" + wheel = _write_wheel(tmp_path) + sdist = _write_sdist(tmp_path) + installs: list[tuple[Path, str, str]] = [] + + def record_install(artifact: Path, expected_version: str, *, label: str) -> None: + installs.append((artifact, expected_version, label)) + + monkeypatch.setattr(validator, "_check_clean_install", record_install) + + validator.validate(tmp_path, SYNTHETIC_VERSION) + + assert installs == [ + (wheel, SYNTHETIC_VERSION, validator.WHEEL_LABEL), + (sdist, SYNTHETIC_VERSION, validator.SDIST_LABEL), + ] + + +def test_validate_reports_success_for_both_artifacts( + monkeypatch, + tmp_path: Path, + capsys, +) -> None: + _write_wheel(tmp_path) + _write_sdist(tmp_path) + _stub_clean_install(monkeypatch) + + validator.validate(tmp_path, SYNTHETIC_VERSION) + + output = capsys.readouterr().out + assert f"installing {validator.WHEEL_LABEL}" in output + assert f"running {validator.WHEEL_LABEL} smoke test" in output + assert f"installing {validator.SDIST_LABEL}" in output + assert f"running {validator.SDIST_LABEL} smoke test" in output + assert "Release validation passed" in output + + +def test_missing_interpreter_in_environment_is_reported(tmp_path: Path) -> None: + with pytest.raises(validator.ValidationError, match="no interpreter found"): + validator._venv_python(tmp_path / "venv") + + +# --------------------------------------------------------------------------- +# Installed smoke-test contract +# --------------------------------------------------------------------------- + + +def test_smoke_test_source_is_valid_python() -> None: + compile(validator.SMOKE_TEST_SOURCE, "release_smoke_test.py", "exec") + + +def test_smoke_test_labels_reverted_strict_defaults() -> None: + """A reverted strict default must fail with an explanatory message. + + An unlabelled AssertionError would not tell a release engineer which + constructor regressed, so both messages are asserted here and in the + generated smoke test. + """ + assert validator.MLB_STRICT_DEFAULT_MESSAGE == ( + "Mlb.strict_http must default to True for the 1.0 contract" + ) + assert validator.ADAPTER_STRICT_DEFAULT_MESSAGE == ( + "MlbDataAdapter.strict_http must default to True for the 1.0 contract" + ) + + source = validator.SMOKE_TEST_SOURCE + assert ( + 'assert mlb_init["strict_http"].default is True, MLB_STRICT_DEFAULT_MESSAGE' + in source + ) + assert ( + 'assert adapter_init["strict_http"].default is True, ' + "ADAPTER_STRICT_DEFAULT_MESSAGE" in source + ) + for message in ( + validator.MLB_STRICT_DEFAULT_MESSAGE, + validator.ADAPTER_STRICT_DEFAULT_MESSAGE, + ): + assert message in source + + +def test_smoke_test_asserts_strict_http_default() -> None: + """The installed-artifact smoke test must match the 1.0 strict default.""" + text = VALIDATE_RELEASE.read_text(encoding="utf-8") + assert 'mlb_init["strict_http"].default is True' in text + assert 'adapter_init["strict_http"].default is True' in text + assert "Compatibility mode is the default in this release." not in text + + +def test_smoke_test_checks_strict_behavior_not_only_signatures() -> None: + """Signature defaults alone cannot prove a final 403 raises.""" + source = validator.SMOKE_TEST_SOURCE + + assert "status_code = 403" in source + assert 'reason = "Forbidden"' in source + assert "https://statsapi.mlb.com/api/v1/sports" in source + assert "https://statsapi.mlb.com/api/v1.1/sports" in source + assert "get_sports" in source + assert "Mlb(session=session, strict_http=True)" in source + assert "Mlb(session=session, strict_http=False)" in source + assert "strict_http=True," in source + assert "strict_http=False," in source + assert "MlbHttpCompatibilityWarning" in source + assert "strict_http=False" in source + + +def test_smoke_test_checks_injected_session_ownership_and_configuration() -> None: + source = validator.SMOKE_TEST_SOURCE + + assert "class OwnershipSession(requests.Session):" in source + assert "release-smoke-test/1.0" in source + assert "X-Release-Test" in source + assert "injected_https_adapter" in source + assert "injected_http_adapter" in source + assert "is injected_https_adapter" in source + assert "is injected_http_adapter" in source + assert "must not close a caller-injected Session" in source + assert "must not mount its retry policy on an injected" in source + assert "finally:\n session.close()" in source + + +def test_smoke_test_checks_library_created_session_configuration() -> None: + source = validator.SMOKE_TEST_SOURCE + + assert 'f"python-mlb-statsapi/{expected_version}"' in source + assert "assert_documented_retry_policy" in source + assert "create_retry_policy() must return a new Retry instance per call" in source + + +@pytest.mark.parametrize( + "symbol", + ( + "Mlb", + "MlbDataAdapter", + "MlbResult", + "create_retry_policy", + "TheMlbStatsApiException", + "MlbTransportError", + "MlbTimeoutError", + "MlbHttpError", + "MlbDecodeError", + "MlbHttpCompatibilityWarning", + "return_splits", + "get_stat_attributes", + ), +) +def test_smoke_test_imports_the_supported_public_symbol(symbol: str) -> None: + assert f" {symbol},\n" in validator.SMOKE_TEST_SOURCE + + +def test_smoke_test_does_not_promote_accidental_submodules() -> None: + """Accidentally exposed submodules stay outside the supported surface.""" + source = validator.SMOKE_TEST_SOURCE + + for submodule in ("mlb_api", "mlb_module", "models"): + assert f"from mlbstatsapi import {submodule}" not in source + assert f"import mlbstatsapi.{submodule}" not in source + + +def test_smoke_test_runs_against_the_installed_distribution() -> None: + source = validator.SMOKE_TEST_SOURCE + + assert "sys.prefix != sys.base_prefix" in source + assert 'sysconfig.get_paths()["purelib"]' in source + assert "is_relative_to(site_packages)" in source + + +def test_smoke_test_makes_no_live_mlb_request() -> None: + source = validator.SMOKE_TEST_SOURCE + + assert "requests.get(" not in source + assert "session.request(" not in source + assert "class ForbiddenSession:" in source + assert "never reaches the MLB API" in source + + +def test_validator_is_not_pinned_to_a_single_release_version() -> None: + """1.0.0 may appear as a usage example, never as the only accepted version.""" + source = VALIDATE_RELEASE.read_text(encoding="utf-8") + + assert 'EXPECTED_VERSION = "1.0.0"' not in source + assert "_read_expected_version" in source + assert "--expected-version" in source + # Terminology now covers both artifacts, not just the wheel. + assert "installed distribution artifact" in source + assert "clean wheel installation" not in source + assert "installed wheel" not in source + + +# --------------------------------------------------------------------------- +# Deterministic CI contract +# --------------------------------------------------------------------------- + + +def _matrix_python_versions() -> list[str]: + text = OFFLINE_WORKFLOW.read_text(encoding="utf-8") + match = re.search( + r"^\s+python-version:\n((?:\s+- \"[^\"]+\"\n)+)", + text, + flags=re.MULTILINE, + ) + assert match is not None, "no python-version matrix found in the offline workflow" + return re.findall(r'- "([^"]+)"', match.group(1)) + def test_ci_watches_the_current_release_branch() -> None: - workflow = PROJECT_ROOT / ".github" / "workflows" / "build-and-test.yml" - text = workflow.read_text(encoding="utf-8") - major, minor, _ = _project_version().split(".") + """Pull requests and pushes must watch main and release/1.0.0. + + The trigger is asserted literally instead of being derived from the package + version, which is still 0.9.0 until the release bump lands. + """ + text = OFFLINE_WORKFLOW.read_text(encoding="utf-8") + + assert text.count(f"- {RELEASE_BRANCH}") == 2, text + assert text.count("- main") == 2, text + assert STALE_RELEASE_BRANCH not in text, ( + f"the stale {STALE_RELEASE_BRANCH} trigger must be removed" + ) + assert "workflow_dispatch:" in text + + +def test_ci_matrix_covers_every_supported_python_version() -> None: + assert _matrix_python_versions() == list(SUPPORTED_PYTHON_VERSIONS) + + +def test_ci_matrix_excludes_prerelease_python() -> None: + """No job may set up a prerelease interpreter, matrix or otherwise.""" + text = OFFLINE_WORKFLOW.read_text(encoding="utf-8") + + assert UNSUPPORTED_PRERELEASE_PYTHON not in _matrix_python_versions() + assert f'- "{UNSUPPORTED_PRERELEASE_PYTHON}"' not in text + assert f'python-version: "{UNSUPPORTED_PRERELEASE_PYTHON}"' not in text + + +def test_ci_minimum_python_matches_the_declared_requirement() -> None: + versions = _matrix_python_versions() + + assert versions[0] == "3.10" + assert ( + f'python = "{DECLARED_PYTHON_REQUIREMENT}"' + in PYPROJECT.read_text(encoding="utf-8") + ) + + +def test_ci_build_job_validates_and_twine_checks_the_artifacts() -> None: + text = OFFLINE_WORKFLOW.read_text(encoding="utf-8") + + assert "rm -rf dist" in text + assert "poetry build" in text + assert "python scripts/validate_release.py" in text + assert "poetry run twine check dist/*" in text + assert f'python-version: "{BUILD_JOB_PYTHON}"' in text + + +def test_ci_runs_offline_tests_without_the_live_suite() -> None: + text = OFFLINE_WORKFLOW.read_text(encoding="utf-8") + + assert "--ignore=tests/external_tests" in text + assert "tests/external_tests/" not in text + + +def test_live_tests_stay_in_a_separate_workflow() -> None: + text = EXTERNAL_WORKFLOW.read_text(encoding="utf-8") + + assert "tests/external_tests/" in text + assert "workflow_dispatch:" in text + assert "schedule:" in text + # Live tests must not be attached to ordinary pushes or pull requests. + assert "pull_request:" not in text + assert "push:" not in text + + +@pytest.mark.parametrize( + "forbidden", + ( + "poetry publish", + "twine upload", + "PYPI_TOKEN", + "PYPI_API_TOKEN", + "POETRY_PYPI_TOKEN", + "TEST_PYPI", + "TESTPYPI", + "pypa/gh-action-pypi-publish", + "softprops/action-gh-release", + "gh release create", + "git tag", + ), +) +def test_no_workflow_publishes_or_tags(forbidden: str) -> None: + for workflow in (OFFLINE_WORKFLOW, EXTERNAL_WORKFLOW): + text = workflow.read_text(encoding="utf-8") + assert forbidden not in text, f"{workflow.name} contains {forbidden!r}" + + +def test_twine_is_a_development_dependency_only() -> None: + text = PYPROJECT.read_text(encoding="utf-8") + sections = dict( + re.findall(r"^\[([^\]]+)\]\n((?:(?!\[)[^\n]*\n)*)", text, flags=re.MULTILINE) + ) + + runtime = sections["tool.poetry.dependencies"] + development = sections["tool.poetry.group.dev.dependencies"] + + assert "twine" not in runtime, "twine must not become a runtime dependency" + assert re.search(r"^twine = ", development, flags=re.MULTILINE), development + - assert f"release/{major}.{minor}.0" in text - assert "- main" in text +def test_twine_is_locked() -> None: + assert 'name = "twine"' in POETRY_LOCK.read_text(encoding="utf-8")