diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 7bbc5d9c..73b19a75 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -24,11 +24,13 @@ jobs: - run: uv run python tools/generate_public.py --check - run: uv run --extra files pytest -q # Keep the byte-frozen runtime and unrelated examples out of formatting. - - run: uv run ruff check tools/generate_client.py tools/generate_public.py tools/probe_public.py tinyercot/catalog.py tinyercot/public tests - - run: uv run ruff format --check tools/generate_client.py tools/generate_public.py tools/probe_public.py tinyercot/catalog.py tinyercot/public tests + - run: uv run ruff check tools tinyercot/catalog.py tinyercot/public tests + - run: uv run ruff format --check tools tinyercot/catalog.py tinyercot/public tests - run: uv build - - run: uv export --frozen --no-dev --no-emit-project --output-file /tmp/tinyercot-requirements.txt + # Include locked typing dependencies for the separate PEP 561 consumer. + - run: uv export --frozen --no-emit-project --output-file /tmp/tinyercot-requirements.txt - run: uv venv /tmp/tinyercot-wheel - run: uv pip sync --python /tmp/tinyercot-wheel/bin/python /tmp/tinyercot-requirements.txt - run: uv pip install --python /tmp/tinyercot-wheel/bin/python --no-deps dist/*.whl - run: /tmp/tinyercot-wheel/bin/python -I tests/wheel_smoke.py + - run: uv run python tools/check_typing.py --python /tmp/tinyercot-wheel/bin/python diff --git a/CLAUDE.md b/CLAUDE.md index c89f5319..72f22fe3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -3,7 +3,7 @@ TinyERCOT preserves its legacy public Python API. The opt-in `tinyercot.catalog` module supplies offline metadata only. Read `docs/public-foundation.md` before adding current data capabilities. Read `docs/public-retrieval.md` for the opt-in -two-endpoint API client, annual file adapter, and ESR website feed. +four-endpoint registry client, annual file adapter, and ESR website feed. ## Local checks @@ -12,8 +12,8 @@ uv sync --frozen --group dev --extra files uv run python tools/generate_client.py --check uv run python tools/generate_public.py --check uv run --extra files pytest -q -uv run ruff check tools/generate_client.py tools/generate_public.py tools/probe_public.py tinyercot/catalog.py tinyercot/public tests -uv run ruff format --check tools/generate_client.py tools/generate_public.py tools/probe_public.py tinyercot/catalog.py tinyercot/public tests +uv run ruff check tools tinyercot/catalog.py tinyercot/public tests +uv run ruff format --check tools tinyercot/catalog.py tinyercot/public tests uv build ``` @@ -36,6 +36,9 @@ It has no `--refresh`, `--cache-products`, or `--pandas` option. New endpoints need actual field evidence. Cached legacy rows do not qualify. Real integration tests require explicit opt-in paths and an installed wheel. +After installing the wheel outside the source tree, run +`uv run python tools/check_typing.py --python /absolute/venv/bin/python`. +CI checks PEP 561 contracts against that installed distribution. The normal suite blocks sockets and skips them. Do not print credential files, auth responses, tokens, or HTTPX request objects from real requests. diff --git a/MANIFEST.in b/MANIFEST.in index 0038c8c4..7f734b75 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,3 +2,4 @@ include api_response_fields.json uv.lock recursive-include tools *.py *.json recursive-include tests *.py *.json recursive-include docs *.md *.json +recursive-include tinyercot *.pyi py.typed diff --git a/README.md b/README.md index 3154c9e9..2b3f9e1e 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,8 @@ Python client with 102 legacy typed ERCOT Public API endpoint families. Legacy imports, signatures, and behavior stay fixed. Legacy server compatibility -is not verified. The opt-in `tinyercot.public` surface supports two current DAM -price endpoints, bounded annual DAM file samples, and the rolling website ESR +is not verified. The opt-in `tinyercot.public` surface supports four current DAM, +RT-price and load endpoints, annual DAM file iteration, and the rolling website ESR feed. The separate offline catalog records observed sources and access boundaries. ## Install @@ -113,7 +113,7 @@ See [scope, provenance, and adapter boundaries](docs/public-foundation.md). ## Opt-in public retrieval -The new client has **2 generated typed operations in 2 products out of 243 +The new client has **4 generated typed operations in 4 products out of 243 observed public data paths across 98 product namespaces**. This is partial coverage. Unknown row schemas fail closed. Restricted services remain excluded. @@ -144,6 +144,15 @@ request and byte limits. Its errors, row models, and pagination are separate from the legacy DataFrame and exception contracts. See [exact coverage, annual files, evidence, and limits](docs/public-retrieval.md). +`ReportsClient` adds generated filters and streaming iteration for all four +verified endpoints. Its page, row, and request budgets can be removed explicitly +for a complete caller-selected query. Budget exhaustion raises an error. +`iter_dam_archive` similarly supports every worksheet and row in one selected +annual file. See the [complete iteration examples](docs/public-retrieval.md#complete-query-and-annual-file-iteration). + +The wheel includes PEP 561 metadata and legacy transport stubs. Installed-wheel +checks verify 510 legacy methods, 1,334 row fields, and new public contracts. + ## Development and legacy generation Generation uses hash-pinned local inputs. It never fetches an upstream URL. @@ -153,8 +162,8 @@ uv sync --frozen --group dev --extra files uv run python tools/generate_client.py --check uv run python tools/generate_public.py --check uv run --extra files pytest -q -uv run ruff check tools/generate_client.py tools/generate_public.py tools/probe_public.py tinyercot/catalog.py tinyercot/public tests -uv run ruff format --check tools/generate_client.py tools/generate_public.py tools/probe_public.py tinyercot/catalog.py tinyercot/public tests +uv run ruff check tools tinyercot/catalog.py tinyercot/public tests +uv run ruff format --check tools tinyercot/catalog.py tinyercot/public tests uv build ``` @@ -164,5 +173,9 @@ working directory. `--check` never writes. Missing inputs or changed hashes fail before output is written. The former authenticated `--refresh` and `--cache-products` developer commands are removed. Metadata refresh and current API generation use separate inputs. `tools/generate_public.py` generates only -the two source-backed current models from `tools/inputs/current/`. It never +the registered source-backed current models from `tools/inputs/current/`. It never downloads upstream specifications or overwrites the legacy generated file. +`tools/discover_public.py` projects additional contracts offline from saved primary +OpenAPI bytes, a bounded public response, and its matching receipt. Missing or +unknown schemas cannot be generated. CI installs the wheel outside the checkout +and runs `tools/check_typing.py --python /tmp/tinyercot-wheel/bin/python`. diff --git a/docs/current-iteration-task-note.md b/docs/current-iteration-task-note.md new file mode 100644 index 00000000..58894bf1 --- /dev/null +++ b/docs/current-iteration-task-note.md @@ -0,0 +1,143 @@ +# Current iteration and typing task note + +Status: Implementation and local checks complete on feat/public-rt-load, stacked +on PR #2 at 01d8b69c4378040ca14d9683e9693e65cc3f93a8. PR #1 and #2 remain open +and unmerged. The new installed-client proof establishes 4/243 typed paths in +4/98 observed product namespaces. Direct PR handoff follows these checks. + +Active scope from inbox 005: reusable schema discovery/generation; complete +filtered API page/row iteration; full worksheet/row iteration for one requested +public annual file; and package-wide static typing with installed-wheel proof. +Real validation stays bounded. Synthetic fixtures prove long iteration without +extracting real bulk history. Legacy runtime files stay byte-identical. + +## NumPy audit (inbox 006) + +`git show d1daad25df42d3fff41f88b907d99ef325b970e0:uv.lock` already pins NumPy +2.4.0 (line 212). PR #1 and #2 retain that pin. This work did not introduce it. +The pre-correction local environment used NumPy 2.4.0 with pandas 2.3.3. PR #2's +941 tests passed on Python 3.11 and 3.14 with that locked dependency set. + +[PyPI's 2.4.0 metadata](https://pypi.org/pypi/numpy/2.4.0/json) reports all 72 +release files yanked for "Backward compatibility bug". The corresponding +[2.4.1 metadata](https://pypi.org/pypi/numpy/2.4.1/json) reports 72 non-yanked +files and the same Python >=3.11 requirement. Both were checked on 2026-09-05. + +The narrow correction is `uv lock --upgrade-package numpy==2.4.1`. This changes +the locked NumPy patch release only. It does not add a new runtime dependency +constraint or broadly upgrade runtime packages. New mypy/pandas-stubs packages +are development tooling for the requested static typing proof. Normal package +installs were not pinned to the yanked version; the repository lock was. +The corrected lock passes the full suite on Python 3.11.14 and 3.14.0 with +NumPy 2.4.1/pandas 2.3.3. Isolated wheel installs also pass import, nonempty +legacy DataFrame conversion, mocked typed RT decoding, and PEP 561 checks with +Python 3.11.14/NumPy 1.24.0/pandas 2.0.0/Pydantic 2.10.0 and Python 3.14.0/ +NumPy 2.5.2/pandas 3.0.5/Pydantic 2.13.5. These are practical boundary samples, +not an exhaustive dependency Cartesian product. No runtime dependency constraint +changed. Only NumPy changed among pre-existing locked runtime package versions. + +## Delivered scope and evidence + +- Shared offline schema projection from the hash-pinned primary Public Reports + OpenAPI export and exact public response/receipt pairs. Missing, null-only, + mistyped, and unknown schema evidence cannot become a typed adapter. Generation + rejects unsupported formats, conflicting names, missing status, and pin drift. +- Registered row models, TypedDict filters, and endpoint constants for DAM + settlement and capacity prices, RT settlement prices, and weather-zone load. + `ReportsClient` supports every pinned query filter and streams pages/rows with + configurable total budgets, bounded response memory, and consistency checks. +- `iter_dam_archive` visits all requested worksheets/rows of one public annual + report-13060 ZIP, retaining member/sheet/row/hash provenance. Existing sampling + defaults remain intact. Synthetic tests exhaust 2,010 rows across two sheets; + API tests exhaust 125 pages without network access. +- PEP 561 marker and companion legacy stubs. The real installed-wheel proof + checks all 510 legacy methods, all 1,334 row fields, public typed filters and + adapters, and four rejected invalid uses. Assertions run from a temporary + directory with PYTHONPATH/MYPYPATH removed and a separate installed interpreter. + No source-tree substitute or ignore-missing-imports fallback is used. +- All 39 audited families, 257 API operations, and two web subsets retain + explicit scope states. Four API operations and the two narrow web subsets are + covered; no broad source family is marked complete. The other 239 data paths, + API archive/bundle routes, additional MIS/live formats, async current clients, + schema epochs, historical completeness, and as-of/revision reconstruction + remain deferred. Secure/Certified/EWS/private records remain restricted; + retired AS/SASM and the missing 2001 load year remain unavailable. + +On 2026-09-06 at 00:11 UTC the actual installed wheel made six bounded public +data requests: two current rows and one oldest-first row for each new endpoint, +plus two one-row pages for a complete filtered RT selection. Both oldest rows +were dated 2023-12-11. Receipts are in +`docs/evidence/installed-reports-receipts.json`; source fixtures and transformations +are in `docs/evidence/fixture-provenance.json`. Prior installed DAM, annual-file, +and ESR evidence is preserved in `docs/evidence/installed-receipts.json`. + +Inbox 001 explicitly authorized existing Public API credentials for these public +requests, superseding the initial audit's unauthenticated-only validation scope. +Authorized authentication occurred; no restricted request, secret copying, paid +access, all-history extraction, forecaster work, no-mistakes, or merge occurred. +The local `.env` remains ignored and mode 0600. No values were printed or committed. + +## Exact local validation + +Commands ran from the repository root unless an isolated interpreter is shown. + +```text +uv sync --frozen --group dev --extra files +uv run --extra files pytest -q + 976 passed, 1 skipped (Python 3.14.0; network blocked) +UV_PROJECT_ENVIRONMENT=/tmp/tinyercot-py311 uv sync --python 3.11 --frozen --group dev --extra files +/tmp/tinyercot-py311/bin/python -m pytest -q + 976 passed, 1 skipped (Python 3.11.14; network blocked) +uv run python tools/generate_client.py --check + Legacy generation matches the pinned baseline +uv run python tools/generate_public.py --check + Current models match pinned response metadata +uv run ruff check tools tinyercot/catalog.py tinyercot/public tests + All checks passed +uv run ruff format --check tools tinyercot/catalog.py tinyercot/public tests + 36 files already formatted +uv build + Built wheel and sdist; log /tmp/tinyercot-iteration-build.log +uv pip install --python /tmp/tinyercot-retrieval-wheel/bin/python --no-deps --reinstall dist/tinyercot-0.2.2-py3-none-any.whl +uv pip install --python /tmp/tinyercot-lower-wheel/bin/python --no-deps --reinstall dist/tinyercot-0.2.2-py3-none-any.whl +/tmp/tinyercot-retrieval-wheel/bin/python -I tests/wheel_smoke.py +/tmp/tinyercot-lower-wheel/bin/python -I tests/wheel_smoke.py + Both passed imports, legacy DataFrame conversion and mocked typed RT decoding +uv run python tools/check_typing.py --python /tmp/tinyercot-retrieval-wheel/bin/python +uv run python tools/check_typing.py --python /tmp/tinyercot-lower-wheel/bin/python + Both passed: 510 methods, 1334 fields, public contracts, four invalid uses rejected +/tmp/tinyercot-retrieval-wheel/bin/python -I tools/probe_public.py --live --reports-only --credentials-file /Users/kevin/.treehouse/tinyercot-1bad13/1/tinyercot/.env --output /Users/kevin/.treehouse/tinyercot-1bad13/1/tinyercot/.local-evidence/installed-reports + Six bounded public data requests passed; eight total rows; no archive download +git diff d1daad25df42d3fff41f88b907d99ef325b970e0 -- tinyercot/__init__.py tinyercot/_client.py tinyercot/_generated.py + Empty: all three legacy runtime files remain byte-identical +git diff --check + Passed +``` + +Lower-install preparation used `uv venv --python 3.11 /tmp/tinyercot-lower-wheel` +and `uv pip install --python /tmp/tinyercot-lower-wheel/bin/python 'pandas==2.0.0' +'numpy==1.24.0' 'pydantic==2.10.0' 'tinyercot[files] @ +file:///Users/kevin/.treehouse/tinyercot-1bad13/1/tinyercot/dist/tinyercot-0.2.2-py3-none-any.whl'`. +The type consumer additionally installed `pandas-stubs==3.0.5.260730 --no-deps`. +CI now exports locked development typing dependencies, installs a separate wheel, +and runs the same proof on Python 3.11 and 3.14 without ERCOT credentials. +The CI preparation was also reproduced locally and passed: + +```text +uv export --frozen --no-emit-project --output-file /tmp/tinyercot-iteration-requirements.txt +uv venv --python 3.11 /tmp/tinyercot-locked-wheel +uv pip sync --python /tmp/tinyercot-locked-wheel/bin/python /tmp/tinyercot-iteration-requirements.txt +uv pip install --python /tmp/tinyercot-locked-wheel/bin/python --no-deps dist/tinyercot-0.2.2-py3-none-any.whl +/tmp/tinyercot-locked-wheel/bin/python -I tests/wheel_smoke.py +uv run python tools/check_typing.py --python /tmp/tinyercot-locked-wheel/bin/python + Both installed-wheel checks passed with the corrected frozen dependency set +``` + +Documentation review: authored public functions document applicable Args, +Returns/Yields, and caller-visible Raises in Google style; generated models +describe source fields. Test helpers follow existing test conventions. The +repository has no approved per-file license boilerplate; none was invented. +The resumed proof initially failed because generated assertions used unqualified +nested model names; the proof was corrected, preserving legacy runtime bytes. +The initial full test run exposed missing fixture provenance; that evidence is +now included. Neither failure was suppressed. diff --git a/docs/evidence/fixture-provenance.json b/docs/evidence/fixture-provenance.json index 1c43110a..cbaa2dd5 100644 --- a/docs/evidence/fixture-provenance.json +++ b/docs/evidence/fixture-provenance.json @@ -166,6 +166,54 @@ "http_last_modified": null }, "transformation": "First two and last two rows from each source day; source timestamps preserved." + }, + { + "fixture": "rt-prices-current.json", + "sha256": "211a6808039b86dfefbdfcab99e8859cf83de61ab67d2da87bd4b4bc13545c6a", + "transformation": "Exact public response bytes.", + "source": { + "source_url": "https://api.ercot.com/api/public-reports/np6-905-cd/spp_node_zone_hub?settlementPoint=HB_HOUSTON&size=2&page=1&sort=deliveryDate&dir=desc&deliveryDateFrom=2026-09-04&deliveryDateTo=2026-09-04", + "retrieved_at": "2026-09-05 21:14:32.319440+00:00", + "sha256": "211a6808039b86dfefbdfcab99e8859cf83de61ab67d2da87bd4b4bc13545c6a", + "byte_count": 1711, + "status": 200 + } + }, + { + "fixture": "rt-prices-oldest.json", + "sha256": "302a06e115f73a1b299564d2c7a12d59cb639c132e3c5036d65e26d9cf2f44a4", + "transformation": "Exact public response bytes.", + "source": { + "source_url": "https://api.ercot.com/api/public-reports/np6-905-cd/spp_node_zone_hub?settlementPoint=HB_HOUSTON&size=1&page=1&sort=deliveryDate&dir=asc", + "retrieved_at": "2026-09-05 21:14:34.390027+00:00", + "sha256": "302a06e115f73a1b299564d2c7a12d59cb639c132e3c5036d65e26d9cf2f44a4", + "byte_count": 1605, + "status": 200 + } + }, + { + "fixture": "system-load-current.json", + "sha256": "e9b68e868a08ba0e04becd610fbbd781421f87545864d26b05f1ac234403cf13", + "transformation": "Exact public response bytes.", + "source": { + "source_url": "https://api.ercot.com/api/public-reports/np6-345-cd/act_sys_load_by_wzn?size=2&page=1&sort=operatingDay&dir=desc&operatingDayFrom=2026-09-04&operatingDayTo=2026-09-04", + "retrieved_at": "2026-09-05 21:14:36.433487+00:00", + "sha256": "e9b68e868a08ba0e04becd610fbbd781421f87545864d26b05f1ac234403cf13", + "byte_count": 2262, + "status": 200 + } + }, + { + "fixture": "system-load-oldest.json", + "sha256": "061222a12fc005428f277f7692ff4e530e2177c2ff8e4fe8e3763426a32e41c7", + "transformation": "Exact public response bytes.", + "source": { + "source_url": "https://api.ercot.com/api/public-reports/np6-345-cd/act_sys_load_by_wzn?size=1&page=1&sort=operatingDay&dir=asc", + "retrieved_at": "2026-09-05 21:14:38.550944+00:00", + "sha256": "061222a12fc005428f277f7692ff4e530e2177c2ff8e4fe8e3763426a32e41c7", + "byte_count": 2098, + "status": 200 + } } ] } diff --git a/docs/evidence/installed-reports-receipts.json b/docs/evidence/installed-reports-receipts.json new file mode 100644 index 00000000..854ff1b5 --- /dev/null +++ b/docs/evidence/installed-reports-receipts.json @@ -0,0 +1,73 @@ +{ + "installed_module": "/private/tmp/tinyercot-retrieval-wheel/lib/python3.14/site-packages/tinyercot/__init__.py", + "receipts": [ + { + "kind": "typed-reports-current", + "source_url": "https://api.ercot.com/api/public-reports/np6-905-cd/spp_node_zone_hub?settlementPoint=HB_HOUSTON&deliveryDateFrom=2026-09-04&deliveryDateTo=2026-09-04&page=1&size=2&dir=desc&sort=deliveryDate", + "retrieved_at": "2026-09-06 00:11:08.089128+00:00", + "sha256": "211a6808039b86dfefbdfcab99e8859cf83de61ab67d2da87bd4b4bc13545c6a", + "byte_count": 1711, + "status": 200, + "endpoint": "/np6-905-cd/spp_node_zone_hub", + "rows": 2, + "first_observed_date": "2026-09-04" + }, + { + "kind": "typed-reports-historical", + "source_url": "https://api.ercot.com/api/public-reports/np6-905-cd/spp_node_zone_hub?settlementPoint=HB_HOUSTON&page=1&size=1&dir=asc&sort=deliveryDate", + "retrieved_at": "2026-09-06 00:11:10.314260+00:00", + "sha256": "b1b0c0845989e9ae6717beb8d093bfcedd9b433971703898429387be1d719a63", + "byte_count": 1605, + "status": 200, + "endpoint": "/np6-905-cd/spp_node_zone_hub", + "rows": 1, + "first_observed_date": "2023-12-11" + }, + { + "kind": "typed-reports-current", + "source_url": "https://api.ercot.com/api/public-reports/np6-345-cd/act_sys_load_by_wzn?operatingDayFrom=2026-09-04&operatingDayTo=2026-09-04&page=1&size=2&dir=desc&sort=operatingDay", + "retrieved_at": "2026-09-06 00:11:12.286973+00:00", + "sha256": "e9b68e868a08ba0e04becd610fbbd781421f87545864d26b05f1ac234403cf13", + "byte_count": 2262, + "status": 200, + "endpoint": "/np6-345-cd/act_sys_load_by_wzn", + "rows": 2, + "first_observed_date": "2026-09-04" + }, + { + "kind": "typed-reports-historical", + "source_url": "https://api.ercot.com/api/public-reports/np6-345-cd/act_sys_load_by_wzn?page=1&size=1&dir=asc&sort=operatingDay", + "retrieved_at": "2026-09-06 00:11:14.396157+00:00", + "sha256": "061222a12fc005428f277f7692ff4e530e2177c2ff8e4fe8e3763426a32e41c7", + "byte_count": 2098, + "status": 200, + "endpoint": "/np6-345-cd/act_sys_load_by_wzn", + "rows": 1, + "first_observed_date": "2023-12-11" + }, + { + "kind": "typed-reports-complete-selection", + "source_url": "https://api.ercot.com/api/public-reports/np6-905-cd/spp_node_zone_hub?settlementPoint=HB_HOUSTON&deliveryDateFrom=2026-09-04&deliveryDateTo=2026-09-04&deliveryHourFrom=1&deliveryHourTo=1&deliveryIntervalFrom=1&deliveryIntervalTo=2&page=1&size=1&dir=asc&sort=deliveryInterval", + "retrieved_at": "2026-09-06 00:11:16.721650+00:00", + "sha256": "1f6339871106b5724302930dcf58f868932db58c825cb9ffa433ed79100ec323", + "byte_count": 1758, + "status": 200, + "endpoint": "/np6-905-cd/spp_node_zone_hub", + "rows": 1, + "page": 1, + "total_records": 2 + }, + { + "kind": "typed-reports-complete-selection", + "source_url": "https://api.ercot.com/api/public-reports/np6-905-cd/spp_node_zone_hub?settlementPoint=HB_HOUSTON&deliveryDateFrom=2026-09-04&deliveryDateTo=2026-09-04&deliveryHourFrom=1&deliveryHourTo=1&deliveryIntervalFrom=1&deliveryIntervalTo=2&page=2&size=1&dir=asc&sort=deliveryInterval", + "retrieved_at": "2026-09-06 00:11:18.548561+00:00", + "sha256": "bbb9edd4c8200d328bf5974736ffb32bf13c57180ecca7cf9e282f96099969c7", + "byte_count": 1758, + "status": 200, + "endpoint": "/np6-905-cd/spp_node_zone_hub", + "rows": 1, + "page": 2, + "total_records": 2 + } + ] +} diff --git a/docs/public-retrieval.md b/docs/public-retrieval.md index 9c00a169..1dad15bf 100644 --- a/docs/public-retrieval.md +++ b/docs/public-retrieval.md @@ -6,10 +6,11 @@ Use `coverage()` for retrieval scope states. ## Coverage and evidence -On 2026-09-05 the isolated installed wheel retrieved and decoded public data. -Generated typed API coverage is **2 operations in 2 products**: NP4-190-CD and -NP4-188-CD. The first installed-client checkpoint covered NP4-190-CD alone; -NP4-188-CD was added from two later source-backed field observations. +On 2026-09-05/06 UTC isolated installed wheels retrieved and decoded public data. +Generated typed API coverage is **4 operations in 4 products**: NP4-190-CD, +NP4-188-CD, NP6-905-CD, and NP6-345-CD. PR #2 established the first two DAM +contracts; this iteration adds RT settlement prices and weather-zone actual load +through a shared registry and retrieval client. The denominator is **243 observed public data paths in 98 product namespaces** (242 public-reports paths and one public-data ESR path). This is a bounded vertical slice, not broad typed coverage. The legacy 102 endpoints do not add @@ -21,9 +22,12 @@ Public Data API. A namespace count does not establish an EMIL product census. | Typed installed-client current fetch | NP4-190-CD DAM settlement prices: two pages of two HB_HOUSTON rows for 2026-09-04; another row after explicit ID-token reacquisition. NP4-188-CD: two REGUP capacity-price rows for 2026-09-04. | | Typed installed-client historical fetch | NP4-190-CD HB_HOUSTON and NP4-188-CD REGUP: one oldest-first row each returned 2023-12-13. NP4-180-ER report 13060: four rows each from 2010/Dec_1 and 2026/Aug workbooks, with truncation and cache reuse verified. | | Typed installed-client live fetch | Website ESR: 482 rolling rows in the final installed probe; source offset/epoch and freshness checks passed. Not four-second API coverage. | +| New typed installed-client current fetch | NP6-905-CD HB_HOUSTON RT prices and NP6-345-CD weather-zone load: two rows each for 2026-09-04. | +| New typed installed-client historical fetch | One oldest-first row per new endpoint, both dated 2023-12-11. These dates establish observations, not complete retention. | +| Complete bounded installed-client query | Two RT rows for HB_HOUSTON, 2026-09-04, hour 1, intervals 1–2, retrieved across two one-row pages with matching totals. | | Real source retrieval only | NP4-190-CD product metadata, retained as a fixture. This is not an implemented product-metadata method. | | Fixture-only behavior | Rate limits, transport retries, expired tokens, 401/403, malformed JSON/ZIP/XLSX, cache mutation, repeated pages, DST transitions. These faults were simulated, not induced on ERCOT. | -| Deferred schema | The other 241 observed data paths. Stage 0 recorded 40 missing and 203 unverified cached schemas. Current field evidence verifies two endpoints. No guessed raw or typed models. | +| Deferred schema | The other 239 observed data paths. Stage 0 recorded 40 missing and 203 unverified cached schemas. Current field evidence verifies four endpoints. No guessed raw or typed models. | | Restricted | Secure, Certified, EWS, private telemetry, bids/COP/awards, participant settlements and customer data. No requests. | | Unavailable | Retired total-AS-offer and SASM sources and the unavailable hourly-load 2001 year, as recorded in the audit. | @@ -37,8 +41,11 @@ The oldest row is the earliest returned observation for the selected point at the retrieval time. It is not proof of complete history or stable retention. Archive publication time, data date, and retrieval time remain separate. No adapter provides an as-of vintage, revision merge, or complete-year claim. -Typed installed-client evidence means generated or explicit Pydantic row -decoding. This milestone does not add a package-wide PEP 561 typing claim. +Typed installed-client retrieval means generated or explicit Pydantic row decoding. +The wheel now separately includes PEP 561 metadata. An isolated mypy proof checks +all 510 legacy method return contracts, all 1,334 legacy row fields, new public +row/filter/iterator contracts, and rejection of four invalid uses. Legacy +runtime files remain byte-identical; companion stubs expose their existing types. ## Use annual files @@ -65,13 +72,63 @@ partial or changed cache entries. The caller must inspect and repair them explicitly. It does not overwrite a prior document when publication changes. Cache writes use POSIX exclusive-file and hard-link operations. -Each call selects one listed public file. Each decode selects one named sheet -and at most 1,000 rows, with a bounded scan and archive expansion limits. +Each download selects one listed public file. `sample_dam_archive` preserves +its one-sheet/1,000-row contract. The new `iter_dam_archive` can visit every +worksheet and row, with configurable scan, row, member, and expansion safeguards. The cache preserves document ID, publication time, original retrieval time, and hash. Workbook prices use Decimal conversion of numeric source cells. Date, hour-ending, and repeated-hour fields stay separate. No guessed UTC mapping joins the source's ambiguous local market intervals. +## Complete query and annual-file iteration + +```python +from datetime import date +from tinyercot.public import Credentials, ReportsClient, RT_PRICES, StreamingLimits + +with ReportsClient( + Credentials.from_env(), limits=StreamingLimits(max_requests=None) +) as client: + for row in RT_PRICES.iter_rows( + client, + filters={ + "deliveryDateFrom": date(2026, 9, 4), + "deliveryDateTo": date(2026, 9, 4), + "settlementPoint": "HB_HOUSTON", + }, + sort="deliveryInterval", size=1000, max_pages=None, max_rows=None, + ): + process(row) # Caller-defined consumer; no accumulation is required. +``` + +`DAM_PRICES`, `DAM_CAPACITY_PRICES`, `RT_PRICES`, and `SYSTEM_LOAD` each bind a +generated row and TypedDict filter contract. Number filters accept float, +Decimal, or int; date filters require Python dates. Unknown fields and invalid +ranges fail before authentication. The 1,000-row page cap and default 100-page, +100,000-row, and 100-request budgets are local safeguards, not asserted ERCOT +service limits. Set the three total budgets to None only for an intentional +complete query. Per-response bytes, retry attempts, and pacing stay bounded. +Iteration rejects changing totals, a repeated preceding page, or final counts +that disagree with `totalRecords`. Source order and duplicates are preserved; +unchanged totals cannot prove a stable as-of snapshot. + +```python +from contextlib import closing +from tinyercot.public import iter_dam_archive + +# download is one previously selected, receipt-verified public annual ZIP. +with closing(iter_dam_archive(download, max_rows=None, max_scan_rows=None)) as rows: + for record in rows: + process(record.row, record.sheet, record.row_number) +``` + +Omit `sheets` to visit every worksheet in source order, or select exact names. +Rows stream through openpyxl's read-only mode; the bounded compressed ZIP and +XLSX member remain in memory. Closing early releases the workbook. Complete +iteration is tested with a synthetic two-sheet, 2,010-row file; real validation +only sampled the two already cached annual files. It does not establish that +ERCOT's requested file contains a complete year or every historical revision. + ## HTTP, token, and time contracts Defaults allow 20 HTTP attempts per client, including authentication and @@ -96,14 +153,26 @@ Mocked DST fixtures cover the repeated fall hour and the missing spring hour. The generators read only hash-pinned local inputs. Current row pins include actual response field descriptors and query provenance. Nullability is unknown; -the two models reject nulls and cover observed non-null rows only. Field order +the four models reject nulls and cover observed non-null rows only. Field order comes from each response, while unknown/missing names or source types fail. -The generated models do not establish coverage of every filter or schema epoch. +The registry generates every pinned query filter. This does not establish every +filter's server-side range semantics or coverage of every schema epoch. + +`tools/discover_public.py --spec saved-openapi.json --response small-response.json +--receipt public-receipt.json --path /np6-905-cd/spp_node_zone_hub --output +candidate.json` projects evidence offline. The exact OpenAPI hash must match +the primary catalog pin; the receipt must match the response URL, bytes and hash. +Review the candidate and register its hash/names in `tools/inputs/current/provenance.json`, +then run `tools/generate_public.py`. Generation never refreshes upstream data. +Missing fields, empty/null samples, or unsupported types remain missing/unknown; +they do not create a raw or typed adapter. The catalog retains every unsupported +operation independently of this four-endpoint registry. `docs/evidence/fixture-provenance.json` records public source hashes, retrieval times, fixture hashes, and excerpt transformations. `installed-receipts.json` records installed-client fetches without headers, credential values, or row values. API fixtures are small public responses. ESR is an explicit excerpt. +`installed-reports-receipts.json` records the six new installed-client requests. Workbook tests build tiny XLSX files from recorded source cells. Full annual archives and raw live snapshots are not checked in. @@ -116,6 +185,8 @@ and JSON-quoted values, as produced by the authorized local vault export. Do not commit that file. `tools/probe_public.py --live` performs a fixed small 2026-09-04/oldest-price check, one rolling ESR request, one public listing, and the two selected annual samples. Reuse its output cache to avoid redownloads. +`--reports-only` instead makes six bounded RT/load data requests: current, +oldest-first, and the complete two-row selection. Neither probe runs in CI. Primary sources: ERCOT's [current Public Reports specification](https://apiexplorer.ercot.com/developer/apis/pubapi-apim-api?export=true&api-version=2022-04-01-preview), [authentication guide](https://developer.ercot.com/applications/pubapi/user-guide/registration-and-authentication/), diff --git a/pyproject.toml b/pyproject.toml index d6aa0ea3..ab4f55d1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,8 @@ files = [ [dependency-groups] dev = [ "ipython>=9.8.0", + "mypy>=2.3.1", + "pandas-stubs>=2.3.3", "pytest>=9.1.1", "ruff>=0.16.6", "tqdm>=4.67.1", @@ -46,5 +48,5 @@ testpaths = ["tests"] pythonpath = [".", "tests"] [tool.setuptools.package-data] -tinyercot = ["_catalog.json"] -"tinyercot.public" = ["_coverage.json"] +tinyercot = ["_catalog.json", "py.typed", "*.pyi"] +"tinyercot.public" = ["_coverage.json", "_contracts.json"] diff --git a/tests/fixtures/public/rt-prices-current.json b/tests/fixtures/public/rt-prices-current.json new file mode 100644 index 00000000..59b7697d --- /dev/null +++ b/tests/fixtures/public/rt-prices-current.json @@ -0,0 +1 @@ +{"_meta":{"totalRecords":96,"pageSize":2,"totalPages":48,"currentPage":1,"query":{"parameterCount":3,"parameters":{"settlementPoint":"HB_HOUSTON","deliveryDateFrom":"2026-09-04","deliveryDateTo":"2026-09-04"},"sortedBy":"deliveryDate: DESC"}},"report":{"reportName":"spp_node_zone_hub","reportDisplayName":"Settlement Point Prices at Resource Nodes, Hubs and Load Zones","reportId":"12301","reportEMIL":"NP6-905-CD","downloadLimit":2000000},"fields":[{"name":"deliveryDate","label":"Delivery Date","cardinality":1,"dataType":"DATE","searchable":true,"sortable":true,"hasRange":true},{"name":"deliveryHour","label":"Delivery Hour","cardinality":2,"dataType":"INTEGER","searchable":true,"sortable":true,"hasRange":true},{"name":"deliveryInterval","label":"Delivery Interval","cardinality":3,"dataType":"INTEGER","searchable":true,"sortable":true,"hasRange":true},{"name":"settlementPoint","label":"Settlement Point","cardinality":4,"dataType":"VARCHAR","searchable":true,"sortable":true,"hasRange":false},{"name":"settlementPointType","label":"Settlement Point Type","cardinality":5,"dataType":"VARCHAR","searchable":true,"sortable":true,"hasRange":false},{"name":"settlementPointPrice","label":"Settlement Point Price","cardinality":6,"dataType":"DOUBLE","searchable":true,"sortable":true,"hasRange":true},{"name":"DSTFlag","label":"DST Flag","cardinality":7,"dataType":"BOOLEAN","searchable":true,"sortable":true,"hasRange":false}],"data":[["2026-09-04",14,1,"HB_HOUSTON","HU",28.6,false],["2026-09-04",16,2,"HB_HOUSTON","HU",41.79,false]],"_links":{"self":{"href":"https://api.ercot.com/api/public-reports/np6-905-cd/spp_node_zone_hub"},"parent":{"href":"https://api.ercot.com/api/public-reports/np6-905-cd"}}} \ No newline at end of file diff --git a/tests/fixtures/public/rt-prices-oldest.json b/tests/fixtures/public/rt-prices-oldest.json new file mode 100644 index 00000000..18594f6b --- /dev/null +++ b/tests/fixtures/public/rt-prices-oldest.json @@ -0,0 +1 @@ +{"_meta":{"totalRecords":95788,"pageSize":1,"totalPages":95788,"currentPage":1,"query":{"parameterCount":1,"parameters":{"settlementPoint":"HB_HOUSTON"},"sortedBy":"deliveryDate: ASC"}},"report":{"reportName":"spp_node_zone_hub","reportDisplayName":"Settlement Point Prices at Resource Nodes, Hubs and Load Zones","reportId":"12301","reportEMIL":"NP6-905-CD","downloadLimit":2000000},"fields":[{"name":"deliveryDate","label":"Delivery Date","cardinality":1,"dataType":"DATE","searchable":true,"sortable":true,"hasRange":true},{"name":"deliveryHour","label":"Delivery Hour","cardinality":2,"dataType":"INTEGER","searchable":true,"sortable":true,"hasRange":true},{"name":"deliveryInterval","label":"Delivery Interval","cardinality":3,"dataType":"INTEGER","searchable":true,"sortable":true,"hasRange":true},{"name":"settlementPoint","label":"Settlement Point","cardinality":4,"dataType":"VARCHAR","searchable":true,"sortable":true,"hasRange":false},{"name":"settlementPointType","label":"Settlement Point Type","cardinality":5,"dataType":"VARCHAR","searchable":true,"sortable":true,"hasRange":false},{"name":"settlementPointPrice","label":"Settlement Point Price","cardinality":6,"dataType":"DOUBLE","searchable":true,"sortable":true,"hasRange":true},{"name":"DSTFlag","label":"DST Flag","cardinality":7,"dataType":"BOOLEAN","searchable":true,"sortable":true,"hasRange":false}],"data":[["2023-12-11",19,3,"HB_HOUSTON","HU",21.33,false]],"_links":{"self":{"href":"https://api.ercot.com/api/public-reports/np6-905-cd/spp_node_zone_hub"},"parent":{"href":"https://api.ercot.com/api/public-reports/np6-905-cd"}}} \ No newline at end of file diff --git a/tests/fixtures/public/system-load-current.json b/tests/fixtures/public/system-load-current.json new file mode 100644 index 00000000..db995da5 --- /dev/null +++ b/tests/fixtures/public/system-load-current.json @@ -0,0 +1 @@ +{"_meta":{"totalRecords":24,"pageSize":2,"totalPages":12,"currentPage":1,"query":{"parameterCount":2,"parameters":{"operatingDayFrom":"2026-09-04","operatingDayTo":"2026-09-04"},"sortedBy":"operatingDay: DESC"}},"report":{"reportName":"act_sys_load_by_wzn","reportDisplayName":"Actual System Load by Weather Zone","reportId":"13101","reportEMIL":"NP6-345-CD","downloadLimit":2000000},"fields":[{"name":"operatingDay","label":"Operating Day","cardinality":1,"dataType":"DATE","searchable":true,"sortable":true,"hasRange":true},{"name":"hourEnding","label":"Hour Ending","cardinality":2,"dataType":"VARCHAR","searchable":true,"sortable":true,"hasRange":false},{"name":"coast","label":"Coast","cardinality":3,"dataType":"DOUBLE","searchable":true,"sortable":true,"hasRange":true},{"name":"east","label":"East","cardinality":4,"dataType":"DOUBLE","searchable":true,"sortable":true,"hasRange":true},{"name":"farWest","label":"Far West","cardinality":5,"dataType":"DOUBLE","searchable":true,"sortable":true,"hasRange":true},{"name":"north","label":"North","cardinality":6,"dataType":"DOUBLE","searchable":true,"sortable":true,"hasRange":true},{"name":"northC","label":"NorthC","cardinality":7,"dataType":"DOUBLE","searchable":true,"sortable":true,"hasRange":true},{"name":"southern","label":"Southern","cardinality":8,"dataType":"DOUBLE","searchable":true,"sortable":true,"hasRange":true},{"name":"southC","label":"SouthC","cardinality":9,"dataType":"DOUBLE","searchable":true,"sortable":true,"hasRange":true},{"name":"west","label":"West","cardinality":10,"dataType":"DOUBLE","searchable":true,"sortable":true,"hasRange":true},{"name":"total","label":"Total","cardinality":11,"dataType":"DOUBLE","searchable":true,"sortable":true,"hasRange":true},{"name":"DSTFlag","label":"DST Flag","cardinality":12,"dataType":"BOOLEAN","searchable":true,"sortable":true,"hasRange":false}],"data":[["2026-09-04","01:00",15592.21,1929.42,7959.05,1897.99,17793.56,5682.16,11038.71,2206.39,64099.49,false],["2026-09-04","02:00",15148.27,1852.45,7970.82,1823.78,16962.88,5404.71,10562.61,2142.33,61867.86,false]],"_links":{"self":{"href":"https://api.ercot.com/api/public-reports/np6-345-cd/act_sys_load_by_wzn"},"parent":{"href":"https://api.ercot.com/api/public-reports/np6-345-cd"}}} \ No newline at end of file diff --git a/tests/fixtures/public/system-load-oldest.json b/tests/fixtures/public/system-load-oldest.json new file mode 100644 index 00000000..78c5615e --- /dev/null +++ b/tests/fixtures/public/system-load-oldest.json @@ -0,0 +1 @@ +{"_meta":{"totalRecords":23951,"pageSize":1,"totalPages":23951,"currentPage":1,"query":{"parameterCount":0,"parameters":{},"sortedBy":"operatingDay: ASC"}},"report":{"reportName":"act_sys_load_by_wzn","reportDisplayName":"Actual System Load by Weather Zone","reportId":"13101","reportEMIL":"NP6-345-CD","downloadLimit":2000000},"fields":[{"name":"operatingDay","label":"Operating Day","cardinality":1,"dataType":"DATE","searchable":true,"sortable":true,"hasRange":true},{"name":"hourEnding","label":"Hour Ending","cardinality":2,"dataType":"VARCHAR","searchable":true,"sortable":true,"hasRange":false},{"name":"coast","label":"Coast","cardinality":3,"dataType":"DOUBLE","searchable":true,"sortable":true,"hasRange":true},{"name":"east","label":"East","cardinality":4,"dataType":"DOUBLE","searchable":true,"sortable":true,"hasRange":true},{"name":"farWest","label":"Far West","cardinality":5,"dataType":"DOUBLE","searchable":true,"sortable":true,"hasRange":true},{"name":"north","label":"North","cardinality":6,"dataType":"DOUBLE","searchable":true,"sortable":true,"hasRange":true},{"name":"northC","label":"NorthC","cardinality":7,"dataType":"DOUBLE","searchable":true,"sortable":true,"hasRange":true},{"name":"southern","label":"Southern","cardinality":8,"dataType":"DOUBLE","searchable":true,"sortable":true,"hasRange":true},{"name":"southC","label":"SouthC","cardinality":9,"dataType":"DOUBLE","searchable":true,"sortable":true,"hasRange":true},{"name":"west","label":"West","cardinality":10,"dataType":"DOUBLE","searchable":true,"sortable":true,"hasRange":true},{"name":"total","label":"Total","cardinality":11,"dataType":"DOUBLE","searchable":true,"sortable":true,"hasRange":true},{"name":"DSTFlag","label":"DST Flag","cardinality":12,"dataType":"BOOLEAN","searchable":true,"sortable":true,"hasRange":false}],"data":[["2023-12-11","24:00",11373.24,1648.6,6330.63,1379.71,12186.52,3170.57,7260.81,1241.1,44591.16,false]],"_links":{"self":{"href":"https://api.ercot.com/api/public-reports/np6-345-cd/act_sys_load_by_wzn"},"parent":{"href":"https://api.ercot.com/api/public-reports/np6-345-cd"}}} \ No newline at end of file diff --git a/tests/test_archive_iteration.py b/tests/test_archive_iteration.py new file mode 100644 index 00000000..cfbea630 --- /dev/null +++ b/tests/test_archive_iteration.py @@ -0,0 +1,102 @@ +"""Prove full worksheet iteration offline without extracting real history.""" + +import datetime +import hashlib +import io +import zipfile +from collections import Counter +from pathlib import Path + +import pytest + +from tinyercot.public import ( + ArchiveDocument, + Download, + LimitError, + Receipt, + iter_dam_archive, +) +from tinyercot.public.archives import HEADER + + +def archive(tmp_path: Path) -> Download: + """Build a small synthetic two-worksheet file with more than 1,000 rows. + + Args: + tmp_path: Pytest temporary directory for the fixture ZIP. + + Returns: + A synthetic public document with a valid byte/hash receipt. + """ + openpyxl = pytest.importorskip("openpyxl") + book = openpyxl.Workbook() + for index, name in enumerate(("Jan", "Feb")): + sheet = book.active if index == 0 else book.create_sheet() + sheet.title = name + sheet.append(HEADER) + for number in range(1005): + sheet.append(["01/01/2026", "24:00", "N", "HB_HOUSTON", number]) + inner = io.BytesIO() + book.save(inner) + book.close() + outer = io.BytesIO() + with zipfile.ZipFile(outer, "w", compression=zipfile.ZIP_DEFLATED) as stream: + stream.writestr("annual.xlsx", inner.getvalue()) + raw = outer.getvalue() + path = tmp_path / "annual.zip" + path.write_bytes(raw) + stamp = datetime.datetime(2026, 9, 5, tzinfo=datetime.UTC) + return Download( + ArchiveDocument("123", 13060, "DAMLZHBSPP_2026", stamp, len(raw), "P"), + path, + Receipt( + "https://www.ercot.com/public-test", + stamp, + hashlib.sha256(raw).hexdigest(), + len(raw), + ), + ) + + +def test_all_worksheets_and_rows_have_no_permanent_sample_ceiling(tmp_path): + download = archive(tmp_path) + rows = iter_dam_archive(download, max_rows=None, max_scan_rows=None) + counts = Counter() + for record in rows: + counts[record.sheet] += 1 + assert record.source_sha256 == download.receipt.sha256 + assert record.row_number == counts[record.sheet] + 1 + assert counts == {"Jan": 1005, "Feb": 1005} + + +@pytest.mark.parametrize("budget", ["rows", "scan"]) +def test_iterator_budgets_raise_instead_of_claiming_completion(tmp_path, budget): + download = archive(tmp_path) + with pytest.raises(LimitError): + list( + iter_dam_archive( + download, + max_rows=2 if budget == "rows" else None, + max_scan_rows=2 if budget == "scan" else None, + ) + ) + + +def test_early_close_releases_workbook_and_selected_sheet_is_preserved( + tmp_path, monkeypatch +): + from openpyxl.workbook.workbook import Workbook + + download = archive(tmp_path) + closed = [] + original = Workbook.close + + def close(book): + closed.append(True) + original(book) + + monkeypatch.setattr(Workbook, "close", close) + rows = iter_dam_archive(download, sheets=("Feb",), max_rows=None) + assert next(rows).sheet == "Feb" + rows.close() + assert closed == [True] diff --git a/tests/test_discover_public.py b/tests/test_discover_public.py new file mode 100644 index 00000000..71939484 --- /dev/null +++ b/tests/test_discover_public.py @@ -0,0 +1,164 @@ +"""Test offline projection and generation rejection with compact fixtures.""" + +import hashlib +import importlib.util +import json +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[1] +SPEC = importlib.util.spec_from_file_location( + "discover_public", ROOT / "tools/discover_public.py" +) +assert SPEC and SPEC.loader +discovery = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(discovery) + + +@pytest.fixture +def evidence(tmp_path, monkeypatch): + pin = json.loads((ROOT / "tools/inputs/current/rt-prices.json").read_text()) + spec = tmp_path / "spec.json" + spec.write_text( + json.dumps( + {"paths": {pin["path"]: {"get": {"parameters": pin["query_parameters"]}}}} + ) + ) + catalog = tmp_path / "catalog.json" + catalog.write_text( + json.dumps( + { + "sources": { + "public-reports": { + "sha256": hashlib.sha256(spec.read_bytes()).hexdigest() + } + }, + "operations": [ + {"service": "public-reports", "kind": "data", "path": pin["path"]} + ], + } + ) + ) + monkeypatch.setattr(discovery, "CATALOG", catalog) + response = tmp_path / "response.json" + response.write_bytes( + (ROOT / "tests/fixtures/public/rt-prices-current.json").read_bytes() + ) + receipt = tmp_path / "receipt.json" + receipt.write_text(json.dumps(pin["response_source"])) + return {"spec": spec, "response": response, "receipt": receipt, "path": pin["path"]} + + +def test_projection_is_deterministic_and_checks_primary_pins(evidence): + first = discovery.discover(**evidence) + assert first == discovery.discover(**evidence) + assert first["row_schema"] == "verified_observed" + assert all(p["in"] == "query" for p in first["query_parameters"]) + evidence["spec"].write_text("{}") + with pytest.raises(ValueError, match="primary-source"): + discovery.discover(**evidence) + + +@pytest.mark.parametrize( + "fault,status", + [ + ("missing", "missing"), + ("empty", "unknown"), + ("null", "unknown"), + ("unknown_type", "unknown"), + ("wrong_type", "unknown"), + ], +) +def test_missing_or_unknown_response_contract_is_explicit(evidence, fault, status): + body = json.loads(evidence["response"].read_bytes()) + if fault == "missing": + body.pop("fields") + elif fault == "empty": + body["data"] = [] + elif fault == "null": + body["data"][0][0] = None + elif fault == "wrong_type": + body["data"][0][1] = "not an integer" + else: + body["fields"][0]["dataType"] = "UNVERIFIED" + raw = json.dumps(body).encode() + evidence["response"].write_bytes(raw) + receipt = json.loads(evidence["receipt"].read_text()) + receipt.update(sha256=hashlib.sha256(raw).hexdigest(), byte_count=len(raw)) + evidence["receipt"].write_text(json.dumps(receipt)) + assert discovery.discover(**evidence)["row_schema"] == status + + +def test_unknown_schema_is_never_generated(monkeypatch, tmp_path): + from tools import generate_public + + pin = json.loads(generate_public.INPUT.read_text()) + pin["row_schema"] = "unknown" + changed = tmp_path / "dam-prices.json" + changed.write_text(json.dumps(pin)) + manifest = json.loads(generate_public.MANIFEST.read_text()) + manifest["inputs"][changed.name] = hashlib.sha256(changed.read_bytes()).hexdigest() + path = tmp_path / "provenance.json" + path.write_text(json.dumps(manifest)) + monkeypatch.setattr(generate_public, "INPUT", changed) + monkeypatch.setattr(generate_public, "MANIFEST", path) + with pytest.raises(ValueError, match="unknown"): + generate_public.render() + + +@pytest.mark.parametrize( + "fault", + [ + "missing_status", + "source_hash", + "duplicate_symbol", + "private_field", + "unknown_format", + "duplicate_query", + ], +) +def test_generator_rejects_unverified_or_ambiguous_contracts( + monkeypatch, tmp_path, fault +): + from tools import generate_public + + manifest = json.loads(generate_public.MANIFEST.read_text()) + for name in manifest["inputs"]: + (tmp_path / name).write_bytes( + (generate_public.MANIFEST.parent / name).read_bytes() + ) + path = tmp_path / "dam-prices.json" + pin = json.loads(path.read_text()) + if fault == "missing_status": + pin.pop("row_schema") + elif fault == "source_hash": + pin["query_source_sha256"] = "unverified" + elif fault == "duplicate_symbol": + manifest["contracts"][0]["constant"] = manifest["contracts"][0]["model"] + elif fault == "private_field": + pin["fields"][0]["name"] = "_hidden" + elif fault == "unknown_format": + pin["query_parameters"][0]["schema"]["format"] = "unknown" + else: + pin["query_parameters"].append(pin["query_parameters"][0]) + path.write_text(json.dumps(pin)) + manifest["inputs"][path.name] = hashlib.sha256(path.read_bytes()).hexdigest() + manifest_path = tmp_path / "provenance.json" + manifest_path.write_text(json.dumps(manifest)) + monkeypatch.setattr(generate_public, "INPUT", path) + monkeypatch.setattr( + generate_public, "CAPACITY_INPUT", tmp_path / "dam-capacity.json" + ) + monkeypatch.setattr(generate_public, "MANIFEST", manifest_path) + with pytest.raises(ValueError): + generate_public.render_registry() + + +def test_registered_models_and_filter_generation_are_deterministic(): + from tools import generate_public + + assert generate_public.render() == generate_public.OUTPUT.read_text() + assert ( + generate_public.render_registry() == generate_public.REGISTRY_OUTPUT.read_text() + ) diff --git a/tests/test_public_coverage.py b/tests/test_public_coverage.py index 9678d42f..37faf453 100644 --- a/tests/test_public_coverage.py +++ b/tests/test_public_coverage.py @@ -8,7 +8,7 @@ def test_every_audited_family_and_operation_has_an_explicit_scope(): entries = coverage() assert len(entries) == 39 + 257 + 2 assert len({entry.key for entry in entries}) == len(entries) - assert Counter(entry.status for entry in entries)["covered"] == 4 + assert Counter(entry.status for entry in entries)["covered"] == 6 assert {entry.status for entry in entries} == { "covered", "deferred", @@ -36,8 +36,10 @@ def test_typed_counts_do_not_claim_broad_api_coverage(): ] assert len(observed) == 243 assert len({(op.service, op.path.split("/")[1]) for op in observed}) == 98 - assert len(typed) == 2 + assert len(typed) == 4 assert {entry.title.split("/")[1] for entry in typed} == { "np4-190-cd", "np4-188-cd", + "np6-905-cd", + "np6-345-cd", } diff --git a/tests/test_public_provenance.py b/tests/test_public_provenance.py index 4723ddae..977bbc72 100644 --- a/tests/test_public_provenance.py +++ b/tests/test_public_provenance.py @@ -25,6 +25,8 @@ def test_current_model_fields_come_from_retained_response_bytes(): for contract_name, fixture_name in ( ("dam-prices", "dam-current-page1"), ("dam-capacity", "capacity-current"), + ("rt-prices", "rt-prices-current"), + ("system-load", "system-load-current"), ): contract = json.loads( (ROOT / f"tools/inputs/current/{contract_name}.json").read_text() diff --git a/tests/test_reports.py b/tests/test_reports.py new file mode 100644 index 00000000..40781628 --- /dev/null +++ b/tests/test_reports.py @@ -0,0 +1,167 @@ +"""Exercise generated field contracts and complete bounded query iteration.""" + +import datetime +import json +from pathlib import Path + +import httpx +import pytest + +from tinyercot.public import ( + DAM_PRICES, + RT_PRICES, + SYSTEM_LOAD, + Credentials, + LimitError, + ReportsClient, + SchemaMismatchError, + StreamingLimits, +) +from tinyercot.public.api import TOKEN_URL + +FIXTURES = Path(__file__).parent / "fixtures/public" +CREDS = Credentials("synthetic", "synthetic", "synthetic") + + +@pytest.mark.parametrize( + "endpoint,fixture,date_field", + [ + (RT_PRICES, "rt-prices", "deliveryDate"), + (SYSTEM_LOAD, "system-load", "operatingDay"), + ], +) +@pytest.mark.parametrize("period", ["current", "oldest"]) +def test_observed_new_contracts(endpoint, fixture, date_field, period): + raw = (FIXTURES / f"{fixture}-{period}.json").read_bytes() + + def handler(request): + if str(request.url) == TOKEN_URL: + return httpx.Response( + 200, json={"id_token": "synthetic", "expires_in": 3600} + ) + assert request.url.path.endswith(endpoint.path) + return httpx.Response(200, content=raw) + + with ReportsClient( + CREDS, + limits=StreamingLimits(min_interval=0), + transport=httpx.MockTransport(handler), + ) as client: + page = client.page(endpoint, size=2) + assert page.rows and isinstance(page.rows[0], endpoint.row_model) + assert isinstance(getattr(page.rows[0], date_field), datetime.date) + assert page.raw == raw + + +def test_all_pages_can_stream_past_old_limits_and_filter_types_are_encoded(): + calls = [] + + def handler(request): + calls.append(request) + if str(request.url) == TOKEN_URL: + return httpx.Response( + 200, json={"id_token": "synthetic", "expires_in": 3600} + ) + params = request.url.params + assert params["deliveryDateFrom"] == "2026-09-04" + assert params["DSTFlag"] == "false" + assert params["hourEnding"] == "24:00" + body = json.loads((FIXTURES / "dam-oldest.json").read_text()) + number = int(params["page"]) + body["_meta"].update( + currentPage=number, totalPages=125, totalRecords=125, pageSize=1 + ) + body["data"][0][3] = number + return httpx.Response(200, json=body) + + with ReportsClient( + CREDS, + limits=StreamingLimits(max_requests=None, min_interval=0), + transport=httpx.MockTransport(handler), + ) as client: + rows = client.iter_rows( + DAM_PRICES, + filters={ + "deliveryDateFrom": datetime.date(2026, 9, 4), + "DSTFlag": False, + "hourEnding": "24:00", + }, + size=1, + max_pages=None, + max_rows=None, + ) + assert sum(1 for _ in rows) == 125 + assert len(calls) == 126 + + +@pytest.mark.parametrize( + "filters", + [ + {"unknown": 1}, + {"DSTFlag": "false"}, + {"deliveryDateFrom": "2026-09-04"}, + {"settlementPointPriceFrom": float("nan")}, + {"settlementPointPriceFrom": 5, "settlementPointPriceTo": 1}, + ], +) +def test_invalid_filters_fail_before_authentication(filters): + def handler(request): + pytest.fail("Invalid filters reached transport") + + with ( + ReportsClient(CREDS, transport=httpx.MockTransport(handler)) as client, + pytest.raises(ValueError), + ): + client.page(DAM_PRICES, filters=filters) + + +@pytest.mark.parametrize( + "fault", + [ + "page_budget", + "row_budget", + "totals", + "repeat", + "wrong_type", + "short_total", + "long_total", + ], +) +def test_iteration_never_silently_returns_partial_or_changed_queries(fault): + def handler(request): + if str(request.url) == TOKEN_URL: + return httpx.Response( + 200, json={"id_token": "synthetic", "expires_in": 3600} + ) + body = json.loads((FIXTURES / "dam-oldest.json").read_text()) + number = int(request.url.params["page"]) + body["_meta"].update( + currentPage=number, totalPages=2, totalRecords=2, pageSize=1 + ) + if fault != "repeat": + body["data"][0][3] = number + if fault == "totals" and number == 2: + body["_meta"]["totalRecords"] = 3 + if fault == "wrong_type": + body["data"][0][4] = 0 + if fault == "short_total": + body["_meta"]["totalRecords"] = 1 + if fault == "long_total": + body["_meta"]["totalRecords"] = 3 + return httpx.Response(200, json=body) + + with ReportsClient( + CREDS, + limits=StreamingLimits(min_interval=0), + transport=httpx.MockTransport(handler), + ) as client: + expected = LimitError if fault.endswith("budget") else SchemaMismatchError + with pytest.raises(expected): + list( + client.iter_rows( + DAM_PRICES, + size=1, + max_pages=1 if fault == "page_budget" else None, + max_rows=1 if fault == "row_budget" else None, + ) + ) diff --git a/tests/typing_invalid.py b/tests/typing_invalid.py new file mode 100644 index 00000000..0091373e --- /dev/null +++ b/tests/typing_invalid.py @@ -0,0 +1,12 @@ +"""Negative static contracts; the installed checker must reject each line.""" + +from tinyercot import np4_190_cd +from tinyercot.public import DAM_PRICES, RT_PRICES, ReportsClient + + +def invalid_contracts(client: ReportsClient) -> None: + """Ensure wrong filters and row fields do not degrade to Any.""" + DAM_PRICES.page(client, filters={"deliveryDateFrom": "2026-09-04"}) + RT_PRICES.page(client, filters={"deliveryHourFrom": "one"}) + _ = client.page(RT_PRICES).rows[0].not_a_source_field + np4_190_cd.dam_stlmnt_pnt_prices(deliveryDateFrom="2026-09-04") diff --git a/tests/typing_public.py b/tests/typing_public.py new file mode 100644 index 00000000..de1d3f98 --- /dev/null +++ b/tests/typing_public.py @@ -0,0 +1,78 @@ +"""Positive static contracts; copy outside the checkout for installed checks.""" + +from collections.abc import AsyncIterator, Iterator +from datetime import date +from decimal import Decimal +from typing import assert_type + +import pandas as pd + +from tinyercot import configure, np4_190_cd +from tinyercot.catalog import Access, operations +from tinyercot.public import ( + DAM_CAPACITY_PRICES, + DAM_PRICES, + RT_PRICES, + SYSTEM_LOAD, + ArchivePrice, + DamCapacityPrice, + DamPrice, + Download, + PublicClient, + RealTimePrice, + ReportsClient, + SystemLoad, + WebClient, + iter_dam_archive, +) + + +def public_contracts( + reports: ReportsClient, prices: PublicClient, web: WebClient, download: Download +) -> None: + """Check public row, query, metadata, file, and legacy method types.""" + configure(username="synthetic") + with reports as scoped: + assert_type(scoped, ReportsClient) + assert_type( + RT_PRICES.iter_rows( + scoped, filters={"settlementPointPriceFrom": Decimal("1.25")} + ), + Iterator[RealTimePrice], + ) + assert_type(operations()[0].access, Access) + assert_type( + DAM_PRICES.page(reports, filters={"deliveryDateFrom": date(2026, 9, 4)}).rows[ + 0 + ], + DamPrice, + ) + assert_type(reports.page(DAM_CAPACITY_PRICES).rows[0], DamCapacityPrice) + assert_type( + RT_PRICES.page(reports, filters={"deliveryHourFrom": 1}).rows[0], RealTimePrice + ) + assert_type(reports.page(SYSTEM_LOAD).rows[0], SystemLoad) + assert_type(reports.page(RT_PRICES).rows[0].deliveryHour, int) + assert_type(reports.page(SYSTEM_LOAD).rows[0].total, Decimal) + assert_type(reports.iter_rows(SYSTEM_LOAD), Iterator[SystemLoad]) + assert_type(prices.dam_prices(settlement_point="HB_HOUSTON").rows[0], DamPrice) + assert_type(web.esr().current_day.data[0].netOutput, Decimal) + assert_type(next(iter_dam_archive(download)).row, ArchivePrice) + legacy = np4_190_cd.dam_stlmnt_pnt_prices() + assert_type(legacy.data[0], np4_190_cd.DamStlmntPntPricesRow) + assert_type(legacy.data[0].settlementPointPrice, Decimal) + assert_type(legacy.to_df(), pd.DataFrame) + assert_type(np4_190_cd.dam_stlmnt_pnt_prices_df(), pd.DataFrame) + assert_type( + np4_190_cd.dam_stlmnt_pnt_prices_iter(), + Iterator[np4_190_cd.DamStlmntPntPricesRow], + ) + assert_type( + np4_190_cd.dam_stlmnt_pnt_prices_iter_async(), + AsyncIterator[np4_190_cd.DamStlmntPntPricesRow], + ) + + +async def legacy_async_contract() -> None: + """Check the legacy async DataFrame return contract without running it.""" + assert_type(await np4_190_cd.dam_stlmnt_pnt_prices_df_async(), pd.DataFrame) diff --git a/tests/wheel_smoke.py b/tests/wheel_smoke.py index 28aea408..e6c0faaa 100644 --- a/tests/wheel_smoke.py +++ b/tests/wheel_smoke.py @@ -1,10 +1,14 @@ """Run with an isolated Python interpreter after installing the built wheel.""" import importlib +import json import socket +from datetime import date from importlib.resources import files from pathlib import Path +import httpx + def deny_network(*args, **kwargs): """Stop any accidental network call during the installed-wheel smoke check. @@ -34,9 +38,56 @@ def deny_network(*args, **kwargs): assert len(tinyercot._generated.__all__) == 35 assert tinyercot.np3_910_er._2d_agg_dsr_loads assert tinyercot.np4_190_cd.DamStlmntPntPricesResponse().to_df().columns.empty +response = tinyercot.np4_190_cd.DamStlmntPntPricesResponse.model_validate( + { + "data": [ + { + "deliveryDate": "2026-09-04", + "hourEnding": "24:00", + "settlementPoint": "HB_HOUSTON", + "settlementPointPrice": "12.25", + "DSTFlag": False, + } + ] + } +) +frame = response.to_df() +assert frame.deliveryDate[0] == date(2026, 9, 4) +assert str(frame.settlementPointPrice.dtype) == "float64" +assert frame.settlementPointPrice[0] == 12.25 +assert str(frame.DSTFlag.dtype) == "bool" public = importlib.import_module("tinyercot.public") assert len(public.coverage()) == 298 +assert files("tinyercot").joinpath("py.typed").is_file() +assert files("tinyercot.public").joinpath("_contracts.json").is_file() +assert public.RT_PRICES.row_model is public.RealTimePrice +assert len([entry for entry in public.coverage() if entry.status == "covered"]) == 6 assert public.Credentials("test", "test", "test") with public.WebClient(): pass +raw = (Path(__file__).parent / "fixtures/public/rt-prices-current.json").read_bytes() + + +def report_transport(request): + """Serve synthetic authentication and retained public data without sockets. + + Args: + request: Installed client's HTTP request. + + Returns: + A synthetic ID token or a small saved public response. + """ + if request.method == "POST": + return httpx.Response(200, json={"id_token": "synthetic", "expires_in": 3600}) + return httpx.Response(200, content=raw) + + +with public.ReportsClient( + public.Credentials("synthetic", "synthetic", "synthetic"), + limits=public.StreamingLimits(min_interval=0), + transport=httpx.MockTransport(report_transport), +) as client: + page = client.page(public.RT_PRICES, size=2) + assert len(page.rows) == len(json.loads(raw)["data"]) == 2 + assert type(page.rows[0]) is public.RealTimePrice print("Installed-wheel legacy imports and opt-in public metadata passed") diff --git a/tinyercot/__init__.pyi b/tinyercot/__init__.pyi new file mode 100644 index 00000000..e2fa714c --- /dev/null +++ b/tinyercot/__init__.pyi @@ -0,0 +1,4 @@ +"""Static exports for the byte-frozen legacy package initializer.""" + +from ._client import configure as configure +from ._generated import * diff --git a/tinyercot/_client.pyi b/tinyercot/_client.pyi new file mode 100644 index 00000000..1f3d9f89 --- /dev/null +++ b/tinyercot/_client.pyi @@ -0,0 +1,30 @@ +"""Static contracts for the byte-frozen legacy transport and response base.""" + +from typing import Any, ClassVar, Generic, TypeVar + +import pandas as pd +from pydantic import BaseModel + +T = TypeVar("T", bound=BaseModel) + +def configure( + *, + username: str | None = None, + password: str | None = None, + subscription_key: str | None = None, +) -> None: ... +def _get( + path: str, schema: dict[str, str] | None = None, **params: Any +) -> dict[str, Any]: ... +async def _aget( + path: str, schema: dict[str, str] | None = None, **params: Any +) -> dict[str, Any]: ... + +class ErcotResponse(BaseModel, Generic[T]): + meta: dict[str, Any] = ... + report: dict[str, Any] = ... + fields: list[dict[str, Any]] = ... + data: list[T] = ... + links: list[dict[str, Any]] | dict[str, Any] = ... + _schema: ClassVar[dict[str, str]] = ... + def to_df(self) -> pd.DataFrame: ... diff --git a/tinyercot/public/__init__.py b/tinyercot/public/__init__.py index 60e1ad4e..1b370b6b 100644 --- a/tinyercot/public/__init__.py +++ b/tinyercot/public/__init__.py @@ -1,10 +1,11 @@ """Opt-in bounded retrieval of selected public ERCOT data sources. -Use PublicClient for current DAM API prices and WebClient for public annual -files and the ESR live feed. Legacy tinyercot imports and behavior are unchanged. +Use ReportsClient for generated current report contracts, PublicClient for DAM +price samples, and WebClient for annual files and the ESR live feed. Legacy +tinyercot imports and behavior are unchanged. """ -from ._generated import DamCapacityPrice, DamPrice +from ._generated import DamCapacityPrice, DamPrice, RealTimePrice, SystemLoad from ._http import ( AccessDeniedError, AuthenticationError, @@ -15,30 +16,54 @@ Receipt, SchemaMismatchError, SourceUnavailableError, + StreamingLimits, +) +from ._schemas import ( + DAM_CAPACITY_PRICES, + DAM_PRICES, + RT_PRICES, + SYSTEM_LOAD, + DamCapacityPriceFilters, + DamPriceFilters, + RealTimePriceFilters, + SystemLoadFilters, ) from .api import Credentials, PricePage, PublicClient from .archives import ( ArchiveDocument, ArchivePrice, + ArchiveRecord, ArchiveSample, Download, + iter_dam_archive, sample_dam_archive, ) from .coverage import Coverage, coverage from .live import EsrRow, EsrSnapshot +from .reports import DataPage, ReportsClient +from .schema import Endpoint from .web import WebClient __all__ = [ + "DAM_CAPACITY_PRICES", + "DAM_PRICES", + "RT_PRICES", + "SYSTEM_LOAD", "AccessDeniedError", "ArchiveDocument", "ArchivePrice", + "ArchiveRecord", "ArchiveSample", "AuthenticationError", "Coverage", "Credentials", "DamCapacityPrice", + "DamCapacityPriceFilters", "DamPrice", + "DamPriceFilters", + "DataPage", "Download", + "Endpoint", "EsrRow", "EsrSnapshot", "LimitError", @@ -47,10 +72,17 @@ "PublicClient", "PublicDataError", "RateLimitError", + "RealTimePrice", + "RealTimePriceFilters", "Receipt", + "ReportsClient", "SchemaMismatchError", "SourceUnavailableError", + "StreamingLimits", + "SystemLoad", + "SystemLoadFilters", "WebClient", "coverage", + "iter_dam_archive", "sample_dam_archive", ] diff --git a/tinyercot/public/_contracts.json b/tinyercot/public/_contracts.json new file mode 100644 index 00000000..c9a2db3c --- /dev/null +++ b/tinyercot/public/_contracts.json @@ -0,0 +1,864 @@ +{ + "dam-prices.json": { + "format_version": 1, + "service": "public-reports", + "path": "/np4-190-cd/dam_stlmnt_pnt_prices", + "observed_at": "2026-09-05", + "query_parameters": [ + { + "name": "deliveryDateFrom", + "in": "query", + "schema": { + "type": "string", + "format": "yyyy-MM-dd" + } + }, + { + "name": "deliveryDateTo", + "in": "query", + "schema": { + "type": "string", + "format": "yyyy-MM-dd" + } + }, + { + "name": "hourEnding", + "in": "query", + "schema": { + "type": "string", + "format": "abc123" + } + }, + { + "name": "settlementPoint", + "in": "query", + "schema": { + "type": "string", + "format": "abc123" + } + }, + { + "name": "settlementPointPriceFrom", + "in": "query", + "schema": { + "type": "number", + "format": "####.###" + } + }, + { + "name": "settlementPointPriceTo", + "in": "query", + "schema": { + "type": "number", + "format": "####.###" + } + }, + { + "name": "DSTFlag", + "in": "query", + "schema": { + "type": "boolean", + "format": "true | false" + } + }, + { + "name": "page", + "in": "query", + "schema": { + "type": "integer", + "format": "###" + } + }, + { + "name": "size", + "in": "query", + "schema": { + "type": "integer", + "format": "###" + } + }, + { + "name": "sort", + "in": "query", + "schema": { + "type": "string", + "format": "abc123" + } + }, + { + "name": "dir", + "in": "query", + "schema": { + "type": "string", + "format": "abc123" + } + } + ], + "fields": [ + { + "name": "deliveryDate", + "label": "Delivery Date", + "cardinality": 1, + "dataType": "DATE", + "searchable": true, + "sortable": true, + "hasRange": true + }, + { + "name": "hourEnding", + "label": "Hour Ending", + "cardinality": 2, + "dataType": "VARCHAR", + "searchable": true, + "sortable": true, + "hasRange": false + }, + { + "name": "settlementPoint", + "label": "Settlement Point", + "cardinality": 3, + "dataType": "VARCHAR", + "searchable": true, + "sortable": true, + "hasRange": false + }, + { + "name": "settlementPointPrice", + "label": "Settlement Point Price", + "cardinality": 4, + "dataType": "DOUBLE", + "searchable": true, + "sortable": true, + "hasRange": true + }, + { + "name": "DSTFlag", + "label": "DST Flag", + "cardinality": 5, + "dataType": "BOOLEAN", + "searchable": true, + "sortable": true, + "hasRange": false + } + ], + "nullability_policy": "Reject nulls; source field metadata does not establish nullability. This model supports the observed non-null price rows only.", + "source_url": "https://apiexplorer.ercot.com/developer/apis/pubapi-apim-api?export=true&api-version=2022-04-01-preview", + "query_source_sha256": "b978bf35fbf9dcb8edca14025d4b5d3108582885a6c1b92a9bf445e69b08a7f8", + "response_source": { + "source_url": "https://api.ercot.com/api/public-reports/np4-190-cd/dam_stlmnt_pnt_prices", + "query": { + "deliveryDateFrom": "2026-09-04", + "deliveryDateTo": "2026-09-04", + "settlementPoint": "HB_HOUSTON", + "size": 2, + "page": 1, + "sort": "deliveryDate", + "dir": "desc" + }, + "retrieved_at": "2026-09-05T20:28:34.745619+00:00", + "bytes": 1384, + "sha256": "dc70cef89b0d7ce0fb53a6441f8d5bdd0f79b5d60fff6125253046017cf37d18", + "status": 200 + }, + "row_schema": "verified_observed" + }, + "dam-capacity.json": { + "format_version": 1, + "service": "public-reports", + "path": "/np4-188-cd/dam_clear_price_for_cap", + "observed_at": "2026-09-05", + "query_parameters": [ + { + "name": "deliveryDateFrom", + "in": "query", + "schema": { + "type": "string", + "format": "yyyy-MM-dd" + } + }, + { + "name": "deliveryDateTo", + "in": "query", + "schema": { + "type": "string", + "format": "yyyy-MM-dd" + } + }, + { + "name": "hourEnding", + "in": "query", + "schema": { + "type": "string", + "format": "abc123" + } + }, + { + "name": "ancillaryType", + "in": "query", + "schema": { + "type": "string", + "format": "abc123" + } + }, + { + "name": "MCPCFrom", + "in": "query", + "schema": { + "type": "number", + "format": "####.###" + } + }, + { + "name": "MCPCTo", + "in": "query", + "schema": { + "type": "number", + "format": "####.###" + } + }, + { + "name": "DSTFlag", + "in": "query", + "schema": { + "type": "boolean", + "format": "true | false" + } + }, + { + "name": "page", + "in": "query", + "description": "Page number of returned values in the collection.", + "schema": { + "type": "integer", + "format": "###" + } + }, + { + "name": "size", + "in": "query", + "description": "Number of returned items per page.", + "schema": { + "type": "integer", + "format": "###" + } + }, + { + "name": "sort", + "in": "query", + "description": "Defines field by which to sort the returned resource values.", + "schema": { + "type": "string", + "format": "abc123" + } + }, + { + "name": "dir", + "in": "query", + "description": "Defines sort order of returned values based on the primary business key of the resource.", + "schema": { + "type": "string", + "format": "abc123" + } + } + ], + "fields": [ + { + "name": "deliveryDate", + "label": "Delivery Date", + "cardinality": 1, + "dataType": "DATE", + "searchable": true, + "sortable": true, + "hasRange": true + }, + { + "name": "hourEnding", + "label": "Hour Ending", + "cardinality": 2, + "dataType": "VARCHAR", + "searchable": true, + "sortable": true, + "hasRange": false + }, + { + "name": "ancillaryType", + "label": "Ancillary Type", + "cardinality": 3, + "dataType": "VARCHAR", + "searchable": true, + "sortable": true, + "hasRange": false + }, + { + "name": "MCPC", + "label": "MCPC", + "cardinality": 4, + "dataType": "DOUBLE", + "searchable": true, + "sortable": true, + "hasRange": true + }, + { + "name": "DSTFlag", + "label": "DST Flag", + "cardinality": 5, + "dataType": "BOOLEAN", + "searchable": true, + "sortable": true, + "hasRange": false + } + ], + "nullability_policy": "Reject nulls; source field metadata does not establish nullability. This model supports the observed non-null price rows only.", + "source_url": "https://apiexplorer.ercot.com/developer/apis/pubapi-apim-api?export=true&api-version=2022-04-01-preview", + "query_source_sha256": "b978bf35fbf9dcb8edca14025d4b5d3108582885a6c1b92a9bf445e69b08a7f8", + "response_source": { + "source_url": "https://api.ercot.com/api/public-reports/np4-188-cd/dam_clear_price_for_cap?deliveryDateFrom=2026-09-04&deliveryDateTo=2026-09-04&ancillaryType=REGUP&size=2&page=1&sort=deliveryDate&dir=desc", + "retrieved_at": "2026-09-05 20:54:00.360875+00:00", + "sha256": "3fa8d075dadb0baf6cb0b4656ec87e887a7c85c6d96ab2923dc3e42bd8373251", + "byte_count": 1336, + "status": 200 + }, + "row_schema": "verified_observed" + }, + "rt-prices.json": { + "format_version": 1, + "service": "public-reports", + "path": "/np6-905-cd/spp_node_zone_hub", + "observed_at": "2026-09-05 21:14:32.319440+00:00", + "row_schema": "verified_observed", + "query_parameters": [ + { + "name": "deliveryDateFrom", + "in": "query", + "schema": { + "type": "string", + "format": "yyyy-MM-dd" + } + }, + { + "name": "deliveryDateTo", + "in": "query", + "schema": { + "type": "string", + "format": "yyyy-MM-dd" + } + }, + { + "name": "deliveryHourFrom", + "in": "query", + "schema": { + "type": "integer", + "format": "###" + } + }, + { + "name": "deliveryHourTo", + "in": "query", + "schema": { + "type": "integer", + "format": "###" + } + }, + { + "name": "deliveryIntervalFrom", + "in": "query", + "schema": { + "type": "integer", + "format": "###" + } + }, + { + "name": "deliveryIntervalTo", + "in": "query", + "schema": { + "type": "integer", + "format": "###" + } + }, + { + "name": "settlementPoint", + "in": "query", + "schema": { + "type": "string", + "format": "abc123" + } + }, + { + "name": "settlementPointType", + "in": "query", + "schema": { + "type": "string", + "format": "abc123" + } + }, + { + "name": "settlementPointPriceFrom", + "in": "query", + "schema": { + "type": "number", + "format": "####.###" + } + }, + { + "name": "settlementPointPriceTo", + "in": "query", + "schema": { + "type": "number", + "format": "####.###" + } + }, + { + "name": "DSTFlag", + "in": "query", + "schema": { + "type": "boolean", + "format": "true | false" + } + }, + { + "name": "page", + "in": "query", + "schema": { + "type": "integer", + "format": "###" + } + }, + { + "name": "size", + "in": "query", + "schema": { + "type": "integer", + "format": "###" + } + }, + { + "name": "sort", + "in": "query", + "schema": { + "type": "string", + "format": "abc123" + } + }, + { + "name": "dir", + "in": "query", + "schema": { + "type": "string", + "format": "abc123" + } + } + ], + "fields": [ + { + "name": "deliveryDate", + "label": "Delivery Date", + "cardinality": 1, + "dataType": "DATE", + "searchable": true, + "sortable": true, + "hasRange": true + }, + { + "name": "deliveryHour", + "label": "Delivery Hour", + "cardinality": 2, + "dataType": "INTEGER", + "searchable": true, + "sortable": true, + "hasRange": true + }, + { + "name": "deliveryInterval", + "label": "Delivery Interval", + "cardinality": 3, + "dataType": "INTEGER", + "searchable": true, + "sortable": true, + "hasRange": true + }, + { + "name": "settlementPoint", + "label": "Settlement Point", + "cardinality": 4, + "dataType": "VARCHAR", + "searchable": true, + "sortable": true, + "hasRange": false + }, + { + "name": "settlementPointType", + "label": "Settlement Point Type", + "cardinality": 5, + "dataType": "VARCHAR", + "searchable": true, + "sortable": true, + "hasRange": false + }, + { + "name": "settlementPointPrice", + "label": "Settlement Point Price", + "cardinality": 6, + "dataType": "DOUBLE", + "searchable": true, + "sortable": true, + "hasRange": true + }, + { + "name": "DSTFlag", + "label": "DST Flag", + "cardinality": 7, + "dataType": "BOOLEAN", + "searchable": true, + "sortable": true, + "hasRange": false + } + ], + "nullability_policy": "Reject nulls; observed fields do not establish nullability.", + "source_url": "https://apiexplorer.ercot.com/developer/apis/pubapi-apim-api?export=true&api-version=2022-04-01-preview", + "query_source_sha256": "b978bf35fbf9dcb8edca14025d4b5d3108582885a6c1b92a9bf445e69b08a7f8", + "response_source": { + "source_url": "https://api.ercot.com/api/public-reports/np6-905-cd/spp_node_zone_hub?settlementPoint=HB_HOUSTON&size=2&page=1&sort=deliveryDate&dir=desc&deliveryDateFrom=2026-09-04&deliveryDateTo=2026-09-04", + "retrieved_at": "2026-09-05 21:14:32.319440+00:00", + "sha256": "211a6808039b86dfefbdfcab99e8859cf83de61ab67d2da87bd4b4bc13545c6a", + "byte_count": 1711, + "status": 200 + } + }, + "system-load.json": { + "format_version": 1, + "service": "public-reports", + "path": "/np6-345-cd/act_sys_load_by_wzn", + "observed_at": "2026-09-05 21:14:36.433487+00:00", + "row_schema": "verified_observed", + "query_parameters": [ + { + "name": "operatingDayFrom", + "in": "query", + "schema": { + "type": "string", + "format": "yyyy-MM-dd" + } + }, + { + "name": "operatingDayTo", + "in": "query", + "schema": { + "type": "string", + "format": "yyyy-MM-dd" + } + }, + { + "name": "hourEnding", + "in": "query", + "schema": { + "type": "string", + "format": "abc123" + } + }, + { + "name": "coastFrom", + "in": "query", + "schema": { + "type": "number", + "format": "####.###" + } + }, + { + "name": "coastTo", + "in": "query", + "schema": { + "type": "number", + "format": "####.###" + } + }, + { + "name": "eastFrom", + "in": "query", + "schema": { + "type": "number", + "format": "####.###" + } + }, + { + "name": "eastTo", + "in": "query", + "schema": { + "type": "number", + "format": "####.###" + } + }, + { + "name": "farWestFrom", + "in": "query", + "schema": { + "type": "number", + "format": "####.###" + } + }, + { + "name": "farWestTo", + "in": "query", + "schema": { + "type": "number", + "format": "####.###" + } + }, + { + "name": "northFrom", + "in": "query", + "schema": { + "type": "number", + "format": "####.###" + } + }, + { + "name": "northTo", + "in": "query", + "schema": { + "type": "number", + "format": "####.###" + } + }, + { + "name": "northCFrom", + "in": "query", + "schema": { + "type": "number", + "format": "####.###" + } + }, + { + "name": "northCTo", + "in": "query", + "schema": { + "type": "number", + "format": "####.###" + } + }, + { + "name": "southernFrom", + "in": "query", + "schema": { + "type": "number", + "format": "####.###" + } + }, + { + "name": "southernTo", + "in": "query", + "schema": { + "type": "number", + "format": "####.###" + } + }, + { + "name": "southCFrom", + "in": "query", + "schema": { + "type": "number", + "format": "####.###" + } + }, + { + "name": "southCTo", + "in": "query", + "schema": { + "type": "number", + "format": "####.###" + } + }, + { + "name": "westFrom", + "in": "query", + "schema": { + "type": "number", + "format": "####.###" + } + }, + { + "name": "westTo", + "in": "query", + "schema": { + "type": "number", + "format": "####.###" + } + }, + { + "name": "totalFrom", + "in": "query", + "schema": { + "type": "number", + "format": "####.###" + } + }, + { + "name": "totalTo", + "in": "query", + "schema": { + "type": "number", + "format": "####.###" + } + }, + { + "name": "DSTFlag", + "in": "query", + "schema": { + "type": "boolean", + "format": "true | false" + } + }, + { + "name": "page", + "in": "query", + "schema": { + "type": "integer", + "format": "###" + } + }, + { + "name": "size", + "in": "query", + "schema": { + "type": "integer", + "format": "###" + } + }, + { + "name": "sort", + "in": "query", + "schema": { + "type": "string", + "format": "abc123" + } + }, + { + "name": "dir", + "in": "query", + "schema": { + "type": "string", + "format": "abc123" + } + } + ], + "fields": [ + { + "name": "operatingDay", + "label": "Operating Day", + "cardinality": 1, + "dataType": "DATE", + "searchable": true, + "sortable": true, + "hasRange": true + }, + { + "name": "hourEnding", + "label": "Hour Ending", + "cardinality": 2, + "dataType": "VARCHAR", + "searchable": true, + "sortable": true, + "hasRange": false + }, + { + "name": "coast", + "label": "Coast", + "cardinality": 3, + "dataType": "DOUBLE", + "searchable": true, + "sortable": true, + "hasRange": true + }, + { + "name": "east", + "label": "East", + "cardinality": 4, + "dataType": "DOUBLE", + "searchable": true, + "sortable": true, + "hasRange": true + }, + { + "name": "farWest", + "label": "Far West", + "cardinality": 5, + "dataType": "DOUBLE", + "searchable": true, + "sortable": true, + "hasRange": true + }, + { + "name": "north", + "label": "North", + "cardinality": 6, + "dataType": "DOUBLE", + "searchable": true, + "sortable": true, + "hasRange": true + }, + { + "name": "northC", + "label": "NorthC", + "cardinality": 7, + "dataType": "DOUBLE", + "searchable": true, + "sortable": true, + "hasRange": true + }, + { + "name": "southern", + "label": "Southern", + "cardinality": 8, + "dataType": "DOUBLE", + "searchable": true, + "sortable": true, + "hasRange": true + }, + { + "name": "southC", + "label": "SouthC", + "cardinality": 9, + "dataType": "DOUBLE", + "searchable": true, + "sortable": true, + "hasRange": true + }, + { + "name": "west", + "label": "West", + "cardinality": 10, + "dataType": "DOUBLE", + "searchable": true, + "sortable": true, + "hasRange": true + }, + { + "name": "total", + "label": "Total", + "cardinality": 11, + "dataType": "DOUBLE", + "searchable": true, + "sortable": true, + "hasRange": true + }, + { + "name": "DSTFlag", + "label": "DST Flag", + "cardinality": 12, + "dataType": "BOOLEAN", + "searchable": true, + "sortable": true, + "hasRange": false + } + ], + "nullability_policy": "Reject nulls; observed fields do not establish nullability.", + "source_url": "https://apiexplorer.ercot.com/developer/apis/pubapi-apim-api?export=true&api-version=2022-04-01-preview", + "query_source_sha256": "b978bf35fbf9dcb8edca14025d4b5d3108582885a6c1b92a9bf445e69b08a7f8", + "response_source": { + "source_url": "https://api.ercot.com/api/public-reports/np6-345-cd/act_sys_load_by_wzn?size=2&page=1&sort=operatingDay&dir=desc&operatingDayFrom=2026-09-04&operatingDayTo=2026-09-04", + "retrieved_at": "2026-09-05 21:14:36.433487+00:00", + "sha256": "e9b68e868a08ba0e04becd610fbbd781421f87545864d26b05f1ac234403cf13", + "byte_count": 2262, + "status": 200 + } + } +} diff --git a/tinyercot/public/_generated.py b/tinyercot/public/_generated.py index d8be44e2..058b2f85 100644 --- a/tinyercot/public/_generated.py +++ b/tinyercot/public/_generated.py @@ -1,14 +1,14 @@ # Generated by tools/generate_public.py from pinned public field metadata. -"""Observed non-null DAM rows; unknown schemas fail decoding.""" +"""Observed non-null public rows; unknown schemas fail decoding.""" from datetime import date from decimal import Decimal -from pydantic import BaseModel, ConfigDict, StrictBool, StrictStr +from pydantic import BaseModel, ConfigDict, StrictBool, StrictInt, StrictStr class DamPrice(BaseModel): - """A non-null row from the observed public DAM field contract. + """A non-null row from the observed public field contract. Attributes: deliveryDate: Source operating date, without an inferred timezone. @@ -37,7 +37,7 @@ class DamPrice(BaseModel): class DamCapacityPrice(BaseModel): - """A non-null row from the observed public DAM field contract. + """A non-null row from the observed public field contract. Attributes: deliveryDate: Source operating date, without an inferred timezone. @@ -63,3 +63,88 @@ class DamCapacityPrice(BaseModel): "MCPC": "DOUBLE", "DSTFlag": "BOOLEAN", } + + +class RealTimePrice(BaseModel): + """A non-null row from the observed public field contract. + + Attributes: + deliveryDate: Source operating date, without an inferred timezone. + deliveryHour: Source deliveryHour value. + deliveryInterval: Source deliveryInterval value. + settlementPoint: Source hub, zone, or resource-node identifier. + settlementPointType: Source settlementPointType value. + settlementPointPrice: Decimal source price in dollars per MWh. + DSTFlag: Raw source flag; no repeated-hour meaning is inferred. + """ + + model_config = ConfigDict(extra="forbid", frozen=True) + + deliveryDate: date + deliveryHour: StrictInt + deliveryInterval: StrictInt + settlementPoint: StrictStr + settlementPointType: StrictStr + settlementPointPrice: Decimal + DSTFlag: StrictBool + + +RT_FIELDS = { + "deliveryDate": "DATE", + "deliveryHour": "INTEGER", + "deliveryInterval": "INTEGER", + "settlementPoint": "VARCHAR", + "settlementPointType": "VARCHAR", + "settlementPointPrice": "DOUBLE", + "DSTFlag": "BOOLEAN", +} + + +class SystemLoad(BaseModel): + """A non-null row from the observed public field contract. + + Attributes: + operatingDay: Source operatingDay value. + hourEnding: Source hour-ending label, including 24:00. + coast: Source coast value. + east: Source east value. + farWest: Source farWest value. + north: Source north value. + northC: Source northC value. + southern: Source southern value. + southC: Source southC value. + west: Source west value. + total: Source total value. + DSTFlag: Raw source flag; no repeated-hour meaning is inferred. + """ + + model_config = ConfigDict(extra="forbid", frozen=True) + + operatingDay: date + hourEnding: StrictStr + coast: Decimal + east: Decimal + farWest: Decimal + north: Decimal + northC: Decimal + southern: Decimal + southC: Decimal + west: Decimal + total: Decimal + DSTFlag: StrictBool + + +LOAD_FIELDS = { + "operatingDay": "DATE", + "hourEnding": "VARCHAR", + "coast": "DOUBLE", + "east": "DOUBLE", + "farWest": "DOUBLE", + "north": "DOUBLE", + "northC": "DOUBLE", + "southern": "DOUBLE", + "southC": "DOUBLE", + "west": "DOUBLE", + "total": "DOUBLE", + "DSTFlag": "BOOLEAN", +} diff --git a/tinyercot/public/_http.py b/tinyercot/public/_http.py index ab72889c..89121b09 100644 --- a/tinyercot/public/_http.py +++ b/tinyercot/public/_http.py @@ -76,6 +76,32 @@ def __post_init__(self) -> None: raise ValueError("max_retry_wait must be finite and nonnegative") +@dataclass(frozen=True) +class StreamingLimits: + """Opt-in transport limits without a permanent total-request ceiling. + + Attributes: + max_requests: Attempt budget, or None to permit complete caller queries. + max_bytes: Per-response decoded byte ceiling. + attempts: Attempts per request, including retries. + min_interval: Seconds between request starts. + max_retry_wait: Maximum server-directed wait in seconds. + """ + + max_requests: int | None = 100 + max_bytes: int = 4_000_000 + attempts: int = 3 + min_interval: float = 2.1 + max_retry_wait: float = 60.0 + + def __post_init__(self) -> None: + if self.max_requests is not None and ( + type(self.max_requests) is not int or self.max_requests < 1 + ): + raise ValueError("max_requests must be positive or None") + Limits(1, self.max_bytes, self.attempts, self.min_interval, self.max_retry_wait) + + @dataclass(frozen=True) class Receipt: """Public payload identity without credentials or response headers. @@ -130,7 +156,7 @@ class _HTTP: def __init__( self, - limits: Limits, + limits: Limits | StreamingLimits, transport: httpx.BaseTransport | None = None, *, sleep: Callable[[float], None] = time.sleep, @@ -166,7 +192,10 @@ def _pace(self) -> None: """ if self.closed: raise PublicDataError("Adapter is closed") - if self.requests >= self.limits.max_requests: + if ( + self.limits.max_requests is not None + and self.requests >= self.limits.max_requests + ): raise LimitError("HTTP request budget exhausted") if self.last_start is not None: delay = self.last_start + self.limits.min_interval - self.clock() diff --git a/tinyercot/public/_schemas.py b/tinyercot/public/_schemas.py new file mode 100644 index 00000000..6369f236 --- /dev/null +++ b/tinyercot/public/_schemas.py @@ -0,0 +1,109 @@ +# Generated by tools/generate_public.py; do not edit. +"""Verified public operation identities and complete query filter types.""" + +from datetime import date +from decimal import Decimal +from typing import TypedDict + +from ._generated import ( + DamCapacityPrice, + DamPrice, + RealTimePrice, + SystemLoad, +) +from .schema import Endpoint + + +class DamPriceFilters(TypedDict, total=False): + """Optional filters from the pinned current query specification.""" + + deliveryDateFrom: date + deliveryDateTo: date + hourEnding: str + settlementPoint: str + settlementPointPriceFrom: float | Decimal + settlementPointPriceTo: float | Decimal + DSTFlag: bool + + +DAM_PRICES: Endpoint[DamPrice, DamPriceFilters] = Endpoint( + "/np4-190-cd/dam_stlmnt_pnt_prices", DamPrice, "dam-prices.json" +) + + +class DamCapacityPriceFilters(TypedDict, total=False): + """Optional filters from the pinned current query specification.""" + + deliveryDateFrom: date + deliveryDateTo: date + hourEnding: str + ancillaryType: str + MCPCFrom: float | Decimal + MCPCTo: float | Decimal + DSTFlag: bool + + +DAM_CAPACITY_PRICES: Endpoint[DamCapacityPrice, DamCapacityPriceFilters] = Endpoint( + "/np4-188-cd/dam_clear_price_for_cap", DamCapacityPrice, "dam-capacity.json" +) + + +class RealTimePriceFilters(TypedDict, total=False): + """Optional filters from the pinned current query specification.""" + + deliveryDateFrom: date + deliveryDateTo: date + deliveryHourFrom: int + deliveryHourTo: int + deliveryIntervalFrom: int + deliveryIntervalTo: int + settlementPoint: str + settlementPointType: str + settlementPointPriceFrom: float | Decimal + settlementPointPriceTo: float | Decimal + DSTFlag: bool + + +RT_PRICES: Endpoint[RealTimePrice, RealTimePriceFilters] = Endpoint( + "/np6-905-cd/spp_node_zone_hub", RealTimePrice, "rt-prices.json" +) + + +class SystemLoadFilters(TypedDict, total=False): + """Optional filters from the pinned current query specification.""" + + operatingDayFrom: date + operatingDayTo: date + hourEnding: str + coastFrom: float | Decimal + coastTo: float | Decimal + eastFrom: float | Decimal + eastTo: float | Decimal + farWestFrom: float | Decimal + farWestTo: float | Decimal + northFrom: float | Decimal + northTo: float | Decimal + northCFrom: float | Decimal + northCTo: float | Decimal + southernFrom: float | Decimal + southernTo: float | Decimal + southCFrom: float | Decimal + southCTo: float | Decimal + westFrom: float | Decimal + westTo: float | Decimal + totalFrom: float | Decimal + totalTo: float | Decimal + DSTFlag: bool + + +SYSTEM_LOAD: Endpoint[SystemLoad, SystemLoadFilters] = Endpoint( + "/np6-345-cd/act_sys_load_by_wzn", SystemLoad, "system-load.json" +) + + +ENDPOINTS = ( + DAM_PRICES, + DAM_CAPACITY_PRICES, + RT_PRICES, + SYSTEM_LOAD, +) diff --git a/tinyercot/public/api.py b/tinyercot/public/api.py index e76233d3..fd3abb68 100644 --- a/tinyercot/public/api.py +++ b/tinyercot/public/api.py @@ -20,6 +20,7 @@ Payload, Receipt, SchemaMismatchError, + StreamingLimits, ) BASE = "https://api.ercot.com/api/public-reports" @@ -192,7 +193,7 @@ def __init__( self, credentials: Credentials, *, - limits: Limits | None = None, + limits: Limits | StreamingLimits | None = None, transport: httpx.BaseTransport | None = None, ) -> None: """Create a client without making an HTTP request. @@ -390,6 +391,21 @@ def _price_payload(self, path: str, params: dict) -> Payload: """ if path not in (PRICE_PATH, CAPACITY_PATH): raise ValueError("Unsupported price path") + return self._authenticated_payload(path, params) + + def _authenticated_payload(self, path: str, params: dict) -> Payload: + """Fetch a route already checked by an opt-in public adapter. + + Args: + path: Adapter-verified relative public report path. + params: Validated public query parameters. + + Returns: + Public bytes after bounded token acquisition and transport retries. + + Raises: + PublicDataError: Authentication, transport, or budgets fail. + """ with self._http.lock: for reacquisition in range(2): if self._token is None or self._http.clock() >= self._expires_at: diff --git a/tinyercot/public/archives.py b/tinyercot/public/archives.py index aa27caf0..e2cc7b7d 100644 --- a/tinyercot/public/archives.py +++ b/tinyercot/public/archives.py @@ -1,4 +1,4 @@ -"""Bounded sampling of the observed public DAM annual ZIP/XLSX format.""" +"""Sampling and opt-in complete iteration of public DAM annual ZIP/XLSX files.""" import datetime import hashlib @@ -6,6 +6,7 @@ import re import stat import zipfile +from collections.abc import Generator, Sequence from dataclasses import dataclass from decimal import Decimal from pathlib import Path, PurePosixPath @@ -99,6 +100,25 @@ class ArchiveSample: source_sha256: str +@dataclass(frozen=True) +class ArchiveRecord: + """A streamed annual row with its worksheet and file identity. + + Attributes: + row: Typed source price fields, without inferred UTC market intervals. + sheet: Source worksheet name. + row_number: One-based source worksheet row number, including the header. + member: XLSX member name within the source ZIP. + source_sha256: Exact downloaded archive hash. + """ + + row: ArchivePrice + sheet: str + row_number: int + member: str + source_sha256: str + + def checked_zip( raw: bytes, *, max_members: int, max_expanded_bytes: int ) -> zipfile.ZipFile: @@ -242,3 +262,131 @@ def sample_dam_archive( except (zipfile.BadZipFile, KeyError, TypeError, ValueError, StopIteration): pass raise SchemaMismatchError("Annual workbook differs from the observed XLSX contract") + + +def iter_dam_archive( + download: Download, + *, + sheets: Sequence[str] | None = None, + max_rows: int | None = 1000, + max_scan_rows: int | None = 100_000, + max_expanded_bytes: int = 64_000_000, + max_members: int = 512, +) -> Generator[ArchiveRecord, None, None]: + """Stream selected worksheets or a complete public annual workbook. + + Use max_rows=None and max_scan_rows=None for complete row iteration. These + are caller choices, not a permanent sampling ceiling. The generator closes + the workbook on exhaustion or close(); close it when stopping early. + + Args: + download: Public report-13060 ZIP with a matching receipt hash. + sheets: Exact worksheet names, or None for all worksheets in source order. + max_rows: Total yielded-row budget, or None for all selected data rows. + max_scan_rows: Data/blank row scan budget, or None to scan complete sheets. + max_expanded_bytes: Caller-selected bound on each ZIP layer's expansion. + max_members: Caller-selected bound on each ZIP layer's member count. + + Yields: + Typed prices with physical row and worksheet identities. Budget exhaustion + raises explicitly; it does not silently claim a complete annual result. + + Raises: + ValueError: A caller-selected bound or sheet selection is invalid. + ImportError: The optional files extra is not installed. + PublicDataError: Source access, hash, ZIP, cell, or budget checks fail. + OSError: The local archive cannot be read. + """ + for value in (max_rows, max_scan_rows): + if value is not None and (type(value) is not int or value < 1): + raise ValueError("Row and scan budgets must be positive or None") + if any( + type(value) is not int or value < 1 + for value in (max_expanded_bytes, max_members) + ): + raise ValueError("Archive structure budgets must be positive") + if isinstance(sheets, str) or ( + sheets is not None and (not sheets or len(set(sheets)) != len(sheets)) + ): + raise ValueError("sheets must contain distinct worksheet names") + if download.document.security != "P" or download.document.report_type_id != 13060: + raise AccessDeniedError("Only public DAM annual files are supported") + if ( + download.path.is_symlink() + or download.path.stat().st_size != download.receipt.byte_count + ): + raise SchemaMismatchError("Archive path or size differs from its receipt") + raw = download.path.read_bytes() + digest = hashlib.sha256(raw).hexdigest() + if digest != download.receipt.sha256: + raise SchemaMismatchError("Archive differs from its receipt hash") + from openpyxl import load_workbook + + try: + with checked_zip( + raw, max_members=max_members, max_expanded_bytes=max_expanded_bytes + ) as outer: + members = [member for member in outer.infolist() if not member.is_dir()] + if len(members) != 1 or not members[0].filename.lower().endswith(".xlsx"): + raise SchemaMismatchError("Unsupported annual archive container") + member = members[0].filename + inner = outer.read(members[0]) + with checked_zip( + inner, max_members=max_members, max_expanded_bytes=max_expanded_bytes + ): + pass + workbook = load_workbook( + io.BytesIO(inner), read_only=True, data_only=True, keep_links=False + ) + try: + selected = list(workbook.sheetnames if sheets is None else sheets) + if any(name not in workbook.sheetnames for name in selected): + raise SchemaMismatchError("Requested source worksheet is absent") + count = scanned = 0 + for name in selected: + iterator = workbook[name].iter_rows(values_only=True) + header = next(iterator, None) + if header is None: + continue + if tuple(header) != HEADER: + raise SchemaMismatchError("Annual workbook headers changed") + for row_number, cells in enumerate(iterator, start=2): + scanned += 1 + if max_scan_rows is not None and scanned > max_scan_rows: + raise LimitError( + "Worksheet scan budget reached before completion" + ) + if all(value is None for value in cells): + continue + if max_rows is not None and count >= max_rows: + raise LimitError("Annual row budget reached before completion") + if len(cells) != 5: + raise SchemaMismatchError("Annual workbook has extra columns") + day, hour, repeated, point, price = cells + if ( + not isinstance(day, str) + or not re.fullmatch(r"\d{2}/\d{2}/\d{4}", day) + or type(price) not in (int, float) + ): + raise SchemaMismatchError("Annual workbook cell types changed") + amount = Decimal(str(price)) + if not amount.is_finite(): + raise SchemaMismatchError("Annual workbook price is nonfinite") + row = ArchivePrice( + delivery_date=datetime.date( + int(day[6:10]), int(day[:2]), int(day[3:5]) + ), + hour_ending=hour, + repeated_hour_flag=repeated, + settlement_point=point, + settlement_point_price=amount, + ) + count += 1 + yield ArchiveRecord(row, name, row_number, member, digest) + finally: + workbook.close() + except (zipfile.BadZipFile, KeyError, TypeError, ValueError): + pass + else: + return + raise SchemaMismatchError("Annual workbook differs from the observed XLSX contract") diff --git a/tinyercot/public/coverage.py b/tinyercot/public/coverage.py index d295dfdb..a36f828d 100644 --- a/tinyercot/public/coverage.py +++ b/tinyercot/public/coverage.py @@ -7,6 +7,8 @@ from tinyercot.catalog import operations +from ._schemas import ENDPOINTS + @dataclass(frozen=True) class Coverage: @@ -50,15 +52,14 @@ def coverage() -> tuple[Coverage, ...]: ] for operation in operations(): supported = (operation.service, operation.method, operation.path) in { - ("public-reports", "GET", "/np4-190-cd/dam_stlmnt_pnt_prices"), - ("public-reports", "GET", "/np4-188-cd/dam_clear_price_for_cap"), + ("public-reports", "GET", endpoint.path) for endpoint in ENDPOINTS } entries.append( Coverage( f"api:{operation.service}:{operation.method}:{operation.path}", operation.path, "covered" if supported else "deferred", - "Bounded non-null DAM price pages; observed field schema only. No full history, all-filter, or schema-epoch claim." + "Observed non-null row schema and generated query filters; opt-in complete page/row iteration with explicit budgets. Current and oldest-first samples verified; no full-history, as-of, or schema-epoch guarantee." if supported else "No opt-in retrieval implementation or verified current row contract.", (operation.source_url,), @@ -68,9 +69,9 @@ def coverage() -> tuple[Coverage, ...]: ( Coverage( "web:dam-annual", - "Public DAM annual ZIP/XLSX samples", + "Public DAM annual ZIP/XLSX retrieval and iteration", "covered", - "Report 13060 public listing, one selected file per call, receipt cache, and <=1000 rows from one named worksheet. Verified 2010 and 2026 samples, not complete years.", + "Report 13060 public listing, one selected file per call, receipt cache, worksheet sampling, and opt-in all-worksheet row iteration. Real 2010 and 2026 samples; complete multi-worksheet iteration verified with synthetic files only. No complete-year data guarantee.", ( "https://www.ercot.com/mp/data-products/data-product-details?id=np4-180-er", ), diff --git a/tinyercot/public/reports.py b/tinyercot/public/reports.py new file mode 100644 index 00000000..bd563b15 --- /dev/null +++ b/tinyercot/public/reports.py @@ -0,0 +1,381 @@ +"""Shared typed current-report retrieval with complete, opt-in iteration.""" + +import datetime +import hashlib +import math +from collections.abc import Iterator, Mapping +from dataclasses import dataclass, field +from decimal import Decimal +from typing import Any, Generic, Literal + +import httpx + +from ._http import LimitError, Payload, Receipt, SchemaMismatchError, StreamingLimits +from ._schemas import ENDPOINTS +from .api import Credentials, PublicClient +from .schema import Endpoint, FiltersT, RowT + + +@dataclass(frozen=True) +class DataPage(Generic[RowT]): + """A typed report page with unchanged source metadata and public bytes. + + Attributes: + rows: Decoded source rows in source order. + meta: Source pagination metadata; no completeness is inferred. + receipt: Exact public response hash and retrieval time. + raw: Original public envelope without request headers or credentials. + """ + + rows: tuple[RowT, ...] + meta: dict[str, Any] + receipt: Receipt + raw: bytes = field(repr=False) + + +def query_parameters( + endpoint: Endpoint[RowT, FiltersT], filters: Mapping[str, object] +) -> dict[str, str]: + """Validate every filter against the pinned authoritative query metadata. + + Args: + endpoint: Registered current operation. + filters: Source filter names and Python values, excluding page controls. + + Returns: + Public URL values with dates, booleans, and decimals encoded explicitly. + + Raises: + ValueError: A filter name, type, or paired range is invalid. + """ + definitions = { + p["name"]: p["schema"] + for p in endpoint.contract["query_parameters"] + if p["name"] not in {"page", "size", "sort", "dir"} + } + result = {} + for name, value in filters.items(): + if name not in definitions: + raise ValueError("Unknown current query filter") + schema = definitions[name] + kind = schema["type"] + if kind == "string" and schema.get("format") == "yyyy-MM-dd": + valid = type(value) is datetime.date + encoded = value.isoformat() if isinstance(value, datetime.date) else "" + elif kind == "string": + valid = type(value) is str and len(value) <= 1024 + encoded = str(value) + elif kind == "boolean": + valid = type(value) is bool + encoded = "true" if value else "false" + elif kind == "integer": + valid = type(value) is int + encoded = str(value) + elif kind == "number": + valid = ( + isinstance(value, (int, float, Decimal)) + and not isinstance(value, bool) + and math.isfinite(value) + ) + encoded = str(value) + else: + valid, encoded = False, "" + if not valid: + raise ValueError("Current query filter has an invalid type or value") + result[name] = encoded + for name, value in filters.items(): + if name.endswith("From") and name[:-4] + "To" in filters: + other = name[:-4] + "To" + low, high = result[name], result[other] + reversed_range = ( + Decimal(low) > Decimal(high) + if definitions[name]["type"] in {"integer", "number"} + else low > high + ) + if reversed_range: + raise ValueError("Current query lower bound follows upper bound") + return result + + +def decode_page( + payload: Payload, endpoint: Endpoint[RowT, FiltersT], *, page: int, size: int +) -> DataPage[RowT]: + """Decode by declared field order, with no unknown-type fallback. + + Args: + payload: Bounded public JSON response and receipt. + endpoint: Generated row model and pinned field contract. + page: Requested one-based page. + size: Requested maximum number of rows. + + Returns: + Non-null typed rows and complete source pagination metadata. + + Raises: + SchemaMismatchError: Row fields, types, or pagination metadata disagree. + """ + body = payload.json() + expected = {f["name"]: f["dataType"] for f in endpoint.contract["fields"]} + try: + fields = body["fields"] + names = [f["name"] for f in fields] + if ( + len(set(names)) != len(names) + or {f["name"]: f["dataType"] for f in fields} != expected + ): + raise ValueError + meta = body["_meta"] + if any( + type(meta[k]) is not int or meta[k] < 0 + for k in ("currentPage", "totalPages", "totalRecords", "pageSize") + ): + raise ValueError + raw_rows = body["data"] + if not isinstance(raw_rows, list) or len(raw_rows) > size: + raise ValueError + empty = not raw_rows and meta["totalRecords"] == meta["totalPages"] == 0 + if meta["currentPage"] != page and not ( + empty and page == 1 and meta["currentPage"] == 0 + ): + raise ValueError + if not empty and ( + not raw_rows + or page > meta["totalPages"] + or meta["pageSize"] < len(raw_rows) + or meta["totalRecords"] < len(raw_rows) + ): + raise ValueError + rows = [] + for values in raw_rows: + if isinstance(values, list) and len(values) == len(names): + values = dict(zip(names, values, strict=True)) + if not isinstance(values, dict) or set(values) != set(expected): + raise ValueError + for name, kind in expected.items(): + value = values[name] + if kind in {"DOUBLE", "FLOAT"}: + if ( + type(value) not in (Decimal, int) + or not Decimal(value).is_finite() + ): + raise ValueError + elif kind in {"INTEGER", "LONG"}: + if type(value) is not int: + raise ValueError + elif kind == "BOOLEAN": + if type(value) is not bool: + raise ValueError + elif kind in {"VARCHAR", "DATE", "DATETIME"}: + if type(value) is not str: + raise ValueError + if ( + kind == "DATE" + and datetime.date.fromisoformat(value).isoformat() != value + ): + raise ValueError + if kind == "DATETIME": + datetime.datetime.fromisoformat(value) + else: + raise ValueError + rows.append(endpoint.row_model.model_validate(values)) + return DataPage(tuple(rows), meta, payload.receipt, payload.body) + except (KeyError, TypeError, ValueError): + pass + raise SchemaMismatchError( + "Current report differs from its verified row/page contract" + ) + + +class ReportsClient(PublicClient): + """A typed registry client with optional complete-query iteration. + + Only generated, registered public endpoints are accepted. Unknown schemas + are not raw adapters. Existing PublicClient methods keep their defaults. + """ + + def __init__( + self, + credentials: Credentials, + *, + limits: StreamingLimits | None = None, + transport: httpx.BaseTransport | None = None, + ) -> None: + """Create the opt-in streaming client without a network request. + + Args: + credentials: Explicit Public API account credentials. + limits: Request budget; max_requests=None allows a complete query. + transport: Optional mocked transport for offline tests. + """ + super().__init__( + credentials, limits=limits or StreamingLimits(), transport=transport + ) + + def page( + self, + endpoint: Endpoint[RowT, FiltersT], + *, + filters: FiltersT | None = None, + page: int = 1, + size: int = 1000, + sort: str | None = None, + direction: Literal["asc", "desc"] = "asc", + ) -> DataPage[RowT]: + """Fetch one typed page using all supported current filter names. + + Args: + endpoint: Generated endpoint constant, with a typed filter dictionary. + filters: Optional source filters from that constant's query contract. + page: One-based page number. + size: Rows per response, from 1 through 1000. + sort: One source-declared sortable field, or source default order. + direction: Ascending or descending source sort direction. + + Returns: + Typed rows and source metadata with an exact public receipt. + + Raises: + ValueError: Local filters or page controls are invalid. + PublicDataError: Transport, access, budget, or schema checks fail. + """ + if not any(endpoint is registered for registered in ENDPOINTS): + raise ValueError("Endpoint has no registered verified public schema") + if ( + type(page) is not int + or page < 1 + or type(size) is not int + or not 1 <= size <= 1000 + or direction not in {"asc", "desc"} + ): + raise ValueError("Invalid current page controls") + parameters: dict[str, str | int] = dict( + query_parameters(endpoint, filters or {}) + ) + parameters.update({"page": page, "size": size, "dir": direction}) + if sort is not None: + if sort not in { + f["name"] + for f in endpoint.contract["fields"] + if f.get("sortable") is True + }: + raise ValueError("Unknown or unsupported sort field") + parameters["sort"] = sort + return decode_page( + self._authenticated_payload(endpoint.path, parameters), + endpoint, + page=page, + size=size, + ) + + def pages( + self, + endpoint: Endpoint[RowT, FiltersT], + *, + filters: FiltersT | None = None, + size: int = 1000, + sort: str | None = None, + direction: Literal["asc", "desc"] = "asc", + max_pages: int | None = 100, + ) -> Iterator[DataPage[RowT]]: + """Stream a complete filtered query or raise at an explicit budget. + + Args: + endpoint: Generated current endpoint constant. + filters: Source filters, without a fixed date-window restriction. + size: Maximum rows per response. + sort: Source-declared sortable field, or source default order. + direction: Source sort direction. + max_pages: Positive page budget, or None for all available pages. + + Yields: + Pages with individual receipts. Budget exhaustion raises instead of + silently presenting a partial result as complete. + + Raises: + ValueError: A local bound or query control is invalid. + PublicDataError: Transport, schema, changing totals, or budgets fail. + """ + if max_pages is not None and (type(max_pages) is not int or max_pages < 1): + raise ValueError("max_pages must be positive or None") + number = 1 + previous = None + totals = None + received = 0 + while True: + if max_pages is not None and number > max_pages: + raise LimitError("Page budget reached before the query completed") + result = self.page( + endpoint, + filters=filters, + page=number, + size=size, + sort=sort, + direction=direction, + ) + current_totals = (result.meta["totalPages"], result.meta["totalRecords"]) + if totals is not None and totals != current_totals: + raise SchemaMismatchError("Source totals changed during pagination") + totals = current_totals + digest = hashlib.sha256( + "\n".join(row.model_dump_json() for row in result.rows).encode() + ).hexdigest() + if result.rows and digest == previous: + raise SchemaMismatchError("Source repeated the preceding page") + previous = digest + received += len(result.rows) + if received > result.meta["totalRecords"] or ( + number >= result.meta["totalPages"] + and received != result.meta["totalRecords"] + ): + raise SchemaMismatchError("Page rows disagree with source totalRecords") + yield result + if number >= result.meta["totalPages"]: + return + number += 1 + + def iter_rows( + self, + endpoint: Endpoint[RowT, FiltersT], + *, + filters: FiltersT | None = None, + size: int = 1000, + sort: str | None = None, + direction: Literal["asc", "desc"] = "asc", + max_pages: int | None = 100, + max_rows: int | None = 100_000, + ) -> Iterator[RowT]: + """Stream typed rows with memory bounded by one response page. + + Args: + endpoint: Generated current endpoint constant. + filters: Source filters for the desired complete selection. + size: Maximum rows retained per response page. + sort: Source-declared sortable field, or source default order. + direction: Source sort direction. + max_pages: Page budget, or None to remove this caller safeguard. + max_rows: Row budget, or None to remove this caller safeguard. + + Yields: + Source rows. Exhaustion means the reported query completed; budgets + raise explicitly. This is not a stable as-of snapshot guarantee. + + Raises: + ValueError: A local bound is invalid. + PublicDataError: Retrieval, source consistency, or a budget fails. + """ + if max_rows is not None and (type(max_rows) is not int or max_rows < 1): + raise ValueError("max_rows must be positive or None") + count = 0 + for response in self.pages( + endpoint, + filters=filters, + size=size, + sort=sort, + direction=direction, + max_pages=max_pages, + ): + for row in response.rows: + if max_rows is not None and count >= max_rows: + raise LimitError("Row budget reached before the query completed") + count += 1 + yield row diff --git a/tinyercot/public/schema.py b/tinyercot/public/schema.py new file mode 100644 index 00000000..9bdd9e8c --- /dev/null +++ b/tinyercot/public/schema.py @@ -0,0 +1,148 @@ +"""Typed identities for verified public contracts; no network access on import.""" + +from __future__ import annotations + +import json +from collections.abc import Iterator, Mapping +from dataclasses import dataclass +from importlib.resources import files +from typing import TYPE_CHECKING, Any, Generic, Literal, TypeVar + +from pydantic import BaseModel + +if TYPE_CHECKING: + from .reports import DataPage, ReportsClient + +RowT = TypeVar("RowT", bound=BaseModel) +FiltersT = TypeVar("FiltersT", bound=Mapping[str, object]) + + +@dataclass(frozen=True) +class Endpoint(Generic[RowT, FiltersT]): + """A generated public data operation and its pinned query/row evidence. + + Attributes: + path: Verified Public Reports data path. + row_model: Generated model for observed non-null rows. + contract_name: Bundled compact source projection key. + """ + + path: str + row_model: type[RowT] + contract_name: str + + @property + def contract(self) -> dict[str, Any]: + """Return a fresh copy of the bundled source-backed contract. + + Returns: + Query descriptors, row fields, and public source receipts. + """ + bundle = json.loads( + files("tinyercot.public").joinpath("_contracts.json").read_text() + ) + return dict(bundle[self.contract_name]) + + def page( + self, + client: ReportsClient, + *, + filters: FiltersT | None = None, + page: int = 1, + size: int = 1000, + sort: str | None = None, + direction: Literal["asc", "desc"] = "asc", + ) -> DataPage[RowT]: + """Fetch one page with filter types bound to this endpoint identity. + + Args: + client: Explicit authenticated public client. + filters: This endpoint's generated optional filter dictionary. + page: One-based page number. + size: Maximum source rows in the response. + sort: A source-declared sortable field, or source default order. + direction: Source sort direction. + + Returns: + Typed rows, source metadata, raw public bytes, and a receipt. + + Raises: + ValueError: Filters or query controls are invalid. + PublicDataError: Retrieval, budget, or source checks fail. + """ + return client.page( + self, filters=filters, page=page, size=size, sort=sort, direction=direction + ) + + def pages( + self, + client: ReportsClient, + *, + filters: FiltersT | None = None, + size: int = 1000, + sort: str | None = None, + direction: Literal["asc", "desc"] = "asc", + max_pages: int | None = 100, + ) -> Iterator[DataPage[RowT]]: + """Stream complete filtered pages or raise at the caller's budget. + + Args: + client: Explicit authenticated public client. + filters: This endpoint's generated optional filter dictionary. + size: Maximum source rows per response. + sort: A source-declared sortable field, or source default order. + direction: Source sort direction. + max_pages: Page budget, or None to permit all available pages. + + Returns: + An iterator of typed pages with separate receipts. + + Raises: + PublicDataError: Iteration encounters a source or budget failure. + """ + return client.pages( + self, + filters=filters, + size=size, + sort=sort, + direction=direction, + max_pages=max_pages, + ) + + def iter_rows( + self, + client: ReportsClient, + *, + filters: FiltersT | None = None, + size: int = 1000, + sort: str | None = None, + direction: Literal["asc", "desc"] = "asc", + max_pages: int | None = 100, + max_rows: int | None = 100_000, + ) -> Iterator[RowT]: + """Stream typed rows with endpoint-specific filter checking. + + Args: + client: Explicit authenticated public client. + filters: This endpoint's generated optional filter dictionary. + size: Maximum rows retained per response. + sort: A source-declared sortable field, or source default order. + direction: Source sort direction. + max_pages: Page budget, or None for a complete query. + max_rows: Row budget, or None for a complete query. + + Returns: + A typed row iterator. Budgets raise instead of hiding truncation. + + Raises: + PublicDataError: Iteration encounters a source or budget failure. + """ + return client.iter_rows( + self, + filters=filters, + size=size, + sort=sort, + direction=direction, + max_pages=max_pages, + max_rows=max_rows, + ) diff --git a/tinyercot/py.typed b/tinyercot/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/tools/check_typing.py b/tools/check_typing.py new file mode 100644 index 00000000..dad75728 --- /dev/null +++ b/tools/check_typing.py @@ -0,0 +1,163 @@ +"""Check public types from an installed wheel outside the source checkout.""" + +import argparse +import ast +import os +import re +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] + + +def legacy_assertions(source: str) -> tuple[str, int]: + """Build static calls for every frozen generated legacy method. + + Args: + source: The installed legacy generated module's Python source. + + Returns: + Uncalled functions checking method returns and row fields, and method count. + """ + lines = [ + '"""Static legacy assertions; this function is never executed."""', + "from collections.abc import AsyncIterator, Iterator", + "from typing import assert_type", + "import datetime", + "from decimal import Decimal", + "import pandas as pd", + "from tinyercot import *", + "", + "async def verify_legacy() -> None:", + ] + count = 0 + field_checks = [] + for category in ast.parse(source).body: + if not isinstance(category, ast.ClassDef): + continue + nested_models = { + node.name for node in category.body if isinstance(node, ast.ClassDef) + } + for model in category.body: + if not isinstance(model, ast.ClassDef) or not model.name.endswith("Row"): + continue + field_checks.extend( + [ + "", + f"def verify_{category.name}_{model.name}(row: {category.name}.{model.name}) -> None:", + ] + ) + for declaration in model.body: + if isinstance(declaration, ast.AnnAssign) and isinstance( + declaration.target, ast.Name + ): + field_checks.append( + f" assert_type(row.{declaration.target.id}, {ast.unparse(declaration.annotation)})" + ) + for method in category.body: + if not isinstance(method, (ast.FunctionDef, ast.AsyncFunctionDef)): + continue + if method.returns is None: + raise ValueError("Legacy method lacks a return annotation") + expected = ( + method.returns.value + if isinstance(method.returns, ast.Constant) + else ast.unparse(method.returns) + ) + for model in nested_models: + expected = re.sub( + rf"\b{re.escape(model)}\b", f"{category.name}.{model}", expected + ) + await_call = isinstance(method, ast.AsyncFunctionDef) and not any( + isinstance(node, (ast.Yield, ast.YieldFrom)) + for node in ast.walk(method) + ) + call = f"{category.name}.{method.name}()" + lines.append( + f" assert_type({'await ' if await_call else ''}{call}, {expected})" + ) + count += 1 + return "\n".join(lines + field_checks) + "\n", count + + +def main() -> None: + """Require precise public types and rejected invalid uses from a real wheel. + + Raises: + SystemExit: The interpreter uses the checkout or a type check fails. + """ + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--python", required=True, type=Path) + args = parser.parse_args() + result = subprocess.run( + [str(args.python), "-I", "-c", "import tinyercot; print(tinyercot.__file__)"], + capture_output=True, + text=True, + check=True, + ) + package = Path(result.stdout.strip()).resolve().parent + if package == ROOT / "tinyercot" or not (package / "py.typed").is_file(): + raise SystemExit("Typing proof requires an installed PEP 561 wheel") + legacy, count = legacy_assertions((package / "_generated.py").read_text()) + if count != 510: + raise SystemExit("Installed legacy method inventory changed") + field_count = legacy.count("assert_type(") - count + if field_count != 1334: + raise SystemExit("Installed legacy row-field inventory changed") + with tempfile.TemporaryDirectory(prefix="tinyercot-typing-") as directory: + scratch = Path(directory) + shutil.copyfile( + ROOT / "tests/typing_public.py", scratch / "public_contracts.py" + ) + shutil.copyfile( + ROOT / "tests/typing_invalid.py", scratch / "invalid_contracts.py" + ) + (scratch / "legacy_contracts.py").write_text(legacy) + environment = { + k: v for k, v in os.environ.items() if k not in {"MYPYPATH", "PYTHONPATH"} + } + command = [ + sys.executable, + "-m", + "mypy", + "--python-executable", + str(args.python), + "--config-file", + os.devnull, + "--strict", + "--no-incremental", + ] + positive = subprocess.run( + command + ["public_contracts.py", "legacy_contracts.py"], + cwd=scratch, + env=environment, + capture_output=True, + text=True, + check=False, + ) + if positive.returncode: + print(positive.stdout) + raise SystemExit("Installed positive typing proof failed") + negative = subprocess.run( + command + ["invalid_contracts.py"], + cwd=scratch, + env=environment, + capture_output=True, + text=True, + check=False, + ) + if negative.returncode != 1 or negative.stdout.count(": error:") != 4: + print(negative.stdout) + raise SystemExit( + "Installed typing proof did not reject exactly four invalid uses" + ) + print( + f"Installed PEP 561 proof passed: {count} legacy methods, {field_count} row fields, public surfaces, four rejected invalid uses" + ) + + +if __name__ == "__main__": + main() diff --git a/tools/discover_public.py b/tools/discover_public.py new file mode 100644 index 00000000..020a7442 --- /dev/null +++ b/tools/discover_public.py @@ -0,0 +1,185 @@ +"""Project a current public contract from saved primary evidence, offline.""" + +import argparse +import datetime +import hashlib +import json +import keyword +from decimal import Decimal +from pathlib import Path +from urllib.parse import parse_qs, urlsplit + +SOURCE_URL = "https://apiexplorer.ercot.com/developer/apis/pubapi-apim-api?export=true&api-version=2022-04-01-preview" +CATALOG = Path(__file__).resolve().parents[1] / "tinyercot/_catalog.json" +ROW_TYPES = { + "DATE", + "DATETIME", + "VARCHAR", + "DOUBLE", + "FLOAT", + "INTEGER", + "LONG", + "BOOLEAN", +} + + +def discover(*, spec: Path, response: Path, receipt: Path, path: str) -> dict: + """Project query and observed row metadata with exact source identities. + + Args: + spec: Saved authoritative Public Reports OpenAPI export. + response: Small saved public data response, including fields and rows. + receipt: Public-only receipt for those exact response bytes. + path: An observed public data GET path from the offline catalog. + + Returns: + A compact contract with verified_observed, missing, or unknown row status. + Empty rows, nulls, and unknown field types do not establish typed coverage. + + Raises: + ValueError: Source identity, hash, or public-operation evidence disagrees. + """ + catalog = json.loads(CATALOG.read_text()) + spec_bytes = spec.read_bytes() + if ( + hashlib.sha256(spec_bytes).hexdigest() + != catalog["sources"]["public-reports"]["sha256"] + ): + raise ValueError("OpenAPI bytes do not match the primary-source catalog pin") + if path not in { + op["path"] + for op in catalog["operations"] + if op["service"] == "public-reports" and op["kind"] == "data" + }: + raise ValueError("Path is not an observed public data GET") + raw = response.read_bytes() + record = json.loads(receipt.read_text()) + if set(record) - { + "source_url", + "retrieved_at", + "sha256", + "byte_count", + "bytes", + "status", + "query", + }: + raise ValueError( + "Receipt contains unsupported metadata; headers are not accepted" + ) + source = urlsplit(record["source_url"]) + if ( + source.scheme != "https" + or source.netloc != "api.ercot.com" + or source.path != "/api/public-reports" + path + or record["status"] != 200 + or record["sha256"] != hashlib.sha256(raw).hexdigest() + or record.get("byte_count", record.get("bytes")) != len(raw) + ): + raise ValueError("Response receipt does not identify the public bytes") + specification = json.loads(spec.read_bytes()) + operation = specification["paths"][path]["get"] + queries = [ + {"name": p["name"], "in": "query", "schema": p["schema"]} + for p in operation["parameters"] + if p["in"] == "query" + ] + query_names = {query["name"] for query in queries} + if ( + set(parse_qs(source.query)) - query_names + or set(record.get("query", {})) - query_names + ): + raise ValueError( + "Receipt URL contains fields outside the public query contract" + ) + body = json.loads(raw, parse_float=Decimal) + fields = body.get("fields", []) + status = "verified_observed" + if not fields: + status = "missing" + elif not isinstance(fields, list): + raise ValueError("Invalid response field metadata") + else: + names = [f["name"] for f in fields] + if len(set(names)) != len(names): + raise ValueError("Duplicate response field names") + if any( + not name.isidentifier() + or keyword.iskeyword(name) + or name.startswith(("_", "model_")) + for name in names + ): + raise ValueError("Response field cannot be represented safely") + if any(f["dataType"] not in ROW_TYPES for f in fields): + status = "unknown" + rows = body.get("data") + if not isinstance(rows, list) or not rows: + status = "unknown" + else: + for row in rows: + if isinstance(row, dict): + if set(row) != set(names): + raise ValueError("Response row keys differ from fields") + values = [row[name] for name in names] + elif isinstance(row, list) and len(row) == len(names): + values = row + else: + raise ValueError("Response row width differs from fields") + if any(value is None for value in values): + status = "unknown" + for descriptor, value in zip(fields, values, strict=True): + kind = descriptor["dataType"] + valid = True + if kind in {"DOUBLE", "FLOAT"}: + valid = ( + type(value) in {int, Decimal} and Decimal(value).is_finite() + ) + elif kind in {"INTEGER", "LONG"}: + valid = type(value) is int + elif kind == "BOOLEAN": + valid = type(value) is bool + elif kind in {"DATE", "DATETIME", "VARCHAR"}: + valid = type(value) is str + if valid and kind in {"DATE", "DATETIME"}: + try: + if kind == "DATE": + valid = ( + datetime.date.fromisoformat(value).isoformat() + == value + ) + else: + datetime.datetime.fromisoformat(value) + except ValueError: + valid = False + if not valid: + status = "unknown" + return { + "format_version": 1, + "service": "public-reports", + "path": path, + "observed_at": record["retrieved_at"], + "row_schema": status, + "query_parameters": queries, + "fields": fields, + "nullability_policy": "Reject nulls; observed fields do not establish nullability.", + "source_url": SOURCE_URL, + "query_source_sha256": hashlib.sha256(spec.read_bytes()).hexdigest(), + "response_source": record, + } + + +def main() -> None: + """Write a compact projection; never fetch upstream or alter legacy files.""" + parser = argparse.ArgumentParser(description=__doc__) + for name in ("spec", "response", "receipt", "output"): + parser.add_argument("--" + name, type=Path, required=True) + parser.add_argument("--path", required=True) + args = parser.parse_args() + contract = discover( + spec=args.spec, response=args.response, receipt=args.receipt, path=args.path + ) + args.output.write_text(json.dumps(contract, indent=2) + "\n") + print(f"Projected row status: {contract['row_schema']}") + + +if __name__ == "__main__": + main() diff --git a/tools/generate_public.py b/tools/generate_public.py index bb81f62d..cbc7f055 100644 --- a/tools/generate_public.py +++ b/tools/generate_public.py @@ -3,6 +3,9 @@ import argparse import hashlib import json +import keyword +import subprocess +import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[1] @@ -10,12 +13,114 @@ CAPACITY_INPUT = ROOT / "tools/inputs/current/dam-capacity.json" MANIFEST = ROOT / "tools/inputs/current/provenance.json" OUTPUT = ROOT / "tinyercot/public/_generated.py" +REGISTRY_OUTPUT = ROOT / "tinyercot/public/_schemas.py" +BUNDLE_OUTPUT = ROOT / "tinyercot/public/_contracts.json" TYPES = { "DATE": "date", "VARCHAR": "StrictStr", "DOUBLE": "Decimal", "BOOLEAN": "StrictBool", + "INTEGER": "StrictInt", + "LONG": "StrictInt", + "DATETIME": "datetime", + "FLOAT": "Decimal", } + + +def contracts() -> list[tuple[dict, dict]]: + """Load configured public contracts only when pins and schemas verify. + + Returns: + Ordered generator names and their verified source projections. + + Raises: + ValueError: A hash, public path, name, or row schema is not supported. + """ + manifest = json.loads(MANIFEST.read_text()) + catalog = json.loads((ROOT / "tinyercot/_catalog.json").read_text()) + public_paths = { + op["path"] + for op in catalog["operations"] + if op["service"] == "public-reports" and op["kind"] == "data" + } + result = [] + identities = set() + symbols = { + "Endpoint", + "ENDPOINTS", + "TypedDict", + "BaseModel", + "ConfigDict", + "date", + "datetime", + "Decimal", + "StrictBool", + "StrictInt", + "StrictStr", + } + for entry in manifest["contracts"]: + path = ( + INPUT + if entry["input"] == "dam-prices.json" + else CAPACITY_INPUT + if entry["input"] == "dam-capacity.json" + else MANIFEST.parent / entry["input"] + ) + if Path(entry["input"]).name != entry["input"]: + raise ValueError("Generator input must be a local basename") + raw = path.read_bytes() + if hashlib.sha256(raw).hexdigest() != manifest["inputs"][entry["input"]]: + raise ValueError("Unverified current response metadata") + contract = json.loads(raw) + if ( + contract["service"] != "public-reports" + or contract["path"] not in public_paths + ): + raise ValueError("No verified public operation for this contract") + if contract.get("row_schema") != "verified_observed": + raise ValueError("Missing or unknown row schemas cannot be generated") + if ( + contract["query_source_sha256"] + != catalog["sources"]["public-reports"]["sha256"] + ): + raise ValueError("Query evidence differs from the primary catalog pin") + fields = contract["fields"] + names = [f["name"] for f in fields] + if not fields or len(set(names)) != len(names): + raise ValueError("Missing or duplicate response fields") + for name in [ + *names, + entry["model"], + entry["constant"], + entry["fields_constant"], + ]: + if ( + not name.isidentifier() + or keyword.iskeyword(name) + or name.startswith(("_", "model_")) + ): + raise ValueError("Unsupported Python field or model name") + if any(f["dataType"] not in TYPES for f in fields): + raise ValueError("Unsupported response field type") + declared = [ + entry["model"], + entry["constant"], + entry["fields_constant"], + entry["model"] + "Filters", + ] + if len(set(declared)) != len(declared) or symbols.intersection(declared): + raise ValueError("Colliding generated symbol") + symbols.update(declared) + query_names = [p["name"] for p in contract["query_parameters"]] + if len(query_names) != len(set(query_names)): + raise ValueError("Duplicate query fields") + if contract["path"] in identities: + raise ValueError("Duplicate public operation") + identities.add(contract["path"]) + result.append((entry, contract)) + return result + + DESCRIPTIONS = { "deliveryDate": "Source operating date, without an inferred timezone.", "hourEnding": "Source hour-ending label, including 24:00.", @@ -28,57 +133,50 @@ def render() -> str: - """Render the two observed non-null DAM row contracts. + """Render all configured, verified non-null public row contracts. Returns: - Model source for two verified endpoints and their ordered fields. + Model source for verified endpoints and their ordered fields. Raises: ValueError: A pin, field type, or supported endpoint differs. """ lines = [ "# Generated by tools/generate_public.py from pinned public field metadata.", - '"""Observed non-null DAM rows; unknown schemas fail decoding."""', + '"""Observed non-null public rows; unknown schemas fail decoding."""', "", - "from datetime import date", + "from datetime import " + + ", ".join( + sorted( + { + TYPES[f["dataType"]] + for _, contract in contracts() + for f in contract["fields"] + if f["dataType"] in {"DATE", "DATETIME"} + } + ) + ), "from decimal import Decimal", "", - "from pydantic import BaseModel, ConfigDict, StrictBool, StrictStr", + "from pydantic import BaseModel, ConfigDict, StrictBool, StrictInt, StrictStr", "", ] - for path, expected_path, model, constant in ( - (INPUT, "/np4-190-cd/dam_stlmnt_pnt_prices", "DamPrice", "DAM_FIELDS"), - ( - CAPACITY_INPUT, - "/np4-188-cd/dam_clear_price_for_cap", - "DamCapacityPrice", - "CAPACITY_FIELDS", - ), - ): - raw = path.read_bytes() - expected = json.loads(MANIFEST.read_text())["inputs"][path.name] - if hashlib.sha256(raw).hexdigest() != expected: - raise ValueError("Unverified current response metadata") - contract = json.loads(raw) - if contract["path"] != expected_path: - raise ValueError("No row generator policy for this endpoint") + for entry, contract in contracts(): + model, constant = entry["model"], entry["fields_constant"] fields = contract["fields"] - if not fields or len({f["name"] for f in fields}) != len(fields): - raise ValueError("Missing or duplicate response fields") - if any( - f["name"] not in DESCRIPTIONS or f["dataType"] not in TYPES for f in fields - ): - raise ValueError("Unsupported response field") lines.extend( [ "", f"class {model}(BaseModel):", - ' """A non-null row from the observed public DAM field contract.', + ' """A non-null row from the observed public field contract.', "", " Attributes:", ] ) - lines.extend(f" {f['name']}: {DESCRIPTIONS[f['name']]}" for f in fields) + lines.extend( + f" {f['name']}: {DESCRIPTIONS.get(f['name'], 'Source ' + f['name'] + ' value.')}" + for f in fields + ) lines.extend( [ ' """', @@ -96,18 +194,118 @@ def render() -> str: return "\n".join(lines) +def render_registry() -> str: + """Render typed filter dictionaries and operation identities. + + Returns: + Registry source for the same verified row contracts. + + Raises: + ValueError: A query field type or name is unsupported. + """ + entries = contracts() + lines = [ + "# Generated by tools/generate_public.py; do not edit.", + '"""Verified public operation identities and complete query filter types."""', + "", + "from datetime import date", + "from decimal import Decimal", + "from typing import TypedDict", + "", + "from ._generated import (", + *[f" {name}," for name in sorted(e["model"] for e, _ in entries)], + ")", + "from .schema import Endpoint", + "", + ] + for entry, contract in entries: + model = entry["model"] + lines.extend( + [ + "", + f"class {model}Filters(TypedDict, total=False):", + ' """Optional filters from the pinned current query specification."""', + "", + ] + ) + for parameter in contract["query_parameters"]: + name = parameter["name"] + if name in {"page", "size", "sort", "dir"}: + continue + if not name.isidentifier() or keyword.iskeyword(name): + raise ValueError("Unsupported query field name") + schema = parameter["schema"] + kind = schema["type"] + formats = { + "string": {None, "abc123", "yyyy-MM-dd"}, + "integer": {None, "###"}, + "number": {None, "####.###"}, + "boolean": {None, "true | false"}, + } + if schema.get("format") not in formats.get(kind, set()): + raise ValueError("Unsupported current query format") + query_type = { + "integer": "int", + "number": "float | Decimal", + "boolean": "bool", + "string": "str", + }.get(kind) + if kind == "string" and schema.get("format") == "yyyy-MM-dd": + query_type = "date" + if query_type is None: + raise ValueError("Unsupported current query type") + lines.append(f" {name}: {query_type}") + lines.extend( + [ + "", + "", + f"{entry['constant']}: Endpoint[{model}, {model}Filters] = Endpoint(", + f" {json.dumps(contract['path'])}, {model}, {json.dumps(entry['input'])}", + ")", + "", + ] + ) + lines.extend( + ["", "ENDPOINTS = (", *[f" {e['constant']}," for e, _ in entries], ")", ""] + ) + return subprocess.run( + [ + sys.executable, + "-m", + "ruff", + "format", + "--config", + str(ROOT / "pyproject.toml"), + "--stdin-filename", + str(REGISTRY_OUTPUT), + ], + input="\n".join(lines), + capture_output=True, + text=True, + check=True, + ).stdout + + def main() -> None: """Write or check additive models without changing legacy generation.""" parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--check", action="store_true") args = parser.parse_args() - output = render() + outputs = { + OUTPUT: render(), + REGISTRY_OUTPUT: render_registry(), + BUNDLE_OUTPUT: json.dumps( + {entry["input"]: contract for entry, contract in contracts()}, indent=2 + ) + + "\n", + } if args.check: - if OUTPUT.read_text() != output: + if any(path.read_text() != output for path, output in outputs.items()): raise SystemExit("Current generated models differ from the pinned inputs") print("Current models match pinned response metadata") else: - OUTPUT.write_text(output) + for path, output in outputs.items(): + path.write_text(output) if __name__ == "__main__": diff --git a/tools/inputs/current/dam-capacity.json b/tools/inputs/current/dam-capacity.json index d11c9f73..3bee7005 100644 --- a/tools/inputs/current/dam-capacity.json +++ b/tools/inputs/current/dam-capacity.json @@ -153,5 +153,6 @@ "sha256": "3fa8d075dadb0baf6cb0b4656ec87e887a7c85c6d96ab2923dc3e42bd8373251", "byte_count": 1336, "status": 200 - } + }, + "row_schema": "verified_observed" } diff --git a/tools/inputs/current/dam-prices.json b/tools/inputs/current/dam-prices.json index 3d1414e5..434e5231 100644 --- a/tools/inputs/current/dam-prices.json +++ b/tools/inputs/current/dam-prices.json @@ -158,5 +158,6 @@ "bytes": 1384, "sha256": "dc70cef89b0d7ce0fb53a6441f8d5bdd0f79b5d60fff6125253046017cf37d18", "status": 200 - } + }, + "row_schema": "verified_observed" } diff --git a/tools/inputs/current/provenance.json b/tools/inputs/current/provenance.json index 87516366..9f4f0703 100644 --- a/tools/inputs/current/provenance.json +++ b/tools/inputs/current/provenance.json @@ -1,7 +1,35 @@ { "format_version": 1, "inputs": { - "dam-prices.json": "7b0a79a756ecfafb3f1d72d5c244b9ce06fdcba40d99f10ab05cd08a341e6bc4", - "dam-capacity.json": "46b3352eb03262a48d47ca499b5a2a7a96bdbb9ba1c769108428b0018f884969" - } + "dam-prices.json": "ba4ecaa6b1721e233b310e3c76fa5f3cd5ab1aed0057b8a2480f36b2f08df4e8", + "dam-capacity.json": "73a04f0a1fd40e2edaaa47d048fd97de999dbbf9a6fedffcf9a49422eddfb1f8", + "rt-prices.json": "d4335f720e452150429066e1d3308b2e2ff2762a0fed73580130ea7b07c11de4", + "system-load.json": "11249700b9bbcd6a2741cb657b99b1d5afc147476b86466459fc3b72aed4f020" + }, + "contracts": [ + { + "input": "dam-prices.json", + "model": "DamPrice", + "constant": "DAM_PRICES", + "fields_constant": "DAM_FIELDS" + }, + { + "input": "dam-capacity.json", + "model": "DamCapacityPrice", + "constant": "DAM_CAPACITY_PRICES", + "fields_constant": "CAPACITY_FIELDS" + }, + { + "input": "rt-prices.json", + "model": "RealTimePrice", + "constant": "RT_PRICES", + "fields_constant": "RT_FIELDS" + }, + { + "input": "system-load.json", + "model": "SystemLoad", + "constant": "SYSTEM_LOAD", + "fields_constant": "LOAD_FIELDS" + } + ] } diff --git a/tools/inputs/current/rt-prices.json b/tools/inputs/current/rt-prices.json new file mode 100644 index 00000000..503de970 --- /dev/null +++ b/tools/inputs/current/rt-prices.json @@ -0,0 +1,204 @@ +{ + "format_version": 1, + "service": "public-reports", + "path": "/np6-905-cd/spp_node_zone_hub", + "observed_at": "2026-09-05 21:14:32.319440+00:00", + "row_schema": "verified_observed", + "query_parameters": [ + { + "name": "deliveryDateFrom", + "in": "query", + "schema": { + "type": "string", + "format": "yyyy-MM-dd" + } + }, + { + "name": "deliveryDateTo", + "in": "query", + "schema": { + "type": "string", + "format": "yyyy-MM-dd" + } + }, + { + "name": "deliveryHourFrom", + "in": "query", + "schema": { + "type": "integer", + "format": "###" + } + }, + { + "name": "deliveryHourTo", + "in": "query", + "schema": { + "type": "integer", + "format": "###" + } + }, + { + "name": "deliveryIntervalFrom", + "in": "query", + "schema": { + "type": "integer", + "format": "###" + } + }, + { + "name": "deliveryIntervalTo", + "in": "query", + "schema": { + "type": "integer", + "format": "###" + } + }, + { + "name": "settlementPoint", + "in": "query", + "schema": { + "type": "string", + "format": "abc123" + } + }, + { + "name": "settlementPointType", + "in": "query", + "schema": { + "type": "string", + "format": "abc123" + } + }, + { + "name": "settlementPointPriceFrom", + "in": "query", + "schema": { + "type": "number", + "format": "####.###" + } + }, + { + "name": "settlementPointPriceTo", + "in": "query", + "schema": { + "type": "number", + "format": "####.###" + } + }, + { + "name": "DSTFlag", + "in": "query", + "schema": { + "type": "boolean", + "format": "true | false" + } + }, + { + "name": "page", + "in": "query", + "schema": { + "type": "integer", + "format": "###" + } + }, + { + "name": "size", + "in": "query", + "schema": { + "type": "integer", + "format": "###" + } + }, + { + "name": "sort", + "in": "query", + "schema": { + "type": "string", + "format": "abc123" + } + }, + { + "name": "dir", + "in": "query", + "schema": { + "type": "string", + "format": "abc123" + } + } + ], + "fields": [ + { + "name": "deliveryDate", + "label": "Delivery Date", + "cardinality": 1, + "dataType": "DATE", + "searchable": true, + "sortable": true, + "hasRange": true + }, + { + "name": "deliveryHour", + "label": "Delivery Hour", + "cardinality": 2, + "dataType": "INTEGER", + "searchable": true, + "sortable": true, + "hasRange": true + }, + { + "name": "deliveryInterval", + "label": "Delivery Interval", + "cardinality": 3, + "dataType": "INTEGER", + "searchable": true, + "sortable": true, + "hasRange": true + }, + { + "name": "settlementPoint", + "label": "Settlement Point", + "cardinality": 4, + "dataType": "VARCHAR", + "searchable": true, + "sortable": true, + "hasRange": false + }, + { + "name": "settlementPointType", + "label": "Settlement Point Type", + "cardinality": 5, + "dataType": "VARCHAR", + "searchable": true, + "sortable": true, + "hasRange": false + }, + { + "name": "settlementPointPrice", + "label": "Settlement Point Price", + "cardinality": 6, + "dataType": "DOUBLE", + "searchable": true, + "sortable": true, + "hasRange": true + }, + { + "name": "DSTFlag", + "label": "DST Flag", + "cardinality": 7, + "dataType": "BOOLEAN", + "searchable": true, + "sortable": true, + "hasRange": false + } + ], + "nullability_policy": "Reject nulls; observed fields do not establish nullability.", + "source_url": "https://apiexplorer.ercot.com/developer/apis/pubapi-apim-api?export=true&api-version=2022-04-01-preview", + "query_source_sha256": "b978bf35fbf9dcb8edca14025d4b5d3108582885a6c1b92a9bf445e69b08a7f8", + "response_source": { + "source_url": "https://api.ercot.com/api/public-reports/np6-905-cd/spp_node_zone_hub?settlementPoint=HB_HOUSTON&size=2&page=1&sort=deliveryDate&dir=desc&deliveryDateFrom=2026-09-04&deliveryDateTo=2026-09-04", + "retrieved_at": "2026-09-05 21:14:32.319440+00:00", + "sha256": "211a6808039b86dfefbdfcab99e8859cf83de61ab67d2da87bd4b4bc13545c6a", + "byte_count": 1711, + "status": 200 + } +} diff --git a/tools/inputs/current/system-load.json b/tools/inputs/current/system-load.json new file mode 100644 index 00000000..629da2a6 --- /dev/null +++ b/tools/inputs/current/system-load.json @@ -0,0 +1,337 @@ +{ + "format_version": 1, + "service": "public-reports", + "path": "/np6-345-cd/act_sys_load_by_wzn", + "observed_at": "2026-09-05 21:14:36.433487+00:00", + "row_schema": "verified_observed", + "query_parameters": [ + { + "name": "operatingDayFrom", + "in": "query", + "schema": { + "type": "string", + "format": "yyyy-MM-dd" + } + }, + { + "name": "operatingDayTo", + "in": "query", + "schema": { + "type": "string", + "format": "yyyy-MM-dd" + } + }, + { + "name": "hourEnding", + "in": "query", + "schema": { + "type": "string", + "format": "abc123" + } + }, + { + "name": "coastFrom", + "in": "query", + "schema": { + "type": "number", + "format": "####.###" + } + }, + { + "name": "coastTo", + "in": "query", + "schema": { + "type": "number", + "format": "####.###" + } + }, + { + "name": "eastFrom", + "in": "query", + "schema": { + "type": "number", + "format": "####.###" + } + }, + { + "name": "eastTo", + "in": "query", + "schema": { + "type": "number", + "format": "####.###" + } + }, + { + "name": "farWestFrom", + "in": "query", + "schema": { + "type": "number", + "format": "####.###" + } + }, + { + "name": "farWestTo", + "in": "query", + "schema": { + "type": "number", + "format": "####.###" + } + }, + { + "name": "northFrom", + "in": "query", + "schema": { + "type": "number", + "format": "####.###" + } + }, + { + "name": "northTo", + "in": "query", + "schema": { + "type": "number", + "format": "####.###" + } + }, + { + "name": "northCFrom", + "in": "query", + "schema": { + "type": "number", + "format": "####.###" + } + }, + { + "name": "northCTo", + "in": "query", + "schema": { + "type": "number", + "format": "####.###" + } + }, + { + "name": "southernFrom", + "in": "query", + "schema": { + "type": "number", + "format": "####.###" + } + }, + { + "name": "southernTo", + "in": "query", + "schema": { + "type": "number", + "format": "####.###" + } + }, + { + "name": "southCFrom", + "in": "query", + "schema": { + "type": "number", + "format": "####.###" + } + }, + { + "name": "southCTo", + "in": "query", + "schema": { + "type": "number", + "format": "####.###" + } + }, + { + "name": "westFrom", + "in": "query", + "schema": { + "type": "number", + "format": "####.###" + } + }, + { + "name": "westTo", + "in": "query", + "schema": { + "type": "number", + "format": "####.###" + } + }, + { + "name": "totalFrom", + "in": "query", + "schema": { + "type": "number", + "format": "####.###" + } + }, + { + "name": "totalTo", + "in": "query", + "schema": { + "type": "number", + "format": "####.###" + } + }, + { + "name": "DSTFlag", + "in": "query", + "schema": { + "type": "boolean", + "format": "true | false" + } + }, + { + "name": "page", + "in": "query", + "schema": { + "type": "integer", + "format": "###" + } + }, + { + "name": "size", + "in": "query", + "schema": { + "type": "integer", + "format": "###" + } + }, + { + "name": "sort", + "in": "query", + "schema": { + "type": "string", + "format": "abc123" + } + }, + { + "name": "dir", + "in": "query", + "schema": { + "type": "string", + "format": "abc123" + } + } + ], + "fields": [ + { + "name": "operatingDay", + "label": "Operating Day", + "cardinality": 1, + "dataType": "DATE", + "searchable": true, + "sortable": true, + "hasRange": true + }, + { + "name": "hourEnding", + "label": "Hour Ending", + "cardinality": 2, + "dataType": "VARCHAR", + "searchable": true, + "sortable": true, + "hasRange": false + }, + { + "name": "coast", + "label": "Coast", + "cardinality": 3, + "dataType": "DOUBLE", + "searchable": true, + "sortable": true, + "hasRange": true + }, + { + "name": "east", + "label": "East", + "cardinality": 4, + "dataType": "DOUBLE", + "searchable": true, + "sortable": true, + "hasRange": true + }, + { + "name": "farWest", + "label": "Far West", + "cardinality": 5, + "dataType": "DOUBLE", + "searchable": true, + "sortable": true, + "hasRange": true + }, + { + "name": "north", + "label": "North", + "cardinality": 6, + "dataType": "DOUBLE", + "searchable": true, + "sortable": true, + "hasRange": true + }, + { + "name": "northC", + "label": "NorthC", + "cardinality": 7, + "dataType": "DOUBLE", + "searchable": true, + "sortable": true, + "hasRange": true + }, + { + "name": "southern", + "label": "Southern", + "cardinality": 8, + "dataType": "DOUBLE", + "searchable": true, + "sortable": true, + "hasRange": true + }, + { + "name": "southC", + "label": "SouthC", + "cardinality": 9, + "dataType": "DOUBLE", + "searchable": true, + "sortable": true, + "hasRange": true + }, + { + "name": "west", + "label": "West", + "cardinality": 10, + "dataType": "DOUBLE", + "searchable": true, + "sortable": true, + "hasRange": true + }, + { + "name": "total", + "label": "Total", + "cardinality": 11, + "dataType": "DOUBLE", + "searchable": true, + "sortable": true, + "hasRange": true + }, + { + "name": "DSTFlag", + "label": "DST Flag", + "cardinality": 12, + "dataType": "BOOLEAN", + "searchable": true, + "sortable": true, + "hasRange": false + } + ], + "nullability_policy": "Reject nulls; observed fields do not establish nullability.", + "source_url": "https://apiexplorer.ercot.com/developer/apis/pubapi-apim-api?export=true&api-version=2022-04-01-preview", + "query_source_sha256": "b978bf35fbf9dcb8edca14025d4b5d3108582885a6c1b92a9bf445e69b08a7f8", + "response_source": { + "source_url": "https://api.ercot.com/api/public-reports/np6-345-cd/act_sys_load_by_wzn?size=2&page=1&sort=operatingDay&dir=desc&operatingDayFrom=2026-09-04&operatingDayTo=2026-09-04", + "retrieved_at": "2026-09-05 21:14:36.433487+00:00", + "sha256": "e9b68e868a08ba0e04becd610fbbd781421f87545864d26b05f1ac234403cf13", + "byte_count": 2262, + "status": 200 + } +} diff --git a/tools/probe_public.py b/tools/probe_public.py index b7413633..e685d597 100644 --- a/tools/probe_public.py +++ b/tools/probe_public.py @@ -13,7 +13,15 @@ from pathlib import Path import tinyercot -from tinyercot.public import Credentials, PublicClient, WebClient, sample_dam_archive +from tinyercot.public import ( + RT_PRICES, + SYSTEM_LOAD, + Credentials, + PublicClient, + ReportsClient, + WebClient, + sample_dam_archive, +) def main() -> None: @@ -24,6 +32,11 @@ def main() -> None: """ parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--live", action="store_true") + parser.add_argument( + "--reports-only", + action="store_true", + help="Check new RT/load contracts with six bounded data requests", + ) parser.add_argument("--credentials-file", type=Path, required=True) parser.add_argument("--output", type=Path, required=True) args = parser.parse_args() @@ -76,6 +89,68 @@ def record(kind, receipt, **metadata): ) del values day = datetime.date(2026, 9, 4) + if args.reports_only: + with ReportsClient(credentials) as client: + for endpoint, date_field, selection in ( + (RT_PRICES, "deliveryDate", {"settlementPoint": "HB_HOUSTON"}), + (SYSTEM_LOAD, "operatingDay", {}), + ): + for period in ("current", "historical"): + filters = dict(selection) + if period == "current": + filters.update( + {date_field + "From": day, date_field + "To": day} + ) + page = client.page( + endpoint, + filters=filters, + size=2 if period == "current" else 1, + sort=date_field, + direction="desc" if period == "current" else "asc", + ) + assert len(page.rows) == (2 if period == "current" else 1) + if period == "current": + assert all( + getattr(row, date_field) == day for row in page.rows + ) + record( + "typed-reports-" + period, + page.receipt, + endpoint=endpoint.path, + rows=len(page.rows), + first_observed_date=str(getattr(page.rows[0], date_field)), + ) + pages = list( + client.pages( + RT_PRICES, + filters={ + "settlementPoint": "HB_HOUSTON", + "deliveryDateFrom": day, + "deliveryDateTo": day, + "deliveryHourFrom": 1, + "deliveryHourTo": 1, + "deliveryIntervalFrom": 1, + "deliveryIntervalTo": 2, + }, + size=1, + sort="deliveryInterval", + max_pages=2, + ) + ) + assert len(pages) == 2 and sum(len(page.rows) for page in pages) == 2 + for page in pages: + record( + "typed-reports-complete-selection", + page.receipt, + endpoint=RT_PRICES.path, + rows=len(page.rows), + page=page.meta["currentPage"], + total_records=page.meta["totalRecords"], + ) + print( + "Installed RT/load current, historical, and complete two-page selection passed" + ) + return with PublicClient(credentials) as client: for page in client.price_pages( start=day, end=day, settlement_point="HB_HOUSTON", size=2, max_pages=2 diff --git a/uv.lock b/uv.lock index ba61458a..82a7ac3d 100644 --- a/uv.lock +++ b/uv.lock @@ -2,7 +2,8 @@ version = 1 revision = 3 requires-python = ">=3.11" resolution-markers = [ - "python_full_version >= '3.12'", + "python_full_version >= '3.15'", + "python_full_version >= '3.12' and python_full_version < '3.15'", "python_full_version < '3.12'", ] @@ -37,6 +38,70 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl", hash = "sha256:dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb", size = 113362, upload-time = "2025-11-28T23:36:57.897Z" }, ] +[[package]] +name = "ast-serialize" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/c0/5bb6885a9608d86ee5712c0d88bc405d3a49f3e44231576e130ea2f53d34/ast_serialize-0.9.0.tar.gz", hash = "sha256:79fe8be1c934aa572940d1811d8dbe4d1b6f22291e3f16755c9b062e9ac92fb7", size = 951293, upload-time = "2026-09-02T15:50:45.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/76/497f19d9bdb3899a1efd82e2957f455d0c6e0cb9ebbc254735acb1f74235/ast_serialize-0.9.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:ae1c46eb97865823f9843c4b80145e011874923e1a4a44b45738a5309d83e9f5", size = 889442, upload-time = "2026-09-02T15:49:21.144Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c5/9fb64b7106c5534739322c74be7b743c4f2e3b5fd05d5b8e677f05c54d5f/ast_serialize-0.9.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af082cb7e6c4fa3a428aa616c13d709a944076eba84a73184de15621cc1a915d", size = 1226721, upload-time = "2026-09-02T15:49:22.612Z" }, + { url = "https://files.pythonhosted.org/packages/27/67/b550fc81aa0133808410783c6d9a1b925e31610d226e836e21337850af55/ast_serialize-0.9.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7e9f2540741ad10657a209209f7e5cc6b530eb3ed145fd77258ab43542d96ad7", size = 1207369, upload-time = "2026-09-02T15:49:23.916Z" }, + { url = "https://files.pythonhosted.org/packages/5c/1e/ed9e66deb7da63e44d0c0fd3a8feef698882ed56ea521a29494d4616eb46/ast_serialize-0.9.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95485d5e8704af2ecc7f723757b88f992ae8122028d687ccf877cad2b4c3da4", size = 1273073, upload-time = "2026-09-02T15:49:25.336Z" }, + { url = "https://files.pythonhosted.org/packages/68/8f/cd337551d7a68c982425bbf91f183943d7ccc62394002c74807a7f0e60db/ast_serialize-0.9.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b559cffac5a71a698d9194e4295765ff2132a10fd1284860f02b30f12c1f729e", size = 1279045, upload-time = "2026-09-02T15:49:26.75Z" }, + { url = "https://files.pythonhosted.org/packages/40/c6/98dc41eb4122d5da83241e805739838ed59e1e1b9006cbed89ded635a17f/ast_serialize-0.9.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dad8a3f7106efcf252fc289c092ee0cee5c3512c0088bcf3fffa01458323092f", size = 1539300, upload-time = "2026-09-02T15:49:28.213Z" }, + { url = "https://files.pythonhosted.org/packages/9b/d2/d94cede4b3f2a4e329d8ca92218f0846cfcc9257be91c5bf1168671f4ab5/ast_serialize-0.9.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:24de2bc930b7e1ca86641136b9875a1e5f80f52b484deddc67893eeaf9077bd9", size = 1291957, upload-time = "2026-09-02T15:49:29.643Z" }, + { url = "https://files.pythonhosted.org/packages/8e/85/8ac18d754225cf13392786b23d4ba84273ceee562699362f22e61942ce64/ast_serialize-0.9.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ac7b4cdf89ca8318aae157d824017596784982851c8a89a621973f261574696", size = 1291779, upload-time = "2026-09-02T15:49:31.199Z" }, + { url = "https://files.pythonhosted.org/packages/62/ed/cc757fec9e96e29f19a6f818e05147e4f2949356258a3243412756f22a2e/ast_serialize-0.9.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:95dcb93f30258dcf09d9b6a302dac9323b70e9df8c18ee0bf4ea1fa7cc5f1875", size = 1299730, upload-time = "2026-09-02T15:49:32.774Z" }, + { url = "https://files.pythonhosted.org/packages/a7/8c/a575ae0ae954f21a187b4c1d8cec28d81a009693bd13a1388eec72d9b55a/ast_serialize-0.9.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1ed5824b4b2fa37ad93fa2db23d8243a3d315b3e2d7ca70f99b2727d8788af05", size = 1344671, upload-time = "2026-09-02T15:49:34.217Z" }, + { url = "https://files.pythonhosted.org/packages/80/41/0b2b15c0ae5f9a95f433016d1a59a3227eebbb378654d6354206c8ac8e8d/ast_serialize-0.9.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e3c69f2ce565c786bd3853ce42495e8a63610516f79651c7cb4f2cd0ddfaee52", size = 1448527, upload-time = "2026-09-02T15:49:35.68Z" }, + { url = "https://files.pythonhosted.org/packages/18/be/ee89cb6a5d3427946532f0611b514befdd69564803e9a9f9ce712f9d2654/ast_serialize-0.9.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f8c824d822a2ac54ec4228b88c0d170ab0024cf285eeee57b9d4f994003fb553", size = 1554045, upload-time = "2026-09-02T15:49:37.15Z" }, + { url = "https://files.pythonhosted.org/packages/e0/4d/eaada807f98a2f0d370fec4b46c84f0e551a62911e751a76ecb32bef4dde/ast_serialize-0.9.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:c55010ea0fafc6bc8231809d328bda781bfe41a01c43522be5eb3713fd855cda", size = 1547578, upload-time = "2026-09-02T15:49:38.791Z" }, + { url = "https://files.pythonhosted.org/packages/b9/0c/046c531f4af4e1bc3314077a84b3099f38b45e2d969faa24acedb3066d92/ast_serialize-0.9.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:322282ac5337e5e776bc416f2b5201e680fa4dacf92ea739bd35e46a28a66c41", size = 1671896, upload-time = "2026-09-02T15:49:40.273Z" }, + { url = "https://files.pythonhosted.org/packages/6b/80/8d32aa0cf4e3e566399b2079a4c47f8ad3c62155f6ee1fe63631b6d3fdde/ast_serialize-0.9.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:ded5ed06ec469407d6cd571ace7a7a25809cc388e4bac1dd35c7747469fc7fdf", size = 1472895, upload-time = "2026-09-02T15:49:41.814Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b9/8204c7e3d8abd4b0c7a56a8d5cce05fcacfd6a5d63ffbd812b8b94040d6d/ast_serialize-0.9.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3ee40752ddb4fb5c6a67161d13f3ce3df7987dcb9272260542a86b0be1519ae2", size = 1492731, upload-time = "2026-09-02T15:49:43.355Z" }, + { url = "https://files.pythonhosted.org/packages/f3/72/ff5c44c19409686798feeb1fbe209f2be78b4b64948d5aa2ddeec8901591/ast_serialize-0.9.0-cp314-cp314t-win32.whl", hash = "sha256:d9c635eacfc02b91da6796d3b5ff9086e511b8a29b19f9b3f4f978b8d170f838", size = 1112847, upload-time = "2026-09-02T15:49:45.181Z" }, + { url = "https://files.pythonhosted.org/packages/20/75/fa5be1a94d189adafadf9c5f07fffd66af7a8061c4cff92f75285ef79d10/ast_serialize-0.9.0-cp314-cp314t-win_amd64.whl", hash = "sha256:62a96e327e2a178d6c295b10422e95c992a9286f0ec1b2bc7cd5b4873252a38c", size = 1146846, upload-time = "2026-09-02T15:49:46.63Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ef/b2bfb331b6e379d435543f6d3be0b590e7a2f441d31ccc17d75f2d2d7cb8/ast_serialize-0.9.0-cp314-cp314t-win_arm64.whl", hash = "sha256:41da4332492222d56345d5e436eed4fbec76caadee959f6ffa3cd2fc1bd51895", size = 1119605, upload-time = "2026-09-02T15:49:48.14Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f9/a4af1bf8b35927814c09d90c3965dbfaa75c489ba34372bffafbc2209f40/ast_serialize-0.9.0-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:383f56e3ae925f154632458f01b4bfcde3dd382f3ec04f5c7f6d72f76524ff48", size = 1226344, upload-time = "2026-09-02T15:49:49.79Z" }, + { url = "https://files.pythonhosted.org/packages/1d/6d/d3a95823a803c21f5c9df595a0bb93aada22e7aa22bf875fe00d89422d7f/ast_serialize-0.9.0-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:b9ef3d4173907bd19aa8f1683be9f06e7862e6cdf2ca6bca3633305c0df32063", size = 1207384, upload-time = "2026-09-02T15:49:51.22Z" }, + { url = "https://files.pythonhosted.org/packages/f0/97/6e7f46c8455b738609c29d1b7655307a168c4b40ce4c7a2c678c8ed9cf2e/ast_serialize-0.9.0-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9482672ca8ec09f85cd050a053fb88c30c882c8e20ce7a140d8defe19c0ef2eb", size = 1273139, upload-time = "2026-09-02T15:49:52.679Z" }, + { url = "https://files.pythonhosted.org/packages/f0/6e/25b70733f061766865cb04d913dc5332037c595796b871d52ab5b569abb8/ast_serialize-0.9.0-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6a568d1d489f0669a31aed90ca4845aa7f08e1b8cd5d05e1905dbdc3ae9b2b0", size = 1278242, upload-time = "2026-09-02T15:49:54.236Z" }, + { url = "https://files.pythonhosted.org/packages/9a/f0/b7820399d9c5a0b7f07c239b6da93d2e21a1b3785137fa00e16528e414b3/ast_serialize-0.9.0-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a4dd005d4095a13eb312dc712c943c7730f262b000a87df963925328a38ffdb", size = 1541009, upload-time = "2026-09-02T15:49:56.149Z" }, + { url = "https://files.pythonhosted.org/packages/08/56/5146f1d2a77516e697f6f42825df79137e43560675cb4605c467775f8b4a/ast_serialize-0.9.0-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:207ac73afa1f4654840593853c130eac2591dd942434176eea33f730afb3359b", size = 1290898, upload-time = "2026-09-02T15:49:57.502Z" }, + { url = "https://files.pythonhosted.org/packages/69/c4/87cd16228796d703de795a369b90b0f57f0f017f90f55c4c5876e3513a03/ast_serialize-0.9.0-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ae01129e2cc5d57a3c434d8a990019de039350310a2e1dd3c9f61311964cf25", size = 1291742, upload-time = "2026-09-02T15:49:58.982Z" }, + { url = "https://files.pythonhosted.org/packages/cc/eb/13465c297268c5170b2bb746d75f37a8fad44a94a89b593071affc1071d0/ast_serialize-0.9.0-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:4c522377f383670abfe21c94edc3032cb3bd34d8fcacd280fa9556907d4edd4b", size = 1300180, upload-time = "2026-09-02T15:50:00.454Z" }, + { url = "https://files.pythonhosted.org/packages/0a/a7/10b84c4274b2507b0ed9cc1654058ad64bcedb6ac574753d6a461bc6e204/ast_serialize-0.9.0-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4cd886f6e900f5e13f758cb8ef359652e690e4f3f9c257a7269e095940534167", size = 1345857, upload-time = "2026-09-02T15:50:01.875Z" }, + { url = "https://files.pythonhosted.org/packages/c7/dc/2702182c9773a15de9aabfaf66da7cb87548a56a6bac24f0c9176a4a13c3/ast_serialize-0.9.0-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:f3f0f3359cf0f22bf096b07021ffe6bf0ec88ac8a2cf7ce5f4701af973112faa", size = 1448544, upload-time = "2026-09-02T15:50:03.496Z" }, + { url = "https://files.pythonhosted.org/packages/59/b5/eeef2124c9563b9861707ef4db91f153f3bb37b3e0cca9543bb88a4e9e53/ast_serialize-0.9.0-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:6c428444656cffbd32c1e76626c6eec5237b58c8fcb0b5d3df75941cd50c4f3c", size = 1551572, upload-time = "2026-09-02T15:50:04.982Z" }, + { url = "https://files.pythonhosted.org/packages/cc/8c/81d18349f1dffdcfeb80671bd737e342c86348e183692d9d0d8f573d1385/ast_serialize-0.9.0-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:54f0babed5e2a4eb86a0716ac612aff33f933e7572e5bc067adcdbe672a26321", size = 1548118, upload-time = "2026-09-02T15:50:06.522Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1f/339131b60d1b0df13d9f3470cfac70f858b5649188ea01b2e7b39caeb720/ast_serialize-0.9.0-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:1b779fdaee34d19900a5ba5fd6bd4cefe225650081f9de28de256eacee5113c2", size = 1674707, upload-time = "2026-09-02T15:50:07.919Z" }, + { url = "https://files.pythonhosted.org/packages/18/0a/ca77596fa229d88f96eca180d45dbe8efa11306f8b2b4f4ee301b3fe465f/ast_serialize-0.9.0-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:4db7f524eaa857fbe650cac33b9cedb5ccda14393d40f640b75dfb06aa13c98c", size = 1473618, upload-time = "2026-09-02T15:50:09.312Z" }, + { url = "https://files.pythonhosted.org/packages/88/5c/6aebeb54dd226b480014ff4488e150aa23b1de3204e2bf3f87de27e6542a/ast_serialize-0.9.0-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:d2a37795a90809da6094825e7063118b4cf723b8701b134973b4566ed8b9ea09", size = 1492025, upload-time = "2026-09-02T15:50:10.755Z" }, + { url = "https://files.pythonhosted.org/packages/75/e6/c355d470a230f778311c28b80f6d934a497d094139f07230433eea18651b/ast_serialize-0.9.0-cp315-abi3.abi3t-win32.whl", hash = "sha256:4411d1cba9eeecb301365343a7e96813b4a44fcdb20181557867ff7e751804cf", size = 1113010, upload-time = "2026-09-02T15:50:12.337Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0d/66609ace58564727b68731293cc986c2ea1d5e6ef40e96571e7fb515f0af/ast_serialize-0.9.0-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:b5c3724faf780e25def89c369eb6340a15dff06ac348160c61781e2373a4cd10", size = 1146404, upload-time = "2026-09-02T15:50:13.761Z" }, + { url = "https://files.pythonhosted.org/packages/b1/fd/da28e1c85f05fb9976f247d2a3aefce68866cb2939abcbdbddd9a5e3b835/ast_serialize-0.9.0-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:1de0933a4c1d104d77d6e75f053f5e628b54cf8f9fea809b8250cb04cda07bd3", size = 1118328, upload-time = "2026-09-02T15:50:15.214Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8b/487a158a99e4564244e000ed18475255dfea53fd34a84d8ca73633710500/ast_serialize-0.9.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:5fc57f17fb4ce49b4eeccfcd2670e4a55659bd740bb8e8aedbe511ccab8b5f03", size = 889484, upload-time = "2026-09-02T15:50:16.643Z" }, + { url = "https://files.pythonhosted.org/packages/92/e4/175b0a64d6c96bc1b96598c6474ce8d1ef34e0b774bcf7183f4ce696fb10/ast_serialize-0.9.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:dac690f99538d9df0d23ce0299e946add2744b007a36b480a292fe361c82553d", size = 1232635, upload-time = "2026-09-02T15:50:18.133Z" }, + { url = "https://files.pythonhosted.org/packages/28/0c/d51d8463aca43aaa833fdf1f25134d6cc1b483764896decca61306ad1f6e/ast_serialize-0.9.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:2223ead73b5a5399d39610cf9c4164ad0b2bf2025226626b87ae15226d93d3f7", size = 1219313, upload-time = "2026-09-02T15:50:19.497Z" }, + { url = "https://files.pythonhosted.org/packages/ef/19/c88bdc64f86095a9d6ab325ae422b2a5e1395cd63cd8aa539003d4d4ae1d/ast_serialize-0.9.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b7c5f5838408fb000d76abd14e886836412b7ec7eccd028dbb5ed5819780008", size = 1279981, upload-time = "2026-09-02T15:50:20.811Z" }, + { url = "https://files.pythonhosted.org/packages/86/58/a492075826df1753896dc8e8f6ababae4016d8883b670ee3a1c34788b154/ast_serialize-0.9.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1e05701fde79affa1cc53e391867f9da3eb03fa8501f87354292796b0f8398fd", size = 1286319, upload-time = "2026-09-02T15:50:22.203Z" }, + { url = "https://files.pythonhosted.org/packages/ae/79/3f6754eaa42fd2a6c36aac066890870cd44cbe0e25f75a67b1b99a2f4d82/ast_serialize-0.9.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d013c36eb2f2ac0cb7d4d0e79918a92ab00fbce8f1542fe47f34a46e06168f82", size = 1551547, upload-time = "2026-09-02T15:50:23.528Z" }, + { url = "https://files.pythonhosted.org/packages/b1/05/8cfb7caadfaf28febaa6b61d31d778262f87f9366eda4dd9bd07ac940b75/ast_serialize-0.9.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:19cde5c2110f7b90ab1210a599178524f6c9f34862b20ba2b9aa7832c67bb35d", size = 1302468, upload-time = "2026-09-02T15:50:24.99Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e2/750a0b136bb02ff8e4a17d65a3a78cd478ee50724704df8215797a226ba3/ast_serialize-0.9.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1514f4a39704e2e815f9fc675fc13f19f694f212086b520110840782cf3c5295", size = 1300563, upload-time = "2026-09-02T15:50:26.354Z" }, + { url = "https://files.pythonhosted.org/packages/ab/17/4c0aa852ff1e4f2d6723e8ce827136c1e1febf2845d7941ccc45426778de/ast_serialize-0.9.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:30651ccdec6d23c49ee4711b1a1096d8dbd3be38eecf2f09fdd98a608ce7ac24", size = 1308999, upload-time = "2026-09-02T15:50:27.901Z" }, + { url = "https://files.pythonhosted.org/packages/4d/1b/6e73d0a29aedb0db30cc68f2557acaac06cd24c9783ccb90f84f89e4ce87/ast_serialize-0.9.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d9a46caf5e3f2cd266e8638b2f4ea8bf54cf376f015f8418397b4633fdb38e9b", size = 1358191, upload-time = "2026-09-02T15:50:29.237Z" }, + { url = "https://files.pythonhosted.org/packages/dc/38/2cf5d552de99e0e9804a16fea73e54d0a7382498adddf57c0f6dc09cbc70/ast_serialize-0.9.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:aed6e413c6c22a23c33c47a01dd2adce01d7a7ed408748e896903f47d0a1aa47", size = 1458944, upload-time = "2026-09-02T15:50:30.77Z" }, + { url = "https://files.pythonhosted.org/packages/4b/0b/5ef87adf955b6a027f616eb7b55f55a154c35ba600e9dd2d06ad2d30e5c2/ast_serialize-0.9.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:871fb7c5b049897ee137b67efad7fe4545ad270f7eccd970da877833f8e63aa7", size = 1563421, upload-time = "2026-09-02T15:50:32.188Z" }, + { url = "https://files.pythonhosted.org/packages/81/dd/9ced05a17feeb0f83e84010d80f5a1b7b7aa19e75f0376f4d3780803654c/ast_serialize-0.9.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:bb378efb5537b43f38660e2e6d6e138a40885cf191d43443bb3ff7ff47e9cd9b", size = 1558536, upload-time = "2026-09-02T15:50:33.861Z" }, + { url = "https://files.pythonhosted.org/packages/5f/8b/8ad486e44fc7081a2471055befc433dddc2e51c3a88dff141b3026f64602/ast_serialize-0.9.0-cp39-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:b373beff65b01fcffca5aaad3269ae629f3a998b09efdd3635e48039008a5dec", size = 1682749, upload-time = "2026-09-02T15:50:35.257Z" }, + { url = "https://files.pythonhosted.org/packages/dd/4c/7c282aba9cfb0b92d79fac45c04e4557d9a7f08d872e5a43577a50867e30/ast_serialize-0.9.0-cp39-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:25c8d517c45cf2b1820fc2af6ac593783654818f79d05646d25d624360678a4e", size = 1482441, upload-time = "2026-09-02T15:50:37.319Z" }, + { url = "https://files.pythonhosted.org/packages/a4/3a/e45914e8cad81b660915f3784d255460a6384183b76bfc2089fdd79ec7df/ast_serialize-0.9.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1675dc46578298ae00936164a160997801a6ca2385913150d8d16df634296cf3", size = 1499042, upload-time = "2026-09-02T15:50:38.76Z" }, + { url = "https://files.pythonhosted.org/packages/99/09/6988921dec19c810beef53539fec2e90ae551cd93853b3303a99fe45f772/ast_serialize-0.9.0-cp39-abi3-win32.whl", hash = "sha256:20fce3885eeff05a3d6afefa845c8168016e3ea1f6fc9cdc84c8db28b863a550", size = 1116391, upload-time = "2026-09-02T15:50:40.229Z" }, + { url = "https://files.pythonhosted.org/packages/fd/eb/839598a22a1f9af56d39e188451cad93dbcb0ce6539a45ac18fb8bf123fa/ast_serialize-0.9.0-cp39-abi3-win_amd64.whl", hash = "sha256:161914666a21d48b681982146ac0fa4086ef099d91c637cf595387f5f06aa099", size = 1156055, upload-time = "2026-09-02T15:50:42.05Z" }, + { url = "https://files.pythonhosted.org/packages/0d/45/c7cd8d36d3b506bbd02db5066fae3340284781168f0d08dac25deef5f69d/ast_serialize-0.9.0-cp39-abi3-win_arm64.whl", hash = "sha256:74473258a5c55855d5306c864a5c799fbff03a0f0ea1197346b2b5cc5b4ea48a", size = 1128237, upload-time = "2026-09-02T15:50:43.496Z" }, +] + [[package]] name = "asttokens" version = "3.0.1" @@ -213,6 +278,122 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" }, ] +[[package]] +name = "librt" +version = "0.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/36/9b/356320fbae2ac8467e21c5e73e1389c80468e4998c62cc7d3536cc51b614/librt-0.15.0.tar.gz", hash = "sha256:4e66cbe84437497d951b799d3e1551291b6fb3d643820a7014b3655d57a59162", size = 214338, upload-time = "2026-08-07T10:49:42.663Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/52/06790ced2ac7117f890c21bda43c39c958ec82aa665c0718e821d33ff939/librt-0.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:823b92cf3c18ecd08afc70c42473888b41b6e8ef5046f3b82c05c154a2fa3d22", size = 148039, upload-time = "2026-08-07T10:46:41.165Z" }, + { url = "https://files.pythonhosted.org/packages/e7/1d/8e150b7fc449a1f33c8a760965cc1f43b14fc1577d9d0b50ab2701420e74/librt-0.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c70bc1b602cf59917e8f0c7a2cbc8bcc6fbc14d5486136b00707a79619121d63", size = 153067, upload-time = "2026-08-07T10:46:42.418Z" }, + { url = "https://files.pythonhosted.org/packages/51/87/a162bc5a66a35599dc619ecb215145f4de7d68e886b479b6d12593139f7c/librt-0.15.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:814ff83a25b5fce8b9c80c4dd803153fb5c5599fc74db9e022466938368957ef", size = 493087, upload-time = "2026-08-07T10:46:43.657Z" }, + { url = "https://files.pythonhosted.org/packages/e5/3a/aeea1fc620cf48060d3065b37614edbf97043c099d0f50782bc8ca61d897/librt-0.15.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:57f5eeb6ad4c180de583b1038e61fe5fbd9796bb69a8a1c1a0c7ddbec4c8c60f", size = 485608, upload-time = "2026-08-07T10:46:45.038Z" }, + { url = "https://files.pythonhosted.org/packages/52/ff/fe571ad416f0856fd0d5578ffc2e6dc531891e586e36b647bcf50569cab8/librt-0.15.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82909c8f7eb9952656b65d3147afde4cf8e6d5a991eebc86418b5e65843b0ab8", size = 498723, upload-time = "2026-08-07T10:46:46.35Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e1/7a65eb5dedb1f00aebd948cdd8e17add48bf066cab3514e9daf84ab45a6c/librt-0.15.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f779070399f991400fc451719e0ea388eb7de313388bada2c127a35de05f798a", size = 516002, upload-time = "2026-08-07T10:46:47.599Z" }, + { url = "https://files.pythonhosted.org/packages/5f/45/59832b0ebfbd08c2742e6ece372ceb53f18bf1faef5d33c8daf3abebf749/librt-0.15.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bac89069bc496ebdf4f79ebb57bbd10d0b214c8454225deb672d91002bd17e18", size = 508607, upload-time = "2026-08-07T10:46:48.873Z" }, + { url = "https://files.pythonhosted.org/packages/ea/0d/37fa73f3b43ebd8259f91ae9102a15e5a54e65d581e48dea72df3e81d7a4/librt-0.15.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e0d00c708fb2f5822b152429b1ac80a58dbbbc3f6c232c4d13a3f7fcf2ea5b4c", size = 530422, upload-time = "2026-08-07T10:46:50.45Z" }, + { url = "https://files.pythonhosted.org/packages/26/02/e046c6fe7a5881ac34623242192f484426ba8a75595fd18f22c53a3f530f/librt-0.15.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6c6624fe268625869485553dd7cc1daf30d22558215bb2a4ff16f67a9801a31a", size = 534303, upload-time = "2026-08-07T10:46:51.693Z" }, + { url = "https://files.pythonhosted.org/packages/95/32/d5e6d861ab0366f3edf74f887ab0c9eb9f535aaf01d32b80b4f734daa179/librt-0.15.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f56b397858a23dacf35ede366ed2212fdc03a6a57a1ad36468ad6e9dc5fac091", size = 536084, upload-time = "2026-08-07T10:46:52.951Z" }, + { url = "https://files.pythonhosted.org/packages/2a/de/d69d725513fe53fc90c6d7a1f86e4428939bad2fb905b17fe4c18d413dde/librt-0.15.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:4388184646efe2054911c5b00a1077d6d1ee86a95b7e8ba96dc7850a809f3f40", size = 514307, upload-time = "2026-08-07T10:46:54.194Z" }, + { url = "https://files.pythonhosted.org/packages/36/93/f8aded0d6682b4f25820fa86e0690f87f01df9fd7bd09ddb04d9167ad021/librt-0.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:97335f59082f9fe2ce6c2a9cc6433a0114bbb6cd4d5c09dd76c95c68b9f9a8b0", size = 557686, upload-time = "2026-08-07T10:46:55.443Z" }, + { url = "https://files.pythonhosted.org/packages/74/09/ffeb6bdeb6cd862b4272fddc8ad05f938dd25d020ed517e631813917d80a/librt-0.15.0-cp311-cp311-win32.whl", hash = "sha256:83380ffde38062a2e9bb55d83e74474f6614665528b98a6928720fc006dfffbb", size = 104917, upload-time = "2026-08-07T10:46:56.605Z" }, + { url = "https://files.pythonhosted.org/packages/96/28/7e2313a3ffbf0b4de7ba3da58a09e488507b4bd1ea2b5e69378354a23415/librt-0.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:f75720477ee05d509a310e856cacc8d909adc182f7b91193c207bcc26d7ee6db", size = 125886, upload-time = "2026-08-07T10:46:57.729Z" }, + { url = "https://files.pythonhosted.org/packages/39/9e/04b8c3cde014ef255ee785730425268354543acc38902093a40afa0dc164/librt-0.15.0-cp311-cp311-win_arm64.whl", hash = "sha256:256237037a3ab001ae8d9803b2d43562a4c3aa38739843694349e4d5ebb0fd56", size = 111885, upload-time = "2026-08-07T10:46:58.787Z" }, + { url = "https://files.pythonhosted.org/packages/ba/39/99c25030e782bdfb7a21be8c05254806a2e4bbb05c8d50c2a2130acbfa05/librt-0.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e87bc679f86a99aa3b26e3c78eeb821a247c9a28eae48eaafcc32c3bf4c3bb9e", size = 151021, upload-time = "2026-08-07T10:47:00.057Z" }, + { url = "https://files.pythonhosted.org/packages/14/43/f4b1bd1b2888798a1409808889a25ea1ba49eaabce7d681ed27734c2df9d/librt-0.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:71599e011ac880e8e45d46047d714871894c7d4ab6f25626f8d4f89da21f368d", size = 155267, upload-time = "2026-08-07T10:47:01.311Z" }, + { url = "https://files.pythonhosted.org/packages/0c/db/3ad9c965c72f1e1d6beeec44ec10a54e17be8ae042fbb4baade16cbadced/librt-0.15.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c802434092b769b1d613ed2e13fac15fbfce1934a74bd10283b03c0fae231cd1", size = 503136, upload-time = "2026-08-07T10:47:02.45Z" }, + { url = "https://files.pythonhosted.org/packages/4b/07/5888a6d76acd62ebce66c61b74d94e9370b9c32929f111e487bb6546f8ed/librt-0.15.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5500eeae393a184d14e1f35645962c27129d20c81afa4069e6ef826ebc2b3aaa", size = 496670, upload-time = "2026-08-07T10:47:03.675Z" }, + { url = "https://files.pythonhosted.org/packages/29/39/ab57cc2f5b276156da02bb7f5a8921bada1cb1993ffec99acf811c602c23/librt-0.15.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6ecfc32dfb46fb7b565bcd6abf9412acf978775a998273d22888a6d7953730dd", size = 513688, upload-time = "2026-08-07T10:47:04.981Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b9/bdbb0b648b5c2befb031f4c6f3b1dd857415e8fb492a25a3c764a6681e6c/librt-0.15.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cc46cfd15022e35084355478c9ac809d90b1152222706ac9a7655ec21df6fa", size = 531904, upload-time = "2026-08-07T10:47:06.211Z" }, + { url = "https://files.pythonhosted.org/packages/93/26/473c2e4b6c104e9e58e27ce95fc8005c8bd4fc36cae4f254371125a92db8/librt-0.15.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d5f51401d102c885b9ca509e62c79b1dbff286e1b9b047fde6f763780789356d", size = 524427, upload-time = "2026-08-07T10:47:07.592Z" }, + { url = "https://files.pythonhosted.org/packages/26/60/03b3abb82b41714671b907bf6989b228e31e6a8af52dec82b5b0728dc250/librt-0.15.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cc30523e3f1a23fb7511cc659834a0d01a1042bb9de359bc1c131cc4ec6c9656", size = 543155, upload-time = "2026-08-07T10:47:08.866Z" }, + { url = "https://files.pythonhosted.org/packages/f2/0e/9bb1f0a4affbd0a1888f4f79dc03ed2a299d9a2c26c59ab2a97dcbf11903/librt-0.15.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:59fe030d8ae4a57e3fb7756bf35a858de74e04066fc8555c53d0af979132af81", size = 546890, upload-time = "2026-08-07T10:47:10.327Z" }, + { url = "https://files.pythonhosted.org/packages/dc/84/6937a280d461f7de6e031ffb02edc2b7c3c90d49d630565ce8ff27cbc5f2/librt-0.15.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5a6526a2a956bbb1e4ae3568c82e650fc99119c66bb011ea60715744955a2b4d", size = 555163, upload-time = "2026-08-07T10:47:11.798Z" }, + { url = "https://files.pythonhosted.org/packages/bc/95/2a2853c1ee014bf102116e7f897a04beeaeb2461b45b79af98bdfb95f1ef/librt-0.15.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:85ea21ec6730194d67156b0e0b5430ccb1d61f8b8b907e39b37f9812b74a13f0", size = 535812, upload-time = "2026-08-07T10:47:13.279Z" }, + { url = "https://files.pythonhosted.org/packages/c9/4c/cf9601c1b4c5f09280acd5d83abdb2e68527a2be8257136eb42304218622/librt-0.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1e47b8ba865d7ede071a91a7163073bbaeb72541f1ef8a07d512c45c7b5007f2", size = 573688, upload-time = "2026-08-07T10:47:14.727Z" }, + { url = "https://files.pythonhosted.org/packages/47/6d/9ac7cbec46189a7625af4b5acbd25f10d827f4141b2002181848c8418923/librt-0.15.0-cp312-cp312-win32.whl", hash = "sha256:a5207ec414d1c4a2a7231b2086970dc036f94293cdf338190984958a013a42f1", size = 106138, upload-time = "2026-08-07T10:47:15.973Z" }, + { url = "https://files.pythonhosted.org/packages/38/d0/2ae99c83be86ce23f925ac1aeeedc777e97f427c4a8d190c70d0a16e9a87/librt-0.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:73b30cfa976659b3917c8f6153bdb0591c6a9ec6583599fd24a689b690622022", size = 126974, upload-time = "2026-08-07T10:47:17.049Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ef/dd24f9635c730b86b87587967dda7516b1845e8b17684603d31607fed598/librt-0.15.0-cp312-cp312-win_arm64.whl", hash = "sha256:a54cf9e0ef47b96af580849db5471142200568ce1e02cbf416addab551369570", size = 112292, upload-time = "2026-08-07T10:47:18.222Z" }, + { url = "https://files.pythonhosted.org/packages/e7/42/467b53a601b406ccd7b97c1fd54b59cb34f9185ad5ce7e9d5c3c4e8961c8/librt-0.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:db13ca398005abcbe538deda87b686d9bd08b7001cf40c4c06b444960ae10a26", size = 151029, upload-time = "2026-08-07T10:47:19.312Z" }, + { url = "https://files.pythonhosted.org/packages/3e/e6/36c2299b7a94b84fdd01220d8a777a71be5be0925bb0dbdf71c0a06a34d9/librt-0.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa1f1995789dca3698bc550aaceb09a51bd5df0a057ff84ff15296cd1975b801", size = 155194, upload-time = "2026-08-07T10:47:20.398Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b6/ed5071f9325845e670bd36012757419767fbf56af77ed483077b9e4db541/librt-0.15.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55456ea87d8df21808446d03817be2f65e20391c1c615d9187440dff28cd08dc", size = 502568, upload-time = "2026-08-07T10:47:21.652Z" }, + { url = "https://files.pythonhosted.org/packages/7f/81/6450c67c3615d87704bcbc21323fafc69c799b06a044c447529f725d4b01/librt-0.15.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5a86a5a08c2235316bdb359d5dbb6ce0abfca7fac06363103e2c5af571d92f95", size = 496153, upload-time = "2026-08-07T10:47:22.925Z" }, + { url = "https://files.pythonhosted.org/packages/e1/d6/5f52b722bc75076954b3bfd49be15ea362df4d580c6fb315d0f617100d30/librt-0.15.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e56b6a368529bed262da40ce13f8fef590db0479819cca84f16a1f01ac356d0b", size = 513336, upload-time = "2026-08-07T10:47:24.213Z" }, + { url = "https://files.pythonhosted.org/packages/8d/e2/c08fd1d36ce63ea5a12b85c5d37f4550b5f86a692167e41e5a74222607ae/librt-0.15.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:234d8d394721fa0d786af15ebf1f3fb7f3ed82fd1cd0cde45c2f247b5d4281d2", size = 531661, upload-time = "2026-08-07T10:47:25.507Z" }, + { url = "https://files.pythonhosted.org/packages/3f/d8/d9482fcbeb177b9eb87bb3899eeb3b42be690313c652f9e146b1d0681fb2/librt-0.15.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d8363d7accb0286ac3a0e633f396e93800dafb8150494505daf9515bbda591f3", size = 524487, upload-time = "2026-08-07T10:47:26.79Z" }, + { url = "https://files.pythonhosted.org/packages/10/cc/075171517b41f861753034fbb151b42cfc83bcc853849f24f5e66fd60ccf/librt-0.15.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0f0ee3644d951f31055ad07d77d92520e84505dd7a432cc4cd501dd70ee06785", size = 543201, upload-time = "2026-08-07T10:47:27.999Z" }, + { url = "https://files.pythonhosted.org/packages/b0/03/42c2330f37eeb475b6affeedd06518f60035f323af3a839335e3fc9fef2d/librt-0.15.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2cfd1a81a648806e6a7717be4cc4d1bb392fa229752bf8444ba365e381e984d6", size = 546467, upload-time = "2026-08-07T10:47:29.396Z" }, + { url = "https://files.pythonhosted.org/packages/57/1e/1ad4c5638f7e64d8560328bd25c54b409a661bdb6ff254b38ff90744288d/librt-0.15.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a6cd22c9da0d866558e46a041f1cc0c2bbb26b61b137b2347fa834c332e1d101", size = 555139, upload-time = "2026-08-07T10:47:30.815Z" }, + { url = "https://files.pythonhosted.org/packages/49/41/39fa7d15db1204cd1cbe6514680fbdc243adf754a0885061308f43afc013/librt-0.15.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6d5225ef8801e4ea5e482fa9b5dfb891dd9ef6f6d870f1f25d449ca2c70ac218", size = 536050, upload-time = "2026-08-07T10:47:32.222Z" }, + { url = "https://files.pythonhosted.org/packages/1e/88/c6dcf0dd8e26dc0c9a499a2abab8646c86dcaf9ecea9524cb46d3686331a/librt-0.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d28a05796b99f749bf8794f17ba9ba1612d0076b802e9cfc62c554634e9ce3b", size = 573700, upload-time = "2026-08-07T10:47:33.527Z" }, + { url = "https://files.pythonhosted.org/packages/1b/9b/ab54c71a7918a7c34fa5327fb61390a77446a07a146fbfb1165250a61035/librt-0.15.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:2067ff438048cead9d223ca5675bae2a25e520a7c3e6c1498bf9c6892d22caab", size = 82194, upload-time = "2026-08-07T10:47:34.835Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b2/4f9a243bb892395f3becb80789ade13771701091f9f07ab8230247953ba8/librt-0.15.0-cp313-cp313-win32.whl", hash = "sha256:1cd3b721f24c206398b9e26da3c3a9c011e6e89d06f318ba8ebefc30f1003890", size = 106231, upload-time = "2026-08-07T10:47:36.251Z" }, + { url = "https://files.pythonhosted.org/packages/bf/af/64aff4885a40b93132382f2c314647d722574605416504379184ef3045ea/librt-0.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:f395a4a9a03ac062dbe9a9f82e0c720502e590a38feee6a757bc82e9c63afbd8", size = 126996, upload-time = "2026-08-07T10:47:37.453Z" }, + { url = "https://files.pythonhosted.org/packages/27/83/335bccf6c7cb9028cb0b54aead27d9ece3f01f83bc6baa2abace5da655c1/librt-0.15.0-cp313-cp313-win_arm64.whl", hash = "sha256:0a15cb554761247d84a3ec0cbdf4078d70725384f0e4662c0fa3b26266eb60ad", size = 112188, upload-time = "2026-08-07T10:47:38.729Z" }, + { url = "https://files.pythonhosted.org/packages/a8/93/949053fb462eecc4a9a5ee770a81f4b40be7b79538b245545d4aebc6b58b/librt-0.15.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f5de7feedc56337a088eb15cd9fafa9938367362221d8cc62c642b7f94821993", size = 149833, upload-time = "2026-08-07T10:47:39.86Z" }, + { url = "https://files.pythonhosted.org/packages/61/ca/8281aa6cd560a3420e4497729f6b704b53be3eeaaef82d5aeadddaf7441f/librt-0.15.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c0eb900c0e91f4aebe680845242e614f1864edfd44106380d0752ac29522bf8", size = 154088, upload-time = "2026-08-07T10:47:41.065Z" }, + { url = "https://files.pythonhosted.org/packages/dd/02/1a1662dceaba6a086360891448d5ce9a7d3555976cae59a31a39d744b9c7/librt-0.15.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e8c9a650a188e38bac005048cbe6342e81407782944d01934540ab75e417df21", size = 494215, upload-time = "2026-08-07T10:47:42.388Z" }, + { url = "https://files.pythonhosted.org/packages/69/84/99211619dc656370a3740c33d2b0b6d5a3fb1e73689314f6ed477a397dc4/librt-0.15.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:92bfed8deec93df30286b9fe9e3b1dd17329cc076a192b4ee5ec223841d54953", size = 491173, upload-time = "2026-08-07T10:47:43.683Z" }, + { url = "https://files.pythonhosted.org/packages/d4/aa/5448d0b05f4579b635d3899176817ebf561af0e57bacd425b5b1887264c1/librt-0.15.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ec4b19788f835711a2072f9dbe6b03b3bf32ed1f0fb30cf399bdd59d9f0c33fa", size = 505512, upload-time = "2026-08-07T10:47:45.314Z" }, + { url = "https://files.pythonhosted.org/packages/95/82/01940e40b83c43a546c4a3c896cf34ca272a9690899d55914e4827b3dcce/librt-0.15.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4c7bacb70930f3d0a56f4ecf1be474a1f0d941b01dd73b756f3c256d42cb879", size = 523073, upload-time = "2026-08-07T10:47:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/88/fa/759c0030f3ee371439eb26de34fc745807caf0abb878af7af4b8b7c3dd3d/librt-0.15.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3e79f05e4a08b4d880342673312bbc895b56df7765605796f15902eb5367d3ae", size = 515080, upload-time = "2026-08-07T10:47:48.319Z" }, + { url = "https://files.pythonhosted.org/packages/0b/27/894e072228fcb159703c655da69f8cd10dbed489c36e3df7dd032a2483be/librt-0.15.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a417149c0cba4d50b61e992e5a15e69eaf96746609b461cc4ed168aeef6b79dd", size = 534164, upload-time = "2026-08-07T10:47:49.875Z" }, + { url = "https://files.pythonhosted.org/packages/98/a3/0078e91c1f36f8815db17827de15650b9a3fe56c55fbf998c854b34e40d3/librt-0.15.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:da7a94d6a3411f579d72aa3e3bc5fbca7ed4549f3dbd7e5de3aa567333374285", size = 540616, upload-time = "2026-08-07T10:47:51.408Z" }, + { url = "https://files.pythonhosted.org/packages/86/33/81a29b796dd52a45e9ef7974c7732926e8f10f15b8d2be505665979f896d/librt-0.15.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:856f743ae607f2c1380eccb566c0038a9fb3eabf0fc2be2704d76d9f73557239", size = 545890, upload-time = "2026-08-07T10:47:52.818Z" }, + { url = "https://files.pythonhosted.org/packages/05/82/8be1baa1350e5d30cfd70ae79d0a6f4dc5862ef47f7bb2808aabc9bb86e5/librt-0.15.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:779a6e7c894737e5983e7790a9c78c4000c30e23c9aada08081bdbea53b0fa60", size = 523287, upload-time = "2026-08-07T10:47:54.165Z" }, + { url = "https://files.pythonhosted.org/packages/c6/4f/d1be6a01a35c20ef734e0e44113f87d4af756a9354a89dcfbe3b4f8af5e1/librt-0.15.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:96bb17dbe8bab3c0954fbebfc69ed395599de75b6bbc35e3270a878e15d4dd65", size = 565868, upload-time = "2026-08-07T10:47:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/67/88/649cfa33f5825927b160610f670bdab012a64d627eddb94fa795ea4292fd/librt-0.15.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:7220697efaa6e5348fc3d18ee7f8563d4bfecd9872b37ffb915bfc1d08840622", size = 81619, upload-time = "2026-08-07T10:47:56.886Z" }, + { url = "https://files.pythonhosted.org/packages/22/31/8e88a8d5e48fc8d1a817787fb6811dfff6499acd6c8683dd83934aa6ede0/librt-0.15.0-cp314-cp314-win32.whl", hash = "sha256:f54598964d357b1c5ab77cf5d92f21e598fe0e23cdbe9618480807f81b4eba15", size = 100138, upload-time = "2026-08-07T10:47:58.093Z" }, + { url = "https://files.pythonhosted.org/packages/80/92/20fd6c4b6a1b1a564b076d55cd3d427d8428217d7638dc25a654cc4791d4/librt-0.15.0-cp314-cp314-win_amd64.whl", hash = "sha256:3ff5893a2c23d886aa9ce786de5ac6ddc74aeeaf90743682b74d920e117d2e28", size = 121258, upload-time = "2026-08-07T10:47:59.564Z" }, + { url = "https://files.pythonhosted.org/packages/fc/28/6af430b44d9ebb897b865a3c363b6dcace51357be2347cc0f8f869656a86/librt-0.15.0-cp314-cp314-win_arm64.whl", hash = "sha256:3722a099730704c9a3d70c879fc0f51daec25fe5f1555672d97bc595abeafb95", size = 106467, upload-time = "2026-08-07T10:48:01.097Z" }, + { url = "https://files.pythonhosted.org/packages/7e/aa/b42bb798942ced219f6d63b27e07f91237887a8d0bd0921666db79a13790/librt-0.15.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:38c0c7d4b6fc06c3324b3f9162c8391bfc4fd9dde53afe1033ce7edb48d5a714", size = 159523, upload-time = "2026-08-07T10:48:02.442Z" }, + { url = "https://files.pythonhosted.org/packages/75/03/1b53cd4ef904e73b1d828a5f90143bf94a2967d7cfff0b9ccf93e12aa9b4/librt-0.15.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8b2fdd7ead3c995c37940a790690660d0ca006c302db26cc51933f6766866fc3", size = 161638, upload-time = "2026-08-07T10:48:03.725Z" }, + { url = "https://files.pythonhosted.org/packages/ac/c4/9f9c9fba097d49e9e694c2b4dc331df31884645ecbc58a93b4b5fc69d2c5/librt-0.15.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2fde98cf1fc4bac144ce23c2c4c017b924ba714509ea9334977b0b27050c837d", size = 701795, upload-time = "2026-08-07T10:48:05.135Z" }, + { url = "https://files.pythonhosted.org/packages/4c/05/0966840bda0380c8ae167b9043c6230202941cc90ea29c48e096964c765e/librt-0.15.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:e3b461183c5fa7681b48560f91515f53a953122fb30c71e07abc67d7ddf58c38", size = 682147, upload-time = "2026-08-07T10:48:06.555Z" }, + { url = "https://files.pythonhosted.org/packages/18/af/1c47ca573c30ea47d195aec26133af522fea1104afaace028d7b32247ea8/librt-0.15.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4bbcc257e3babea20a91715c361b24554ec4e8f51aa578568afc230799fe1a19", size = 696397, upload-time = "2026-08-07T10:48:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/2e/0f/1aed6223d4f9f9d1171a8596ff100ea4c3f7699fea7a4ba657c3e60daa6c/librt-0.15.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b845b8d48088fad0cadc84be4b8fda63203be7e9237b71015b3925443c1f35ab", size = 722542, upload-time = "2026-08-07T10:48:09.569Z" }, + { url = "https://files.pythonhosted.org/packages/c6/22/9e3a929aea456c97d69e6ef3884efea56d4807f97399471cc946baebd8af/librt-0.15.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b30e600e8f337b9bd7f39b86d9fdfedc73cc46e3d0f745931a23a234220bb7e2", size = 729709, upload-time = "2026-08-07T10:48:11.129Z" }, + { url = "https://files.pythonhosted.org/packages/e9/1b/c327ef6018e3a9ca0b8e7c5eddeeb331ba8f9b76c24e126d37d0f6d62faf/librt-0.15.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:64b0c8c35aa4c4ed79896359f3e0b285cbe4e610042106500da4811c322cc108", size = 752891, upload-time = "2026-08-07T10:48:12.558Z" }, + { url = "https://files.pythonhosted.org/packages/d7/d1/d5f1ea02c56930087009e39db9b70660a663e76c730b27b925d786718457/librt-0.15.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0da0d94cb802f32a0524653e7201f2cef72d5f700a5407678f5290483d4fcd08", size = 745301, upload-time = "2026-08-07T10:48:14.55Z" }, + { url = "https://files.pythonhosted.org/packages/d9/3c/5f7c585d15ebb2250c73e7c0ee4e9e47be72c65d520c07ddbcdc62037674/librt-0.15.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:4a6369168d371207339b1e50d4532b06a7121586141f82599505a3f315751d47", size = 747921, upload-time = "2026-08-07T10:48:16.453Z" }, + { url = "https://files.pythonhosted.org/packages/7f/52/1443a446486eba966bcbca1696b472e4f210320ec42f490a47f48fbf0fdc/librt-0.15.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c434e072557ade9cbc642d052c89d031efe47d5c9614523619d0d74a02378e81", size = 727561, upload-time = "2026-08-07T10:48:18.089Z" }, + { url = "https://files.pythonhosted.org/packages/79/91/2270a9380f11725cf83ce1925a5e32dd1dde2be9bba597f25c10a38644e7/librt-0.15.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7eec6a42018bc1d45763b1c162d3d2bf7c3b9a1b0ed30d3e91dcba390efefcc", size = 774417, upload-time = "2026-08-07T10:48:19.611Z" }, + { url = "https://files.pythonhosted.org/packages/9e/3b/f4b1548d4f5b99186737fe27aec238e9823e8d5d23bf4df007c030689dc5/librt-0.15.0-cp314-cp314t-win32.whl", hash = "sha256:6912fa5e635d74529ac7cdb1bdf6ca3af4453da8d1edbe0110ee1cb4ad407ebf", size = 104381, upload-time = "2026-08-07T10:48:21.048Z" }, + { url = "https://files.pythonhosted.org/packages/80/b6/134afad262def1de04c0843c376d02135f1168af43f22e09a52bd8394727/librt-0.15.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8e11699ed745931c395acd3621b07062e0f840efa6935aad87a64ed0995f0915", size = 127034, upload-time = "2026-08-07T10:48:22.561Z" }, + { url = "https://files.pythonhosted.org/packages/99/5f/1b6846b20572bd699c9e9ec321a5f781845bee477df2aa2a43b28bc40119/librt-0.15.0-cp314-cp314t-win_arm64.whl", hash = "sha256:5d2a91724463bfed4f573cd7a9fdc856d2e230d0c0e5a61416a93481dccd8605", size = 110827, upload-time = "2026-08-07T10:48:23.804Z" }, + { url = "https://files.pythonhosted.org/packages/c6/44/4de9f4ddadb009a55c7758eb5736d62534a7daaf27bd71bc50e64b606b06/librt-0.15.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:8443e38dcfcfdbcf5add5118c623efd788d65ac2e25756d6251a54a06a4d0aca", size = 149843, upload-time = "2026-08-07T10:48:25.148Z" }, + { url = "https://files.pythonhosted.org/packages/1f/eb/5d9ab71e30119c44094e0275f38b47dd327aea0f843a080396677029d508/librt-0.15.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:6d15a29033c57490cfe2069097c6fc4049e4e65ffbb749be7dc453b7c4c68965", size = 154510, upload-time = "2026-08-07T10:48:26.485Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9c/8505d1b8f5e8c19587bd03f7429993b3e9ce5c06819d856bfb11d919374c/librt-0.15.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d2c05c729b589e734c09578bf5964be48a911765484840d017bbc84f49d4c4ad", size = 497543, upload-time = "2026-08-07T10:48:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/1d/9a/3a8390775cb095765aded027ac9c63e7c8ea74e731498607544c6505de0e/librt-0.15.0-cp315-cp315-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:fa60887537e1d0cd2d9982269d33a709bf54b195cd2b9364fc0a758022af5bd9", size = 480452, upload-time = "2026-08-07T10:48:29.531Z" }, + { url = "https://files.pythonhosted.org/packages/e7/40/258a4a7117ee915d66de5cd9b8ade65a440993161107ce3a686f1859955c/librt-0.15.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d8bc24219b24c0af375718942ab75e3544b2763085f40f965be4326734ae8328", size = 507768, upload-time = "2026-08-07T10:48:31.007Z" }, + { url = "https://files.pythonhosted.org/packages/6b/c6/2f4dd296c97a0b85b98894519b279408ec9dd602d4f692b1ea0e25dee670/librt-0.15.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:86a21a7bd3fe3a419512ef424cc1c020f6771d0b29cfddff36d1635a855e63f0", size = 525122, upload-time = "2026-08-07T10:48:32.7Z" }, + { url = "https://files.pythonhosted.org/packages/49/dd/29eab42be13b2bf0ea8cb227135a45d44693e30a7e8b92871981ff56b82b/librt-0.15.0-cp315-cp315-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dbab647e88d90b3167b91efe7091e248653688ed4337e4f90907a722c7361bb9", size = 520371, upload-time = "2026-08-07T10:48:34.294Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/4bad71adeca8fe208b775c2a35417fa5a2584c8f4791daaf89a89450fea1/librt-0.15.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:d8edcf6f550e918dca779c069b9e156385c60b406f99fc7641f32c52f7193659", size = 537258, upload-time = "2026-08-07T10:48:35.88Z" }, + { url = "https://files.pythonhosted.org/packages/4c/63/59dba6143fdcc7240c54458b629f3250000a61b8945890fc9efd451b19c5/librt-0.15.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:8b62076030baa2d8b1501a46bf0e19c27a489aa90671c55665bff7887f7660b0", size = 527432, upload-time = "2026-08-07T10:48:37.466Z" }, + { url = "https://files.pythonhosted.org/packages/ec/21/21a24c6a2327d8362580efebe77286bf47b0f4062ec5ea41766e609d3c7d/librt-0.15.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:d00d20d1818e82a07a0ee0aa89a98b17ed7916b92441090b683719cb20a59b6d", size = 548108, upload-time = "2026-08-07T10:48:39.384Z" }, + { url = "https://files.pythonhosted.org/packages/5a/6d/fc68c89a7971418b41f9a873623ff935cb864097544c6a2f8ce491c8ef5d/librt-0.15.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:4e6ee93fc3cf848dcbf0cce2eca73d8e7dcd0cc2b6df3a529d57750b30a4c55c", size = 529681, upload-time = "2026-08-07T10:48:41.392Z" }, + { url = "https://files.pythonhosted.org/packages/65/7e/c2d98766124400d722063a630b0fde38a9fc768705d37eecca15c47dc192/librt-0.15.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:32896a0af72508ea979e0acb4e4c04cbeeae04938167950d535c83c45597167d", size = 567736, upload-time = "2026-08-07T10:48:43.124Z" }, + { url = "https://files.pythonhosted.org/packages/55/6c/f8c34a95e3a515c6e1c192b89511e7253c89a7760c6b500d57ffdb8d2dc8/librt-0.15.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:ec3ba415afaf951f6951b1dd16d3c8e4f540065fc382d7e70b823a79567ca374", size = 81673, upload-time = "2026-08-07T10:48:44.645Z" }, + { url = "https://files.pythonhosted.org/packages/c9/9e/e23fa8e78679ec45728188650b39e8ff476c83b691c96f749217df3b1b7c/librt-0.15.0-cp315-cp315-win32.whl", hash = "sha256:d2813ba2503764f0450680c533d13df7cff9b49df1411062eded5f67db4195b9", size = 100081, upload-time = "2026-08-07T10:48:46.171Z" }, + { url = "https://files.pythonhosted.org/packages/e1/dc/3eb4c5e297343f0620a55532cd7c8d764d3001fa2159212dadf480464827/librt-0.15.0-cp315-cp315-win_amd64.whl", hash = "sha256:b87d67e33afaf265262f2a66db578284b88ee2e6fcd224579cb5c15518677ad8", size = 121228, upload-time = "2026-08-07T10:48:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/97/70/43abce19f04e49762f8ec834c8fafee13cc40fd6b94a72a24e534febfcd0/librt-0.15.0-cp315-cp315-win_arm64.whl", hash = "sha256:713bd7df21170b982e729e46870f31d6b437bd1a9b4648cffb529bd3c2ec5c4b", size = 106487, upload-time = "2026-08-07T10:48:49.095Z" }, + { url = "https://files.pythonhosted.org/packages/de/15/83f2deddb9368b8951ec8c9477269b5b9b8bd9bbf15e57402d0f38817dca/librt-0.15.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:3de789c82752730f94782a5ee518baf9c05edf85733aeaf73bb6e518755cdf54", size = 159448, upload-time = "2026-08-07T10:48:50.649Z" }, + { url = "https://files.pythonhosted.org/packages/06/bf/043097353f9b3c73b583d07f6b8e552795463f4bfc8caf85e42eee50c26a/librt-0.15.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:e0b5deec9a8664eb722c797241970fd4aa1894d25fda36a1ddac0f7407606bd6", size = 161686, upload-time = "2026-08-07T10:48:52.174Z" }, + { url = "https://files.pythonhosted.org/packages/f4/2a/8ae77f9719d42ce71cd708560a3557b38ac3c17a0383e57f87084de45bbe/librt-0.15.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5563302a8359bc2295bb7084d1a8ed1519df96afb30eb2aa4e0bff7b54228988", size = 710668, upload-time = "2026-08-07T10:48:53.782Z" }, + { url = "https://files.pythonhosted.org/packages/61/34/c0436ea134deb9a0d6da80a396a2739a81cb31e0418f7227239e23140898/librt-0.15.0-cp315-cp315t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:22d6263b9d39d7bbb286fa791945646e3218f1be2d693e36fb630f1d0e59cd13", size = 679396, upload-time = "2026-08-07T10:48:55.645Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9f/001e0d99aa9250d5cd5715a9081291a20656083459f9019cda15255329e1/librt-0.15.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:39ffd14646190c454f0d86e0d256b33f00a87a26ab410e619773b841d0e41416", size = 704313, upload-time = "2026-08-07T10:48:57.46Z" }, + { url = "https://files.pythonhosted.org/packages/2d/53/b34fa9d0ff00f136f4d58ebb4c411ff634baed1eb412bb602a2bc8dcafcb/librt-0.15.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c47318cd3a61401452de11282242937e3e057c4fd3dbaf601e269d0928a06c0a", size = 729847, upload-time = "2026-08-07T10:48:59.231Z" }, + { url = "https://files.pythonhosted.org/packages/86/ac/fa4d7a424665040e95baf480a6d523446057684b6758624c85338e8a23b2/librt-0.15.0-cp315-cp315t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a56a1d4f859a82ca5b99fc4b82c9b027b15e3c455c5cd99e7d0719f27bb20b6c", size = 742736, upload-time = "2026-08-07T10:49:01.151Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f1/e17a9bb5de6fb8c3186ed1a7d68d21618b027ac2d3633e03d3b6109c67ae/librt-0.15.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:077471b3182db4e17c36ae91555f36a4d2c00080b267f749bcad34a478a9a302", size = 763454, upload-time = "2026-08-07T10:49:03.039Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ec/ecd02cd30935b931b9cdbfed6ab5a099c51b280b4e7baa274da80978ed27/librt-0.15.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:411ca4d1b905b860ceba7570dd6717a71dedaddcc4b0f77ece710aa41ee11f8d", size = 743296, upload-time = "2026-08-07T10:49:04.941Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b5/b3c2b8353ce820a4854f78d19321344242f89fa71c975b71132ba9bf242a/librt-0.15.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:1256589e0b0adb31751d685a68bce29d73407ddf4ef05d4188f49d5dcf9566d9", size = 756217, upload-time = "2026-08-07T10:49:06.825Z" }, + { url = "https://files.pythonhosted.org/packages/3c/52/6cc22542ba59146b05cca2a656f9ff8bb67e38e63d12c3b0cc183d837bf1/librt-0.15.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:f42b74a53e5f26a0ba0007411a7455b66c67ce4022a39cc1f56fc4efd65bcbab", size = 741934, upload-time = "2026-08-07T10:49:08.839Z" }, + { url = "https://files.pythonhosted.org/packages/40/32/a04b72b1aa86e3be23b2ecff8c1aad2dcc955bd3956d6d26e7e34267e57a/librt-0.15.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:291bf73caf78b9e88d6fae9bfd693207ff7d832e2fdbe2cf8e746bc13f5f892b", size = 783763, upload-time = "2026-08-07T10:49:10.661Z" }, + { url = "https://files.pythonhosted.org/packages/6c/f0/89eb11dffbe9279ff37144dec786927314502ae0b114f1449dc78c458aab/librt-0.15.0-cp315-cp315t-win32.whl", hash = "sha256:c16d15ee371643ab48dc8248a3e680ebbeca573a13af2c3dd0c985b142d77162", size = 104313, upload-time = "2026-08-07T10:49:12.305Z" }, + { url = "https://files.pythonhosted.org/packages/6d/4a/1f1978c200f563beda63c36adff2d65bbecb81e365e8e69e572f5f70fbc6/librt-0.15.0-cp315-cp315t-win_amd64.whl", hash = "sha256:dbd605739f228912dc49027cb764456b9757750bdc2b6b7773164db7096c6fd1", size = 126889, upload-time = "2026-08-07T10:49:13.881Z" }, + { url = "https://files.pythonhosted.org/packages/38/a6/800800bfed7b1fb10fc3f3d557785c3854e80d3f7a9800d784b176a1fc2d/librt-0.15.0-cp315-cp315t-win_arm64.whl", hash = "sha256:84d244b00604d17df3fc7736c327892d6bba66181254aa4087be807b6c342bdc", size = 110700, upload-time = "2026-08-07T10:49:15.499Z" }, +] + [[package]] name = "matplotlib-inline" version = "0.2.1" @@ -225,83 +406,152 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl", hash = "sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76", size = 9516, upload-time = "2025-10-23T09:00:20.675Z" }, ] +[[package]] +name = "mypy" +version = "2.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ast-serialize" }, + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/6a/878cc1097d4035f82bd516658d0c528d2a9955bc7b363afcbd0b07fea11b/mypy-2.3.1.tar.gz", hash = "sha256:47c1b1207258513a9d93495f69c8be9de73916186f0e52703e8c461b7a623419", size = 3992554, upload-time = "2026-08-15T03:03:38.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/be/c624d4241484f37dc62839e177ab607a9b8b3e96f0866544ca99e8e41d51/mypy-2.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94f04929f1c44c35fb0061e912087edaf504acede963a4a7d00680bd089d8531", size = 13936739, upload-time = "2026-08-15T03:03:26.475Z" }, + { url = "https://files.pythonhosted.org/packages/53/84/e3cf72f90dce5960871c82551c8fba6da05fc1018f79be41c047bd126bdd/mypy-2.3.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5d716048611e85ca9eefb2e1baa5d73ede389b5820ded260ea27c757d667af8", size = 14166460, upload-time = "2026-08-15T03:01:50.565Z" }, + { url = "https://files.pythonhosted.org/packages/4a/ff/6b97d58aa0f79a5ab9b472db1f6d6df1b11a51d74d0c08ab3760d3a613ba/mypy-2.3.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b091a455111214cb5c9d54a57b9618e9a49f9fe2a42e4e1ac86e9d104ed96ce8", size = 15100476, upload-time = "2026-08-15T03:03:12.079Z" }, + { url = "https://files.pythonhosted.org/packages/da/f0/cbb4b7d2ae3ac635f6b4f2d9b04070b8a92edf50da599d3b39e5ed109001/mypy-2.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:df12e20c9efd614738c71b390007ecd0181125afc4ccafca04d78a1d2eed2c01", size = 15347826, upload-time = "2026-08-15T03:03:02.856Z" }, + { url = "https://files.pythonhosted.org/packages/5f/10/91dcdc6f8d43fc08e6a06ab1f9732f3abaaf835ac1b2e67b9dff56910855/mypy-2.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:52eaf3a155f35cf80b40220288c861eb45f14a2340c1f6cbfbdb0feff32879d1", size = 11142615, upload-time = "2026-08-15T03:03:36.316Z" }, + { url = "https://files.pythonhosted.org/packages/3d/8a/28d54535bf4b9aa43b2d8918c2ef660378b9f66b23d78dcee052744ae622/mypy-2.3.1-cp311-cp311-win_arm64.whl", hash = "sha256:9b4eacbee8a69836c06eff6d0dd4e134a07c2b047755b30c08625fe214f322c6", size = 10141145, upload-time = "2026-08-15T03:03:07.406Z" }, + { url = "https://files.pythonhosted.org/packages/85/da/d6effc4f808a842d91edc22535dc9e799d2ff6e91449168b7f47a0771f54/mypy-2.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a32bbbb940af990d3be0b8af321c7b6815bb1b3b48142fe7459b9cc5f58959ff", size = 14047547, upload-time = "2026-08-15T03:02:57.707Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e6/478229701dab76f26485fc8ff5d6f241f393da22447400bbc56f6946aebe/mypy-2.3.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff715e45b2231a8e85de1d163d1b42791e4d7aab8f5145f85fee1b710b735aff", size = 14216515, upload-time = "2026-08-15T03:01:26.496Z" }, + { url = "https://files.pythonhosted.org/packages/8d/fe/7c42327a3b21e84681f691982cbfe43f334a3685f3b683b72c376476c4fa/mypy-2.3.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:858fc57d3d91fa728e33e7ad71def60fc6272694607b306cd3292db53ae39080", size = 15307789, upload-time = "2026-08-15T03:03:31.62Z" }, + { url = "https://files.pythonhosted.org/packages/59/f4/7e597edbe01b5a56fa958ce541302dcaabfed979966f1dffedbea0ea0fc2/mypy-2.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:851833db876e7b650f93719c74b7879a08e338979c96054fdfc3bfd90a486355", size = 15548831, upload-time = "2026-08-15T03:03:15.55Z" }, + { url = "https://files.pythonhosted.org/packages/a3/52/cb31e084bc0314a1e384bdd677a4b80e55af04ccac077545e2238b9d320a/mypy-2.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:4c5095a327483591c94e0c8d3ef9e50d4ab1369b541eae007c1f23bc2a41f6bb", size = 11226359, upload-time = "2026-08-15T03:03:29.002Z" }, + { url = "https://files.pythonhosted.org/packages/7a/47/88fcf6217b43fa2da81a8c2611370af18141536a4f0294bbf98b457d456d/mypy-2.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:bbfe022634a2a195406bd469e888d2eaf193b02ba7e607391cd7640374aaae3b", size = 10214707, upload-time = "2026-08-15T03:02:48.807Z" }, + { url = "https://files.pythonhosted.org/packages/de/cf/862010ee800ca9c2bd0c4c0dacf0f092e5411824a09b8f97ad4be8fe250e/mypy-2.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:114dff494000f18bd10d5d95d84b8567b26da60279ecbe838131841df20e635d", size = 13964542, upload-time = "2026-08-15T03:02:21.43Z" }, + { url = "https://files.pythonhosted.org/packages/75/5a/3f3a2107b41e3e92e617e25daaee121413b91e9784bea733131ed4fecc5d/mypy-2.3.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8637731bb5eee3671eb2c3200827aa3564ed8a9309ecee4d1afe77e6d031bdb", size = 14168922, upload-time = "2026-08-15T03:03:00.351Z" }, + { url = "https://files.pythonhosted.org/packages/8b/41/04dc4fe7e63d7820fa4eff272e95157d30cbea921388f3ab3fe77794cd0b/mypy-2.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c80fbc405ed8020f5ff3802dc18cf060197bcdd3fbdd6a26ef2fd34dfdd5226", size = 15244791, upload-time = "2026-08-15T03:02:31.089Z" }, + { url = "https://files.pythonhosted.org/packages/96/fc/c3053b26b9054949285aa868cb6af8c10e7591541cacd79c5dcc06a1fcf9/mypy-2.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:84081f538ce27375045c02e3d7f81bd11d853400621ae245d87ce7b6c420ec74", size = 15501627, upload-time = "2026-08-15T03:03:34.128Z" }, + { url = "https://files.pythonhosted.org/packages/70/4e/d77daab008bbc4e5001374d7928f4a260d28f0e6747af444fc4763f7a310/mypy-2.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:e9144ac16fde007096f9563eb2041b4433c2d705c4218edeb79e7e9d01035ee6", size = 11243961, upload-time = "2026-08-15T03:02:11.952Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f8/7eb68c136e4abd30569fe31ef2bfcb7eceae9952cab80017c04cd09f5d0c/mypy-2.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:77ad9529e67dca28e511f5cd5671436584ce91f6d3bac159a353158187b986ac", size = 10213219, upload-time = "2026-08-15T03:02:26.361Z" }, + { url = "https://files.pythonhosted.org/packages/be/c4/42a49d44aeff804edf1b19acce0b49e8bd1a9c57dee9605dd8d980aa43d7/mypy-2.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:192abaedf75da1bc0b1cef104927e70ec49c1ef0031cc4825c7ee10a438ed24d", size = 13986778, upload-time = "2026-08-15T03:01:33.69Z" }, + { url = "https://files.pythonhosted.org/packages/45/13/9331fd2dfed7194d66c5304072894a8be3e51e9deda6863c1eceaa35a43d/mypy-2.3.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf678dffd16efcda2c15cbd30e9ecc0081388e29ea23687a88e686ed92638dc3", size = 14188467, upload-time = "2026-08-15T03:02:40.554Z" }, + { url = "https://files.pythonhosted.org/packages/78/f7/f4a34edab45667c5465855dc585a20e87978ffa8aee711445b7239d120c6/mypy-2.3.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e036f06b41630f4c8a1d48f9ac6aa26acc65f8be089973f5519da643318f03f", size = 15225538, upload-time = "2026-08-15T03:03:09.761Z" }, + { url = "https://files.pythonhosted.org/packages/40/05/534b3590757bd05794f73e07f6666c2a77b8597ffed795c94ce570096aa0/mypy-2.3.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:71af9c8a894e862b58e92abb08e53b05a384a1e5e5d6dc7cda59126211a53d82", size = 15480805, upload-time = "2026-08-15T03:01:41.134Z" }, + { url = "https://files.pythonhosted.org/packages/55/da/bdfba852e2562f599624af5bb7d29e36b0b4f526f2b8bac85efe0dd1803d/mypy-2.3.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:3c80cd23d85368bdd9f37d5231dfd97d35bcbf5bf41af96ef3a9b078ad1957f9", size = 7761712, upload-time = "2026-08-15T03:02:36.008Z" }, + { url = "https://files.pythonhosted.org/packages/98/31/60fc64a74cdba4f2a5d642d32317993e479163e1ac7d91b695e5d15e2264/mypy-2.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:4956f34d145e145562a0a0bf367f642bbc85c04ec2baf47ae015947c3169a85d", size = 11423968, upload-time = "2026-08-15T03:02:06.931Z" }, + { url = "https://files.pythonhosted.org/packages/a9/23/eb5950b24cd26ba3b78f87707a275568d633c77dae8e61c9661be6055ca6/mypy-2.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:cfb12e360242d23d91f5e978d94f58ea66acf5804c4fb6f2f794a20d4cb1b595", size = 10399323, upload-time = "2026-08-15T03:02:33.671Z" }, + { url = "https://files.pythonhosted.org/packages/82/c7/f80f4e46c0b9a00eb5f78a79d49dda8bdf56a5230f7257fb33e76be04da7/mypy-2.3.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5f1c50bb05b64e2026b52867e8d21106f01313c744a2c4ecc34c90d12e8d6e2", size = 15121308, upload-time = "2026-08-15T03:01:46.053Z" }, + { url = "https://files.pythonhosted.org/packages/5d/74/9b04f17c7074cc5188f02fb63a2ca1d43fedf479e84fe3091c39061a1d7f/mypy-2.3.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:667196b352f4cf304ded4c10f90cfc179263a1acfb3cdcfa984bdfd340d498bc", size = 15536590, upload-time = "2026-08-15T03:01:35.941Z" }, + { url = "https://files.pythonhosted.org/packages/26/04/c837ef6208e567774e2ed1f863f8ba6ec4817b1b6dd426315e5d559b6ec9/mypy-2.3.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9c53e395c12cad2c6d4b67d5da7c6057638a132d85c08b73646b18f802a0045", size = 16791074, upload-time = "2026-08-15T03:01:31.073Z" }, + { url = "https://files.pythonhosted.org/packages/37/68/48730230afa45192d5bd429a6a2ff24a6f8dedda90fdf2b221792b54518f/mypy-2.3.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:18162b128c3f9c703cd35f5537446900b0d21a2549aa7a95d21380d2ef643fb0", size = 17069183, upload-time = "2026-08-15T03:02:28.566Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ea/ca23fc9c20eeda09a15c9cbcf50015d0e73f409f6ead059e42aa69a608ff/mypy-2.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:30c0477d4aab7b7f39c8397dc877f2c96b9fe5588ec379f372c56eb63d599f63", size = 12154679, upload-time = "2026-08-15T03:02:04.809Z" }, + { url = "https://files.pythonhosted.org/packages/3b/67/8d982126034990869466f73b8db80dcb2234a7ac39b4dad093e047a79835/mypy-2.3.1-cp314-cp314t-win_arm64.whl", hash = "sha256:6941ab3619377bc3f32ca02876b07d27f216f5201604b664d3937ea0fdd23bb4", size = 10969159, upload-time = "2026-08-15T03:02:38.152Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f7/41e7f2d8117fbc7a7587286162ffe2f688984b69c46ed63cf5f2e4fc3bae/mypy-2.3.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:6f041a6de52c9217ca125e78ba0a335cb7fd98a1c0580978e49ab2b126f70b57", size = 13990694, upload-time = "2026-08-15T03:03:21.919Z" }, + { url = "https://files.pythonhosted.org/packages/06/85/8f665811a0c8f3bf6fa1d9acd665ec2d97a2bcc453ae68dcd92340941cd6/mypy-2.3.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5159ae60f5dbc3a498af5ba8365505808ac8031bc63f9e00304ad545d40bdd9b", size = 14203518, upload-time = "2026-08-15T03:01:48.455Z" }, + { url = "https://files.pythonhosted.org/packages/2d/82/91b866c8546b120bff83b73a439d90d2d63ef3aff113599e6b8e4d566848/mypy-2.3.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47a8a7a0a7f6f6e63995c0ac36fa0c07b127413fdc81f0439b7f3dccafd33561", size = 15220224, upload-time = "2026-08-15T03:01:23.577Z" }, + { url = "https://files.pythonhosted.org/packages/c8/78/c226c99208ee40de7c768369fa533f933afa003dfdc606ff021450724e91/mypy-2.3.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2329c0501293d4e1f33bc15d04d6304d65a1cdda967ee93a05c1e681a3923133", size = 15501512, upload-time = "2026-08-15T03:02:09.453Z" }, + { url = "https://files.pythonhosted.org/packages/a9/e7/7cfb3f106c393979f4cc37ad6c0586044d50401e3c35b0c003e4f3ba6bc9/mypy-2.3.1-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:bb26deed807bdb0457cf3e3f1cd7c4a1cf9d66864eaf1b4a61e06805d4c6b1f9", size = 7761913, upload-time = "2026-08-15T03:01:55.65Z" }, + { url = "https://files.pythonhosted.org/packages/99/3c/52affefa273b97939a1f474ae4a349c8718635c15b941112dfab4291b0c1/mypy-2.3.1-cp315-cp315-win_amd64.whl", hash = "sha256:375d7013876a8233b2d05be185bfa09f689696cd999ce8b1cfe6acac5c80e8a3", size = 11422533, upload-time = "2026-08-15T03:03:24.101Z" }, + { url = "https://files.pythonhosted.org/packages/2a/b7/75643e70c72a5b346d8a9b1543c967ea8824df2ee3fb7ccba652c272b7bb/mypy-2.3.1-cp315-cp315-win_arm64.whl", hash = "sha256:586b3612214cceabb3c0f588c97e7d1e535393f06a60e912e994f6b3ace97523", size = 10397931, upload-time = "2026-08-15T03:02:55.265Z" }, + { url = "https://files.pythonhosted.org/packages/10/ce/53be21f2d4adfcd26f63f1184a13ed797015ab463853f117e2e11e4d726f/mypy-2.3.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:ef0c6335cda9d807f8193d8ff6204a72bc909fa9882aacbca14f43cdb7188306", size = 15118669, upload-time = "2026-08-15T03:02:51.479Z" }, + { url = "https://files.pythonhosted.org/packages/62/43/20de757cd42989d291a17fad607742c4c74e875ce5cea00e5a5225020ac1/mypy-2.3.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e598c8c66401d26b150872154a286e6d484cf2789c3bb28a7556806298423021", size = 15545627, upload-time = "2026-08-15T03:03:05.132Z" }, + { url = "https://files.pythonhosted.org/packages/7e/fc/092bdf77ad280eaf501422f0f3b966012b528076cc13e41a774861c907d1/mypy-2.3.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eda22fd4efa9dcd39331d1dede9b5b8b8a7fd69af07592e778433da98610d29e", size = 16764157, upload-time = "2026-08-15T03:02:23.958Z" }, + { url = "https://files.pythonhosted.org/packages/94/5c/c94c4d62d909b07f552d0d9356d7acc943825558e602a64822ffa2231536/mypy-2.3.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:2a0ba2e57847849fb0d1fcdabb32786d223095ed8bc121dfe322bcdb3d9c46bc", size = 17073258, upload-time = "2026-08-15T03:02:14.573Z" }, + { url = "https://files.pythonhosted.org/packages/c0/f7/511a88b89e478053c02d22039bb8f3ce4183efe8fd7a4f0a5910a8bb0a32/mypy-2.3.1-cp315-cp315t-win_amd64.whl", hash = "sha256:3f7e865dd51f235f60a2dbcd8728a1c095f5ca28f095d48a725b84cd935735c4", size = 12135505, upload-time = "2026-08-15T03:02:16.714Z" }, + { url = "https://files.pythonhosted.org/packages/71/bf/02573b56964ecb0f7c644f915f53c325ae15c3faec521c5adf11599a32df/mypy-2.3.1-cp315-cp315t-win_arm64.whl", hash = "sha256:8ad80807dc3ab8ea978b1b2b6e4a657194ace1d4ef03e0e731aff1abd517da29", size = 10962647, upload-time = "2026-08-15T03:01:43.712Z" }, + { url = "https://files.pythonhosted.org/packages/8e/41/9675c7a1e78edecfba0b79e587a52594c56e189368261dc7b3a7fffb9527/mypy-2.3.1-py3-none-any.whl", hash = "sha256:6ed5c7e3419083268e5c9258bd1c1ef91af44a9e89374dbcaf37b775716e72eb", size = 2754338, upload-time = "2026-08-15T03:02:53.4Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + [[package]] name = "numpy" -version = "2.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a4/7a/6a3d14e205d292b738db449d0de649b373a59edb0d0b4493821d0a3e8718/numpy-2.4.0.tar.gz", hash = "sha256:6e504f7b16118198f138ef31ba24d985b124c2c469fe8467007cf30fd992f934", size = 20685720, upload-time = "2025-12-20T16:18:19.023Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/26/7e/7bae7cbcc2f8132271967aa03e03954fc1e48aa1f3bf32b29ca95fbef352/numpy-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:316b2f2584682318539f0bcaca5a496ce9ca78c88066579ebd11fd06f8e4741e", size = 16940166, upload-time = "2025-12-20T16:15:43.434Z" }, - { url = "https://files.pythonhosted.org/packages/0f/27/6c13f5b46776d6246ec884ac5817452672156a506d08a1f2abb39961930a/numpy-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2718c1de8504121714234b6f8241d0019450353276c88b9453c9c3d92e101db", size = 12641781, upload-time = "2025-12-20T16:15:45.701Z" }, - { url = "https://files.pythonhosted.org/packages/14/1c/83b4998d4860d15283241d9e5215f28b40ac31f497c04b12fa7f428ff370/numpy-2.4.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:21555da4ec4a0c942520ead42c3b0dc9477441e085c42b0fbdd6a084869a6f6b", size = 5470247, upload-time = "2025-12-20T16:15:47.943Z" }, - { url = "https://files.pythonhosted.org/packages/54/08/cbce72c835d937795571b0464b52069f869c9e78b0c076d416c5269d2718/numpy-2.4.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:413aa561266a4be2d06cd2b9665e89d9f54c543f418773076a76adcf2af08bc7", size = 6799807, upload-time = "2025-12-20T16:15:49.795Z" }, - { url = "https://files.pythonhosted.org/packages/ff/be/2e647961cd8c980591d75cdcd9e8f647d69fbe05e2a25613dc0a2ea5fb1a/numpy-2.4.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0feafc9e03128074689183031181fac0897ff169692d8492066e949041096548", size = 14701992, upload-time = "2025-12-20T16:15:51.615Z" }, - { url = "https://files.pythonhosted.org/packages/a2/fb/e1652fb8b6fd91ce6ed429143fe2e01ce714711e03e5b762615e7b36172c/numpy-2.4.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8fdfed3deaf1928fb7667d96e0567cdf58c2b370ea2ee7e586aa383ec2cb346", size = 16646871, upload-time = "2025-12-20T16:15:54.129Z" }, - { url = "https://files.pythonhosted.org/packages/62/23/d841207e63c4322842f7cd042ae981cffe715c73376dcad8235fb31debf1/numpy-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e06a922a469cae9a57100864caf4f8a97a1026513793969f8ba5b63137a35d25", size = 16487190, upload-time = "2025-12-20T16:15:56.147Z" }, - { url = "https://files.pythonhosted.org/packages/bc/a0/6a842c8421ebfdec0a230e65f61e0dabda6edbef443d999d79b87c273965/numpy-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:927ccf5cd17c48f801f4ed43a7e5673a2724bd2171460be3e3894e6e332ef83a", size = 18580762, upload-time = "2025-12-20T16:15:58.524Z" }, - { url = "https://files.pythonhosted.org/packages/0a/d1/c79e0046641186f2134dde05e6181825b911f8bdcef31b19ddd16e232847/numpy-2.4.0-cp311-cp311-win32.whl", hash = "sha256:882567b7ae57c1b1a0250208cc21a7976d8cbcc49d5a322e607e6f09c9e0bd53", size = 6233359, upload-time = "2025-12-20T16:16:00.938Z" }, - { url = "https://files.pythonhosted.org/packages/fc/f0/74965001d231f28184d6305b8cdc1b6fcd4bf23033f6cb039cfe76c9fca7/numpy-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:8b986403023c8f3bf8f487c2e6186afda156174d31c175f747d8934dfddf3479", size = 12601132, upload-time = "2025-12-20T16:16:02.484Z" }, - { url = "https://files.pythonhosted.org/packages/65/32/55408d0f46dfebce38017f5bd931affa7256ad6beac1a92a012e1fbc67a7/numpy-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:3f3096405acc48887458bbf9f6814d43785ac7ba2a57ea6442b581dedbc60ce6", size = 10573977, upload-time = "2025-12-20T16:16:04.77Z" }, - { url = "https://files.pythonhosted.org/packages/8b/ff/f6400ffec95de41c74b8e73df32e3fff1830633193a7b1e409be7fb1bb8c/numpy-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2a8b6bb8369abefb8bd1801b054ad50e02b3275c8614dc6e5b0373c305291037", size = 16653117, upload-time = "2025-12-20T16:16:06.709Z" }, - { url = "https://files.pythonhosted.org/packages/fd/28/6c23e97450035072e8d830a3c411bf1abd1f42c611ff9d29e3d8f55c6252/numpy-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2e284ca13d5a8367e43734148622caf0b261b275673823593e3e3634a6490f83", size = 12369711, upload-time = "2025-12-20T16:16:08.758Z" }, - { url = "https://files.pythonhosted.org/packages/bc/af/acbef97b630ab1bb45e6a7d01d1452e4251aa88ce680ac36e56c272120ec/numpy-2.4.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:49ff32b09f5aa0cd30a20c2b39db3e669c845589f2b7fc910365210887e39344", size = 5198355, upload-time = "2025-12-20T16:16:10.902Z" }, - { url = "https://files.pythonhosted.org/packages/c1/c8/4e0d436b66b826f2e53330adaa6311f5cac9871a5b5c31ad773b27f25a74/numpy-2.4.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:36cbfb13c152b1c7c184ddac43765db8ad672567e7bafff2cc755a09917ed2e6", size = 6545298, upload-time = "2025-12-20T16:16:12.607Z" }, - { url = "https://files.pythonhosted.org/packages/ef/27/e1f5d144ab54eac34875e79037011d511ac57b21b220063310cb96c80fbc/numpy-2.4.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:35ddc8f4914466e6fc954c76527aa91aa763682a4f6d73249ef20b418fe6effb", size = 14398387, upload-time = "2025-12-20T16:16:14.257Z" }, - { url = "https://files.pythonhosted.org/packages/67/64/4cb909dd5ab09a9a5d086eff9586e69e827b88a5585517386879474f4cf7/numpy-2.4.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc578891de1db95b2a35001b695451767b580bb45753717498213c5ff3c41d63", size = 16363091, upload-time = "2025-12-20T16:16:17.32Z" }, - { url = "https://files.pythonhosted.org/packages/9d/9c/8efe24577523ec6809261859737cf117b0eb6fdb655abdfdc81b2e468ce4/numpy-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:98e81648e0b36e325ab67e46b5400a7a6d4a22b8a7c8e8bbfe20e7db7906bf95", size = 16176394, upload-time = "2025-12-20T16:16:19.524Z" }, - { url = "https://files.pythonhosted.org/packages/61/f0/1687441ece7b47a62e45a1f82015352c240765c707928edd8aef875d5951/numpy-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d57b5046c120561ba8fa8e4030fbb8b822f3063910fa901ffadf16e2b7128ad6", size = 18287378, upload-time = "2025-12-20T16:16:22.866Z" }, - { url = "https://files.pythonhosted.org/packages/d3/6f/f868765d44e6fc466467ed810ba9d8d6db1add7d4a748abfa2a4c99a3194/numpy-2.4.0-cp312-cp312-win32.whl", hash = "sha256:92190db305a6f48734d3982f2c60fa30d6b5ee9bff10f2887b930d7b40119f4c", size = 5955432, upload-time = "2025-12-20T16:16:25.06Z" }, - { url = "https://files.pythonhosted.org/packages/d4/b5/94c1e79fcbab38d1ca15e13777477b2914dd2d559b410f96949d6637b085/numpy-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:680060061adb2d74ce352628cb798cfdec399068aa7f07ba9fb818b2b3305f98", size = 12306201, upload-time = "2025-12-20T16:16:26.979Z" }, - { url = "https://files.pythonhosted.org/packages/70/09/c39dadf0b13bb0768cd29d6a3aaff1fb7c6905ac40e9aaeca26b1c086e06/numpy-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:39699233bc72dd482da1415dcb06076e32f60eddc796a796c5fb6c5efce94667", size = 10308234, upload-time = "2025-12-20T16:16:29.417Z" }, - { url = "https://files.pythonhosted.org/packages/a7/0d/853fd96372eda07c824d24adf02e8bc92bb3731b43a9b2a39161c3667cc4/numpy-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a152d86a3ae00ba5f47b3acf3b827509fd0b6cb7d3259665e63dafbad22a75ea", size = 16649088, upload-time = "2025-12-20T16:16:31.421Z" }, - { url = "https://files.pythonhosted.org/packages/e3/37/cc636f1f2a9f585434e20a3e6e63422f70bfe4f7f6698e941db52ea1ac9a/numpy-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:39b19251dec4de8ff8496cd0806cbe27bf0684f765abb1f4809554de93785f2d", size = 12364065, upload-time = "2025-12-20T16:16:33.491Z" }, - { url = "https://files.pythonhosted.org/packages/ed/69/0b78f37ca3690969beee54103ce5f6021709134e8020767e93ba691a72f1/numpy-2.4.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:009bd0ea12d3c784b6639a8457537016ce5172109e585338e11334f6a7bb88ee", size = 5192640, upload-time = "2025-12-20T16:16:35.636Z" }, - { url = "https://files.pythonhosted.org/packages/1d/2a/08569f8252abf590294dbb09a430543ec8f8cc710383abfb3e75cc73aeda/numpy-2.4.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:5fe44e277225fd3dff6882d86d3d447205d43532c3627313d17e754fb3905a0e", size = 6541556, upload-time = "2025-12-20T16:16:37.276Z" }, - { url = "https://files.pythonhosted.org/packages/93/e9/a949885a4e177493d61519377952186b6cbfdf1d6002764c664ba28349b5/numpy-2.4.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f935c4493eda9069851058fa0d9e39dbf6286be690066509305e52912714dbb2", size = 14396562, upload-time = "2025-12-20T16:16:38.953Z" }, - { url = "https://files.pythonhosted.org/packages/99/98/9d4ad53b0e9ef901c2ef1d550d2136f5ac42d3fd2988390a6def32e23e48/numpy-2.4.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8cfa5f29a695cb7438965e6c3e8d06e0416060cf0d709c1b1c1653a939bf5c2a", size = 16351719, upload-time = "2025-12-20T16:16:41.503Z" }, - { url = "https://files.pythonhosted.org/packages/28/de/5f3711a38341d6e8dd619f6353251a0cdd07f3d6d101a8fd46f4ef87f895/numpy-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ba0cb30acd3ef11c94dc27fbfba68940652492bc107075e7ffe23057f9425681", size = 16176053, upload-time = "2025-12-20T16:16:44.552Z" }, - { url = "https://files.pythonhosted.org/packages/2a/5b/2a3753dc43916501b4183532e7ace862e13211042bceafa253afb5c71272/numpy-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:60e8c196cd82cbbd4f130b5290007e13e6de3eca79f0d4d38014769d96a7c475", size = 18277859, upload-time = "2025-12-20T16:16:47.174Z" }, - { url = "https://files.pythonhosted.org/packages/2c/c5/a18bcdd07a941db3076ef489d036ab16d2bfc2eae0cf27e5a26e29189434/numpy-2.4.0-cp313-cp313-win32.whl", hash = "sha256:5f48cb3e88fbc294dc90e215d86fbaf1c852c63dbdb6c3a3e63f45c4b57f7344", size = 5953849, upload-time = "2025-12-20T16:16:49.554Z" }, - { url = "https://files.pythonhosted.org/packages/4f/f1/719010ff8061da6e8a26e1980cf090412d4f5f8060b31f0c45d77dd67a01/numpy-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:a899699294f28f7be8992853c0c60741f16ff199205e2e6cdca155762cbaa59d", size = 12302840, upload-time = "2025-12-20T16:16:51.227Z" }, - { url = "https://files.pythonhosted.org/packages/f5/5a/b3d259083ed8b4d335270c76966cb6cf14a5d1b69e1a608994ac57a659e6/numpy-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:9198f447e1dc5647d07c9a6bbe2063cc0132728cc7175b39dbc796da5b54920d", size = 10308509, upload-time = "2025-12-20T16:16:53.313Z" }, - { url = "https://files.pythonhosted.org/packages/31/01/95edcffd1bb6c0633df4e808130545c4f07383ab629ac7e316fb44fff677/numpy-2.4.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74623f2ab5cc3f7c886add4f735d1031a1d2be4a4ae63c0546cfd74e7a31ddf6", size = 12491815, upload-time = "2025-12-20T16:16:55.496Z" }, - { url = "https://files.pythonhosted.org/packages/59/ea/5644b8baa92cc1c7163b4b4458c8679852733fa74ca49c942cfa82ded4e0/numpy-2.4.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:0804a8e4ab070d1d35496e65ffd3cf8114c136a2b81f61dfab0de4b218aacfd5", size = 5320321, upload-time = "2025-12-20T16:16:57.468Z" }, - { url = "https://files.pythonhosted.org/packages/26/4e/e10938106d70bc21319bd6a86ae726da37edc802ce35a3a71ecdf1fdfe7f/numpy-2.4.0-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:02a2038eb27f9443a8b266a66911e926566b5a6ffd1a689b588f7f35b81e7dc3", size = 6641635, upload-time = "2025-12-20T16:16:59.379Z" }, - { url = "https://files.pythonhosted.org/packages/b3/8d/a8828e3eaf5c0b4ab116924df82f24ce3416fa38d0674d8f708ddc6c8aac/numpy-2.4.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1889b3a3f47a7b5bee16bc25a2145bd7cb91897f815ce3499db64c7458b6d91d", size = 14456053, upload-time = "2025-12-20T16:17:01.768Z" }, - { url = "https://files.pythonhosted.org/packages/68/a1/17d97609d87d4520aa5ae2dcfb32305654550ac6a35effb946d303e594ce/numpy-2.4.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85eef4cb5625c47ee6425c58a3502555e10f45ee973da878ac8248ad58c136f3", size = 16401702, upload-time = "2025-12-20T16:17:04.235Z" }, - { url = "https://files.pythonhosted.org/packages/18/32/0f13c1b2d22bea1118356b8b963195446f3af124ed7a5adfa8fdecb1b6ca/numpy-2.4.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6dc8b7e2f4eb184b37655195f421836cfae6f58197b67e3ffc501f1333d993fa", size = 16242493, upload-time = "2025-12-20T16:17:06.856Z" }, - { url = "https://files.pythonhosted.org/packages/ae/23/48f21e3d309fbc137c068a1475358cbd3a901b3987dcfc97a029ab3068e2/numpy-2.4.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:44aba2f0cafd287871a495fb3163408b0bd25bbce135c6f621534a07f4f7875c", size = 18324222, upload-time = "2025-12-20T16:17:09.392Z" }, - { url = "https://files.pythonhosted.org/packages/ac/52/41f3d71296a3dcaa4f456aaa3c6fc8e745b43d0552b6bde56571bb4b4a0f/numpy-2.4.0-cp313-cp313t-win32.whl", hash = "sha256:20c115517513831860c573996e395707aa9fb691eb179200125c250e895fcd93", size = 6076216, upload-time = "2025-12-20T16:17:11.437Z" }, - { url = "https://files.pythonhosted.org/packages/35/ff/46fbfe60ab0710d2a2b16995f708750307d30eccbb4c38371ea9e986866e/numpy-2.4.0-cp313-cp313t-win_amd64.whl", hash = "sha256:b48e35f4ab6f6a7597c46e301126ceba4c44cd3280e3750f85db48b082624fa4", size = 12444263, upload-time = "2025-12-20T16:17:13.182Z" }, - { url = "https://files.pythonhosted.org/packages/a3/e3/9189ab319c01d2ed556c932ccf55064c5d75bb5850d1df7a482ce0badead/numpy-2.4.0-cp313-cp313t-win_arm64.whl", hash = "sha256:4d1cfce39e511069b11e67cd0bd78ceff31443b7c9e5c04db73c7a19f572967c", size = 10378265, upload-time = "2025-12-20T16:17:15.211Z" }, - { url = "https://files.pythonhosted.org/packages/ab/ed/52eac27de39d5e5a6c9aadabe672bc06f55e24a3d9010cd1183948055d76/numpy-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c95eb6db2884917d86cde0b4d4cf31adf485c8ec36bf8696dd66fa70de96f36b", size = 16647476, upload-time = "2025-12-20T16:17:17.671Z" }, - { url = "https://files.pythonhosted.org/packages/77/c0/990ce1b7fcd4e09aeaa574e2a0a839589e4b08b2ca68070f1acb1fea6736/numpy-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:65167da969cd1ec3a1df31cb221ca3a19a8aaa25370ecb17d428415e93c1935e", size = 12374563, upload-time = "2025-12-20T16:17:20.216Z" }, - { url = "https://files.pythonhosted.org/packages/37/7c/8c5e389c6ae8f5fd2277a988600d79e9625db3fff011a2d87ac80b881a4c/numpy-2.4.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3de19cfecd1465d0dcf8a5b5ea8b3155b42ed0b639dba4b71e323d74f2a3be5e", size = 5203107, upload-time = "2025-12-20T16:17:22.47Z" }, - { url = "https://files.pythonhosted.org/packages/e6/94/ca5b3bd6a8a70a5eec9a0b8dd7f980c1eff4b8a54970a9a7fef248ef564f/numpy-2.4.0-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:6c05483c3136ac4c91b4e81903cb53a8707d316f488124d0398499a4f8e8ef51", size = 6538067, upload-time = "2025-12-20T16:17:24.001Z" }, - { url = "https://files.pythonhosted.org/packages/79/43/993eb7bb5be6761dde2b3a3a594d689cec83398e3f58f4758010f3b85727/numpy-2.4.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36667db4d6c1cea79c8930ab72fadfb4060feb4bfe724141cd4bd064d2e5f8ce", size = 14411926, upload-time = "2025-12-20T16:17:25.822Z" }, - { url = "https://files.pythonhosted.org/packages/03/75/d4c43b61de473912496317a854dac54f1efec3eeb158438da6884b70bb90/numpy-2.4.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9a818668b674047fd88c4cddada7ab8f1c298812783e8328e956b78dc4807f9f", size = 16354295, upload-time = "2025-12-20T16:17:28.308Z" }, - { url = "https://files.pythonhosted.org/packages/b8/0a/b54615b47ee8736a6461a4bb6749128dd3435c5a759d5663f11f0e9af4ac/numpy-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1ee32359fb7543b7b7bd0b2f46294db27e29e7bbdf70541e81b190836cd83ded", size = 16190242, upload-time = "2025-12-20T16:17:30.993Z" }, - { url = "https://files.pythonhosted.org/packages/98/ce/ea207769aacad6246525ec6c6bbd66a2bf56c72443dc10e2f90feed29290/numpy-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e493962256a38f58283de033d8af176c5c91c084ea30f15834f7545451c42059", size = 18280875, upload-time = "2025-12-20T16:17:33.327Z" }, - { url = "https://files.pythonhosted.org/packages/17/ef/ec409437aa962ea372ed601c519a2b141701683ff028f894b7466f0ab42b/numpy-2.4.0-cp314-cp314-win32.whl", hash = "sha256:6bbaebf0d11567fa8926215ae731e1d58e6ec28a8a25235b8a47405d301332db", size = 6002530, upload-time = "2025-12-20T16:17:35.729Z" }, - { url = "https://files.pythonhosted.org/packages/5f/4a/5cb94c787a3ed1ac65e1271b968686521169a7b3ec0b6544bb3ca32960b0/numpy-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:3d857f55e7fdf7c38ab96c4558c95b97d1c685be6b05c249f5fdafcbd6f9899e", size = 12435890, upload-time = "2025-12-20T16:17:37.599Z" }, - { url = "https://files.pythonhosted.org/packages/48/a0/04b89db963af9de1104975e2544f30de89adbf75b9e75f7dd2599be12c79/numpy-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:bb50ce5fb202a26fd5404620e7ef820ad1ab3558b444cb0b55beb7ef66cd2d63", size = 10591892, upload-time = "2025-12-20T16:17:39.649Z" }, - { url = "https://files.pythonhosted.org/packages/53/e5/d74b5ccf6712c06c7a545025a6a71bfa03bdc7e0568b405b0d655232fd92/numpy-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:355354388cba60f2132df297e2d53053d4063f79077b67b481d21276d61fc4df", size = 12494312, upload-time = "2025-12-20T16:17:41.714Z" }, - { url = "https://files.pythonhosted.org/packages/c2/08/3ca9cc2ddf54dfee7ae9a6479c071092a228c68aef08252aa08dac2af002/numpy-2.4.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:1d8f9fde5f6dc1b6fc34df8162f3b3079365468703fee7f31d4e0cc8c63baed9", size = 5322862, upload-time = "2025-12-20T16:17:44.145Z" }, - { url = "https://files.pythonhosted.org/packages/87/74/0bb63a68394c0c1e52670cfff2e309afa41edbe11b3327d9af29e4383f34/numpy-2.4.0-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:e0434aa22c821f44eeb4c650b81c7fbdd8c0122c6c4b5a576a76d5a35625ecd9", size = 6644986, upload-time = "2025-12-20T16:17:46.203Z" }, - { url = "https://files.pythonhosted.org/packages/06/8f/9264d9bdbcf8236af2823623fe2f3981d740fc3461e2787e231d97c38c28/numpy-2.4.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40483b2f2d3ba7aad426443767ff5632ec3156ef09742b96913787d13c336471", size = 14457958, upload-time = "2025-12-20T16:17:48.017Z" }, - { url = "https://files.pythonhosted.org/packages/8c/d9/f9a69ae564bbc7236a35aa883319364ef5fd41f72aa320cc1cbe66148fe2/numpy-2.4.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9e6a7664ddd9746e20b7325351fe1a8408d0a2bf9c63b5e898290ddc8f09544", size = 16398394, upload-time = "2025-12-20T16:17:50.409Z" }, - { url = "https://files.pythonhosted.org/packages/34/c7/39241501408dde7f885d241a98caba5421061a2c6d2b2197ac5e3aa842d8/numpy-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ecb0019d44f4cdb50b676c5d0cb4b1eae8e15d1ed3d3e6639f986fc92b2ec52c", size = 16241044, upload-time = "2025-12-20T16:17:52.661Z" }, - { url = "https://files.pythonhosted.org/packages/7c/95/cae7effd90e065a95e59fe710eeee05d7328ed169776dfdd9f789e032125/numpy-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d0ffd9e2e4441c96a9c91ec1783285d80bf835b677853fc2770a89d50c1e48ac", size = 18321772, upload-time = "2025-12-20T16:17:54.947Z" }, - { url = "https://files.pythonhosted.org/packages/96/df/3c6c279accd2bfb968a76298e5b276310bd55d243df4fa8ac5816d79347d/numpy-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:77f0d13fa87036d7553bf81f0e1fe3ce68d14c9976c9851744e4d3e91127e95f", size = 6148320, upload-time = "2025-12-20T16:17:57.249Z" }, - { url = "https://files.pythonhosted.org/packages/92/8d/f23033cce252e7a75cae853d17f582e86534c46404dea1c8ee094a9d6d84/numpy-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b1f5b45829ac1848893f0ddf5cb326110604d6df96cdc255b0bf9edd154104d4", size = 12623460, upload-time = "2025-12-20T16:17:58.963Z" }, - { url = "https://files.pythonhosted.org/packages/a4/4f/1f8475907d1a7c4ef9020edf7f39ea2422ec896849245f00688e4b268a71/numpy-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:23a3e9d1a6f360267e8fbb38ba5db355a6a7e9be71d7fce7ab3125e88bb646c8", size = 10661799, upload-time = "2025-12-20T16:18:01.078Z" }, - { url = "https://files.pythonhosted.org/packages/4b/ef/088e7c7342f300aaf3ee5f2c821c4b9996a1bef2aaf6a49cc8ab4883758e/numpy-2.4.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b54c83f1c0c0f1d748dca0af516062b8829d53d1f0c402be24b4257a9c48ada6", size = 16819003, upload-time = "2025-12-20T16:18:03.41Z" }, - { url = "https://files.pythonhosted.org/packages/ff/ce/a53017b5443b4b84517182d463fc7bcc2adb4faa8b20813f8e5f5aeb5faa/numpy-2.4.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:aabb081ca0ec5d39591fc33018cd4b3f96e1a2dd6756282029986d00a785fba4", size = 12567105, upload-time = "2025-12-20T16:18:05.594Z" }, - { url = "https://files.pythonhosted.org/packages/77/58/5ff91b161f2ec650c88a626c3905d938c89aaadabd0431e6d9c1330c83e2/numpy-2.4.0-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:8eafe7c36c8430b7794edeab3087dec7bf31d634d92f2af9949434b9d1964cba", size = 5395590, upload-time = "2025-12-20T16:18:08.031Z" }, - { url = "https://files.pythonhosted.org/packages/1d/4e/f1a084106df8c2df8132fc437e56987308e0524836aa7733721c8429d4fe/numpy-2.4.0-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:2f585f52b2baf07ff3356158d9268ea095e221371f1074fadea2f42544d58b4d", size = 6709947, upload-time = "2025-12-20T16:18:09.836Z" }, - { url = "https://files.pythonhosted.org/packages/63/09/3d8aeb809c0332c3f642da812ac2e3d74fc9252b3021f8c30c82e99e3f3d/numpy-2.4.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32ed06d0fe9cae27d8fb5f400c63ccee72370599c75e683a6358dd3a4fb50aaf", size = 14535119, upload-time = "2025-12-20T16:18:12.105Z" }, - { url = "https://files.pythonhosted.org/packages/fd/7f/68f0fc43a2cbdc6bb239160c754d87c922f60fbaa0fa3cd3d312b8a7f5ee/numpy-2.4.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:57c540ed8fb1f05cb997c6761cd56db72395b0d6985e90571ff660452ade4f98", size = 16475815, upload-time = "2025-12-20T16:18:14.433Z" }, - { url = "https://files.pythonhosted.org/packages/11/73/edeacba3167b1ca66d51b1a5a14697c2c40098b5ffa01811c67b1785a5ab/numpy-2.4.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a39fb973a726e63223287adc6dafe444ce75af952d711e400f3bf2b36ef55a7b", size = 12489376, upload-time = "2025-12-20T16:18:16.524Z" }, +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/62/ae72ff66c0f1fd959925b4c11f8c2dea61f47f6acaea75a08512cdfe3fed/numpy-2.4.1.tar.gz", hash = "sha256:a1ceafc5042451a858231588a104093474c6a5c57dcc724841f5c888d237d690", size = 20721320, upload-time = "2026-01-10T06:44:59.619Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/34/2b1bc18424f3ad9af577f6ce23600319968a70575bd7db31ce66731bbef9/numpy-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0cce2a669e3c8ba02ee563c7835f92c153cf02edff1ae05e1823f1dde21b16a5", size = 16944563, upload-time = "2026-01-10T06:42:14.615Z" }, + { url = "https://files.pythonhosted.org/packages/2c/57/26e5f97d075aef3794045a6ca9eada6a4ed70eb9a40e7a4a93f9ac80d704/numpy-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:899d2c18024984814ac7e83f8f49d8e8180e2fbe1b2e252f2e7f1d06bea92425", size = 12645658, upload-time = "2026-01-10T06:42:17.298Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ba/80fc0b1e3cb2fd5c6143f00f42eb67762aa043eaa05ca924ecc3222a7849/numpy-2.4.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:09aa8a87e45b55a1c2c205d42e2808849ece5c484b2aab11fecabec3841cafba", size = 5474132, upload-time = "2026-01-10T06:42:19.637Z" }, + { url = "https://files.pythonhosted.org/packages/40/ae/0a5b9a397f0e865ec171187c78d9b57e5588afc439a04ba9cab1ebb2c945/numpy-2.4.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:edee228f76ee2dab4579fad6f51f6a305de09d444280109e0f75df247ff21501", size = 6804159, upload-time = "2026-01-10T06:42:21.44Z" }, + { url = "https://files.pythonhosted.org/packages/86/9c/841c15e691c7085caa6fd162f063eff494099c8327aeccd509d1ab1e36ab/numpy-2.4.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a92f227dbcdc9e4c3e193add1a189a9909947d4f8504c576f4a732fd0b54240a", size = 14708058, upload-time = "2026-01-10T06:42:23.546Z" }, + { url = "https://files.pythonhosted.org/packages/5d/9d/7862db06743f489e6a502a3b93136d73aea27d97b2cf91504f70a27501d6/numpy-2.4.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:538bf4ec353709c765ff75ae616c34d3c3dca1a68312727e8f2676ea644f8509", size = 16651501, upload-time = "2026-01-10T06:42:25.909Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9c/6fc34ebcbd4015c6e5f0c0ce38264010ce8a546cb6beacb457b84a75dfc8/numpy-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ac08c63cb7779b85e9d5318e6c3518b424bc1f364ac4cb2c6136f12e5ff2dccc", size = 16492627, upload-time = "2026-01-10T06:42:28.938Z" }, + { url = "https://files.pythonhosted.org/packages/aa/63/2494a8597502dacda439f61b3c0db4da59928150e62be0e99395c3ad23c5/numpy-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4f9c360ecef085e5841c539a9a12b883dff005fbd7ce46722f5e9cef52634d82", size = 18585052, upload-time = "2026-01-10T06:42:31.312Z" }, + { url = "https://files.pythonhosted.org/packages/6a/93/098e1162ae7522fc9b618d6272b77404c4656c72432ecee3abc029aa3de0/numpy-2.4.1-cp311-cp311-win32.whl", hash = "sha256:0f118ce6b972080ba0758c6087c3617b5ba243d806268623dc34216d69099ba0", size = 6236575, upload-time = "2026-01-10T06:42:33.872Z" }, + { url = "https://files.pythonhosted.org/packages/8c/de/f5e79650d23d9e12f38a7bc6b03ea0835b9575494f8ec94c11c6e773b1b1/numpy-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:18e14c4d09d55eef39a6ab5b08406e84bc6869c1e34eef45564804f90b7e0574", size = 12604479, upload-time = "2026-01-10T06:42:35.778Z" }, + { url = "https://files.pythonhosted.org/packages/dd/65/e1097a7047cff12ce3369bd003811516b20ba1078dbdec135e1cd7c16c56/numpy-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:6461de5113088b399d655d45c3897fa188766415d0f568f175ab071c8873bd73", size = 10578325, upload-time = "2026-01-10T06:42:38.518Z" }, + { url = "https://files.pythonhosted.org/packages/78/7f/ec53e32bf10c813604edf07a3682616bd931d026fcde7b6d13195dfb684a/numpy-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d3703409aac693fa82c0aee023a1ae06a6e9d065dba10f5e8e80f642f1e9d0a2", size = 16656888, upload-time = "2026-01-10T06:42:40.913Z" }, + { url = "https://files.pythonhosted.org/packages/b8/e0/1f9585d7dae8f14864e948fd7fa86c6cb72dee2676ca2748e63b1c5acfe0/numpy-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7211b95ca365519d3596a1d8688a95874cc94219d417504d9ecb2df99fa7bfa8", size = 12373956, upload-time = "2026-01-10T06:42:43.091Z" }, + { url = "https://files.pythonhosted.org/packages/8e/43/9762e88909ff2326f5e7536fa8cb3c49fb03a7d92705f23e6e7f553d9cb3/numpy-2.4.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:5adf01965456a664fc727ed69cc71848f28d063217c63e1a0e200a118d5eec9a", size = 5202567, upload-time = "2026-01-10T06:42:45.107Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ee/34b7930eb61e79feb4478800a4b95b46566969d837546aa7c034c742ef98/numpy-2.4.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:26f0bcd9c79a00e339565b303badc74d3ea2bd6d52191eeca5f95936cad107d0", size = 6549459, upload-time = "2026-01-10T06:42:48.152Z" }, + { url = "https://files.pythonhosted.org/packages/79/e3/5f115fae982565771be994867c89bcd8d7208dbfe9469185497d70de5ddf/numpy-2.4.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0093e85df2960d7e4049664b26afc58b03236e967fb942354deef3208857a04c", size = 14404859, upload-time = "2026-01-10T06:42:49.947Z" }, + { url = "https://files.pythonhosted.org/packages/d9/7d/9c8a781c88933725445a859cac5d01b5871588a15969ee6aeb618ba99eee/numpy-2.4.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ad270f438cbdd402c364980317fb6b117d9ec5e226fff5b4148dd9aa9fc6e02", size = 16371419, upload-time = "2026-01-10T06:42:52.409Z" }, + { url = "https://files.pythonhosted.org/packages/a6/d2/8aa084818554543f17cf4162c42f162acbd3bb42688aefdba6628a859f77/numpy-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:297c72b1b98100c2e8f873d5d35fb551fce7040ade83d67dd51d38c8d42a2162", size = 16182131, upload-time = "2026-01-10T06:42:54.694Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/0425216684297c58a8df35f3284ef56ec4a043e6d283f8a59c53562caf1b/numpy-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf6470d91d34bf669f61d515499859fa7a4c2f7c36434afb70e82df7217933f9", size = 18295342, upload-time = "2026-01-10T06:42:56.991Z" }, + { url = "https://files.pythonhosted.org/packages/31/4c/14cb9d86240bd8c386c881bafbe43f001284b7cce3bc01623ac9475da163/numpy-2.4.1-cp312-cp312-win32.whl", hash = "sha256:b6bcf39112e956594b3331316d90c90c90fb961e39696bda97b89462f5f3943f", size = 5959015, upload-time = "2026-01-10T06:42:59.631Z" }, + { url = "https://files.pythonhosted.org/packages/51/cf/52a703dbeb0c65807540d29699fef5fda073434ff61846a564d5c296420f/numpy-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:e1a27bb1b2dee45a2a53f5ca6ff2d1a7f135287883a1689e930d44d1ff296c87", size = 12310730, upload-time = "2026-01-10T06:43:01.627Z" }, + { url = "https://files.pythonhosted.org/packages/69/80/a828b2d0ade5e74a9fe0f4e0a17c30fdc26232ad2bc8c9f8b3197cf7cf18/numpy-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:0e6e8f9d9ecf95399982019c01223dc130542960a12edfa8edd1122dfa66a8a8", size = 10312166, upload-time = "2026-01-10T06:43:03.673Z" }, + { url = "https://files.pythonhosted.org/packages/04/68/732d4b7811c00775f3bd522a21e8dd5a23f77eb11acdeb663e4a4ebf0ef4/numpy-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d797454e37570cfd61143b73b8debd623c3c0952959adb817dd310a483d58a1b", size = 16652495, upload-time = "2026-01-10T06:43:06.283Z" }, + { url = "https://files.pythonhosted.org/packages/20/ca/857722353421a27f1465652b2c66813eeeccea9d76d5f7b74b99f298e60e/numpy-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82c55962006156aeef1629b953fd359064aa47e4d82cfc8e67f0918f7da3344f", size = 12368657, upload-time = "2026-01-10T06:43:09.094Z" }, + { url = "https://files.pythonhosted.org/packages/81/0d/2377c917513449cc6240031a79d30eb9a163d32a91e79e0da47c43f2c0c8/numpy-2.4.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:71abbea030f2cfc3092a0ff9f8c8fdefdc5e0bf7d9d9c99663538bb0ecdac0b9", size = 5197256, upload-time = "2026-01-10T06:43:13.634Z" }, + { url = "https://files.pythonhosted.org/packages/17/39/569452228de3f5de9064ac75137082c6214be1f5c532016549a7923ab4b5/numpy-2.4.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:5b55aa56165b17aaf15520beb9cbd33c9039810e0d9643dd4379e44294c7303e", size = 6545212, upload-time = "2026-01-10T06:43:15.661Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a4/77333f4d1e4dac4395385482557aeecf4826e6ff517e32ca48e1dafbe42a/numpy-2.4.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0faba4a331195bfa96f93dd9dfaa10b2c7aa8cda3a02b7fd635e588fe821bf5", size = 14402871, upload-time = "2026-01-10T06:43:17.324Z" }, + { url = "https://files.pythonhosted.org/packages/ba/87/d341e519956273b39d8d47969dd1eaa1af740615394fe67d06f1efa68773/numpy-2.4.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e3087f53e2b4428766b54932644d148613c5a595150533ae7f00dab2f319a8", size = 16359305, upload-time = "2026-01-10T06:43:19.376Z" }, + { url = "https://files.pythonhosted.org/packages/32/91/789132c6666288eaa20ae8066bb99eba1939362e8f1a534949a215246e97/numpy-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:49e792ec351315e16da54b543db06ca8a86985ab682602d90c60ef4ff4db2a9c", size = 16181909, upload-time = "2026-01-10T06:43:21.808Z" }, + { url = "https://files.pythonhosted.org/packages/cf/b8/090b8bd27b82a844bb22ff8fdf7935cb1980b48d6e439ae116f53cdc2143/numpy-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79e9e06c4c2379db47f3f6fc7a8652e7498251789bf8ff5bd43bf478ef314ca2", size = 18284380, upload-time = "2026-01-10T06:43:23.957Z" }, + { url = "https://files.pythonhosted.org/packages/67/78/722b62bd31842ff029412271556a1a27a98f45359dea78b1548a3a9996aa/numpy-2.4.1-cp313-cp313-win32.whl", hash = "sha256:3d1a100e48cb266090a031397863ff8a30050ceefd798f686ff92c67a486753d", size = 5957089, upload-time = "2026-01-10T06:43:27.535Z" }, + { url = "https://files.pythonhosted.org/packages/da/a6/cf32198b0b6e18d4fbfa9a21a992a7fca535b9bb2b0cdd217d4a3445b5ca/numpy-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:92a0e65272fd60bfa0d9278e0484c2f52fe03b97aedc02b357f33fe752c52ffb", size = 12307230, upload-time = "2026-01-10T06:43:29.298Z" }, + { url = "https://files.pythonhosted.org/packages/44/6c/534d692bfb7d0afe30611320c5fb713659dcb5104d7cc182aff2aea092f5/numpy-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:20d4649c773f66cc2fc36f663e091f57c3b7655f936a4c681b4250855d1da8f5", size = 10313125, upload-time = "2026-01-10T06:43:31.782Z" }, + { url = "https://files.pythonhosted.org/packages/da/a1/354583ac5c4caa566de6ddfbc42744409b515039e085fab6e0ff942e0df5/numpy-2.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f93bc6892fe7b0663e5ffa83b61aab510aacffd58c16e012bb9352d489d90cb7", size = 12496156, upload-time = "2026-01-10T06:43:34.237Z" }, + { url = "https://files.pythonhosted.org/packages/51/b0/42807c6e8cce58c00127b1dc24d365305189991f2a7917aa694a109c8d7d/numpy-2.4.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:178de8f87948163d98a4c9ab5bee4ce6519ca918926ec8df195af582de28544d", size = 5324663, upload-time = "2026-01-10T06:43:36.211Z" }, + { url = "https://files.pythonhosted.org/packages/fe/55/7a621694010d92375ed82f312b2f28017694ed784775269115323e37f5e2/numpy-2.4.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:98b35775e03ab7f868908b524fc0a84d38932d8daf7b7e1c3c3a1b6c7a2c9f15", size = 6645224, upload-time = "2026-01-10T06:43:37.884Z" }, + { url = "https://files.pythonhosted.org/packages/50/96/9fa8635ed9d7c847d87e30c834f7109fac5e88549d79ef3324ab5c20919f/numpy-2.4.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:941c2a93313d030f219f3a71fd3d91a728b82979a5e8034eb2e60d394a2b83f9", size = 14462352, upload-time = "2026-01-10T06:43:39.479Z" }, + { url = "https://files.pythonhosted.org/packages/03/d1/8cf62d8bb2062da4fb82dd5d49e47c923f9c0738032f054e0a75342faba7/numpy-2.4.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:529050522e983e00a6c1c6b67411083630de8b57f65e853d7b03d9281b8694d2", size = 16407279, upload-time = "2026-01-10T06:43:41.93Z" }, + { url = "https://files.pythonhosted.org/packages/86/1c/95c86e17c6b0b31ce6ef219da00f71113b220bcb14938c8d9a05cee0ff53/numpy-2.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2302dc0224c1cbc49bb94f7064f3f923a971bfae45c33870dcbff63a2a550505", size = 16248316, upload-time = "2026-01-10T06:43:44.121Z" }, + { url = "https://files.pythonhosted.org/packages/30/b4/e7f5ff8697274c9d0fa82398b6a372a27e5cef069b37df6355ccb1f1db1a/numpy-2.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9171a42fcad32dcf3fa86f0a4faa5e9f8facefdb276f54b8b390d90447cff4e2", size = 18329884, upload-time = "2026-01-10T06:43:46.613Z" }, + { url = "https://files.pythonhosted.org/packages/37/a4/b073f3e9d77f9aec8debe8ca7f9f6a09e888ad1ba7488f0c3b36a94c03ac/numpy-2.4.1-cp313-cp313t-win32.whl", hash = "sha256:382ad67d99ef49024f11d1ce5dcb5ad8432446e4246a4b014418ba3a1175a1f4", size = 6081138, upload-time = "2026-01-10T06:43:48.854Z" }, + { url = "https://files.pythonhosted.org/packages/16/16/af42337b53844e67752a092481ab869c0523bc95c4e5c98e4dac4e9581ac/numpy-2.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:62fea415f83ad8fdb6c20840578e5fbaf5ddd65e0ec6c3c47eda0f69da172510", size = 12447478, upload-time = "2026-01-10T06:43:50.476Z" }, + { url = "https://files.pythonhosted.org/packages/6c/f8/fa85b2eac68ec631d0b631abc448552cb17d39afd17ec53dcbcc3537681a/numpy-2.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a7870e8c5fc11aef57d6fea4b4085e537a3a60ad2cdd14322ed531fdca68d261", size = 10382981, upload-time = "2026-01-10T06:43:52.575Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a7/ef08d25698e0e4b4efbad8d55251d20fe2a15f6d9aa7c9b30cd03c165e6f/numpy-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3869ea1ee1a1edc16c29bbe3a2f2a4e515cc3a44d43903ad41e0cacdbaf733dc", size = 16652046, upload-time = "2026-01-10T06:43:54.797Z" }, + { url = "https://files.pythonhosted.org/packages/8f/39/e378b3e3ca13477e5ac70293ec027c438d1927f18637e396fe90b1addd72/numpy-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e867df947d427cdd7a60e3e271729090b0f0df80f5f10ab7dd436f40811699c3", size = 12378858, upload-time = "2026-01-10T06:43:57.099Z" }, + { url = "https://files.pythonhosted.org/packages/c3/74/7ec6154f0006910ed1fdbb7591cf4432307033102b8a22041599935f8969/numpy-2.4.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:e3bd2cb07841166420d2fa7146c96ce00cb3410664cbc1a6be028e456c4ee220", size = 5207417, upload-time = "2026-01-10T06:43:59.037Z" }, + { url = "https://files.pythonhosted.org/packages/f7/b7/053ac11820d84e42f8feea5cb81cc4fcd1091499b45b1ed8c7415b1bf831/numpy-2.4.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:f0a90aba7d521e6954670550e561a4cb925713bd944445dbe9e729b71f6cabee", size = 6542643, upload-time = "2026-01-10T06:44:01.852Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c4/2e7908915c0e32ca636b92e4e4a3bdec4cb1e7eb0f8aedf1ed3c68a0d8cd/numpy-2.4.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d558123217a83b2d1ba316b986e9248a1ed1971ad495963d555ccd75dcb1556", size = 14418963, upload-time = "2026-01-10T06:44:04.047Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c0/3ed5083d94e7ffd7c404e54619c088e11f2e1939a9544f5397f4adb1b8ba/numpy-2.4.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2f44de05659b67d20499cbc96d49f2650769afcb398b79b324bb6e297bfe3844", size = 16363811, upload-time = "2026-01-10T06:44:06.207Z" }, + { url = "https://files.pythonhosted.org/packages/0e/68/42b66f1852bf525050a67315a4fb94586ab7e9eaa541b1bef530fab0c5dd/numpy-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:69e7419c9012c4aaf695109564e3387f1259f001b4326dfa55907b098af082d3", size = 16197643, upload-time = "2026-01-10T06:44:08.33Z" }, + { url = "https://files.pythonhosted.org/packages/d2/40/e8714fc933d85f82c6bfc7b998a0649ad9769a32f3494ba86598aaf18a48/numpy-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2ffd257026eb1b34352e749d7cc1678b5eeec3e329ad8c9965a797e08ccba205", size = 18289601, upload-time = "2026-01-10T06:44:10.841Z" }, + { url = "https://files.pythonhosted.org/packages/80/9a/0d44b468cad50315127e884802351723daca7cf1c98d102929468c81d439/numpy-2.4.1-cp314-cp314-win32.whl", hash = "sha256:727c6c3275ddefa0dc078524a85e064c057b4f4e71ca5ca29a19163c607be745", size = 6005722, upload-time = "2026-01-10T06:44:13.332Z" }, + { url = "https://files.pythonhosted.org/packages/7e/bb/c6513edcce5a831810e2dddc0d3452ce84d208af92405a0c2e58fd8e7881/numpy-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:7d5d7999df434a038d75a748275cd6c0094b0ecdb0837342b332a82defc4dc4d", size = 12438590, upload-time = "2026-01-10T06:44:15.006Z" }, + { url = "https://files.pythonhosted.org/packages/e9/da/a598d5cb260780cf4d255102deba35c1d072dc028c4547832f45dd3323a8/numpy-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:ce9ce141a505053b3c7bce3216071f3bf5c182b8b28930f14cd24d43932cd2df", size = 10596180, upload-time = "2026-01-10T06:44:17.386Z" }, + { url = "https://files.pythonhosted.org/packages/de/bc/ea3f2c96fcb382311827231f911723aeff596364eb6e1b6d1d91128aa29b/numpy-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4e53170557d37ae404bf8d542ca5b7c629d6efa1117dac6a83e394142ea0a43f", size = 12498774, upload-time = "2026-01-10T06:44:19.467Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ab/ef9d939fe4a812648c7a712610b2ca6140b0853c5efea361301006c02ae5/numpy-2.4.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:a73044b752f5d34d4232f25f18160a1cc418ea4507f5f11e299d8ac36875f8a0", size = 5327274, upload-time = "2026-01-10T06:44:23.189Z" }, + { url = "https://files.pythonhosted.org/packages/bd/31/d381368e2a95c3b08b8cf7faac6004849e960f4a042d920337f71cef0cae/numpy-2.4.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:fb1461c99de4d040666ca0444057b06541e5642f800b71c56e6ea92d6a853a0c", size = 6648306, upload-time = "2026-01-10T06:44:25.012Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e5/0989b44ade47430be6323d05c23207636d67d7362a1796ccbccac6773dd2/numpy-2.4.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423797bdab2eeefbe608d7c1ec7b2b4fd3c58d51460f1ee26c7500a1d9c9ee93", size = 14464653, upload-time = "2026-01-10T06:44:26.706Z" }, + { url = "https://files.pythonhosted.org/packages/10/a7/cfbe475c35371cae1358e61f20c5f075badc18c4797ab4354140e1d283cf/numpy-2.4.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:52b5f61bdb323b566b528899cc7db2ba5d1015bda7ea811a8bcf3c89c331fa42", size = 16405144, upload-time = "2026-01-10T06:44:29.378Z" }, + { url = "https://files.pythonhosted.org/packages/f8/a3/0c63fe66b534888fa5177cc7cef061541064dbe2b4b60dcc60ffaf0d2157/numpy-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42d7dd5fa36d16d52a84f821eb96031836fd405ee6955dd732f2023724d0aa01", size = 16247425, upload-time = "2026-01-10T06:44:31.721Z" }, + { url = "https://files.pythonhosted.org/packages/6b/2b/55d980cfa2c93bd40ff4c290bf824d792bd41d2fe3487b07707559071760/numpy-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7b6b5e28bbd47b7532698e5db2fe1db693d84b58c254e4389d99a27bb9b8f6b", size = 18330053, upload-time = "2026-01-10T06:44:34.617Z" }, + { url = "https://files.pythonhosted.org/packages/23/12/8b5fc6b9c487a09a7957188e0943c9ff08432c65e34567cabc1623b03a51/numpy-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:5de60946f14ebe15e713a6f22850c2372fa72f4ff9a432ab44aa90edcadaa65a", size = 6152482, upload-time = "2026-01-10T06:44:36.798Z" }, + { url = "https://files.pythonhosted.org/packages/00/a5/9f8ca5856b8940492fc24fbe13c1bc34d65ddf4079097cf9e53164d094e1/numpy-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:8f085da926c0d491ffff3096f91078cc97ea67e7e6b65e490bc8dcda65663be2", size = 12627117, upload-time = "2026-01-10T06:44:38.828Z" }, + { url = "https://files.pythonhosted.org/packages/ad/0d/eca3d962f9eef265f01a8e0d20085c6dd1f443cbffc11b6dede81fd82356/numpy-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:6436cffb4f2bf26c974344439439c95e152c9a527013f26b3577be6c2ca64295", size = 10667121, upload-time = "2026-01-10T06:44:41.644Z" }, + { url = "https://files.pythonhosted.org/packages/1e/48/d86f97919e79314a1cdee4c832178763e6e98e623e123d0bada19e92c15a/numpy-2.4.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8ad35f20be147a204e28b6a0575fbf3540c5e5f802634d4258d55b1ff5facce1", size = 16822202, upload-time = "2026-01-10T06:44:43.738Z" }, + { url = "https://files.pythonhosted.org/packages/51/e9/1e62a7f77e0f37dcfb0ad6a9744e65df00242b6ea37dfafb55debcbf5b55/numpy-2.4.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:8097529164c0f3e32bb89412a0905d9100bf434d9692d9fc275e18dcf53c9344", size = 12569985, upload-time = "2026-01-10T06:44:45.945Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7e/914d54f0c801342306fdcdce3e994a56476f1b818c46c47fc21ae968088c/numpy-2.4.1-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:ea66d2b41ca4a1630aae5507ee0a71647d3124d1741980138aa8f28f44dac36e", size = 5398484, upload-time = "2026-01-10T06:44:48.012Z" }, + { url = "https://files.pythonhosted.org/packages/1c/d8/9570b68584e293a33474e7b5a77ca404f1dcc655e40050a600dee81d27fb/numpy-2.4.1-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:d3f8f0df9f4b8be57b3bf74a1d087fec68f927a2fab68231fdb442bf2c12e426", size = 6713216, upload-time = "2026-01-10T06:44:49.725Z" }, + { url = "https://files.pythonhosted.org/packages/33/9b/9dd6e2db8d49eb24f86acaaa5258e5f4c8ed38209a4ee9de2d1a0ca25045/numpy-2.4.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2023ef86243690c2791fd6353e5b4848eedaa88ca8a2d129f462049f6d484696", size = 14538937, upload-time = "2026-01-10T06:44:51.498Z" }, + { url = "https://files.pythonhosted.org/packages/53/87/d5bd995b0f798a37105b876350d346eea5838bd8f77ea3d7a48392f3812b/numpy-2.4.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8361ea4220d763e54cff2fbe7d8c93526b744f7cd9ddab47afeff7e14e8503be", size = 16479830, upload-time = "2026-01-10T06:44:53.931Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c7/b801bf98514b6ae6475e941ac05c58e6411dd863ea92916bfd6d510b08c1/numpy-2.4.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:4f1b68ff47680c2925f8063402a693ede215f0257f02596b1318ecdfb1d79e33", size = 12492579, upload-time = "2026-01-10T06:44:57.094Z" }, ] [[package]] @@ -379,6 +629,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" }, ] +[[package]] +name = "pandas-stubs" +version = "3.0.5.260730" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/d2/dea4a3a56b7b5f69c5fbca9f14625fcf28e1a39a657e9833d4a10bcac593/pandas_stubs-3.0.5.260730.tar.gz", hash = "sha256:f70a232c57d93a5a2c81f8a53953e10891a5374bc92652277deb325e2e4d0ff3", size = 114631, upload-time = "2026-07-30T14:31:42.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/c2/959caec5c46f484b5f8bb6def4b0cf7a45ba6acda26f12b75142d98cc5ae/pandas_stubs-3.0.5.260730-py3-none-any.whl", hash = "sha256:60e90e3e1eda6937e337e243cbe6217e151c11137cd7eddf832af537c7310bfd", size = 174807, upload-time = "2026-07-30T14:31:41.17Z" }, +] + [[package]] name = "parso" version = "0.8.5" @@ -388,6 +650,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl", hash = "sha256:646204b5ee239c396d040b90f9e272e9a8017c630092bf59980beb62fd033887", size = 106668, upload-time = "2025-08-23T15:15:25.663Z" }, ] +[[package]] +name = "pathspec" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, +] + [[package]] name = "pexpect" version = "4.9.0" @@ -667,6 +938,8 @@ files = [ [package.dev-dependencies] dev = [ { name = "ipython" }, + { name = "mypy" }, + { name = "pandas-stubs" }, { name = "pytest" }, { name = "ruff" }, { name = "tqdm" }, @@ -688,6 +961,8 @@ provides-extras = ["files"] [package.metadata.requires-dev] dev = [ { name = "ipython", specifier = ">=9.8.0" }, + { name = "mypy", specifier = ">=2.3.1" }, + { name = "pandas-stubs", specifier = ">=2.3.3" }, { name = "pytest", specifier = ">=9.1.1" }, { name = "ruff", specifier = ">=0.16.6" }, { name = "tqdm", specifier = ">=4.67.1" },