diff --git a/.gitignore b/.gitignore index 8e211bb8..70d379dd 100755 --- a/.gitignore +++ b/.gitignore @@ -165,6 +165,12 @@ data/ # Documentation build site/ data/ +# Exception: package-bundled NSVB coefficient tables (vendored from GTR-WO-104 Supp1) +!src/pyfia/carbon/nsvb/data/ +!src/pyfia/carbon/nsvb/data/** +# Exception: NGHGI report-target CSVs (frozen EPA Chapter 6 published numbers) +!src/pyfia/carbon/data/ +!src/pyfia/carbon/data/nghgi_*.csv # JOSS paper (kept separate from repo) paper.md diff --git a/CHANGELOG.md b/CHANGELOG.md index c6887dc2..6a11cf2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,36 @@ All notable changes to pyFIA will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added +- **`live_tree()` function** — NSVB live tree carbon estimation: + - Recomputes above-ground biomass from scratch using the NSVB framework (Westfall et al. 2023, GTR-WO-104) + - Species-specific S10a carbon fractions (0.40–0.55) replace the flat 0.47 multiplier used by `biomass()` + - 3-level coefficient lookup precedence: Bailey DIVISION, species-level, Jenkins fallback + - Cull adjustment using Harmon et al. (2011) DECAYCD=3 density proportions + - `pool='ag'|'bg'|'total'` — AG via NSVB, BG bridges to FIADB `TREE.CARBON_BG` + - Validated against FIADB `TREE.CARBON_AG` on Georgia EVALID 132401 (130,952 trees): median per-tree relative error 0.085% +- **`standing_dead()` function** — NSVB standing dead carbon estimation: + - Same NSVB biomass pipeline as `live_tree()`, plus decay-class reductions from `REF_TREE_DECAY_PROP` + - `DENSITY_PROP` x wood, `BARK_LOSS_PROP` x bark, `BRANCH_LOSS_PROP` x branch by hardwood/softwood x DECAYCD + - S10b dead-tree carbon fractions by hardwood/softwood x DECAYCD + - No `TREE.CULL` adjustment for dead trees (per FIADB Appendix K) + - `pool='ag'|'bg'|'total'` — same pool semantics as `live_tree()` + - Broken-top corrections: crown-proportion adjustment (Appendix K) + paraboloid volume-ratio for trees with `ACTUALHT < HT` + - Vendored Table S11 (`REF_TREE_STND_DEAD_CR_PROP`) for mean intact crown ratios by ecoregion province + - Validated against FIADB on Georgia EVALID 132401 (6,870 trees): median 10.9% per-tree relative error +- **`pyfia.carbon` subpackage** — NSVB equation library, coefficient loaders, carbon fractions: + - `pyfia.carbon.nsvb.equations` — Models 1, 2, 4, 5, harmonization, vectorized pipelines + - `pyfia.carbon.nsvb.coefficients` — S1a–S8b coefficient tables, Bailey DIVISION lookup + - `pyfia.carbon.nsvb.carbon_fractions` — S10a (live), S10b (dead), `REF_TREE_DECAY_PROP` loaders + - Vendored coefficient CSVs from GTR-WO-104 supplementary archive +- **NSVB validation gate** — `tests/validation/test_live_tree_nsvb.py` and `test_standing_dead_nsvb.py`: + - Per-tree parity tests against FIADB `CARBON_AG` on real Georgia inventory data + - Layered diagnostics: carbon rel-error, biomass ratio, FIADB-implied carbon fraction + - Ratchet thresholds that detect regressions and reward improvements + - EVALID-scoped to the current annual evaluation (avoids legacy CRM data contamination) + ## [1.2.0] - 2025-01-18 ### Added diff --git a/CLAUDE.md b/CLAUDE.md index 482bfd6d..4824bcbb 100755 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -70,6 +70,7 @@ pyfia/ ├── src/pyfia/ # Library source code │ ├── core/ # Database, backends, and settings │ ├── estimation/ # Statistical estimation +│ ├── carbon/ # Carbon estimation (all 6 IPCC/NGHGI pools + total_ecosystem) │ ├── filtering/ # Domain filtering │ ├── downloader/ # FIA data download from DataMart │ ├── evalidator/ # EVALIDator API client for validation diff --git a/README.md b/README.md index 083031b8..4fc2a613 100755 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ pip install pyfia ``` ```python -from pyfia import FIA, biomass, tpa, volume, area +from pyfia import FIA, tpa, volume, area, total_ecosystem with FIA("path/to/FIA_database.duckdb") as db: db.clip_by_state(37) # North Carolina @@ -41,9 +41,11 @@ with FIA("path/to/FIA_database.duckdb") as db: # Core estimates trees = tpa(db, tree_domain="STATUSCD == 1") - carbon = biomass(db, by_species=True) timber = volume(db, land_type="timber") forest = area(db, land_type="forest") + + # Total ecosystem carbon (all 6 IPCC/NGHGI pools) + carbon = total_ecosystem(db) ``` ## Core Functions @@ -54,6 +56,14 @@ with FIA("path/to/FIA_database.duckdb") as db: | `biomass()` | Above/belowground biomass | `biomass(db, by_species=True)` | | `volume()` | Merchantable volume (ft³) | `volume(db, land_type="timber")` | | `area()` | Forest land area | `area(db, grp_by="FORTYPCD")` | +| `live_tree()` | NSVB live tree carbon | `live_tree(db, pool="ag")` | +| `standing_dead()` | NSVB standing dead carbon | `standing_dead(db, pool="ag")` | +| `understory()` | Understory vegetation carbon | `understory(db, pool="total")` | +| `downed_dead()` | Downed dead wood carbon | `downed_dead(db)` | +| `litter()` | Litter carbon | `litter(db)` | +| `soil_organic()` | Soil organic carbon | `soil_organic(db)` | +| `total_ecosystem()` | Total ecosystem carbon (all 6 pools) | `total_ecosystem(db)` | +| `stock_change()` | Carbon stock change between periods | `stock_change(db, pool="all")` | | `site_index()` | Site productivity index | `site_index(db, grp_by="COUNTYCD")` | | `mortality()` | Annual mortality rates | `mortality(db)` | | `growth()` | Net growth estimation | `growth(db)` | diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 7cc7827d..f43fd54a 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -47,6 +47,17 @@ pyfia/ │ ├── fia.py # Main FIA database class │ ├── data_reader.py # Efficient data loading │ └── backends/ # DuckDB, SQLite, MotherDuck backends +├── carbon/ # NSVB carbon estimation +│ ├── live_tree.py # Live tree carbon (NSVB AG + FIADB BG bridge) +│ ├── standing_dead.py # Standing dead carbon (NSVB + decay reductions) +│ ├── understory.py # Understory vegetation carbon (condition-level) +│ ├── downed_dead.py # Downed dead wood carbon (condition-level) +│ ├── litter.py # Litter carbon (condition-level) +│ ├── soil_organic.py # Soil organic carbon (condition-level) +│ ├── total_ecosystem.py # Sum of all 6 pools +│ ├── stock_change.py # Carbon stock change between inventory periods +│ ├── data/ # Non-NSVB coefficient tables (Birdsey/Smith & Heath) +│ └── nsvb/ # NSVB equation library, coefficients, carbon fractions ├── estimation/ # Statistical estimation │ ├── base.py # BaseEstimator with Template Method pattern │ ├── grm.py # GRM data loading and adjustment @@ -85,7 +96,7 @@ pyfia/ - Key methods: `clip_by_evalid()`, `clip_by_state()`, `clip_most_recent()` **Estimation Functions** -- Simple API: `area()`, `biomass()`, `volume()`, `tpa()`, `mortality()`, `growth()`, `removals()`, `area_change()`, `site_index()`, `tree_metrics()`, `carbon_pools()` +- Simple API: `area()`, `biomass()`, `volume()`, `tpa()`, `live_tree()`, `standing_dead()`, `understory()`, `downed_dead()`, `litter()`, `soil_organic()`, `total_ecosystem()`, `stock_change()`, `mortality()`, `growth()`, `removals()`, `area_change()`, `site_index()`, `tree_metrics()` - All support domain filtering, grouping, variance calculations - BaseEstimator uses Template Method for consistent workflow diff --git a/docs/api/index.md b/docs/api/index.md index dc79ee2e..996dadf0 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -25,6 +25,8 @@ result = estimator(db, **options) # Returns pl.DataFrame | [`volume()`](volume.md) | Estimate tree volume | | [`tpa()`](tpa.md) | Trees per acre and basal area | | [`biomass()`](biomass.md) | Tree biomass and carbon | +| [`live_tree()`](live_tree.md) | NSVB live tree carbon | +| [`standing_dead()`](standing_dead.md) | NSVB standing dead carbon | | [`site_index()`](site_index.md) | Area-weighted mean site index | | [`mortality()`](mortality.md) | Annual tree mortality | | [`growth()`](growth.md) | Annual tree growth | diff --git a/docs/api/live_tree.md b/docs/api/live_tree.md new file mode 100644 index 00000000..acb14481 --- /dev/null +++ b/docs/api/live_tree.md @@ -0,0 +1,119 @@ +# Live Tree Carbon Estimation + +Estimate live tree carbon using the NSVB biomass framework with species-specific carbon fractions. + +## Overview + +The `live_tree()` function recomputes above-ground live tree biomass from scratch using the National Scale Volume and Biomass (NSVB) framework of Westfall et al. (2023, GTR-WO-104) and converts to carbon via species-specific S10a carbon fractions. This produces carbon estimates that align with the EPA NGHGI LULUCF live tree pool and match FIADB's pre-computed `CARBON_AG` column for NSVB-era inventories (September 2023 onward). + +```python +import pyfia + +db = pyfia.FIA("georgia.duckdb") +db.clip_by_state("GA") +db.clip_most_recent(eval_type="VOL") + +# Above-ground live tree carbon +result = pyfia.live_tree(db, pool="ag") + +# Total carbon (AG + BG bridge) +total = pyfia.live_tree(db, pool="total") +``` + +## Function Reference + +::: pyfia.live_tree + options: + show_root_heading: true + show_source: true + +## Carbon Pools + +| Pool | Description | Method | +|------|-------------|--------| +| `"ag"` | Above-ground (default) | NSVB pipeline: stem wood + bark + branches, harmonized to total AGB, then multiplied by species-specific S10a carbon fractions | +| `"bg"` | Below-ground (coarse roots) | Bridge to FIADB `TREE.CARBON_BG` (Phase 1 shortcut; native NSVB root model planned) | +| `"total"` | AG + BG | NSVB above-ground + FIADB below-ground bridge | + +## How It Differs from `biomass()` + +| | `live_tree()` | `biomass()` | +|---|---|---| +| **Biomass source** | Recomputed from scratch via NSVB equations | Reads FIADB pre-computed `DRYBIO_*` columns | +| **Carbon fraction** | Species-specific S10a (0.40-0.55) | Flat 0.47 multiplier | +| **Coefficient lookup** | 3-level precedence (DIVISION, species, Jenkins) | N/A (pre-computed) | +| **Cull adjustment** | NSVB cull formula with DECAYCD=3 density prop | Built into FIADB values | +| **Transparency** | Full recompute, auditable | Black-box FIADB values | + +For NSVB-era inventories (2024+), both should agree closely. `live_tree()` is the preferred path for carbon accounting work that needs methodological transparency. + +## Technical Notes + +The NSVB pipeline predicts five biomass components per tree: + +1. Stem inside-bark wood volume (S1a) x wood density x 62.4 = gross wood weight +2. Stem bark biomass (S6a) +3. Branch biomass (S7a) +4. Total above-ground biomass (S8a) - directly predicted + +The component sum is harmonized proportionally to the directly-predicted total AGB. Cull-reduced wood uses the Harmon et al. (2011) DECAYCD=3 density proportion (0.54 hardwood, 0.92 softwood). Carbon = harmonized AGB x species-specific S10a fraction. + +The optional `PLOTGEOM.ECOSUBCD` join activates Level 2 of the NSVB coefficient precedence (SPCD + Bailey DIVISION), closing a ~3% growing-stock biomass bias present in the species-level-only fallback. When `PLOTGEOM` is missing from older databases, the estimator falls back gracefully with a one-shot log warning. + +## Examples + +### Above-Ground Carbon Per Acre + +```python +result = pyfia.live_tree(db, pool="ag") +print(f"Carbon: {result['CARBON_ACRE'][0]:.2f} tons/acre") +``` + +### Carbon by Species + +```python +result = pyfia.live_tree(db, pool="ag", by_species=True) +result = pyfia.join_species_names(result, db) +print(result.sort("CARBON_ACRE", descending=True).head(10)) +``` + +### Carbon by Ownership Group + +```python +result = pyfia.live_tree( + db, + pool="total", + grp_by="OWNGRPCD", + totals=True, + variance=True, +) +# OWNGRPCD: 10=National Forest, 20=Other Federal, +# 30=State/Local, 40=Private +print(result) +``` + +### Large Tree Carbon by Forest Type + +```python +result = pyfia.live_tree( + db, + pool="ag", + grp_by="FORTYPCD", + tree_domain="DIA >= 20.0", +) +result = pyfia.join_forest_type_names(result, db) +print(result) +``` + +### Carbon on Timberland with Standard Errors + +```python +result = pyfia.live_tree( + db, + pool="ag", + land_type="timber", + variance=True, +) +print(f"Carbon: {result['CARBON_ACRE'][0]:.2f} +/- " + f"{result['CARBON_ACRE_SE'][0]:.2f} tons/acre") +``` diff --git a/docs/api/standing_dead.md b/docs/api/standing_dead.md new file mode 100644 index 00000000..290ca637 --- /dev/null +++ b/docs/api/standing_dead.md @@ -0,0 +1,152 @@ +# Standing Dead Carbon Estimation + +Estimate standing dead tree carbon using the NSVB biomass framework with decay-class reductions and dead-tree carbon fractions. + +## Overview + +The `standing_dead()` function recomputes above-ground standing dead tree biomass from scratch using the National Scale Volume and Biomass (NSVB) framework and applies the FIADB `REF_TREE_DECAY_PROP` density and structural-loss reductions by decay class. The reduced biomass is converted to carbon via S10b dead-tree carbon fractions (hardwood/softwood x DECAYCD). This produces carbon estimates that align with the EPA NGHGI LULUCF standing dead pool. + +```python +import pyfia + +db = pyfia.FIA("georgia.duckdb") +db.clip_by_state("GA") +db.clip_most_recent(eval_type="VOL") + +# Above-ground standing dead carbon +result = pyfia.standing_dead(db, pool="ag") + +# Standing dead carbon by decay class +by_decay = pyfia.standing_dead(db, pool="ag", grp_by="DECAYCD") +``` + +## Function Reference + +::: pyfia.standing_dead + options: + show_root_heading: true + show_source: true + +## Carbon Pools + +| Pool | Description | Method | +|------|-------------|--------| +| `"ag"` | Above-ground (default) | NSVB pipeline with REF_TREE_DECAY_PROP reductions + S10b dead carbon fractions | +| `"bg"` | Below-ground (coarse roots) | Bridge to FIADB `TREE.CARBON_BG` (same as live tree) | +| `"total"` | AG + BG | NSVB dead above-ground + FIADB below-ground bridge | + +## Decay-Class Reductions + +Standing dead trees lose mass through decomposition. The FIADB `REF_TREE_DECAY_PROP` table provides three multiplicative reduction factors applied to each gross NSVB component by hardwood/softwood classification and decay class (1-5): + +| Factor | Applied to | Description | +|--------|-----------|-------------| +| `DENSITY_PROP` | Stem wood | Fraction of wood biomass remaining after density loss | +| `BARK_LOSS_PROP` | Stem bark | Fraction of bark biomass remaining | +| `BRANCH_LOSS_PROP` | Branches | Fraction of branch biomass remaining | + +### Reduction Factors by Decay Class + +| Decay Class | Description | HW Density | HW Bark | HW Branch | SW Density | SW Bark | SW Branch | +|:-----------:|-------------|:----------:|:-------:|:---------:|:----------:|:-------:|:---------:| +| 1 | All limbs present, intact | 0.99 | 1.00 | 1.00 | 0.97 | 1.00 | 1.00 | +| 2 | Few limbs, no fine branches | 0.80 | 0.80 | 0.50 | 1.00 | 0.80 | 0.50 | +| 3 | Limb stubs only, top broken | 0.54 | 0.50 | 0.10 | 0.92 | 0.50 | 0.10 | +| 4 | Few stubs, top broken | 0.43 | 0.20 | 0.00 | 0.55 | 0.20 | 0.00 | +| 5 | No limbs, <20% bark | 0.43 | 0.00 | 0.00 | 0.55 | 0.00 | 0.00 | + +Per FIADB User Guide v9.1 Appendix K, `TREE.CULL` is **not** applied to standing dead tree biomass. The decay reductions above are the only mass adjustments. + +## Dead Carbon Fractions (S10b) + +Unlike live trees (which use per-species S10a fractions), dead tree carbon fractions come from S10b and vary only by hardwood/softwood and decay class: + +| Decay Class | Hardwood | Softwood | +|:-----------:|:--------:|:--------:| +| 1 | 0.470 | 0.501 | +| 2 | 0.473 | 0.504 | +| 3 | 0.481 | 0.506 | +| 4 | 0.480 | 0.520 | +| 5 | 0.472 | 0.527 | + +## Population Filter + +The standing-dead population is automatically filtered as: + +- `STATUSCD = 2` (dead tree) +- `STANDING_DEAD_CD = 1` (standing, not downed) +- `DECAYCD IS NOT NULL` (required for the decay-proportion lookup) +- `DIA >= 1.0` (NSVB floor) + +Trees with `STANDING_DEAD_CD = 0` (downed dead) belong to the down dead wood pool and are excluded. + +## Broken-Top Corrections + +Approximately 75% of standing dead trees have broken tops (`ACTUALHT < HT`). The pipeline applies two adjustments for these trees per FIADB User Guide v9.1 Appendix K: + +| Adjustment | Component | Formula | +|-----------|-----------|---------| +| Crown proportion | Branch biomass | `Broken_crn_prop = max(0, (ACTUALHT - (1 - CRprop_HT) * HT) / (CRprop_HT * HT))` | +| Volume ratio | Wood & bark | `(ACTUALHT / HT) ^ (2/3)` — paraboloid taper approximation | + +The mean intact crown ratio (`CR_MEAN`) is looked up from Table S11 (`REF_TREE_STND_DEAD_CR_PROP`) by Bailey ecoregion province and hardwood/softwood classification. When the province is unknown, the UNDEFINED fallback (softwood: 46.8%, hardwood: 38.0%) is used. + +The volume-ratio adjustment uses a paraboloid taper exponent (2/3) rather than FIADB's Model 6 (Schumacher-Hall) volume-ratio model, which is not implemented. This approximation accounts for the fact that the wider lower stem contains a disproportionately large fraction of total stem volume. + +## Technical Notes + +## Examples + +### Standing Dead Carbon Per Acre + +```python +result = pyfia.standing_dead(db, pool="ag") +print(f"SD Carbon: {result['CARBON_ACRE'][0]:.2f} tons/acre") +``` + +### Carbon by Decay Class + +```python +result = pyfia.standing_dead( + db, + pool="ag", + grp_by="DECAYCD", +) +for row in result.iter_rows(named=True): + print(f"Decay {row['DECAYCD']}: {row['CARBON_ACRE']:.3f} tons/acre") +``` + +### Carbon by Species + +```python +result = pyfia.standing_dead(db, pool="ag", by_species=True) +result = pyfia.join_species_names(result, db) +print(result.sort("CARBON_ACRE", descending=True).head(10)) +``` + +### Large Snag Carbon by Ownership + +```python +result = pyfia.standing_dead( + db, + pool="ag", + grp_by="OWNGRPCD", + tree_domain="DIA >= 20.0", + totals=True, +) +print(result) +``` + +### Total Standing Dead Carbon with Variance + +```python +result = pyfia.standing_dead( + db, + pool="total", + land_type="forest", + variance=True, + totals=True, +) +print(f"SD Carbon: {result['CARBON_TOTAL'][0]:,.0f} +/- " + f"{result['CARBON_TOTAL_SE'][0]:,.0f} tons") +``` diff --git a/docs/getting-started.md b/docs/getting-started.md index e188f820..19a605b3 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -124,6 +124,24 @@ volume_with_names = pyfia.join_species_names(volume, db) print(volume_with_names) ``` +### 5. Carbon Estimation (NSVB) + +For NGHGI-aligned carbon accounting, use `live_tree()` and `standing_dead()` — these recompute biomass from scratch using the NSVB framework with species-specific carbon fractions: + +```python +import pyfia + +# Above-ground live tree carbon +live_c = pyfia.live_tree(db, pool="ag") +print(f"Live AG carbon: {live_c['CARBON_ACRE'][0]:.2f} tons/acre") + +# Standing dead carbon by decay class +sd_c = pyfia.standing_dead(db, pool="ag", grp_by="DECAYCD") +print(sd_c) +``` + +See [live_tree()](api/live_tree.md) and [standing_dead()](api/standing_dead.md) for details. + ## Complete Example ```python diff --git a/docs/index.md b/docs/index.md index 58aba996..724d96eb 100644 --- a/docs/index.md +++ b/docs/index.md @@ -106,6 +106,8 @@ with FIA(db_path) as db: | [`volume()`](api/volume.md) | Estimate standing tree volume | | [`tpa()`](api/tpa.md) | Calculate trees per acre and basal area | | [`biomass()`](api/biomass.md) | Estimate tree biomass and carbon | +| [`live_tree()`](api/live_tree.md) | NSVB live tree carbon (species-specific fractions) | +| [`standing_dead()`](api/standing_dead.md) | NSVB standing dead carbon (decay-class reductions) | | [`site_index()`](api/site_index.md) | Estimate area-weighted mean site index | | [`mortality()`](api/mortality.md) | Calculate annual mortality rates | | [`growth()`](api/growth.md) | Estimate annual growth | diff --git a/mkdocs.yml b/mkdocs.yml index 150bc693..18196246 100755 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -115,6 +115,8 @@ nav: - Volume: api/volume.md - Trees Per Acre: api/tpa.md - Biomass: api/biomass.md + - Live Tree Carbon: api/live_tree.md + - Standing Dead Carbon: api/standing_dead.md - Site Index: api/site_index.md - Mortality: api/mortality.md - Growth: api/growth.md diff --git a/pyproject.toml b/pyproject.toml index 0aa62a7a..b71799c7 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -103,6 +103,10 @@ package-dir = {"" = "src"} where = ["src"] include = ["pyfia*"] +[tool.setuptools.package-data] +# Vendored NSVB coefficient tables (GTR-WO-104 Supp1) — load via importlib.resources +"pyfia.carbon.nsvb.data" = ["*.csv", "*.md"] + [tool.ruff] line-length = 88 target-version = "py311" diff --git a/scripts/nghgi/__init__.py b/scripts/nghgi/__init__.py new file mode 100644 index 00000000..286c5fb5 --- /dev/null +++ b/scripts/nghgi/__init__.py @@ -0,0 +1,18 @@ +"""NGHGI report reproduction utilities (not part of the pyfia public API). + +These modules live under ``scripts/`` rather than ``pyfia/`` because they +exist to reproduce EPA Chapter 6 / Annex 3.13 forest carbon tables, not +to provide new estimation capability. The pyfia carbon estimators +themselves stay in ``pyfia.carbon``. + +Stages +------ +- ``stage_a`` — reproduce EPA Table 6-10 forest carbon stocks +- ``stage_b`` — state-level flux vs EPA Annex 3.13 Table A-208 +- ``multi_year`` — multi-year stock comparison 2019-2023 +- ``dead_wood_diagnostic`` — isolate the NSVB-vs-FIADB standing-dead gap +- ``_compile`` — pool aggregation helper used by the above + +All scripts read state DuckDB files from a directory configured via +``--db-dir`` or the ``PYFIA_FIADB_DIR`` environment variable. +""" diff --git a/scripts/nghgi/_compile.py b/scripts/nghgi/_compile.py new file mode 100644 index 00000000..72290a96 --- /dev/null +++ b/scripts/nghgi/_compile.py @@ -0,0 +1,383 @@ +""" +NGHGI pool aggregation — EPA Chapter 6 LULUCF. + +The EPA NGHGI report (Inventory of U.S. GHG Emissions and Sinks) reports forest +ecosystem carbon under six pools whose decomposition does NOT line up 1:1 with +pyFIA's pool estimators: + + EPA pool pyFIA module(s) + -------- --------------- + Aboveground Biomass live_tree(pool='ag') + understory(pool='ag') + Belowground Biomass live_tree(pool='bg') + understory(pool='bg') + Dead Wood standing_dead(pool='total') + downed_dead + Litter litter + Soil (Mineral) soil_organic [Domke 2017 mineral soil model] + Soil (Organic) not reproducible [Histosols, IPCC defaults] + +This module is a *report-reproduction helper*. It belongs under ``scripts/``, +not in the pyfia public API, because it bakes in EPA Chapter 6 column +semantics and lives only to validate pyfia's carbon estimators against the +published NGHGI tables. + +Units +----- +pyFIA pool estimators return short tons. EPA Table 6-10 reports MMT C +(million metric tons of carbon). Conversion: 1 short ton = 0.907185 metric +tons. + +References +---------- +- USEPA (2024). Chapter 6 LULUCF, Tables 6-9 and 6-10. +- Westfall et al. (2023) GTR-WO-104 (NSVB). +- Domke et al. (2013, 2016, 2017) for downed dead, litter, soil. +""" + +from __future__ import annotations + +from pathlib import Path +from typing import Iterable + +import polars as pl + +from pyfia.core import FIA + +SHORT_TONS_PER_METRIC_TON = 1.0 / 0.907185 +METRIC_TONS_PER_SHORT_TON = 0.907185 +MMT_C_PER_SHORT_TON = METRIC_TONS_PER_SHORT_TON / 1_000_000 + +_DATA_DIR = Path(__file__).parent / "data" + + +EPA_POOLS = ( + "AGB", + "BGB", + "DEAD_WOOD", + "LITTER", + "SOIL_MINERAL", + "SOIL_ORGANIC", + "FOREST_ECOSYSTEM", +) + + +def _short_tons_to_mmt_c(short_tons: float) -> float: + """Convert short tons of carbon to million metric tons (MMT C).""" + return short_tons * MMT_C_PER_SHORT_TON + + +def compile_state_stocks( + db: str | FIA, + *, + evalid: int | None = None, + state_label: str | None = None, + mode: str = "fiadb", +) -> pl.DataFrame: + """ + Compute EPA-pool-aggregated carbon stocks for one state's evaluation. + + Runs six carbon pool estimators against the supplied EVALID, then + re-aggregates them into the six EPA NGHGI pools (plus Forest Ecosystem + total) used in EPA Chapter 6 Table 6-10. + + Parameters + ---------- + db : str or FIA + Path or open FIA instance. If a path is given, the connection is + opened, used, and closed by this function. + evalid : int, optional + EVALID to clip to. If None, the caller is expected to have already + clipped the FIA instance. + state_label : str, optional + Free-form label propagated into the output (e.g. "Georgia", + state postal code, FIPS). Useful when stacking multiple states. + mode : {'fiadb', 'nsvb'}, default 'fiadb' + Tree-level carbon source. ``'fiadb'`` reads FIADB-stored + ``TREE.CARBON_AG`` and ``TREE.CARBON_BG`` via + ``pyfia.estimation.estimators.carbon_pool`` — this is the path + EPA's report ultimately consumes and is the correct choice for + report reproduction (especially for pre-2023 EVALIDs which were + compiled before the NSVB framework transition). ``'nsvb'`` uses + pyFIA's NSVB-recomputation pool estimators + (``pyfia.carbon.live_tree`` / ``standing_dead``); use this only + for methodology comparison or post-2023 inventories. + + Returns + ------- + pl.DataFrame + One row per EPA pool (AGB, BGB, DEAD_WOOD, LITTER, SOIL_MINERAL, + FOREST_ECOSYSTEM). Columns: + + - STATE: state_label (if provided) + - EPA_POOL: EPA pool name + - STOCK_SHORT_TONS: population total in short tons of C + - STOCK_MMT_C: same total converted to million metric tons C + - STOCK_PER_ACRE_ST: short tons per acre + - N_PLOTS: plot count from the underlying estimator(s) + - SOURCE: which estimator(s) contributed + + SOIL_ORGANIC is NOT included — it is not reproducible from FIADB + attributes alone (EPA uses IPCC defaults applied to Histosol plots). + Callers comparing to EPA totals must add it separately from + ``nghgi_2024_table_6_10.csv``. + + Notes + ----- + Forest-remaining-forest only is approximated via ``land_type='forest'`` + in each estimator. This includes both forest-remaining-forest and + land-converted-to-forest in EPA's CRT taxonomy. EPA Section 6.2 + excludes the latter; reproducing that split is Phase 2 work. + """ + from pyfia.carbon.downed_dead import downed_dead + from pyfia.carbon.litter import litter + from pyfia.carbon.soil_organic import soil_organic + from pyfia.carbon.understory import understory + from pyfia.estimation.estimators.carbon_pools import carbon_pool + + mode = mode.lower() + if mode not in ("fiadb", "nsvb"): + raise ValueError(f"mode must be 'fiadb' or 'nsvb', got {mode!r}") + + if isinstance(db, str): + owns = True + fia = FIA(db) + fia.__enter__() + else: + owns = False + fia = db + + try: + if evalid is not None: + fia.clip_by_evalid(evalid) + + common = dict(land_type="forest", totals=True, variance=False) + + if mode == "fiadb": + live_ag = carbon_pool(fia, pool="ag", tree_type="live", **common) + live_bg = carbon_pool(fia, pool="bg", tree_type="live", **common) + std_dead = carbon_pool(fia, pool="total", tree_type="dead", **common) + tree_source = "FIADB CARBON_AG/BG via carbon_pool" + else: # mode == "nsvb" + from pyfia.carbon.live_tree import live_tree + from pyfia.carbon.standing_dead import standing_dead + + live_ag = live_tree(fia, pool="ag", **common) + live_bg = live_tree(fia, pool="bg", **common) + std_dead = standing_dead(fia, pool="total", **common) + tree_source = "NSVB recomputation" + + und_ag = understory(fia, pool="ag", **common) + und_bg = understory(fia, pool="bg", **common) + dwn_dead = downed_dead(fia, **common) + lit = litter(fia, **common) + soil = soil_organic(fia, **common) + + def _stock(df: pl.DataFrame, label: str) -> tuple[float, float, int]: + """Extract (total, per-acre, n_plots) from a pool result. + + Empty results from a pool estimator are reported, not silently + coerced to zero — this prevents an upstream filter that removes + all plots from masquerading as a true zero stock. + """ + if df is None or len(df) == 0: + raise ValueError( + f"Pool estimator {label!r} returned an empty result " + "(no plots after stratification). The EPA pool rollup " + "would be misleading; fix the filter or EVALID." + ) + total = ( + float(df["CARBON_TOTAL"][0]) if "CARBON_TOTAL" in df.columns else 0.0 + ) + acre = float(df["CARBON_ACRE"][0]) if "CARBON_ACRE" in df.columns else 0.0 + n = int(df["N_PLOTS"][0]) if "N_PLOTS" in df.columns else 0 + return total, acre, n + + lt_ag_t, lt_ag_a, n_lt = _stock(live_ag, "live_tree(ag)") + lt_bg_t, lt_bg_a, _ = _stock(live_bg, "live_tree(bg)") + sd_t, sd_a, n_sd = _stock(std_dead, "standing_dead") + u_ag_t, u_ag_a, n_u = _stock(und_ag, "understory(ag)") + u_bg_t, u_bg_a, _ = _stock(und_bg, "understory(bg)") + dd_t, dd_a, n_dd = _stock(dwn_dead, "downed_dead") + li_t, li_a, n_li = _stock(lit, "litter") + sm_t, sm_a, n_sm = _stock(soil, "soil_organic") + + agb_t = lt_ag_t + u_ag_t + bgb_t = lt_bg_t + u_bg_t + dw_t = sd_t + dd_t + eco_t = agb_t + bgb_t + dw_t + li_t + sm_t + + agb_a = lt_ag_a + u_ag_a + bgb_a = lt_bg_a + u_bg_a + dw_a = sd_a + dd_a + eco_a = agb_a + bgb_a + dw_a + li_a + sm_a + + rows = [ + ( + "AGB", + agb_t, + agb_a, + max(n_lt, n_u), + f"live[{tree_source}](ag) + understory(ag)", + ), + ( + "BGB", + bgb_t, + bgb_a, + max(n_lt, n_u), + f"live[{tree_source}](bg) + understory(bg)", + ), + ( + "DEAD_WOOD", + dw_t, + dw_a, + max(n_sd, n_dd), + f"standing_dead[{tree_source}](total) + downed_dead", + ), + ("LITTER", li_t, li_a, n_li, "litter"), + ("SOIL_MINERAL", sm_t, sm_a, n_sm, "soil_organic [Domke 2017 mineral]"), + ( + "FOREST_ECOSYSTEM", + eco_t, + eco_a, + max(n_lt, n_sm), + "AGB+BGB+DEAD_WOOD+LITTER+SOIL_MINERAL", + ), + ] + + df = pl.DataFrame( + { + "EPA_POOL": [r[0] for r in rows], + "STOCK_SHORT_TONS": [r[1] for r in rows], + "STOCK_MMT_C": [_short_tons_to_mmt_c(r[1]) for r in rows], + "STOCK_PER_ACRE_ST": [r[2] for r in rows], + "N_PLOTS": [r[3] for r in rows], + "SOURCE": [r[4] for r in rows], + } + ) + + if state_label is not None: + df = df.with_columns(pl.lit(state_label).alias("STATE")) + df = df.select(["STATE", *[c for c in df.columns if c != "STATE"]]) + + return df + + finally: + if owns: + fia.__exit__(None, None, None) + + +def compile_conus_stocks( + state_evalids: Iterable[tuple[str, str, int]], +) -> pl.DataFrame: + """ + Compile per-state EPA-pool stocks across multiple state databases. + + Iterates over state databases, runs :func:`compile_state_stocks` on + each, and concatenates results. Caller is responsible for picking the + correct EVALID per state (e.g. most recent annual VOL evaluation + covering the report's reference year). + + Parameters + ---------- + state_evalids : iterable of (state_label, db_path, evalid) + One tuple per state. ``state_label`` is propagated into the + ``STATE`` column. + + Returns + ------- + pl.DataFrame + Long-format frame with columns ``STATE``, ``EPA_POOL``, + ``STOCK_SHORT_TONS``, ``STOCK_MMT_C``, ``STOCK_PER_ACRE_ST``, + ``N_PLOTS``, ``SOURCE``. Use ``group_by('EPA_POOL').agg(...)`` + downstream to roll up to CONUS totals. + """ + frames: list[pl.DataFrame] = [] + for state_label, db_path, evalid in state_evalids: + df = compile_state_stocks(db_path, evalid=evalid, state_label=state_label) + frames.append(df) + if not frames: + return pl.DataFrame() + return pl.concat(frames, how="vertical_relaxed") + + +def load_published_targets(year: int = 2022) -> pl.DataFrame: + """ + Load EPA Chapter 6 Table 6-10 published carbon stocks for a given year. + + Returns the published per-pool MMT C target values for comparison with + pyFIA-computed totals. Available years: 1990, 2005, 2019, 2020, 2021, + 2022, 2023. + + Parameters + ---------- + year : int + Reporting year matching a column in EPA Table 6-10. + + Returns + ------- + pl.DataFrame + Columns: EPA_POOL, YEAR, STOCK_MMT_C, NOTE. + """ + df = pl.read_csv(_DATA_DIR / "nghgi_2024_table_6_10.csv") + df = df.rename( + { + "epa_pool": "EPA_POOL", + "year": "YEAR", + "stock_mmt_c": "STOCK_MMT_C", + "note": "NOTE", + } + ) + return df.filter(pl.col("YEAR") == year) + + +def load_state_flux_targets() -> pl.DataFrame: + """Load EPA Annex 3.13 Table A-208 state-level flux (2022).""" + return pl.read_csv(_DATA_DIR / "nghgi_2024_table_a_208_state_flux_2022.csv") + + +def compare_to_published( + pyfia_stocks: pl.DataFrame, + *, + year: int = 2022, +) -> pl.DataFrame: + """ + Side-by-side compare a pyFIA stocks rollup to the EPA published targets. + + Parameters + ---------- + pyfia_stocks : pl.DataFrame + Output of :func:`compile_state_stocks` (single state) or a CONUS + rollup with one row per ``EPA_POOL`` and a ``STOCK_MMT_C`` column. + year : int + EPA reporting year to compare against. + + Returns + ------- + pl.DataFrame + Columns: EPA_POOL, PYFIA_MMT_C, EPA_MMT_C, ABS_DIFF_MMT_C, + PCT_DIFF. Useful for quick eyeballing of where the rollup + agrees vs diverges. + """ + if "STATE" in pyfia_stocks.columns: + rollup = ( + pyfia_stocks.group_by("EPA_POOL") + .agg(pl.col("STOCK_MMT_C").sum()) + .rename({"STOCK_MMT_C": "PYFIA_MMT_C"}) + ) + else: + rollup = pyfia_stocks.select(["EPA_POOL", "STOCK_MMT_C"]).rename( + {"STOCK_MMT_C": "PYFIA_MMT_C"} + ) + + targets = ( + load_published_targets(year=year) + .select(["EPA_POOL", "STOCK_MMT_C"]) + .rename({"STOCK_MMT_C": "EPA_MMT_C"}) + ) + + merged = rollup.join(targets, on="EPA_POOL", how="full", coalesce=True) + merged = merged.with_columns( + (pl.col("PYFIA_MMT_C") - pl.col("EPA_MMT_C")).alias("ABS_DIFF_MMT_C"), + ( + 100.0 * (pl.col("PYFIA_MMT_C") - pl.col("EPA_MMT_C")) / pl.col("EPA_MMT_C") + ).alias("PCT_DIFF"), + ) + return merged.sort("EPA_POOL") diff --git a/scripts/nghgi/_paths.py b/scripts/nghgi/_paths.py new file mode 100644 index 00000000..38b0930b --- /dev/null +++ b/scripts/nghgi/_paths.py @@ -0,0 +1,35 @@ +"""Shared path resolution for NGHGI reproduction scripts.""" + +from __future__ import annotations + +import os +from pathlib import Path + + +def resolve_db_dir(cli_value: str | os.PathLike | None) -> Path: + """Resolve the state-DB directory from CLI, env, or default. + + Precedence: + 1. ``cli_value`` (``--db-dir`` argument), if not None + 2. ``PYFIA_FIADB_DIR`` environment variable, if set + 3. Default: ``./data/fiadb`` relative to the current working directory + + Raises ``FileNotFoundError`` with a clear message if the resolved path + does not exist. + """ + if cli_value is not None: + path = Path(cli_value).expanduser().resolve() + source = "--db-dir" + elif os.environ.get("PYFIA_FIADB_DIR"): + path = Path(os.environ["PYFIA_FIADB_DIR"]).expanduser().resolve() + source = "$PYFIA_FIADB_DIR" + else: + path = (Path.cwd() / "data" / "fiadb").resolve() + source = "default (./data/fiadb)" + + if not path.is_dir(): + raise FileNotFoundError( + f"FIA state-DB directory not found: {path} (resolved from {source}). " + "Pass --db-dir or set PYFIA_FIADB_DIR to override." + ) + return path diff --git a/scripts/nghgi/data/nghgi_2024_table_6_10.csv b/scripts/nghgi/data/nghgi_2024_table_6_10.csv new file mode 100644 index 00000000..b259c3d0 --- /dev/null +++ b/scripts/nghgi/data/nghgi_2024_table_6_10.csv @@ -0,0 +1,57 @@ +epa_pool,year,stock_mmt_c,note +Forest_Area_kha,1990,283500,Forest land area in 1000 hectares; CONUS+AK+HI+territories +Forest_Area_kha,2005,282521, +Forest_Area_kha,2019,281137, +Forest_Area_kha,2020,281779, +Forest_Area_kha,2021,281780, +Forest_Area_kha,2022,281752, +Forest_Area_kha,2023,281725, +AGB,1990,12739,Aboveground Biomass: live tree AG + understory AG +AGB,2005,15122, +AGB,2019,17199, +AGB,2020,17340, +AGB,2021,17483, +AGB,2022,17622, +AGB,2023,17757, +BGB,1990,2255,Belowground Biomass: live tree BG + understory BG (10%) +BGB,2005,2718, +BGB,2019,3124, +BGB,2020,3151, +BGB,2021,3179, +BGB,2022,3207, +BGB,2023,3233, +DEAD_WOOD,1990,1977,Dead Wood: standing dead + downed dead +DEAD_WOOD,2005,2521, +DEAD_WOOD,2019,3038, +DEAD_WOOD,2020,3074, +DEAD_WOOD,2021,3111, +DEAD_WOOD,2022,3148, +DEAD_WOOD,2023,3184, +LITTER,1990,3789,Litter and duff (Domke 2016 model) +LITTER,2005,3794, +LITTER,2019,3775, +LITTER,2020,3767, +LITTER,2021,3768, +LITTER,2022,3768, +LITTER,2023,3761, +SOIL_MINERAL,1990,28407,Mineral soil to 1m depth (Domke 2017 model) +SOIL_MINERAL,2005,28401, +SOIL_MINERAL,2019,28400, +SOIL_MINERAL,2020,28400, +SOIL_MINERAL,2021,28401, +SOIL_MINERAL,2022,28401, +SOIL_MINERAL,2023,28401, +SOIL_ORGANIC,1990,5976,"Histosols/organic soils; not reproducible from FIADB attributes alone (IPCC defaults)" +SOIL_ORGANIC,2005,5981, +SOIL_ORGANIC,2019,5983, +SOIL_ORGANIC,2020,5983, +SOIL_ORGANIC,2021,5983, +SOIL_ORGANIC,2022,5983, +SOIL_ORGANIC,2023,5983, +FOREST_ECOSYSTEM,1990,55142,Sum of AGB+BGB+Dead Wood+Litter+Soil(Mineral)+Soil(Organic) +FOREST_ECOSYSTEM,2005,58536, +FOREST_ECOSYSTEM,2019,61519, +FOREST_ECOSYSTEM,2020,61717, +FOREST_ECOSYSTEM,2021,61926, +FOREST_ECOSYSTEM,2022,62130, +FOREST_ECOSYSTEM,2023,62320, diff --git a/scripts/nghgi/data/nghgi_2024_table_a_208_state_flux_2022.csv b/scripts/nghgi/data/nghgi_2024_table_a_208_state_flux_2022.csv new file mode 100644 index 00000000..25e871ac --- /dev/null +++ b/scripts/nghgi/data/nghgi_2024_table_a_208_state_flux_2022.csv @@ -0,0 +1,57 @@ +state,state_name,flux_mmt_c_2022,lower_bound_mmt_c,lower_bound_pct,upper_bound_mmt_c,upper_bound_pct +AL,Alabama,-13.5,-15.3,-14,-11.6,14 +AK,Alaska,7.1,0.0,-101,14.2,101 +AZ,Arizona,0.6,0.2,-70,1.0,70 +AR,Arkansas,-9.3,-10.9,-17,-7.8,17 +CA,California,-8.0,-16.0,-100,0.0,100 +CO,Colorado,2.3,-6.3,-375,10.8,375 +CT,Connecticut,-0.8,-1.1,-35,-0.5,35 +DE,Delaware,-0.0,-0.1,-105,0.0,105 +FL,Florida,-3.4,-4.0,-20,-2.7,20 +GA,Georgia,-8.8,-9.3,-6,-8.4,6 +HI,Hawaii,0.9,-5.3,-490,3.5,490 +ID,Idaho,-0.1,-3.7,-2570,3.4,2570 +IL,Illinois,-2.0,-3.0,-51,-1.0,51 +IN,Indiana,-3.0,-4.6,-53,-1.4,53 +IA,Iowa,-1.0,-1.3,-32,-0.7,32 +KS,Kansas,-0.8,-1.2,-53,-0.4,53 +KY,Kentucky,-6.2,-7.8,-25,-4.6,25 +LA,Louisiana,-7.0,-7.5,-7,-6.5,7 +ME,Maine,-5.1,-8.1,-59,-2.1,59 +MD,Maryland,-1.4,-1.9,-39,-0.8,39 +MA,Massachusetts,-1.4,-1.7,-27,-1.0,27 +MI,Michigan,-5.9,-9.5,-60,-2.4,60 +MN,Minnesota,-5.3,-7.6,-43,-3.0,43 +MS,Mississippi,-15.9,-18.7,-18,-13.0,18 +MO,Missouri,-4.8,-7.4,-54,-2.2,54 +MT,Montana,1.4,-6.6,-588,9.4,588 +NE,Nebraska,-0.3,-0.3,-19,-0.2,19 +NV,Nevada,-0.1,-0.3,-460,0.2,460 +NH,New Hampshire,-1.7,-2.3,-36,-1.1,36 +NJ,New Jersey,-0.9,-1.0,-10,-0.8,10 +NM,New Mexico,0.3,-1.7,-713,2.2,713 +NY,New York,-8.9,-11.2,-26,-6.6,26 +NC,North Carolina,-8.9,-10.1,-14,-7.6,14 +ND,North Dakota,-0.1,-0.2,-159,0.0,159 +OH,Ohio,-3.9,-6.0,-53,-1.8,53 +OK,Oklahoma,-2.8,-3.4,-22,-2.2,22 +OR,Oregon,-10.7,-12.8,-20,-8.6,20 +PA,Pennsylvania,-6.2,-10.6,-72,-1.7,72 +RI,Rhode Island,-0.1,-0.3,-115,0.0,115 +SC,South Carolina,-4.3,-4.8,-13,-3.7,13 +SD,South Dakota,0.3,0.0,-107,0.6,107 +TN,Tennessee,-7.8,-9.3,-19,-6.3,19 +TX,Texas,-10.7,-11.2,-5,-10.2,5 +UT,Utah,0.4,-1.0,-337,1.8,337 +VT,Vermont,-1.7,-2.4,-43,-0.9,43 +VA,Virginia,-9.8,-12.5,-28,-7.1,28 +WA,Washington,-4.6,-9.2,-100,0.0,100 +WV,West Virginia,-7.3,-9.1,-24,-5.6,24 +WI,Wisconsin,-6.5,-6.9,-7,-6.0,7 +WY,Wyoming,0.2,-0.4,-319,0.8,319 +AS,American Samoa,-0.0,-0.2,-656,0.1,656 +GU,Guam,-0.0,-0.3,-584,0.2,584 +MP,Northern Mariana Islands,-0.0,-0.2,-673,0.2,673 +PR,Puerto Rico,-0.3,-2.8,-780,2.2,780 +VI,US Virgin Islands,-0.0,-0.2,-787,0.1,787 +TOTAL,Total,-189.3,-209.9,-11,-168.8,11 diff --git a/scripts/nghgi/dead_wood_diagnostic.py b/scripts/nghgi/dead_wood_diagnostic.py new file mode 100644 index 00000000..d8665bbc --- /dev/null +++ b/scripts/nghgi/dead_wood_diagnostic.py @@ -0,0 +1,205 @@ +""" +Dead Wood diagnostic — isolate whether the +38% over-count vs EPA Chapter 6 +comes from standing dead, downed dead, or both. + +Compares three estimates per state, all routed through pyfia's identical +stratification pipeline (EVALID, land_type='forest', BaseEstimator path) so +the only variable is the per-tree/per-condition carbon source: + + A) `carbon.standing_dead(pool='total')` — NSVB recomputation per tree + (Westfall 2023) with REF_TREE_DECAY_PROP reductions and Appendix K + broken-top corrections. AG via NSVB; BG via bridge to TREE.CARBON_BG. + + B) `estimation.carbon_pool(pool='total', tree_type='dead')` — uses + FIADB-stored TREE.CARBON_AG + TREE.CARBON_BG where STATUSCD=2. + This is the path EPA's report ultimately consumes. + + C) `carbon.downed_dead` — uses pre-computed COND.CARBON_DOWN_DEAD + (Domke 2013 model). Shared between pyfia and EPA, no recomputation. + +If A >> B, the standing-dead NSVB recomputation is the over-count source. +If A ≈ B, the issue is elsewhere (downed_dead or pool definition). + +Usage: + uv run python scripts/nghgi/dead_wood_diagnostic.py + uv run python scripts/nghgi/dead_wood_diagnostic.py --states GA,OR,TX,WA + uv run python scripts/nghgi/dead_wood_diagnostic.py --db-dir /path/to/fiadb + +Database directory resolution (in order): + 1. --db-dir CLI argument + 2. $PYFIA_FIADB_DIR environment variable + 3. ./data/fiadb (relative to current working directory) +""" + +from __future__ import annotations + +import argparse +import sys +import time +from pathlib import Path + +import polars as pl + +sys.path.insert(0, str(Path(__file__).parent)) + +from _paths import resolve_db_dir # noqa: E402 + +from pyfia import FIA # noqa: E402 +from pyfia.carbon.downed_dead import downed_dead # noqa: E402 +from pyfia.carbon.standing_dead import standing_dead # noqa: E402 +from pyfia.estimation.estimators.carbon_pools import carbon_pool # noqa: E402 + +DEFAULT_STATES = ["GA", "OR", "TX", "WA"] + +MMT_C_PER_SHORT_TON = 0.907185 / 1_000_000 + + +def _to_mmt(short_tons: float) -> float: + return short_tons * MMT_C_PER_SHORT_TON + + +def _select_2022_evalid(db: FIA) -> int: + evals = db.query( + """ + SELECT EVALID, END_INVYR + FROM POP_EVAL + WHERE EVAL_DESCR LIKE '%CURRENT AREA, CURRENT VOLUME%' + ORDER BY END_INVYR DESC + """ + ) + match = evals.filter(pl.col("END_INVYR") == 2022) + return int(match["EVALID"][0]) if not match.is_empty() else int(evals["EVALID"][0]) + + +def diagnose_state(state: str, *, db_dir: Path) -> dict: + db_path = db_dir / f"{state}.duckdb" + t0 = time.perf_counter() + + with FIA(str(db_path)) as db: + evalid = _select_2022_evalid(db) + db.clip_by_evalid(evalid) + + sd_nsvb = standing_dead(db, pool="total", land_type="forest", totals=True) + a_total_st = float(sd_nsvb["CARBON_TOTAL"][0]) if len(sd_nsvb) else 0.0 + a_n_plots = int(sd_nsvb["N_PLOTS"][0]) if len(sd_nsvb) else 0 + a_n_trees = int(sd_nsvb["N_TREES"][0]) if "N_TREES" in sd_nsvb.columns else 0 + + sd_fiadb = carbon_pool( + db, pool="total", tree_type="dead", land_type="forest", totals=True + ) + b_total_st = float(sd_fiadb["CARBON_TOTAL"][0]) if len(sd_fiadb) else 0.0 + b_n_plots = int(sd_fiadb["N_PLOTS"][0]) if "N_PLOTS" in sd_fiadb.columns else 0 + b_n_trees = int(sd_fiadb["N_TREES"][0]) if "N_TREES" in sd_fiadb.columns else 0 + + dd = downed_dead(db, land_type="forest", totals=True) + c_total_st = float(dd["CARBON_TOTAL"][0]) if len(dd) else 0.0 + + return { + "state": state, + "evalid": evalid, + "elapsed_s": time.perf_counter() - t0, + "A_standing_dead_nsvb_st": a_total_st, + "A_standing_dead_nsvb_mmt": _to_mmt(a_total_st), + "A_n_plots": a_n_plots, + "A_n_trees": a_n_trees, + "B_standing_dead_fiadb_st": b_total_st, + "B_standing_dead_fiadb_mmt": _to_mmt(b_total_st), + "B_n_plots": b_n_plots, + "B_n_trees": b_n_trees, + "C_downed_dead_mmt": _to_mmt(c_total_st), + "A_minus_B_mmt": _to_mmt(a_total_st - b_total_st), + "A_over_B_pct": ( + 100.0 * (a_total_st - b_total_st) / b_total_st + if b_total_st + else float("nan") + ), + } + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--states", + default=",".join(DEFAULT_STATES), + help=f"Comma-separated postal codes (default: {','.join(DEFAULT_STATES)})", + ) + parser.add_argument( + "--db-dir", + type=str, + default=None, + help="Directory containing per-state DuckDB files (overrides $PYFIA_FIADB_DIR).", + ) + args = parser.parse_args() + + try: + db_dir = resolve_db_dir(args.db_dir) + except FileNotFoundError as e: + print(f"ERROR: {e}", file=sys.stderr) + return 2 + + states = [s.strip().upper() for s in args.states.split(",") if s.strip()] + + print(f"\n=== Dead Wood diagnostic — {len(states)} states ===") + print(" A = standing_dead (NSVB recomputation, pyfia new path)") + print(" B = carbon_pool(pool=total, tree_type=dead) (FIADB CARBON_AG+BG)") + print(" C = downed_dead (Domke 2013, shared with EPA)") + print(f" State DB directory: {db_dir}\n") + + rows = [] + for st in states: + r = diagnose_state(st, db_dir=db_dir) + rows.append(r) + print( + f" {st}: EVALID={r['evalid']} " + f"A={r['A_standing_dead_nsvb_mmt']:.2f} " + f"B={r['B_standing_dead_fiadb_mmt']:.2f} " + f"A−B={r['A_minus_B_mmt']:+.2f} MMT C " + f"({r['A_over_B_pct']:+.1f}%) " + f"plotsA/B={r['A_n_plots']}/{r['B_n_plots']} " + f"treesA/B={r['A_n_trees']}/{r['B_n_trees']} " + f"({r['elapsed_s']:.1f}s)" + ) + + df = pl.DataFrame(rows) + + print("\n=== Per-state results (MMT C) ===") + summary = df.select( + [ + "state", + "A_standing_dead_nsvb_mmt", + "B_standing_dead_fiadb_mmt", + "A_minus_B_mmt", + "A_over_B_pct", + "C_downed_dead_mmt", + ] + ) + with pl.Config(tbl_rows=20, tbl_cols=10, fmt_str_lengths=40, tbl_width_chars=140): + print(summary) + + a_sum = df["A_standing_dead_nsvb_mmt"].sum() + b_sum = df["B_standing_dead_fiadb_mmt"].sum() + c_sum = df["C_downed_dead_mmt"].sum() + print(f"\n=== Across {len(states)} states ===") + print(f" A (NSVB recompute) standing dead total: {a_sum:.1f} MMT C") + print(f" B (FIADB CARBON_AG+BG) standing dead total: {b_sum:.1f} MMT C") + print( + f" Standing dead delta (A − B): {a_sum - b_sum:+.1f} MMT C " + f"({100.0 * (a_sum - b_sum) / b_sum:+.1f}%)" + ) + print(f" C (downed dead, shared method) total: {c_sum:.1f} MMT C") + print() + print("Interpretation:") + if abs(100.0 * (a_sum - b_sum) / b_sum) > 5: + print(" → standing_dead NSVB recomputation diverges materially from") + print(" FIADB-stored CARBON_AG+CARBON_BG. EPA's report is built on the") + print(" FIADB-stored values; reproducing the report requires using path B,") + print(" not pyfia's new NSVB recomputation.") + else: + print(" → standing_dead path is consistent with FIADB-stored values.") + print(" The +38% Dead Wood gap must come from downed_dead or pool") + print(" definition. Check downed_dead validation status.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/nghgi/multi_year.py b/scripts/nghgi/multi_year.py new file mode 100644 index 00000000..8b5290e2 --- /dev/null +++ b/scripts/nghgi/multi_year.py @@ -0,0 +1,308 @@ +""" +NGHGI multi-year validation — confirm Stage A accuracy across years +2019-2023 (the inventory years for which EPA Table 6-10 publishes +distinct stock values; 1990 and 2005 are modeled by EPA via age-class +projection and not directly reproducible from FIADB). + +For each target year, picks each CONUS-48 state's CURRENT AREA, CURRENT +VOLUME EVALID with the closest END_INVYR ≤ target year, runs the +EPA-pool-aggregated stock compilation in FIADB-fidelity mode, sums to +CONUS, and compares to the EPA Table 6-10 published value for that year. + +Usage: + uv run python scripts/nghgi/multi_year.py + uv run python scripts/nghgi/multi_year.py --years 2020,2022 + uv run python scripts/nghgi/multi_year.py --db-dir /path/to/fiadb + +Database directory resolution (in order): + 1. --db-dir CLI argument + 2. $PYFIA_FIADB_DIR environment variable + 3. ./data/fiadb (relative to current working directory) +""" + +from __future__ import annotations + +import argparse +import sys +import time +from pathlib import Path + +import polars as pl + +sys.path.insert(0, str(Path(__file__).parent)) + +from _compile import compile_state_stocks, load_published_targets # noqa: E402 +from _paths import resolve_db_dir # noqa: E402 + +from pyfia import FIA # noqa: E402 + +DEFAULT_YEARS = [2019, 2020, 2021, 2022, 2023] + +CONUS_48 = [ + "AL", + "AR", + "AZ", + "CA", + "CO", + "CT", + "DE", + "FL", + "GA", + "IA", + "ID", + "IL", + "IN", + "KS", + "KY", + "LA", + "MA", + "MD", + "ME", + "MI", + "MN", + "MO", + "MS", + "MT", + "NC", + "ND", + "NE", + "NH", + "NJ", + "NM", + "NV", + "NY", + "OH", + "OK", + "OR", + "PA", + "RI", + "SC", + "SD", + "TN", + "TX", + "UT", + "VA", + "VT", + "WA", + "WI", + "WV", + "WY", +] + + +def _select_evalid_for_year(db: FIA, target_year: int) -> tuple[int, int] | None: + """Return (evalid, end_invyr) for the closest annual VOL EVALID with + END_INVYR ≤ target_year. Returns None if no such EVALID exists.""" + evals = db.query( + """ + SELECT EVALID, END_INVYR + FROM POP_EVAL + WHERE EVAL_DESCR LIKE '%CURRENT AREA, CURRENT VOLUME%' + ORDER BY END_INVYR DESC + """ + ) + if evals.is_empty(): + return None + le_target = evals.filter(pl.col("END_INVYR") <= target_year) + if le_target.is_empty(): + row = evals.tail(1) + else: + row = le_target.head(1) + return int(row["EVALID"][0]), int(row["END_INVYR"][0]) + + +def run_state_year( + state: str, year: int, *, db_dir: Path +) -> tuple[pl.DataFrame, dict] | None: + db_path = db_dir / f"{state}.duckdb" + if not db_path.exists(): + return None + t0 = time.perf_counter() + with FIA(str(db_path)) as db: + sel = _select_evalid_for_year(db, year) + if sel is None: + return None + evalid, end_yr = sel + db.clip_by_evalid(evalid) + df = compile_state_stocks(db, state_label=state, mode="fiadb") + df = df.with_columns( + pl.lit(evalid).alias("EVALID"), + pl.lit(end_yr).alias("END_INVYR"), + pl.lit(year).alias("TARGET_YEAR"), + ) + return df, { + "state": state, + "year": year, + "evalid": evalid, + "end_invyr": end_yr, + "elapsed_s": time.perf_counter() - t0, + } + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--years", default=",".join(str(y) for y in DEFAULT_YEARS)) + parser.add_argument("--states", default=",".join(CONUS_48)) + parser.add_argument( + "--db-dir", + type=str, + default=None, + help="Directory containing per-state DuckDB files (overrides $PYFIA_FIADB_DIR).", + ) + args = parser.parse_args() + + try: + db_dir = resolve_db_dir(args.db_dir) + except FileNotFoundError as e: + print(f"ERROR: {e}", file=sys.stderr) + return 2 + + years = [int(y) for y in args.years.split(",") if y.strip()] + states = [s.strip().upper() for s in args.states.split(",") if s.strip()] + + print("\n=== NGHGI multi-year validation ===") + print(f" States: {len(states)} (CONUS-48 default)") + print(f" Years: {years}") + print(f" State DB directory: {db_dir}\n") + + all_rows: list[pl.DataFrame] = [] + skipped: list[tuple[str, int]] = [] + end_yr_summary: dict[int, dict[int, int]] = {y: {} for y in years} + + for year in years: + print(f"[Year {year}]") + t_year = time.perf_counter() + n_ok = 0 + for state in states: + res = run_state_year(state, year, db_dir=db_dir) + if res is None: + skipped.append((state, year)) + continue + df, meta = res + all_rows.append(df) + end_yr_summary[year].setdefault(meta["end_invyr"], 0) + end_yr_summary[year][meta["end_invyr"]] += 1 + n_ok += 1 + print( + f" {n_ok}/{len(states)} states ok in {time.perf_counter() - t_year:.1f}s. " + f"END_INVYR distribution: " + f"{dict(sorted(end_yr_summary[year].items(), reverse=True))}" + ) + + if not all_rows: + print("No successful runs — aborting.") + return 1 + + combined = pl.concat(all_rows, how="vertical_relaxed") + + rollup = ( + combined.group_by(["TARGET_YEAR", "EPA_POOL"]) + .agg(pl.col("STOCK_MMT_C").sum().alias("PYFIA_MMT_C")) + .sort(["TARGET_YEAR", "EPA_POOL"]) + ) + + epa_all = pl.concat( + [ + load_published_targets(year=y) + .select(["EPA_POOL", "YEAR", "STOCK_MMT_C"]) + .rename({"STOCK_MMT_C": "EPA_MMT_C", "YEAR": "TARGET_YEAR"}) + for y in years + ], + how="vertical_relaxed", + ) + + cmp = rollup.join(epa_all, on=["TARGET_YEAR", "EPA_POOL"], how="left") + cmp = cmp.with_columns( + (pl.col("PYFIA_MMT_C") - pl.col("EPA_MMT_C")).alias("ABS_DIFF"), + ( + 100.0 * (pl.col("PYFIA_MMT_C") - pl.col("EPA_MMT_C")) / pl.col("EPA_MMT_C") + ).alias("PCT_DIFF"), + ) + + # Synthesize a "Forest Ecosystem + EPA Soil Organic" row per year. + eco_rows = [] + for y in years: + py_eco = float( + cmp.filter( + (pl.col("TARGET_YEAR") == y) + & (pl.col("EPA_POOL") == "FOREST_ECOSYSTEM") + )["PYFIA_MMT_C"][0] + ) + epa_eco = float( + cmp.filter( + (pl.col("TARGET_YEAR") == y) + & (pl.col("EPA_POOL") == "FOREST_ECOSYSTEM") + )["EPA_MMT_C"][0] + ) + epa_so = float( + load_published_targets(y).filter(pl.col("EPA_POOL") == "SOIL_ORGANIC")[ + "STOCK_MMT_C" + ][0] + ) + py_eco_full = py_eco + epa_so + eco_rows.append( + { + "TARGET_YEAR": y, + "EPA_POOL": "FOREST_ECO_PLUS_SO", + "PYFIA_MMT_C": py_eco_full, + "EPA_MMT_C": epa_eco, + "ABS_DIFF": py_eco_full - epa_eco, + "PCT_DIFF": 100.0 * (py_eco_full - epa_eco) / epa_eco, + } + ) + cmp_full = pl.concat([cmp, pl.DataFrame(eco_rows)], how="vertical_relaxed").sort( + ["TARGET_YEAR", "EPA_POOL"] + ) + + print("\n=== Per-year, per-pool comparison (CONUS-48 vs EPA, MMT C) ===") + with pl.Config(tbl_rows=80, tbl_cols=10, fmt_str_lengths=30, tbl_width_chars=140): + print(cmp_full) + + print( + "\n=== Headline: Forest Ecosystem (CONUS-48 + EPA Soil Organic) vs EPA Total ===" + ) + headline = cmp_full.filter(pl.col("EPA_POOL") == "FOREST_ECO_PLUS_SO").select( + ["TARGET_YEAR", "PYFIA_MMT_C", "EPA_MMT_C", "ABS_DIFF", "PCT_DIFF"] + ) + with pl.Config(tbl_rows=20, tbl_cols=10, fmt_str_lengths=30, tbl_width_chars=120): + print(headline) + + print("\n=== Combined Dead Organic Matter (Dead Wood + Litter) vs EPA ===") + dom_rows = [] + for y in years: + py_dom = float( + cmp.filter( + (pl.col("TARGET_YEAR") == y) + & (pl.col("EPA_POOL").is_in(["DEAD_WOOD", "LITTER"])) + )["PYFIA_MMT_C"].sum() + ) + epa_dom = float( + cmp.filter( + (pl.col("TARGET_YEAR") == y) + & (pl.col("EPA_POOL").is_in(["DEAD_WOOD", "LITTER"])) + )["EPA_MMT_C"].sum() + ) + dom_rows.append( + { + "TARGET_YEAR": y, + "PYFIA_DOM": py_dom, + "EPA_DOM": epa_dom, + "ABS_DIFF": py_dom - epa_dom, + "PCT_DIFF": 100.0 * (py_dom - epa_dom) / epa_dom, + } + ) + with pl.Config(tbl_rows=20, tbl_cols=10, fmt_str_lengths=30, tbl_width_chars=120): + print(pl.DataFrame(dom_rows)) + + if skipped: + print(f"\nSkipped (no eligible EVALID): {len(skipped)} state-years") + for st, yr in skipped[:10]: + print(f" {st} {yr}") + if len(skipped) > 10: + print(f" ... and {len(skipped) - 10} more") + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/nghgi/stage_a.py b/scripts/nghgi/stage_a.py new file mode 100644 index 00000000..47f7ad1e --- /dev/null +++ b/scripts/nghgi/stage_a.py @@ -0,0 +1,277 @@ +""" +NGHGI Stage A — reproduce EPA Chapter 6 Table 6-10 forest ecosystem +carbon stocks across CONUS using pyfia. + +Iterates the 48 conterminous US state DuckDB files (and optionally Alaska), +runs the EPA-pool-aggregated stock compilation per state, sums to CONUS, and +prints a side-by-side comparison vs the published EPA Table 6-10. + +Usage: + uv run python scripts/nghgi/stage_a.py + uv run python scripts/nghgi/stage_a.py --include-ak + uv run python scripts/nghgi/stage_a.py --states GA,FL,SC + uv run python scripts/nghgi/stage_a.py --db-dir /path/to/fiadb + +Database directory resolution (in order): + 1. --db-dir CLI argument + 2. $PYFIA_FIADB_DIR environment variable + 3. ./data/fiadb (relative to current working directory) + +The output is intentionally "honest about what we're reproducing": each +state's selected EVALID and END_INVYR is logged so methodological +discrepancies vs the published 2022 target (which used the September 2023 +FIADB snapshot) are visible. +""" + +from __future__ import annotations + +import argparse +import sys +import time +from pathlib import Path + +import polars as pl + +# Allow `from _compile import ...` regardless of CWD: scripts in +# ``scripts/nghgi/`` add their own directory to sys.path automatically when +# invoked directly, but we add it explicitly for clarity. +sys.path.insert(0, str(Path(__file__).parent)) + +from _compile import compare_to_published, compile_state_stocks # noqa: E402 +from _paths import resolve_db_dir # noqa: E402 + +from pyfia import FIA # noqa: E402 + +# 48 conterminous US states. EPA's Section 6.2 reproduction target uses +# stock-difference here. AK uses a mix (coastal SD, interior gain-loss); +# HI / AS / GU / MP / PR / VI use gain-loss only and are out of scope for +# this reproduction. +CONUS_48 = [ + "AL", + "AR", + "AZ", + "CA", + "CO", + "CT", + "DE", + "FL", + "GA", + "IA", + "ID", + "IL", + "IN", + "KS", + "KY", + "LA", + "MA", + "MD", + "ME", + "MI", + "MN", + "MO", + "MS", + "MT", + "NC", + "ND", + "NE", + "NH", + "NJ", + "NM", + "NV", + "NY", + "OH", + "OK", + "OR", + "PA", + "RI", + "SC", + "SD", + "TN", + "TX", + "UT", + "VA", + "VT", + "WA", + "WI", + "WV", + "WY", +] + + +def _select_evalid_for_state(db: FIA, prefer_end_year: int | None = None) -> int: + """Pick the most recent annual VOL EVALID for the open state DB. + + If ``prefer_end_year`` is given, return the EVALID whose END_INVYR + matches if present, else fall back to most-recent. + """ + evals = db.query( + """ + SELECT EVALID, END_INVYR, EVAL_DESCR + FROM POP_EVAL + WHERE EVAL_DESCR LIKE '%CURRENT AREA, CURRENT VOLUME%' + ORDER BY END_INVYR DESC + """ + ) + if evals.is_empty(): + raise RuntimeError("No CURRENT AREA, CURRENT VOLUME EVALIDs found") + if prefer_end_year is not None: + match = evals.filter(pl.col("END_INVYR") == prefer_end_year) + if not match.is_empty(): + return int(match["EVALID"][0]) + return int(evals["EVALID"][0]) + + +def run_state( + state: str, + *, + db_dir: Path, + prefer_end_year: int | None = None, +) -> tuple[pl.DataFrame, dict]: + """Compile EPA-pool stocks for one state. Returns (df, meta).""" + db_path = db_dir / f"{state}.duckdb" + if not db_path.exists(): + raise FileNotFoundError(db_path) + + t0 = time.perf_counter() + with FIA(str(db_path)) as db: + evalid = _select_evalid_for_state(db, prefer_end_year=prefer_end_year) + db.clip_by_evalid(evalid) + end_yr = int( + db.query(f"SELECT END_INVYR FROM POP_EVAL WHERE EVALID = {evalid}")[ + "END_INVYR" + ][0] + ) + df = compile_state_stocks(db, state_label=state) + df = df.with_columns( + pl.lit(evalid).alias("EVALID"), + pl.lit(int(end_yr)).alias("END_INVYR"), + ) + meta = { + "state": state, + "evalid": evalid, + "end_invyr": int(end_yr), + "elapsed_s": time.perf_counter() - t0, + } + return df, meta + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--states", + type=str, + default=None, + help="Comma-separated state postal codes to run. Default: all CONUS-48.", + ) + parser.add_argument( + "--include-ak", + action="store_true", + help="Also include Alaska (mixes stock-difference and gain-loss methods in EPA report).", + ) + parser.add_argument( + "--prefer-end-year", + type=int, + default=2022, + help="Prefer EVALIDs with this END_INVYR if available (default 2022 to match EPA Chapter 6 reporting year).", + ) + parser.add_argument( + "--target-year", + type=int, + default=2022, + help="EPA Table 6-10 column to compare against (default 2022).", + ) + parser.add_argument( + "--db-dir", + type=str, + default=None, + help="Directory containing per-state DuckDB files (overrides $PYFIA_FIADB_DIR).", + ) + args = parser.parse_args() + + try: + db_dir = resolve_db_dir(args.db_dir) + except FileNotFoundError as e: + print(f"ERROR: {e}", file=sys.stderr) + return 2 + + if args.states: + states = [s.strip().upper() for s in args.states.split(",") if s.strip()] + else: + states = list(CONUS_48) + if args.include_ak: + states.append("AK") + + print(f"\n=== NGHGI Stage A reproduction — {len(states)} states ===") + print(f" Target: EPA Chapter 6 Table 6-10, year {args.target_year}") + print(f" Prefer EVALID with END_INVYR={args.prefer_end_year} per state") + print(f" State DB directory: {db_dir}\n") + + all_frames: list[pl.DataFrame] = [] + metas: list[dict] = [] + failures: list[tuple[str, str]] = [] + + for i, st in enumerate(states, 1): + try: + df, meta = run_state( + st, db_dir=db_dir, prefer_end_year=args.prefer_end_year + ) + all_frames.append(df) + metas.append(meta) + print( + f" [{i:>2}/{len(states)}] {st}: EVALID={meta['evalid']} " + f"end_yr={meta['end_invyr']} ({meta['elapsed_s']:.1f}s)" + ) + except Exception as e: + print(f" [{i:>2}/{len(states)}] {st}: FAILED — {type(e).__name__}: {e}") + failures.append((st, str(e))) + + if not all_frames: + print("\nNo successful state runs — aborting.") + return 1 + + print( + f"\nSucceeded: {len(all_frames)}/{len(states)} states. " + f"Failed: {len(failures)}.\n" + ) + + long_df = pl.concat(all_frames, how="vertical_relaxed") + + # Per-state pivot for inspection + per_state = long_df.pivot(values="STOCK_MMT_C", index="STATE", on="EPA_POOL").sort( + "STATE" + ) + print("=== Per-state stocks (MMT C) ===") + with pl.Config(tbl_rows=60, tbl_cols=10, fmt_str_lengths=80): + print(per_state) + + # CONUS rollup + print("\n=== CONUS rollup vs EPA Chapter 6 Table 6-10 ===") + cmp = compare_to_published(long_df, year=args.target_year) + with pl.Config(tbl_rows=12, tbl_cols=10, fmt_str_lengths=40): + print(cmp) + + print( + "\nNotes:\n" + " - SOIL_ORGANIC (Histosols) is NOT reproduced from FIADB; EPA uses\n" + " IPCC defaults for that pool. Compare the SOIL_ORGANIC row\n" + " against the constant published target only.\n" + " - EPA's Table 6-10 stock columns include AK + HI + territories.\n" + " This run is CONUS-48" + + (" + AK" if "AK" in states else "") + + ". Expect a gap from non-CONUS forest land.\n" + " - HWP (Harvested Wood) and Drained Organic Soils are out of scope.\n" + " - Pool decomposition: EPA AGB = pyfia live_tree(AG) + understory(AG);\n" + " EPA Dead Wood = standing_dead + downed_dead. Verify mapping in\n" + " scripts/nghgi/_compile.py if numbers look off.\n" + ) + + if failures: + print("\nFailures:") + for st, err in failures: + print(f" {st}: {err}") + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/nghgi/stage_b.py b/scripts/nghgi/stage_b.py new file mode 100644 index 00000000..72097332 --- /dev/null +++ b/scripts/nghgi/stage_b.py @@ -0,0 +1,318 @@ +""" +NGHGI Stage B — state-level flux validation against EPA Annex 3.13 Table A-208. + +Annex 3.13 publishes state-level *flux* (annual carbon stock change) for +all forest pools combined, 2022 only — no state-level stocks table exists. +This script runs pyfia's ``stock_change`` (condition-level: understory, +downed dead wood, litter, soil organic) per CONUS-48 state and compares +to the EPA published total per state. + +Two important caveats: + +1. pyfia's ``stock_change`` covers **only condition-level pools** — + live tree and standing dead flux (the dominant component, ~70-80% + of national flux) are deferred to a tree-level GRM decomposition. + The per-state residual ``EPA − pyfia_condition`` is the expected + tree-level component. + +2. EPA Table A-208's "Stock Change" sign convention: + negative = net carbon uptake (sink). pyfia's stock_change column + ``CARBON_TOTAL`` is ΔC = C(t₂) − C(t₁), so positive = accumulation + (also a sink). We negate pyfia values to align with EPA's flux + convention (negative = sink). + +Usage: + uv run python scripts/nghgi/stage_b.py + uv run python scripts/nghgi/stage_b.py --states GA,SC,FL + uv run python scripts/nghgi/stage_b.py --db-dir /path/to/fiadb + +Database directory resolution (in order): + 1. --db-dir CLI argument + 2. $PYFIA_FIADB_DIR environment variable + 3. ./data/fiadb (relative to current working directory) +""" + +from __future__ import annotations + +import argparse +import sys +import time +from pathlib import Path + +import polars as pl + +sys.path.insert(0, str(Path(__file__).parent)) + +from _compile import load_state_flux_targets # noqa: E402 +from _paths import resolve_db_dir # noqa: E402 + +from pyfia import FIA # noqa: E402 +from pyfia.carbon.stock_change import stock_change # noqa: E402 + +CONUS_48 = [ + "AL", + "AR", + "AZ", + "CA", + "CO", + "CT", + "DE", + "FL", + "GA", + "IA", + "ID", + "IL", + "IN", + "KS", + "KY", + "LA", + "MA", + "MD", + "ME", + "MI", + "MN", + "MO", + "MS", + "MT", + "NC", + "ND", + "NE", + "NH", + "NJ", + "NM", + "NV", + "NY", + "OH", + "OK", + "OR", + "PA", + "RI", + "SC", + "SD", + "TN", + "TX", + "UT", + "VA", + "VT", + "WA", + "WI", + "WV", + "WY", +] + +MMT_C_PER_SHORT_TON = 0.907185 / 1_000_000 + + +def _select_2022_evalid(db: FIA) -> int | None: + evals = db.query( + """ + SELECT EVALID, END_INVYR + FROM POP_EVAL + WHERE EVAL_DESCR LIKE '%CURRENT AREA, CURRENT VOLUME%' + ORDER BY END_INVYR DESC + """ + ) + if evals.is_empty(): + return None + le = evals.filter(pl.col("END_INVYR") <= 2022) + return int(le["EVALID"][0]) if not le.is_empty() else int(evals["EVALID"][0]) + + +def run_state(state: str, *, db_dir: Path) -> dict | None: + db_path = db_dir / f"{state}.duckdb" + if not db_path.exists(): + return None + t0 = time.perf_counter() + with FIA(str(db_path)) as db: + evalid = _select_2022_evalid(db) + if evalid is None: + return None + db.clip_by_evalid(evalid) + try: + sc = stock_change(db, pool="all") + except Exception as e: + return { + "state": state, + "evalid": evalid, + "error": f"{type(e).__name__}: {e}", + "elapsed_s": time.perf_counter() - t0, + } + + if sc is None or len(sc) == 0: + return { + "state": state, + "evalid": evalid, + "pyfia_condition_flux_mmt_c": 0.0, + "n_remeasured_plots": 0, + "elapsed_s": time.perf_counter() - t0, + } + + total_st = ( + float(sc["CARBON_CHANGE_TOTAL"].sum()) + if "CARBON_CHANGE_TOTAL" in sc.columns + else 0.0 + ) + n_plots = int(sc["N_PLOTS"][0]) if "N_PLOTS" in sc.columns and len(sc) > 0 else 0 + + # stock_change already annualizes by REMPER. Convert to MMT C and + # negate to match EPA's sign convention (negative = net uptake). + pyfia_mmt = -total_st * MMT_C_PER_SHORT_TON + + return { + "state": state, + "evalid": evalid, + "pyfia_condition_flux_mmt_c": pyfia_mmt, + "n_remeasured_plots": n_plots, + "elapsed_s": time.perf_counter() - t0, + } + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--states", default=",".join(CONUS_48)) + parser.add_argument( + "--db-dir", + type=str, + default=None, + help="Directory containing per-state DuckDB files (overrides $PYFIA_FIADB_DIR).", + ) + args = parser.parse_args() + + try: + db_dir = resolve_db_dir(args.db_dir) + except FileNotFoundError as e: + print(f"ERROR: {e}", file=sys.stderr) + return 2 + + states = [s.strip().upper() for s in args.states.split(",") if s.strip()] + + print("\n=== NGHGI Stage B — state-level flux validation ===") + print(" pyfia: condition-level stock_change (4 pools)") + print(" EPA: Table A-208 all-pools state flux, 2022") + print(" Expected residual: ~70-80% (= tree-level flux, not yet reproduced)") + print(f" State DB directory: {db_dir}\n") + + rows: list[dict] = [] + failures: list[tuple[str, str]] = [] + for i, st in enumerate(states, 1): + r = run_state(st, db_dir=db_dir) + if r is None: + failures.append((st, "no DB or no EVALID")) + continue + if "error" in r: + failures.append((st, r["error"])) + print(f" [{i:>2}/{len(states)}] {st}: FAILED — {r['error']}") + continue + rows.append(r) + print( + f" [{i:>2}/{len(states)}] {st}: EVALID={r['evalid']} " + f"pyfia_cond={r['pyfia_condition_flux_mmt_c']:+.2f} MMT C/yr " + f"({r['n_remeasured_plots']} plots, {r['elapsed_s']:.1f}s)" + ) + + if not rows: + print("\nNo successful runs. Failures:") + for st, err in failures: + print(f" {st}: {err}") + return 1 + + pyfia_df = pl.DataFrame(rows).rename({"state": "STATE"}) + + epa_df = load_state_flux_targets().rename( + { + "state": "STATE", + "flux_mmt_c_2022": "EPA_FLUX_MMT_C", + "lower_bound_mmt_c": "EPA_LOWER", + "upper_bound_mmt_c": "EPA_UPPER", + } + ) + + cmp = pyfia_df.join(epa_df, on="STATE", how="left").select( + [ + "STATE", + "evalid", + "n_remeasured_plots", + "pyfia_condition_flux_mmt_c", + "EPA_FLUX_MMT_C", + "EPA_LOWER", + "EPA_UPPER", + ] + ) + + cmp = cmp.with_columns( + (pl.col("EPA_FLUX_MMT_C") - pl.col("pyfia_condition_flux_mmt_c")).alias( + "RESIDUAL_TREE_LEVEL_MMT_C" + ) + ) + + out = ( + cmp.rename( + { + "evalid": "EVALID", + "n_remeasured_plots": "N_REMEAS", + "pyfia_condition_flux_mmt_c": "PYFIA_COND", + } + ) + .select( + [ + "STATE", + "EVALID", + "N_REMEAS", + "PYFIA_COND", + "EPA_FLUX_MMT_C", + "EPA_LOWER", + "EPA_UPPER", + "RESIDUAL_TREE_LEVEL_MMT_C", + ] + ) + .sort("EPA_FLUX_MMT_C") + ) + + print("\n=== Per-state flux comparison (2022, MMT C/yr) ===") + print(" PYFIA_COND = pyfia condition-level (4 pools) flux") + print(" EPA_FLUX = EPA Table A-208 all-pools flux") + print(" RESIDUAL = EPA - pyfia_cond = expected tree-level flux") + print(" Sign convention: negative = net uptake (sink)\n") + with pl.Config(tbl_rows=60, tbl_cols=10, fmt_str_lengths=20, tbl_width_chars=160): + print(out) + + print("\n=== CONUS-48 rollup ===") + py_total = float(out["PYFIA_COND"].sum()) + epa_total = float(out["EPA_FLUX_MMT_C"].sum()) + res_total = float(out["RESIDUAL_TREE_LEVEL_MMT_C"].sum()) + print(f" pyfia condition flux total: {py_total:+.1f} MMT C/yr") + print(f" EPA all-pools flux total: {epa_total:+.1f} MMT C/yr") + print(f" Residual (= tree-level): {res_total:+.1f} MMT C/yr") + pct_cond = 100.0 * py_total / epa_total if epa_total else float("nan") + pct_tree = 100.0 * res_total / epa_total if epa_total else float("nan") + print(f" Condition pools account for: {pct_cond:.1f}% of EPA total") + print(f" Tree pools account for: {pct_tree:.1f}% of EPA total") + + print("\n Note: EPA Table A-208 total reported in Annex 3.13: -189.3 MMT C/yr") + print(f" Our CONUS-48 EPA sum: {epa_total:+.1f} MMT C/yr") + print(" (delta from -189.3 reflects HI/AK/territories in EPA total)\n") + + if failures: + print(f"Failures ({len(failures)}):") + for st, err in failures: + print(f" {st}: {err}") + + in_band = out.filter( + (pl.col("PYFIA_COND") >= pl.col("EPA_LOWER")) + & (pl.col("PYFIA_COND") <= pl.col("EPA_UPPER")) + ) + print( + f"\nStates where pyfia condition flux falls within EPA all-pools " + f"uncertainty band: {len(in_band)}/{len(out)}" + ) + print( + "(expected to be low — pyfia condition flux is only part of EPA's " + "all-pools flux; it would only fall in the band when tree-level " + "flux is small or EPA uncertainty is huge)" + ) + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/pyfia/__init__.py b/src/pyfia/__init__.py index 6670b3c3..ade88331 100755 --- a/src/pyfia/__init__.py +++ b/src/pyfia/__init__.py @@ -1,14 +1,8 @@ """ PyFIA - A Python library for USDA Forest Inventory and Analysis (FIA) data analysis. -A high-performance Python library for analyzing USDA Forest Inventory and Analysis (FIA) data -using modern data science tools like Polars and DuckDB. - -Part of the FIA Python Ecosystem: -- PyFIA: Survey/plot data analysis (https://github.com/mihiarc/pyfia) -- GridFIA: Spatial raster analysis (https://github.com/mihiarc/gridfia) -- PyFVS: Growth/yield simulation (https://github.com/mihiarc/pyfvs) -- AskFIA: AI conversational interface (https://github.com/mihiarc/askfia) +A high-performance Python library for analyzing USDA Forest Inventory and +Analysis (FIA) data using modern data science tools like Polars and DuckDB. """ from __future__ import annotations @@ -17,6 +11,15 @@ __author__ = "Chris Mihiar" # Core exports - Main functionality +# Estimation functions - High-level API +from pyfia.carbon.downed_dead import downed_dead +from pyfia.carbon.litter import litter +from pyfia.carbon.live_tree import live_tree +from pyfia.carbon.soil_organic import soil_organic +from pyfia.carbon.standing_dead import standing_dead +from pyfia.carbon.stock_change import stock_change +from pyfia.carbon.total_ecosystem import total_ecosystem +from pyfia.carbon.understory import understory from pyfia.core.data_reader import FIADataReader from pyfia.core.exceptions import ( ConfigurationError, @@ -50,8 +53,6 @@ clear_cache, download, ) - -# Estimation functions - High-level API from pyfia.estimation.estimators.area import area from pyfia.estimation.estimators.area_change import area_change from pyfia.estimation.estimators.biomass import biomass @@ -113,6 +114,14 @@ "area", "area_change", "biomass", + "downed_dead", + "litter", + "live_tree", + "soil_organic", + "standing_dead", + "stock_change", + "total_ecosystem", + "understory", "volume", "tpa", "mortality", diff --git a/src/pyfia/carbon/__init__.py b/src/pyfia/carbon/__init__.py new file mode 100644 index 00000000..bede7de9 --- /dev/null +++ b/src/pyfia/carbon/__init__.py @@ -0,0 +1,180 @@ +""" +Forest carbon estimation for pyFIA — all six IPCC/NGHGI reporting pools. + +This subpackage implements forest carbon stock estimation across the IPCC and +NGHGI reporting pools. Tree-level pools (live tree, standing dead) use the +National Scale Volume and Biomass framework (NSVB; Westfall et al. 2023, +GTR-WO-104) with species-specific carbon fractions. Condition-level pools +(understory, downed dead wood, litter, soil organic carbon) read pre-computed +attributes from the FIADB COND table. :func:`total_ecosystem` sums all six +pools into a single estimate. + +Pools implemented +================= + +Live tree +--------- +:func:`live_tree` — above-ground live tree carbon via the vectorized NSVB +pipeline (Models 1/2/4/5, 3-level coefficient lookup precedence: Bailey +DIVISION → species-level → Jenkins fallback), with species-specific S10a +carbon fractions. Cull adjustment per Appendix K. BG bridges to FIADB +``TREE.CARBON_BG``. + +Validated against FIADB ``TREE.CARBON_AG`` on Georgia EVALID 132401 +(130,952 trees): median per-tree relative error 0.085%. + +Standing dead +------------- +:func:`standing_dead` — standing dead tree carbon via the same NSVB pipeline +with ``REF_TREE_DECAY_PROP`` decay reductions (DENSITY_PROP × wood, +BARK_LOSS_PROP × bark, BRANCH_LOSS_PROP × branch) and S10b dead carbon +fractions. No ``TREE.CULL`` adjustment for dead trees (per Appendix K). + +Broken-top corrections (``ACTUALHT < HT``) apply the Appendix K +crown-proportion adjustment to branch biomass and a paraboloid taper +volume-ratio approximation to wood/bark, using mean intact crown ratios +from Table S11 (``REF_TREE_STND_DEAD_CR_PROP``). + +Validated against FIADB on Georgia EVALID 132401 (6,870 trees): median +per-tree relative error 10.89%. + +Understory vegetation +--------------------- +:func:`understory` — understory vegetation carbon (seedlings + woody +shrubs < 2.54 cm DBH) via the Smith & Heath (2008) model, reading +pre-computed ``COND.CARBON_UNDERSTORY_AG`` and ``COND.CARBON_UNDERSTORY_BG`` +from the FIADB. The model descends from the Birdsey (1996) ratios by +forest type group and region; the AG/BG split is 90/10 (Smith et al. 2006). + +This is a **condition-level** estimator (no TREE table); it inherits from +``BaseEstimator`` directly rather than ``CarbonEstimatorBase``. + +Validated against manual SQL replication on Georgia EVALID 132301: +population totals match exactly. + +Downed dead wood +----------------- +:func:`downed_dead` — downed dead wood (coarse woody debris) carbon via +the Domke et al. (2013) model, reading pre-computed +``COND.CARBON_DOWN_DEAD`` from the FIADB. No AG/BG split. + +This is a **condition-level** estimator (no TREE table); it inherits from +``BaseEstimator`` directly rather than ``CarbonEstimatorBase``. + +Validated against manual SQL replication on Georgia EVALID 132301: +population totals match exactly. + +Litter +------ +:func:`litter` — litter and duff carbon via the Domke et al. (2016) +model, reading pre-computed ``COND.CARBON_LITTER`` from the FIADB. +No AG/BG split. + +This is a **condition-level** estimator (no TREE table); it inherits from +``BaseEstimator`` directly rather than ``CarbonEstimatorBase``. + +Validated against manual SQL replication on Georgia EVALID 132301: +population totals match exactly. + +Soil organic carbon +------------------- +:func:`soil_organic` — soil organic carbon (mineral soil to 1 m depth) +via the Domke et al. (2017) model, reading pre-computed +``COND.CARBON_SOIL_ORG`` from the FIADB. No AG/BG split. + +This is a **condition-level** estimator (no TREE table); it inherits from +``BaseEstimator`` directly rather than ``CarbonEstimatorBase``. + +Validated against manual SQL replication on Georgia EVALID 132301: +population totals match exactly. + +Total ecosystem +--------------- +:func:`total_ecosystem` — convenience function that estimates all six pools +independently and sums per-acre and population totals. The result contains +one row per pool plus a ``TOTAL_ECOSYSTEM`` summary row. + +Validated against sum of individual pool calls on Georgia EVALID 132301: +exact match (0.00 difference). + +Stock change (condition-level) +------------------------------ +:func:`stock_change` — carbon stock-change accounting for condition-level +pools. Computes ``C(t₂) − C(t₁)`` per remeasured condition, annualized +by REMPER, aggregated via the two-stage post-stratified pipeline using +t₂'s stratification. Follows the ``AreaChangeEstimator`` pattern: t₂ from +EVALID-scoped pipeline, t₁ from full COND table via ``PREV_PLT_CN``. + +Supports: understory, downed dead, litter, soil organic (``pool='all'``). +Tree-level stock change (live tree, standing dead via GRM decomposition) deferred. + +Validated against manual SQL replication on Georgia EVALID 132301: +population totals match exactly for all three non-understory pools. + +Pools deferred +============== +- Tree-level stock change (live tree, standing dead via GRM decomposition) +- Native NSVB belowground coarse-root model (replaces the BG bridge) + +Architectural rules +=================== +1. **Public API is functions, not classes.** ``live_tree(db, ...)``, + ``standing_dead(db, ...)``. Match the pyfia convention. + +2. **Vectorize coefficient lookups via polars joins.** The scalar + ``predict_tree_biomass`` is a test oracle only. The production path + is ``compute_nsvb_biomass`` / ``compute_nsvb_dead_biomass``. + +3. **Inherit from ``CarbonEstimatorBase`` (tree-level) or ``BaseEstimator`` + (condition-level).** New pool estimators must follow the template-method + pattern (``load_data → apply_filters → calculate_values → + aggregate_results → calculate_variance → format_output``). + +4. **Match the ``mortality()`` docstring quality.** + +5. **Bridge BG carbon to FIADB ``CARBON_BG`` for now.** The bridge is + acknowledged tech debt; a native NSVB root model will replace it. + +References +---------- +- Westfall, J.A. et al. (2023). GTR-WO-104. DOI: 10.2737/WO-GTR-104 +- Woodall, C.W. et al. (2015). GTR-NRS-154 (FCAF methodology blueprint). +- Harmon, M.E. et al. (2011). GTR-WO-104 Table 1 (dead-tree density). +- Smith, J.E. et al. (2006). GTR-NE-343 (understory yield tables). +- Smith, J.E.; Heath, L.S. (2008). GTR-NRS-13 (FIADB carbon attributes). +- Birdsey, R.A. (1996). Forests and Global Change Vol. 2 (understory ratios). +- Domke, G.M. et al. (2013). Forest Ecol. Manage. 292, 50-57 (downed dead). +- Domke, G.M. et al. (2016). Sci. Total Environ. 557-558, 469-478 (litter). +- Domke, G.M. et al. (2017). Ecol. Appl. 27(4), 1223-1235 (soil organic C). +- Bechtold, W.A. & Patterson, P.L. (2005). GTR-SRS-80, Ch. 4 (change estimation). +- USEPA (2024). NGHGI Annex 3.13. +""" + +from __future__ import annotations + +from pyfia.carbon.downed_dead import DownedDeadEstimator, downed_dead +from pyfia.carbon.litter import LitterEstimator, litter +from pyfia.carbon.live_tree import LiveTreeEstimator, live_tree +from pyfia.carbon.soil_organic import SoilOrganicEstimator, soil_organic +from pyfia.carbon.standing_dead import StandingDeadEstimator, standing_dead +from pyfia.carbon.stock_change import CarbonStockChangeEstimator, stock_change +from pyfia.carbon.total_ecosystem import total_ecosystem +from pyfia.carbon.understory import UnderstoryEstimator, understory + +__all__ = [ + "downed_dead", + "litter", + "live_tree", + "soil_organic", + "standing_dead", + "stock_change", + "total_ecosystem", + "understory", + "CarbonStockChangeEstimator", + "DownedDeadEstimator", + "LitterEstimator", + "LiveTreeEstimator", + "SoilOrganicEstimator", + "StandingDeadEstimator", + "UnderstoryEstimator", +] diff --git a/src/pyfia/carbon/_estimator_base.py b/src/pyfia/carbon/_estimator_base.py new file mode 100644 index 00000000..0fecf946 --- /dev/null +++ b/src/pyfia/carbon/_estimator_base.py @@ -0,0 +1,270 @@ +""" +Shared base class for NSVB carbon pool estimators. + +Extracts the ~200 lines of duplicated infrastructure that LiveTreeEstimator +and StandingDeadEstimator share verbatim: reference-table loading +(_load_ref_species, _load_plotgeom), the two-stage aggregation pipeline, +variance calculation, and output formatting. Pool-specific logic +(calculate_values, apply_filters, get_tree_columns) stays in the subclasses. +""" + +from __future__ import annotations + +import logging + +import polars as pl + +from ..core import FIA +from ..estimation.base import AggregationResult, BaseEstimator +from ..estimation.columns import get_cond_columns as _get_cond_columns +from ..estimation.constants import LBS_TO_SHORT_TONS +from ..estimation.tree_expansion import apply_tree_adjustment_factors +from ..estimation.utils import ( + validate_aggregation_result, + validate_required_columns, +) +from .nsvb.coefficients import ecosubcd_to_division_expr + +logger = logging.getLogger(__name__) + + +class CarbonEstimatorBase(BaseEstimator): + """Shared infrastructure for NSVB carbon pool estimators. + + Subclasses must implement :meth:`get_tree_columns` and + :meth:`calculate_values`. Everything else — table requirements, + condition columns, reference-table loading, aggregation, variance, + and output formatting — is identical across pools. + + Class attributes that subclasses should set: + + - ``_estimator_label``: human-readable name for validation messages + (e.g., ``"live_tree"``, ``"standing_dead"``). + """ + + _estimator_label: str = "carbon" + + def __init__(self, db: str | FIA, config: dict) -> None: + super().__init__(db, config) + self._plotgeom_cache: pl.DataFrame | None = None + + # ------------------------------------------------------------------ + # Table / column requirements + # ------------------------------------------------------------------ + + def get_required_tables(self) -> list[str]: + return ["TREE", "COND", "PLOT", "POP_PLOT_STRATUM_ASSGN", "POP_STRATUM"] + + def get_cond_columns(self) -> list[str]: + return _get_cond_columns( + land_type=self.config.get("land_type", "forest"), + grp_by=self.config.get("grp_by"), + include_prop_basis=False, + ) + + # ------------------------------------------------------------------ + # Reference-table helpers + # ------------------------------------------------------------------ + + def _load_ref_species(self) -> pl.DataFrame: + """Load REF_SPECIES columns needed by the NSVB pipeline. + + Returns ``(SPCD Int64, JENKINS_SPGRPCD Int64, WDSG Float64)``. + Cached on the instance for the duration of one estimator run. + """ + if self._ref_species_cache is not None: + return self._ref_species_cache + df = self.db._reader.read_table( + "REF_SPECIES", + columns=["SPCD", "JENKINS_SPGRPCD", "WOOD_SPGR_GREENVOL_DRYWT"], + ) + if hasattr(df, "collect"): + df = df.collect() + df = df.with_columns( + [ + pl.col("SPCD").cast(pl.Int64), + pl.col("JENKINS_SPGRPCD").cast(pl.Int64), + pl.col("WOOD_SPGR_GREENVOL_DRYWT").cast(pl.Float64).alias("WDSG"), + ] + ).select(["SPCD", "JENKINS_SPGRPCD", "WDSG"]) + self._ref_species_cache = df + return df + + def _load_plotgeom(self) -> pl.DataFrame | None: + """Load ``PLOTGEOM.ECOSUBCD`` for the DIVISION / ECOPROV lookups. + + Returns ``(PLT_CN, ECOSUBCD)`` or ``None`` when the table is + missing. Negative result cached as an empty DataFrame sentinel. + """ + if self._plotgeom_cache is not None: + return self._plotgeom_cache if self._plotgeom_cache.height > 0 else None + + try: + df = self.db._reader.read_table( + "PLOTGEOM", + columns=["CN", "ECOSUBCD"], + ) + except Exception as exc: # noqa: BLE001 + logger.warning( + "PLOTGEOM not available (%s) — DIVISION lookup disabled, " + "falling back to species-level + Jenkins coefficient " + "precedence (~3%% high biomass bias on growing-stock trees).", + exc, + ) + self._plotgeom_cache = pl.DataFrame() + return None + + if hasattr(df, "collect"): + df = df.collect() + df = df.select( + [ + pl.col("CN").alias("PLT_CN"), + pl.col("ECOSUBCD"), + ] + ).unique(subset=["PLT_CN"]) + self._plotgeom_cache = df + return df + + # ------------------------------------------------------------------ + # Shared helpers used inside calculate_values + # ------------------------------------------------------------------ + + def _join_ref_species(self, data: pl.LazyFrame) -> pl.LazyFrame: + """Normalize SPCD dtype and join REF_SPECIES for WDSG + JENKINS_SPGRPCD.""" + data = data.with_columns(pl.col("SPCD").cast(pl.Int64)) + ref_species = self._load_ref_species() + return data.join(ref_species.lazy(), on="SPCD", how="left") + + def _join_plotgeom_division(self, data: pl.LazyFrame) -> pl.LazyFrame: + """Join PLOTGEOM and derive DIVISION from ECOSUBCD. + + Adds both ``ECOSUBCD`` and ``DIVISION`` columns when PLOTGEOM is + available; otherwise returns the frame unchanged. + """ + plotgeom = self._load_plotgeom() + if plotgeom is None: + return data + data = data.join(plotgeom.lazy(), on="PLT_CN", how="left") + data = data.with_columns( + ecosubcd_to_division_expr("ECOSUBCD").alias("DIVISION") + ) + return data + + def _apply_bg_bridge(self, data: pl.LazyFrame, pool: str) -> pl.LazyFrame: + """Add ``_CARBON_BG_LB`` column from the FIADB BG bridge. + + For ``pool in ('bg', 'total')``, reads ``TREE.CARBON_BG`` directly + (in lb per tree). For ``pool='ag'``, zeroes out the BG + contribution. BG bridge is in pounds per tree — the same units as + ``_CARBON_AG_LB``. + """ + if pool in ("bg", "total"): + # TREE.CARBON_BG is in pounds per tree (same as _CARBON_AG_LB). + # This BG bridge will be replaced by a native NSVB root model. + return data.with_columns( + pl.col("CARBON_BG") + .cast(pl.Float64) + .fill_null(0.0) + .alias("_CARBON_BG_LB") + ) + return data.with_columns(pl.lit(0.0).alias("_CARBON_BG_LB")) + + def _compute_carbon_acre(self, data: pl.LazyFrame) -> pl.LazyFrame: + """Sum AG + BG, convert lb → short tons, multiply by TPA_UNADJ.""" + return data.with_columns( + ( + (pl.col("_CARBON_AG_LB") + pl.col("_CARBON_BG_LB")) + * pl.col("TPA_UNADJ").cast(pl.Float64) + * LBS_TO_SHORT_TONS + ).alias("CARBON_ACRE"), + ) + + # ------------------------------------------------------------------ + # Aggregation / variance / formatting (identical across pools) + # ------------------------------------------------------------------ + + def aggregate_results(self, data: pl.LazyFrame | None) -> AggregationResult: + if data is None: + return AggregationResult( + results=pl.DataFrame(), + plot_tree_data=pl.DataFrame(), + group_cols=[], + ) + + validate_required_columns( + data, ["PLT_CN", "CARBON_ACRE"], f"{self._estimator_label} carbon data" + ) + + strat_data = self._get_stratification_data() + data_with_strat = data.join(strat_data, on="PLT_CN", how="inner") + + data_with_strat = apply_tree_adjustment_factors( + data_with_strat, + size_col="DIA", + macro_breakpoint_col="MACRO_BREAKPOINT_DIA", + ) + + data_with_strat = data_with_strat.with_columns( + (pl.col("CARBON_ACRE") * pl.col("ADJ_FACTOR")).alias("CARBON_ADJ") + ) + + group_cols = self._setup_grouping() + + plot_tree_data, data_with_strat = self._preserve_plot_tree_data( + data_with_strat, + metric_cols=["CARBON_ADJ"], + group_cols=group_cols, + ) + + results = self._apply_two_stage_aggregation( + data_with_strat=data_with_strat, + metric_mappings={"CARBON_ADJ": "CONDITION_CARBON"}, + group_cols=group_cols, + use_grm_adjustment=False, + ) + + if not self.config.get("totals", True): + if "CARBON_TOTAL" in results.columns: + results = results.drop("CARBON_TOTAL") + + return AggregationResult( + results=results, + plot_tree_data=plot_tree_data, + group_cols=group_cols, + ) + + def calculate_variance(self, agg_result: AggregationResult) -> pl.DataFrame: + validate_aggregation_result(agg_result, self._estimator_label) + metric_configs = [ + { + "adjusted_col": "CARBON_ADJ", + "acre_se_col": "CARBON_ACRE_SE", + "total_se_col": "CARBON_TOTAL_SE", + }, + ] + return self._calculate_variance_for_metrics(agg_result, metric_configs) + + def format_output(self, results: pl.DataFrame) -> pl.DataFrame: + year = self._extract_evaluation_year() + results = results.with_columns(pl.lit(year).alias("YEAR")) + + pool = self.config.get("pool", "ag").upper() + results = results.with_columns(pl.lit(pool).alias("POOL")) + + col_order = [ + "YEAR", + "POOL", + "CARBON_ACRE", + "CARBON_TOTAL", + "CARBON_ACRE_SE", + "CARBON_TOTAL_SE", + "N_PLOTS", + "N_TREES", + ] + + for col in results.columns: + if col not in col_order: + col_order.insert(1, col) + + final_cols = [col for col in col_order if col in results.columns] + return results.select(final_cols) diff --git a/src/pyfia/carbon/downed_dead.py b/src/pyfia/carbon/downed_dead.py new file mode 100644 index 00000000..b2f23a85 --- /dev/null +++ b/src/pyfia/carbon/downed_dead.py @@ -0,0 +1,417 @@ +""" +Downed dead wood carbon estimation from FIADB condition-level attributes. + +Downed dead wood — coarse woody debris (CWD) lying on the forest floor +— is estimated in the FIADB using the +Domke et al. (2013) model, which relates CWD carbon density to geographic +area, forest type group, and stand age class. + +This estimator reads ``COND.CARBON_DOWN_DEAD`` (short tons per acre), +then runs it through pyFIA's post-stratified aggregation pipeline to +produce per-acre and population estimates that match EVALIDator. + +Downed dead wood has no above-ground / below-ground split; the single +``CARBON_DOWN_DEAD`` column represents the total pool. + +National magnitude: ~2,700 Tg C total downed dead wood, roughly 3 % of +total forest ecosystem carbon (GTR-NRS-154, Table 2). + +Public API: :func:`downed_dead`. See its docstring for parameters, +examples, and the pool semantics. + +References +---------- +- Domke, G.M.; Woodall, C.W.; Smith, J.E. (2013). Accounting for density + reductions in dead wood decay classes. Forest Ecology and Management, + 292, 50-57. +- Woodall, C.W. et al. (2015). GTR-NRS-154 (FCAF methodology blueprint). +- USEPA (2024). NGHGI Annex 3.13. +""" + +from __future__ import annotations + +import logging + +import polars as pl + +from ..core import FIA +from ..estimation.base import AggregationResult, BaseEstimator +from ..estimation.columns import get_cond_columns as _get_cond_columns +from ..estimation.utils import ( + ensure_evalid_set, + ensure_fia_instance, + validate_aggregation_result, + validate_required_columns, +) + +logger = logging.getLogger(__name__) + + +class DownedDeadEstimator(BaseEstimator): + """Downed dead wood carbon estimator. + + Reads pre-computed ``COND.CARBON_DOWN_DEAD`` from the FIADB and + aggregates via the standard post-stratified estimation pipeline. + + This is a **condition-level** estimator — there is no tree-level data. + The TREE table is not loaded; data comes from ``COND x PLOT`` only. + The adjustment factor is ``ADJ_FACTOR_SUBP`` (subplot-level), + matching FIADB/EVALIDator conventions for condition-level carbon + attributes. + """ + + _estimator_label = "DownedDead" + + # ------------------------------------------------------------------ + # Table / column requirements + # ------------------------------------------------------------------ + + def get_required_tables(self) -> list[str]: + return ["COND", "PLOT", "POP_PLOT_STRATUM_ASSGN", "POP_STRATUM"] + + def get_tree_columns(self) -> list[str]: + return [] + + def get_cond_columns(self) -> list[str]: + cols = _get_cond_columns( + land_type=self.config.get("land_type", "forest"), + grp_by=self.config.get("grp_by"), + include_prop_basis=False, + ) + if "CARBON_DOWN_DEAD" not in cols: + cols.append("CARBON_DOWN_DEAD") + return cols + + # ------------------------------------------------------------------ + # Override apply_filters for condition-level data + # ------------------------------------------------------------------ + + def apply_filters(self, data: pl.LazyFrame) -> pl.LazyFrame: + """Apply condition/area filters only (no tree-type filtering).""" + from ..filtering import apply_area_filters, get_land_domain_indicator + + columns = data.collect_schema().names() + + if self.config.get("area_domain"): + data = apply_area_filters(data, area_domain=self.config["area_domain"]) + + land_type = self.config.get("land_type", "forest") + if land_type and land_type != "all" and "COND_STATUS_CD" in columns: + data = data.filter(get_land_domain_indicator(land_type)) + + return data + + # ------------------------------------------------------------------ + # Core estimation logic + # ------------------------------------------------------------------ + + def calculate_values(self, data: pl.LazyFrame) -> pl.LazyFrame: + """Alias CARBON_DOWN_DEAD to CARBON_ACRE.""" + cond_carbon = pl.col("CARBON_DOWN_DEAD").cast(pl.Float64).fill_null(0.0) + return data.with_columns(cond_carbon.alias("CARBON_ACRE")) + + # ------------------------------------------------------------------ + # Aggregation — condition-level with ADJ_FACTOR_SUBP + # ------------------------------------------------------------------ + + def aggregate_results(self, data: pl.LazyFrame | None) -> AggregationResult: + if data is None: + return AggregationResult( + results=pl.DataFrame(), + plot_tree_data=pl.DataFrame(), + group_cols=[], + ) + + validate_required_columns( + data, ["PLT_CN", "CARBON_ACRE"], "downed dead carbon data" + ) + + strat_data = self._get_stratification_data() + data_with_strat = data.join(strat_data, on="PLT_CN", how="inner") + + # Condition-level attribute: CARBON_ACRE is a density (tons/acre). + # The two-stage pipeline denominator is sum(CONDPROP_UNADJ * EXPNS), + # so the numerator must include CONDPROP_UNADJ to give the + # condition's contribution proportional to its area on the plot. + # ADJ_FACTOR_SUBP corrects for nonresponse at the subplot level. + data_with_strat = data_with_strat.with_columns( + pl.col("ADJ_FACTOR_SUBP").cast(pl.Float64).alias("ADJ_FACTOR") + ) + + data_with_strat = data_with_strat.with_columns( + ( + pl.col("CARBON_ACRE") + * pl.col("CONDPROP_UNADJ").cast(pl.Float64) + * pl.col("ADJ_FACTOR") + ).alias("CARBON_ADJ") + ) + + group_cols = self._setup_grouping() + + plot_tree_data, data_with_strat = self._preserve_plot_tree_data( + data_with_strat, + metric_cols=["CARBON_ADJ"], + group_cols=group_cols, + ) + + results = self._apply_two_stage_aggregation( + data_with_strat=data_with_strat, + metric_mappings={"CARBON_ADJ": "CONDITION_CARBON"}, + group_cols=group_cols, + use_grm_adjustment=False, + ) + + if not self.config.get("totals", True): + if "CARBON_TOTAL" in results.columns: + results = results.drop("CARBON_TOTAL") + + return AggregationResult( + results=results, + plot_tree_data=plot_tree_data, + group_cols=group_cols, + ) + + # ------------------------------------------------------------------ + # Variance and output formatting + # ------------------------------------------------------------------ + + def calculate_variance(self, agg_result: AggregationResult) -> pl.DataFrame: + validate_aggregation_result(agg_result, self._estimator_label) + metric_configs = [ + { + "adjusted_col": "CARBON_ADJ", + "acre_se_col": "CARBON_ACRE_SE", + "total_se_col": "CARBON_TOTAL_SE", + }, + ] + return self._calculate_variance_for_metrics(agg_result, metric_configs) + + def format_output(self, results: pl.DataFrame) -> pl.DataFrame: + year = self._extract_evaluation_year() + results = results.with_columns(pl.lit(year).alias("YEAR")) + results = results.with_columns(pl.lit("TOTAL").alias("POOL")) + + col_order = [ + "YEAR", + "POOL", + "CARBON_ACRE", + "CARBON_TOTAL", + "CARBON_ACRE_SE", + "CARBON_TOTAL_SE", + "N_PLOTS", + "N_TREES", + ] + + for col in results.columns: + if col not in col_order: + col_order.insert(1, col) + + final_cols = [col for col in col_order if col in results.columns] + return results.select(final_cols) + + +# ====================================================================== +# Public API +# ====================================================================== + + +def downed_dead( + db: str | FIA, + pool: str = "total", + grp_by: str | list[str] | None = None, + land_type: str = "forest", + area_domain: str | None = None, + plot_domain: str | None = None, + totals: bool = True, + variance: bool = False, + most_recent: bool = False, +) -> pl.DataFrame: + """ + Estimate downed dead wood carbon from FIA data. + + Downed dead wood (coarse woody debris) encompasses all dead wood + lying on the forest floor with a diameter >= 7.62 cm (3 in) at the + point of intersection with a sampling transect. Carbon density is + estimated from the Domke et al. (2013) model, parameterised on + geographic area, forest type group, and stand age. The FIADB + pre-computes condition-level carbon density and stores it in + ``COND.CARBON_DOWN_DEAD``. + + Nationally, downed dead wood carbon totals ~2,700 Tg C, representing + ~3 % of total forest ecosystem carbon (GTR-NRS-154, Table 2). + Per-acre values are typically 1-5 short tons/acre depending on forest + type and region. + + Parameters + ---------- + db : str | FIA + Database connection or path to FIA database. Can be either a path + string to a DuckDB/SQLite file or an existing FIA connection object. + pool : {'total'}, default 'total' + Carbon pool to estimate. Downed dead wood has no above-ground / + below-ground split — only ``'total'`` is accepted. + grp_by : str or list of str, optional + Column name(s) to group results by. Can be any column from the + FIA COND or PLOT tables. Common grouping columns include: + + - 'FORTYPCD': Forest type code + - 'FORTYPGRPCD': Forest type group code + - 'OWNGRPCD': Ownership group + - 'STATECD': State FIPS code + - 'COUNTYCD': County code + + For complete column descriptions, see USDA FIA Database User Guide. + land_type : {'forest', 'timber', 'all'}, default 'forest' + Land type to include in estimation: + + - 'forest': All forestland + - 'timber': Productive timberland only (unreserved, productive) + - 'all': All land conditions + area_domain : str, optional + SQL-like filter expression for area/condition-level filtering. + Example: ``"OWNGRPCD == 40 AND FORTYPCD == 161"``. + plot_domain : str, optional + SQL-like filter expression for plot-level filtering. + totals : bool, default True + If True, include population-level total estimates in addition to + per-acre values. + variance : bool, default False + If True, calculate and include variance and standard error + estimates following Bechtold & Patterson (2005). + most_recent : bool, default False + If True, automatically filter to the most recent EXPVOL evaluation + for each state in the database before estimation. + + Returns + ------- + pl.DataFrame + Downed dead wood carbon estimates with the following columns: + + - **YEAR** : int + Evaluation reference year from EVALID. + - **POOL** : str + Pool identifier — always ``'TOTAL'``. + - **CARBON_ACRE** : float + Carbon per acre in short tons. + - **CARBON_TOTAL** : float (if ``totals=True``) + Total carbon in short tons expanded to population level. + - **CARBON_ACRE_SE** : float (if ``variance=True``) + Standard error of the per-acre estimate. + - **CARBON_TOTAL_SE** : float (if ``variance=True`` and ``totals=True``) + Standard error of the population total. + - **N_PLOTS** : int + Number of FIA plots included in the estimation. + - **N_TREES** : int + Number of tree records (used for distribution weighting). + - **[grouping columns]** : various + Any columns specified in ``grp_by``. + + See Also + -------- + live_tree : Estimate live tree carbon using the NSVB framework. + standing_dead : Estimate standing dead tree carbon. + understory : Estimate understory vegetation carbon. + litter : Estimate litter carbon. + soil_organic : Estimate soil organic carbon. + pyfia.carbon : Overview of all carbon pool estimators. + + Notes + ----- + **Methodology** + + Downed dead wood is not directly measured on standard FIA plots in a + manner that produces per-condition densities. Instead, the FIADB + pre-computes carbon density per condition using the Domke et al. + (2013) model, which accounts for density reductions across decay + classes and uses regional CWD volume equations. + + This estimator reads those pre-computed values directly from the COND + table and runs them through the standard post-stratified aggregation + pipeline, ensuring exact agreement with EVALIDator estimates. + + **No AG/BG split** + + Unlike understory vegetation, downed dead wood has no above-ground / + below-ground partitioning. The ``CARBON_DOWN_DEAD`` column represents + the entire pool. + + Examples + -------- + Total downed dead wood carbon per acre on forestland: + + >>> results = downed_dead(db, pool="total") + >>> print(f"Carbon: {results['CARBON_ACRE'][0]:.3f} tons/acre") + + Downed dead wood carbon by forest type group: + + >>> results = downed_dead(db, pool="total", grp_by="FORTYPGRPCD") + + Downed dead wood carbon on timberland with standard errors: + + >>> results = downed_dead( + ... db, + ... land_type="timber", + ... variance=True, + ... ) + + References + ---------- + .. [1] Domke, G.M.; Woodall, C.W.; Smith, J.E. (2013). Accounting for + density reductions in dead wood decay classes. Forest Ecology and + Management, 292, 50-57. + .. [2] Woodall, C.W. et al. (2015). The current and future role of + forest carbon in the United States. Gen. Tech. Rep. NRS-154. + .. [3] USEPA (2024). Inventory of U.S. Greenhouse Gas Emissions and + Sinks, Chapter 6 and Annex 3.13. + """ + from ..validation import ( + validate_boolean, + validate_domain_expression, + validate_grp_by, + validate_land_type, + ) + + # ----- Validate pool ----- + pool = pool.lower() + if pool != "total": + raise ValueError( + f"Invalid pool '{pool}' for downed dead wood. " + f"Only 'total' is supported — downed dead wood has no AG/BG split." + ) + + # ----- Validate standard inputs ----- + land_type = validate_land_type(land_type) + grp_by = validate_grp_by(grp_by) + area_domain = validate_domain_expression(area_domain, "area_domain") + plot_domain = validate_domain_expression(plot_domain, "plot_domain") + totals = validate_boolean(totals, "totals") + variance = validate_boolean(variance, "variance") + most_recent = validate_boolean(most_recent, "most_recent") + + # ----- Resolve db + EVALID ----- + db, owns_db = ensure_fia_instance(db) + if most_recent and db.evalid is None: + db.clip_most_recent(eval_type="VOL") + else: + ensure_evalid_set(db, eval_type="VOL", estimator_name="downed_dead") + + # ----- Build config and run estimator ----- + config = { + "pool": pool, + "grp_by": grp_by, + "by_species": False, + "by_size_class": False, + "land_type": land_type, + "area_domain": area_domain, + "plot_domain": plot_domain, + "totals": totals, + "variance": variance, + "most_recent": most_recent, + } + + try: + estimator = DownedDeadEstimator(db, config) + return estimator.estimate() + finally: + if owns_db and hasattr(db, "close"): + db.close() diff --git a/src/pyfia/carbon/litter.py b/src/pyfia/carbon/litter.py new file mode 100644 index 00000000..55a70681 --- /dev/null +++ b/src/pyfia/carbon/litter.py @@ -0,0 +1,414 @@ +""" +Litter carbon estimation from FIADB condition-level attributes. + +Litter — organic material on the forest floor including decomposing +leaves, needles, fine woody debris, and humus (duff) — is estimated in +the FIADB using the Domke et al. (2016) model, +which relates litter carbon density to geographic area, forest type +group, and stand age class. + +This estimator reads ``COND.CARBON_LITTER`` (short tons per acre), then +runs it through pyFIA's post-stratified aggregation pipeline to produce +per-acre and population estimates that match EVALIDator. + +Litter has no above-ground / below-ground split; the single +``CARBON_LITTER`` column represents the total pool. + +National magnitude: ~5,200 Tg C total litter, roughly 6 % of total +forest ecosystem carbon (GTR-NRS-154, Table 2). + +Public API: :func:`litter`. See its docstring for parameters, +examples, and the pool semantics. + +References +---------- +- Domke, G.M.; Perry, C.H.; Walters, B.F.; et al. (2016). Estimating + litter carbon stocks on forest land in the United States. Science of + the Total Environment, 557-558, 469-478. +- Woodall, C.W. et al. (2015). GTR-NRS-154 (FCAF methodology blueprint). +- USEPA (2024). NGHGI Annex 3.13. +""" + +from __future__ import annotations + +import logging + +import polars as pl + +from ..core import FIA +from ..estimation.base import AggregationResult, BaseEstimator +from ..estimation.columns import get_cond_columns as _get_cond_columns +from ..estimation.utils import ( + ensure_evalid_set, + ensure_fia_instance, + validate_aggregation_result, + validate_required_columns, +) + +logger = logging.getLogger(__name__) + + +class LitterEstimator(BaseEstimator): + """Litter carbon estimator. + + Reads pre-computed ``COND.CARBON_LITTER`` from the FIADB and + aggregates via the standard post-stratified estimation pipeline. + + This is a **condition-level** estimator — there is no tree-level data. + The TREE table is not loaded; data comes from ``COND x PLOT`` only. + The adjustment factor is ``ADJ_FACTOR_SUBP`` (subplot-level), + matching FIADB/EVALIDator conventions for condition-level carbon + attributes. + """ + + _estimator_label = "Litter" + + # ------------------------------------------------------------------ + # Table / column requirements + # ------------------------------------------------------------------ + + def get_required_tables(self) -> list[str]: + return ["COND", "PLOT", "POP_PLOT_STRATUM_ASSGN", "POP_STRATUM"] + + def get_tree_columns(self) -> list[str]: + return [] + + def get_cond_columns(self) -> list[str]: + cols = _get_cond_columns( + land_type=self.config.get("land_type", "forest"), + grp_by=self.config.get("grp_by"), + include_prop_basis=False, + ) + if "CARBON_LITTER" not in cols: + cols.append("CARBON_LITTER") + return cols + + # ------------------------------------------------------------------ + # Override apply_filters for condition-level data + # ------------------------------------------------------------------ + + def apply_filters(self, data: pl.LazyFrame) -> pl.LazyFrame: + """Apply condition/area filters only (no tree-type filtering).""" + from ..filtering import apply_area_filters, get_land_domain_indicator + + columns = data.collect_schema().names() + + if self.config.get("area_domain"): + data = apply_area_filters(data, area_domain=self.config["area_domain"]) + + land_type = self.config.get("land_type", "forest") + if land_type and land_type != "all" and "COND_STATUS_CD" in columns: + data = data.filter(get_land_domain_indicator(land_type)) + + return data + + # ------------------------------------------------------------------ + # Core estimation logic + # ------------------------------------------------------------------ + + def calculate_values(self, data: pl.LazyFrame) -> pl.LazyFrame: + """Alias CARBON_LITTER to CARBON_ACRE.""" + cond_carbon = pl.col("CARBON_LITTER").cast(pl.Float64).fill_null(0.0) + return data.with_columns(cond_carbon.alias("CARBON_ACRE")) + + # ------------------------------------------------------------------ + # Aggregation — condition-level with ADJ_FACTOR_SUBP + # ------------------------------------------------------------------ + + def aggregate_results(self, data: pl.LazyFrame | None) -> AggregationResult: + if data is None: + return AggregationResult( + results=pl.DataFrame(), + plot_tree_data=pl.DataFrame(), + group_cols=[], + ) + + validate_required_columns(data, ["PLT_CN", "CARBON_ACRE"], "litter carbon data") + + strat_data = self._get_stratification_data() + data_with_strat = data.join(strat_data, on="PLT_CN", how="inner") + + # Condition-level attribute: CARBON_ACRE is a density (tons/acre). + # The two-stage pipeline denominator is sum(CONDPROP_UNADJ * EXPNS), + # so the numerator must include CONDPROP_UNADJ to give the + # condition's contribution proportional to its area on the plot. + # ADJ_FACTOR_SUBP corrects for nonresponse at the subplot level. + data_with_strat = data_with_strat.with_columns( + pl.col("ADJ_FACTOR_SUBP").cast(pl.Float64).alias("ADJ_FACTOR") + ) + + data_with_strat = data_with_strat.with_columns( + ( + pl.col("CARBON_ACRE") + * pl.col("CONDPROP_UNADJ").cast(pl.Float64) + * pl.col("ADJ_FACTOR") + ).alias("CARBON_ADJ") + ) + + group_cols = self._setup_grouping() + + plot_tree_data, data_with_strat = self._preserve_plot_tree_data( + data_with_strat, + metric_cols=["CARBON_ADJ"], + group_cols=group_cols, + ) + + results = self._apply_two_stage_aggregation( + data_with_strat=data_with_strat, + metric_mappings={"CARBON_ADJ": "CONDITION_CARBON"}, + group_cols=group_cols, + use_grm_adjustment=False, + ) + + if not self.config.get("totals", True): + if "CARBON_TOTAL" in results.columns: + results = results.drop("CARBON_TOTAL") + + return AggregationResult( + results=results, + plot_tree_data=plot_tree_data, + group_cols=group_cols, + ) + + # ------------------------------------------------------------------ + # Variance and output formatting + # ------------------------------------------------------------------ + + def calculate_variance(self, agg_result: AggregationResult) -> pl.DataFrame: + validate_aggregation_result(agg_result, self._estimator_label) + metric_configs = [ + { + "adjusted_col": "CARBON_ADJ", + "acre_se_col": "CARBON_ACRE_SE", + "total_se_col": "CARBON_TOTAL_SE", + }, + ] + return self._calculate_variance_for_metrics(agg_result, metric_configs) + + def format_output(self, results: pl.DataFrame) -> pl.DataFrame: + year = self._extract_evaluation_year() + results = results.with_columns(pl.lit(year).alias("YEAR")) + results = results.with_columns(pl.lit("TOTAL").alias("POOL")) + + col_order = [ + "YEAR", + "POOL", + "CARBON_ACRE", + "CARBON_TOTAL", + "CARBON_ACRE_SE", + "CARBON_TOTAL_SE", + "N_PLOTS", + "N_TREES", + ] + + for col in results.columns: + if col not in col_order: + col_order.insert(1, col) + + final_cols = [col for col in col_order if col in results.columns] + return results.select(final_cols) + + +# ====================================================================== +# Public API +# ====================================================================== + + +def litter( + db: str | FIA, + pool: str = "total", + grp_by: str | list[str] | None = None, + land_type: str = "forest", + area_domain: str | None = None, + plot_domain: str | None = None, + totals: bool = True, + variance: bool = False, + most_recent: bool = False, +) -> pl.DataFrame: + """ + Estimate litter carbon from FIA data. + + Litter encompasses all organic material on the forest floor including + decomposing leaves, needles, fine woody debris (< 7.62 cm / 3 in + diameter), and humus (duff). Carbon density is estimated from the + Domke et al. (2016) model, parameterised on geographic area, forest + type group, and stand age. The FIADB pre-computes condition-level + carbon density and stores it in ``COND.CARBON_LITTER``. + + Nationally, litter carbon totals ~5,200 Tg C, representing ~6 % of + total forest ecosystem carbon (GTR-NRS-154, Table 2). Per-acre + values are typically 3-10 short tons/acre depending on forest type + and region. + + Parameters + ---------- + db : str | FIA + Database connection or path to FIA database. Can be either a path + string to a DuckDB/SQLite file or an existing FIA connection object. + pool : {'total'}, default 'total' + Carbon pool to estimate. Litter has no above-ground / + below-ground split — only ``'total'`` is accepted. + grp_by : str or list of str, optional + Column name(s) to group results by. Can be any column from the + FIA COND or PLOT tables. Common grouping columns include: + + - 'FORTYPCD': Forest type code + - 'FORTYPGRPCD': Forest type group code + - 'OWNGRPCD': Ownership group + - 'STATECD': State FIPS code + - 'COUNTYCD': County code + + For complete column descriptions, see USDA FIA Database User Guide. + land_type : {'forest', 'timber', 'all'}, default 'forest' + Land type to include in estimation: + + - 'forest': All forestland + - 'timber': Productive timberland only (unreserved, productive) + - 'all': All land conditions + area_domain : str, optional + SQL-like filter expression for area/condition-level filtering. + Example: ``"OWNGRPCD == 40 AND FORTYPCD == 161"``. + plot_domain : str, optional + SQL-like filter expression for plot-level filtering. + totals : bool, default True + If True, include population-level total estimates in addition to + per-acre values. + variance : bool, default False + If True, calculate and include variance and standard error + estimates following Bechtold & Patterson (2005). + most_recent : bool, default False + If True, automatically filter to the most recent EXPVOL evaluation + for each state in the database before estimation. + + Returns + ------- + pl.DataFrame + Litter carbon estimates with the following columns: + + - **YEAR** : int + Evaluation reference year from EVALID. + - **POOL** : str + Pool identifier — always ``'TOTAL'``. + - **CARBON_ACRE** : float + Carbon per acre in short tons. + - **CARBON_TOTAL** : float (if ``totals=True``) + Total carbon in short tons expanded to population level. + - **CARBON_ACRE_SE** : float (if ``variance=True``) + Standard error of the per-acre estimate. + - **CARBON_TOTAL_SE** : float (if ``variance=True`` and ``totals=True``) + Standard error of the population total. + - **N_PLOTS** : int + Number of FIA plots included in the estimation. + - **N_TREES** : int + Number of tree records (used for distribution weighting). + - **[grouping columns]** : various + Any columns specified in ``grp_by``. + + See Also + -------- + live_tree : Estimate live tree carbon using the NSVB framework. + standing_dead : Estimate standing dead tree carbon. + understory : Estimate understory vegetation carbon. + downed_dead : Estimate downed dead wood carbon. + soil_organic : Estimate soil organic carbon. + pyfia.carbon : Overview of all carbon pool estimators. + + Notes + ----- + **Methodology** + + Litter is not directly measured on standard FIA plots in a manner + that produces per-condition densities. Instead, the FIADB + pre-computes carbon density per condition using the Domke et al. + (2016) model, which uses empirical relationships between litter + carbon stocks and forest type, stand age, and geographic region. + + This estimator reads those pre-computed values directly from the COND + table and runs them through the standard post-stratified aggregation + pipeline, ensuring exact agreement with EVALIDator estimates. + + **No AG/BG split** + + Litter has no above-ground / below-ground partitioning. The + ``CARBON_LITTER`` column represents the entire pool. + + Examples + -------- + Total litter carbon per acre on forestland: + + >>> results = litter(db, pool="total") + >>> print(f"Carbon: {results['CARBON_ACRE'][0]:.3f} tons/acre") + + Litter carbon by forest type group: + + >>> results = litter(db, pool="total", grp_by="FORTYPGRPCD") + + Litter carbon on timberland with standard errors: + + >>> results = litter( + ... db, + ... land_type="timber", + ... variance=True, + ... ) + + References + ---------- + .. [1] Domke, G.M.; Perry, C.H.; Walters, B.F.; et al. (2016). + Estimating litter carbon stocks on forest land in the United + States. Science of the Total Environment, 557-558, 469-478. + .. [2] Woodall, C.W. et al. (2015). The current and future role of + forest carbon in the United States. Gen. Tech. Rep. NRS-154. + .. [3] USEPA (2024). Inventory of U.S. Greenhouse Gas Emissions and + Sinks, Chapter 6 and Annex 3.13. + """ + from ..validation import ( + validate_boolean, + validate_domain_expression, + validate_grp_by, + validate_land_type, + ) + + # ----- Validate pool ----- + pool = pool.lower() + if pool != "total": + raise ValueError( + f"Invalid pool '{pool}' for litter. " + f"Only 'total' is supported — litter has no AG/BG split." + ) + + # ----- Validate standard inputs ----- + land_type = validate_land_type(land_type) + grp_by = validate_grp_by(grp_by) + area_domain = validate_domain_expression(area_domain, "area_domain") + plot_domain = validate_domain_expression(plot_domain, "plot_domain") + totals = validate_boolean(totals, "totals") + variance = validate_boolean(variance, "variance") + most_recent = validate_boolean(most_recent, "most_recent") + + # ----- Resolve db + EVALID ----- + db, owns_db = ensure_fia_instance(db) + if most_recent and db.evalid is None: + db.clip_most_recent(eval_type="VOL") + else: + ensure_evalid_set(db, eval_type="VOL", estimator_name="litter") + + # ----- Build config and run estimator ----- + config = { + "pool": pool, + "grp_by": grp_by, + "by_species": False, + "by_size_class": False, + "land_type": land_type, + "area_domain": area_domain, + "plot_domain": plot_domain, + "totals": totals, + "variance": variance, + "most_recent": most_recent, + } + + try: + estimator = LitterEstimator(db, config) + return estimator.estimate() + finally: + if owns_db and hasattr(db, "close"): + db.close() diff --git a/src/pyfia/carbon/live_tree.py b/src/pyfia/carbon/live_tree.py new file mode 100644 index 00000000..df3fc5d4 --- /dev/null +++ b/src/pyfia/carbon/live_tree.py @@ -0,0 +1,372 @@ +""" +Live tree carbon estimation using the NSVB biomass framework. + +Recomputes above-ground live tree biomass tree-by-tree via the vectorized +NSVB pipeline in :mod:`pyfia.carbon.nsvb.equations`, converts to carbon +using species-specific S10a live-tree carbon fractions, and expands to +per-acre and population estimates via pyfia's post-stratified estimator. + +Belowground (BG) carbon is currently bridged directly to the FIADB +``TREE.CARBON_BG`` column; a native NSVB coarse-root model is deferred. +The BG bridge is acknowledged as architectural tech debt at +``pyfia/carbon/__init__.py``. + +Public API: :func:`live_tree`. See its docstring for parameters, +examples, and the pool semantics. +""" + +from __future__ import annotations + +import logging + +import polars as pl + +from ..core import FIA +from ..estimation.columns import get_tree_columns as _get_tree_columns +from ..estimation.utils import ( + ensure_evalid_set, + ensure_fia_instance, +) +from ._estimator_base import CarbonEstimatorBase +from .nsvb.carbon_fractions import ( + _compute_default_live_carbon_fraction, + load_carbon_fractions_live_df, +) +from .nsvb.equations import compute_nsvb_biomass + +logger = logging.getLogger(__name__) + + +class LiveTreeEstimator(CarbonEstimatorBase): + """Live tree carbon estimator using the NSVB biomass framework. + + Inherits shared infrastructure (reference-table loading, aggregation, + variance, formatting) from :class:`CarbonEstimatorBase`. The only + estimator-specific logic is :meth:`calculate_values` (NSVB pipeline + + S10a carbon fractions) and the tree column list. + """ + + _estimator_label = "LiveTree" + + def get_tree_columns(self) -> list[str]: + estimator_cols = ["SPCD", "DIA", "HT", "CULL", "CARBON_BG"] + return _get_tree_columns( + estimator_cols=estimator_cols, + grp_by=self.config.get("grp_by"), + ) + + def calculate_values(self, data: pl.LazyFrame) -> pl.LazyFrame: + """Run the NSVB pipeline and produce per-acre carbon columns. + + Steps: join REF_SPECIES → join PLOTGEOM/DIVISION → filter sub-inch + trees → NSVB biomass → S10a carbon fractions → BG bridge → + CARBON_ACRE. + """ + pool = self.config.get("pool", "ag").lower() + + # Join REF_SPECIES and PLOTGEOM/DIVISION + data = self._join_ref_species(data) + data = self._join_plotgeom_division(data) + + # Filter sub-inch trees (NSVB not parameterized below 1.0") + data = data.filter(pl.col("DIA") >= 1.0) + + # NSVB biomass + S10a carbon conversion + if pool in ("ag", "total"): + data = compute_nsvb_biomass(data) + default_frac = _compute_default_live_carbon_fraction() + cf_df = load_carbon_fractions_live_df() + data = data.join(cf_df.lazy(), on="SPCD", how="left") + data = data.with_columns( + pl.col("CARBON_FRAC_LIVE") + .fill_null(default_frac) + .alias("CARBON_FRAC_LIVE"), + ) + data = data.with_columns( + (pl.col("agb") * pl.col("CARBON_FRAC_LIVE")).alias("_CARBON_AG_LB") + ) + else: # pool == "bg" + data = data.with_columns(pl.lit(0.0).alias("_CARBON_AG_LB")) + + # BG bridge + CARBON_ACRE + data = self._apply_bg_bridge(data, pool) + data = self._compute_carbon_acre(data) + + return data + + +def live_tree( + db: str | FIA, + pool: str = "ag", + grp_by: str | list[str] | None = None, + by_species: bool = False, + by_size_class: bool = False, + land_type: str = "forest", + tree_domain: str | None = None, + area_domain: str | None = None, + plot_domain: str | None = None, + totals: bool = True, + variance: bool = False, + most_recent: bool = False, +) -> pl.DataFrame: + """ + Estimate live tree carbon from FIA data using the NSVB framework. + + Recomputes above-ground live tree biomass from scratch using the + National Scale Volume and Biomass (NSVB) framework of Westfall et al. + (2023, GTR-WO-104) — the same framework USDA FIA uses to populate the + FIADB ``CARBON_AG`` column for inventories from September 2023 onward. + Species-specific live carbon fractions from Table S10a (GTR-WO-104) + replace the flat ~0.47 multiplier used by ``pyfia.biomass()``, producing + carbon estimates that align with the EPA NGHGI LULUCF live tree pool. + + Belowground carbon is bridged directly to the FIADB pre-computed + ``TREE.CARBON_BG`` column; a native NSVB coarse-root model is + deferred. + + Parameters + ---------- + db : str | FIA + Database connection or path to FIA database. Can be either a path + string to a DuckDB/SQLite file or an existing FIA connection object. + pool : {'ag', 'bg', 'total'}, default 'ag' + Live tree carbon pool to estimate: + + - 'ag': Above-ground live tree carbon via the NSVB pipeline — + stem wood + stem bark + branches, harmonized to the directly- + predicted total AGB and converted to carbon via species-specific + S10a fractions. Foliage is excluded (not part of AGB in NSVB). + - 'bg': Below-ground live tree carbon (coarse roots) via a bridge + to FIADB ``TREE.CARBON_BG``. A native NSVB root model is deferred. + - 'total': ``'ag' + 'bg'`` (NSVB AG + FIADB BG bridge). + grp_by : str or list of str, optional + Column name(s) to group results by. Can be any column from the + FIA tables used in the estimation (PLOT, COND, TREE). Common + grouping columns include: + + - 'FORTYPCD': Forest type code + - 'OWNGRPCD': Ownership group (10=National Forest, 20=Other Federal, + 30=State/Local, 40=Private) + - 'STATECD': State FIPS code + - 'COUNTYCD': County code + - 'INVYR': Inventory year + - 'STDAGE': Stand age class + - 'SITECLCD': Site productivity class + + For complete column descriptions, see USDA FIA Database User Guide. + by_species : bool, default False + If True, group results by species code (SPCD). Convenience parameter + equivalent to adding 'SPCD' to ``grp_by``. + by_size_class : bool, default False + If True, group results by diameter size classes (1.0-4.9", 5.0-9.9", + 10.0-19.9", 20.0-29.9", 30.0+ in). + land_type : {'forest', 'timber', 'all'}, default 'forest' + Land type to include in estimation: + + - 'forest': All forestland + - 'timber': Productive timberland only (unreserved, productive) + - 'all': All land conditions + tree_domain : str, optional + SQL-like filter expression for tree-level filtering. Example: + ``"DIA >= 10.0 AND SPCD == 131"``. Applied on top of the live-tree + filter (``STATUSCD == 1``), which is always on for this function. + area_domain : str, optional + SQL-like filter expression for area/condition-level filtering. + Example: ``"OWNGRPCD == 40 AND FORTYPCD == 161"``. + plot_domain : str, optional + SQL-like filter expression for plot-level filtering. + totals : bool, default True + If True, include population-level total estimates in addition to + per-acre values. + variance : bool, default False + If True, calculate and include variance and standard error + estimates following Bechtold & Patterson (2005). + most_recent : bool, default False + If True, automatically filter to the most recent EXPVOL evaluation + for each state in the database before estimation. + + Returns + ------- + pl.DataFrame + Live tree carbon estimates with the following columns: + + - **YEAR** : int + Evaluation reference year from EVALID. + - **POOL** : str + Pool identifier — one of ``'AG'``, ``'BG'``, ``'TOTAL'``. + - **CARBON_ACRE** : float + Carbon per acre in short tons. + - **CARBON_TOTAL** : float (if ``totals=True``) + Total carbon in short tons expanded to population level. + - **CARBON_ACRE_SE** : float (if ``variance=True``) + Standard error of the per-acre estimate. + - **CARBON_TOTAL_SE** : float (if ``variance=True`` and ``totals=True``) + Standard error of the population total. + - **N_PLOTS** : int + Number of FIA plots included in the estimation. + - **N_TREES** : int + Number of individual tree records. + - **[grouping columns]** : various + Any columns specified in ``grp_by`` or via ``by_species`` / + ``by_size_class``. + + See Also + -------- + biomass : Estimate tree biomass (dry weight) using FIA's pre-computed DRYBIO columns. + pyfia.estimation.estimators.carbon.carbon : Legacy carbon estimator that reads the + FIADB ``CARBON_AG`` / ``CARBON_BG`` columns directly. ``live_tree`` is the + NSVB-native alternative; both should agree at the tree level for NSVB-era + inventories (Sep 2023 and later). + pyfia.carbon.nsvb.equations.compute_nsvb_biomass : The vectorized NSVB biomass + pipeline this function wraps. + + Notes + ----- + **NSVB Pipeline** + + For each live tree the function predicts: + + 1. Stem inside-bark wood volume (S1a) + 2. Stem bark volume (S2a) + 3. Stem bark biomass (S6a) + 4. Branch biomass (S7a) + 5. Total AGB (S8a), predicted directly from D and H + + The first four are summed and harmonized proportionally to the + directly-predicted total AGB (which becomes the truth), yielding + ``w_wood + w_bark + w_branch == agb`` by construction. Cull-reduced + wood weight uses the Harmon et al. (2011) ``DECAYCD=3`` density + proportions (0.54 hardwood, 0.92 softwood). The hardwood/softwood + split is the ``SPCD < 300`` rule, which is consistent with the + NSVB Model 2 ``k`` constant selection and correctly classifies + SPCD=10 (fir spp.) as softwood despite S10a's misclassification. + + Carbon = AGB × species-specific S10a fraction. Species missing from + S10a fall back to the S10a arithmetic mean (~0.4741), with a + warn-once log entry. + + **Belowground Bridge** + + The current implementation does not include the Heath et al. (2009) + coarse-root model. When ``pool in ('bg', 'total')``, the function reads + FIADB ``TREE.CARBON_BG`` directly and adds it to the estimate. A native + NSVB BG model is deferred. + + **EVALID Handling** + + If no EVALID is set on the database and ``most_recent=True``, the + function auto-selects the most recent EXPVOL evaluation. For explicit + control, call ``db.clip_by_evalid(...)`` before calling + ``live_tree``. + + Examples + -------- + Above-ground live tree carbon per acre on forestland: + + >>> results = live_tree(db, pool="ag") + >>> print(f"Carbon: {results['CARBON_ACRE'][0]:.1f} tons/acre") + + Total live tree carbon (AG + BG bridge) by ownership group: + + >>> results = live_tree(db, pool="total", grp_by="OWNGRPCD") + >>> for row in results.iter_rows(named=True): + ... print(f"OWNGRPCD {row['OWNGRPCD']}: {row['CARBON_ACRE']:.2f} tons/acre") + + Above-ground carbon by species on timberland with standard errors: + + >>> results = live_tree( + ... db, + ... pool="ag", + ... by_species=True, + ... land_type="timber", + ... variance=True, + ... ) + + Large live tree carbon (≥ 20" DBH) by forest type: + + >>> results = live_tree( + ... db, + ... pool="ag", + ... grp_by="FORTYPCD", + ... tree_domain="DIA >= 20.0", + ... totals=True, + ... ) + """ + from ..validation import ( + validate_boolean, + validate_domain_expression, + validate_grp_by, + validate_land_type, + ) + + # ----- Validate pool ----- + pool = pool.lower() + valid_pools = {"ag", "bg", "total"} + if pool not in valid_pools: + raise ValueError( + f"Invalid pool '{pool}'. Must be one of: {sorted(valid_pools)}" + ) + + # ----- Validate standard estimator inputs ----- + land_type = validate_land_type(land_type) + grp_by = validate_grp_by(grp_by) + tree_domain = validate_domain_expression(tree_domain, "tree_domain") + area_domain = validate_domain_expression(area_domain, "area_domain") + plot_domain = validate_domain_expression(plot_domain, "plot_domain") + by_species = validate_boolean(by_species, "by_species") + by_size_class = validate_boolean(by_size_class, "by_size_class") + totals = validate_boolean(totals, "totals") + variance = validate_boolean(variance, "variance") + most_recent = validate_boolean(most_recent, "most_recent") + + # ----- Resolve db + EVALID ----- + db, owns_db = ensure_fia_instance(db) + # Live tree carbon uses EXPVOL evaluations (same as biomass). + if most_recent and db.evalid is None: + db.clip_most_recent(eval_type="VOL") + else: + ensure_evalid_set(db, eval_type="VOL", estimator_name="live_tree") + + # ----- Build config and run estimator ----- + config = { + "pool": pool, + "grp_by": grp_by, + "by_species": by_species, + "by_size_class": by_size_class, + "land_type": land_type, + "tree_type": "live", + "tree_domain": tree_domain, + "area_domain": area_domain, + "plot_domain": plot_domain, + "totals": totals, + "variance": variance, + "most_recent": most_recent, + } + + try: + estimator = LiveTreeEstimator(db, config) + if pool == "total": + # The cross-era warning is best-effort: if we can't determine + # the inventory year (EVALID parse failures, missing POP_EVAL, + # type coercion problems), skip the warning rather than fail + # the whole estimation. + try: + year = estimator._extract_evaluation_year() + if int(year) < 2024: + logger.warning( + "live_tree(pool='total'): selected EVALID year (%d) " + "pre-dates the NSVB framework transition " + "(September 2023). The BG bridge reads FIADB " + "TREE.CARBON_BG directly, which for pre-NSVB " + "inventories was computed via legacy Jenkins-based " + "allometry — combining it with NSVB-recomputed AG " + "may produce cross-era inconsistencies. Use " + "pool='ag' if you need NSVB-only consistency.", + int(year), + ) + except (ValueError, TypeError, AttributeError, IndexError, KeyError) as exc: + logger.debug("Skipping live_tree year warning: %s", exc) + return estimator.estimate() + finally: + if owns_db and hasattr(db, "close"): + db.close() diff --git a/src/pyfia/carbon/nsvb/__init__.py b/src/pyfia/carbon/nsvb/__init__.py new file mode 100644 index 00000000..238a70ac --- /dev/null +++ b/src/pyfia/carbon/nsvb/__init__.py @@ -0,0 +1,77 @@ +""" +NSVB (Westfall et al. 2023, GTR-WO-104) equation library and coefficient loaders. + +This subpackage implements the National Scale Volume and Biomass framework +from scratch in pure Python, validated against the worked examples in the +GTR-WO-104 source PDF. The reverse-engineering approach is intentional — see +``pyfia_carbon_tech_spec.md`` section 1.2 for the rationale. + +Phase 1 implements Models 1, 2, 4, and 5 (the model forms used by the live +tree biomass pipeline). Models 3 and 6 are deferred to a later phase. +""" + +from pyfia.carbon.nsvb.carbon_fractions import ( + DEFAULT_LIVE_CARBON_FRACTION, + get_carbon_fraction_dead, + get_carbon_fraction_live, + load_carbon_fractions_dead, + load_carbon_fractions_dead_df, + load_carbon_fractions_live, + load_carbon_fractions_live_df, + load_dead_decay_proportions_df, +) +from pyfia.carbon.nsvb.coefficients import ( + CoefficientTables, + VectorizedLookupTables, + build_division_lookup, + build_jenkins_lookup, + build_species_level_lookup, + ecosubcd_to_division, + get_vectorized_lookup_tables, + load_nsvb_coefficients, + lookup_coefficients, +) +from pyfia.carbon.nsvb.equations import ( + Coefficients, + TreeBiomassResult, + compute_nsvb_biomass, + compute_nsvb_dead_biomass, + harmonize_components, + model_1, + model_2, + model_4, + model_5_jenkins, + nsvb_biomass_expr, + predict_tree_biomass, +) + +__all__ = [ + "Coefficients", + "CoefficientTables", + "DEFAULT_LIVE_CARBON_FRACTION", + "TreeBiomassResult", + "VectorizedLookupTables", + "build_division_lookup", + "build_jenkins_lookup", + "build_species_level_lookup", + "compute_nsvb_biomass", + "compute_nsvb_dead_biomass", + "ecosubcd_to_division", + "get_carbon_fraction_dead", + "get_carbon_fraction_live", + "get_vectorized_lookup_tables", + "harmonize_components", + "load_carbon_fractions_dead", + "load_carbon_fractions_dead_df", + "load_carbon_fractions_live", + "load_carbon_fractions_live_df", + "load_dead_decay_proportions_df", + "load_nsvb_coefficients", + "lookup_coefficients", + "model_1", + "model_2", + "model_4", + "model_5_jenkins", + "nsvb_biomass_expr", + "predict_tree_biomass", +] diff --git a/src/pyfia/carbon/nsvb/carbon_fractions.py b/src/pyfia/carbon/nsvb/carbon_fractions.py new file mode 100644 index 00000000..7705aa66 --- /dev/null +++ b/src/pyfia/carbon/nsvb/carbon_fractions.py @@ -0,0 +1,318 @@ +""" +Species-specific carbon fraction lookups for NSVB live and dead trees. + +Implements the species-level carbon fraction lookup from Tables S10a (live) and +S10b (dead, by hardwood/softwood × DECAYCD) of GTR-WO-104. Live tree carbon +fractions average ~47.4% across species but vary from roughly 40% to 55% +depending on species; the flat 0.47 multiplier used by the existing pyFIA +``biomass()`` estimator is replaced here with a species-specific lookup. + +S10a (live) powers ``pyfia.carbon.live_tree``. S10b (dead) and the related +``REF_TREE_DECAY_PROP`` density / bark / branch loss proportions power +``pyfia.carbon.standing_dead`` (Phase 2). Both pipelines hit the same +hardwood/softwood × DECAYCD lookup, so the loaders are colocated here. +""" + +from __future__ import annotations + +import functools +import logging +from importlib import resources +from typing import Any + +import polars as pl + +logger = logging.getLogger(__name__) + +# Track which unknown SPCDs we've already warned about, to avoid log spam when +# processing 1M-tree state inventories. +_warned_unknown_spcds: set[int] = set() + + +@functools.cache +def _compute_default_live_carbon_fraction() -> float: + """Compute the S10a arithmetic-mean live carbon fraction. + + Backs the ``DEFAULT_LIVE_CARBON_FRACTION`` module attribute via PEP 562 + ``__getattr__``. Calculated lazily (on first access) so that importing + this module does no file I/O, and cached so subsequent accesses are O(1). + + The reason this is computed rather than hardcoded: the S10a CSV is + vendored under ``pyfia.carbon.nsvb.data``, and a future re-vendor could + perturb the population mean. A hardcoded constant would silently drift; + a lazy computation always reflects the current CSV. + """ + table = load_carbon_fractions_live() + return sum(table.values()) / len(table) + + +def __getattr__(name: str) -> Any: + """PEP 562 lazy module attribute resolver. + + Enables ``from pyfia.carbon.nsvb.carbon_fractions import DEFAULT_LIVE_CARBON_FRACTION`` + to work as if the constant were defined at the top of the module, while + actually computing the value lazily from the S10a table on first access. + After the first access the value is cached in ``globals()`` so lookups + become standard attribute access. + """ + if name == "DEFAULT_LIVE_CARBON_FRACTION": + value = _compute_default_live_carbon_fraction() + globals()[name] = value + return value + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + +@functools.lru_cache(maxsize=1) +def load_carbon_fractions_live() -> dict[int, float]: + """Load Table S10a live tree carbon fractions as a SPCD-keyed dict. + + The vendored CSV stores values as percent (e.g., ``48.04`` for 48.04%); + this loader divides by 100 so callers receive a fraction in the + [0.40, 0.55] range. Cached at process level via ``@lru_cache``. + + This is the scalar reference loader used by ``get_carbon_fraction_live`` + and by the PR 1 test oracle. The vectorized live-tree estimator uses + :func:`load_carbon_fractions_live_df` instead, which returns a Polars + DataFrame ready for joining to a trees frame on ``SPCD``. + + Returns + ------- + dict[int, float] + Map from FIA species code to live carbon fraction (decimal, not percent). + """ + data_pkg = resources.files("pyfia.carbon.nsvb.data") + with resources.as_file(data_pkg / "carbon_fraction_live.csv") as path: + df = pl.read_csv(path) + return { + int(row["SPCD"]): float(row["fia_wood_c"]) / 100.0 + for row in df.iter_rows(named=True) + } + + +@functools.lru_cache(maxsize=1) +def load_carbon_fractions_live_df() -> pl.DataFrame: + """Load Table S10a live tree carbon fractions as a join-ready DataFrame. + + Returns a 2-column DataFrame ``(SPCD Int64, CARBON_FRAC_LIVE Float64)`` + suitable for a left join against a trees LazyFrame in the vectorized + live-tree estimator. The ``CARBON_FRAC_LIVE`` column is already + converted from percent to decimal (e.g., 48.04 → 0.4804), matching + the units returned by :func:`load_carbon_fractions_live`. + + Trees whose SPCD has no row in S10a should be filled with + :data:`DEFAULT_LIVE_CARBON_FRACTION` after the join using + ``pl.col("CARBON_FRAC_LIVE").fill_null(DEFAULT_LIVE_CARBON_FRACTION)``. + + Cached at process level via ``@lru_cache``. + + Returns + ------- + pl.DataFrame + Columns ``(SPCD, CARBON_FRAC_LIVE)`` with one row per FIA species + code present in S10a (~2,676 rows). + """ + data_pkg = resources.files("pyfia.carbon.nsvb.data") + with resources.as_file(data_pkg / "carbon_fraction_live.csv") as path: + df = pl.read_csv(path, schema_overrides={"SPCD": pl.Int64}) + return df.select( + [ + pl.col("SPCD"), + (pl.col("fia_wood_c").cast(pl.Float64) / 100.0).alias("CARBON_FRAC_LIVE"), + ] + ) + + +def get_carbon_fraction_live(spcd: int, fallback: float | None = None) -> float: + """Look up the live carbon fraction for a given species code. + + Parameters + ---------- + spcd : int + FIA species code from ``TREE.SPCD``. + fallback : float, optional + Carbon fraction to return if ``spcd`` has no row in S10a. Defaults + to the S10a arithmetic mean (computed lazily from the vendored table, + approximately 0.4741). The first time a given unknown SPCD is + encountered, a warning is logged. + + Returns + ------- + float + Live carbon fraction in decimal form (e.g., 0.4804 for SPCD=10). + """ + if fallback is None: + fallback = _compute_default_live_carbon_fraction() + table = load_carbon_fractions_live() + if spcd in table: + return table[spcd] + if spcd not in _warned_unknown_spcds: + _warned_unknown_spcds.add(spcd) + logger.warning( + "SPCD=%d not found in S10a live carbon fractions; falling back to " + "S10a mean (%.4f). Future occurrences for this SPCD will not log.", + spcd, + fallback, + ) + return fallback + + +@functools.lru_cache(maxsize=1) +def load_carbon_fractions_dead() -> dict[tuple[str, int], float]: + """Load Table S10b dead tree carbon fractions, keyed by (hw_sw, DECAYCD). + + Returns + ------- + dict[tuple[str, int], float] + Map from ``("hardwood" | "softwood", decay_class_1_to_5)`` to carbon + fraction (decimal). Source CSV stores values as percent; this loader + divides by 100. + """ + data_pkg = resources.files("pyfia.carbon.nsvb.data") + with resources.as_file(data_pkg / "carbon_fraction_dead.csv") as path: + df = pl.read_csv(path) + out: dict[tuple[str, int], float] = {} + for row in df.iter_rows(named=True): + hw_sw = row["S/H"].strip().lower() + decay = int(row["Decay code"]) + frac = float(row["C fraction"]) / 100.0 + out[(hw_sw, decay)] = frac + return out + + +@functools.lru_cache(maxsize=1) +def load_carbon_fractions_dead_df() -> pl.DataFrame: + """Load Table S10b dead tree carbon fractions as a join-ready DataFrame. + + Returns a 3-column DataFrame ``(hw_sw Utf8, DECAYCD Int64, + CARBON_FRAC_DEAD Float64)`` suitable for a left join against a trees + LazyFrame in the vectorized standing-dead estimator. The + ``CARBON_FRAC_DEAD`` column is converted from percent to decimal + (e.g., 47.0 → 0.4700) at load time, matching the units returned by + :func:`load_carbon_fractions_dead`. + + Cached at process level via ``@lru_cache``. + + Returns + ------- + pl.DataFrame + Columns ``(hw_sw, DECAYCD, CARBON_FRAC_DEAD)`` with one row per + ``(hardwood|softwood) × decay class 1-5`` combination (10 rows total). + """ + data_pkg = resources.files("pyfia.carbon.nsvb.data") + with resources.as_file(data_pkg / "carbon_fraction_dead.csv") as path: + df = pl.read_csv(path) + return df.select( + [ + pl.col("S/H").str.strip_chars().str.to_lowercase().alias("hw_sw"), + pl.col("Decay code").cast(pl.Int64).alias("DECAYCD"), + (pl.col("C fraction").cast(pl.Float64) / 100.0).alias("CARBON_FRAC_DEAD"), + ] + ) + + +def get_carbon_fraction_dead(hw_sw: str, decaycd: int) -> float: + """Look up the dead tree carbon fraction by hardwood/softwood and decay class. + + Parameters + ---------- + hw_sw : str + ``"hardwood"`` or ``"softwood"``. + decaycd : int + FIA decay class (1-5). + + Returns + ------- + float + Dead carbon fraction in decimal form. + + Raises + ------ + KeyError + If the (hw_sw, decaycd) pair is not in S10b. + """ + table = load_carbon_fractions_dead() + return table[(hw_sw.lower(), decaycd)] + + +@functools.lru_cache(maxsize=1) +def load_dead_cr_prop_df() -> pl.DataFrame: + """Load Table S11 mean intact crown ratios for standing dead trees. + + The vendored ``dead_cr_prop.csv`` mirrors the FIADB + ``REF_TREE_STND_DEAD_CR_PROP`` table — the mean compacted crown ratio + for intact standing dead trees by Bailey ecoregion province (ECOPROV) + and hardwood/softwood classification. Used by the Phase 2.5 broken-top + correction in :func:`pyfia.carbon.nsvb.equations.compute_nsvb_dead_biomass` + to estimate how much branch biomass is missing above the break point. + + Source: GTR-WO-104 Table S11 (Westfall et al. 2023). + + Returns + ------- + pl.DataFrame + Columns ``(ECOPROV Utf8, hw_sw Utf8, CR_MEAN Float64)`` with one + row per province × hardwood/softwood combination (~88 rows including + the UNDEFINED fallback). ``CR_MEAN`` is in percent (e.g., 43.9 + means 43.9% crown ratio). Ready for a left join on + ``["ECOPROV", "hw_sw"]``. + """ + data_pkg = resources.files("pyfia.carbon.nsvb.data") + with resources.as_file(data_pkg / "dead_cr_prop.csv") as path: + df = pl.read_csv( + path, + schema_overrides={ + "ECOPROV": pl.Utf8, + "hw_sw": pl.Utf8, + "CR_MEAN": pl.Float64, + }, + ) + return df + + +@functools.lru_cache(maxsize=1) +def load_dead_decay_proportions_df() -> pl.DataFrame: + """Load the FIADB ``REF_TREE_DECAY_PROP`` density / loss proportions. + + The vendored ``dead_decay_proportions.csv`` file mirrors the canonical + FIADB ``REF_TREE_DECAY_PROP`` table (FIADB User Guide v9.1 §11.36) and + matches the consolidated NSVB hardwood/softwood × DECAYCD values published + in GTR-WO-104 Table 1 (Westfall et al. 2023). It supplies the three + multiplicative reduction factors that ``pyfia.carbon.standing_dead`` + applies to the gross NSVB component biomass: + + - ``DENSITY_PROP`` — fraction of stem wood biomass remaining after decay + - ``BARK_LOSS_PROP`` — fraction of stem bark biomass remaining after decay + (despite the ``LOSS`` suffix in the FIADB column name, the value is the + *remaining* proportion, not the *lost* proportion — this matches the + FIADB User Guide §11.36 description and the verbatim Appendix K formula + ``BARK_LOSS_PROP * Bark``) + - ``BRANCH_LOSS_PROP`` — fraction of branch biomass remaining after decay + + Returns + ------- + pl.DataFrame + Columns ``(hw_sw, DECAYCD, DENSITY_PROP, BARK_LOSS_PROP, + BRANCH_LOSS_PROP)`` with one row per ``(hardwood|softwood) × decay + class 1-5`` combination (10 rows total). Ready for a left join on + ``["hw_sw", "DECAYCD"]``. + + Notes + ----- + The five values per row are constants — they do not vary by species + within the hardwood/softwood class. This is the FIADB-canonical + parameterization; finer per-species DRFs from Harmon et al. (2011) + Table 7 are not used by FIADB and are not vendored here. + """ + data_pkg = resources.files("pyfia.carbon.nsvb.data") + with resources.as_file(data_pkg / "dead_decay_proportions.csv") as path: + df = pl.read_csv( + path, + schema_overrides={ + "hw_sw": pl.Utf8, + "DECAYCD": pl.Int64, + "DENSITY_PROP": pl.Float64, + "BARK_LOSS_PROP": pl.Float64, + "BRANCH_LOSS_PROP": pl.Float64, + }, + ) + return df diff --git a/src/pyfia/carbon/nsvb/coefficients.py b/src/pyfia/carbon/nsvb/coefficients.py new file mode 100644 index 00000000..76895578 --- /dev/null +++ b/src/pyfia/carbon/nsvb/coefficients.py @@ -0,0 +1,523 @@ +""" +NSVB coefficient table loaders and lookup precedence. + +Loads the vendored CSVs from ``pyfia.carbon.nsvb.data`` via ``importlib.resources`` +(wheel-safe) and provides per-tree coefficient resolution following the NSVB +lookup precedence documented in the GTR-WO-104 worked examples. + +Lookup precedence (per ``gtr_wo104_westfall2023.md:684``): + +1. ``SPCD + DIVISION + STDORGCD`` exact match +2. ``SPCD + DIVISION`` (STDORGCD null) +3. ``SPCD`` only (DIVISION and STDORGCD both null) — the species-level fallback +4. ``JENKINS_SPGRPCD`` fallback (Model 5, with WDSG multiplication required) + +The CSVs already include species-level fallback rows; we do not need to synthesize +them. Phase 1.5 added the ``ECOSUBCD → Bailey DIVISION`` mapping via +:func:`ecosubcd_to_division`, activating Level 2 of the lookup precedence. +Level 1 (STDORGCD) is still unused (~10 rows across all 5 tables). +""" + +from __future__ import annotations + +import functools +from dataclasses import dataclass +from importlib import resources + +import polars as pl + +# Filename map for the vendored CSVs (relative to ``pyfia.carbon.nsvb.data``). +_CSV_FILES = { + "volib_spcd": "volib_spcd.csv", + "volib_jenkins": "volib_jenkins.csv", + "volbk_spcd": "volbk_spcd.csv", + "volbk_jenkins": "volbk_jenkins.csv", + "bark_biomass_spcd": "bark_biomass_spcd.csv", + "bark_biomass_jenkins": "bark_biomass_jenkins.csv", + "branch_biomass_spcd": "branch_biomass_spcd.csv", + "branch_biomass_jenkins": "branch_biomass_jenkins.csv", + "total_biomass_spcd": "total_biomass_spcd.csv", + "total_biomass_jenkins": "total_biomass_jenkins.csv", +} + + +@dataclass(frozen=True) +class CoefficientTables: + """Bundle of all NSVB coefficient tables loaded as Polars DataFrames. + + Each ``*_spcd`` table has columns + ``SPCD, DIVISION, STDORGCD, model, a, a1, b, b1, c, c1`` and each + ``*_jenkins`` table has columns ``JENKINS_SPGRPCD, model, a, b, c`` + (or close to it — the schemas vary slightly across the S*b tables). + """ + + volib_spcd: pl.DataFrame + volib_jenkins: pl.DataFrame + volbk_spcd: pl.DataFrame + volbk_jenkins: pl.DataFrame + bark_biomass_spcd: pl.DataFrame + bark_biomass_jenkins: pl.DataFrame + branch_biomass_spcd: pl.DataFrame + branch_biomass_jenkins: pl.DataFrame + total_biomass_spcd: pl.DataFrame + total_biomass_jenkins: pl.DataFrame + + +@dataclass(frozen=True) +class VectorizedLookupTables: + """Bundle of join-ready NSVB coefficient lookup tables for the vectorized path. + + Each component is represented by three parallel lookups for Levels 2–4 of + the NSVB precedence: + + - ``*_div``: DIVISION-specific rows (DIVISION non-null + STDORGCD null) with + the columns ``(SPCD, DIVISION, model, a, b, b1, c)`` — joined on the + ``(SPCD, DIVISION)`` composite key (Level 2). + - ``*_spcd``: species-level rows (DIVISION null + STDORGCD null) with the + columns ``(SPCD, model, a, b, b1, c)`` — ready for a left join on + ``SPCD`` (Level 3). + - ``*_jen``: Jenkins-group fallback rows with the columns + ``(JENKINS_SPGRPCD, model, a, b, b1, c)`` — ready for a left join on + ``JENKINS_SPGRPCD`` (Level 4). ``b1`` is synthesized as ``0.0`` because + the Jenkins tables only carry ``(a, b, c)`` and Model 5 (the only form + Jenkins rows dispatch to) does not use ``b1``. + + The vectorized orchestrator runs all three joins per component and then + coalesces DIVISION first, species-level second, Jenkins third, replicating + the NSVB lookup precedence (Level 2 → Level 3 → Level 4) without any + Python-level loops. Level 1 (SPCD + DIVISION + STDORGCD) is still dead + code in Phase 1.5 — only ~10 rows across all 5 tables — and is deferred + until the validation gate justifies it. + """ + + volib_spcd: pl.DataFrame + volib_jen: pl.DataFrame + volib_div: pl.DataFrame + volbk_spcd: pl.DataFrame + volbk_jen: pl.DataFrame + volbk_div: pl.DataFrame + bark_bio_spcd: pl.DataFrame + bark_bio_jen: pl.DataFrame + bark_bio_div: pl.DataFrame + branch_bio_spcd: pl.DataFrame + branch_bio_jen: pl.DataFrame + branch_bio_div: pl.DataFrame + total_agb_spcd: pl.DataFrame + total_agb_jen: pl.DataFrame + total_agb_div: pl.DataFrame + + +# Common coefficient columns used by the vectorized path. Matches the inputs +# consumed by ``nsvb_biomass_expr``. +_VECTORIZED_COEF_COLS = ("model", "a", "b", "b1", "c") + + +def build_species_level_lookup(table_spcd: pl.DataFrame) -> pl.DataFrame: + """Prepare a ``*_spcd`` coefficient table for vectorized SPCD joins. + + Filters to Phase 1's species-level rows (``DIVISION`` is null AND + ``STDORGCD`` is null), selects the common coefficient columns used by + the vectorized biomass expression, and returns a DataFrame keyed on + ``SPCD``. The DIVISION-specific rows (Levels 1-2 of the NSVB lookup + precedence) are deliberately dropped because Phase 1 has no + ``PLOT.ECOSUBCD → DIVISION`` mapping. + + Also drops Model 2 / Model 4 rows with a null ``b1`` as a defensive + measure. In the downstream ``_join_and_eval_component`` coalesce, a + null ``b1`` on the species-level side would silently fall back to the + Jenkins ``b1=0.0`` synthetic value and corrupt the Model 2/4 math + row-wise. The current vendored CSVs have no such rows (all null + ``b1`` entries are on Model 1 rows, which do not consume ``b1``), + so this filter is a no-op today. It is a regression guard against + future CSV re-vendor drift: a rogue null ``b1`` on a Model 2/4 row + will be dropped here and the SPCD will fall through to Jenkins as a + whole, rather than silently producing wrong per-row math. + + Parameters + ---------- + table_spcd : pl.DataFrame + One of the 5 ``*_spcd`` coefficient tables from + :class:`CoefficientTables`. + + Returns + ------- + pl.DataFrame + Columns ``(SPCD, model, a, b, b1, c)``. One row per SPCD that has a + species-level entry in the source table. + """ + return table_spcd.filter( + pl.col("DIVISION").is_null() + & pl.col("STDORGCD").is_null() + & ~(pl.col("model").is_in([2, 4]) & pl.col("b1").is_null()) + ).select(["SPCD", *_VECTORIZED_COEF_COLS]) + + +def build_jenkins_lookup(table_jenkins: pl.DataFrame) -> pl.DataFrame: + """Prepare a ``*_jenkins`` coefficient table for vectorized joins. + + Selects the common coefficient columns and synthesizes a ``b1`` column + set to ``0.0`` — Jenkins tables only carry ``(a, b, c)`` because Model 5 + (the only form Jenkins rows ever dispatch to) has no ``b1`` parameter. + The synthesized column keeps the schema identical to the species-level + lookup so the downstream orchestrator can coalesce across both tables + uniformly. + + Parameters + ---------- + table_jenkins : pl.DataFrame + One of the 5 ``*_jenkins`` coefficient tables from + :class:`CoefficientTables`. + + Returns + ------- + pl.DataFrame + Columns ``(JENKINS_SPGRPCD, model, a, b, b1, c)``. + """ + return table_jenkins.select( + [ + pl.col("JENKINS_SPGRPCD"), + pl.col("model"), + pl.col("a"), + pl.col("b"), + pl.lit(0.0, dtype=pl.Float64).alias("b1"), + pl.col("c"), + ] + ) + + +def build_division_lookup(table_spcd: pl.DataFrame) -> pl.DataFrame: + """Prepare a ``*_spcd`` coefficient table for vectorized ``(SPCD, DIVISION)`` + joins (Level 2 of the NSVB lookup precedence). + + Filters to rows where ``DIVISION`` is non-null AND ``STDORGCD`` is null + (the DIVISION-specific Bailey ecoprovince refinements that Phase 1 of + the carbon estimator skipped). Returns a DataFrame keyed on the + composite ``(SPCD, DIVISION)``; the vectorized orchestrator joins this + first, then falls through to the species-level lookup (Level 3) and + the Jenkins fallback (Level 4) via a 3-way coalesce. + + Level 1 of the NSVB precedence (``SPCD + DIVISION + STDORGCD``) is still + deliberately excluded — across all 5 coefficient tables it is only ~10 + rows and would require threading ``COND.STDORGCD`` through the pipeline. + Revisit once the DIVISION closure has been measured. + + Applies the same defensive null-``b1`` filter as + :func:`build_species_level_lookup`: a Model 2 / Model 4 row with a null + ``b1`` would silently mix rows in the downstream coalesce, so such rows + are dropped from the lookup. + + Parameters + ---------- + table_spcd : pl.DataFrame + One of the 5 ``*_spcd`` coefficient tables from + :class:`CoefficientTables`. + + Returns + ------- + pl.DataFrame + Columns ``(SPCD, DIVISION, model, a, b, b1, c)``. One row per + unique ``(SPCD, DIVISION)`` pair with a Level-2 entry. + """ + return table_spcd.filter( + pl.col("DIVISION").is_not_null() + & pl.col("STDORGCD").is_null() + & ~(pl.col("model").is_in([2, 4]) & pl.col("b1").is_null()) + ).select(["SPCD", "DIVISION", *_VECTORIZED_COEF_COLS]) + + +def ecosubcd_to_division(ecosubcd: str | None) -> str | None: + """Extract the Bailey DIVISION code from a ``PLOTGEOM.ECOSUBCD`` value. + + ``ECOSUBCD`` is a 5-7 character Bailey ecoprovince subsection code + (e.g., ``"231Ae"`` for the Southeastern Mixed Forest section 231A, + subsection 231Ae, or ``"M231Aa"`` for the Ouachita Mixed Forest + mountain variant). The Bailey hierarchy is: + + :: + + Domain → Division → Province → Section → Subsection + 200 → 230 → 231 → 231A → 231Ae + + This function walks one level up the hierarchy from Subsection to + Division by: + + - extracting the 3-digit Province code (first 3 chars after any ``M``) + - replacing its last digit with ``"0"`` to obtain the Division + - preserving the ``"M"`` prefix for mountain Divisions + + Examples + -------- + >>> ecosubcd_to_division("231Ae") + '230' + >>> ecosubcd_to_division("232Bh") + '230' + >>> ecosubcd_to_division("M231Aa") + 'M230' + >>> ecosubcd_to_division("220Eb") + '220' + >>> ecosubcd_to_division(None) is None + True + >>> ecosubcd_to_division("") is None + True + >>> ecosubcd_to_division("XYZ") is None + True + + Parameters + ---------- + ecosubcd : str or None + The ECOSUBCD value from ``PLOTGEOM.ECOSUBCD`` or ``PLOT.ECOSUBCD`` + (the column exists on both tables — pyfia's DataMart CSV downloads + pull it reliably from ``PLOTGEOM``). + + Returns + ------- + str or None + The Bailey DIVISION code matching the ``DIVISION`` column in the + NSVB coefficient tables (e.g., ``"230"``, ``"M230"``, ``"240"``), + or ``None`` if the input is null, empty, or malformed. + """ + if ecosubcd is None: + return None + s = ecosubcd.strip().upper() + if not s: + return None + m_prefix = "" + if s.startswith("M"): + m_prefix = "M" + s = s[1:] + if len(s) < 3 or not s[:2].isdigit() or not s[2].isdigit(): + return None + return m_prefix + s[:2] + "0" + + +def ecosubcd_to_division_expr(col: str = "ECOSUBCD") -> pl.Expr: + """Vectorized Polars expression equivalent of :func:`ecosubcd_to_division`. + + Replaces ``pl.col(col).map_elements(ecosubcd_to_division)`` with pure + Polars string operations — no per-row Python dispatch. Logic: + + 1. Uppercase + strip the input. + 2. If it starts with ``"M"``, take ``"M" + chars[1:3] + "0"``. + 3. Otherwise take ``chars[0:2] + "0"``. + 4. Null / empty / malformed inputs produce null. + + Returns + ------- + pl.Expr + Expression producing the Bailey DIVISION code (Utf8, nullable). + """ + raw = pl.col(col).str.strip_chars().str.to_uppercase() + has_m = raw.str.starts_with("M") + # Characters after any M prefix + digits_m = raw.str.slice(1) # "M231Aa" → "231Aa" + digits_no_m = raw # "231Ae" → "231Ae" + + # Build division: first 2 digits + "0", with M prefix preserved + div_m = pl.lit("M") + digits_m.str.slice(0, 2) + pl.lit("0") + div_no_m = digits_no_m.str.slice(0, 2) + pl.lit("0") + + # Validate: the 3rd character (after M if present) must be a digit, + # and minimum 3 chars after prefix. Use a regex match for the prefix + # digits to detect malformed values. + valid_m = digits_m.str.contains(r"^\d{3}") + valid_no_m = digits_no_m.str.contains(r"^\d{3}") + + return ( + pl.when(raw.is_null() | (raw == "")) + .then(pl.lit(None, dtype=pl.Utf8)) + .when(has_m & valid_m) + .then(div_m) + .when(~has_m & valid_no_m) + .then(div_no_m) + .otherwise(pl.lit(None, dtype=pl.Utf8)) + ) + + +@functools.lru_cache(maxsize=1) +def get_vectorized_lookup_tables() -> VectorizedLookupTables: + """Return the full bundle of join-ready NSVB lookup tables. + + Calls :func:`build_species_level_lookup`, :func:`build_jenkins_lookup`, + and :func:`build_division_lookup` on each of the 5 component table + pairs from :func:`load_nsvb_coefficients` and caches the result at + process level. This is the single entry point the vectorized live-tree + biomass orchestrator uses. + + The bundle is small (~1200 rows across all 15 DataFrames) and is shared + across every ``LiveTreeEstimator`` invocation in the process. + """ + raw = load_nsvb_coefficients() + return VectorizedLookupTables( + volib_spcd=build_species_level_lookup(raw.volib_spcd), + volib_jen=build_jenkins_lookup(raw.volib_jenkins), + volib_div=build_division_lookup(raw.volib_spcd), + volbk_spcd=build_species_level_lookup(raw.volbk_spcd), + volbk_jen=build_jenkins_lookup(raw.volbk_jenkins), + volbk_div=build_division_lookup(raw.volbk_spcd), + bark_bio_spcd=build_species_level_lookup(raw.bark_biomass_spcd), + bark_bio_jen=build_jenkins_lookup(raw.bark_biomass_jenkins), + bark_bio_div=build_division_lookup(raw.bark_biomass_spcd), + branch_bio_spcd=build_species_level_lookup(raw.branch_biomass_spcd), + branch_bio_jen=build_jenkins_lookup(raw.branch_biomass_jenkins), + branch_bio_div=build_division_lookup(raw.branch_biomass_spcd), + total_agb_spcd=build_species_level_lookup(raw.total_biomass_spcd), + total_agb_jen=build_jenkins_lookup(raw.total_biomass_jenkins), + total_agb_div=build_division_lookup(raw.total_biomass_spcd), + ) + + +# Explicit dtypes for the SPCD-keyed tables. DIVISION is a Bailey ecoprovince +# code (e.g., "M240", "240A") — always Utf8. STDORGCD is a nullable integer +# (1 or 2), stored as Int64. Passing these explicitly avoids relying on schema +# inference, which previously only worked because letters happened to appear +# in the first 10k rows of every table and would silently break if a future +# re-vendor reordered the rows. +_SPCD_DTYPES = { + "SPCD": pl.Int64, + "DIVISION": pl.Utf8, + "STDORGCD": pl.Int64, + "model": pl.Int64, + "a": pl.Float64, + "a1": pl.Float64, + "b": pl.Float64, + "b1": pl.Float64, + "c": pl.Float64, + "c1": pl.Float64, +} + +# Jenkins-keyed tables have a narrower schema and no DIVISION/STDORGCD. +_JENKINS_DTYPES = { + "JENKINS_SPGRPCD": pl.Int64, + "model": pl.Int64, + "a": pl.Float64, + "b": pl.Float64, + "c": pl.Float64, +} + + +@functools.lru_cache(maxsize=1) +def load_nsvb_coefficients() -> CoefficientTables: + """Load all NSVB coefficient tables from the vendored CSV bundle. + + Cached at process level via ``@lru_cache``: subsequent calls return the same + ``CoefficientTables`` instance in O(1). Uses ``importlib.resources.files`` + so it works in dev installs, installed wheels, zip-imported wheels, and + PyOxidizer-style frozen builds without ``__file__`` path tricks. + + Schemas are passed explicitly via ``schema_overrides`` so DIVISION is always + loaded as ``Utf8`` and STDORGCD as ``Int64`` regardless of the first-row + content of the CSV. This is what lets the upcoming ``ECOSUBCD → DIVISION`` + lookup key-join cleanly against the coefficient tables. + """ + data_pkg = resources.files("pyfia.carbon.nsvb.data") + loaded: dict[str, pl.DataFrame] = {} + for key, filename in _CSV_FILES.items(): + schema = _JENKINS_DTYPES if key.endswith("_jenkins") else _SPCD_DTYPES + with resources.as_file(data_pkg / filename) as path: + df = pl.read_csv(path, schema_overrides=schema) + loaded[key] = df + return CoefficientTables(**loaded) + + +def _row_to_dict(row: pl.DataFrame, source: str) -> dict: + """Convert a single-row Polars DataFrame to a coefficient dict. + + Returns the row's columns as a plain Python dict, with NaN/null values + coerced to 0.0 for numeric fields and the lookup ``source`` tag attached. + """ + raw = row.to_dicts()[0] + out: dict = {"source": source} + for key in ("model", "a", "a1", "b", "b1", "c", "c1"): + val = raw.get(key) + if val is None: + out[key] = 0.0 + else: + out[key] = float(val) + out["model"] = int(out["model"]) + return out + + +def lookup_coefficients( + table_spcd: pl.DataFrame, + table_jenkins: pl.DataFrame, + spcd: int, + jenkins_spgrpcd: int | None = None, + division: str | None = None, + stdorgcd: int | None = None, +) -> dict: + """Resolve a coefficient row for one tree following NSVB lookup precedence. + + The Phase 1 implementation walks precedence levels 1-4 in order. Phase 1 + typically only hits levels 3 and 4: the SPCD-only species-level row, or + the Jenkins fallback for unsupported species. The DIVISION-specific levels + 1 and 2 are wired but unused until pyFIA gains a ``PLOT.ECOSUBCD → + DIVISION`` mapping. + + Parameters + ---------- + table_spcd : pl.DataFrame + The ``S*a`` species-keyed table (e.g., ``volib_spcd``). + table_jenkins : pl.DataFrame + The ``S*b`` Jenkins-keyed fallback table. + spcd : int + FIA species code from ``TREE.SPCD``. + jenkins_spgrpcd : int, optional + Jenkins species group code from ``REF_SPECIES.JENKINS_SPGRPCD``. + Required if the species is not in the SPCD table (level 4 fallback). + division : str, optional + Bailey ecoprovince division code (e.g., ``"M240"``). Phase 1 always + passes ``None`` here. + stdorgcd : int, optional + Stand origin code from ``COND.STDORGCD``. Phase 1 always passes ``None``. + + Returns + ------- + dict + Coefficient dict with keys ``model, a, a1, b, b1, c, c1, source``. + ``source`` is one of ``"spcd_division_stdorg"``, ``"spcd_division"``, + ``"spcd"``, or ``"jenkins"``. + + Raises + ------ + KeyError + If neither the SPCD nor any Jenkins fallback can be resolved. + """ + # TODO(PR 2): Scalar reference implementation. The vectorized + # LiveTreeEstimator must replace per-tree calls with a polars join on + # SPCD against the coefficient tables, not call this function in a loop. + # See `pyfia/carbon/__init__.py` "Architectural rules" rule 2. + df = table_spcd.filter(pl.col("SPCD") == spcd) + + # Level 1: SPCD + DIVISION + STDORGCD exact match + if division is not None and stdorgcd is not None: + match = df.filter( + (pl.col("DIVISION") == division) & (pl.col("STDORGCD") == stdorgcd) + ) + if match.height > 0: + return _row_to_dict(match.head(1), "spcd_division_stdorg") + + # Level 2: SPCD + DIVISION (STDORGCD null) + if division is not None: + match = df.filter( + (pl.col("DIVISION") == division) & pl.col("STDORGCD").is_null() + ) + if match.height > 0: + return _row_to_dict(match.head(1), "spcd_division") + + # Level 3: SPCD only (DIVISION and STDORGCD both null) — the species-level row + match = df.filter(pl.col("DIVISION").is_null() & pl.col("STDORGCD").is_null()) + if match.height > 0: + return _row_to_dict(match.head(1), "spcd") + + # Level 4: Jenkins fallback. Note that S*b tables have different schemas + # (no DIVISION/STDORGCD columns) and are keyed on JENKINS_SPGRPCD. + if jenkins_spgrpcd is not None: + jdf = table_jenkins.filter(pl.col("JENKINS_SPGRPCD") == jenkins_spgrpcd) + if jdf.height > 0: + return _row_to_dict(jdf.head(1), "jenkins") + + raise KeyError( + f"No NSVB coefficients for SPCD={spcd}, JENKINS_SPGRPCD={jenkins_spgrpcd}, " + f"DIVISION={division}, STDORGCD={stdorgcd}. Check the species coverage " + "in src/pyfia/carbon/nsvb/data/." + ) diff --git a/src/pyfia/carbon/nsvb/data/README.md b/src/pyfia/carbon/nsvb/data/README.md new file mode 100644 index 00000000..8c24f0ee --- /dev/null +++ b/src/pyfia/carbon/nsvb/data/README.md @@ -0,0 +1,144 @@ +# NSVB Coefficient Tables (vendored) + +This directory contains coefficient and carbon-fraction tables from the National Scale +Volume and Biomass (NSVB) framework documented in: + +> Westfall, J.A. et al. (2023). *A National-Scale Tree Volume, Biomass, and Carbon +> Modeling System for the United States.* Gen. Tech. Rep. WO-104. Washington, DC: +> U.S. Department of Agriculture, Forest Service, Washington Office. +> DOI: 10.2737/WO-GTR-104 +> Supplementary archive: 10.2737/WO-GTR-104-Supp1 + +These CSVs are loaded by `pyfia.carbon.nsvb.coefficients` and +`pyfia.carbon.nsvb.carbon_fractions` via `importlib.resources` and ship inside the wheel. + +## File map + +| File | Source table (Supp1) | Rows | Purpose | +|---|---|---|---| +| `volib_spcd.csv` | S1a | 406 | Stem inside-bark wood volume coefficients by SPCD/DIVISION/STDORGCD | +| `volib_jenkins.csv` | S1b | 9 | Same, Jenkins-group fallback | +| `volbk_spcd.csv` | S2a | 339 | Stem bark volume coefficients | +| `volbk_jenkins.csv` | S2b | 9 | Jenkins fallback | +| `bark_biomass_spcd.csv` | S6a | 206 | Stem bark dry biomass | +| `bark_biomass_jenkins.csv` | S6b | 9 | Jenkins fallback | +| `branch_biomass_spcd.csv` | S7a | 175 | Branch dry biomass | +| `branch_biomass_jenkins.csv` | S7b | 9 | Jenkins fallback | +| `total_biomass_spcd.csv` | S8a | 173 | Total above-ground biomass (used in harmonization) | +| `total_biomass_jenkins.csv` | S8b | 9 | Jenkins fallback | +| `carbon_fraction_live.csv` | S10a (trimmed) | 2676 | Live tree carbon fractions by SPCD | +| `carbon_fraction_dead.csv` | S10b | 10 | Dead tree carbon fractions by hw/sw × DECAYCD | +| `dead_decay_proportions.csv` | REF_TREE_DECAY_PROP / Table 1 | 10 | Standing dead density/bark/branch loss proportions by hw/sw × DECAYCD | +| `dead_cr_prop.csv` | S11 / REF_TREE_STND_DEAD_CR_PROP | 86 | Mean intact crown ratio by Bailey ecoregion province × hw/sw (broken-top corrections) | + +## Coefficient table schema (S1a–S8b) + +Columns: `SPCD, DIVISION, STDORGCD, model, a, a1, b, b1, c, c1` + +- `SPCD` — FIA species code +- `DIVISION` — Bailey ecoprovince division code (e.g., `M240`, `210`). May be empty for the + species-level fallback row. +- `STDORGCD` — Stand origin code. Usually empty. +- `model` — Which NSVB model form to use (1, 2, 4, or 5). See `equations.py`. +- `a, a1, b, b1, c, c1` — Model parameters. Which are populated depends on `model`. + +**Lookup precedence** (per NSVB worked example, `gtr_wo104_westfall2023.md:684`): +1. SPCD + DIVISION + STDORGCD exact match +2. SPCD + DIVISION (STDORGCD null) +3. SPCD only (DIVISION and STDORGCD both null) — the species-level fallback +4. JENKINS_SPGRPCD fallback (Model 5 with WDSG multiplication) + +The CSVs already include species-level fallback rows; the loader does not need to +synthesize them. + +## Carbon fraction tables + +### `carbon_fraction_live.csv` (trimmed from S10a) + +Columns: `SPCD, hw_sw, fia_wood_c` + +- `SPCD` — FIA species code +- `hw_sw` — `"hardwood"` or `"softwood"`. Renamed from S10a's `division` column + (which uses `angiosperm`/`gymnosperm` — botanically equivalent but semantically + collides with the ecological `DIVISION` column in S1a–S8b). The values were also + remapped: `angiosperm` → `hardwood`, `gymnosperm` → `softwood`. +- `fia_wood_c` — Carbon as percent (e.g., `48.04`). The loader divides by 100 at + load time to produce a fraction in `[0.40, 0.55]`. + +### `carbon_fraction_dead.csv` (S10b) + +Columns: `Decay code, S/H, C fraction` + +10 rows: hardwood/softwood × decay class 1–5. Loaded by `carbon_fractions.py` and +used by the Phase 2 standing dead estimator for the biomass → carbon conversion. + +### `dead_decay_proportions.csv` (REF_TREE_DECAY_PROP / GTR-WO-104 Table 1) + +Columns: `hw_sw, DECAYCD, DENSITY_PROP, BARK_LOSS_PROP, BRANCH_LOSS_PROP` + +10 rows: hardwood/softwood × decay class 1–5. Mirrors the FIADB +`REF_TREE_DECAY_PROP` table and the consolidated NSVB hardwood/softwood × DECAYCD +values from GTR-WO-104 Table 1 (Westfall et al. 2023). Loaded by `carbon_fractions.py` +and used by `equations.compute_nsvb_dead_biomass` to apply decay reductions to gross +NSVB component biomass (stem wood × DENSITY_PROP, bark × BARK_LOSS_PROP, branch × +BRANCH_LOSS_PROP). Despite the `LOSS` suffix in the FIADB column names, the values +are the *remaining* proportions (not the *lost* proportions). + +### `dead_cr_prop.csv` (Table S11 / REF_TREE_STND_DEAD_CR_PROP) + +Columns: `ECOPROV, hw_sw, CR_MEAN` + +86 rows: Bailey ecoregion province × hardwood/softwood, plus two UNDEFINED +fallback rows. Vendored from GTR-WO-104 Table S11. The `ECOPROV` column +contains 3-digit Bailey province codes (e.g., `231`, `M242`) — note these +are **province** codes, not the `DIVISION` codes used by the NSVB coefficient +tables (which replace the last digit with `0`). `CR_MEAN` is the mean +compacted crown ratio in percent (e.g., `43.9` means 43.9%). Loaded by +`carbon_fractions.load_dead_cr_prop_df()` and used by +`equations.compute_nsvb_dead_biomass` for the Appendix K broken-top +crown-proportion correction on standing dead trees with `ACTUALHT < HT`. + +## Vendoring procedure (for maintainers) + +When WO-104 is revised, re-vendor as follows: + +1. Fetch the supplementary archive from . +2. Copy `Table S{1a,1b,2a,2b,6a,6b,7a,7b,8a,8b,10a,10b}*.csv` from the archive into + this directory, renaming each to its short form (see file map above). +3. Trim `carbon_fraction_live.csv` to keep only `SPCD, hw_sw, fia_wood_c`, and + remap `angiosperm`/`gymnosperm` → `hardwood`/`softwood`. Reference one-liner: + + ```python + import csv + mapping = {'angiosperm': 'hardwood', 'gymnosperm': 'softwood'} + with open('Table S10a_fia_wood_c_frac_live.csv.csv') as fin, \ + open('carbon_fraction_live.csv', 'w', newline='') as fout: + reader = csv.DictReader(fin) + writer = csv.DictWriter(fout, fieldnames=['SPCD', 'hw_sw', 'fia_wood_c']) + writer.writeheader() + for row in reader: + writer.writerow({ + 'SPCD': row['SPCD'], + 'hw_sw': mapping.get(row['division'].strip(), row['division'].strip()), + 'fia_wood_c': row['fia.wood.c'], + }) + ``` + +4. Run `uv run pytest tests/unit/test_nsvb_*.py tests/unit/test_carbon_fractions.py` + to verify no schema regressions. +5. Bump the patch version in `pyproject.toml` and commit with a message referencing + the Supp1 revision date. + +## Known issues in source data + +- **SPCD 10 (`fir spp.`)** is labeled `angiosperm` in S10a but should be `gymnosperm` + (Abies is a softwood genus). This is a data error in the source CSV; we preserve + it as-is rather than silently fixing it. SPCD 11 (Pacific silver fir, Abies amabilis) + is correctly labeled `gymnosperm`. +- The CSV coefficients are stored at ~9 significant figures. The WO-104 worked + examples cite coefficients to 12+ significant figures. As a result, end-to-end + pipeline tests using CSV-loaded coefficients are accurate to ~6 decimal places, + not the 9+ decimal places shown in the prose. The `tests/unit/test_nsvb_equations.py` + regression tests use hand-coded high-precision coefficients from the worked + example prose, separate from the CSV path, so the equation math itself is verified + to 9 decimal places. diff --git a/src/pyfia/carbon/nsvb/data/bark_biomass_jenkins.csv b/src/pyfia/carbon/nsvb/data/bark_biomass_jenkins.csv new file mode 100644 index 00000000..85ebf414 --- /dev/null +++ b/src/pyfia/carbon/nsvb/data/bark_biomass_jenkins.csv @@ -0,0 +1,10 @@ +JENKINS_SPGRPCD,model,a,b,c +1,1,0.001056271,1.75979085,1.595518427 +2,1,0.024642529,1.646231709,0.998562497 +3,1,0.037089719,2.860515142,0.142562661 +4,1,0.031937896,1.664554704,0.881859685 +5,1,0.097302469,1.616028784,0.578632787 +6,1,0.167920201,1.877056641,0.439608792 +7,1,0.120087385,1.990554372,0.41355154 +8,1,0.060205448,1.933727566,0.590397069 +9,1,0.036743204,1.630276513,0.962121833 diff --git a/src/pyfia/carbon/nsvb/data/bark_biomass_spcd.csv b/src/pyfia/carbon/nsvb/data/bark_biomass_spcd.csv new file mode 100644 index 00000000..44f4f516 --- /dev/null +++ b/src/pyfia/carbon/nsvb/data/bark_biomass_spcd.csv @@ -0,0 +1,207 @@ +SPCD,DIVISION,STDORGCD,model,a,b,b1,c +11,,,1,0.048055045,2.112855427,,0.543316117 +12,130,,2,0.027530352,2.165991411,2.242656509,0.639554183 +12,210,,2,0.004554801,1.618961857,1.705068319,1.381801534 +12,,,2,0.020335672,2.040082238,2.119684476,0.780832477 +15,M260,,1,0.000104701,0.869227967,,2.907020845 +15,,,1,0.011441127,2.168321806,,0.949708668 +16,,,1,0.048055045,2.032598212,,0.543316117 +17,,,1,0.048055045,2.125583418,,0.543316117 +19,M330,,2,0.022436719,2.00445589,1.742779656,0.775790533 +19,,,2,0.020827837,1.84907566,1.899617911,0.863463343 +20,,,1,0.048055045,2.259631473,,0.543316117 +68,,,1,0.059594973,1.562441301,,0.602911936 +71,130,,1,0.048319892,1.560752017,,0.739990007 +71,210,,1,0.081220229,1.909673882,,0.419221616 +71,,,1,0.074211558,1.729204892,,0.538671194 +73,,,1,0.059594973,1.881231523,,0.602911936 +90,,,1,0.052650851,1.669699254,,0.671493473 +93,,,1,0.058071391,1.433523972,,0.819746866 +94,130,,1,0.036605874,1.726868832,,0.744349517 +94,210,,1,0.034629031,1.589629422,,0.787146323 +94,M130,,1,0.099609876,1.934312453,,0.424070637 +94,M210,,1,0.030369934,1.556865877,,0.888769054 +94,,,1,0.034795222,1.649734858,,0.810728368 +95,130,,1,0.038118181,1.679771775,,0.796496664 +95,210,,1,0.05016543,1.592127236,,0.717866724 +95,M130,,1,0.079779291,1.803275465,,0.584944341 +95,,,1,0.051116053,1.72358547,,0.703195012 +97,,,1,0.058014505,1.91780615,,0.547480628 +98,M240,,1,0.049553302,1.463704472,,0.759819073 +98,,,1,0.049553302,1.463704472,,0.759819073 +105,130,,2,0.03706776,1.76533371,1.446274944,0.670566959 +105,210,,2,0.058683145,1.448466441,1.824891694,0.720389028 +105,,,2,0.048147163,1.6935246,1.543556927,0.642405307 +107,230,,1,0.530178789,1.808252468,,-0.016375423 +107,,,1,0.530178789,1.808252468,,-0.016375423 +108,M130,,1,0.039437496,1.758115853,,0.683384567 +108,M210,,1,0.073194929,1.733598811,,0.54749036 +108,M330,,1,0.04916401,1.564770043,,0.692947924 +108,,,1,0.061204576,1.52979646,,0.683116029 +110,230,,1,0.020450554,1.789271239,,0.928946762 +110,M230,,1,0.551138099,1.983882846,,0.006365659 +110,,,1,0.034039673,1.874929814,,0.754498102 +111,230,0,1,0.046930237,0.819023691,,1.393983197 +111,,0,1,0.052649522,0.836104011,,1.35741823 +111,230,1,4,0.197373655,1.450674694,-0.050385585,0.538874889 +111,,1,4,0.197373655,1.450674694,-0.050385585,0.538874889 +115,230,,1,0.069590888,1.694923675,,0.618372743 +115,,,1,0.069590888,1.694923675,,0.618372743 +121,230,,1,0.061210129,1.596483262,,0.817962656 +121,,,1,0.061210129,1.596483262,,0.817962656 +122,M310,,1,0.077785157,1.675881164,,0.712880315 +122,,,1,0.07732639,1.761223104,,0.66295371 +123,M220,,1,0.032703997,1.903092443,,0.698871535 +123,,,1,0.030322951,1.828656086,,0.751189292 +125,130,,2,0.014681172,1.707481851,1.549684489,0.924695694 +125,210,,2,0.043657756,1.745977544,1.861107773,0.619728947 +125,,,2,0.030527229,1.68789083,1.758110982,0.747370748 +126,M220,,1,0.067742191,2.067725566,,0.498583263 +126,,,1,0.04600067,1.943277897,,0.661031637 +128,230,,1,0.078850422,1.75060765,,0.610216932 +128,,,1,0.078850422,1.75060765,,0.610216932 +129,210,,2,0.01873811,1.816986181,1.722985549,0.914618822 +129,M220,,2,0.027010815,1.530603739,1.815264037,0.953058057 +129,,,2,0.027582546,1.698093152,1.784740212,0.879141183 +131,230,0,4,0.035614081,1.384818216,-0.034932785,0.887335694 +131,,0,4,0.036927899,1.386630276,-0.035296634,0.876614885 +131,230,1,2,0.039939177,1.346298933,1.668126552,0.970566893 +131,,1,2,0.039461408,1.338142889,1.668488629,0.977391519 +132,230,,2,0.048433435,1.31534608,1.63778088,0.832266145 +132,M220,,2,0.073196157,1.642379926,1.892943768,0.605900182 +132,,,2,0.173040879,1.675442682,2.012437755,0.338522647 +202,240,,1,0.009106538,1.437894425,,1.336514273 +202,330,,1,0.045085287,1.800164805,,0.778003763 +202,M240,,1,0.029824871,1.861320045,,0.807261719 +202,M330,,1,7.47E-05,1.537559786,,2.389550265 +202,,,1,0.024642529,1.646231709,,0.998562497 +211,,,1,0.059594973,2.002454779,,0.602911936 +221,,,1,0.059594973,1.573192089,,0.602911936 +222,230,,1,4.328839658,1.558860694,,-0.213551356 +222,,,1,4.328839658,1.558860694,,-0.213551356 +241,130,,1,0.008799669,1.422499197,,1.24900644 +241,210,,1,0.01256292,1.831305496,,0.89053981 +241,,,1,0.013991446,1.745415054,,0.921218343 +242,,,1,0.000174097,1.633025554,,2.021774074 +261,130,,1,0.027333424,1.746397896,,0.935785497 +261,210,,1,0.099785564,2.232315555,,0.306304728 +261,M220,,1,0.098129475,2.096557539,,0.35216957 +261,,,1,0.128155232,2.260152577,,0.223983908 +263,M240,,1,0.002691896,2.135699852,,1.081782804 +263,,,1,0.043792138,2.123475696,,0.50781265 +264,,,1,0.048055045,2.249067373,,0.543316117 +313,230,,1,0.009581207,1.52549991,,1.054697566 +313,,,1,0.009581207,1.52549991,,1.054697566 +315,,,1,0.03996461,1.716125924,,0.73051117 +316,210,,1,0.130552822,1.753461556,,0.478632372 +316,230,,1,0.335780727,1.941398343,,0.180068985 +316,M220,,1,0.025448081,1.796895609,,0.897411906 +316,,,1,0.061595466,1.818642599,,0.654020672 +317,,,1,0.020092592,1.641073769,,0.934597897 +318,210,,1,0.004702083,1.369695351,,1.552226865 +318,,,1,0.008394402,1.46735871,,1.36184751 +351,,,1,0.059876965,1.907596922,,0.591508758 +370,,,1,0.03996461,1.937895905,,0.73051117 +371,130,,1,0.006424937,2.230209409,,1.045990095 +371,210,,1,0.049357419,1.877077027,,0.708571957 +371,,,1,0.05342594,1.917273516,,0.661735026 +375,130,,1,0.030933231,1.828569491,,0.868394248 +375,210,,1,0.089977546,1.985205747,,0.519427946 +375,,,1,0.034921649,1.881891477,,0.808611606 +379,,,1,0.03996461,1.913051469,,0.73051117 +391,,,1,0.042470213,1.882553252,,0.62090469 +400,230,,1,1.281742462,2.370036025,,-0.252998674 +400,M220,,1,0.11099649,2.07991383,,0.465604628 +400,,,1,0.144473761,2.198748093,,0.333891534 +402,,,1,0.069324075,1.795489654,,0.668415758 +403,,,1,0.069324075,2.043995922,,0.668415758 +460,,,1,0.043189363,1.646982196,,0.721413052 +461,230,,1,0.344501515,2.59844774,,-0.240966269 +461,,,1,0.344501515,2.59844774,,-0.240966269 +462,,,1,0.043189363,1.792799082,,0.721413052 +471,,,1,0.043189363,1.728397877,,0.721413052 +491,230,,1,0.053441117,1.965607463,,0.581318882 +491,,,1,0.123716593,1.853188268,,0.3130394 +521,,,1,0.043189363,1.717121184,,0.721413052 +531,210,,1,0.008707451,1.335945824,,1.302453936 +531,,,1,0.004564655,1.663986793,,1.261458372 +540,230,,1,0.10107119,1.728040552,,0.543560667 +540,,,1,0.060291998,1.794718443,,0.659079038 +541,210,,1,0.068234627,1.404590738,,0.864834655 +541,,,1,0.023697423,1.414387662,,1.114578401 +543,,,1,0.070159883,1.499808864,,0.767507468 +544,230,,1,0.331570259,1.867023995,,0.265790137 +544,,,1,0.1431526,1.785413805,,0.505353671 +591,,,1,0.043189363,1.65728608,,0.721413052 +602,,,1,0.043189363,1.881413549,,0.721413052 +611,230,,1,0.141691891,2.104690776,,0.297740423 +611,,,1,0.139825921,2.102503676,,0.301999389 +621,230,,1,0.107312155,2.037579706,,0.420410281 +621,M220,,1,0.024129227,1.472766889,,1.09867876 +621,,,1,0.048493635,1.736187448,,0.778049432 +653,,,1,0.043189363,1.822420582,,0.721413052 +691,230,,1,0.026251715,1.672010995,,0.826260357 +691,,,1,0.026251715,1.672010995,,0.826260357 +693,,,1,0.043189363,1.983871056,,0.721413052 +694,230,,1,2.089884144,2.520751263,,-0.575919934 +694,,,1,2.089884144,2.520751263,,-0.575919934 +701,,,1,0.043189363,1.847478014,,0.721413052 +711,,,1,0.043189363,1.66887874,,0.721413052 +731,,,1,0.043189363,1.454348741,,0.721413052 +740,230,,1,0.035002562,1.783242306,,0.829947671 +740,,,1,0.116997623,1.952229546,,0.466401185 +741,130,,1,0.123534682,1.934646049,,0.453581245 +741,210,,1,0.00606087,1.691200925,,1.260603079 +741,,,1,0.053575624,1.855834897,,0.675319365 +742,,,1,0.059876965,1.887803513,,0.591508758 +743,130,,1,0.068761677,2.035541305,,0.588834347 +743,210,,1,0.098556033,1.89767377,,0.603632394 +743,,,1,0.071561258,2.016466413,,0.602902901 +746,130,,1,0.01983217,2.08357845,,0.864866324 +746,210,,1,0.046238316,2.118384377,,0.632713914 +746,M130,,1,0.145423698,2.1722623,,0.268241691 +746,M330,,1,0.060233309,1.855788621,,0.724388352 +746,,,1,0.026472212,2.074312152,,0.790524962 +747,,,1,0.059876965,1.881814192,,0.591508758 +762,210,,1,0.082449134,1.559858651,,0.718131806 +762,,,1,0.051569203,1.685381601,,0.759448784 +802,210,,2,0.158403991,1.88138115,1.670270643,0.375830383 +802,220,,2,0.031279848,1.619489579,1.782880288,0.982414795 +802,230,,2,0.005981948,1.192756982,1.845274907,1.589009143 +802,M220,,2,0.013653816,2.255437356,1.777569692,0.830992811 +802,,,2,0.013365029,1.50353735,1.723259531,1.241874401 +806,220,,1,0.090424375,1.862634913,,0.653223886 +806,,,1,0.07731243,1.850129172,,0.68302626 +812,,,1,0.1092744,1.879109108,,0.639890406 +813,,,1,0.069324075,1.769307108,,0.668415758 +819,,,1,0.069324075,2.24385994,,0.668415758 +820,,,1,0.069324075,1.942060251,,0.668415758 +822,230,,1,0.074324751,1.861137323,,0.566854755 +822,,,1,0.074324751,1.861137323,,0.566854755 +823,,,1,0.069324075,1.754330621,,0.668415758 +824,,,1,0.069324075,2.166985786,,0.668415758 +827,230,,1,0.117906384,2.108262928,,0.433220334 +827,,,1,0.117906384,2.108262928,,0.433220334 +828,230,,1,0.050861995,1.654023301,,0.858803877 +828,,,1,0.050861995,1.654023301,,0.858803877 +831,230,,1,0.173096336,1.910303875,,0.451401951 +831,,,1,0.173096336,1.910303875,,0.451401951 +832,M220,,1,0.024070617,1.713500826,,1.084618944 +832,,,1,0.025304081,1.72185094,,1.067920803 +833,210,,1,0.045294504,1.934420196,,0.744389199 +833,,,1,0.050329057,1.69689077,,0.874728262 +835,,,1,0.069324075,1.879969187,,0.668415758 +837,,,1,0.097971895,2.132010173,,0.476609435 +840,,,1,0.069324075,2.057758736,,0.668415758 +842,,,1,0.069324075,2.209397375,,0.668415758 +901,,,1,0.043189363,1.910547866,,0.721413052 +920,,,1,0.059876965,1.92206279,,0.591508758 +922,,,1,0.059876965,1.813473252,,0.591508758 +950,210,,1,0.008061591,1.547319459,,1.303065667 +950,,,1,0.011664833,1.531653002,,1.225866433 +951,,,1,0.043189363,1.967344086,,0.721413052 +970,230,,1,0.074372612,1.566362814,,0.650273029 +970,,,1,0.067321192,1.530699224,,0.691167405 +972,210,,1,0.040951245,2.52604176,,0.320576203 +972,,,1,0.065080213,2.467772369,,0.238290111 +999,,,1,0.043189363,1.662442226,,0.721413052 diff --git a/src/pyfia/carbon/nsvb/data/branch_biomass_jenkins.csv b/src/pyfia/carbon/nsvb/data/branch_biomass_jenkins.csv new file mode 100644 index 00000000..9619069a --- /dev/null +++ b/src/pyfia/carbon/nsvb/data/branch_biomass_jenkins.csv @@ -0,0 +1,10 @@ +JENKINS_SPGRPCD,model,a,b,c +1,5,139.7525338,2.923771514,-1.685241344 +2,5,14.02057768,2.279310614,-0.624678091 +3,5,10.47636902,2.458428362,-0.592824364 +4,5,3.356500484,3.465833243,-1.004347672 +5,5,16.30105568,3.051079332,-1.033076897 +6,5,1.276802137,3.002972944,-0.393304301 +7,5,0.005866986,2.776217173,1.019083999 +8,5,0.79860485,2.969162133,-0.301902411 +9,5,0.198994402,2.681943186,0.288130955 diff --git a/src/pyfia/carbon/nsvb/data/branch_biomass_spcd.csv b/src/pyfia/carbon/nsvb/data/branch_biomass_spcd.csv new file mode 100644 index 00000000..f969cb84 --- /dev/null +++ b/src/pyfia/carbon/nsvb/data/branch_biomass_spcd.csv @@ -0,0 +1,176 @@ +SPCD,DIVISION,STDORGCD,model,a,b,b1,c +11,,,1,1.767896986,2.375511274,,-0.44008936 +12,210,,1,2.499595466,2.702376969,,-0.609498392 +12,,,1,2.480510019,2.780354376,,-0.655208294 +15,M260,,1,0.000319109,0.708305088,,2.652928182 +15,,,1,0.153048772,2.054800965,,0.364064058 +16,,,1,1.767896986,2.538687396,,-0.44008936 +17,,,1,1.767896986,2.579440817,,-0.44008936 +19,,,1,3.602922008,3.483213479,,-1.134599534 +20,,,1,1.767896986,2.455354719,,-0.44008936 +68,,,1,40.63738529,3.208073935,,-1.822117032 +71,210,,1,21.64217343,2.998147615,,-1.378684498 +71,,,1,89.82141277,2.998440851,,-1.702958595 +73,,,1,40.63738529,3.208074019,,-1.822117032 +93,M330,,1,3.077431492,2.655568994,,-0.611016386 +93,,,1,3.357410195,2.687974266,,-0.653744801 +94,210,,1,5.682407325,2.518946065,,-0.736092657 +94,,,1,5.938962915,2.517562438,,-0.748338064 +95,210,,1,2.190442162,3.261325455,,-0.846370745 +95,,,1,2.190442162,3.261325455,,-0.846370745 +97,,,1,3.180696903,2.790777214,,-0.722447255 +98,,,1,3.180696903,2.790775112,,-0.722447255 +105,210,,1,1.335397652,3.179300991,,-0.745237308 +105,,,1,1.335397652,3.179300991,,-0.745237308 +108,M330,,1,10.27831517,3.467024095,,-1.401401964 +108,,,1,13.5811022,3.166978212,,-1.312080923 +110,230,,1,0.768432033,3.985480604,,-1.164906102 +110,M230,,1,2.40993845,3.670518765,,-1.266797846 +110,,,1,0.652240998,3.962110403,,-1.117194052 +111,230,0,1,0.858432091,3.748201043,,-1.095286703 +111,,0,1,0.858432091,3.748201043,,-1.095286703 +111,230,1,1,2.14272707,3.381493445,,-0.971240972 +111,,1,1,2.14272707,3.381493445,,-0.971240972 +116,,,1,2.177600402,3.270319267,,-0.966812486 +121,230,,1,1.377818699,3.612612683,,-1.02431869 +121,,,1,1.377818699,3.612612683,,-1.02431869 +122,M260,,1,0.310460138,2.948000576,,-0.270055097 +122,M310,,1,0.952443009,3.282512866,,-0.821331779 +122,M330,,1,1.856855288,2.424659528,,-0.402509507 +122,,,1,1.033539089,2.996683362,,-0.628170258 +123,,,1,2.177600402,3.375344252,,-0.966812486 +125,210,,1,4.893214352,3.24371335,,-1.158985685 +125,,,1,4.16426642,3.259358498,,-1.132436577 +126,,,1,2.177600402,3.385066034,,-0.966812486 +129,210,,1,4.745112692,3.288200186,,-1.189935767 +129,M220,,1,34.34567261,3.406583265,,-1.651412383 +129,,,1,4.945637294,3.215726191,,-1.128810156 +131,230,0,1,0.61214932,3.17447514,,-0.655518923 +131,,0,1,0.535768778,3.17327766,,-0.625507896 +131,230,1,2,1.797223551,2.57376148,3.243154531,-0.612092191 +131,,1,2,1.797223551,2.57376148,3.243154531,-0.612092191 +132,M220,,1,5.527036308,2.998114064,,-0.943857532 +132,,,1,4.92001951,3.058501806,,-0.94192471 +202,240,,1,9.521330809,1.762316117,,-0.405742592 +202,M240,,1,6.516255149,2.23536737,,-0.593040429 +202,M260,,1,0.843599248,2.649405414,,-0.473312712 +202,M330,,1,18.70878641,3.289218661,,-1.474720141 +202,,,1,6.308889592,2.279290294,,-0.624651961 +211,,,1,40.63738529,3.208073725,,-1.822117032 +221,,,1,40.63738529,3.208073741,,-1.822117032 +222,230,,1,2.01E-06,2.270462007,,2.638610446 +222,,,1,2.01E-06,2.270462007,,2.638610446 +241,210,,1,2.807057635,3.270615486,,-1.142255091 +241,,,1,2.807057635,3.270615486,,-1.142255091 +261,210,,1,0.209520151,2.599997678,,0.030682068 +261,M220,,1,2.163238692,2.490148365,,-0.371162316 +261,,,1,0.190102207,2.380522491,,0.214711885 +263,,,1,21.55054557,2.975974249,,-1.29085747 +264,,,1,1.767896986,2.412041062,,-0.44008936 +313,230,,1,5.178883875,4.051628802,,-1.508197247 +313,,,1,5.178883875,4.051628802,,-1.508197247 +316,210,,1,0.018713618,2.61154576,,0.706802784 +316,230,,1,0.650481799,3.660192373,,-0.773985538 +316,M220,,1,0.013549262,4.30984129,,-0.246339129 +316,,,1,0.011144618,3.269520661,,0.421304344 +317,,,1,3287.212531,3.842100097,,-2.804812895 +318,210,,1,0.114467503,2.323367173,,0.480514905 +318,,,1,0.000593508,2.412393595,,1.667537289 +370,,,1,2.243999639,3.17203173,,-0.698427914 +371,210,,1,0.002455463,2.364872835,,1.34170809 +371,,,1,0.006040684,2.371198348,,1.129119601 +375,210,,1,21.60137175,3.225808314,,-1.344948117 +375,M210,,1,0.292824997,2.808504213,,-0.283122561 +375,,,1,53.53641954,3.564350978,,-1.781283042 +391,,,1,1.410201356,2.781242551,,-0.314666479 +400,210,,1,0.001634244,2.627497151,,1.344829093 +400,230,,1,0.010690367,3.319883187,,0.459778969 +400,M220,,1,2.129226819,3.686694526,,-0.962395961 +400,,,1,0.588387535,3.205600343,,-0.3663898 +402,,,1,0.758420484,3.022696028,,-0.257945085 +403,,,1,0.758420484,2.822908142,,-0.257945085 +460,,,1,1.966990266,3.130869139,,-0.652108323 +461,230,,1,0.002476966,2.539060702,,1.2262041 +461,,,1,0.002476966,2.539060702,,1.2262041 +462,,,1,1.966990266,3.215183441,,-0.652108323 +471,,,1,1.966990266,3.187916367,,-0.652108323 +491,230,,1,0.039303521,1.832306533,,1.057089845 +491,,,1,0.049253822,2.152630259,,0.9154909 +521,,,1,1.966990266,2.924323616,,-0.652108323 +531,210,,1,0.108326703,2.500877956,,0.379909614 +531,,,1,0.006237593,2.742701938,,0.919022185 +540,230,,1,1.479473428,2.802762416,,-0.550722765 +540,,,1,0.883064342,3.290460186,,-0.617185602 +541,210,,1,2.151051371,2.275862814,,-0.230791334 +541,,,1,5.714918919,2.356392172,,-0.519470024 +543,,,1,1.966990266,2.84946125,,-0.652108323 +544,230,,1,0.042383409,2.360442871,,0.507494199 +544,,,1,0.046228652,2.284297987,,0.541565848 +591,,,1,1.966990266,3.027340372,,-0.652108323 +602,,,1,1.966990266,3.153596215,,-0.652108323 +611,230,,1,0.103333948,3.167562399,,-0.162651127 +611,,,1,0.10403541,3.167263369,,-0.163911362 +621,230,,1,0.000635683,3.318521789,,0.844234441 +621,M220,,1,23.48007224,3.736573074,,-1.748229761 +621,,,1,3.27407423,4.467917367,,-1.771559868 +653,,,1,1.966990266,2.804869307,,-0.652108323 +691,230,,1,89.16082807,4.007538791,,-2.429335987 +691,,,1,89.16082807,4.007538791,,-2.429335987 +693,,,1,1.966990266,2.977888434,,-0.652108323 +694,230,,1,1.217533923,3.454135011,,-0.961298039 +694,,,1,1.217533923,3.454135011,,-0.961298039 +711,,,1,1.966990266,2.868612326,,-0.652108323 +731,,,1,1.966990266,2.905631737,,-0.652108323 +740,230,,1,0.033110007,3.661532544,,-0.226955513 +740,,,1,0.280072764,3.597254938,,-0.652023746 +741,210,,1,0.369538503,2.95886454,,-0.310082632 +741,,,1,0.369538503,2.95886454,,-0.310082632 +742,,,1,1.010223097,3.008141414,,-0.589318704 +743,210,,1,14.51555219,4.360877771,,-2.000439579 +743,,,1,14.51555219,4.360877771,,-2.000439579 +746,210,,1,0.862897487,3.171609189,,-0.615515966 +746,M330,,1,1.912605524,2.603843413,,-0.540177991 +746,,,1,0.120809399,2.851123305,,0.005918399 +762,210,,1,0.004782639,2.669533389,,1.005314035 +762,,,1,0.198470356,2.710227003,,0.064879296 +802,210,,1,0.02015083,2.417295711,,0.851545753 +802,220,,1,0.004294937,3.409007669,,0.578652976 +802,230,,1,0.036614884,3.162708086,,0.278384905 +802,M220,,1,0.003795935,2.337549206,,1.305869513 +802,,,1,0.004880537,2.511268248,,1.145892662 +806,M220,,1,0.002376907,2.460564251,,1.310671276 +806,,,1,0.05840055,2.504821007,,0.579911434 +812,,,1,4.171865572,3.577388681,,-1.098866713 +813,,,1,0.758420484,2.823799159,,-0.257945085 +819,,,1,0.758420484,3.086134542,,-0.257945085 +820,,,1,0.758420484,2.798398658,,-0.257945085 +822,230,,1,2.548259162,2.96468673,,-0.661938179 +822,,,1,2.548259162,2.96468673,,-0.661938179 +823,,,1,0.758420484,2.714376632,,-0.257945085 +824,,,1,0.758420484,2.953772595,,-0.257945085 +827,230,,1,2.132780009,2.726513601,,-0.413598786 +827,,,1,2.132780009,2.726513601,,-0.413598786 +828,230,,1,0.004867674,3.128305695,,0.638410104 +828,,,1,0.004867674,3.128305695,,0.638410104 +831,230,,1,0.000631864,2.543286624,,1.443703136 +831,,,1,0.000631864,2.543286624,,1.443703136 +832,M220,,1,3.280061412,3.312682089,,-0.901822133 +832,,,1,2.972562894,3.340347865,,-0.898908125 +833,210,,1,0.523539459,2.743873795,,-0.106595112 +833,M220,,1,0.588884857,2.882222544,,-0.201587078 +833,,,1,0.390519222,2.968861696,,-0.165695923 +835,,,1,0.758420484,2.798931928,,-0.257945085 +837,,,1,0.979216985,3.534470622,,-0.773528755 +840,,,1,0.758420484,2.898556249,,-0.257945085 +842,,,1,0.758420484,3.119369413,,-0.257945085 +901,,,1,1.966990266,2.932794214,,-0.652108323 +920,,,1,1.010223097,3.008140626,,-0.589318704 +922,,,1,1.010223097,3.008140294,,-0.589318704 +950,210,,1,0.044311803,2.076150262,,0.661917261 +950,,,1,0.007909337,2.039703829,,1.086819465 +951,,,1,1.966990266,2.871770768,,-0.652108323 +970,230,,1,0.074202394,3.90057498,,-0.230521859 +970,,,1,0.127117059,3.827908317,,-0.317081918 +972,210,,1,6.231186737,2.420015222,,-0.67523185 +972,,,1,2.118050713,2.365726312,,-0.361419565 +999,,,1,1.966990266,3.000632578,,-0.652108323 diff --git a/src/pyfia/carbon/nsvb/data/carbon_fraction_dead.csv b/src/pyfia/carbon/nsvb/data/carbon_fraction_dead.csv new file mode 100644 index 00000000..e36a3b1c --- /dev/null +++ b/src/pyfia/carbon/nsvb/data/carbon_fraction_dead.csv @@ -0,0 +1,11 @@ +Decay code,S/H,C fraction +1,Hardwood,47 +2,Hardwood,47.3 +3,Hardwood,48.1 +4,Hardwood,48 +5,Hardwood,47.2 +1,Softwood,50.1 +2,Softwood,50.4 +3,Softwood,50.6 +4,Softwood,52 +5,Softwood,52.7 diff --git a/src/pyfia/carbon/nsvb/data/carbon_fraction_live.csv b/src/pyfia/carbon/nsvb/data/carbon_fraction_live.csv new file mode 100644 index 00000000..33e53bee --- /dev/null +++ b/src/pyfia/carbon/nsvb/data/carbon_fraction_live.csv @@ -0,0 +1,2677 @@ +SPCD,hw_sw,fia_wood_c +10,hardwood,48.04 +11,softwood,49.31666667 +12,softwood,50.55153846 +14,softwood,48.04 +15,softwood,50.97 +16,softwood,48.04 +17,softwood,48.075 +18,softwood,48.04 +19,softwood,48.215 +20,softwood,51.51857143 +21,softwood,48.04 +22,softwood,50.75 +40,softwood,48.005 +41,softwood,47.935 +42,softwood,47.83 +43,softwood,47.50333333 +50,softwood,47.865 +51,softwood,47.865 +52,softwood,47.865 +53,softwood,47.865 +54,softwood,47.865 +55,softwood,47.865 +56,softwood,47.865 +57,softwood,47.725 +58,softwood,47.725 +59,softwood,47.725 +60,softwood,48.10625 +61,softwood,47.725 +62,softwood,47.725 +63,softwood,47.62 +64,softwood,47.975 +65,softwood,46.92 +66,softwood,47.725 +67,softwood,52.14 +68,softwood,52.14 +69,softwood,47.725 +70,softwood,47.585 +71,softwood,47.585 +72,softwood,47.585 +73,softwood,47.6 +74,softwood,48.13916929 +75,softwood,47.585 +81,softwood,53.75857143 +90,softwood,48.04 +91,softwood,47.83071154 +92,softwood,48.04 +93,softwood,48.145 +94,softwood,51.16333333 +95,softwood,47.97 +96,softwood,48.04 +97,softwood,48.005 +98,softwood,49.95 +100,softwood,47.795 +101,softwood,47.795 +102,softwood,47.795 +103,softwood,47.935 +104,softwood,47.795 +105,softwood,47.9 +106,softwood,47.55 +107,softwood,47.69 +108,softwood,50.32 +109,softwood,47.795 +110,softwood,47.655 +111,softwood,51.337 +112,softwood,50.2 +113,softwood,48.005 +114,softwood,47 +115,softwood,47.865 +116,softwood,51.85142857 +117,softwood,48.11 +118,softwood,49.4 +119,softwood,48.04 +120,softwood,47.725 +121,softwood,47.41 +122,softwood,51.33333333 +123,softwood,47.585 +124,softwood,47.9 +125,softwood,53.28 +126,softwood,47.655 +127,softwood,47.9 +128,softwood,47.515 +129,softwood,50.70666667 +130,softwood,47.11355062 +131,softwood,47.655 +132,softwood,47.725 +133,softwood,47.795 +134,softwood,47.795 +135,softwood,47.795 +136,softwood,52.8125 +137,softwood,47.795 +138,softwood,47.25 +139,softwood,47.795 +140,softwood,48.45 +141,softwood,47.795 +142,softwood,47.795 +143,softwood,47.795 +144,softwood,51.337 +200,softwood,47.725 +201,softwood,47.725 +202,softwood,51.55958333 +211,softwood,53.084 +212,softwood,53.82375 +220,softwood,47.83 +221,softwood,47.83 +222,softwood,47.83 +223,softwood,45.33333333 +230,softwood,47.2 +231,softwood,47.2 +232,softwood,47.2 +240,softwood,48.25 +241,softwood,50.07428571 +242,softwood,50.38 +250,softwood,47.865 +251,softwood,47.865 +252,softwood,47.865 +260,softwood,47.865 +261,softwood,47.97 +262,softwood,47.865 +263,softwood,50.7 +264,softwood,47.83 +299,hardwood,47.865 +300,hardwood,47.48 +303,hardwood,47.48 +304,hardwood,45.4 +310,hardwood,47.655 +311,hardwood,47.655 +312,hardwood,49.64 +313,hardwood,48.67 +314,hardwood,47.48 +315,hardwood,47.82 +316,hardwood,48.57333333 +317,hardwood,47.76 +318,hardwood,48.54638634 +319,hardwood,47.655 +320,hardwood,47.655 +321,hardwood,47.655 +322,hardwood,47.655 +323,hardwood,47.655 +330,hardwood,48.145 +331,hardwood,48.145 +332,hardwood,48.145 +333,hardwood,48.145 +334,hardwood,48.145 +336,hardwood,48.145 +337,hardwood,48.145 +341,hardwood,47.69 +345,hardwood,47.48 +350,hardwood,48.005 +351,hardwood,48.3 +352,hardwood,48.005 +353,hardwood,48.005 +355,hardwood,48.005 +356,hardwood,46.99 +357,hardwood,46.99 +358,hardwood,46.99 +360,hardwood,47.27 +361,hardwood,47.27 +362,hardwood,47.27 +363,hardwood,49.88 +367,hardwood,47.48 +370,hardwood,47.515 +371,hardwood,48.77833333 +372,hardwood,47.2 +373,hardwood,47.585 +374,hardwood,47.515 +375,hardwood,51.90875 +376,hardwood,47.62 +377,hardwood,47.515 +378,hardwood,47.515 +379,hardwood,47.725 +381,hardwood,47.48 +391,hardwood,47.27 +400,hardwood,47.13 +401,hardwood,47.165 +402,hardwood,47.2 +403,hardwood,46.99 +404,hardwood,47.2 +405,hardwood,47.13 +406,hardwood,47.34 +407,hardwood,47.06 +408,hardwood,47.13 +409,hardwood,47.06 +410,hardwood,47.13 +411,hardwood,47.13 +412,hardwood,47.13 +413,hardwood,47.13 +420,hardwood,47.9 +421,hardwood,46.37 +422,hardwood,47.9 +423,hardwood,47.9 +424,hardwood,47.9 +431,hardwood,47.83 +450,hardwood,47.97 +451,hardwood,46.8684875 +452,hardwood,47.97 +460,hardwood,47.585 +461,hardwood,44.79 +462,hardwood,47.585 +463,hardwood,44.79 +471,hardwood,43.8 +475,hardwood,47.48 +481,hardwood,47.48 +490,hardwood,47.165 +491,hardwood,45.94704167 +492,hardwood,47.27 +500,hardwood,47.48 +501,hardwood,47.48 +502,hardwood,47.48 +503,hardwood,47.48 +504,hardwood,47.48 +505,hardwood,47.48 +506,hardwood,47.48 +507,hardwood,47.48 +508,hardwood,47.48 +509,hardwood,47.48 +510,hardwood,47.48 +511,hardwood,47.8 +512,hardwood,45.44 +513,hardwood,47.48 +514,hardwood,47.48 +520,hardwood,47.06 +521,hardwood,47.06 +522,hardwood,47.06 +523,hardwood,47.48 +531,hardwood,47.81666667 +540,hardwood,47.515 +541,hardwood,47.375 +542,hardwood,47.55 +543,hardwood,48 +544,hardwood,48.6 +545,hardwood,47.62 +546,hardwood,47.445 +547,hardwood,47.515 +548,hardwood,47.515 +549,hardwood,47.515 +550,hardwood,47.2 +551,hardwood,47.2 +552,hardwood,42.3481525 +555,hardwood,47.48 +561,softwood,47.48 +571,hardwood,47.445 +580,hardwood,47.83 +581,hardwood,47.83 +582,hardwood,47.83 +583,hardwood,47.83 +591,hardwood,52.74189167 +600,hardwood,47.76 +601,hardwood,48.53 +602,hardwood,46.73333438 +603,hardwood,47.76 +604,hardwood,47.76 +605,hardwood,47.76 +606,hardwood,42 +611,hardwood,44.47357188 +621,hardwood,45.67688333 +631,hardwood,47.27 +641,hardwood,46.64 +650,hardwood,47.795 +651,hardwood,47.76 +652,hardwood,43.56139375 +653,hardwood,47.83 +654,hardwood,46.51201853 +655,hardwood,47.9 +657,hardwood,47.795 +658,hardwood,47.43140833 +660,hardwood,47.165 +661,hardwood,47.165 +662,hardwood,47.165 +663,hardwood,47.165 +664,hardwood,47.165 +680,hardwood,47.48 +681,hardwood,51.14 +682,hardwood,47.48 +683,hardwood,47.48 +684,hardwood,47.48 +690,hardwood,47.69 +691,hardwood,47.69 +692,hardwood,47.69 +693,hardwood,45.07389447 +694,hardwood,47.69 +701,hardwood,47.095 +711,hardwood,47.55 +712,hardwood,47.48 +715,hardwood,46.675 +718,hardwood,48.005 +720,hardwood,47.48 +721,hardwood,47.48 +722,hardwood,47.48 +729,hardwood,47.69 +730,hardwood,47.69 +731,hardwood,49.33637375 +732,hardwood,47.69 +740,hardwood,48.075 +741,hardwood,48.215 +742,hardwood,46.84189107 +743,hardwood,48.04 +744,hardwood,48.075 +745,hardwood,47.84189107 +746,hardwood,47.92375 +747,hardwood,48.215 +748,hardwood,47.865 +749,hardwood,48.075 +752,hardwood,48.075 +753,hardwood,48.075 +755,hardwood,46.57 +756,hardwood,45.9 +757,hardwood,46.57 +758,hardwood,46.57 +760,hardwood,47.655 +761,hardwood,47.655 +762,hardwood,47.655 +763,hardwood,47.655 +764,hardwood,47.655 +765,hardwood,47.655 +766,hardwood,47.655 +768,hardwood,47.655 +769,hardwood,47.655 +770,hardwood,47.655 +771,hardwood,50.53467015 +772,hardwood,47.655 +773,hardwood,47.655 +774,hardwood,47.655 +800,hardwood,47.235 +801,hardwood,44.4 +802,hardwood,49.57 +803,hardwood,47.235 +804,hardwood,47.06 +805,hardwood,46.85 +806,hardwood,47.2 +807,hardwood,47.235 +808,hardwood,47.235 +809,hardwood,47.235 +810,hardwood,47.235 +811,hardwood,47.235 +812,hardwood,47.48 +813,hardwood,47.165 +814,hardwood,42.258 +815,hardwood,47.06 +816,hardwood,47.235 +817,hardwood,47.235 +818,hardwood,47.515 +819,hardwood,47.235 +820,hardwood,47.34 +821,hardwood,47.375 +822,hardwood,47.305 +823,hardwood,47.27 +824,hardwood,47.235 +825,hardwood,47.2 +826,hardwood,47.235 +827,hardwood,47.34 +828,hardwood,47.235 +829,hardwood,47.235 +830,hardwood,47.27 +831,hardwood,47.34 +832,hardwood,47.305 +833,hardwood,47.83125 +834,hardwood,47.235 +835,hardwood,47.2 +836,hardwood,47.235 +837,hardwood,47.34 +838,hardwood,47.99 +839,hardwood,47.235 +840,hardwood,47.235 +841,hardwood,47.235 +842,hardwood,47.235 +843,hardwood,47.235 +844,hardwood,47.235 +845,hardwood,47.235 +846,hardwood,47.235 +847,hardwood,46.5 +851,hardwood,48.71333333 +852,hardwood,47.48 +853,hardwood,47.48 +854,hardwood,43.99777778 +855,hardwood,47.48 +856,hardwood,47.48 +857,hardwood,47.48 +858,hardwood,47.48 +859,hardwood,47.48 +860,hardwood,47.48 +863,hardwood,47.48 +864,hardwood,47.48 +865,hardwood,47.48 +866,hardwood,47.48 +867,hardwood,42.46571429 +868,hardwood,44.31666667 +869,hardwood,47.48 +870,hardwood,47.48 +873,hardwood,47.48 +874,hardwood,47.48 +876,hardwood,47.48 +877,hardwood,47.48 +882,hardwood,47.48 +883,hardwood,47.48 +884,hardwood,46.4 +885,hardwood,47.48 +886,hardwood,47.48 +887,hardwood,43.4 +888,hardwood,47.48 +890,hardwood,47.48 +891,hardwood,47.48 +895,hardwood,47.48 +896,hardwood,47.48 +897,hardwood,47.48 +901,hardwood,44.63 +902,hardwood,46.99 +906,hardwood,47.48 +907,hardwood,47.48 +908,hardwood,42.75 +909,hardwood,47.48 +911,hardwood,47.48 +912,hardwood,47.48 +913,hardwood,47.48 +914,hardwood,47.48 +915,hardwood,47.48 +919,hardwood,47.48 +920,hardwood,48.04 +921,hardwood,48.04 +922,hardwood,48.04 +923,hardwood,48.04 +924,hardwood,48.04 +925,hardwood,48.04 +926,hardwood,48.04 +927,hardwood,48.04 +928,hardwood,48.04 +929,hardwood,48.04 +931,hardwood,45.01598887 +934,hardwood,47.48 +935,hardwood,47.48 +936,hardwood,47.44272624 +937,hardwood,47.48 +940,hardwood,47.315 +950,hardwood,48.18 +951,hardwood,48.20833333 +952,hardwood,48.20833333 +953,hardwood,48.20833333 +970,hardwood,47.41 +971,hardwood,47.2 +972,hardwood,47.69 +973,hardwood,47.235 +974,hardwood,44.29136824 +975,hardwood,47.62 +976,hardwood,47.41 +977,hardwood,47.305 +981,hardwood,47.515 +982,hardwood,47.48 +986,hardwood,47.48 +987,hardwood,44 +988,hardwood,43.7 +989,hardwood,42.7 +990,hardwood,47.48 +991,hardwood,47.48 +992,hardwood,47.48 +993,hardwood,47.48 +994,hardwood,47.48 +995,hardwood,47.48 +996,hardwood,47.48 +997,hardwood,47.48 +998,hardwood,47.48 +999,hardwood,47.48 +5091,hardwood,47.48 +5092,hardwood,47.48 +5093,hardwood,47.48 +5145,hardwood,47.64882353 +5146,hardwood,47.655 +5147,hardwood,47.64882353 +5148,hardwood,47.64882353 +5149,hardwood,43.82 +5150,hardwood,49.64 +5151,hardwood,41.8 +5152,hardwood,57.86514087 +5154,hardwood,47.64882353 +5155,hardwood,47.64882353 +5156,hardwood,49.5460594 +5157,hardwood,47.64882353 +5163,hardwood,46.8585125 +5164,hardwood,48.145 +5188,hardwood,47.99582417 +5189,hardwood,47.99582417 +5190,hardwood,47.99582417 +5192,hardwood,47.99582417 +5203,hardwood,46.99 +5232,hardwood,46.5 +5233,hardwood,46.5 +5256,hardwood,47.48 +5259,hardwood,46.5 +5260,hardwood,46.5 +5261,hardwood,47.48 +5279,hardwood,41.825 +5280,hardwood,46.11761194 +5281,hardwood,51.66531656 +5322,hardwood,48.32 +5328,hardwood,47.48 +5329,hardwood,47.48 +5363,hardwood,47.48 +5364,hardwood,47.48 +5375,hardwood,46.5 +5378,hardwood,47.48 +5401,hardwood,47.9 +5402,hardwood,47.1 +5411,hardwood,47.8475 +5415,hardwood,46.5 +5420,softwood,50.3 +5421,softwood,50.37142857 +5423,softwood,47.865 +5432,hardwood,46.5 +5435,hardwood,45.22911667 +5436,hardwood,47.48 +5443,softwood,49.75 +5444,softwood,48.005 +5462,hardwood,45.7 +5491,hardwood,47.515 +5492,hardwood,47.50692308 +5493,hardwood,47.375 +5523,hardwood,47.41 +5529,hardwood,46.5 +5559,hardwood,45.7 +5591,hardwood,53.40147083 +5599,hardwood,47.375 +5600,hardwood,47.375 +5603,hardwood,47.48 +5776,hardwood,47.06 +5800,softwood,47.865 +5818,hardwood,47.48 +5965,hardwood,43.69202053 +5983,hardwood,47.1175625 +6001,hardwood,47.48 +6002,softwood,41.1385 +6003,hardwood,44.64685714 +6004,hardwood,46.804815 +6006,hardwood,47.375 +6007,hardwood,46.605 +6008,hardwood,47.48 +6009,hardwood,45.20342857 +6010,hardwood,39.4295 +6011,hardwood,39.5721 +6012,hardwood,47.48 +6013,hardwood,47.48 +6014,hardwood,46.605 +6015,hardwood,47.48 +6016,hardwood,40.3555 +6017,hardwood,47.14645 +6018,hardwood,47.48 +6019,hardwood,47.48 +6020,hardwood,47.14645 +6021,hardwood,47.48 +6023,hardwood,47.48 +6025,hardwood,47.48 +6026,hardwood,47.48 +6028,hardwood,47.48 +6029,hardwood,46.92 +6032,hardwood,47.48 +6034,softwood,45.3 +6035,softwood,47.48 +6036,softwood,47.48 +6037,softwood,47.48 +6042,hardwood,46.99 +6043,hardwood,46.99 +6044,hardwood,46.99 +6046,hardwood,46.99 +6047,hardwood,46.99 +6048,hardwood,46.979605 +6049,hardwood,46.6029 +6051,hardwood,47.98911 +6053,hardwood,47.48 +6055,hardwood,47.48 +6056,hardwood,47.48 +6057,hardwood,48.25 +6058,hardwood,48.305545 +6059,hardwood,47.48 +6060,hardwood,47.48 +6061,hardwood,47.69 +6062,hardwood,47.305 +6063,hardwood,47.416545 +6064,hardwood,47.48 +6066,hardwood,47.48 +6069,hardwood,46.012275 +6073,hardwood,46.012275 +6075,hardwood,47.48 +6077,hardwood,48.010215 +6078,hardwood,47.795 +6080,hardwood,47.48 +6082,hardwood,47.48 +6083,hardwood,47.515 +6084,hardwood,47.515 +6085,hardwood,47.515 +6086,hardwood,47.949945 +6088,hardwood,47.55 +6089,hardwood,47.55 +6090,hardwood,47.55 +6091,hardwood,47.9 +6092,hardwood,47.48 +6093,hardwood,47.48 +6095,hardwood,47.341225 +6096,hardwood,47.943925 +6097,hardwood,47.943925 +6101,hardwood,47.48 +6103,hardwood,47.48 +6106,hardwood,47.48 +6107,hardwood,47.48 +6108,hardwood,46.605 +6109,hardwood,46.617285 +6111,hardwood,47.48 +6114,hardwood,47.48 +6118,hardwood,47.55 +6120,hardwood,47.48 +6124,hardwood,47.48 +6125,hardwood,47.48 +6127,hardwood,47.48 +6128,hardwood,47.48 +6129,hardwood,47.48 +6130,hardwood,47.55 +6131,hardwood,47.48 +6135,hardwood,46.955 +6137,hardwood,47.48 +6138,hardwood,47.115195 +6139,hardwood,46.955 +6142,hardwood,47.115195 +6143,hardwood,47.115195 +6144,hardwood,47.115195 +6145,hardwood,47.115195 +6146,hardwood,47.48 +6147,hardwood,47.48 +6148,hardwood,47.025 +6149,hardwood,47.48 +6150,hardwood,47.48 +6151,hardwood,47.48 +6152,hardwood,47.48 +6154,softwood,47.48 +6155,softwood,47.865 +6156,softwood,47.865 +6157,softwood,47.48 +6158,softwood,47.74089 +6159,hardwood,46.40704 +6160,hardwood,47.48 +6161,hardwood,47.13 +6162,hardwood,47.48 +6163,hardwood,47.48 +6164,hardwood,47.48 +6165,hardwood,47.48 +6166,hardwood,47.13 +6167,hardwood,47.62 +6169,hardwood,47.62 +6171,hardwood,47.48 +6173,hardwood,47.48 +6175,hardwood,47.606 +6176,hardwood,47.606 +6177,hardwood,47.375 +6178,hardwood,47.606 +6179,hardwood,47.08506 +6180,hardwood,47.27 +6181,hardwood,47.725 +6184,hardwood,46.9025 +6185,hardwood,46.92 +6186,hardwood,46.92 +6187,hardwood,46.92 +6188,hardwood,46.92 +6189,hardwood,46.92 +6190,hardwood,46.92 +6193,hardwood,47.025 +6197,hardwood,47.83 +6198,hardwood,47.48 +6199,hardwood,47.34 +6200,hardwood,47.445 +6203,hardwood,42.644 +6205,hardwood,47.025 +6206,hardwood,47.48 +6208,hardwood,47.27 +6212,softwood,42.35 +6213,hardwood,47.025 +6215,hardwood,47.55 +6216,hardwood,47.48 +6217,hardwood,47.48 +6218,hardwood,47.48 +6219,hardwood,47.48 +6220,hardwood,47.48 +6221,hardwood,39.523 +6222,hardwood,47.657625 +6223,hardwood,47.657625 +6224,hardwood,47.48 +6225,hardwood,46.8892 +6226,hardwood,47.48 +6227,hardwood,47.48 +6228,hardwood,47.48 +6229,hardwood,47.48 +6230,hardwood,46.8892 +6231,hardwood,47.48 +6232,hardwood,47.48 +6233,hardwood,47.48 +6234,hardwood,46.5 +6235,hardwood,47.48 +6236,hardwood,44.7 +6237,hardwood,47.280955 +6238,hardwood,47.48 +6239,hardwood,48.18 +6240,hardwood,47.48 +6241,hardwood,47.40615 +6242,hardwood,47.025 +6243,hardwood,47.025 +6244,hardwood,47.025 +6245,hardwood,47.025 +6246,hardwood,47.025 +6247,hardwood,47.48 +6248,hardwood,47.55 +6250,hardwood,48.026 +6251,hardwood,47.48 +6252,hardwood,47.48 +6253,hardwood,47.48 +6255,hardwood,47.48 +6257,hardwood,47.48 +6258,hardwood,47.48 +6259,hardwood,47.48 +6260,hardwood,48.565 +6262,hardwood,48.285 +6264,hardwood,47.76 +6266,hardwood,44.73 +6267,hardwood,46.71 +6268,hardwood,46.71 +6269,hardwood,46.64 +6270,hardwood,47.48 +6272,hardwood,47.48 +6273,hardwood,47.48 +6274,hardwood,47.48 +6275,hardwood,47.48 +6277,hardwood,47.865 +6278,hardwood,47.865 +6279,hardwood,47.865 +6280,hardwood,47.865 +6283,hardwood,47.48 +6284,hardwood,47.48 +6286,hardwood,48.32 +6294,hardwood,47.48 +6295,hardwood,47.48 +6297,hardwood,47.48 +6299,hardwood,47.55 +6303,hardwood,47.48 +6304,hardwood,47.48 +6306,hardwood,47.48 +6308,hardwood,47.6598932 +6311,hardwood,47.48 +6313,hardwood,47.48 +6315,hardwood,47.48 +6316,hardwood,47.48 +6317,hardwood,47.48 +6318,hardwood,45.9288 +6319,hardwood,47.48 +6320,hardwood,47.48 +6325,hardwood,47.48 +6326,hardwood,47.48 +6328,hardwood,47.48 +6329,hardwood,47.48 +6331,softwood,47.865 +6337,hardwood,47.48 +6338,hardwood,47.48 +6341,hardwood,47.48 +6342,hardwood,47.55 +6343,hardwood,47.31109 +6344,hardwood,47.795 +6345,hardwood,47.31109 +6346,hardwood,47.48 +6347,hardwood,47.55 +6350,hardwood,47.48 +6351,hardwood,47.48 +6352,hardwood,47.48 +6353,hardwood,47.48 +6354,hardwood,47.48 +6355,hardwood,47.48 +6356,hardwood,47.48 +6358,hardwood,47.48 +6359,hardwood,47.48 +6360,hardwood,47.48 +6366,hardwood,48.005 +6367,hardwood,48.005 +6370,hardwood,47.48 +6372,hardwood,47.585 +6373,hardwood,47.585 +6374,hardwood,47.341225 +6375,hardwood,47.585 +6377,hardwood,47.585 +6378,hardwood,47.41 +6379,hardwood,47.76 +6380,hardwood,47.48 +6381,hardwood,46.77167 +6383,hardwood,47.48 +6384,hardwood,47.48 +6386,hardwood,47.48 +6387,hardwood,47.48 +6389,hardwood,47.48 +6390,hardwood,47.48 +6393,hardwood,47.48 +6395,hardwood,47.48 +6396,hardwood,48.74 +6397,hardwood,47.55 +6398,hardwood,47.55 +6399,hardwood,47.62 +6400,hardwood,47.62 +6401,hardwood,47.62 +6402,hardwood,47.48 +6403,hardwood,47.48 +6405,hardwood,47.13028 +6406,hardwood,47.48 +6407,hardwood,47.48 +6408,hardwood,47.13028 +6410,hardwood,47.48 +6415,hardwood,47.48 +6417,hardwood,47.48 +6418,hardwood,47.48 +6419,hardwood,47.48 +6420,hardwood,47.964 +6422,hardwood,46.815 +6425,hardwood,47.48 +6427,hardwood,47.48 +6429,hardwood,47.48 +6430,hardwood,46.75 +6433,hardwood,47.48 +6434,hardwood,43.2 +6437,hardwood,46.40704 +6438,hardwood,47.50333333 +6439,hardwood,47.48 +6441,hardwood,44.02666667 +6443,hardwood,47.48 +6444,hardwood,48.1576 +6445,hardwood,43.12857143 +6447,hardwood,47.48 +6448,hardwood,47.48 +6449,hardwood,42.67428571 +6452,hardwood,47.55 +6454,hardwood,47.48 +6457,hardwood,47.48 +6458,hardwood,39.578 +6459,hardwood,47.96923 +6460,hardwood,47.943925 +6461,hardwood,47.97 +6462,hardwood,48.32 +6463,hardwood,47.96923 +6468,hardwood,47.48 +6469,hardwood,47.48 +6470,hardwood,47.41 +6472,hardwood,49.82 +6473,hardwood,47.76 +6474,hardwood,47.48 +6475,hardwood,47.48 +6477,hardwood,47.48 +6478,hardwood,47.76 +6479,hardwood,47.095 +6480,hardwood,47.48 +6481,hardwood,47.48 +6482,hardwood,47.61244 +6483,hardwood,47.61244 +6492,hardwood,47.61244 +6493,hardwood,47.61244 +6494,hardwood,47.61244 +6495,hardwood,47.61244 +6496,hardwood,47.61244 +6497,hardwood,47.62 +6498,hardwood,47.62 +6499,hardwood,47.62 +6500,hardwood,47.62 +6503,hardwood,47.62 +6504,hardwood,47.62 +6507,hardwood,47.655 +6508,hardwood,47.655 +6509,hardwood,47.655 +6510,hardwood,47.655 +6511,hardwood,47.48 +6513,hardwood,47.655 +6514,hardwood,47.655 +6516,hardwood,50.19278651 +6517,hardwood,47.55 +6518,hardwood,47.55 +6519,hardwood,47.48 +6520,hardwood,47.48 +6521,hardwood,47.48 +6522,hardwood,47.48 +6523,hardwood,47.48 +6524,hardwood,47.3925 +6525,hardwood,46.955 +6526,hardwood,47.48 +6528,hardwood,47.48 +6529,hardwood,47.45 +6532,hardwood,47.48 +6533,softwood,48.04777778 +6535,hardwood,47.48 +6538,softwood,48.04777778 +6539,hardwood,47.48 +6541,hardwood,43.96 +6542,hardwood,47.48 +6543,hardwood,47.48 +6545,hardwood,48.565 +6546,hardwood,48.635 +6547,hardwood,48.53 +6548,hardwood,48.565 +6549,hardwood,48.565 +6552,hardwood,47.41 +6553,hardwood,47.41 +6554,hardwood,47.48 +6555,hardwood,47.69 +6557,hardwood,47.655 +6559,hardwood,47.48 +6560,hardwood,47.48 +6561,hardwood,47.655 +6562,hardwood,47.655 +6563,hardwood,47.655 +6564,hardwood,47.48 +6565,hardwood,47.48 +6567,hardwood,47.48 +6568,hardwood,47.025 +6569,hardwood,47.48 +6570,hardwood,47.26587 +6572,hardwood,46.85 +6573,hardwood,47.48 +6574,hardwood,47.48 +6575,hardwood,47.48 +6576,hardwood,47.48 +6577,hardwood,47.48 +6578,hardwood,46.85 +6579,hardwood,46.85 +6580,hardwood,46.85 +6581,hardwood,47.48 +6582,hardwood,47.48 +6583,hardwood,46.85 +6584,hardwood,47.48 +6586,hardwood,48.075 +6588,hardwood,48.075 +6589,hardwood,48.075 +6590,hardwood,48.075 +6591,hardwood,48.075 +6592,hardwood,48.075 +6593,hardwood,47.27 +6594,hardwood,47.27 +6595,hardwood,47.27 +6596,hardwood,47.55 +6597,hardwood,47.55 +6601,hardwood,47.55 +6604,hardwood,47.55 +6605,hardwood,47.55 +6606,hardwood,47.55 +6610,hardwood,47.55 +6611,hardwood,47.55 +6612,hardwood,47.55 +6613,hardwood,47.55 +6614,hardwood,47.55 +6615,hardwood,47.55 +6616,hardwood,47.55 +6620,hardwood,47.55 +6621,hardwood,47.55 +6622,hardwood,47.55 +6625,hardwood,47.55 +6626,hardwood,47.55 +6627,hardwood,47.55 +6628,hardwood,47.55 +6629,hardwood,47.55 +6630,hardwood,47.55 +6631,hardwood,47.48 +6632,hardwood,47.41 +6633,hardwood,47.175465 +6634,hardwood,47.41 +6635,hardwood,47.41 +6636,hardwood,47.41 +6637,hardwood,47.48 +6639,hardwood,47.48 +6641,hardwood,47.48 +6642,hardwood,47.48 +6644,hardwood,47.48 +6645,hardwood,47.48 +6646,hardwood,47.48 +6648,hardwood,47.48 +6650,hardwood,47.48 +6651,hardwood,47.48 +6652,hardwood,47.34 +6653,hardwood,47.48 +6655,hardwood,47.48 +6658,hardwood,47.48 +6660,hardwood,47.48 +6661,hardwood,47.48 +6662,hardwood,47.48 +6663,hardwood,47.48 +6664,hardwood,47.48 +6665,hardwood,47.48 +6666,hardwood,47.48 +6668,hardwood,47.48 +6669,hardwood,47.48 +6670,hardwood,47.48 +6671,hardwood,47.48 +6673,hardwood,47.48 +6679,hardwood,47.48 +6681,hardwood,47.62 +6683,hardwood,47.48 +6684,hardwood,45.37312064 +6685,hardwood,47.48 +6686,hardwood,47.48 +6687,hardwood,47.13028 +6688,hardwood,47.48 +6689,hardwood,47.48 +6691,hardwood,48.11 +6693,hardwood,47.48 +6694,hardwood,46.85 +6696,hardwood,47.62 +6697,hardwood,46.85 +6699,hardwood,46.85 +6700,hardwood,47.48 +6702,hardwood,46.5 +6703,hardwood,48.11 +6705,hardwood,47.48 +6706,hardwood,47.48 +6707,hardwood,47.61543478 +6708,hardwood,47.655 +6709,hardwood,46.885 +6710,hardwood,47.48 +6711,hardwood,47.48 +6712,hardwood,47.48 +6714,hardwood,47.48 +6716,hardwood,47.62 +6717,hardwood,46.7835 +6718,hardwood,46.7835 +6719,hardwood,46.7835 +6720,hardwood,46.7835 +6721,hardwood,47.62 +6722,hardwood,47.62 +6724,hardwood,47.62 +6726,hardwood,46.7835 +6728,hardwood,47.5 +6729,hardwood,47.641 +6730,hardwood,47.48 +6731,hardwood,45 +6733,hardwood,47.97 +6735,hardwood,47.48 +6736,hardwood,47.641 +6737,hardwood,47.48 +6738,hardwood,47.48 +6739,hardwood,47.48 +6741,hardwood,47.641 +6742,hardwood,47.61244 +6743,hardwood,47.48 +6744,hardwood,47.55 +6745,hardwood,47.55 +6746,hardwood,47.48 +6747,hardwood,47.48 +6748,hardwood,47.165 +6749,hardwood,46.33774 +6750,hardwood,47.48 +6751,hardwood,46.33774 +6752,hardwood,46.7786 +6754,hardwood,44.5 +6755,hardwood,47.220685 +6756,hardwood,47.48 +6758,hardwood,47.79325 +6759,hardwood,45.7 +6760,hardwood,43.6 +6761,hardwood,47.48 +6762,hardwood,47.48 +6763,hardwood,47.48 +6765,hardwood,47.48 +6767,hardwood,47.48 +6768,hardwood,47.21657895 +6769,hardwood,46.64 +6771,hardwood,46.941 +6772,hardwood,47.48 +6773,hardwood,47.48 +6774,hardwood,47.48 +6775,hardwood,47.48 +6778,hardwood,47.2854 +6779,hardwood,47.2854 +6781,hardwood,47.2854 +6782,hardwood,47.21657895 +6783,hardwood,47.2854 +6784,hardwood,47.2854 +6785,hardwood,47.21657895 +6786,softwood,52.59982954 +6787,softwood,48.215 +6788,softwood,47.96741928 +6790,hardwood,47.48 +6791,hardwood,47.21657895 +6792,hardwood,47.48 +6794,hardwood,46.305 +6795,softwood,47.865 +6796,softwood,47.865 +6797,hardwood,47.2 +6799,hardwood,47.2 +6800,hardwood,47.55 +6801,hardwood,47.55 +6802,hardwood,47.55 +6805,hardwood,47.55 +6806,hardwood,47.55 +6807,hardwood,47.55 +6810,hardwood,47.55 +6811,hardwood,47.55 +6812,hardwood,47.55 +6813,hardwood,47.55 +6814,hardwood,47.55 +6815,hardwood,47.55 +6818,hardwood,47.55 +6819,hardwood,47.55 +6822,hardwood,47.55 +6823,hardwood,47.55 +6824,hardwood,47.55 +6825,hardwood,47.55 +6826,hardwood,47.55 +6827,hardwood,47.55 +6828,hardwood,47.55 +6829,hardwood,47.55 +6830,hardwood,47.55 +6833,hardwood,47.55 +6834,hardwood,47.48 +6835,hardwood,47.48 +6837,hardwood,47.55 +6838,hardwood,47.55 +6839,hardwood,47.48 +6840,hardwood,47.55 +6841,hardwood,47.55 +6842,hardwood,47.55 +6843,hardwood,47.48 +6844,hardwood,47.55 +6847,hardwood,47.55 +6848,hardwood,47.48 +6849,hardwood,47.55 +6850,hardwood,47.48 +6852,softwood,47.48 +6853,softwood,47.55 +6854,softwood,47.55 +6855,hardwood,47.025 +6857,hardwood,47.48 +6858,hardwood,46.745 +6859,hardwood,47.48 +6860,hardwood,47.76 +6862,hardwood,47.48 +6863,hardwood,47.55 +6864,hardwood,47.55 +6865,hardwood,47.55 +6866,hardwood,47.55 +6867,hardwood,50.96666667 +6868,hardwood,45.72044273 +6869,hardwood,47.48 +6870,hardwood,47.48 +6871,hardwood,47.48 +6872,hardwood,47.48 +6873,hardwood,47.48 +6875,hardwood,47.55 +6876,hardwood,47.55 +6877,hardwood,47.55 +6880,hardwood,47.55 +6881,hardwood,47.55 +6882,hardwood,47.55 +6883,hardwood,47.48 +6884,hardwood,47.73599 +6885,hardwood,48.57676 +6886,hardwood,48.57676 +6887,hardwood,48.57676 +6888,hardwood,46.10666667 +6889,hardwood,47.48 +6891,hardwood,47.62 +6896,hardwood,47.48 +6898,hardwood,47.725 +6899,hardwood,47.48 +6900,hardwood,46.22 +6902,hardwood,46.71 +6903,hardwood,46.85 +6904,hardwood,46.85 +6905,hardwood,45.87 +6906,hardwood,46.85 +6907,hardwood,47.37136 +6909,hardwood,47.48 +6910,hardwood,46.85 +6911,hardwood,46.71 +6912,hardwood,47.48 +6918,hardwood,47.48 +6921,hardwood,47.0495 +6923,hardwood,47.48 +6924,hardwood,47.783345 +6927,hardwood,47.48 +6928,hardwood,48.18 +6930,hardwood,47.48 +6932,hardwood,47.48 +6933,hardwood,47.85352 +6934,hardwood,47.48 +6935,hardwood,46.7975 +6936,hardwood,46.7975 +6937,hardwood,47.48 +6938,hardwood,47.48 +6939,hardwood,47.48 +6940,hardwood,47.48 +6941,hardwood,46.798795 +6942,hardwood,46.798795 +6943,hardwood,46.798795 +6944,hardwood,47.025 +6945,hardwood,47.025 +6946,hardwood,47.025 +6947,hardwood,47.025 +6948,hardwood,47.025 +6949,hardwood,46.7975 +6952,hardwood,47.025 +6953,hardwood,47.025 +6954,hardwood,47.96 +6955,hardwood,48.04 +6957,hardwood,47.025 +6958,hardwood,47.025 +6959,hardwood,47.96 +6961,hardwood,47.48 +6963,hardwood,47.96 +6965,hardwood,47.61244 +6966,hardwood,47.48 +6967,hardwood,47.2 +6968,hardwood,47.27 +6969,hardwood,47.27 +6970,hardwood,47.461765 +6971,hardwood,47.27 +6972,hardwood,47.396 +6973,hardwood,44.66666667 +6974,hardwood,47.62 +6975,hardwood,47.62 +6976,hardwood,47.62 +6977,hardwood,47.62 +6978,hardwood,47.62 +6979,hardwood,47.62 +6980,hardwood,47.62 +6981,hardwood,47.62 +6982,hardwood,47.62 +6983,hardwood,47.62 +6984,hardwood,47.62 +6985,hardwood,47.62 +6990,hardwood,46.979605 +6991,hardwood,47.63925 +6992,hardwood,47.55 +6994,hardwood,47.039875 +6996,hardwood,44.76666667 +6997,hardwood,47.48 +6998,hardwood,44.436 +6999,hardwood,47.48 +7000,hardwood,47.48 +7003,hardwood,47.48 +7004,hardwood,47.48 +7005,hardwood,47.48 +7006,hardwood,47.48 +7007,hardwood,47.48 +7008,hardwood,47.48 +7011,hardwood,47.48 +7012,hardwood,48.397 +7013,hardwood,48.397 +7014,hardwood,48.495 +7015,hardwood,47.48 +7016,hardwood,47.48 +7017,hardwood,47.83 +7019,hardwood,47.48 +7021,hardwood,47.48 +7022,hardwood,47.48 +7023,hardwood,47.40461538 +7024,hardwood,47.48 +7025,hardwood,47.0635 +7026,hardwood,46.481135 +7028,hardwood,47.0005 +7030,hardwood,47.48 +7031,hardwood,46.12795 +7032,hardwood,46.05977 +7033,hardwood,47.1685 +7034,hardwood,47.48 +7035,hardwood,47.000283 +7036,hardwood,47.48 +7038,hardwood,46.26242 +7039,hardwood,46.715775 +7040,hardwood,47.48 +7041,hardwood,46.40704 +7043,hardwood,47.48 +7044,hardwood,46.82893 +7045,hardwood,46.400495 +7046,hardwood,47.48 +7047,hardwood,46.743215 +7048,hardwood,46.015285 +7049,hardwood,47.48 +7051,hardwood,47.48 +7052,hardwood,46.40704 +7053,hardwood,47.48 +7054,hardwood,47.48 +7056,hardwood,46.40438 +7057,hardwood,47.202555 +7059,hardwood,46.745 +7060,hardwood,47.48 +7061,hardwood,47.48 +7062,hardwood,47.48 +7063,hardwood,47.48 +7065,hardwood,46.745 +7066,hardwood,47.48 +7067,hardwood,47.48 +7068,hardwood,47.48 +7069,hardwood,47.48 +7071,hardwood,47.48 +7072,hardwood,47.48 +7075,hardwood,47.48 +7076,hardwood,47.48 +7078,hardwood,46.745 +7079,hardwood,46.745 +7081,hardwood,47.48 +7082,hardwood,50.50034647 +7083,hardwood,47.48 +7084,hardwood,47.48 +7086,hardwood,46.745 +7087,hardwood,46.745 +7088,hardwood,46.745 +7089,hardwood,47.48 +7090,hardwood,47.48 +7091,hardwood,46.745 +7092,hardwood,47.57870256 +7093,hardwood,47.48 +7094,hardwood,47.48 +7096,hardwood,46.745 +7098,hardwood,47.48 +7099,hardwood,46.745 +7100,hardwood,47.48 +7101,hardwood,46.745 +7102,hardwood,46.745 +7103,hardwood,47.48 +7104,hardwood,47.48 +7105,hardwood,47.48 +7109,hardwood,47.48 +7110,hardwood,47.725 +7111,hardwood,47.48 +7112,hardwood,47.48 +7113,hardwood,47.48 +7114,hardwood,47.725 +7115,hardwood,47.725 +7116,hardwood,47.48 +7117,hardwood,46.885 +7119,hardwood,47.55 +7120,hardwood,47.55 +7123,hardwood,47.935 +7124,hardwood,47.935 +7125,hardwood,47.935 +7126,hardwood,47.935 +7127,hardwood,47.935 +7128,hardwood,47.935 +7129,hardwood,47.97 +7131,hardwood,46.78371 +7132,hardwood,46.78371 +7133,hardwood,47.62 +7135,hardwood,47.48 +7136,hardwood,47.48 +7137,hardwood,47.48 +7141,hardwood,46.40704 +7142,hardwood,46.71 +7143,hardwood,46.71 +7144,hardwood,47.795 +7145,hardwood,47.795 +7146,hardwood,46.42 +7147,hardwood,47.48 +7148,hardwood,47.48 +7149,hardwood,47.48 +7150,hardwood,47.48 +7151,hardwood,47.48 +7152,hardwood,47.76 +7153,hardwood,47.48 +7154,hardwood,47.48 +7155,hardwood,45.4 +7156,hardwood,47.9 +7158,hardwood,47.48 +7159,hardwood,47.48 +7160,hardwood,47.48 +7162,hardwood,47.48 +7163,hardwood,47.56904 +7164,hardwood,47.48 +7165,hardwood,47.9 +7166,hardwood,47.48 +7167,hardwood,47.795 +7168,hardwood,47.9 +7169,hardwood,47.9 +7171,hardwood,47.9 +7173,hardwood,47.48 +7174,hardwood,47.48 +7175,hardwood,47.8083 +7176,hardwood,47.9 +7177,hardwood,47.48 +7178,hardwood,47.9 +7179,hardwood,48.0946 +7180,hardwood,47.220685 +7181,hardwood,47.48 +7182,hardwood,47.025 +7184,hardwood,47.48 +7185,hardwood,47.48 +7186,hardwood,46.675 +7188,hardwood,47.655 +7190,hardwood,47.48 +7191,hardwood,47.61244 +7192,hardwood,47.61244 +7193,hardwood,47.61244 +7194,hardwood,47.48 +7195,hardwood,47.48 +7196,hardwood,47.48 +7198,hardwood,47.48 +7199,hardwood,47.40413167 +7200,hardwood,46.955 +7202,hardwood,47.48 +7203,hardwood,46.5 +7205,hardwood,47.50692308 +7206,hardwood,44 +7207,hardwood,46.864 +7208,hardwood,46.864 +7209,hardwood,46.864 +7210,hardwood,48.54 +7211,hardwood,47.48 +7212,hardwood,47.48 +7213,hardwood,47.48 +7214,hardwood,47.48 +7215,hardwood,46.74976 +7216,hardwood,46.675 +7217,hardwood,46.74976 +7218,hardwood,47.48 +7219,hardwood,46.74976 +7220,hardwood,46.7485 +7221,hardwood,46.74976 +7223,hardwood,47.48 +7224,hardwood,46.979605 +7225,hardwood,46.979605 +7226,hardwood,46.979605 +7227,hardwood,46.979605 +7228,hardwood,46.979605 +7229,hardwood,47.48 +7231,hardwood,47.48 +7233,hardwood,47.039875 +7235,hardwood,47.48 +7237,hardwood,47.48 +7239,hardwood,47.48 +7241,hardwood,47.725 +7243,hardwood,46.5 +7245,hardwood,41.64142857 +7246,hardwood,45.6 +7247,hardwood,47.34 +7248,hardwood,47.34 +7249,hardwood,47.34 +7250,hardwood,47.34 +7251,hardwood,47.657625 +7252,hardwood,47.585 +7253,hardwood,47.657625 +7254,softwood,47.165 +7255,hardwood,45.6 +7256,hardwood,47.48 +7257,hardwood,47.34 +7258,hardwood,47.48 +7260,hardwood,47.97 +7262,hardwood,47.48 +7264,hardwood,47.48 +7268,hardwood,47.48 +7272,hardwood,47.3617 +7273,hardwood,47.48 +7274,hardwood,47.3617 +7275,hardwood,47.69 +7279,hardwood,47.48 +7280,hardwood,47.48 +7282,hardwood,47.3344 +7285,hardwood,47.48 +7286,hardwood,47.48 +7288,hardwood,47.48 +7290,hardwood,48.66333333 +7294,hardwood,47.48 +7295,hardwood,47.48 +7298,hardwood,41.97833333 +7299,hardwood,47.48 +7300,hardwood,47.48 +7302,hardwood,47.48 +7303,hardwood,47.48 +7305,hardwood,47.48 +7306,hardwood,47.48 +7307,hardwood,47.235 +7309,hardwood,47.48 +7311,hardwood,47.42295 +7312,hardwood,47.42295 +7313,hardwood,47.62 +7315,hardwood,47.48 +7317,hardwood,47.48 +7319,hardwood,48.47127 +7321,hardwood,42.4 +7327,hardwood,47.48 +7328,hardwood,44.68881663 +7329,hardwood,46.5 +7330,hardwood,47.48 +7331,hardwood,46.5 +7332,hardwood,46.815 +7333,hardwood,47.48 +7334,hardwood,47.585 +7335,hardwood,46.72666667 +7336,hardwood,47.48 +7338,hardwood,47.220685 +7340,hardwood,47.220685 +7341,hardwood,47.48 +7343,hardwood,47.97 +7344,hardwood,47.97 +7345,hardwood,47.97 +7346,hardwood,47.97 +7347,hardwood,47.48 +7349,hardwood,45.1 +7350,hardwood,48.635 +7353,hardwood,47.48 +7354,hardwood,47.48 +7355,hardwood,47.48 +7357,hardwood,47.48 +7359,hardwood,48.07 +7360,hardwood,47.025 +7362,hardwood,47.025 +7363,hardwood,48.32 +7364,hardwood,48.32 +7365,hardwood,48.425 +7366,hardwood,47.48 +7367,hardwood,48.32 +7368,hardwood,48.32 +7370,hardwood,47.025 +7371,hardwood,47.025 +7372,hardwood,47.025 +7373,hardwood,47.025 +7374,hardwood,47.165 +7376,hardwood,47.165 +7377,hardwood,47.62 +7381,hardwood,48.32 +7384,hardwood,47.60943 +7385,hardwood,47.60943 +7386,hardwood,47.60943 +7387,hardwood,47.60943 +7388,hardwood,47.60943 +7389,hardwood,47.60943 +7390,hardwood,47.60943 +7391,hardwood,47.60943 +7392,hardwood,47.60943 +7393,hardwood,47.8083 +7397,hardwood,47.8083 +7401,hardwood,47.8083 +7402,hardwood,47.8083 +7403,hardwood,47.48 +7404,hardwood,47.8083 +7407,hardwood,48.18 +7408,hardwood,47.8083 +7409,hardwood,47.48 +7410,hardwood,47.48 +7411,hardwood,47.8083 +7412,hardwood,47.48 +7413,hardwood,47.8083 +7418,hardwood,47.48 +7420,hardwood,47.48 +7422,hardwood,47.48 +7424,hardwood,46.753575 +7427,hardwood,47.9 +7428,hardwood,47.9 +7429,hardwood,47.9 +7430,hardwood,47.9 +7431,hardwood,47.9 +7432,hardwood,46.5 +7433,hardwood,47.48 +7434,hardwood,46.00592399 +7438,hardwood,47.48 +7440,hardwood,47.41 +7441,hardwood,47.41 +7442,hardwood,47.48 +7445,hardwood,47.48 +7446,hardwood,47.48 +7448,hardwood,45.8525 +7453,hardwood,47.62 +7454,hardwood,47.48 +7455,hardwood,47.48 +7456,hardwood,47.48 +7457,hardwood,47.48 +7458,hardwood,47.48 +7459,hardwood,47.48 +7460,hardwood,47.62 +7462,hardwood,47.48 +7463,hardwood,47.48 +7464,hardwood,47.48 +7465,hardwood,47.48 +7466,hardwood,47.48 +7467,hardwood,47.48 +7469,hardwood,47.48 +7470,hardwood,47.48 +7471,hardwood,47.48 +7472,hardwood,47.48 +7474,hardwood,45 +7475,hardwood,47.76 +7477,hardwood,46.801315 +7479,hardwood,47.48 +7481,hardwood,47.48 +7482,hardwood,45.6 +7483,hardwood,47.935 +7485,hardwood,47.48 +7487,hardwood,47.48 +7489,softwood,47.725 +7490,hardwood,47.48 +7491,hardwood,47.48 +7492,hardwood,47.48 +7493,hardwood,47.48 +7494,hardwood,48.39 +7495,hardwood,47.48 +7496,hardwood,47.729375 +7497,hardwood,47.025 +7499,hardwood,48.62 +7501,hardwood,47.48 +7503,hardwood,47.48 +7506,hardwood,46.05 +7507,hardwood,47.445 +7508,hardwood,47.48 +7509,hardwood,47.60943 +7510,hardwood,47.60943 +7511,hardwood,47.60943 +7512,hardwood,47.60943 +7513,hardwood,47.60943 +7514,hardwood,47.48 +7515,hardwood,45.87781667 +7516,hardwood,47.63333333 +7517,hardwood,46.598 +7518,hardwood,47.039875 +7519,hardwood,47.039875 +7520,hardwood,47.039875 +7521,hardwood,47.039875 +7522,hardwood,47.039875 +7523,hardwood,47.039875 +7524,hardwood,47.039875 +7528,hardwood,47.039875 +7529,hardwood,47.039875 +7530,hardwood,47.48 +7531,hardwood,47.48 +7532,hardwood,47.48 +7533,hardwood,47.605 +7539,hardwood,46.92 +7541,hardwood,47.48 +7543,hardwood,47.62 +7550,hardwood,42.196 +7552,hardwood,47.48 +7556,hardwood,47.48 +7558,hardwood,46.43 +7559,hardwood,46.43 +7560,hardwood,46.43 +7561,hardwood,45.709 +7562,hardwood,46.43 +7564,hardwood,47.2 +7565,hardwood,47.48 +7566,hardwood,47.2 +7567,hardwood,47.38666667 +7569,hardwood,47.48 +7570,hardwood,47.48 +7571,hardwood,47.69 +7573,hardwood,47.48 +7574,hardwood,47.48 +7575,hardwood,47.31109 +7576,hardwood,47.31109 +7577,hardwood,47.375 +7578,hardwood,47.375 +7579,hardwood,47.370436 +7580,hardwood,46.5 +7583,hardwood,46.437175 +7586,hardwood,47.83 +7587,hardwood,47.83 +7588,hardwood,46.815 +7590,hardwood,47.48 +7591,hardwood,47.48 +7592,hardwood,47.48 +7593,hardwood,46.5 +7595,hardwood,47.48 +7598,hardwood,46.6 +7600,hardwood,51.57 +7602,hardwood,46.955 +7604,hardwood,47.48 +7606,hardwood,47.48 +7608,hardwood,47.48 +7614,hardwood,47.27 +7616,hardwood,47.27 +7617,hardwood,47.27 +7618,hardwood,48.145 +7619,hardwood,48.145 +7620,hardwood,48.145 +7621,hardwood,48.355 +7623,hardwood,48.145 +7625,hardwood,48.145 +7626,hardwood,47.795 +7627,hardwood,48.145 +7628,hardwood,47.48 +7630,hardwood,47.48 +7631,hardwood,44.05973417 +7632,hardwood,43 +7633,hardwood,47.48 +7634,hardwood,47.795 +7635,hardwood,47.48 +7636,hardwood,47.48 +7637,hardwood,47.74115385 +7638,hardwood,47.585 +7639,hardwood,47.34 +7641,hardwood,47.585 +7642,hardwood,47.585 +7643,hardwood,47.48 +7644,hardwood,47.48 +7645,hardwood,47.48 +7646,hardwood,47.48 +7647,hardwood,47.48 +7648,hardwood,47.48 +7649,hardwood,47.165 +7650,hardwood,47.165 +7651,hardwood,47.165 +7652,hardwood,47.48 +7653,hardwood,46.2865 +7654,hardwood,47.040995 +7655,hardwood,47.040995 +7657,hardwood,47.461765 +7658,hardwood,47.725 +7659,hardwood,47.4919 +7660,hardwood,47.61244 +7661,hardwood,47.61244 +7662,hardwood,47.48 +7663,hardwood,47.48 +7664,hardwood,46.255 +7666,hardwood,46.255 +7667,hardwood,47.48 +7669,hardwood,47.48 +7671,hardwood,46.255 +7672,hardwood,46.255 +7673,hardwood,47.48 +7674,hardwood,46.68571429 +7677,hardwood,47.48 +7679,hardwood,46.605 +7680,hardwood,47.55 +7682,hardwood,47.48 +7684,hardwood,47.48 +7688,hardwood,47.48 +7689,hardwood,47.48 +7694,hardwood,47.48 +7695,hardwood,47.48 +7697,hardwood,47.48 +7698,hardwood,47.48 +7699,hardwood,47.48 +7700,hardwood,46.675 +7702,hardwood,47.48 +7704,hardwood,47.935 +7705,hardwood,47.935 +7706,hardwood,47.935 +7709,hardwood,46.9648 +7710,hardwood,48.11 +7712,hardwood,47.69 +7713,hardwood,47.69 +7715,hardwood,47.3512 +7716,hardwood,47.83 +7717,hardwood,47.48 +7719,hardwood,47.708865 +7720,hardwood,47.708865 +7721,hardwood,47.708865 +7722,hardwood,47.708865 +7723,hardwood,47.708865 +7724,hardwood,47.708865 +7725,hardwood,47.708865 +7726,hardwood,47.708865 +7727,hardwood,47.708865 +7728,hardwood,47.708865 +7729,hardwood,47.708865 +7730,hardwood,47.708865 +7731,hardwood,47.708865 +7732,hardwood,47.708865 +7733,hardwood,47.708865 +7734,hardwood,47.708865 +7735,hardwood,47.708865 +7736,hardwood,47.708865 +7737,hardwood,47.708865 +7738,hardwood,47.708865 +7739,hardwood,47.708865 +7740,hardwood,47.708865 +7741,hardwood,47.708865 +7742,hardwood,47.708865 +7743,hardwood,47.708865 +7744,hardwood,47.708865 +7745,hardwood,47.708865 +7746,hardwood,47.708865 +7747,hardwood,47.708865 +7748,hardwood,47.708865 +7749,hardwood,47.708865 +7750,hardwood,47.9 +7751,hardwood,47.708865 +7752,hardwood,47.708865 +7753,hardwood,47.708865 +7754,hardwood,47.708865 +7755,hardwood,47.708865 +7756,hardwood,47.708865 +7757,hardwood,47.708865 +7758,hardwood,47.708865 +7759,hardwood,47.2525 +7763,hardwood,47.48 +7764,hardwood,47.48 +7766,hardwood,48.18 +7767,hardwood,48.18 +7768,hardwood,47.48 +7769,hardwood,48.18 +7770,hardwood,48.18 +7771,hardwood,47.76 +7774,hardwood,47.32614 +7776,hardwood,47.865 +7777,hardwood,47.865 +7778,hardwood,47.865 +7779,softwood,50.1 +7781,hardwood,46.64 +7782,hardwood,46.885 +7783,hardwood,46.885 +7792,hardwood,46.78 +7793,hardwood,46.78 +7794,hardwood,46.78 +7795,hardwood,46.78 +7798,hardwood,47.62 +7799,hardwood,47.62 +7800,hardwood,47.62 +7801,hardwood,42.786 +7803,hardwood,47.48 +7804,hardwood,47.48 +7805,hardwood,47.9 +7806,hardwood,47.48 +7807,hardwood,47.48 +7808,hardwood,47.48 +7810,hardwood,51.15 +7812,hardwood,47.48 +7813,hardwood,47.48 +7814,hardwood,47.48 +7815,hardwood,47.48 +7816,hardwood,47.48 +7817,hardwood,47.48 +7818,hardwood,47.48 +7819,hardwood,47.48 +7821,hardwood,47.48 +7822,hardwood,47.48 +7823,hardwood,47.48 +7824,hardwood,46.99 +7828,hardwood,47.48 +7829,hardwood,47.48 +7831,hardwood,46.75676 +7833,hardwood,47.48 +7835,hardwood,46.43567 +7839,hardwood,47.48 +7841,hardwood,47.025 +7842,hardwood,47.025 +7845,hardwood,47.48 +7846,hardwood,47.41 +7847,hardwood,47.48 +7848,hardwood,47.41 +7849,hardwood,47.48 +7850,hardwood,47.585 +7851,hardwood,47.585 +7852,hardwood,47.585 +7853,hardwood,47.585 +7855,hardwood,47.48 +7857,hardwood,47.48 +7861,hardwood,47.48 +7862,hardwood,47.48 +7863,hardwood,47.48 +7865,hardwood,47.865 +7867,hardwood,44.2 +7868,hardwood,48.25 +7869,hardwood,47.48 +7870,hardwood,47.074 +7872,hardwood,47.55 +7873,hardwood,47.55 +7874,hardwood,47.55 +7875,hardwood,47.55 +7876,hardwood,47.55 +7877,hardwood,47.55 +7878,hardwood,47.55 +7879,hardwood,47.55 +7880,hardwood,47.025 +7881,hardwood,47.025 +7882,hardwood,47.025 +7883,hardwood,46.2172 +7884,hardwood,46.2172 +7886,hardwood,47.48 +7887,hardwood,47.48 +7888,hardwood,47.48 +7889,hardwood,47.48 +7890,hardwood,47.48 +7891,hardwood,47.48 +7892,hardwood,46.437175 +7893,hardwood,47.48 +7894,hardwood,47.48 +7895,hardwood,47.48 +7899,hardwood,46.865085 +7900,hardwood,47.41 +7902,hardwood,47.76 +7903,hardwood,47.76 +7904,hardwood,47.76 +7905,hardwood,47.48 +7906,hardwood,47.76 +7907,hardwood,47.48 +7910,hardwood,47.445 +7911,hardwood,47.48 +7912,hardwood,47.48 +7913,hardwood,46.675 +7914,hardwood,46.675 +7915,hardwood,46.675 +7916,hardwood,46.675 +7918,hardwood,46.675 +7919,hardwood,46.675 +7920,hardwood,46.675 +7921,hardwood,47.445 +7922,hardwood,47.445 +7923,hardwood,46.675 +7924,hardwood,46.675 +7925,hardwood,46.675 +7926,hardwood,47.445 +7927,hardwood,47.445 +7928,hardwood,46.675 +7929,hardwood,46.535 +7932,hardwood,47.48 +7933,hardwood,47.48 +7934,hardwood,47.48 +7935,hardwood,47.48 +7936,hardwood,47.48 +7939,hardwood,47.48 +7940,hardwood,47.48 +7942,hardwood,47.445 +7944,hardwood,47.48 +7946,hardwood,47.48 +7948,hardwood,46.955 +7952,hardwood,41.984 +7953,hardwood,47.48 +7954,hardwood,48.11 +7956,hardwood,47.48 +7958,hardwood,46.895325 +7960,hardwood,46.97999 +7961,hardwood,46.97999 +7962,hardwood,48.11 +7964,hardwood,47.76 +7965,hardwood,47.76 +7966,hardwood,47.76 +7967,hardwood,47.76 +7968,hardwood,47.76 +7969,hardwood,47.76 +7970,hardwood,47.76 +7971,hardwood,47.62 +7972,hardwood,47.62 +7974,hardwood,47.62 +7976,hardwood,47.48 +7977,hardwood,46.693305 +7978,hardwood,46.693305 +7980,hardwood,48.35333333 +7982,hardwood,47.69 +7983,hardwood,47.69 +7984,hardwood,47.69 +7985,hardwood,47.69 +7986,hardwood,47.69 +7987,hardwood,47.69 +7990,hardwood,47.48 +7991,hardwood,47.48 +7994,hardwood,47.48 +7996,hardwood,47.48 +7997,hardwood,47.48 +7999,hardwood,47.48 +8000,hardwood,47.55 +8001,hardwood,47.48 +8003,hardwood,47.48 +8004,hardwood,47.48 +8007,hardwood,46.675 +8008,hardwood,48.33568 +8009,hardwood,48.33568 +8010,hardwood,48.33568 +8011,hardwood,47.76 +8013,hardwood,47.41 +8014,hardwood,47.41 +8015,hardwood,47.41 +8018,hardwood,47.41 +8019,hardwood,47.795 +8020,hardwood,47.48 +8022,hardwood,48.005 +8023,hardwood,48.005 +8024,hardwood,48.005 +8027,hardwood,47.48 +8029,hardwood,47.48 +8030,hardwood,47.48 +8032,hardwood,47.48 +8033,hardwood,47.48 +8034,hardwood,47.48 +8036,hardwood,41.8 +8037,hardwood,47.48 +8044,hardwood,47.79325 +8045,hardwood,47.48 +8047,hardwood,47.48 +8049,hardwood,47.48 +8051,hardwood,46.2 +8053,hardwood,48.1415 +8054,hardwood,48.14185 +8055,hardwood,48.14185 +8056,hardwood,48.14185 +8057,hardwood,48.14185 +8058,hardwood,48.14185 +8059,hardwood,48.14185 +8060,hardwood,48.14185 +8061,hardwood,48.14185 +8062,hardwood,48.14185 +8063,hardwood,48.14185 +8064,hardwood,48.14185 +8065,hardwood,48.14185 +8066,hardwood,48.14185 +8067,hardwood,48.14185 +8068,hardwood,48.14185 +8069,hardwood,48.14185 +8070,hardwood,48.14185 +8071,hardwood,48.14185 +8072,hardwood,48.14185 +8073,hardwood,48.14185 +8074,hardwood,47.375 +8075,hardwood,48.14185 +8076,hardwood,48.14185 +8077,hardwood,48.14185 +8078,hardwood,48.14185 +8079,hardwood,48.14185 +8080,hardwood,48.14185 +8081,hardwood,48.14185 +8082,hardwood,48.14185 +8083,hardwood,48.14185 +8084,hardwood,48.14185 +8085,hardwood,48.14185 +8086,hardwood,48.14185 +8087,hardwood,48.14185 +8088,hardwood,47.48 +8090,hardwood,48.14185 +8091,hardwood,47.655 +8092,hardwood,48.14185 +8099,hardwood,47.48 +8103,hardwood,47.025 +8104,hardwood,46.885 +8105,hardwood,46.885 +8106,hardwood,47.48 +8107,hardwood,47.865 +8108,hardwood,47.865 +8110,hardwood,47.48 +8111,hardwood,36.5 +8112,hardwood,47.193 +8113,hardwood,47.48 +8114,hardwood,47.48 +8115,hardwood,47.3365 +8121,hardwood,47.48 +8123,hardwood,48.6 +8125,hardwood,47.48 +8127,hardwood,47.48 +8129,hardwood,46.859065 +8131,hardwood,47.865 +8134,hardwood,47.48 +8138,hardwood,47.48 +8141,hardwood,47.48 +8143,hardwood,47.48 +8144,hardwood,47.48 +8146,hardwood,47.48 +8150,hardwood,47.62 +8151,hardwood,47.62 +8152,hardwood,47.62 +8153,hardwood,47.62 +8154,hardwood,47.48 +8155,hardwood,46.8465 +8156,hardwood,46.8465 +8157,hardwood,47.48 +8159,hardwood,47.27 +8160,hardwood,47.48 +8162,hardwood,47.48 +8164,hardwood,47.48 +8167,hardwood,47.48 +8169,hardwood,47.48 +8171,hardwood,47.48 +8173,hardwood,47.48 +8175,hardwood,47.48 +8177,hardwood,47.48 +8178,hardwood,47.48 +8180,hardwood,46.15 +8181,hardwood,47.62 +8182,softwood,47.48 +8183,softwood,51.11 +8184,softwood,47.865 +8185,softwood,47.865 +8186,softwood,47.865 +8187,softwood,48.2 +8188,softwood,47.865 +8189,softwood,47.125 +8190,hardwood,47.48 +8191,hardwood,47.48 +8192,hardwood,47.48 +8193,hardwood,47.48 +8194,hardwood,47.48 +8195,hardwood,47.48 +8196,hardwood,47.48 +8197,softwood,47.798125 +8198,softwood,47.225 +8199,hardwood,47.48 +8200,softwood,47.798125 +8201,softwood,47.795 +8202,softwood,47.795 +8203,softwood,52 +8205,hardwood,48.25 +8206,hardwood,48.25 +8207,hardwood,48.145 +8208,hardwood,47.48 +8210,hardwood,47.235 +8211,hardwood,47.48 +8212,hardwood,48.245275 +8213,hardwood,48.245275 +8214,hardwood,48.245275 +8215,hardwood,48.245275 +8216,hardwood,47.48 +8217,hardwood,48.46 +8218,hardwood,48.245275 +8219,hardwood,41.849 +8220,hardwood,45.4 +8222,hardwood,47.305 +8223,hardwood,47.48 +8224,hardwood,47.305 +8225,hardwood,47.305 +8226,hardwood,47.305 +8227,hardwood,47.305 +8228,hardwood,47.305 +8229,hardwood,47.305 +8230,hardwood,47.305 +8231,hardwood,47.305 +8232,hardwood,47.305 +8233,hardwood,47.305 +8234,hardwood,47.305 +8235,hardwood,47.305 +8236,hardwood,47.4226 +8238,hardwood,47.305 +8239,hardwood,47.305 +8240,hardwood,46.693305 +8241,hardwood,47.08506 +8242,hardwood,46.895325 +8243,hardwood,46.895325 +8244,hardwood,46.895325 +8246,hardwood,46.895325 +8247,hardwood,46.895325 +8248,hardwood,46.895325 +8249,softwood,47.865 +8250,hardwood,47.13 +8251,hardwood,47.13 +8252,hardwood,47.13 +8253,hardwood,47.69 +8254,hardwood,47.69 +8255,hardwood,47.48 +8256,hardwood,47.69 +8257,hardwood,47.55 +8258,hardwood,47.55 +8259,hardwood,47.55 +8260,hardwood,47.55 +8261,hardwood,47.55 +8262,hardwood,47.55 +8263,hardwood,47.55 +8266,hardwood,47.48 +8268,hardwood,47.48 +8269,hardwood,47.48 +8271,hardwood,47.48 +8272,hardwood,47.55 +8273,softwood,47.48 +8274,softwood,47.48 +8275,hardwood,47.48 +8276,hardwood,47.48 +8279,hardwood,47.48 +8280,hardwood,47.48 +8283,hardwood,48.0946 +8284,hardwood,47.48 +8285,hardwood,48.0946 +8286,hardwood,48.18 +8287,hardwood,48.0946 +8288,hardwood,48.0946 +8289,hardwood,48.0946 +8290,hardwood,47.220685 +8292,hardwood,47.62 +8293,hardwood,47.62 +8294,hardwood,47.62 +8295,hardwood,47.274935 +8296,hardwood,47.445 +8297,hardwood,46.325 +8298,hardwood,46.7205 +8299,hardwood,46.6 +8300,hardwood,47.48 +8301,hardwood,47.48 +8302,hardwood,47.48 +8303,hardwood,47.13028 +8304,hardwood,46.7205 +8305,hardwood,47.48 +8306,hardwood,46.7205 +8307,hardwood,47.305 +8308,hardwood,47.305 +8309,hardwood,47.375 +8310,hardwood,47.305 +8311,hardwood,47.48 +8313,hardwood,48.07208333 +8314,hardwood,48.07208333 +8315,hardwood,47.62 +8316,hardwood,47.62 +8317,hardwood,47.62 +8318,hardwood,47.62 +8319,hardwood,47.62 +8320,hardwood,47.62 +8321,hardwood,47.62 +8322,hardwood,47.62 +8323,hardwood,47.62 +8324,hardwood,47.62 +8325,hardwood,47.62 +8326,hardwood,47.62 +8327,hardwood,47.62 +8328,hardwood,47.62 +8329,hardwood,47.62 +8330,hardwood,47.62 +8331,hardwood,47.62 +8336,hardwood,47.62 +8337,hardwood,47.62 +8338,hardwood,47.62 +8339,hardwood,47.62 +8340,hardwood,47.48 +8341,hardwood,48.11 +8342,hardwood,47.48 +8343,hardwood,46.5 +8344,hardwood,47.48 +8345,hardwood,47.655 +8346,hardwood,47.48 +8347,hardwood,47.48 +8348,hardwood,47.61543478 +8349,hardwood,47.48 +8350,hardwood,60.9143382 +8351,hardwood,47.61543478 +8352,hardwood,47.48 +8353,hardwood,47.48 +8354,hardwood,47.48 +8355,hardwood,46.885 +8356,hardwood,43.8 +8357,hardwood,47.61543478 +8358,hardwood,47.48 +8359,hardwood,47.48 +8360,hardwood,47.655 +8361,hardwood,47.48 +8362,hardwood,47.48 +8363,hardwood,47.48 +8364,hardwood,47.48 +8365,hardwood,47.41 +8366,hardwood,47.48 +8367,hardwood,47.48 +8368,hardwood,47.48 +8369,hardwood,47.48 +8370,hardwood,47.48 +8373,hardwood,47.41 +8377,hardwood,47.48 +8382,hardwood,47.48 +8386,hardwood,47.48 +8387,hardwood,47.48 +8388,hardwood,47.48 +8389,hardwood,47.48 +8390,hardwood,47.48 +8391,hardwood,47.48 +8392,hardwood,47.48 +8393,hardwood,47.48 +8394,hardwood,47.48 +8395,hardwood,49.17 +8397,hardwood,47.48 +8398,hardwood,47.48 +8399,hardwood,47.48 +8400,hardwood,47.48 +8401,hardwood,47.48 +8402,hardwood,46.256365 +8403,hardwood,46.5 +8404,hardwood,47.445 +8405,hardwood,47.445 +8406,hardwood,47.445 +8407,hardwood,47.48 +8408,hardwood,47.48 +8409,hardwood,47.48 +8410,hardwood,47.48 +8411,hardwood,46.5 +8412,hardwood,47.62 +8413,hardwood,47.48 +8415,hardwood,47.62 +8416,hardwood,47.62 +8418,hardwood,47.62 +8419,hardwood,47.48 +8420,hardwood,47.34 +8421,hardwood,47.34 +8422,hardwood,47.48 +8423,hardwood,47.34 +8424,hardwood,46.605 +8425,hardwood,47.48 +8426,hardwood,47.34 +8427,hardwood,47.21657895 +8429,hardwood,47.21657895 +8430,hardwood,47.0495 +8431,hardwood,47.678975 +8432,hardwood,47.678975 +8433,hardwood,47.48 +8434,hardwood,47.678975 +8435,hardwood,47.678975 +8436,hardwood,47.48 +8437,hardwood,47.21657895 +8438,hardwood,47.2 +8439,hardwood,47.48 +8440,hardwood,47.865 +8441,hardwood,47.21657895 +8442,hardwood,47.865 +8443,hardwood,47.865 +8444,hardwood,47.48 +8445,hardwood,47.48 +8447,hardwood,47.48 +8449,hardwood,47.99 +8450,hardwood,47.21657895 +8453,hardwood,47.21657895 +8455,hardwood,47.21657895 +8456,hardwood,47.06 +8457,hardwood,47.21657895 +8458,hardwood,46.376905 +8459,hardwood,47.21657895 +8460,hardwood,46.325 +8461,hardwood,47.21657895 +8462,hardwood,46.6029 +8463,hardwood,46.325 +8464,hardwood,46.36 +8465,hardwood,46.572765 +8466,hardwood,46.572765 +8467,hardwood,47.66669 +8468,hardwood,47.66669 +8469,hardwood,48.005 +8470,hardwood,45.6 +8471,hardwood,46.5 +8472,hardwood,47.48 +8473,hardwood,47.61244 +8474,hardwood,46.99 +8475,hardwood,47.783345 +8476,hardwood,47.48 +8477,hardwood,47.783345 +8478,hardwood,47.48 +8479,hardwood,47.795 +8480,hardwood,48.18 +8481,hardwood,47.48 +8483,hardwood,47.48 +8484,hardwood,47.48 +8485,hardwood,47.48 +8486,hardwood,46.99 +8487,hardwood,47.21657895 +8489,hardwood,47.48 +8490,hardwood,47.48 +8491,hardwood,47.62 +8492,hardwood,47.21657895 +8494,hardwood,47.48 +8495,hardwood,47.48 +8499,hardwood,47.48 +8501,hardwood,47.48 +8502,hardwood,48.04 +8503,hardwood,47.97 +8504,hardwood,42.4175 +8505,hardwood,47.48 +8506,hardwood,46.941 +8507,hardwood,47.96 +8508,hardwood,47.96 +8509,hardwood,47.48 +8510,hardwood,47.7985 +8511,hardwood,47.235 +8512,hardwood,42.4 +8513,hardwood,47.235 +8514,hardwood,47.21 +8515,hardwood,47.935 +8516,hardwood,46.6421 +8517,hardwood,46.6421 +8518,hardwood,47.48 +8519,hardwood,47.96 +8521,hardwood,46.6421 +8522,hardwood,46.6421 +8523,hardwood,47.96 +8524,hardwood,47.96 +8525,hardwood,46.6421 +8526,hardwood,46.6421 +8527,hardwood,47.63925 +8528,hardwood,46.85 +8529,hardwood,47.48 +8531,hardwood,46.85 +8532,hardwood,46.85 +8533,hardwood,48.675 +8534,hardwood,47.9 +8535,hardwood,47.48 +8536,hardwood,47.48 +8544,hardwood,47.025 +8546,hardwood,47.48 +8548,hardwood,47.55 +8549,hardwood,47.55 +8550,hardwood,47.55 +8551,hardwood,47.55 +8552,hardwood,47.55 +8553,hardwood,47.837 +8554,hardwood,47.48 +8555,hardwood,46.85 +8556,hardwood,47.48 +8557,hardwood,47.48 +8558,hardwood,47.48 +8559,hardwood,47.85352 +8560,hardwood,47.55 +8561,hardwood,42.4 +8563,hardwood,47.48 +8565,hardwood,46.3275 +8567,hardwood,47.48 +8571,hardwood,47.48 +8572,hardwood,47.48 +8573,hardwood,47.48 +8577,hardwood,46.885 +8583,hardwood,46.99 +8585,hardwood,47.48 +8586,hardwood,48.18 +8588,hardwood,47.48 +8589,hardwood,47.48 +8590,hardwood,47.305 +8591,hardwood,47.48 +8592,hardwood,47.305 +8594,hardwood,47.48 +8595,hardwood,47.305 +8596,hardwood,47.964 +8597,hardwood,47.48 +8598,hardwood,47.305 +8599,hardwood,47.48 +8600,hardwood,47.48 +8601,hardwood,48.18 +8603,hardwood,48.18 +8605,hardwood,47.48 +8606,hardwood,47.795 +8607,hardwood,47.795 +8608,hardwood,47.55 +8609,hardwood,47.60943 +8610,hardwood,47.60943 +8611,hardwood,47.48 +8612,hardwood,46.92 +8613,hardwood,47.48 +8614,hardwood,47.48 +8615,hardwood,47.40691563 +8616,hardwood,47.40691563 +8617,hardwood,47.48 +8618,hardwood,46.88258111 +8619,hardwood,47.48 +8620,hardwood,47.48 +8621,hardwood,47.48 +8622,hardwood,47.48 +8623,hardwood,47.48 +8624,hardwood,47.48 +8626,hardwood,47.48 +8627,hardwood,47.48 +8628,hardwood,48.075 +8629,hardwood,47.48 +8631,hardwood,48.075 +8632,hardwood,47.48 +8633,hardwood,47.48 +8634,hardwood,47.48 +8635,hardwood,48.075 +8636,hardwood,47.48 +8639,hardwood,48.47 +8641,hardwood,47.18453 +8642,hardwood,47.18453 +8643,hardwood,47.18453 +8644,hardwood,47.48 +8645,hardwood,48.486355 +8646,hardwood,47.375 +8647,hardwood,46.82168333 +8648,hardwood,47.2 +8649,hardwood,47.48 +8650,hardwood,47.2 +8651,hardwood,42.72857143 +8652,hardwood,47.48 +8653,hardwood,48.075 +8654,hardwood,47.48 +8655,hardwood,47.515 +8656,hardwood,48.495 +8657,hardwood,46.5 +8658,hardwood,47.48 +8659,hardwood,47.48 +8660,hardwood,47.48 +8664,hardwood,47 +8665,hardwood,47.795 +8666,hardwood,47.48 +8667,hardwood,47.795 +8668,hardwood,46.5 +8669,hardwood,46.81003 +8670,hardwood,46.81003 +8671,hardwood,46.81003 +8672,hardwood,46.5 +8673,hardwood,47.48 +8674,hardwood,47.48 +8675,hardwood,46.5 +8676,hardwood,47.48 +8678,hardwood,47.48 +8679,hardwood,46.3474326 +8683,hardwood,47.48 +8684,hardwood,47.48 +8685,hardwood,47.48 +8686,hardwood,47.48 +8687,hardwood,47.48 +8689,hardwood,46.63916 +8690,hardwood,46.679865 +8691,hardwood,47.41 +8693,hardwood,45.7 +8694,hardwood,47.76 +8695,hardwood,47.08506 +8696,hardwood,47.08506 +8697,hardwood,47.08506 +8698,hardwood,48.02 +8699,hardwood,47.08506 +8700,hardwood,46.497445 +8701,hardwood,50.50034647 +8702,hardwood,47.48 +8703,hardwood,47.08506 +8704,hardwood,47.08506 +8705,hardwood,47.08506 +8706,hardwood,47.08506 +8707,hardwood,47.48 +8708,hardwood,47.08506 +8709,hardwood,45.4 +8710,hardwood,47.48 +8712,hardwood,47.48 +8713,hardwood,47.48 +8714,hardwood,47.27 +8715,hardwood,47.48 +8716,hardwood,43.66666667 +8717,hardwood,47.48 +8718,hardwood,46.465 +8719,hardwood,47.48 +8720,hardwood,47.48 +8722,hardwood,47.48 +8723,hardwood,47.48 +8727,hardwood,47.48 +8728,hardwood,47.48 +8729,hardwood,47.48 +8737,hardwood,46.991645 +8738,softwood,47.2 +8739,softwood,47.2 +8741,hardwood,46.745 +8743,hardwood,47.48 +8744,hardwood,47.48 +8745,hardwood,47.24249 +8748,hardwood,47.48 +8749,hardwood,47.54041 +8750,hardwood,47.48 +8751,hardwood,47.54041 +8752,hardwood,47.54041 +8753,hardwood,47.48 +8754,hardwood,47.48 +8755,hardwood,47.91379 +8756,hardwood,47.48 +8757,hardwood,47.48 +8758,hardwood,47.642575 +8759,hardwood,47.54041 +8761,hardwood,47.48 +8762,hardwood,47.48 +8763,hardwood,47.48 +8764,hardwood,47.48 +8766,hardwood,47.48 +8767,hardwood,47.48 +8768,hardwood,47.48 +8770,hardwood,47.865 +8771,hardwood,47.865 +8772,hardwood,47.865 +8773,hardwood,47.865 +8774,hardwood,47.865 +8775,hardwood,47.865 +8776,hardwood,47.865 +8777,hardwood,47.865 +8778,hardwood,47.48 +8779,hardwood,46.92 +8780,hardwood,47.48 +8781,hardwood,47.48 +8783,hardwood,47.48 +8784,hardwood,46.6 +8786,hardwood,47.48 +8787,hardwood,47.48 +8788,hardwood,47.165 +8789,hardwood,47.48 +8793,hardwood,47.48 +8794,hardwood,47.48 +8799,hardwood,47.3015 +8800,hardwood,47.37136 +8801,hardwood,47.37136 +8803,hardwood,47.48 +8804,hardwood,47.165 +8805,hardwood,47.165 +8806,hardwood,47.37136 +8807,hardwood,47.37136 +8808,hardwood,47.37136 +8809,hardwood,47.37136 +8810,hardwood,47.37136 +8811,hardwood,47.48 +8812,hardwood,47.48 +8813,hardwood,38.75 +8814,hardwood,48.18 +8815,hardwood,47.48 +8816,hardwood,47.48 +8821,hardwood,45.6 +8822,hardwood,48.11 +8823,hardwood,48.11 +8824,hardwood,47.655 +8825,hardwood,47.48 +8826,hardwood,47.655 +8827,hardwood,48.160645 +8828,hardwood,47.48 +8829,hardwood,45.845 +8830,hardwood,46.5 +8831,hardwood,48.11 +8832,hardwood,48.160645 +8833,hardwood,47.48 +8834,hardwood,50.46 +8836,hardwood,47.48 +8837,hardwood,48.124735 +8838,hardwood,48.18 +8839,hardwood,48.18 +8842,hardwood,47.48 +8843,hardwood,47.48 +8844,hardwood,47.48 +8846,hardwood,47.280955 +8848,hardwood,47.48 +8849,hardwood,46.6365 +8850,hardwood,47.48 +8851,hardwood,47.41 +8852,hardwood,47.41 +8853,hardwood,47.48 +8854,hardwood,47.48 +8855,hardwood,47.48 +8856,hardwood,48.67 +8857,hardwood,48.67 +8858,hardwood,48.67 +8859,hardwood,45.6 +8860,hardwood,47.445 +8861,hardwood,47.48 +8862,hardwood,47.41 +8863,hardwood,47.375 +8866,hardwood,47.62 +8868,hardwood,47.795 +8869,hardwood,47.61244 +8870,hardwood,47.61244 +8871,hardwood,47.48 +8872,hardwood,47.02479 +8873,hardwood,47.48 +8874,hardwood,47.48 +8875,hardwood,47.48 +8876,hardwood,47.48 +8877,hardwood,47.4625 +8878,hardwood,47.48 +8879,hardwood,47.445 +8880,hardwood,47.4625 +8881,hardwood,47.48 +8883,hardwood,47.48 +8884,hardwood,47.025 +8885,hardwood,47.48 +8886,hardwood,47.235 +8887,hardwood,47.48 +8889,hardwood,47.48 +8890,hardwood,47.48 +8891,hardwood,47.48 +8892,hardwood,47.48 +8895,hardwood,47.48 +8896,hardwood,47.48 +8897,hardwood,47.48 +8898,hardwood,47.48 +8899,hardwood,47.48 +8900,hardwood,47.48 +8901,hardwood,47.48 +8902,hardwood,47.33805556 +8903,hardwood,47.461765 +8904,hardwood,48.83 +8905,hardwood,47.352075 +8906,hardwood,47.48 +8907,hardwood,47.2245 +8908,hardwood,47.2245 +8909,hardwood,47.2245 +8910,hardwood,47.48 +8911,hardwood,47.2245 +8912,hardwood,47.48 +8913,hardwood,47.48 +8914,hardwood,45.7 +8915,hardwood,47.2245 +8916,hardwood,47.48 +8917,hardwood,47.48 +8918,hardwood,47.48 +8919,hardwood,47.48 +8923,hardwood,47.48 +8924,hardwood,47.48 +8925,hardwood,47.2 +8928,hardwood,47.48 +8929,hardwood,47.2 +8930,hardwood,47.2 +8931,hardwood,47.48 +8932,hardwood,47.48 +8933,hardwood,47.2 +8934,hardwood,47.48 +8935,hardwood,47.48 +8936,hardwood,47.2 +8937,hardwood,47.48 +8938,hardwood,47.48 +8939,hardwood,47.48 +8940,hardwood,47.48 +8941,hardwood,47.48 +8942,hardwood,47.3725 +8943,hardwood,47.48 +8944,hardwood,43.8 +8947,hardwood,47.48 diff --git a/src/pyfia/carbon/nsvb/data/dead_cr_prop.csv b/src/pyfia/carbon/nsvb/data/dead_cr_prop.csv new file mode 100644 index 00000000..7158dd98 --- /dev/null +++ b/src/pyfia/carbon/nsvb/data/dead_cr_prop.csv @@ -0,0 +1,87 @@ +ECOPROV,hw_sw,CR_MEAN +132,softwood,43.9 +132,hardwood,41.6 +133,softwood,49.6 +133,hardwood,41.5 +211,softwood,42.6 +211,hardwood,39.2 +212,softwood,48.0 +212,hardwood,38.6 +221,softwood,42.2 +221,hardwood,38.5 +222,softwood,44.5 +222,hardwood,38.8 +223,softwood,42.8 +223,hardwood,38.5 +231,softwood,34.8 +231,hardwood,37.4 +232,softwood,33.1 +232,hardwood,35.8 +234,softwood,40.6 +234,hardwood,38.9 +242,softwood,48.2 +242,hardwood,35.6 +251,softwood,62.1 +251,hardwood,38.5 +255,softwood,43.1 +255,hardwood,36.6 +261,softwood,38.9 +261,hardwood,40.2 +262,softwood,28.8 +262,hardwood,40.0 +263,softwood,40.3 +263,hardwood,38.1 +313,softwood,65.7 +313,hardwood,46.9 +315,softwood,74.2 +315,hardwood,43.6 +321,softwood,63.1 +321,hardwood,63.8 +322,softwood,64.8 +322,hardwood,56.5 +331,softwood,56.3 +331,hardwood,41.2 +332,softwood,56.4 +332,hardwood,38.9 +341,softwood,68.5 +341,hardwood,39.9 +342,softwood,65.6 +342,hardwood,40.2 +411,softwood,29.5 +411,hardwood,28.1 +M132,softwood,45.9 +M132,hardwood,42.4 +M133,softwood,55.8 +M133,hardwood,44.9 +M134,softwood,58.7 +M134,hardwood,52.4 +M211,softwood,41.2 +M211,hardwood,39.0 +M221,softwood,42.2 +M221,hardwood,37.6 +M223,softwood,44.3 +M223,hardwood,40.3 +M231,softwood,36.4 +M231,hardwood,41.4 +M241,softwood,42.7 +M241,hardwood,44.0 +M242,softwood,50.4 +M242,hardwood,37.8 +M261,softwood,48.6 +M261,hardwood,40.7 +M262,softwood,61.7 +M262,hardwood,41.1 +M313,softwood,56.7 +M313,hardwood,53.3 +M331,softwood,56.1 +M331,hardwood,37.2 +M332,softwood,53.6 +M332,hardwood,43.5 +M333,softwood,48.8 +M333,hardwood,38.9 +M334,softwood,49.8 +M334,hardwood,47.9 +M341,softwood,66.6 +M341,hardwood,40.9 +UNDEFINED,softwood,46.8 +UNDEFINED,hardwood,38.0 diff --git a/src/pyfia/carbon/nsvb/data/dead_decay_proportions.csv b/src/pyfia/carbon/nsvb/data/dead_decay_proportions.csv new file mode 100644 index 00000000..17d66cc5 --- /dev/null +++ b/src/pyfia/carbon/nsvb/data/dead_decay_proportions.csv @@ -0,0 +1,11 @@ +hw_sw,DECAYCD,DENSITY_PROP,BARK_LOSS_PROP,BRANCH_LOSS_PROP +hardwood,1,0.99,1.0,1.0 +hardwood,2,0.80,0.8,0.5 +hardwood,3,0.54,0.5,0.1 +hardwood,4,0.43,0.2,0.0 +hardwood,5,0.43,0.0,0.0 +softwood,1,0.97,1.0,1.0 +softwood,2,1.00,0.8,0.5 +softwood,3,0.92,0.5,0.1 +softwood,4,0.55,0.2,0.0 +softwood,5,0.55,0.0,0.0 diff --git a/src/pyfia/carbon/nsvb/data/total_biomass_jenkins.csv b/src/pyfia/carbon/nsvb/data/total_biomass_jenkins.csv new file mode 100644 index 00000000..e8d5e04f --- /dev/null +++ b/src/pyfia/carbon/nsvb/data/total_biomass_jenkins.csv @@ -0,0 +1,10 @@ +JENKINS_SPGRPCD,model,a,b,c +1,5,0.523742886,1.92197857,0.78279546 +2,5,0.38826478,1.726525224,0.984599835 +3,5,0.772534536,2.184545296,0.575832011 +4,5,0.35304343,2.204853568,0.728083165 +5,5,0.947256394,1.904257586,0.723009833 +6,5,1.650798031,2.107712564,0.48072183 +7,5,0.332263991,2.060786768,0.850173608 +8,5,0.433906441,2.115626102,0.735074518 +9,5,0.303328094,2.01600356,0.903672396 diff --git a/src/pyfia/carbon/nsvb/data/total_biomass_spcd.csv b/src/pyfia/carbon/nsvb/data/total_biomass_spcd.csv new file mode 100644 index 00000000..d6e076ea --- /dev/null +++ b/src/pyfia/carbon/nsvb/data/total_biomass_spcd.csv @@ -0,0 +1,174 @@ +SPCD,DIVISION,STDORGCD,model,a,a1,b,b1,c,c1 +11,,,1,0.349568157,,2.036595585,,0.610614134, +12,210,,2,0.328925371,,2.068305047,2.173714588,0.600489623, +12,,,2,0.328136654,,2.068856431,2.178341624,0.600282409, +15,M260,,1,0.014607388,,1.512436368,,1.71211905, +15,,,1,0.073497618,,1.998948783,,1.008150026, +16,,,1,0.349568157,,2.068859342,,0.610614134, +17,,,1,0.349568157,,2.083812739,,0.610614134, +19,,,2,0.443491134,,2.038793267,2.314536598,0.520420476, +20,,,1,0.349568157,,2.070000431,,0.610614134, +68,,,1,0.301398904,,2.034237148,,0.695059531, +71,210,,1,0.664116474,,2.010379452,,0.50920278, +71,,,1,0.882434652,,2.025368218,,0.435750124, +73,,,1,0.301398904,,1.991589532,,0.695059531, +93,M330,,1,0.213931593,,1.67741863,,0.941537364, +93,,,1,0.286625447,,1.737783775,,0.837560422, +94,210,,1,0.039235479,,2.226011879,,1.144335464, +94,,,1,0.21805211,,2.489339298,,0.520617556, +95,210,,1,0.313619663,,2.122370911,,0.62071126, +95,,,1,0.313619663,,2.122370911,,0.62071126, +97,,,1,0.371615725,,2.043904648,,0.646763187, +98,,,1,0.371615725,,1.970403305,,0.646763187, +105,210,,1,0.495643869,,2.109006809,,0.508476798, +105,,,1,0.495643869,,2.109006809,,0.508476798, +108,M330,,1,0.181923396,,2.064633748,,0.783734848, +108,,,1,0.303679972,,2.060661231,,0.658443677, +110,230,,1,0.057622485,,2.148749824,,1.021064662, +110,M230,,1,0.748830041,,2.118508655,,0.435975843, +110,,,1,0.186963564,,2.19009994,,0.725056549, +111,230,0,2,0.177868614,,2.180014222,1.976884888,0.78319536, +111,,0,2,0.177868614,,2.180014222,1.976884888,0.78319536, +111,230,1,3,0.358815475,1.849604207,0.149984321,,0.841295541,0.332484731 +111,,1,3,0.358815475,1.849604207,0.149984321,,0.841295541,0.332484731 +116,,,1,0.148830682,,2.057563893,,0.810673827, +121,230,,1,0.112404906,,2.075821407,,0.926658282, +121,,,1,0.112404906,,2.075821407,,0.926658282, +122,M260,,1,0.23095964,,2.180139622,,0.636401464, +122,M310,,1,0.231449281,,1.925735742,,0.805486816, +122,M330,,1,0.38869001,,2.095942335,,0.554494316, +122,,,1,0.380820507,,2.118642907,,0.552282327, +123,,,1,0.148830682,,2.151179164,,0.810673827, +125,210,,2,0.163474811,,2.033616764,2.140153969,0.782544189, +125,,,2,0.156724247,,2.014189076,2.123600697,0.805766856, +126,,,1,0.148830682,,2.190447581,,0.810673827, +129,210,,1,0.272341852,,1.978761046,,0.706249069, +129,M220,,1,0.370462954,,2.217732339,,0.489143001, +129,,,1,0.278363855,,2.106451786,,0.623519753, +131,230,0,2,0.045928912,,1.956540143,1.955393942,1.154203033, +131,,0,2,0.0472227,,1.955146233,1.971465942,1.146284919, +131,230,1,2,0.078468409,,1.688933865,2.157576692,1.157707856, +131,,1,2,0.078468409,,1.688933865,2.157576692,1.157707856, +132,M220,,1,0.744572557,,2.21183998,,0.3744599, +132,,,1,0.784128198,,2.287530154,,0.32258062, +202,240,,1,0.135206507,,1.713527048,,1.047613377, +202,M240,,1,0.120040708,,1.71368785,,1.061101895, +202,M260,,1,0.017636841,,1.546946282,,1.607689586, +202,M330,,1,0.142806947,,1.970143963,,0.878880818, +202,,,1,0.174719172,,1.726525291,,0.984599765, +211,,,1,0.301398904,,1.904691556,,0.695059531, +221,,,1,0.301398904,,1.84425738,,0.695059531, +222,230,,1,0.110459157,,1.538421675,,1.171293113, +222,,,1,0.110459157,,1.538421675,,1.171293113, +241,210,,2,0.393287534,,1.920335843,2.030340224,0.560517316, +241,,,2,0.393287534,,1.920335843,2.030340224,0.560517316, +261,210,,2,0.428326758,,1.859288817,2.075952205,0.673695108, +261,M220,,2,0.435207622,,2.125444089,2.041079911,0.547532483, +261,,,2,0.401758567,,1.891153446,2.055171185,0.676829753, +263,,,1,0.116109718,,1.895372365,,0.955815802, +264,,,1,0.349568157,,2.074751581,,0.610614134, +313,230,,1,0.219077829,,2.34655145,,0.566149295, +313,,,1,0.219077829,,2.34655145,,0.566149295, +316,210,,4,0.181426445,,1.710544262,-0.025073986,0.953960978, +316,230,,4,0.480472826,,1.901115542,-0.026261863,0.597190558, +316,M220,,4,1.742544318,,1.672298098,-0.06539757,0.340272222, +316,,,4,0.315730276,,1.853839844,-0.024745685,0.740557379, +317,,,1,4.724200785,,2.266637565,,-0.062750913, +318,210,,1,0.197488976,,1.931805898,,0.92308401, +318,,,1,0.030114004,,1.906921766,,1.385635015, +370,,,1,0.531644989,,2.204051341,,0.517144891, +371,210,,1,0.065196083,,1.90492982,,1.186839134, +371,,,1,0.093272971,,1.90993593,,1.100520353, +375,210,,1,0.921838183,,2.20808746,,0.365204162, +375,M210,,1,0.092749057,,1.907269894,,1.052397971, +375,,,1,0.731333337,,2.201429775,,0.42310958, +391,,,1,0.592401108,,2.28787127,,0.476750767, +400,230,,1,0.154205459,,2.406949282,,0.708207968, +400,M220,,1,0.138758302,,2.116086022,,0.914738151, +400,,,1,0.14249435,,2.231464781,,0.835308377, +402,,,1,0.426589681,,2.160514868,,0.637323715, +403,,,1,0.426589681,,2.16506594,,0.637323715, +460,,,1,0.359289578,,2.063221986,,0.610643492, +461,230,,1,0.068502885,,2.112095282,,1.010901178, +461,,,1,0.068502885,,2.112095282,,1.010901178, +462,,,1,0.359289578,,2.199636684,,0.610643492, +471,,,1,0.359289578,,2.214982904,,0.610643492, +491,230,,1,0.150841077,,1.863068053,,1.025042672, +491,,,1,0.238121203,,1.981857962,,0.847835409, +521,,,1,0.359289578,,2.168847162,,0.610643492, +531,210,,1,0.114158762,,1.796155886,,1.118192132, +531,,,1,0.032731339,,2.05914548,,1.26086221, +540,230,,1,0.475068336,,1.944479885,,0.61796786, +540,,,1,0.398124318,,2.167230187,,0.573789458, +541,210,,1,0.505880958,,1.841960689,,0.716696756, +541,,,1,0.818458154,,1.873535737,,0.581382983, +543,,,1,0.359289578,,2.125186018,,0.610643492, +544,230,,1,0.30653383,,1.812211917,,0.803589437, +544,,,1,0.22558181,,1.792987614,,0.883721143, +591,,,1,0.359289578,,2.176037856,,0.610643492, +602,,,1,0.359289578,,2.179254712,,0.610643492, +611,230,,1,0.176552299,,2.219508076,,0.698773109, +611,,,1,0.176429479,,2.219442715,,0.698966292, +621,230,,4,0.25057406,,2.114664101,-0.003592877,0.657241844, +621,M220,,4,0.154317349,,2.568115687,0.011588086,0.543413156, +621,,,4,0.284661872,,2.001103314,-0.016648396,0.650792314, +653,,,1,0.359289578,,2.055452878,,0.610643492, +691,230,,1,0.194805944,,1.939361634,,0.7682613, +691,,,1,0.194805944,,1.939361634,,0.7682613, +693,,,1,0.359289578,,2.113238225,,0.610643492, +694,230,,1,0.960248321,,2.412438666,,0.177228973, +694,,,1,0.960248321,,2.412438666,,0.177228973, +711,,,1,0.359289578,,2.071689208,,0.610643492, +731,,,1,0.359289578,,2.083871933,,0.610643492, +740,230,,1,0.099825153,,2.032553584,,0.898045231, +740,,,1,0.337387708,,2.148198161,,0.56216026, +741,210,,1,0.063679544,,2.044922633,,1.029077254, +741,,,1,0.063679544,,2.044922633,,1.029077254, +742,,,1,0.183406635,,1.980557158,,0.776309008, +743,210,,1,0.446369541,,2.307513916,,0.440759181, +743,,,1,0.446369541,,2.307513916,,0.440759181, +746,210,,1,0.219583283,,2.152173348,,0.702499308, +746,M330,,1,0.148096171,,1.960083605,,0.877910539, +746,,,1,0.077568867,,2.00707876,,1.018290332, +762,210,,1,0.0923161,,1.819131811,,1.13745364, +762,,,1,0.198661401,,1.998036007,,0.847256421, +802,210,,2,0.185615748,,1.909923379,1.867881975,0.937170791, +802,220,,2,0.260621543,,1.68852516,2.401181762,0.962303387, +802,230,,2,0.165274989,,2.001535824,2.305559605,0.922473495, +802,M220,,2,0.024470323,,1.93799905,1.88681949,1.403264432, +802,,,2,0.050815817,,1.901310248,1.959335919,1.2649823, +806,M220,,1,0.127408231,,1.959122505,,1.013780167, +806,,,1,0.264408284,,2.018938385,,0.81646861, +812,,,1,0.422428568,,2.282802709,,0.535120267, +813,,,1,0.426589681,,2.054761007,,0.637323715, +819,,,1,0.426589681,,2.241349172,,0.637323715, +820,,,1,0.426589681,,2.12930132,,0.637323715, +822,230,,1,0.256737292,,2.19465205,,0.659512841, +822,,,1,0.256737292,,2.19465205,,0.659512841, +823,,,1,0.426589681,,2.023817449,,0.637323715, +824,,,1,0.426589681,,2.174921885,,0.637323715, +827,230,,1,0.198112644,,1.949045814,,0.918899075, +827,,,1,0.198112644,,1.949045814,,0.918899075, +828,230,,1,0.288336273,,2.226705257,,0.637411393, +828,,,1,0.288336273,,2.226705257,,0.637411393, +831,230,,1,0.146976739,,1.975301052,,0.953728002, +831,,,1,0.146976739,,1.975301052,,0.953728002, +832,M220,,1,0.358022711,,2.3286472,,0.545529814, +832,,,1,0.330459585,,2.332430728,,0.560678945, +833,210,,1,0.806921231,,2.086126467,,0.501127694, +833,,,1,0.642811096,,2.173042958,,0.508678825, +835,,,1,0.426589681,,2.093743422,,0.637323715, +837,,,1,0.673108646,,2.297017028,,0.403264056, +840,,,1,0.426589681,,2.170505127,,0.637323715, +842,,,1,0.426589681,,2.237831724,,0.637323715, +901,,,1,0.359289578,,2.159720641,,0.610643492, +920,,,1,0.183406635,,1.934937524,,0.776309008, +922,,,1,0.183406635,,1.928925794,,0.776309008, +950,210,,1,0.157124269,,1.974207604,,0.834615788, +950,,,1,0.150199697,,2.022000111,,0.813433571, +951,,,1,0.359289578,,2.04306793,,0.610643492, +970,230,,1,0.364527236,,1.959591428,,0.694720835, +970,,,1,0.404477403,,1.978292925,,0.661280749, +972,210,,1,0.840212676,,2.389262957,,0.240469269, +972,,,1,0.808350136,,2.356851122,,0.269505607, +999,,,1,0.359289578,,2.10136858,,0.610643492, diff --git a/src/pyfia/carbon/nsvb/data/volbk_jenkins.csv b/src/pyfia/carbon/nsvb/data/volbk_jenkins.csv new file mode 100644 index 00000000..9abb71cd --- /dev/null +++ b/src/pyfia/carbon/nsvb/data/volbk_jenkins.csv @@ -0,0 +1,10 @@ +JENKINS_SPGRPCD,model,a,b,c +1,1,0.000628047,1.695696318,1.061090008 +2,1,0.003452267,2.305064228,0.353845624 +3,1,0.008445325,2.442093833,0.020466657 +4,1,0.003742405,2.047884489,0.476931052 +5,1,0.003579986,1.870401387,0.454720079 +6,1,0.006055511,2.323948217,0.168927852 +7,1,0.001806756,2.094420478,0.523012456 +8,1,0.001879521,1.721074102,0.825002196 +9,1,0.003935233,1.728309956,0.626634668 diff --git a/src/pyfia/carbon/nsvb/data/volbk_spcd.csv b/src/pyfia/carbon/nsvb/data/volbk_spcd.csv new file mode 100644 index 00000000..afde65d3 --- /dev/null +++ b/src/pyfia/carbon/nsvb/data/volbk_spcd.csv @@ -0,0 +1,340 @@ +SPCD,DIVISION,STDORGCD,model,a,a1,b,b1,c,c1 +12,130,,3,0.000484401,1.657292042,7.89E-05,,0.796510523,-0.037889503 +12,210,,3,0.000809508,1.721122112,0.076389862,,1.002938522,0.130774755 +12,M210,,3,0.00032021,1.43433668,0.000372581,,0.99979284,-0.060089956 +12,,,3,0.001525095,1.63438562,0.214307111,,0.863301423,0.378701398 +15,M260,,4,0.004022027,,2.041693377,0.000475399,0.473846647, +15,M310,,4,0.000191947,,2.195708324,0.019478744,1.224481596, +15,M330,,4,0.001232885,,2.875038679,0.02310691,0.285665079, +15,,,4,0.002881567,,2.714926297,0.016576837,0.195054071, +16,,,1,0.001476691,,1.789389151,,0.690095344, +17,,,1,0.007186707,,1.888260674,,0.381699463, +18,,,1,0.001476691,,2.010768137,,0.690095344, +19,M330,,1,0.000712,,1.334613968,,1.218436929, +19,,,1,0.000805726,,1.44946075,,1.120513552, +20,M260,,1,0.001028662,,1.906887195,,0.865838665, +20,,,1,0.001176377,,1.848238959,,0.870784867, +42,M240,,1,0.002273122,,1.783016646,,0.607173928, +42,,,1,0.002273122,,1.783016646,,0.607173928, +43,230,,1,0.000961844,,1.962782009,,0.83033836, +43,,,1,0.000961844,,1.962782009,,0.83033836, +68,220,,1,0.001418328,,1.358242463,,0.707365618, +68,230,,1,0.009570535,,1.548287675,,0.239335951, +68,,,1,0.003173692,,1.245542704,,0.6637845, +71,130,,2,0.001524413,,1.62989658,0.793399215,0.793408748, +71,210,,2,0.006470565,,1.772149325,1.973865728,0.379076063, +71,,,2,0.002345496,,1.668725081,1.556148925,0.675960767, +73,,,1,0.002697391,,2.073979685,,0.588855007, +81,M260,,1,0.006609648,,1.701400168,,0.621195025, +81,,,1,0.006609648,,1.701400168,,0.621195025, +90,,,1,0.002064754,,1.847586886,,0.617888296, +91,210,,1,0.000887314,,1.88878877,,0.798624355, +91,,,1,0.000887314,,1.88878877,,0.798624355, +93,M330,,1,0.001369079,,1.69438534,,0.810859276, +93,M340,,1,0.002324087,,1.698860265,,0.700622413, +93,,,1,0.00166105,,1.749896216,,0.73344746, +94,130,,2,0.002549966,,1.776647563,1.875850454,0.559739502, +94,210,,2,0.000821622,,1.314466337,1.548586152,1.072086058, +94,M130,,2,0.001786126,,1.899457623,1.284780618,0.640971462, +94,,,2,0.002410153,,1.741751243,1.835963391,0.594313279, +95,130,,4,0.002729399,,1.714748538,-0.02520825,0.540685145, +95,210,,4,0.000634198,,1.961404016,0.034110373,0.892483444, +95,M130,,4,0.002525561,,1.544625604,-0.060119511,0.626736301, +95,,,4,0.002109404,,1.610152288,-0.030923511,0.645959656, +97,130,,1,0.006816407,,2.448977047,,-0.026499485, +97,210,,1,0.003103593,,2.119564592,,0.386086337, +97,M210,,1,0.002170589,,1.784474855,,0.659240898, +97,,,1,0.002066275,,1.931784135,,0.587190434, +98,M240,,1,0.001567466,,1.556704969,,0.829144213, +98,,,1,0.001567466,,1.556704969,,0.829144213, +100,130,,1,0.002656071,,1.632033796,,0.620290765, +100,,,1,0.002656071,,1.632033796,,0.620290765, +105,130,,1,0.001481211,,1.764710327,,0.698382566, +105,210,,1,0.002132735,,1.981955273,,0.534274965, +105,220,,1,0.036121769,,2.125293877,,-0.27673538, +105,,,1,0.002221084,,1.92573134,,0.527395651, +107,230,,1,0.001046495,,1.646609539,,0.897787082, +107,,,1,0.001046495,,1.646609539,,0.897787082, +108,130,,1,0.001702752,,1.837988715,,0.575379939, +108,330,,1,0.000556399,,1.585575707,,1.078951208, +108,M130,,1,0.000151875,,1.578749794,,1.40483514, +108,M330,,1,5.45E-05,,1.541032167,,1.607713777, +108,,,1,0.001369651,,1.899586903,,0.609484007, +110,220,,2,0.040726229,,2.605474295,1.807965234,-0.415019821, +110,230,,2,0.007228624,,1.984096038,1.840468279,0.40039502, +110,M220,,2,0.005022124,,1.850376248,1.857715126,0.567472063, +110,M230,,2,0.005171597,,2.366430844,2.055896143,0.254020237, +110,,,2,0.00820158,,1.964502121,1.90890667,0.372681153, +111,230,0,4,0.004307564,,2.013121859,0.01696735,0.586315836, +111,,0,4,0.004273753,,2.0110003,0.01689905,0.58920059, +111,230,1,1,0.002964213,,1.784092618,,0.764628515, +111,,1,1,0.002964213,,1.784092618,,0.764628515, +113,,,1,0.004712262,,1.781480341,,0.50751576, +115,230,,1,0.001060525,,1.855595769,,0.827897194, +115,,,1,0.001022706,,1.859030529,,0.833432597, +116,,,1,0.004712262,,2.033491645,,0.50751576, +117,M260,,1,0.013319871,,1.878440209,,0.307224456, +117,,,1,0.012063844,,1.880200966,,0.326862342, +119,,,1,0.004712262,,1.692715262,,0.50751576, +121,230,,2,0.004177015,,2.088051517,1.737769193,0.489236705, +121,,,2,0.004178544,,2.087760842,1.737894549,0.489278128, +122,310,,1,0.00170945,,1.50462576,,1.077494107, +122,330,,1,0.076333403,,1.882648894,,-0.285840186, +122,M260,,1,0.00488747,,1.718841628,,0.661641642, +122,M310,,1,0.009679711,,1.760631465,,0.493905276, +122,M330,,1,0.001877467,,1.65213619,,0.944980429, +122,M340,,1,0.00255397,,1.948948717,,0.668914063, +122,,,1,0.002455465,,1.760649438,,0.803993557, +123,M220,,1,0.002154621,,1.785235416,,0.760357133, +123,,,1,0.001724638,,1.859828766,,0.76399641, +125,130,,4,0.002420729,,1.298384474,-0.05231367,0.791694246, +125,210,,4,0.001268245,,2.027055891,0.010766132,0.689054958, +125,220,,4,0.015429814,,1.833721876,-0.037580197,0.024596498, +125,,,4,0.001964288,,1.480430411,-0.03842923,0.766622554, +126,M220,,2,0.002189694,,1.739350975,1.843115098,0.83776901, +126,,,2,0.002509253,,1.713978118,1.85847642,0.815423404, +128,230,,2,0.00405058,,1.842515644,1.82284991,0.659650272, +128,,,2,0.00405058,,1.842515644,1.82284991,0.659650272, +129,130,,2,0.000137705,,2.158777681,1.658358683,1.181182254, +129,210,,2,0.000201356,,2.216645401,1.478482243,1.100303531, +129,220,,2,0.041218558,,2.1115625,2.061878863,-0.225406805, +129,230,,2,0.002805216,,2.421971008,1.833468696,0.359830181, +129,M220,,2,0.000732365,,2.055791745,1.744309451,0.867007458, +129,,,2,0.000380933,,2.070937078,1.702419266,0.992867886, +130,,,1,0.004712262,,1.189836327,,0.50751576, +131,220,0,1,0.004110367,,2.915595521,,-0.111516009, +131,230,0,1,0.006552547,,1.935687678,,0.407960682, +131,M230,0,1,3.78E-05,,1.452281284,,1.929277263, +131,,0,1,0.006059748,,1.938552383,,0.424056533, +131,230,1,2,0.008700201,,1.876608714,2.086262249,0.359651659, +131,,1,2,0.008606621,,1.879486956,2.083305718,0.361025758, +132,220,,4,0.001446968,,1.587831446,0.00530712,0.818854033, +132,230,,4,0.009671618,,2.070212324,0.003662216,0.172936833, +132,M220,,4,0.004145321,,2.259375861,0.049513361,0.372254786, +132,,,4,0.004762816,,2.311019839,0.046793148,0.306067847, +202,130,,1,0.001452326,,1.709588148,,0.901029636, +202,240,,1,3.19E-05,,1.21260514,,1.978577264, +202,260,,1,0.002100852,,1.884646261,,0.700654103, +202,340,,1,0.000512146,,1.807078077,,1.036692597, +202,M240,,1,0.001063883,,1.997797852,,0.812133446, +202,M260,,1,0.00179144,,1.667012302,,0.906708904, +202,M310,,1,0.002088204,,2.038610285,,0.695151799, +202,M330,,1,0.009639716,,2.190731601,,0.185960074, +202,,,1,0.003452267,,2.305064228,,0.353845624, +211,260,,1,0.008107942,,1.818720791,,0.54050807, +211,,,1,0.008273056,,1.815304237,,0.539010929, +221,230,,1,0.000525286,,1.966837091,,0.861838987, +221,,,1,0.000525286,,1.966837091,,0.861838987, +222,230,,1,0.007968209,,1.638796978,,0.559610738, +222,,,1,0.007968209,,1.638796978,,0.559610738, +241,210,,1,0.000286358,,1.813407343,,1.088034046, +241,,,1,0.00176953,,1.735356776,,0.697560591, +242,340,,1,4.59E-05,,1.344799294,,1.737342802, +242,M240,,1,0.012466962,,2.31861888,,-0.076016009, +242,,,1,0.010729962,,2.340461713,,-0.06241362, +260,,,1,0.001476691,,1.898116586,,0.690095344, +261,210,,4,0.001146558,,1.97575011,0.006333785,0.7989032, +261,M220,,4,0.001693378,,1.430903834,-0.018954602,0.954160273, +261,,,4,0.001435279,,1.491504325,-0.017978057,0.951102597, +263,340,,1,0.00021709,,1.483734053,,1.341771528, +263,M240,,1,0.013886125,,2.264280071,,-0.013861296, +263,,,1,0.007650815,,2.156146417,,0.184628908, +264,,,1,0.001476691,,2.021932329,,0.690095344, +311,,,1,0.001054267,,2.048804918,,0.706389235, +313,,,1,0.011767409,,1.960394873,,0.132576193, +315,,,1,0.001054267,,2.076244456,,0.706389235, +316,210,,2,0.006430157,,2.259305257,1.723918813,0.122778631, +316,220,,2,0.064536,,2.349323967,2.239462771,-0.583135396, +316,230,,2,0.001324841,,2.088348699,1.479715571,0.624493466, +316,M220,,2,0.000568209,,1.943167982,1.680022557,0.901331324, +316,,,2,0.003743084,,2.226890355,1.685993126,0.275066356, +317,,,1,0.000263031,,1.893791179,,0.970766524, +318,210,,1,0.000553238,,1.88866006,,0.939353068, +318,220,,1,0.001842367,,2.106680855,,0.566968121, +318,M210,,1,0.000100461,,1.94258807,,1.352053658, +318,M220,,1,0.008755995,,1.935730503,,0.200128934, +318,,,1,0.000758475,,2.080827896,,0.767248478, +330,M220,,1,7.51E-05,,1.491876625,,1.683982614, +330,,,1,7.51E-05,,1.491876625,,1.683982614, +351,M240,,2,0.000295075,,1.924666692,2.104661577,1.012856434, +351,,,2,0.000293511,,1.907625331,2.083155477,1.02469019, +370,230,,1,0.009031624,,1.589886773,,0.462805981, +370,M220,,1,0.000534115,,1.799921283,,0.981964106, +370,,,1,0.002035234,,1.699439805,,0.735157037, +371,130,,1,0.001132862,,2.416332053,,0.389459657, +371,210,,1,0.000227212,,1.760966749,,1.230576056, +371,220,,1,0.002722044,,2.213610774,,0.374853937, +371,M210,,1,0.000218312,,2.220891531,,0.960071389, +371,,,1,0.000193188,,1.982769368,,1.130184078, +372,,,1,0.001054267,,2.00970169,,0.706389235, +373,,,1,0.001054267,,1.93472619,,0.706389235, +375,130,,2,0.000129759,,1.907259307,1.562218954,1.285594335, +375,210,,2,0.001960609,,1.963997295,1.822548327,0.587529205, +375,220,,2,0.043725867,,2.111552566,2.324814065,-0.302736762, +375,M210,,2,0.000674499,,2.0481022,1.723945031,0.786162111, +375,,,2,0.000703292,,1.965289855,1.779211671,0.824945635, +391,,,1,0.001744408,,1.741958469,,0.741518961, +400,220,,2,0.002800889,,1.931153532,1.609585238,0.636783249, +400,230,,2,0.002572852,,2.083050155,1.785368808,0.580612523, +400,M220,,2,0.001635907,,2.061766991,1.797197788,0.698537746, +400,M230,,2,0.06628967,,1.789774179,2.020697609,0.022171719, +400,,,2,0.002075239,,2.080086835,1.804009301,0.633915692, +402,,,1,0.002969141,,1.604118423,,0.579604019, +403,,,1,0.002553546,,1.236234878,,0.857985061, +404,230,,1,0.000650851,,1.481149919,,1.229881588, +404,,,1,0.000650851,,1.481149919,,1.229881588, +405,,,1,0.002969141,,1.565349222,,0.579604019, +407,,,1,0.002969141,,1.687370787,,0.579604019, +409,,,1,0.002969141,,1.695920915,,0.579604019, +421,220,,1,0.00629276,,1.94224316,,0.38064225, +421,,,1,0.00629276,,1.94224316,,0.38064225, +460,230,,1,0.000780565,,1.645688697,,0.992305441, +460,,,1,0.000798063,,1.652632389,,0.982665556, +461,230,,1,0.007930569,,2.402916327,,-0.115417943, +461,,,1,0.007930569,,2.402916327,,-0.115417943, +462,,,1,0.001744408,,1.483706438,,0.741518961, +471,,,1,0.001744408,,1.412900316,,0.741518961, +491,230,,1,0.002483526,,1.966987952,,0.596238007, +491,,,1,0.004605796,,2.025073487,,0.381220132, +521,230,,1,0.001023356,,2.003106477,,0.871797891, +521,,,1,0.001156925,,2.014515942,,0.834836349, +531,210,,1,0.002974714,,1.975222913,,0.368240028, +531,220,,1,0.004281864,,1.954632814,,0.292507561, +531,230,,1,0.00050323,,1.729732387,,0.959532916, +531,M210,,1,0.000643915,,1.932605455,,0.746503931, +531,M220,,1,0.004793026,,2.089350605,,0.233334908, +531,,,1,0.002809496,,2.012927857,,0.36486066, +540,230,,2,0.001296507,,1.920326593,1.814721154,0.80887389, +540,M220,,2,0.015844135,,2.018599922,1.950185796,0.177185481, +540,,,2,0.003461621,,1.976359107,1.914732462,0.544331595, +541,210,,1,0.001193857,,1.4611559,,1.069904291, +541,220,,1,0.000431925,,1.588094359,,1.216866689, +541,M210,,1,0.000770948,,1.885114887,,0.959925027, +541,M220,,1,0.000108772,,1.132376435,,1.845663412, +541,,,1,0.000498953,,1.355322827,,1.344908781, +543,,,1,0.000125104,,1.917567783,,1.271805096, +544,230,,1,0.000742742,,1.478640942,,1.160537041, +544,,,1,0.001108655,,1.746760599,,0.87214416, +552,,,1,0.001744408,,1.740699648,,0.741518961, +555,230,,1,0.437114343,,2.339903699,,-0.89296527, +555,,,1,0.437114343,,2.339903699,,-0.89296527, +591,230,,1,0.002213969,,1.909989459,,0.546929876, +591,,,1,0.002213969,,1.909989459,,0.546929876, +601,,,1,0.001744408,,1.963997731,,0.741518961, +602,M220,,1,0.001962088,,1.681245421,,0.910524732, +602,,,1,0.015725237,,1.279127327,,0.627424154, +611,230,,2,0.001753005,,2.00967935,1.632942474,0.686493001, +611,,,2,0.001853256,,1.999978352,1.62847449,0.676412418, +621,220,,2,0.063442028,,2.070430116,2.54714347,-0.318573929, +621,230,,2,0.001111153,,1.822534128,1.690117168,0.916225502, +621,M220,,2,0.001040279,,1.682806018,1.736349231,0.993950769, +621,,,2,0.001168292,,1.73262579,1.693088326,0.940870668, +651,,,1,0.000271268,,1.482267378,,1.349485593, +652,,,1,0.001744408,,1.846403558,,0.741518961, +653,230,,1,0.00304385,,1.825311391,,0.667633282, +653,,,1,0.00304385,,1.825311391,,0.667633282, +680,,,1,0.001744408,,1.800628292,,0.741518961, +691,230,,1,0.001087874,,1.510731565,,1.024431557, +691,,,1,0.001087874,,1.510731565,,1.024431557, +693,220,,1,0.231768149,,2.384102692,,-0.850771734, +693,230,,1,0.002519793,,2.020513458,,0.591277811, +693,M220,,1,0.000707745,,1.994085289,,0.957475018, +693,,,1,0.000972403,,2.109817347,,0.780426687, +694,230,,2,0.001784259,,2.003467276,1.638342467,0.72777884, +694,,,2,0.001784259,,2.003467276,1.638342467,0.72777884, +701,,,1,0.001744408,,1.644047672,,0.741518961, +711,,,1,0.001744408,,1.889227004,,0.741518961, +731,230,,1,0.003911884,,1.896395118,,0.396272117, +731,,,1,0.005700766,,1.755503979,,0.391672516, +740,130,,1,0.005118606,,2.319397498,,0.295301263, +740,M330,,1,0.002153096,,2.11324129,,0.518605695, +740,,,1,0.001516442,,2.515229222,,0.418111236, +741,130,,1,0.000999015,,2.108578245,,0.797497482, +741,210,,1,0.000282157,,1.761812261,,1.22493237, +741,,,1,0.000681989,,2.009738347,,0.927324666, +742,,,1,0.095615974,,1.855688806,,-0.145172597, +743,130,,1,0.001789411,,1.957495189,,0.6603248, +743,210,,1,0.015547636,,1.919919969,,0.127464612, +743,,,1,0.017977295,,1.889814647,,0.116481215, +746,130,,1,0.000760044,,2.156827731,,0.749462678, +746,210,,1,0.002393267,,2.100160067,,0.488837224, +746,220,,1,0.002570712,,1.784207242,,0.647615404, +746,M130,,1,0.003558967,,2.233110622,,0.322153628, +746,M330,,1,0.002592711,,2.179893265,,0.469174748, +746,,,1,0.001185123,,2.163587546,,0.636399076, +762,210,,1,0.000624601,,1.400292505,,1.167591663, +762,230,,1,0.002416124,,2.014847293,,0.525807427, +762,M220,,1,0.045505854,,1.733434843,,0.003948824, +762,,,1,0.004333606,,1.642114774,,0.580065769, +800,230,,2,0.002863318,,1.845527431,1.404391454,0.719187158, +800,,,2,0.002863318,,1.845527431,1.404391454,0.719187158, +802,210,,2,0.001092904,,2.063243696,1.516173477,0.732606854, +802,220,,2,0.048156728,,2.233896682,1.778353943,-0.308756767, +802,230,,2,0.002653685,,2.067478381,1.739577547,0.5496798, +802,M220,,2,0.002020026,,1.957775263,1.618455676,0.67740074, +802,M230,,2,0.000502457,,1.664265743,1.825828718,1.172759145, +802,,,2,0.002857648,,1.987455158,1.647347191,0.568545113, +804,,,1,0.002969141,,1.849770714,,0.579604019, +806,220,,4,0.006689643,,2.593407024,0.078642351,0.193829097, +806,230,,4,0.001080475,,2.224390555,0.035057579,0.77143692, +806,M220,,4,0.007938209,,2.035573658,0.013813296,0.339111859, +806,,,4,0.00483399,,2.181008537,0.028840573,0.411787898, +809,,,1,0.002969141,,1.882638547,,0.579604019, +812,230,,2,0.000976291,,2.478012707,1.556921469,0.579068056, +812,,,2,0.000848972,,2.526760747,1.520428135,0.590242753, +813,230,,1,0.008917632,,1.980337238,,0.246688028, +813,,,1,0.007155065,,1.913024827,,0.33231006, +820,230,,2,0.001093837,,1.898978274,1.594984613,0.807437764, +820,,,2,0.001105108,,1.895632593,1.595470802,0.806850906, +822,230,,1,0.000387584,,1.52334874,,1.283542613, +822,,,1,0.000387584,,1.52334874,,1.283542613, +823,,,1,0.002969141,,1.957831441,,0.579604019, +825,230,,1,0.000475104,,1.601640486,,1.160924995, +825,,,1,0.000335712,,1.557620307,,1.275268856, +826,,,1,0.002969141,,1.870064803,,0.579604019, +827,230,,2,0.000888848,,1.825896827,1.595342608,0.87913613, +827,,,2,0.000888848,,1.825896827,1.595342608,0.87913613, +828,230,,1,0.000879945,,1.424976767,,1.115280789, +828,,,1,0.000879945,,1.424976767,,1.115280789, +830,,,1,0.002969141,,1.645263564,,0.579604019, +831,230,,1,0.003602145,,1.511724439,,0.774736595, +831,,,1,0.00350555,,1.510582592,,0.783373932, +832,220,,4,0.041420562,,2.720947888,0.06851534,-0.310487688, +832,230,,4,0.001314588,,2.158752018,0.040648564,0.83474738, +832,M220,,4,0.00236156,,2.031531526,0.025045866,0.709154717, +832,,,4,0.00365848,,2.092495888,0.030335414,0.583862936, +833,210,,2,0.00249306,,1.91536804,1.654412432,0.59743108, +833,220,,2,0.015124699,,2.113334911,1.763690991,0.045159175, +833,230,,2,0.008234074,,2.339332163,1.840260441,0.12033872, +833,M220,,2,0.001487407,,2.137290956,1.495356687,0.65082177, +833,,,2,0.00227434,,2.079127272,1.56244791,0.564440462, +834,,,1,0.002969141,,1.789269866,,0.579604019, +835,220,,2,0.048517485,,1.508665536,1.647083567,0.093611109, +835,230,,2,0.002060805,,1.986618063,1.539291452,0.680788156, +835,,,2,0.001349057,,1.68255986,1.483832045,0.947720425, +837,220,,2,0.045671465,,1.607140974,2.05210992,0.05757605, +837,230,,2,0.002276437,,2.234035467,1.570133281,0.548435313, +837,M210,,2,0.002508938,,1.698009383,1.709361795,0.787093652, +837,M220,,2,0.002045255,,2.219573043,1.651947554,0.578287612, +837,,,2,0.00122725,,1.806258237,1.559926644,0.908781323, +838,230,,1,0.003275484,,1.384747907,,0.961214474, +838,,,1,0.003275484,,1.384747907,,0.961214474, +901,M220,,1,0.00155477,,1.60776771,,1.021717043, +901,,,1,0.001356292,,1.643684068,,1.031971546, +920,,,1,0.001934081,,2.297544334,,0.517232194, +922,,,1,0.001934081,,2.143614632,,0.517232194, +931,,,1,0.001744408,,1.709242179,,0.741518961, +950,210,,1,0.000830129,,1.678854137,,1.094911331, +950,M220,,1,0.015307087,,1.809461644,,0.301293621, +950,,,1,0.003457908,,1.901796041,,0.614250824, +951,210,,1,0.000389432,,1.543034139,,1.240864013, +951,,,1,0.000438463,,1.567170618,,1.19476273, +970,230,,1,0.000635908,,1.474389655,,1.196459588, +970,,,1,0.000942902,,1.53060248,,1.071535656, +971,,,1,0.001744408,,1.619743693,,0.741518961, +972,210,,1,0.000634963,,1.456764851,,1.152037325, +972,,,1,0.007011161,,2.34756431,,0.077868254, +975,,,1,0.001744408,,1.619284656,,0.741518961, +999,230,,2,0.000824759,,1.795031481,3.009885463,0.999091843, +999,M220,,2,0.007233279,,2.010557819,0.835700122,0.379123913, +999,,,2,0.003633332,,1.982891885,1.364338128,0.530057408, diff --git a/src/pyfia/carbon/nsvb/data/volib_jenkins.csv b/src/pyfia/carbon/nsvb/data/volib_jenkins.csv new file mode 100644 index 00000000..765e03ab --- /dev/null +++ b/src/pyfia/carbon/nsvb/data/volib_jenkins.csv @@ -0,0 +1,10 @@ +JENKINS_SPGRPCD,model,a,b,c +1,1,0.006419335,1.808424036,0.857982524 +2,1,0.002661495,1.701389437,1.118450094 +3,1,0.002186946,1.774662786,1.138638416 +4,1,0.002966164,1.892635521,1.004043997 +5,1,0.002589658,1.720103984,1.133217595 +6,1,0.003638299,1.797344245,1.002439989 +7,1,0.004762398,2.000019654,0.824681735 +8,1,0.002340041,1.894587354,1.03509406 +9,1,0.002653263,1.8973528,1.015463475 diff --git a/src/pyfia/carbon/nsvb/data/volib_spcd.csv b/src/pyfia/carbon/nsvb/data/volib_spcd.csv new file mode 100644 index 00000000..6dff9873 --- /dev/null +++ b/src/pyfia/carbon/nsvb/data/volib_spcd.csv @@ -0,0 +1,407 @@ +SPCD,DIVISION,STDORGCD,model,a,a1,b,b1,c,c1 +10,M240,,1,0.002105266,,1.646091312,,1.244413373, +10,,,1,0.001955587,,1.650546706,,1.256038073, +11,240,,4,0.001314136,,2.037385804,0.014090669,1.156702494, +11,M210,,4,0.001025611,,2.022202393,0.01342096,1.21244134, +11,M240,,4,0.002106226,,1.825077968,0.003283116,1.14481908, +11,,,4,0.002022903,,1.844885667,0.004062689,1.143651072, +12,130,,2,0.004116364,,1.997786965,1.883244117,0.872349268, +12,210,,2,0.002286977,,1.873151281,1.589354736,1.08803123, +12,220,,2,0.003473653,,1.979336137,1.532151317,0.930889614, +12,M210,,2,0.003441041,,2.085508739,1.794777907,0.882612879, +12,,,2,0.003256399,,1.931516072,1.783286999,0.967436664, +15,M260,,1,0.001779048,,1.684069848,,1.25331477, +15,M310,,1,0.005906791,,1.892653872,,0.803067573, +15,M330,,1,0.002324652,,1.709973345,,1.171636864, +15,,,1,0.001814677,,1.617253621,,1.284112391, +16,,,1,0.002401761,,1.890612135,,1.085865387, +17,,,1,0.002506212,,1.612819191,,1.215027357, +18,,,1,0.002401761,,1.817644441,,1.085865387, +19,M130,,2,0.001634352,,1.73257167,1.68330915,1.233341676, +19,M210,,2,0.002100712,,1.675751008,1.746979096,1.199543175, +19,M240,,2,0.001112761,,1.506559646,1.540586029,1.450721623, +19,M330,,2,0.002130275,,1.87086212,1.745591354,1.091486421, +19,,,2,0.002075426,,1.710841252,1.758231751,1.180918784, +20,M260,,1,0.002088051,,1.748667994,,1.171822967, +20,,,1,0.002091937,,1.760055688,,1.164393456, +42,M240,,1,0.004782723,,1.902390808,,0.892831119, +42,,,1,0.004765815,,1.904044761,,0.892448554, +43,230,,2,0.001314921,,1.804774826,1.467690305,1.266282758, +43,,,2,0.001314921,,1.804774826,1.467690305,1.266282758, +68,220,,2,0.003563439,,1.84780681,1.550725595,1.009488024, +68,230,,2,0.00234356,,1.901522463,1.881085501,1.073171229, +68,250,,2,0.00431645,,1.817732753,1.542554839,0.975870253, +68,,,2,0.003655101,,1.877328983,1.724682224,0.979761822, +71,130,,2,0.0029289,,1.884167216,1.83074748,0.996152271, +71,210,,2,0.002363171,,1.740952976,1.661712629,1.138337566, +71,M130,,2,0.00131403,,1.714403851,1.559860863,1.277668518, +71,M210,,2,0.001077559,,1.81085226,1.6324531,1.247804983, +71,M330,,2,0.002466809,,2.057237984,1.747497577,0.935116618, +71,,,2,0.003337275,,2.076380058,1.718831197,0.870308519, +73,,,1,0.003805829,,1.745228173,,1.00454214, +81,M260,,1,0.003602549,,1.647778389,,1.065920541, +81,,,1,0.003602549,,1.647778389,,1.065920541, +90,130,,4,0.002206547,,2.034324492,0.01617144,1.034936034, +90,M130,,4,0.001590526,,1.831495104,0.006044958,1.184669479, +90,M210,,4,0.001292676,,1.901373,0.011681872,1.210613952, +90,M330,,4,0.000692235,,1.924058351,0.013813752,1.343102035, +90,,,4,0.001359241,,1.876480245,0.009562428,1.207913332, +91,210,,2,0.002221498,,1.909883425,1.762413576,1.09898488, +91,,,2,0.002221498,,1.909883425,1.762413576,1.09898488, +93,M130,,4,0.000969821,,2.000275384,0.015967746,1.226887668, +93,M210,,4,0.001719606,,1.824534232,0.003627456,1.168334129, +93,M240,,4,0.001568996,,1.955379242,0.005074652,1.106631833, +93,M330,,4,0.002122802,,1.847377308,0.006760462,1.121644312, +93,M340,,4,0.001550133,,1.768834396,0.00607262,1.256850412, +93,,,4,0.001863819,,1.84729939,0.00326328,1.134537296, +94,130,,4,0.002199397,,1.931048845,0.007812733,1.067838362, +94,210,,4,0.002168351,,1.886029998,0.000139482,1.094006839, +94,220,,4,0.002237806,,2.318239458,0.04927362,0.975157307, +94,M130,,4,0.002803334,,1.804703839,-0.007055569,1.035663195, +94,,,4,0.002275203,,1.974478813,0.010230184,1.042932158, +95,130,,2,0.002562647,,1.968702693,1.894349233,1.004527267, +95,210,,2,0.002317146,,1.862338392,1.893127276,1.088914627, +95,M130,,2,0.003002177,,1.833525183,1.428601355,1.026454355, +95,,,2,0.002537257,,1.944862219,1.894822781,1.019802825, +96,,,1,0.002529244,,1.867546803,,1.067509649, +97,130,,2,0.004414533,,2.038946661,1.794791813,0.855116324, +97,210,,2,0.004826864,,2.013044776,1.952442367,0.844278004, +97,M210,,2,0.004364511,,1.992816099,2.005057214,0.878014732, +97,,,2,0.004787209,,2.008719787,1.97491536,0.847621627, +98,M240,,1,0.002837429,,1.810551218,,1.075638729, +98,,,1,0.002837429,,1.810551218,,1.075638729, +100,130,,1,0.002280915,,1.829853625,,1.10918291, +100,M210,,1,0.001303017,,2.012605391,,1.106612619, +100,,,1,0.001831937,,1.8208787,,1.164820025, +101,,,1,0.002107678,,1.9564651,,1.055158362, +105,130,,2,0.002055476,,1.863097305,1.730596861,1.12132034, +105,210,,2,0.002070105,,1.873781246,1.805997844,1.10287782, +105,220,,2,0.002092019,,1.898833801,1.756631113,1.093707752, +105,,,2,0.002062653,,1.840224205,1.757372072,1.127733145, +107,230,,2,0.001132306,,1.890891904,1.722455796,1.259784582, +107,,,2,0.001132306,,1.890891904,1.722455796,1.259784582, +108,130,,2,0.002878149,,1.877791943,1.790914456,1.043195677, +108,330,,2,0.001480653,,1.740027609,1.737088605,1.288522763, +108,M130,,2,0.003085159,,1.977581944,1.796364133,0.98120355, +108,M210,,2,0.002923993,,2.016821843,1.825815722,0.960877109, +108,M240,,2,0.00288801,,1.955076824,1.692491493,1.005097398, +108,M330,,2,0.002386352,,1.80023918,1.779867924,1.115355104, +108,,,2,0.00319606,,1.917177746,1.815139513,0.993202346, +110,220,,2,0.000777045,,1.919017458,2.006929021,1.329213138, +110,230,,2,0.00110232,,2.041432642,1.986621938,1.156272324, +110,250,,2,0.002041275,,1.762557001,1.926880267,1.182270401, +110,M220,,2,0.001383088,,2.211772956,2.049872774,1.019775368, +110,M230,,2,0.001560914,,1.853400869,1.978535587,1.182948709, +110,,,2,0.001122038,,2.105001407,2.000099461,1.124110515, +111,230,0,2,0.001206001,,2.141422639,1.948751732,1.068957337, +111,,0,2,0.001203979,,2.140767169,1.948181836,1.069732453, +111,230,1,4,0.001776285,,1.98252695,-0.009785143,1.022562887, +111,,1,4,0.001776285,,1.98252695,-0.009785143,1.022562887, +113,,,1,0.002107678,,1.936442718,,1.055158362, +115,230,,1,0.001507831,,1.955151491,,1.125108578, +115,,,1,0.001578253,,1.952258698,,1.116891165, +116,,,1,0.002107678,,1.8938979,,1.055158362, +117,M260,,1,0.004717751,,2.268910691,,0.604399249, +117,,,1,0.003680052,,2.262009549,,0.661332692, +119,M240,,1,0.002377883,,1.81067185,,1.10617161, +119,M330,,1,0.002603725,,1.753066662,,1.108065187, +119,,,1,0.001932675,,1.775262301,,1.162125091, +121,230,,4,0.001516615,,2.090049345,0.007746959,1.062848127, +121,,,4,0.001516613,,2.089580462,0.007707589,1.062997184, +122,310,,2,0.001738495,,1.967634993,2.030178118,1.049873371, +122,330,,2,0.003233972,,1.624669152,1.926200718,1.150818232, +122,M210,,2,0.003389828,,1.869091154,2.208548542,0.909497477, +122,M240,,2,0.002658385,,0.557011298,1.831877115,1.67376658, +122,M260,,2,0.001495525,,1.985321888,1.817312257,1.100526301, +122,M310,,2,0.002005666,,1.649348057,2.008272661,1.194346702, +122,M330,,2,0.003085977,,2.040219273,2.124108483,0.876678418, +122,M340,,2,0.005313948,,1.657143777,2.272248249,0.919258585, +122,,,2,0.00288708,,2.028730208,2.098375293,0.898715884, +123,M220,,2,0.001916279,,1.95623575,1.778492147,1.093486861, +123,,,2,0.00225438,,1.904602953,1.75650464,1.085488889, +125,130,,4,0.001496485,,2.061619028,0.017128978,1.135973982, +125,210,,4,0.001789916,,1.875382444,0.003157603,1.160635652, +125,220,,4,0.001903835,,1.770729106,-0.00057922,1.19321363, +125,,,4,0.001658315,,1.964368348,0.010223394,1.147746414, +126,M220,,1,0.002864736,,2.064160936,,0.897989349, +126,,,1,0.002410363,,2.047558886,,0.948865028, +128,230,,4,0.001006663,,2.176431567,0.020582943,1.143591808, +128,,,4,0.001006663,,2.176431567,0.020582943,1.143591808, +129,130,,4,0.003081552,,1.977446041,0.009246034,0.985991629, +129,210,,4,0.003177188,,1.926231112,0.003642885,0.98874777, +129,220,,4,0.003357912,,1.388552145,-0.026868928,1.200023985, +129,230,,4,0.001871984,,1.455588893,-0.022280263,1.278899493, +129,M210,,4,0.000235838,,2.292797529,0.031390185,1.4530897, +129,M220,,4,0.002888533,,1.674086228,-0.008616141,1.089758937, +129,,,4,0.002766541,,1.941352268,0.006566437,1.014721582, +130,,,1,0.002107678,,2.029106434,,1.055158362, +131,220,0,1,0.00178627,,1.791805472,,1.183627837, +131,230,0,1,0.00126518,,1.924197683,,1.179503155, +131,M230,0,1,0.005147286,,2.122674603,,0.725865544, +131,,0,1,0.001300178,,1.926380427,,1.172025932, +131,230,1,2,0.001141513,,1.902700511,2.037892774,1.195155008, +131,,1,2,0.001143214,,1.901569242,2.038424885,1.195341345, +132,220,,1,0.001315129,,1.920971347,,1.205888915, +132,230,,1,0.002233565,,1.854086876,,1.098984731, +132,M220,,1,0.002006403,,1.888810417,,1.105899124, +132,,,1,0.001961933,,1.908848701,,1.101819254, +202,130,,2,0.002747235,,1.840903919,1.79172925,1.02989805, +202,240,,2,0.0019291,,2.162413104,1.690400253,0.985444005, +202,260,,2,0.001976401,,1.8881851,1.718329952,1.100838973, +202,340,,2,0.001369305,,2.002624571,1.717735113,1.125427379, +202,M210,,2,0.001727351,,1.893583851,1.75442179,1.102884611, +202,M240,,2,0.001794806,,1.766486444,1.707744264,1.163749516, +202,M260,,2,0.00128569,,1.998382259,1.709014674,1.150088674, +202,M310,,2,0.004126027,,1.688585292,1.92854407,0.982841682, +202,M330,,2,0.00241067,,1.867664023,1.809698364,1.041719416, +202,,,2,0.001707245,,1.907559079,1.698695714,1.117368903, +211,260,,1,0.001390364,,1.885069779,,1.10545256, +211,,,1,0.001395458,,1.885281581,,1.104590997, +221,230,,1,0.00122729,,1.863282782,,1.219826036, +221,,,1,0.001206517,,1.873827338,,1.218170639, +222,230,,1,0.007497039,,1.89423149,,0.771194455, +222,,,1,0.007497039,,1.89423149,,0.771194455, +241,210,,2,0.00212743,,1.912945803,1.57638436,1.097990759, +241,,,2,0.002261084,,1.847313878,1.636435811,1.111331969, +242,240,,2,0.002787384,,1.807030086,1.513098872,1.096964328, +242,340,,2,0.001412069,,1.290589734,1.439865993,1.49544182, +242,M210,,2,0.001243136,,2.332375314,1.61634417,0.996367093, +242,M240,,2,0.004994986,,1.848602703,1.808037228,0.900560216, +242,M330,,2,0.002505673,,1.832657642,1.641979225,1.078424562, +242,,,2,0.00376795,,1.845236586,1.739437151,0.969154449, +260,M210,,1,0.002295553,,1.967535518,,0.996321144, +260,M240,,1,0.001607959,,1.644483976,,1.282810954, +260,M330,,1,0.002278835,,1.947987958,,1.012737559, +260,,,1,0.002145457,,1.909455942,,1.050023161, +261,210,,4,0.004089111,,2.053259982,0.001841566,0.824543404, +261,M220,,4,0.008141989,,1.747586901,-0.020037769,0.757384869, +261,,,4,0.010314737,,1.833480121,-0.015769951,0.674341816, +263,240,,4,0.002035248,,1.640018892,-0.002749061,1.225971607, +263,340,,4,0.00148065,,1.911172335,0.00986047,1.180406183, +263,M210,,4,0.001260367,,2.087852465,0.010920378,1.104263084, +263,M240,,4,0.002748271,,1.909673082,0.003311733,1.016882715, +263,M330,,4,0.001547035,,1.991258598,0.008035215,1.103901654, +263,,,4,0.002136768,,1.930652162,0.004590853,1.061907236, +264,M240,,1,0.003965719,,1.728150183,,1.037510985, +264,,,1,0.00399982,,1.729039674,,1.035173752, +311,,,1,0.003420514,,1.912068806,,0.937612533, +312,M240,,2,0.001315191,,1.862940338,1.916341099,1.185416225, +312,,,2,0.001357571,,1.860085589,1.921258958,1.179426027, +313,,,1,0.000928715,,1.631281941,,1.413397747, +315,,,1,0.003420514,,1.917428517,,0.937612533, +316,210,,1,0.002134379,,1.841807926,,1.101421884, +316,220,,1,0.002223863,,1.796439352,,1.118825956, +316,230,,1,0.003032006,,1.78864021,,1.032556102, +316,M220,,1,0.003240914,,1.951577767,,0.926042887, +316,,,1,0.001983919,,1.810559393,,1.129417635, +317,,,1,0.002521072,,1.878400471,,1.030604096, +318,210,,2,0.001655057,,2.13941586,1.872806283,1.0162263, +318,220,,2,0.001461983,,2.10718681,1.890669621,1.058901006, +318,M210,,2,0.002806116,,2.13181472,1.921851342,0.896031529, +318,M220,,2,0.001575159,,1.75427547,1.91744041,1.215607529, +318,,,2,0.00163448,,2.104341893,1.88866572,1.035961689, +330,M220,,1,0.002727231,,1.957425134,,0.961347984, +330,,,1,0.002727231,,1.957425134,,0.961347984, +351,M240,,2,0.002404979,,1.967517279,1.760757271,1.021067222, +351,,,2,0.002348128,,1.957931122,1.779346537,1.030792692, +370,230,,2,0.003012876,,2.121240091,1.869350326,0.838521457, +370,M220,,2,0.002862473,,1.828387739,1.750178759,1.029313084, +370,,,2,0.002559698,,1.887532685,1.77187523,1.01843694, +371,130,,4,0.008175378,,1.868692126,-0.002743875,0.730288933, +371,210,,4,0.006416697,,2.468913107,0.021823931,0.542518638, +371,220,,4,0.004003013,,1.963712822,0.002227596,0.917388791, +371,M210,,4,0.004130309,,2.163610439,0.012163806,0.817695631, +371,,,4,0.004006005,,2.259689281,0.013940879,0.760939024, +372,,,1,0.003420514,,1.933940098,,0.937612533, +373,,,1,0.003420514,,1.988985866,,0.937612533, +374,,,1,0.003420514,,1.945999784,,0.937612533, +375,130,,2,0.004137357,,1.941341387,1.841535881,0.886162874, +375,210,,2,0.001532816,,1.972556365,1.661557506,1.118811904, +375,220,,2,0.008598549,,2.044287328,1.979066142,0.672613905, +375,M130,,2,0.000784949,,1.885364041,1.926013591,1.303150388, +375,M210,,2,0.006038416,,2.09088544,1.8171439,0.730065754, +375,M330,,2,0.001854874,,1.840943453,1.830996306,1.124974281, +375,,,2,0.003077203,,2.007445989,1.768143384,0.932016496, +391,,,1,0.002952358,,1.92984922,,0.96875716, +400,220,,1,0.001599897,,1.909315098,,1.113037138, +400,230,,1,0.001690636,,1.96098553,,1.059868513, +400,M220,,1,0.001781929,,1.963341334,,1.044868888, +400,M230,,1,0.001547891,,2.074547562,,1.009832735, +400,,,1,0.001806415,,1.963197399,,1.043202022, +402,,,1,0.00286459,,1.984717259,,0.964612913, +403,220,,1,0.003579591,,1.988806838,,0.917140667, +403,,,1,0.002961637,,1.996786998,,0.954636138, +404,230,,1,0.002890361,,1.953740226,,0.947535649, +404,,,1,0.003889204,,1.778187873,,0.991834979, +405,,,1,0.00286459,,1.974160267,,0.964612913, +407,,,1,0.004059511,,1.776091574,,1.019398878, +409,,,1,0.00286459,,1.984143211,,0.964612913, +421,220,,1,0.006317437,,2.01610988,,0.710510609, +421,,,1,0.006317437,,2.01610988,,0.710510609, +460,230,,1,0.001544739,,1.811657529,,1.173202988, +460,,,1,0.001480553,,1.812395764,,1.182263869, +461,230,,1,0.002174056,,1.872068509,,1.057363664, +461,,,1,0.002174056,,1.872068509,,1.057363664, +462,,,1,0.002952358,,1.921394533,,0.96875716, +471,,,1,0.002952358,,1.901309261,,0.96875716, +491,230,,1,0.005543119,,1.879496453,,0.80097398, +491,,,1,0.007055807,,1.882487527,,0.722396627, +521,230,,2,0.002574739,,2.00042444,2.059038499,0.952788408, +521,,,2,0.002637669,,1.918160504,2.128306223,0.984801242, +531,210,,4,0.002882326,,2.372917446,0.029673624,0.841559652, +531,220,,4,0.002103831,,2.541082778,0.034710751,0.842802235, +531,230,,4,0.003110158,,1.995089986,0.007533484,0.941904998, +531,M210,,4,0.002631898,,2.21241101,0.017554554,0.926550456, +531,M220,,4,0.00240694,,1.897563413,0.00377738,1.048515915, +531,,,4,0.00161488,,2.396448915,0.032188753,0.967808454, +540,230,,1,0.004060862,,1.922974062,,0.884947291, +540,M220,,1,0.002063468,,1.967024344,,1.014142781, +540,,,1,0.003305092,,1.940569042,,0.921463069, +541,210,,1,0.003229716,,1.823841733,,1.021430399, +541,220,,1,0.005425594,,1.90798864,,0.852367734, +541,M210,,1,0.008118161,,1.929370666,,0.738919826, +541,M220,,1,0.001416382,,1.990435175,,1.074822866, +541,,,1,0.003788303,,1.909085468,,0.927529384, +543,210,,1,0.004339818,,1.957028745,,0.871261553, +543,,,1,0.004123524,,1.976087323,,0.876473762, +544,230,,1,0.006501623,,1.756174388,,0.844616988, +544,,,1,0.004484906,,1.541717812,,1.086752062, +552,,,1,0.002952358,,1.9270468,,0.96875716, +555,230,,1,0.001481168,,2.220248262,,0.966119291, +555,,,1,0.001481168,,2.220248262,,0.966119291, +591,230,,1,0.003454084,,1.912609551,,0.955459241, +591,,,1,0.003454084,,1.912609551,,0.955459241, +601,,,1,0.002952358,,1.914942848,,0.96875716, +602,M220,,1,0.001421079,,1.833759151,,1.16695459, +602,,,1,0.00114511,,2.008248891,,1.131444529, +611,230,,1,0.001955814,,1.917063525,,1.057972896, +611,,,1,0.001893084,,1.915298564,,1.067328281, +621,220,,1,0.001423832,,1.727426323,,1.270789193, +621,230,,1,0.001433246,,1.856819575,,1.158180104, +621,M220,,1,0.001967332,,1.916098396,,1.054831101, +621,,,1,0.001557686,,1.898952303,,1.118616007, +651,,,1,0.002529171,,1.937556424,,1.009251728, +652,,,1,0.002952358,,1.930826237,,0.96875716, +653,230,,1,0.003736089,,1.815541095,,0.967414933, +653,,,1,0.003736089,,1.815541095,,0.967414933, +680,,,1,0.002952358,,1.882153944,,0.96875716, +691,230,,1,0.005905446,,1.633320682,,0.957794215, +691,,,1,0.005905446,,1.633320682,,0.957794215, +693,220,,1,0.001879015,,1.778146274,,1.187603529, +693,230,,1,0.004028823,,1.904705744,,0.893713686, +693,M220,,1,0.005971665,,2.04253298,,0.71291215, +693,,,1,0.004379082,,1.944488798,,0.852584831, +694,230,,1,0.002566117,,1.807335095,,1.07187035, +694,,,1,0.002566117,,1.807335095,,1.07187035, +701,,,1,0.002952358,,1.967731326,,0.96875716, +711,,,1,0.002952358,,1.912442565,,0.96875716, +731,230,,1,0.002347552,,1.758407752,,1.115262916, +731,,,1,0.002479836,,1.818806265,,1.069737014, +740,130,,2,0.008067214,,2.210828772,2.100238302,0.548591652, +740,230,,2,0.000268264,,1.167831143,1.746002714,1.883847936, +740,M330,,2,0.003191939,,1.939069943,2.018903049,0.965799112, +740,,,2,0.005173832,,2.106571405,1.874455783,0.75270036, +741,130,,2,0.001423838,,1.902192451,1.886618917,1.133881544, +741,210,,2,0.001525721,,2.016786059,2.011959696,1.091663293, +741,,,2,0.001415458,,1.919367366,1.933729943,1.134219405, +742,250,,1,0.007134525,,1.93836459,,0.754973524, +742,,,1,0.006817443,,1.850039258,,0.825414657, +743,130,,1,0.003391884,,2.005799612,,0.899210112, +743,210,,1,0.001737379,,1.943190202,,1.099432508, +743,220,,1,0.001473045,,1.912980565,,1.156194615, +743,,,1,0.001733,,1.952649878,,1.09444816, +746,130,,2,0.003702323,,2.038601841,1.814900864,0.870797938, +746,210,,2,0.002170498,,1.978777,1.900581566,1.029913203, +746,220,,2,0.002205077,,1.992320886,1.860334276,1.026155773, +746,M130,,2,0.002655636,,1.736690549,1.842798634,1.070544148, +746,M210,,2,0.000437115,,1.884891243,1.629741549,1.42300866, +746,M240,,2,0.00167632,,1.679186659,1.731815738,1.226659878, +746,M330,,2,0.002971107,,1.949314015,1.76660212,0.950962798, +746,,,2,0.002627679,,2.004530693,1.755105144,0.968856786, +747,130,,1,0.001046112,,1.839040949,,1.264304506, +747,M130,,1,0.000900672,,1.86516073,,1.282163875, +747,M210,,1,0.001689187,,1.803074558,,1.171240355, +747,M330,,1,0.000721864,,1.723252169,,1.409360786, +747,,,1,0.001024036,,1.828976606,,1.272915882, +762,210,,1,0.002145982,,1.717374336,,1.192709513, +762,230,,1,0.002423304,,1.872908477,,1.042807321, +762,M220,,1,0.001265628,,1.94015381,,1.16285797, +762,,,1,0.001853589,,1.797632465,,1.1701677, +800,230,,3,0.005109889,1.750900491,0.184334722,,0.964427633,0.268865063 +800,,,3,0.005109889,1.750900491,0.184334722,,0.964427633,0.268865063 +802,210,,1,0.001761824,,1.738205473,,1.20958169, +802,220,,1,0.003073136,,1.928952615,,0.976820125, +802,230,,1,0.001405746,,1.828081387,,1.198100829, +802,250,,1,0.003985787,,2.058459872,,0.837255089, +802,M220,,1,0.002062932,,1.852527629,,1.093126447, +802,M230,,1,0.005130883,,2.022468839,,0.776669402, +802,,,1,0.002321951,,1.889060817,,1.049599115, +804,,,1,0.00286459,,1.951784393,,0.964612913, +806,220,,4,0.001503245,,2.177705887,0.01293309,1.037801124, +806,230,,4,0.001362221,,2.167149067,0.019200117,1.06228207, +806,M220,,4,0.001688126,,2.162396983,0.015105215,1.003848294, +806,,,4,0.001385735,,2.176710667,0.01740908,1.053132652, +809,220,,1,0.001351837,,1.741953319,,1.283106567, +809,,,1,0.00157276,,1.740125879,,1.247013813, +812,230,,2,0.001709916,,1.830473869,1.863288638,1.137334753, +812,,,2,0.001746521,,1.817355585,1.890927424,1.137859843, +813,230,,1,0.002349662,,1.882192367,,1.046294152, +813,,,1,0.002530902,,1.90267752,,1.018542816, +817,,,1,0.00286459,,1.960936975,,0.964612913, +820,230,,2,0.002785156,,2.062018029,1.776182956,0.916787282, +820,,,2,0.002775138,,2.063316971,1.776038172,0.916922327, +822,230,,1,0.002293046,,1.903918224,,1.020081966, +822,,,1,0.002293046,,1.903918224,,1.020081966, +823,,,1,0.000738076,,1.887570874,,1.324172635, +825,230,,1,0.001615338,,1.723088553,,1.233047491, +825,,,1,0.001804094,,1.738949632,,1.1964815, +826,,,1,0.00286459,,1.956666999,,0.964612913, +827,230,,2,0.002510178,,1.870602163,1.825369835,1.041912932, +827,,,2,0.002510178,,1.870602163,1.825369835,1.041912932, +828,230,,1,0.002492455,,1.91102546,,1.010625345, +828,,,1,0.002492455,,1.91102546,,1.010625345, +830,,,1,0.00286459,,1.968248331,,0.964612913, +831,230,,1,0.002308302,,1.84342235,,1.060906206, +831,,,1,0.002329714,,1.84192663,,1.060155664, +832,220,,1,0.001340168,,1.904576977,,1.172515505, +832,230,,1,0.001557419,,1.873893241,,1.133291605, +832,M220,,1,0.002081683,,1.960328445,,1.013901564, +832,,,1,0.001714369,,1.936348905,,1.076702143, +833,210,,1,0.002322315,,1.799849142,,1.117365216, +833,220,,1,0.001573268,,1.823458182,,1.190545046, +833,230,,1,0.001472015,,1.838192568,,1.17871924, +833,M220,,1,0.002134823,,1.905313358,,1.048458986, +833,,,1,0.002362152,,1.874299976,,1.055751972, +834,,,1,0.00286459,,1.949227114,,0.964612913, +835,220,,1,0.001673819,,1.936343771,,1.111522047, +835,230,,1,0.002373136,,1.920734316,,1.01402981, +835,,,1,0.003305717,,1.988355446,,0.896401484, +837,210,,2,0.001518878,,2.113998971,1.649991362,1.046010305, +837,220,,2,0.000437947,,2.428124901,1.79498111,1.160762625, +837,230,,2,0.00155351,,1.889213723,2.042730169,1.115496949, +837,250,,2,0.000919117,,2.706014496,1.694757756,0.846614554, +837,M210,,2,0.002954279,,2.08159177,1.928845656,0.887282921, +837,M220,,2,0.00126221,,2.046186215,1.895886476,1.0848866, +837,,,2,0.00151444,,2.22320961,1.883670123,0.959477251, +838,230,,1,0.002131697,,1.767292517,,1.120625347, +838,,,1,0.002131697,,1.767292517,,1.120625347, +901,M220,,1,0.001660506,,1.818805739,,1.117347086, +901,,,1,0.001693706,,1.817515456,,1.113882311, +920,,,1,0.00247111,,1.817154576,,1.031521892, +922,,,1,0.00247111,,1.860927908,,1.031521892, +931,,,1,0.002952358,,1.931093987,,0.96875716, +950,210,,4,0.003226314,,2.302737543,0.017167865,0.793085798, +950,M220,,4,0.002008879,,2.001563697,0.004256522,1.025581517, +950,,,4,0.003505685,,2.274426602,0.01484614,0.780472682, +951,210,,2,0.001934393,,2.153803577,1.932362544,0.963175885, +951,220,,2,0.001186781,,1.933040653,1.927964512,1.188299017, +951,,,2,0.001590209,,2.081672706,1.932067041,1.045752102, +970,230,,1,0.001524725,,1.798591052,,1.190571411, +970,,,1,0.001844723,,1.826132667,,1.129759189, +971,,,1,0.002952358,,1.913957722,,0.96875716, +972,210,,2,0.001288116,,1.816246182,1.834838985,1.217959936, +972,,,2,0.001506761,,1.879128146,1.790820226,1.148150698, +975,,,1,0.002952358,,1.946558607,,0.96875716, +999,230,,1,0.004080563,,1.865598495,,0.894005944, +999,M220,,1,0.001871464,,1.892577254,,1.081824247, +999,,,1,0.002017556,,1.864705353,,1.084696923, diff --git a/src/pyfia/carbon/nsvb/equations.py b/src/pyfia/carbon/nsvb/equations.py new file mode 100644 index 00000000..d1bf9009 --- /dev/null +++ b/src/pyfia/carbon/nsvb/equations.py @@ -0,0 +1,1285 @@ +""" +NSVB allometric equation forms (Westfall et al. 2023, GTR-WO-104). + +Pure-math equation library for the National Scale Volume and Biomass framework. +No I/O, no Polars dependency — just floats in and floats out. Coefficient lookup +lives in coefficients.py; carbon fractions live in carbon_fractions.py. + +The four model forms used in Phase 1 (live tree biomass) are: + +- **Model 1**: ``y = a * D^b * H^c`` — power form, the most common. +- **Model 2**: ``y = a * k^(b-b1) * D^b1 * H^c`` — with ``k = 9`` for softwoods + (SPCD < 300) and ``k = 11`` for hardwoods (SPCD >= 300). The constants 9 and 11 + match the sawlog top-diameter cutoffs used elsewhere in NSVB. Verified against + the Douglas-fir (SPCD=202) wood-volume worked example and back-solved against + the red maple (SPCD=316) bark-volume worked example. +- **Model 4**: ``y = a * D^b * H^c * exp(-b1 * D)`` — power form modulated by an + exponential of D. The b1 parameter is typically a small negative number, so + the multiplicative factor is slightly greater than 1 across the practical D + range. Verified against the red maple S8a (total AGB) worked example. +- **Model 5 (Jenkins fallback)**: ``y = a * D^b * H^c * WDSG`` — used when a + species lacks SPCD-specific coefficients and falls back to its Jenkins species + group. Wood density (WDSG) comes from FIADB ``REF_SPECIES.WOOD_SPGR_GREENVOL_DRYWT``. + +**Model 3 and Model 6 are NOT implemented in Phase 1**. Model 3 is a three-parameter +form whose exact shape is picture-omitted in the source PDF and unverified. +Model 6 is the iterative Schumacher-Hall volume-ratio model used for merchantable +subdivision (DRYBIO_BOLE / DRYBIO_TOP / DRYBIO_STUMP); total above-ground biomass +and total carbon do not require it. Both will arrive in a later phase if downstream +work needs them. + +The orchestrator :func:`predict_tree_biomass` runs the full per-tree pipeline: +predict component biomasses (wood, bark, branches), predict directly the total AGB, +then harmonize the components so they sum exactly to the predicted total. The +harmonization is documented in ``gtr_wo104_westfall2023.md`` lines 600-612 (live tree +worked example) and lines 866-898 (cull-adjusted variant for the red maple example). + +References +---------- +- Westfall, J.A. et al. (2023). GTR-WO-104. DOI: 10.2737/WO-GTR-104 +- Worked examples: ``gtr_wo104_westfall2023.md`` lines 394-680 (Douglas-fir, + no cull) and lines 682-960 (red maple, with 3% cull). +""" + +from __future__ import annotations + +import math +from dataclasses import dataclass +from typing import TYPE_CHECKING, Literal + +import polars as pl + +if TYPE_CHECKING: + from pyfia.carbon.nsvb.coefficients import VectorizedLookupTables + +# Sawlog top-diameter cutoffs that double as the Model 2 ``k`` constant. +# Per worked example line 462: softwood top diameter is 7-9 inches and hardwood is +# 9-11 inches, depending on species. The Model 2 base constant matches the upper +# bound of these cutoffs. +_K_SOFTWOOD = 9.0 +_K_HARDWOOD = 11.0 +_HARDWOOD_SPCD_THRESHOLD = 300 + + +def _model_k(spcd: int) -> float: + """Return the Model 2 base constant ``k`` for a given species code. + + Softwoods (SPCD < 300) use k=9; hardwoods (SPCD >= 300) use k=11. The split + point matches the FIA hardwood/softwood classification used throughout NSVB. + """ + return _K_HARDWOOD if spcd >= _HARDWOOD_SPCD_THRESHOLD else _K_SOFTWOOD + + +def model_1(d: float, h: float, a: float, b: float, c: float) -> float: + """NSVB Model 1: ``y = a * D^b * H^c``. + + The most common NSVB form, used for stem wood volume (S1a), stem bark volume + (S2a), stem bark biomass (S6a), branch biomass (S7a), total AGB (S8a), and + foliage biomass (S9a) for the majority of FIA species. + + Parameters + ---------- + d : float + Diameter at breast height (inches). + h : float + Total tree height (feet). + a, b, c : float + Model 1 coefficients from the relevant ``S*a`` table. + + Returns + ------- + float + Predicted quantity in the units of the source table (cubic feet for + volume tables, pounds for biomass tables). + """ + return float(a * (d**b) * (h**c)) + + +def model_2( + d: float, h: float, a: float, b: float, b1: float, c: float, k: float +) -> float: + """NSVB Model 2: ``y = a * k^(b - b1) * D^b1 * H^c``. + + Power form with a species-class base constant ``k``. Used for stem wood + volume and stem bark volume on a subset of species. The ``k`` constant + is 9 for softwoods (SPCD < 300) and 11 for hardwoods (SPCD >= 300); use + :func:`_model_k` (or pass it explicitly) when calling from the orchestrator. + + Parameters + ---------- + d : float + Diameter at breast height (inches). + h : float + Total tree height (feet). + a, b, b1, c : float + Model 2 coefficients from the relevant ``S*a`` table. + k : float + Species-class base constant — typically 9.0 (softwood) or 11.0 (hardwood). + + Returns + ------- + float + Predicted quantity in source-table units. + """ + return float(a * (k ** (b - b1)) * (d**b1) * (h**c)) + + +def model_4(d: float, h: float, a: float, b: float, b1: float, c: float) -> float: + """NSVB Model 4: ``y = a * D^b * H^c * exp(-b1 * D)``. + + Power form modulated by an exponential of D. Used for total AGB on a small + set of species (e.g., red maple SPCD=316). The b1 coefficient is typically + small (often slightly negative), so the exp factor is close to 1. + + Parameters + ---------- + d : float + Diameter at breast height (inches). + h : float + Total tree height (feet). + a, b, b1, c : float + Model 4 coefficients from the relevant ``S*a`` table. + + Returns + ------- + float + Predicted quantity in source-table units. + """ + return float(a * (d**b) * (h**c) * math.exp(-b1 * d)) + + +def model_5_jenkins( + d: float, h: float, a: float, b: float, c: float, wdsg: float +) -> float: + """NSVB Model 5 (Jenkins-group fallback): ``y = a * D^b * H^c * WDSG``. + + Used when a species lacks SPCD-specific coefficients in S1a-S8a and falls + back to its Jenkins species group from S1b-S8b. Wood density (WDSG) is + sourced from FIADB ``REF_SPECIES.WOOD_SPGR_GREENVOL_DRYWT``. + + Parameters + ---------- + d : float + Diameter at breast height (inches). + h : float + Total tree height (feet). + a, b, c : float + Model 5 coefficients from the relevant ``S*b`` Jenkins table. + wdsg : float + Wood specific gravity (green volume, dry weight basis) for the species. + + Returns + ------- + float + Predicted quantity in source-table units. + """ + return float(a * (d**b) * (h**c) * wdsg) + + +def harmonize_components( + agb_predicted: float, + w_wood: float, + w_bark: float, + w_branch: float, +) -> tuple[float, float, float]: + """Proportionally redistribute component weights to sum to the predicted AGB. + + NSVB makes two independent predictions of total above-ground biomass: one + by summing the component predictions (wood + bark + branch) and one by + directly predicting AGB from D and H via S8a/S8b. The two estimates rarely + agree exactly. Westfall et al. (2023) chose the directly-predicted AGB as + the truth and rescale the component sum proportionally so the harmonized + components sum exactly to ``agb_predicted`` while preserving their relative + ratios. + + Per worked example lines 600-612 (Douglas-fir, no cull) and 886-898 (red + maple, with cull). For trees with cull or decay, the caller should pass + the *cull-reduced* component weights and the *cull-reduced* predicted AGB + (``agb_predicted = AGB_predicted * AGBReduce``); see + :func:`predict_tree_biomass` for the full pipeline. + + Parameters + ---------- + agb_predicted : float + Directly-predicted total above-ground biomass (lb). + w_wood, w_bark, w_branch : float + Component biomass predictions (lb). + + Returns + ------- + tuple[float, float, float] + Harmonized (wood, bark, branch) weights such that + ``wood_h + bark_h + branch_h == agb_predicted`` to numerical precision. + If the component sum is zero or negative, returns + ``(agb_predicted, 0.0, 0.0)`` as a degenerate fallback. + """ + component_sum = w_wood + w_bark + w_branch + if component_sum <= 0: + return (agb_predicted, 0.0, 0.0) + wood_h = agb_predicted * (w_wood / component_sum) + bark_h = agb_predicted * (w_bark / component_sum) + branch_h = agb_predicted * (w_branch / component_sum) + return wood_h, bark_h, branch_h + + +# DECAYCD=3 wood density proportions used to discount cull wood weight, per +# worked example line 550 and Harmon et al. (2011) Table 1. Cull wood is +# typically partially rotten, so its weight is reduced (but not removed entirely). +_CULL_DENS_PROP = {"hardwood": 0.54, "softwood": 0.92} + + +@dataclass(frozen=True) +class Coefficients: + """A bundle of coefficients for one tree's NSVB pipeline. + + Each component table (volib, volbk, bark biomass, branch biomass, total + AGB) supplies its own coefficient row, looked up via + :func:`pyfia.carbon.nsvb.coefficients.lookup_coefficients`. The + ``volib`` and ``volbk`` entries can be Model 1 or Model 2; ``bark_bio`` + and ``branch_bio`` are Model 1 in the data we've inspected; + ``total_agb`` can be Model 1 or Model 4. + + Each entry is a dict with keys ``model, a, a1, b, b1, c, c1, source``. + ``source`` is the lookup-precedence outcome (``spcd_division_stdorg``, + ``spcd_division``, ``spcd``, or ``jenkins``). + """ + + volib: dict + volbk: dict + bark_bio: dict + branch_bio: dict + total_agb: dict + + +@dataclass(frozen=True) +class TreeBiomassResult: + """Per-tree NSVB biomass output (component-harmonized). + + All weights are in pounds. ``agb`` equals ``w_wood + w_bark + w_branch`` + by construction (harmonization invariant). + """ + + w_wood: float + w_bark: float + w_branch: float + agb: float + v_wood_ib: float # for adjusted wood density downstream (Phase 2+) + v_bark: float + + +def _eval_component(coef: dict, d: float, h: float, spcd: int, wdsg: float) -> float: + """Dispatch a single component prediction to the right model form.""" + model = int(coef["model"]) + if model == 1: + return model_1(d, h, coef["a"], coef["b"], coef["c"]) + if model == 2: + return model_2( + d, h, coef["a"], coef["b"], coef["b1"], coef["c"], _model_k(spcd) + ) + if model == 4: + return model_4(d, h, coef["a"], coef["b"], coef["b1"], coef["c"]) + if model == 5: + return model_5_jenkins(d, h, coef["a"], coef["b"], coef["c"], wdsg) + raise ValueError( + f"NSVB model {model} not supported in Phase 1 — only models 1, 2, 4, 5 are implemented." + ) + + +def predict_tree_biomass( + spcd: int, + dia: float, + ht: float, + coefficients: Coefficients, + wdsg: float, + hw_sw: Literal["hardwood", "softwood"], + cull: float = 0.0, +) -> TreeBiomassResult: + """Run the full NSVB per-tree biomass pipeline for one live tree. + + Pipeline (matches the Douglas-fir example at ``gtr_wo104_westfall2023.md:394`` + and the red maple example at ``:682``): + + 1. Predict total stem inside-bark wood volume from S1a/S1b. + 2. Predict total stem bark volume from S2a/S2b. + 3. Convert wood volume to gross weight: ``W_w = V_w * WDSG * 62.4``. + 4. Apply cull deduction to wood weight: ``W_w_red = V_w * (1 - CULL/100 * (1 - DensProp)) * WDSG * 62.4`` + where ``DensProp`` is 0.54 for hardwoods and 0.92 for softwoods (the + DECAYCD=3 wood density proportion from Harmon et al. 2011, used as the + standard cull-density assumption per worked example line 550). + 5. Predict stem bark biomass from S6a/S6b. + 6. Predict branch biomass from S7a. + 7. Predict total AGB directly from S8a/S8b. + 8. Compute the cull-reduction factor: + ``AGBReduce = (W_w_red + W_b + W_br) / (W_w + W_b + W_br)``. + 9. Reduce predicted AGB: ``AGB_predicted_red = AGB_predicted * AGBReduce``. + 10. Harmonize components against ``AGB_predicted_red`` so they sum exactly + to it (proportional redistribution). + + Note: Phase 1 assumes live trees with intact tops, so bark and branch + weights have no broken-top deductions. Standing dead trees and broken-top + deductions arrive in Phase 2. + + Foliage is **not** part of AGB and is not computed here. + + Belowground (coarse root) biomass is **not** computed here. Phase 1 reads + FIADB ``CARBON_BG`` directly via the live-tree estimator's BG bridge. + + Parameters + ---------- + spcd : int + FIA species code. Used to select the Model 2 ``k`` constant. + dia : float + Diameter at breast height (inches). Must be >= 1.0. + ht : float + Total tree height (feet). + coefficients : Coefficients + Bundle of coefficient rows for the five tables (volib, volbk, + bark_bio, branch_bio, total_agb), looked up upstream by + :func:`pyfia.carbon.nsvb.coefficients.lookup_coefficients`. + wdsg : float + Wood specific gravity (green volume, dry weight) from FIADB + ``REF_SPECIES.WOOD_SPGR_GREENVOL_DRYWT``. + hw_sw : {"hardwood", "softwood"} + Hardwood/softwood classification for the cull density proportion. + Case-insensitive at runtime (``"Hardwood"`` and ``"HARDWOOD"`` are + accepted and normalized). Callers should derive this from + ``"softwood" if spcd < 300 else "hardwood"`` to stay consistent with + :func:`_model_k`, which uses the same SPCD threshold to select the + Model 2 base constant. This rule also resolves the SPCD=10 ("fir spp.") + edge case — S10a misclassifies SPCD=10 as hardwood, but the SPCD<300 + rule correctly classifies it as softwood. + cull : float, default 0.0 + Cull percentage from FIADB ``TREE.CULL`` (0-100). Defaults to 0 for + live trees with no cull. + + Returns + ------- + TreeBiomassResult + Harmonized component weights and total AGB in pounds, plus the gross + wood and bark volumes (cubic feet) for downstream adjusted-density + calculations. + + Raises + ------ + ValueError + If ``dia < 1.0`` (NSVB is not parameterized below the FIA minimum + tally diameter of 1.0 inch, and some Model 1 forms would produce + complex-number results for d<1 with fractional b). + If ``hw_sw`` is not one of ``"hardwood"``/``"softwood"`` (after + case-insensitive normalization). + If a coefficient row specifies a Model 3 or Model 6 (not implemented + in Phase 1). + """ + # TODO(PR 2): Scalar reference implementation. PR 2's LiveTreeEstimator + # must implement this pipeline as polars expressions on a LazyFrame + # joined to the coefficient tables on SPCD, not by calling this function + # per tree. See `pyfia/carbon/__init__.py` "Architectural rules" rule 2. + + # Boundary validation — give clear errors instead of letting the math + # functions raise cryptic complex-number TypeErrors or KeyErrors. + if dia < 1.0: + raise ValueError( + f"dia must be >= 1.0 inches (FIA minimum tally diameter); got {dia}. " + "NSVB Models 1-5 are not parameterized below 1.0 and can produce " + "complex-number results for fractional b exponents." + ) + hw_sw_norm = hw_sw.lower() + if hw_sw_norm not in ("hardwood", "softwood"): + raise ValueError( + f"hw_sw must be 'hardwood' or 'softwood' (case-insensitive); got {hw_sw!r}" + ) + + # Step 1: Total stem inside-bark wood volume (cubic feet) + v_wood_ib = _eval_component(coefficients.volib, dia, ht, spcd, wdsg) + + # Step 2: Total stem bark volume (cubic feet) + v_bark = _eval_component(coefficients.volbk, dia, ht, spcd, wdsg) + + # Step 3-4: Convert wood volume to weight, with cull-reduced variant + dens_prop = _CULL_DENS_PROP[hw_sw_norm] + w_wood_gross = v_wood_ib * wdsg * 62.4 + w_wood_red = v_wood_ib * (1.0 - cull / 100.0 * (1.0 - dens_prop)) * wdsg * 62.4 + + # Step 5: Stem bark biomass (live, no broken-top deduction) + w_bark = _eval_component(coefficients.bark_bio, dia, ht, spcd, wdsg) + + # Step 6: Branch biomass (live, no broken-top deduction) + w_branch = _eval_component(coefficients.branch_bio, dia, ht, spcd, wdsg) + + # Step 7: Directly-predicted total AGB + agb_predicted = _eval_component(coefficients.total_agb, dia, ht, spcd, wdsg) + + # Step 8: Cull-reduction factor (NB: only wood is cull-reduced for live trees; + # bark and branch use their gross values in the denominator per worked example + # lines 870-880). + component_gross_sum = w_wood_gross + w_bark + w_branch + component_red_sum = w_wood_red + w_bark + w_branch + if component_gross_sum <= 0: + agb_reduce = 0.0 + else: + agb_reduce = component_red_sum / component_gross_sum + + # Step 9: Reduce predicted AGB + agb_predicted_red = agb_predicted * agb_reduce + + # Step 10: Harmonize the cull-reduced components against the reduced predicted AGB + w_wood_h, w_bark_h, w_branch_h = harmonize_components( + agb_predicted_red, w_wood_red, w_bark, w_branch + ) + + return TreeBiomassResult( + w_wood=w_wood_h, + w_bark=w_bark_h, + w_branch=w_branch_h, + agb=w_wood_h + w_bark_h + w_branch_h, + v_wood_ib=v_wood_ib, + v_bark=v_bark, + ) + + +# --------------------------------------------------------------------------- +# Vectorized NSVB pipeline (PR 2) — the production data path. +# --------------------------------------------------------------------------- +# +# The functions above (``predict_tree_biomass`` and ``_eval_component``) are +# scalar reference implementations retained as test oracles. The functions +# below replicate the same math as polars expressions over a LazyFrame, +# enabling FIA-scale (1M+ trees) evaluation via coefficient-table joins +# instead of per-tree Python dispatch. See ``pyfia/carbon/__init__.py`` +# "Architectural rules" rule 2. + + +def nsvb_biomass_expr( + *, + model: pl.Expr, + a: pl.Expr, + b: pl.Expr, + b1: pl.Expr, + c: pl.Expr, + d: pl.Expr, + h: pl.Expr, + spcd: pl.Expr, + wdsg: pl.Expr, +) -> pl.Expr: + """Build a polars expression that dispatches NSVB Models 1/2/4/5. + + The scalar equivalent of this function is :func:`_eval_component`. + It returns a single expression suitable for use inside + ``LazyFrame.with_columns`` that produces the predicted component value + (volume or biomass, in source-table units) for every row, with the + model form selected per-row from the ``model`` column. + + Models 3 and 6 are not implemented in Phase 1; rows dispatching to + those return ``None`` which will surface as a null downstream. The + orchestrator :func:`compute_nsvb_biomass` asserts that no nulls appear + in the output columns, so an unsupported model becomes a loud failure. + + The Model 2 base constant ``k`` is ``11`` for hardwoods (SPCD >= 300) + and ``9`` for softwoods, matching :func:`_model_k` used by the scalar + path. This also resolves the SPCD=10 misclassification that S10a + carries — see ``pyfia/carbon/__init__.py`` for the architectural + discussion. + + Parameters + ---------- + model, a, b, b1, c : pl.Expr + Coefficient column expressions (typically the output of a coalesce + across species-level + Jenkins lookup joins, or ``pl.col(...)`` + references to pre-joined coefficient columns). All numeric + coefficient columns must be ``Float64``; ``model`` must be an + integer dtype. + d, h : pl.Expr + Diameter at breast height (inches) and total height (feet) column + expressions. Must be ``Float64``. + spcd : pl.Expr + Species code column expression used to select the Model 2 ``k`` + constant via the ``SPCD < 300`` rule. + wdsg : pl.Expr + Wood specific gravity column expression (``REF_SPECIES.WOOD_SPGR_GREENVOL_DRYWT``). + Used only by Model 5. + + Returns + ------- + pl.Expr + An expression that evaluates to the predicted quantity (volume or + biomass) in source-table units (cubic feet or pounds). Call + ``.alias("v_wood_ib")`` (or similar) to name the output column. + """ + k = ( + pl.when(spcd >= _HARDWOOD_SPCD_THRESHOLD) + .then(_K_HARDWOOD) + .otherwise(_K_SOFTWOOD) + ) + return ( + pl.when(model == 1) + .then(a * d.pow(b) * h.pow(c)) + .when(model == 2) + .then(a * k.pow(b - b1) * d.pow(b1) * h.pow(c)) + .when(model == 4) + .then(a * d.pow(b) * h.pow(c) * (-b1 * d).exp()) + .when(model == 5) + .then(a * d.pow(b) * h.pow(c) * wdsg) + .otherwise(None) + ) + + +def _join_and_eval_component( + trees: pl.LazyFrame, + spcd_table: pl.DataFrame, + jen_table: pl.DataFrame, + div_table: pl.DataFrame, + out_col: str, + *, + has_division: bool, +) -> pl.LazyFrame: + """Join a LazyFrame to the (division, species-level, Jenkins) coefficient + triple for one component and evaluate the NSVB biomass expression. + + Implementation of NSVB lookup precedence Levels 2 → 3 → 4 as polars + joins: + + 1. If ``has_division`` is True: left-join ``div_table`` on + ``(SPCD, DIVISION)`` (Level 2). + 2. Left-join ``spcd_table`` on ``SPCD`` (species-level fallback, Level 3). + 3. Left-join ``jen_table`` on ``JENKINS_SPGRPCD`` (Jenkins fallback, + Level 4). + 4. Coalesce the three coefficient rows in precedence order: + division → species-level → Jenkins. + 5. Evaluate :func:`nsvb_biomass_expr` to produce ``out_col``. + 6. Drop the temporary coefficient columns. + + When ``has_division`` is False (the trees frame has no ``DIVISION`` + column — backward-compatible path for synthetic tests and for callers + that haven't yet wired the ``PLOTGEOM.ECOSUBCD`` join), the division + join is skipped entirely and the behavior matches the old 2-way + coalesce exactly. + + Parameters + ---------- + trees : pl.LazyFrame + Input frame with at least ``SPCD``, ``DIA``, ``HT``, ``WDSG``, + ``JENKINS_SPGRPCD`` columns. If ``has_division`` is True, must also + have a ``DIVISION`` column (Utf8/String, nullable). + spcd_table : pl.DataFrame + Species-level lookup from + :func:`pyfia.carbon.nsvb.coefficients.build_species_level_lookup` + with columns ``(SPCD, model, a, b, b1, c)``. + jen_table : pl.DataFrame + Jenkins fallback from + :func:`pyfia.carbon.nsvb.coefficients.build_jenkins_lookup` with + columns ``(JENKINS_SPGRPCD, model, a, b, b1, c)``. + div_table : pl.DataFrame + DIVISION-specific lookup from + :func:`pyfia.carbon.nsvb.coefficients.build_division_lookup` with + columns ``(SPCD, DIVISION, model, a, b, b1, c)``. Only consulted + when ``has_division`` is True. + out_col : str + Name for the output column (e.g., ``"v_wood_ib"``). + has_division : bool + Whether the caller has populated a ``DIVISION`` column on the trees + frame. When False, skip the division join entirely. + + Returns + ------- + pl.LazyFrame + The input frame with ``out_col`` appended. Temporary coefficient + columns are dropped before return. + """ + # Rename coefficient columns on each side so they don't collide with the + # trees frame or with each other. + spcd_lf = spcd_table.lazy().rename( + { + "model": "_model_s", + "a": "_a_s", + "b": "_b_s", + "b1": "_b1_s", + "c": "_c_s", + } + ) + jen_lf = jen_table.lazy().rename( + { + "model": "_model_j", + "a": "_a_j", + "b": "_b_j", + "b1": "_b1_j", + "c": "_c_j", + } + ) + + if has_division: + div_lf = div_table.lazy().rename( + { + "model": "_model_d", + "a": "_a_d", + "b": "_b_d", + "b1": "_b1_d", + "c": "_c_d", + } + ) + trees = trees.join(div_lf, on=["SPCD", "DIVISION"], how="left") + + trees = trees.join(spcd_lf, on="SPCD", how="left") + trees = trees.join(jen_lf, on="JENKINS_SPGRPCD", how="left") + + # Coalesce: division (Level 2) → species-level (Level 3) → Jenkins (Level 4). + # When has_division=False, the _*_d columns don't exist and we fall back + # to the 2-way coalesce matching the original implementation. + if has_division: + model_expr = pl.coalesce( + pl.col("_model_d"), pl.col("_model_s"), pl.col("_model_j") + ) + a_expr = pl.coalesce(pl.col("_a_d"), pl.col("_a_s"), pl.col("_a_j")) + b_expr = pl.coalesce(pl.col("_b_d"), pl.col("_b_s"), pl.col("_b_j")) + b1_expr = pl.coalesce(pl.col("_b1_d"), pl.col("_b1_s"), pl.col("_b1_j")) + c_expr = pl.coalesce(pl.col("_c_d"), pl.col("_c_s"), pl.col("_c_j")) + else: + model_expr = pl.coalesce(pl.col("_model_s"), pl.col("_model_j")) + a_expr = pl.coalesce(pl.col("_a_s"), pl.col("_a_j")) + b_expr = pl.coalesce(pl.col("_b_s"), pl.col("_b_j")) + b1_expr = pl.coalesce(pl.col("_b1_s"), pl.col("_b1_j")) + c_expr = pl.coalesce(pl.col("_c_s"), pl.col("_c_j")) + + trees = trees.with_columns( + nsvb_biomass_expr( + model=model_expr, + a=a_expr, + b=b_expr, + b1=b1_expr, + c=c_expr, + d=pl.col("DIA").cast(pl.Float64), + h=pl.col("HT").cast(pl.Float64), + spcd=pl.col("SPCD"), + wdsg=pl.col("WDSG").cast(pl.Float64), + ).alias(out_col) + ) + + drop_cols = [ + "_model_s", + "_a_s", + "_b_s", + "_b1_s", + "_c_s", + "_model_j", + "_a_j", + "_b_j", + "_b1_j", + "_c_j", + ] + if has_division: + drop_cols.extend(["_model_d", "_a_d", "_b_d", "_b1_d", "_c_d"]) + return trees.drop(drop_cols) + + +def compute_nsvb_biomass( + trees: pl.LazyFrame, + lookup: VectorizedLookupTables | None = None, +) -> pl.LazyFrame: + """Vectorized NSVB live-tree biomass pipeline (the production data path). + + Executes the same 10-step pipeline as :func:`predict_tree_biomass` + (5 component predictions → cull adjustment → harmonization) as a + sequence of polars joins and expressions over a LazyFrame, with no + per-tree Python dispatch. + + The hardwood/softwood classification used for the cull density + proportion and the Model 2 ``k`` constant is derived from the + ``SPCD < 300`` rule to stay consistent with :func:`_model_k` and to + sidestep the S10a misclassification of SPCD=10 — see + ``pyfia/carbon/__init__.py`` "Items deferred from PR 1 review". + + Parameters + ---------- + trees : pl.LazyFrame + Input frame with at least the following columns: + + - ``SPCD`` (Int): FIA species code + - ``DIA`` (Float): diameter at breast height (inches, must be >= 1.0) + - ``HT`` (Float): total tree height (feet) + - ``CULL`` (Float, nullable): cull percentage (0-100). Null is + treated as 0. + - ``WDSG`` (Float): wood specific gravity + (``REF_SPECIES.WOOD_SPGR_GREENVOL_DRYWT``) + - ``JENKINS_SPGRPCD`` (Int): Jenkins species group for the Level 4 + fallback (``REF_SPECIES.JENKINS_SPGRPCD``) + + Optional: + + - ``DIVISION`` (Utf8, nullable): Bailey ecoprovince DIVISION code + (e.g., ``"230"``, ``"M230"``, computed from ``PLOT.ECOSUBCD`` via + :func:`pyfia.carbon.nsvb.coefficients.ecosubcd_to_division`). + When present, the orchestrator activates Level 2 of the NSVB + lookup precedence: the ``(SPCD, DIVISION)`` lookup runs first, + falling through to species-level (Level 3) and Jenkins (Level 4) + per-row via coalesce. When absent, only Levels 3 + 4 are used + (backward-compatible with callers that haven't wired the + PLOTGEOM join). + + Additional columns (grouping variables, plot identifiers, etc.) + pass through untouched. + lookup : VectorizedLookupTables, optional + Pre-built coefficient lookup bundle from + :func:`pyfia.carbon.nsvb.coefficients.get_vectorized_lookup_tables`. + If omitted, fetches the cached process-level bundle. + + Returns + ------- + pl.LazyFrame + The input frame with six new columns appended: + + - ``v_wood_ib`` (Float, cu ft): total stem inside-bark wood volume + - ``v_bark`` (Float, cu ft): total stem bark volume + - ``w_wood`` (Float, lb): harmonized stem wood biomass + - ``w_bark`` (Float, lb): harmonized stem bark biomass + - ``w_branch`` (Float, lb): harmonized branch biomass + - ``agb`` (Float, lb): harmonized total above-ground biomass + (equals ``w_wood + w_bark + w_branch`` by construction) + + All other input columns pass through. + """ + if lookup is None: + from pyfia.carbon.nsvb.coefficients import get_vectorized_lookup_tables + + lookup = get_vectorized_lookup_tables() + + # Probe the schema once so the 5 per-component joins don't each re-collect + # it. DIVISION column detection activates the Level 2 lookup path. + has_division = "DIVISION" in trees.collect_schema().names() + + # Step 1 — Total stem inside-bark wood volume (cubic feet). + trees = _join_and_eval_component( + trees, + lookup.volib_spcd, + lookup.volib_jen, + lookup.volib_div, + "v_wood_ib", + has_division=has_division, + ) + + # Step 2 — Total stem bark volume (cubic feet). + trees = _join_and_eval_component( + trees, + lookup.volbk_spcd, + lookup.volbk_jen, + lookup.volbk_div, + "v_bark", + has_division=has_division, + ) + + # Step 5 — Stem bark biomass (lb). + trees = _join_and_eval_component( + trees, + lookup.bark_bio_spcd, + lookup.bark_bio_jen, + lookup.bark_bio_div, + "_w_bark_pre", + has_division=has_division, + ) + + # Step 6 — Branch biomass (lb). + trees = _join_and_eval_component( + trees, + lookup.branch_bio_spcd, + lookup.branch_bio_jen, + lookup.branch_bio_div, + "_w_branch_pre", + has_division=has_division, + ) + + # Step 7 — Directly-predicted total AGB (lb). + trees = _join_and_eval_component( + trees, + lookup.total_agb_spcd, + lookup.total_agb_jen, + lookup.total_agb_div, + "_agb_predicted", + has_division=has_division, + ) + + # Step 3-4 — Convert wood volume to weight, with a cull-reduced variant. + # DECAYCD=3 wood density proportion: 0.92 softwood, 0.54 hardwood. The + # split on SPCD<300 is consistent with _model_k and sidesteps the SPCD=10 + # S10a misclassification (SPCD=10 is softwood under this rule). + trees = trees.with_columns( + [ + pl.col("CULL").fill_null(0.0).cast(pl.Float64).alias("_cull"), + pl.when(pl.col("SPCD") >= _HARDWOOD_SPCD_THRESHOLD) + .then(pl.lit(_CULL_DENS_PROP["hardwood"])) + .otherwise(pl.lit(_CULL_DENS_PROP["softwood"])) + .alias("_dens_prop"), + ] + ) + trees = trees.with_columns( + [ + (pl.col("v_wood_ib") * pl.col("WDSG").cast(pl.Float64) * 62.4).alias( + "_w_wood_gross" + ), + ( + pl.col("v_wood_ib") + * (1.0 - pl.col("_cull") / 100.0 * (1.0 - pl.col("_dens_prop"))) + * pl.col("WDSG").cast(pl.Float64) + * 62.4 + ).alias("_w_wood_red"), + ] + ) + + # Step 8 — Cull-reduction factor. Matches the scalar path: bark/branch use + # their gross values in both the numerator and denominator (only wood is + # cull-reduced for live trees per worked example lines 870-880). + trees = trees.with_columns( + [ + ( + pl.col("_w_wood_gross") + + pl.col("_w_bark_pre") + + pl.col("_w_branch_pre") + ).alias("_comp_gross_sum"), + ( + pl.col("_w_wood_red") + pl.col("_w_bark_pre") + pl.col("_w_branch_pre") + ).alias("_comp_red_sum"), + ] + ) + trees = trees.with_columns( + [ + pl.when(pl.col("_comp_gross_sum") <= 0) + .then(pl.lit(0.0)) + .otherwise(pl.col("_comp_red_sum") / pl.col("_comp_gross_sum")) + .alias("_agb_reduce"), + ] + ) + + # Step 9 — Reduce the directly-predicted AGB by the cull factor. + trees = trees.with_columns( + [(pl.col("_agb_predicted") * pl.col("_agb_reduce")).alias("_agb_pred_red")] + ) + + # Step 10 — Harmonize (wood, bark, branch) so they sum to _agb_pred_red + # while preserving the relative component ratios. Matches the scalar + # harmonize_components exactly, including the degenerate + # (component_sum <= 0) fallback: all AGB in wood, zeros elsewhere. + trees = trees.with_columns( + [ + pl.when(pl.col("_comp_red_sum") > 0) + .then( + pl.col("_agb_pred_red") + * pl.col("_w_wood_red") + / pl.col("_comp_red_sum") + ) + .otherwise(pl.col("_agb_pred_red")) + .alias("w_wood"), + pl.when(pl.col("_comp_red_sum") > 0) + .then( + pl.col("_agb_pred_red") + * pl.col("_w_bark_pre") + / pl.col("_comp_red_sum") + ) + .otherwise(pl.lit(0.0)) + .alias("w_bark"), + pl.when(pl.col("_comp_red_sum") > 0) + .then( + pl.col("_agb_pred_red") + * pl.col("_w_branch_pre") + / pl.col("_comp_red_sum") + ) + .otherwise(pl.lit(0.0)) + .alias("w_branch"), + ] + ) + trees = trees.with_columns( + [(pl.col("w_wood") + pl.col("w_bark") + pl.col("w_branch")).alias("agb")] + ) + + # Drop all temporary columns so the caller sees only the public outputs. + return trees.drop( + [ + "_cull", + "_dens_prop", + "_w_wood_gross", + "_w_wood_red", + "_w_bark_pre", + "_w_branch_pre", + "_agb_predicted", + "_comp_gross_sum", + "_comp_red_sum", + "_agb_reduce", + "_agb_pred_red", + ] + ) + + +def compute_nsvb_dead_biomass( + trees: pl.LazyFrame, + decay_props: pl.DataFrame, + lookup: VectorizedLookupTables | None = None, + cr_prop_table: pl.DataFrame | None = None, +) -> pl.LazyFrame: + """Vectorized NSVB standing-dead biomass pipeline. + + Mirrors :func:`compute_nsvb_biomass` but applies the FIADB + ``REF_TREE_DECAY_PROP`` reductions (``DENSITY_PROP``, ``BARK_LOSS_PROP``, + ``BRANCH_LOSS_PROP``) by hardwood/softwood × ``DECAYCD`` *instead* of + the live-tree ``CULL`` reduction. Per FIADB User Guide v9.1 Appendix K + "Cull" subsection: "For dead tree biomass, no adjustments for TREE.CULL + or other types of cull are made." So ``TREE.CULL`` is intentionally + ignored on this path even when populated. + + **Broken-top corrections** (Phase 2.5): when the trees frame has an + ``ACTUALHT`` column and ``cr_prop_table`` is provided, trees with + ``ACTUALHT < HT`` receive two adjustments before the decay reductions: + + - *Branch biomass* is multiplied by ``Broken_crn_prop``, the fraction + of the intact crown remaining below the break, computed from the + mean intact crown ratio in ``REF_TREE_STND_DEAD_CR_PROP`` (Table S11) + keyed on Bailey ecoregion province × hardwood/softwood. + - *Wood and bark* are reduced by ``ACTUALHT / HT``, a linear + approximation of the stem volume ratio below the break. This + replaces the Model 6 (Schumacher-Hall) volume-ratio computation + that FIADB uses for sub-stem partitioning, which is not implemented. + + Parameters + ---------- + trees : pl.LazyFrame + Input frame with at least: ``SPCD``, ``DIA``, ``HT``, ``DECAYCD``, + ``WDSG``, ``JENKINS_SPGRPCD``. + + Optional: + + - ``DIVISION`` (Utf8, nullable): activates Level 2 NSVB lookup. + - ``ACTUALHT`` (Float, nullable): actual measured height for + broken-top trees. When present and ``cr_prop_table`` is not None, + trees with ``ACTUALHT < HT`` receive broken-top corrections. + - ``ECOSUBCD`` (Utf8, nullable): used to derive Bailey ecoregion + province for the ``REF_TREE_STND_DEAD_CR_PROP`` lookup. When + absent, all trees fall back to the UNDEFINED mean crown ratio. + decay_props : pl.DataFrame + FIADB ``REF_TREE_DECAY_PROP`` lookup with columns ``(hw_sw, + DECAYCD, DENSITY_PROP, BARK_LOSS_PROP, BRANCH_LOSS_PROP)``. + lookup : VectorizedLookupTables, optional + Pre-built coefficient lookup bundle. If omitted, uses the cached + process-level bundle. + cr_prop_table : pl.DataFrame, optional + Table S11 mean crown ratios from + :func:`pyfia.carbon.nsvb.carbon_fractions.load_dead_cr_prop_df`. + Columns ``(ECOPROV, hw_sw, CR_MEAN)``. When None, broken-top + corrections are skipped (Phase 2 baseline behavior). + + Returns + ------- + pl.LazyFrame + The input frame with new columns: ``v_wood_ib``, ``v_bark``, + ``w_wood``, ``w_bark``, ``w_branch``, ``agb``. For broken-top + trees, volumes and biomass reflect the remaining stem/crown below + the break point. + """ + if lookup is None: + from pyfia.carbon.nsvb.coefficients import get_vectorized_lookup_tables + + lookup = get_vectorized_lookup_tables() + + has_division = "DIVISION" in trees.collect_schema().names() + + # Steps 1-2 — Stem wood and stem bark volumes (cu ft). + trees = _join_and_eval_component( + trees, + lookup.volib_spcd, + lookup.volib_jen, + lookup.volib_div, + "v_wood_ib", + has_division=has_division, + ) + trees = _join_and_eval_component( + trees, + lookup.volbk_spcd, + lookup.volbk_jen, + lookup.volbk_div, + "v_bark", + has_division=has_division, + ) + + # Steps 4-5 — Bark and branch biomass (lb), gross intact predictions. + trees = _join_and_eval_component( + trees, + lookup.bark_bio_spcd, + lookup.bark_bio_jen, + lookup.bark_bio_div, + "_w_bark_pre", + has_division=has_division, + ) + trees = _join_and_eval_component( + trees, + lookup.branch_bio_spcd, + lookup.branch_bio_jen, + lookup.branch_bio_div, + "_w_branch_pre", + has_division=has_division, + ) + + # Step 6 — Directly-predicted total AGB (lb), intact. + trees = _join_and_eval_component( + trees, + lookup.total_agb_spcd, + lookup.total_agb_jen, + lookup.total_agb_div, + "_agb_predicted", + has_division=has_division, + ) + + # Step 3 — Convert wood volume to gross weight. + trees = trees.with_columns( + [ + (pl.col("v_wood_ib") * pl.col("WDSG").cast(pl.Float64) * 62.4).alias( + "_w_wood_gross" + ), + ] + ) + + # ----- Broken-top corrections (Phase 2.5) ----- + # For trees with ACTUALHT < HT, reduce branch biomass by the crown + # proportion remaining below the break, and reduce wood/bark by a + # linear volume ratio ACTUALHT/HT. Corrections applied BEFORE the + # decay reductions so both adjustments compound. The intact gross sum + # is saved BEFORE the correction so AGBReduce = broken_decayed / intact + # captures both the broken-top and decay reductions against the intact + # AGB prediction. + schema_names = trees.collect_schema().names() + has_actualht = "ACTUALHT" in schema_names + _has_broken_top_correction = False + if has_actualht and cr_prop_table is not None: + has_ecosubcd = "ECOSUBCD" in schema_names + + # Derive Bailey ecoregion province from ECOSUBCD. Province is the + # 3-digit numeric prefix (with optional M) — NOT the same as + # DIVISION, which replaces the last digit with 0. + if has_ecosubcd: + ecoprov_expr = ( + pl.when(pl.col("ECOSUBCD").is_null() | (pl.col("ECOSUBCD") == "")) + .then(pl.lit("UNDEFINED")) + .when(pl.col("ECOSUBCD").str.to_uppercase().str.starts_with("M")) + .then( + pl.lit("M") + pl.col("ECOSUBCD").str.to_uppercase().str.slice(1, 3) + ) + .otherwise(pl.col("ECOSUBCD").str.to_uppercase().str.slice(0, 3)) + ) + else: + ecoprov_expr = pl.lit("UNDEFINED") + + hw_sw_expr = ( + pl.when(pl.col("SPCD") >= _HARDWOOD_SPCD_THRESHOLD) + .then(pl.lit("hardwood")) + .otherwise(pl.lit("softwood")) + ) + + _has_broken_top_correction = True + + # Save intact gross component sum BEFORE applying corrections. + # AGBReduce will use this as the denominator so that it captures + # both the broken-top and decay reductions against the intact + # AGB prediction from S8a/S8b. + trees = trees.with_columns( + ( + pl.col("_w_wood_gross") + + pl.col("_w_bark_pre") + + pl.col("_w_branch_pre") + ).alias("_comp_gross_sum_intact") + ) + + trees = trees.with_columns( + [ + ecoprov_expr.alias("_ecoprov"), + hw_sw_expr.alias("_bt_hw_sw"), + ] + ) + + # Join CR_MEAN from Table S11 on (ECOPROV, hw_sw). + cr_lf = cr_prop_table.lazy().rename( + {"ECOPROV": "_ecoprov", "hw_sw": "_bt_hw_sw", "CR_MEAN": "_cr_mean"} + ) + trees = trees.join(cr_lf, on=["_ecoprov", "_bt_hw_sw"], how="left") + + # Fall back to UNDEFINED CR_MEAN for unmatched provinces. + undef = cr_prop_table.filter(pl.col("ECOPROV") == "UNDEFINED") + undef_sw = float(undef.filter(pl.col("hw_sw") == "softwood")["CR_MEAN"][0]) + undef_hw = float(undef.filter(pl.col("hw_sw") == "hardwood")["CR_MEAN"][0]) + trees = trees.with_columns( + pl.col("_cr_mean") + .fill_null( + pl.when(pl.col("SPCD") >= _HARDWOOD_SPCD_THRESHOLD) + .then(pl.lit(undef_hw)) + .otherwise(pl.lit(undef_sw)) + ) + .alias("_cr_mean") + ) + + # Identify broken-top trees: ACTUALHT < HT and ACTUALHT not null. + actualht = pl.col("ACTUALHT").cast(pl.Float64, strict=False) + ht = pl.col("HT").cast(pl.Float64) + is_broken = actualht.is_not_null() & (actualht < ht) & (actualht > 0) + + # Appendix K crown-proportion formula: + # CRprop_HT = (HT - ACTUALHT * (1 - CR/100)) / HT + # Broken_crn_prop = max(0, (ACTUALHT - (1-CRprop_HT)*HT) / (CRprop_HT*HT)) + cr_frac = pl.col("_cr_mean") / 100.0 + crprop_ht = (ht - actualht * (1.0 - cr_frac)) / ht + + # Guard against zero/negative CRprop_HT (degenerate cases where + # ACTUALHT ≈ 0 or CR_MEAN ≈ 100). + broken_crn = ((actualht - (1.0 - crprop_ht) * ht) / (crprop_ht * ht)).clip( + lower_bound=0.0 + ) + + # Volume ratio: paraboloid taper approximation of stem volume below + # break. Real stems have a shape between a paraboloid (exponent 2/3) + # and a cone (exponent 1). The 2/3 exponent captures the fact that + # the wider lower stem contains a disproportionately large fraction of + # total stem volume, preventing the over-correction a linear ratio + # produces. This replaces the Schumacher-Hall Model 6 volume-ratio + # calculation that FIADB uses for sub-stem partitioning. + vol_ratio = (actualht / ht).pow(2.0 / 3.0) + + trees = trees.with_columns( + [ + pl.when(is_broken).then(broken_crn).otherwise(1.0).alias("_crn_prop"), + pl.when(is_broken).then(vol_ratio).otherwise(1.0).alias("_vol_ratio"), + ] + ) + + # Apply broken-top reductions to gross components. + trees = trees.with_columns( + [ + (pl.col("_w_wood_gross") * pl.col("_vol_ratio")).alias("_w_wood_gross"), + (pl.col("_w_bark_pre") * pl.col("_vol_ratio")).alias("_w_bark_pre"), + (pl.col("_w_branch_pre") * pl.col("_crn_prop")).alias("_w_branch_pre"), + (pl.col("v_wood_ib") * pl.col("_vol_ratio")).alias("v_wood_ib"), + (pl.col("v_bark") * pl.col("_vol_ratio")).alias("v_bark"), + ] + ) + + trees = trees.drop( + ["_ecoprov", "_bt_hw_sw", "_cr_mean", "_crn_prop", "_vol_ratio"] + ) + + # Step 7 — Join the FIADB REF_TREE_DECAY_PROP table on (hw_sw, DECAYCD). + # The hw_sw column is derived from the SPCD<300 rule to stay consistent + # with _model_k and the live-tree pipeline (sidesteps the SPCD=10 S10a + # misclassification). + decay_lf = decay_props.lazy().rename( + { + "hw_sw": "_hw_sw", + "DECAYCD": "_decay_join", + "DENSITY_PROP": "_density_prop", + "BARK_LOSS_PROP": "_bark_loss_prop", + "BRANCH_LOSS_PROP": "_branch_loss_prop", + } + ) + trees = trees.with_columns( + [ + pl.when(pl.col("SPCD") >= _HARDWOOD_SPCD_THRESHOLD) + .then(pl.lit("hardwood")) + .otherwise(pl.lit("softwood")) + .alias("_hw_sw"), + pl.col("DECAYCD").cast(pl.Int64).alias("_decay_join"), + ] + ) + trees = trees.join(decay_lf, on=["_hw_sw", "_decay_join"], how="left") + + # Step 8 — Apply per-component decay reductions. + trees = trees.with_columns( + [ + (pl.col("_w_wood_gross") * pl.col("_density_prop")).alias("_w_wood_dead"), + (pl.col("_w_bark_pre") * pl.col("_bark_loss_prop")).alias("_w_bark_dead"), + (pl.col("_w_branch_pre") * pl.col("_branch_loss_prop")).alias( + "_w_branch_dead" + ), + ] + ) + + # Step 9 — AGB reduction factor and reduced predicted AGB. + # When broken-top corrections were applied, the denominator must use + # the intact gross sum (saved before the corrections) so that + # AGBReduce = broken_decayed / intact captures both the broken-top + # reduction and the decay reduction against the intact AGB prediction. + trees = trees.with_columns( + [ + ( + pl.col("_w_wood_dead") + + pl.col("_w_bark_dead") + + pl.col("_w_branch_dead") + ).alias("_comp_dead_sum"), + ] + ) + if _has_broken_top_correction: + gross_denom = pl.col("_comp_gross_sum_intact") + else: + trees = trees.with_columns( + ( + pl.col("_w_wood_gross") + + pl.col("_w_bark_pre") + + pl.col("_w_branch_pre") + ).alias("_comp_gross_sum"), + ) + gross_denom = pl.col("_comp_gross_sum") + trees = trees.with_columns( + pl.when(gross_denom <= 0) + .then(pl.lit(0.0)) + .otherwise(pl.col("_comp_dead_sum") / gross_denom) + .alias("_agb_reduce"), + ) + trees = trees.with_columns( + (pl.col("_agb_predicted") * pl.col("_agb_reduce")).alias("_agb_pred_dead") + ) + + # Step 10 — Harmonize dead components against the reduced predicted AGB. + # Same proportional redistribution as the live path; see + # ``harmonize_components`` for the scalar reference. + trees = trees.with_columns( + [ + pl.when(pl.col("_comp_dead_sum") > 0) + .then( + pl.col("_agb_pred_dead") + * pl.col("_w_wood_dead") + / pl.col("_comp_dead_sum") + ) + .otherwise(pl.col("_agb_pred_dead")) + .alias("w_wood"), + pl.when(pl.col("_comp_dead_sum") > 0) + .then( + pl.col("_agb_pred_dead") + * pl.col("_w_bark_dead") + / pl.col("_comp_dead_sum") + ) + .otherwise(pl.lit(0.0)) + .alias("w_bark"), + pl.when(pl.col("_comp_dead_sum") > 0) + .then( + pl.col("_agb_pred_dead") + * pl.col("_w_branch_dead") + / pl.col("_comp_dead_sum") + ) + .otherwise(pl.lit(0.0)) + .alias("w_branch"), + ] + ) + trees = trees.with_columns( + [(pl.col("w_wood") + pl.col("w_bark") + pl.col("w_branch")).alias("agb")] + ) + + drop_cols = [ + "_hw_sw", + "_decay_join", + "_density_prop", + "_bark_loss_prop", + "_branch_loss_prop", + "_w_wood_gross", + "_w_bark_pre", + "_w_branch_pre", + "_agb_predicted", + "_w_wood_dead", + "_w_bark_dead", + "_w_branch_dead", + "_comp_dead_sum", + "_agb_reduce", + "_agb_pred_dead", + ] + if _has_broken_top_correction: + drop_cols.append("_comp_gross_sum_intact") + else: + drop_cols.append("_comp_gross_sum") + return trees.drop(drop_cols) diff --git a/src/pyfia/carbon/soil_organic.py b/src/pyfia/carbon/soil_organic.py new file mode 100644 index 00000000..99e2cdc6 --- /dev/null +++ b/src/pyfia/carbon/soil_organic.py @@ -0,0 +1,417 @@ +""" +Soil organic carbon estimation from FIADB condition-level attributes. + +Soil organic carbon (SOC) — the organic carbon stored in mineral soil to +a depth of 1 metre — is estimated in the FIADB using the Domke et al. +(2017) model, which relates SOC density +to soil taxonomic order, clay content, and climate variables. + +This estimator reads ``COND.CARBON_SOIL_ORG`` (short tons per acre), +then runs it through pyFIA's post-stratified aggregation pipeline to +produce per-acre and population estimates that match EVALIDator. + +Soil organic carbon has no above-ground / below-ground split; the single +``CARBON_SOIL_ORG`` column represents the total pool. + +National magnitude: ~20,400 Tg C total SOC on forestland, roughly 52 % +of total forest ecosystem carbon and the single largest pool +(GTR-NRS-154, Table 2). + +Public API: :func:`soil_organic`. See its docstring for parameters, +examples, and the pool semantics. + +References +---------- +- Domke, G.M.; Perry, C.H.; Walters, B.F.; et al. (2017). Toward + inventory-based estimates of soil organic carbon in forests of the + United States. Ecological Applications, 27(4), 1223-1235. +- Woodall, C.W. et al. (2015). GTR-NRS-154 (FCAF methodology blueprint). +- USEPA (2024). NGHGI Annex 3.13. +""" + +from __future__ import annotations + +import logging + +import polars as pl + +from ..core import FIA +from ..estimation.base import AggregationResult, BaseEstimator +from ..estimation.columns import get_cond_columns as _get_cond_columns +from ..estimation.utils import ( + ensure_evalid_set, + ensure_fia_instance, + validate_aggregation_result, + validate_required_columns, +) + +logger = logging.getLogger(__name__) + + +class SoilOrganicEstimator(BaseEstimator): + """Soil organic carbon estimator. + + Reads pre-computed ``COND.CARBON_SOIL_ORG`` from the FIADB and + aggregates via the standard post-stratified estimation pipeline. + + This is a **condition-level** estimator — there is no tree-level data. + The TREE table is not loaded; data comes from ``COND x PLOT`` only. + The adjustment factor is ``ADJ_FACTOR_SUBP`` (subplot-level), + matching FIADB/EVALIDator conventions for condition-level carbon + attributes. + """ + + _estimator_label = "SoilOrganic" + + # ------------------------------------------------------------------ + # Table / column requirements + # ------------------------------------------------------------------ + + def get_required_tables(self) -> list[str]: + return ["COND", "PLOT", "POP_PLOT_STRATUM_ASSGN", "POP_STRATUM"] + + def get_tree_columns(self) -> list[str]: + return [] + + def get_cond_columns(self) -> list[str]: + cols = _get_cond_columns( + land_type=self.config.get("land_type", "forest"), + grp_by=self.config.get("grp_by"), + include_prop_basis=False, + ) + if "CARBON_SOIL_ORG" not in cols: + cols.append("CARBON_SOIL_ORG") + return cols + + # ------------------------------------------------------------------ + # Override apply_filters for condition-level data + # ------------------------------------------------------------------ + + def apply_filters(self, data: pl.LazyFrame) -> pl.LazyFrame: + """Apply condition/area filters only (no tree-type filtering).""" + from ..filtering import apply_area_filters, get_land_domain_indicator + + columns = data.collect_schema().names() + + if self.config.get("area_domain"): + data = apply_area_filters(data, area_domain=self.config["area_domain"]) + + land_type = self.config.get("land_type", "forest") + if land_type and land_type != "all" and "COND_STATUS_CD" in columns: + data = data.filter(get_land_domain_indicator(land_type)) + + return data + + # ------------------------------------------------------------------ + # Core estimation logic + # ------------------------------------------------------------------ + + def calculate_values(self, data: pl.LazyFrame) -> pl.LazyFrame: + """Alias CARBON_SOIL_ORG to CARBON_ACRE.""" + cond_carbon = pl.col("CARBON_SOIL_ORG").cast(pl.Float64).fill_null(0.0) + return data.with_columns(cond_carbon.alias("CARBON_ACRE")) + + # ------------------------------------------------------------------ + # Aggregation — condition-level with ADJ_FACTOR_SUBP + # ------------------------------------------------------------------ + + def aggregate_results(self, data: pl.LazyFrame | None) -> AggregationResult: + if data is None: + return AggregationResult( + results=pl.DataFrame(), + plot_tree_data=pl.DataFrame(), + group_cols=[], + ) + + validate_required_columns( + data, ["PLT_CN", "CARBON_ACRE"], "soil organic carbon data" + ) + + strat_data = self._get_stratification_data() + data_with_strat = data.join(strat_data, on="PLT_CN", how="inner") + + # Condition-level attribute: CARBON_ACRE is a density (tons/acre). + # The two-stage pipeline denominator is sum(CONDPROP_UNADJ * EXPNS), + # so the numerator must include CONDPROP_UNADJ to give the + # condition's contribution proportional to its area on the plot. + # ADJ_FACTOR_SUBP corrects for nonresponse at the subplot level. + data_with_strat = data_with_strat.with_columns( + pl.col("ADJ_FACTOR_SUBP").cast(pl.Float64).alias("ADJ_FACTOR") + ) + + data_with_strat = data_with_strat.with_columns( + ( + pl.col("CARBON_ACRE") + * pl.col("CONDPROP_UNADJ").cast(pl.Float64) + * pl.col("ADJ_FACTOR") + ).alias("CARBON_ADJ") + ) + + group_cols = self._setup_grouping() + + plot_tree_data, data_with_strat = self._preserve_plot_tree_data( + data_with_strat, + metric_cols=["CARBON_ADJ"], + group_cols=group_cols, + ) + + results = self._apply_two_stage_aggregation( + data_with_strat=data_with_strat, + metric_mappings={"CARBON_ADJ": "CONDITION_CARBON"}, + group_cols=group_cols, + use_grm_adjustment=False, + ) + + if not self.config.get("totals", True): + if "CARBON_TOTAL" in results.columns: + results = results.drop("CARBON_TOTAL") + + return AggregationResult( + results=results, + plot_tree_data=plot_tree_data, + group_cols=group_cols, + ) + + # ------------------------------------------------------------------ + # Variance and output formatting + # ------------------------------------------------------------------ + + def calculate_variance(self, agg_result: AggregationResult) -> pl.DataFrame: + validate_aggregation_result(agg_result, self._estimator_label) + metric_configs = [ + { + "adjusted_col": "CARBON_ADJ", + "acre_se_col": "CARBON_ACRE_SE", + "total_se_col": "CARBON_TOTAL_SE", + }, + ] + return self._calculate_variance_for_metrics(agg_result, metric_configs) + + def format_output(self, results: pl.DataFrame) -> pl.DataFrame: + year = self._extract_evaluation_year() + results = results.with_columns(pl.lit(year).alias("YEAR")) + results = results.with_columns(pl.lit("TOTAL").alias("POOL")) + + col_order = [ + "YEAR", + "POOL", + "CARBON_ACRE", + "CARBON_TOTAL", + "CARBON_ACRE_SE", + "CARBON_TOTAL_SE", + "N_PLOTS", + "N_TREES", + ] + + for col in results.columns: + if col not in col_order: + col_order.insert(1, col) + + final_cols = [col for col in col_order if col in results.columns] + return results.select(final_cols) + + +# ====================================================================== +# Public API +# ====================================================================== + + +def soil_organic( + db: str | FIA, + pool: str = "total", + grp_by: str | list[str] | None = None, + land_type: str = "forest", + area_domain: str | None = None, + plot_domain: str | None = None, + totals: bool = True, + variance: bool = False, + most_recent: bool = False, +) -> pl.DataFrame: + """ + Estimate soil organic carbon from FIA data. + + Soil organic carbon (SOC) encompasses all organic carbon stored in + mineral soil to a depth of 1 metre on forestland. Carbon density is + estimated from the Domke et al. (2017) model, parameterised on soil + taxonomic order, clay content, and climate variables. The FIADB + pre-computes condition-level carbon density and stores it in + ``COND.CARBON_SOIL_ORG``. + + Nationally, SOC totals ~20,400 Tg C, representing ~52 % of total + forest ecosystem carbon — the single largest pool (GTR-NRS-154, + Table 2). Per-acre values are typically 15-40 short tons/acre + depending on soil type, climate, and region. + + Parameters + ---------- + db : str | FIA + Database connection or path to FIA database. Can be either a path + string to a DuckDB/SQLite file or an existing FIA connection object. + pool : {'total'}, default 'total' + Carbon pool to estimate. Soil organic carbon has no above-ground / + below-ground split — only ``'total'`` is accepted. + grp_by : str or list of str, optional + Column name(s) to group results by. Can be any column from the + FIA COND or PLOT tables. Common grouping columns include: + + - 'FORTYPCD': Forest type code + - 'FORTYPGRPCD': Forest type group code + - 'OWNGRPCD': Ownership group + - 'STATECD': State FIPS code + - 'COUNTYCD': County code + + For complete column descriptions, see USDA FIA Database User Guide. + land_type : {'forest', 'timber', 'all'}, default 'forest' + Land type to include in estimation: + + - 'forest': All forestland + - 'timber': Productive timberland only (unreserved, productive) + - 'all': All land conditions + area_domain : str, optional + SQL-like filter expression for area/condition-level filtering. + Example: ``"OWNGRPCD == 40 AND FORTYPCD == 161"``. + plot_domain : str, optional + SQL-like filter expression for plot-level filtering. + totals : bool, default True + If True, include population-level total estimates in addition to + per-acre values. + variance : bool, default False + If True, calculate and include variance and standard error + estimates following Bechtold & Patterson (2005). + most_recent : bool, default False + If True, automatically filter to the most recent EXPVOL evaluation + for each state in the database before estimation. + + Returns + ------- + pl.DataFrame + Soil organic carbon estimates with the following columns: + + - **YEAR** : int + Evaluation reference year from EVALID. + - **POOL** : str + Pool identifier — always ``'TOTAL'``. + - **CARBON_ACRE** : float + Carbon per acre in short tons. + - **CARBON_TOTAL** : float (if ``totals=True``) + Total carbon in short tons expanded to population level. + - **CARBON_ACRE_SE** : float (if ``variance=True``) + Standard error of the per-acre estimate. + - **CARBON_TOTAL_SE** : float (if ``variance=True`` and ``totals=True``) + Standard error of the population total. + - **N_PLOTS** : int + Number of FIA plots included in the estimation. + - **N_TREES** : int + Number of tree records (used for distribution weighting). + - **[grouping columns]** : various + Any columns specified in ``grp_by``. + + See Also + -------- + live_tree : Estimate live tree carbon using the NSVB framework. + standing_dead : Estimate standing dead tree carbon. + understory : Estimate understory vegetation carbon. + downed_dead : Estimate downed dead wood carbon. + litter : Estimate litter carbon. + pyfia.carbon : Overview of all carbon pool estimators. + + Notes + ----- + **Methodology** + + Soil organic carbon is not directly measured on FIA plots. Instead, + the FIADB pre-computes carbon density per condition using the Domke + et al. (2017) model, which combines gridded soil survey data + (gNATSGO/SSURGO) with FIA plot locations to assign SOC densities + based on soil taxonomic order, clay content, and climate variables. + + This estimator reads those pre-computed values directly from the COND + table and runs them through the standard post-stratified aggregation + pipeline, ensuring exact agreement with EVALIDator estimates. + + **No AG/BG split** + + Soil organic carbon has no above-ground / below-ground partitioning. + The ``CARBON_SOIL_ORG`` column represents the entire pool (mineral + soil to 1 m depth). + + Examples + -------- + Total soil organic carbon per acre on forestland: + + >>> results = soil_organic(db, pool="total") + >>> print(f"Carbon: {results['CARBON_ACRE'][0]:.3f} tons/acre") + + Soil organic carbon by forest type group: + + >>> results = soil_organic(db, pool="total", grp_by="FORTYPGRPCD") + + Soil organic carbon on timberland with standard errors: + + >>> results = soil_organic( + ... db, + ... land_type="timber", + ... variance=True, + ... ) + + References + ---------- + .. [1] Domke, G.M.; Perry, C.H.; Walters, B.F.; et al. (2017). + Toward inventory-based estimates of soil organic carbon in forests + of the United States. Ecological Applications, 27(4), 1223-1235. + .. [2] Woodall, C.W. et al. (2015). The current and future role of + forest carbon in the United States. Gen. Tech. Rep. NRS-154. + .. [3] USEPA (2024). Inventory of U.S. Greenhouse Gas Emissions and + Sinks, Chapter 6 and Annex 3.13. + """ + from ..validation import ( + validate_boolean, + validate_domain_expression, + validate_grp_by, + validate_land_type, + ) + + # ----- Validate pool ----- + pool = pool.lower() + if pool != "total": + raise ValueError( + f"Invalid pool '{pool}' for soil organic carbon. " + f"Only 'total' is supported — soil organic carbon has no AG/BG split." + ) + + # ----- Validate standard inputs ----- + land_type = validate_land_type(land_type) + grp_by = validate_grp_by(grp_by) + area_domain = validate_domain_expression(area_domain, "area_domain") + plot_domain = validate_domain_expression(plot_domain, "plot_domain") + totals = validate_boolean(totals, "totals") + variance = validate_boolean(variance, "variance") + most_recent = validate_boolean(most_recent, "most_recent") + + # ----- Resolve db + EVALID ----- + db, owns_db = ensure_fia_instance(db) + if most_recent and db.evalid is None: + db.clip_most_recent(eval_type="VOL") + else: + ensure_evalid_set(db, eval_type="VOL", estimator_name="soil_organic") + + # ----- Build config and run estimator ----- + config = { + "pool": pool, + "grp_by": grp_by, + "by_species": False, + "by_size_class": False, + "land_type": land_type, + "area_domain": area_domain, + "plot_domain": plot_domain, + "totals": totals, + "variance": variance, + "most_recent": most_recent, + } + + try: + estimator = SoilOrganicEstimator(db, config) + return estimator.estimate() + finally: + if owns_db and hasattr(db, "close"): + db.close() diff --git a/src/pyfia/carbon/standing_dead.py b/src/pyfia/carbon/standing_dead.py new file mode 100644 index 00000000..be0fe962 --- /dev/null +++ b/src/pyfia/carbon/standing_dead.py @@ -0,0 +1,323 @@ +""" +Standing dead tree carbon estimation using the NSVB biomass framework. + +Recomputes above-ground standing dead tree biomass tree-by-tree via the +vectorized NSVB pipeline in :mod:`pyfia.carbon.nsvb.equations`, applies +the FIADB ``REF_TREE_DECAY_PROP`` density and loss reductions +(``DENSITY_PROP`` × wood, ``BARK_LOSS_PROP`` × bark, ``BRANCH_LOSS_PROP`` × +branch) keyed by hardwood/softwood × ``DECAYCD``, and converts the reduced +biomass to carbon via species-class S10b dead-tree carbon fractions. + +Broken-top corrections (``ACTUALHT < HT``) apply the Appendix K +crown-proportion adjustment to branch biomass and a volume-ratio adjustment +to wood/bark biomass, using the mean intact crown ratio from Table S11 +(``REF_TREE_STND_DEAD_CR_PROP``) keyed by Bailey ecoregion province × +hardwood/softwood. + +Belowground (BG) carbon for standing dead trees is bridged directly to the +FIADB ``TREE.CARBON_BG`` column, mirroring the live-tree estimator's +BG bridge. A native NSVB coarse-root model for dead trees is deferred. + +Public API: :func:`standing_dead`. See its docstring for parameters, +examples, and the pool semantics. +""" + +from __future__ import annotations + +import logging + +import polars as pl + +from ..core import FIA +from ..estimation.columns import get_tree_columns as _get_tree_columns +from ..estimation.utils import ( + ensure_evalid_set, + ensure_fia_instance, +) +from ._estimator_base import CarbonEstimatorBase +from .nsvb.carbon_fractions import ( + load_carbon_fractions_dead_df, + load_dead_cr_prop_df, + load_dead_decay_proportions_df, +) +from .nsvb.equations import compute_nsvb_dead_biomass + +logger = logging.getLogger(__name__) + + +class StandingDeadEstimator(CarbonEstimatorBase): + """Standing dead tree carbon estimator using the NSVB biomass framework. + + Inherits shared infrastructure from :class:`CarbonEstimatorBase`. The + standing-dead-specific logic is in :meth:`apply_filters` (the + ``STANDING_DEAD_CD = 1 + DECAYCD IS NOT NULL`` requirements) and + :meth:`calculate_values` (dead biomass pipeline + S10b fractions + + broken-top corrections). + """ + + _estimator_label = "StandingDead" + + def get_tree_columns(self) -> list[str]: + estimator_cols = [ + "SPCD", + "DIA", + "HT", + "ACTUALHT", + "DECAYCD", + "STANDING_DEAD_CD", + "CARBON_BG", + ] + return _get_tree_columns( + estimator_cols=estimator_cols, + grp_by=self.config.get("grp_by"), + ) + + def apply_filters(self, data: pl.LazyFrame) -> pl.LazyFrame: + """Apply standard filters plus standing-dead requirements. + + On top of ``STATUSCD = 2`` (from ``tree_type = 'dead'``): + ``STANDING_DEAD_CD = 1``, ``DECAYCD IS NOT NULL``, ``DIA >= 1.0``. + """ + data = super().apply_filters(data) + + columns = data.collect_schema().names() + if "STANDING_DEAD_CD" in columns: + data = data.filter( + pl.col("STANDING_DEAD_CD").cast(pl.Utf8, strict=False) == "1" + ) + if "DECAYCD" in columns: + data = data.filter( + pl.col("DECAYCD").cast(pl.Int64, strict=False).is_not_null() + ) + if "DIA" in columns: + data = data.filter(pl.col("DIA") >= 1.0) + return data + + def calculate_values(self, data: pl.LazyFrame) -> pl.LazyFrame: + """Run the NSVB dead pipeline with broken-top corrections. + + Steps: join REF_SPECIES → join PLOTGEOM/DIVISION → cast DECAYCD → + NSVB dead biomass (with broken-top corrections) → S10b carbon + fractions → BG bridge → CARBON_ACRE. + """ + pool = self.config.get("pool", "ag").lower() + + # Join REF_SPECIES and PLOTGEOM/DIVISION + data = self._join_ref_species(data) + data = self._join_plotgeom_division(data) + + # Cast DECAYCD from Utf8 to Int64 for the decay-prop join + data = data.with_columns(pl.col("DECAYCD").cast(pl.Int64, strict=False)) + + # NSVB dead biomass pipeline (with broken-top corrections when + # ACTUALHT is available and the CR prop table loads) + if pool in ("ag", "total"): + decay_props = load_dead_decay_proportions_df() + cr_prop_table = load_dead_cr_prop_df() + data = compute_nsvb_dead_biomass( + data, decay_props, cr_prop_table=cr_prop_table + ) + + # S10b dead carbon fractions joined on (hw_sw, DECAYCD) + cf_df = load_carbon_fractions_dead_df() + data = data.with_columns( + pl.when(pl.col("SPCD") >= 300) + .then(pl.lit("hardwood")) + .otherwise(pl.lit("softwood")) + .alias("_hw_sw_cf"), + ) + data = data.join( + cf_df.rename({"hw_sw": "_hw_sw_cf"}).lazy(), + on=["_hw_sw_cf", "DECAYCD"], + how="left", + ) + data = data.with_columns( + (pl.col("agb") * pl.col("CARBON_FRAC_DEAD")).alias("_CARBON_AG_LB") + ) + data = data.drop(["_hw_sw_cf"]) + else: # pool == "bg" + data = data.with_columns(pl.lit(0.0).alias("_CARBON_AG_LB")) + + # BG bridge + CARBON_ACRE + data = self._apply_bg_bridge(data, pool) + data = self._compute_carbon_acre(data) + + return data + + +def standing_dead( + db: str | FIA, + pool: str = "ag", + grp_by: str | list[str] | None = None, + by_species: bool = False, + by_size_class: bool = False, + land_type: str = "forest", + tree_domain: str | None = None, + area_domain: str | None = None, + plot_domain: str | None = None, + totals: bool = True, + variance: bool = False, + most_recent: bool = False, +) -> pl.DataFrame: + """ + Estimate standing dead tree carbon from FIA data using the NSVB framework. + + Recomputes above-ground standing dead tree biomass from scratch using + the National Scale Volume and Biomass (NSVB) framework of Westfall et + al. (2023, GTR-WO-104) and applies the FIADB ``REF_TREE_DECAY_PROP`` + decay reductions (DENSITY_PROP × wood, BARK_LOSS_PROP × bark, + BRANCH_LOSS_PROP × branch) keyed by hardwood/softwood × DECAYCD. The + reduced biomass is then converted to carbon via species-class S10b + dead-tree carbon fractions from GTR-WO-104, replacing the flat ~0.47 + multiplier and producing carbon estimates that align with the EPA + NGHGI LULUCF standing dead pool. + + Broken-top corrections apply the Appendix K crown-proportion + adjustment to branch biomass and a volume-ratio adjustment to wood/bark + for trees with ``ACTUALHT < HT``, using the mean intact crown ratio + from Table S11 (``REF_TREE_STND_DEAD_CR_PROP``) keyed by Bailey + ecoregion province × hardwood/softwood. + + Belowground carbon for standing dead trees is bridged directly to the + FIADB pre-computed ``TREE.CARBON_BG`` column; a native NSVB coarse-root + model for dead trees is deferred. + + The standing-dead population is filtered as + ``STATUSCD = 2 AND STANDING_DEAD_CD = 1 AND DECAYCD IS NOT NULL``, + which matches the trees FIADB itself populates ``CARBON_AG`` for. + Trees with ``STANDING_DEAD_CD = 0`` (downed dead) belong to the down + dead wood pool and are excluded. + + Parameters + ---------- + db : str | FIA + Database connection or path to FIA database. Can be either a path + string to a DuckDB/SQLite file or an existing FIA connection object. + pool : {'ag', 'bg', 'total'}, default 'ag' + Standing dead carbon pool to estimate: + + - 'ag': Above-ground standing dead carbon via the NSVB pipeline + + REF_TREE_DECAY_PROP reductions + broken-top corrections + S10b + dead carbon fractions. + - 'bg': Below-ground standing dead carbon (coarse roots) via the + bridge to FIADB ``TREE.CARBON_BG``. + - 'total': ``'ag' + 'bg'`` (NSVB AG + FIADB BG bridge). + grp_by : str or list of str, optional + Column name(s) to group results by. + by_species : bool, default False + If True, group results by species code (SPCD). + by_size_class : bool, default False + If True, group results by diameter size classes. + land_type : {'forest', 'timber', 'all'}, default 'forest' + Land type to include in estimation. + tree_domain : str, optional + SQL-like filter expression for tree-level filtering. + area_domain : str, optional + SQL-like filter expression for area/condition-level filtering. + plot_domain : str, optional + SQL-like filter expression for plot-level filtering. + totals : bool, default True + If True, include population-level total estimates. + variance : bool, default False + If True, calculate variance and standard error estimates. + most_recent : bool, default False + If True, auto-filter to the most recent EXPVOL evaluation. + + Returns + ------- + pl.DataFrame + Standing dead carbon estimates with columns: YEAR, POOL, + CARBON_ACRE, CARBON_TOTAL (if totals), CARBON_ACRE_SE (if + variance), CARBON_TOTAL_SE (if variance and totals), N_PLOTS, + N_TREES, plus any grouping columns. + + See Also + -------- + live_tree : Estimate live tree carbon via the NSVB framework. + + Examples + -------- + Above-ground standing dead carbon per acre on forestland: + + >>> results = standing_dead(db, pool="ag") + >>> print(f"SD Carbon: {results['CARBON_ACRE'][0]:.1f} tons/acre") + + Standing dead carbon by decay class on timberland: + + >>> results = standing_dead( + ... db, + ... pool="ag", + ... grp_by="DECAYCD", + ... land_type="timber", + ... ) + """ + from ..validation import ( + validate_boolean, + validate_domain_expression, + validate_grp_by, + validate_land_type, + ) + + pool = pool.lower() + valid_pools = {"ag", "bg", "total"} + if pool not in valid_pools: + raise ValueError( + f"Invalid pool '{pool}'. Must be one of: {sorted(valid_pools)}" + ) + + land_type = validate_land_type(land_type) + grp_by = validate_grp_by(grp_by) + tree_domain = validate_domain_expression(tree_domain, "tree_domain") + area_domain = validate_domain_expression(area_domain, "area_domain") + plot_domain = validate_domain_expression(plot_domain, "plot_domain") + by_species = validate_boolean(by_species, "by_species") + by_size_class = validate_boolean(by_size_class, "by_size_class") + totals = validate_boolean(totals, "totals") + variance = validate_boolean(variance, "variance") + most_recent = validate_boolean(most_recent, "most_recent") + + db, owns_db = ensure_fia_instance(db) + if most_recent and db.evalid is None: + db.clip_most_recent(eval_type="VOL") + else: + ensure_evalid_set(db, eval_type="VOL", estimator_name="standing_dead") + + config = { + "pool": pool, + "grp_by": grp_by, + "by_species": by_species, + "by_size_class": by_size_class, + "land_type": land_type, + "tree_type": "dead", + "tree_domain": tree_domain, + "area_domain": area_domain, + "plot_domain": plot_domain, + "totals": totals, + "variance": variance, + "most_recent": most_recent, + } + + try: + estimator = StandingDeadEstimator(db, config) + if pool == "total": + # Best-effort cross-era warning; see live_tree.py for rationale. + try: + year = estimator._extract_evaluation_year() + if int(year) < 2024: + logger.warning( + "standing_dead(pool='total'): selected EVALID year " + "(%d) pre-dates the NSVB framework transition " + "(September 2023). The BG bridge reads FIADB " + "TREE.CARBON_BG directly, which for pre-NSVB " + "inventories was computed via legacy CRM-based " + "allometry — combining it with NSVB-recomputed AG " + "may produce cross-era inconsistencies. Use " + "pool='ag' if you need NSVB-only consistency.", + int(year), + ) + except (ValueError, TypeError, AttributeError, IndexError, KeyError) as exc: + logger.debug("Skipping standing_dead year warning: %s", exc) + return estimator.estimate() + finally: + if owns_db and hasattr(db, "close"): + db.close() diff --git a/src/pyfia/carbon/stock_change.py b/src/pyfia/carbon/stock_change.py new file mode 100644 index 00000000..3957c87c --- /dev/null +++ b/src/pyfia/carbon/stock_change.py @@ -0,0 +1,568 @@ +""" +Carbon stock-change estimation for condition-level FIADB carbon pools. + +Computes the change in carbon stocks between two inventory periods for +the four condition-level pools: understory vegetation, downed dead wood, +litter, and soil organic carbon. Stock change is calculated as +``C(t₂) − C(t₁)`` per remeasured condition, optionally annualized by +the remeasurement period (REMPER), then aggregated via the standard +post-stratified estimation pipeline using t₂'s stratification. + +This module follows the ``AreaChangeEstimator`` pattern: t₂ data comes +from the EVALID-scoped pipeline; t₁ data is loaded from the full +(unfiltered) COND table and linked via ``PREV_PLT_CN + CONDID``. + +Tree-level stock change (live tree, standing dead) requires GRM fate +decomposition and is not yet implemented. + +Public API: :func:`stock_change`. + +References +---------- +- Bechtold, W.A. & Patterson, P.L. (2005). GTR-SRS-80, Chapter 4: + Change Estimation. +- Woodall, C.W. et al. (2015). GTR-NRS-154 (FCAF methodology). +""" + +from __future__ import annotations + +import logging + +import polars as pl + +from ..core import FIA +from ..estimation.base import AggregationResult, BaseEstimator +from ..estimation.columns import get_cond_columns as _get_cond_columns +from ..estimation.utils import ( + ensure_evalid_set, + ensure_fia_instance, + validate_aggregation_result, + validate_required_columns, +) + +logger = logging.getLogger(__name__) + +# Pool name → COND column(s) for the carbon density attribute. +# Understory has AG + BG; the other three have a single total column. +_POOL_COLUMNS: dict[str, list[str]] = { + "understory": ["CARBON_UNDERSTORY_AG", "CARBON_UNDERSTORY_BG"], + "downed_dead": ["CARBON_DOWN_DEAD"], + "litter": ["CARBON_LITTER"], + "soil_organic": ["CARBON_SOIL_ORG"], +} + +_VALID_POOLS = set(_POOL_COLUMNS.keys()) + + +class CarbonStockChangeEstimator(BaseEstimator): + """Condition-level carbon stock-change estimator. + + Computes ``C(t₂) − C(t₁)`` per remeasured condition for one + condition-level carbon pool, then aggregates via the standard + post-stratified two-stage pipeline. + + Follows the ``AreaChangeEstimator`` pattern: + + * t₂ data is loaded through the standard EVALID-scoped pipeline. + * t₁ data is loaded from the **full** COND table (unfiltered by + EVALID) via ``db._reader.read_table()``, then joined to t₂ via + ``PREV_PLT_CN + CONDID``. + * Stratification (``POP_PLOT_STRATUM_ASSGN``, ``POP_STRATUM``) is + always from t₂'s EVALID. + """ + + _estimator_label = "CarbonStockChange" + + # ------------------------------------------------------------------ + # Table / column requirements + # ------------------------------------------------------------------ + + def get_required_tables(self) -> list[str]: + return ["COND", "PLOT", "POP_PLOT_STRATUM_ASSGN", "POP_STRATUM"] + + def get_tree_columns(self) -> list[str]: + return [] + + def get_cond_columns(self) -> list[str]: + cols = _get_cond_columns( + land_type=self.config.get("land_type", "forest"), + grp_by=self.config.get("grp_by"), + include_prop_basis=False, + ) + # Add the carbon column(s) for this pool + pool = self.config["pool"] + for c in _POOL_COLUMNS[pool]: + if c not in cols: + cols.append(c) + return cols + + # ------------------------------------------------------------------ + # load_data — the key override (area_change.py pattern) + # ------------------------------------------------------------------ + + def load_data(self) -> pl.LazyFrame | None: + """Load t₂ and t₁ condition data and link via PREV_PLT_CN. + + Join sequence: + 1. COND (t₂) — EVALID-scoped via standard pipeline + 2. PLOT (t₂) — EVALID-scoped; filter to PREV_PLT_CN IS NOT NULL + 3. COND (t₁) — **full table** loaded via ``db._reader.read_table`` + 4. Stratification data — from t₂'s EVALID + """ + pool = self.config["pool"] + carbon_cols = _POOL_COLUMNS[pool] + + # --- Load EVALID-scoped t2 tables --- + for table in self.get_required_tables(): + if table not in self.db.tables: + self.db.load_table(table) + + cond = self.db.tables["COND"] + if not isinstance(cond, pl.LazyFrame): + cond = cond.lazy() + + # Select the columns we need from COND (t2) + cond_cols = self.get_cond_columns() + available = cond.collect_schema().names() + cond_select = [c for c in cond_cols if c in available] + cond_t2 = cond.select(cond_select) + + # --- Load PLOT for REMPER and PREV_PLT_CN --- + plot = self.db.tables["PLOT"] + if not isinstance(plot, pl.LazyFrame): + plot = plot.lazy() + + plot_cols = ["CN", "STATECD", "INVYR", "REMPER", "PREV_PLT_CN"] + plot_available = plot.collect_schema().names() + plot_cols = [c for c in plot_cols if c in plot_available] + plot = plot.select(plot_cols) + + # Join t2 COND with PLOT + data = cond_t2.join( + plot, + left_on="PLT_CN", + right_on="CN", + how="inner", + ) + + # Filter to remeasured plots only + data = data.filter( + pl.col("PREV_PLT_CN").is_not_null() + & pl.col("REMPER").is_not_null() + & (pl.col("REMPER") > 0) + ) + + # --- Load FULL COND table (unfiltered) for t1 --- + # IMPORTANT: PREV_PLT_CN references plots from previous inventory + # cycles that are outside the current EVALID scope. + t1_cols = ["PLT_CN", "CONDID"] + carbon_cols + cond_prev = self.db._reader.read_table( + "COND", + columns=t1_cols, + lazy=True, + ) + + # Add a sentinel column to detect successful joins (vs NULL carbon) + cond_prev = cond_prev.with_columns(pl.lit(True).alias("_t1_matched")) + + # Rename t1 carbon columns with t1_ prefix + t1_rename: dict[str, str] = {"PLT_CN": "t1_PLT_CN", "CONDID": "t1_CONDID"} + for c in carbon_cols: + t1_rename[c] = f"t1_{c}" + cond_prev = cond_prev.rename(t1_rename) + + # Rename t2 carbon columns with t2_ prefix for clarity + t2_rename: dict[str, str] = {} + for c in carbon_cols: + if c in data.collect_schema().names(): + t2_rename[c] = f"t2_{c}" + if t2_rename: + data = data.rename(t2_rename) + + # Join t1 conditions via PREV_PLT_CN + matching CONDID. + # CONDID is assigned per condition per plot visit; most conditions + # retain the same CONDID across remeasurements (~97% in Georgia). + # The more precise PREVCOND mapping lives in SUBP_COND_CHNG_MTRX + # (subplot-level), which is overkill for condition-level carbon. + data = data.join( + cond_prev, + left_on=["PREV_PLT_CN", "CONDID"], + right_on=["t1_PLT_CN", "t1_CONDID"], + how="left", + ) + + # Filter to conditions where t1 match was found (LEFT JOIN hit). + # The _t1_matched sentinel distinguishes "no match" (NULL) from + # "match but NULL carbon value" (which fill_null handles later). + data = data.filter(pl.col("_t1_matched").is_not_null()) + data = data.drop("_t1_matched") + + # --- Join stratification data (from t2's EVALID) --- + strat_data = self._get_stratification_data() + data = data.join(strat_data, on="PLT_CN", how="inner") + + return data + + # ------------------------------------------------------------------ + # apply_filters — condition/area filters on t2 data + # ------------------------------------------------------------------ + + def apply_filters(self, data: pl.LazyFrame) -> pl.LazyFrame: + """Apply condition/area filters only (no tree-type filtering).""" + from ..filtering import apply_area_filters, get_land_domain_indicator + + columns = data.collect_schema().names() + + if self.config.get("area_domain"): + data = apply_area_filters(data, area_domain=self.config["area_domain"]) + + land_type = self.config.get("land_type", "forest") + if land_type and land_type != "all" and "COND_STATUS_CD" in columns: + data = data.filter(get_land_domain_indicator(land_type)) + + return data + + # ------------------------------------------------------------------ + # calculate_values — compute delta per condition + # ------------------------------------------------------------------ + + def calculate_values(self, data: pl.LazyFrame) -> pl.LazyFrame: + """Compute ``C(t₂) − C(t₁)`` and optionally annualize by REMPER.""" + pool = self.config["pool"] + carbon_cols = _POOL_COLUMNS[pool] + annualize = self.config.get("annualize", True) + + # Exclude conditions where ALL carbon columns are NULL at both + # t1 and t2 — these have no carbon data and would produce spurious + # zero deltas. Conditions with NULL on one side only are kept + # (fill_null(0.0) handles the asymmetric case correctly). + all_null_expr = pl.lit(True) + for c in carbon_cols: + all_null_expr = ( + all_null_expr + & pl.col(f"t2_{c}").is_null() + & pl.col(f"t1_{c}").is_null() + ) + data = data.filter(~all_null_expr) + + # Sum all carbon columns for this pool (understory has AG+BG) + t2_expr = pl.lit(0.0) + t1_expr = pl.lit(0.0) + for c in carbon_cols: + t2_expr = t2_expr + pl.col(f"t2_{c}").cast(pl.Float64).fill_null(0.0) + t1_expr = t1_expr + pl.col(f"t1_{c}").cast(pl.Float64).fill_null(0.0) + + delta = t2_expr - t1_expr + + if annualize: + delta = delta / pl.col("REMPER").cast(pl.Float64) + + return data.with_columns(delta.alias("CARBON_CHANGE_ACRE")) + + # ------------------------------------------------------------------ + # aggregate_results — condition-level with ADJ_FACTOR_SUBP + # ------------------------------------------------------------------ + + def aggregate_results(self, data: pl.LazyFrame | None) -> AggregationResult: + if data is None: + return AggregationResult( + results=pl.DataFrame(), + plot_tree_data=pl.DataFrame(), + group_cols=[], + ) + + validate_required_columns( + data, ["PLT_CN", "CARBON_CHANGE_ACRE"], "carbon stock-change data" + ) + + # Condition-level attribute: CARBON_CHANGE_ACRE is a density + # (tons/acre/year). The two-stage pipeline denominator is + # sum(CONDPROP_UNADJ * EXPNS), so the numerator must include + # CONDPROP_UNADJ to give the condition's contribution proportional + # to its area on the plot. ADJ_FACTOR_SUBP corrects for nonresponse. + data = data.with_columns( + pl.col("ADJ_FACTOR_SUBP").cast(pl.Float64).alias("ADJ_FACTOR") + ) + + data = data.with_columns( + ( + pl.col("CARBON_CHANGE_ACRE") + * pl.col("CONDPROP_UNADJ").cast(pl.Float64) + * pl.col("ADJ_FACTOR") + ).alias("CARBON_CHANGE_ADJ") + ) + + group_cols = self._setup_grouping() + + plot_tree_data, data = self._preserve_plot_tree_data( + data, + metric_cols=["CARBON_CHANGE_ADJ"], + group_cols=group_cols, + ) + + results = self._apply_two_stage_aggregation( + data_with_strat=data, + metric_mappings={"CARBON_CHANGE_ADJ": "CONDITION_CARBON_CHANGE"}, + group_cols=group_cols, + use_grm_adjustment=False, + ) + + if not self.config.get("totals", True): + if "CARBON_CHANGE_TOTAL" in results.columns: + results = results.drop("CARBON_CHANGE_TOTAL") + + return AggregationResult( + results=results, + plot_tree_data=plot_tree_data, + group_cols=group_cols, + ) + + # ------------------------------------------------------------------ + # Variance and output formatting + # ------------------------------------------------------------------ + + def calculate_variance(self, agg_result: AggregationResult) -> pl.DataFrame: + validate_aggregation_result(agg_result, self._estimator_label) + metric_configs = [ + { + "adjusted_col": "CARBON_CHANGE_ADJ", + "acre_se_col": "CARBON_CHANGE_ACRE_SE", + "total_se_col": "CARBON_CHANGE_TOTAL_SE", + }, + ] + return self._calculate_variance_for_metrics(agg_result, metric_configs) + + def format_output(self, results: pl.DataFrame) -> pl.DataFrame: + year = self._extract_evaluation_year() + results = results.with_columns(pl.lit(year).alias("YEAR")) + + pool = self.config["pool"].upper() + results = results.with_columns(pl.lit(pool).alias("POOL")) + + col_order = [ + "YEAR", + "POOL", + "CARBON_CHANGE_ACRE", + "CARBON_CHANGE_TOTAL", + "CARBON_CHANGE_ACRE_SE", + "CARBON_CHANGE_TOTAL_SE", + "N_PLOTS", + "N_TREES", + ] + + for col in results.columns: + if col not in col_order: + col_order.insert(2, col) + + final_cols = [col for col in col_order if col in results.columns] + return results.select(final_cols) + + +# ====================================================================== +# Public API +# ====================================================================== + + +def stock_change( + db: str | FIA, + pool: str | list[str] = "all", + grp_by: str | list[str] | None = None, + land_type: str = "forest", + area_domain: str | None = None, + annualize: bool = True, + totals: bool = True, + variance: bool = False, + most_recent: bool = False, +) -> pl.DataFrame: + """ + Estimate carbon stock change between two inventory periods. + + Computes the change in carbon stocks for condition-level pools + (understory, downed dead wood, litter, soil organic carbon) between + the current evaluation and the previous measurement on remeasured + plots. The t₂ evaluation is the EVALID set on the database; t₁ is + found automatically via ``PLOT.PREV_PLT_CN``. + + Stock change is calculated as ``C(t₂) − C(t₁)`` per remeasured + condition, optionally annualized by dividing by the remeasurement + period (REMPER). Positive values indicate carbon accumulation + (sequestration); negative values indicate carbon loss. + + Parameters + ---------- + db : str | FIA + Database connection or path to FIA database. + pool : str or list of str, default 'all' + Carbon pool(s) to estimate stock change for. Accepts: + + - ``'understory'``: Understory vegetation (AG + BG) + - ``'downed_dead'``: Downed dead wood + - ``'litter'``: Litter and duff + - ``'soil_organic'``: Soil organic carbon + - ``'all'``: All four condition-level pools (default) + + Tree-level pools (``'live_tree'``, ``'standing_dead'``) are not + yet supported and will raise ``ValueError``. + grp_by : str or list of str, optional + Column name(s) to group results by. + land_type : {'forest', 'timber', 'all'}, default 'forest' + Land type to include in estimation. + area_domain : str, optional + SQL-like filter expression for condition-level filtering. + annualize : bool, default True + If True, divide stock change by the remeasurement period + (REMPER) to produce annual change rates (tons/acre/year). + If False, report total change over the remeasurement period. + totals : bool, default True + If True, include population-level total estimates. + variance : bool, default False + If True, calculate standard error estimates. + most_recent : bool, default False + If True, automatically filter to the most recent evaluation. + + Returns + ------- + pl.DataFrame + Stock-change estimates with columns: + + - **YEAR** : int — Evaluation reference year (t₂). + - **POOL** : str — Pool identifier. + - **CARBON_CHANGE_ACRE** : float — Change per acre (tons/acre + or tons/acre/year if annualized). + - **CARBON_CHANGE_TOTAL** : float — Population total change + (if ``totals=True``). + - **CARBON_CHANGE_ACRE_SE** / **CARBON_CHANGE_TOTAL_SE** : + float — Standard errors (if ``variance=True``). + - **N_PLOTS** : int — Number of remeasured plots. + + See Also + -------- + downed_dead : Estimate downed dead wood carbon stocks. + litter : Estimate litter carbon stocks. + soil_organic : Estimate soil organic carbon stocks. + understory : Estimate understory vegetation carbon stocks. + total_ecosystem : Estimate total ecosystem carbon stocks. + + Notes + ----- + **Methodology** + + Stock change follows Bechtold & Patterson (2005), Chapter 4. For + each remeasured condition, the change in carbon density is computed + from pre-computed COND attributes. The delta is annualized by + REMPER (typically 5–7 years), adjusted by ``CONDPROP_UNADJ × + ADJ_FACTOR_SUBP``, and aggregated via the two-stage post-stratified + pipeline using t₂'s stratification. + + Only remeasured plots (``PREV_PLT_CN IS NOT NULL AND REMPER > 0``) + contribute. The t₁ conditions are loaded from the full COND table + (unfiltered by EVALID) and linked via ``PREV_PLT_CN + CONDID``. + + **Tree-level pools** + + Live tree and standing dead stock change require GRM fate + decomposition and are not yet implemented. + + Examples + -------- + Annual stock change for all condition-level pools: + + >>> results = stock_change(db) + + Downed dead wood stock change by ownership: + + >>> results = stock_change(db, pool="downed_dead", grp_by="OWNGRPCD") + + Non-annualized litter change with standard errors: + + >>> results = stock_change(db, pool="litter", annualize=False, variance=True) + + References + ---------- + .. [1] Bechtold, W.A. & Patterson, P.L. (2005). The Enhanced Forest + Inventory and Analysis Program. GTR-SRS-80, Chapter 4. + .. [2] Woodall, C.W. et al. (2015). GTR-NRS-154. + """ + from ..validation import ( + validate_boolean, + validate_domain_expression, + validate_grp_by, + validate_land_type, + ) + + # ----- Validate pool ----- + tree_pools = {"live_tree", "standing_dead"} + if isinstance(pool, str): + pool_lower = pool.lower() + if pool_lower in tree_pools: + raise ValueError( + f"Tree-level stock change for '{pool}' is not yet implemented. " + f"Supported pools: {sorted(_VALID_POOLS)} or 'all'." + ) + if pool_lower == "all": + pools = sorted(_VALID_POOLS) + elif pool_lower in _VALID_POOLS: + pools = [pool_lower] + else: + raise ValueError( + f"Invalid pool '{pool}'. " + f"Must be one of: {sorted(_VALID_POOLS | {'all'})}" + ) + elif isinstance(pool, list): + pools = [] + for p in pool: + p_lower = p.lower() + if p_lower in tree_pools: + raise ValueError( + f"Tree-level stock change for '{p}' is not yet implemented." + ) + if p_lower not in _VALID_POOLS: + raise ValueError( + f"Invalid pool '{p}'. Must be one of: {sorted(_VALID_POOLS)}" + ) + pools.append(p_lower) + else: + raise TypeError(f"pool must be str or list[str], got {type(pool).__name__}") + + # ----- Validate standard inputs ----- + land_type = validate_land_type(land_type) + grp_by = validate_grp_by(grp_by) + area_domain = validate_domain_expression(area_domain, "area_domain") + annualize = validate_boolean(annualize, "annualize") + totals = validate_boolean(totals, "totals") + variance = validate_boolean(variance, "variance") + most_recent = validate_boolean(most_recent, "most_recent") + + # ----- Resolve db + EVALID ----- + db, owns_db = ensure_fia_instance(db) + if most_recent and db.evalid is None: + db.clip_most_recent(eval_type="VOL") + else: + ensure_evalid_set(db, eval_type="VOL", estimator_name="stock_change") + + try: + results = [] + for p in pools: + config = { + "pool": p, + "grp_by": grp_by, + "by_species": False, + "by_size_class": False, + "land_type": land_type, + "area_domain": area_domain, + "annualize": annualize, + "totals": totals, + "variance": variance, + } + estimator = CarbonStockChangeEstimator(db, config) + results.append(estimator.estimate()) + + if len(results) == 1: + return results[0] + + # Stack multi-pool results + return pl.concat(results, how="diagonal_relaxed") + + finally: + if owns_db and hasattr(db, "close"): + db.close() diff --git a/src/pyfia/carbon/total_ecosystem.py b/src/pyfia/carbon/total_ecosystem.py new file mode 100644 index 00000000..c2b4f56f --- /dev/null +++ b/src/pyfia/carbon/total_ecosystem.py @@ -0,0 +1,249 @@ +""" +Total ecosystem carbon estimation — sum of all six IPCC/NGHGI pools. + +Combines live tree, standing dead, understory vegetation, downed dead +wood, litter, and soil organic carbon into a single total ecosystem +estimate. Each pool is estimated independently via its own estimator, +then the per-acre and population totals are summed. + +The summed total can be validated against EVALIDator snum=103 ("Total +all pools") and snum=97 ("Total forest carbon"). + +Public API: :func:`total_ecosystem`. +""" + +from __future__ import annotations + +import logging +from typing import Callable + +import polars as pl + +from ..core import FIA +from ..estimation.utils import ensure_evalid_set, ensure_fia_instance + +logger = logging.getLogger(__name__) + + +def total_ecosystem( + db: str | FIA, + grp_by: str | list[str] | None = None, + land_type: str = "forest", + area_domain: str | None = None, + plot_domain: str | None = None, + totals: bool = True, + variance: bool = False, + most_recent: bool = False, +) -> pl.DataFrame: + """ + Estimate total ecosystem carbon across all six IPCC/NGHGI pools. + + Sums per-acre and population total estimates from: + + 1. Live tree (AG + BG) + 2. Standing dead (AG + BG) + 3. Understory vegetation (AG + BG) + 4. Downed dead wood + 5. Litter + 6. Soil organic carbon + + Each pool is estimated independently, then results are stacked and + summed across pools. This produces the total forest ecosystem carbon + stock comparable to EVALIDator snum=103 ("Total all pools"). + + Parameters + ---------- + db : str | FIA + Database connection or path to FIA database. + grp_by : str or list of str, optional + Column name(s) to group results by (e.g., 'FORTYPCD', 'OWNGRPCD'). + Forwarded to every pool estimator; the ``TOTAL_ECOSYSTEM`` summary + row is summed per group. + land_type : {'forest', 'timber', 'all'}, default 'forest' + Land type to include in estimation. + area_domain : str, optional + SQL-like filter expression for area/condition-level filtering. + plot_domain : str, optional + SQL-like filter expression for plot-level filtering. + totals : bool, default True + If True, include population-level total estimates. + variance : bool, default False + If True, calculate and include standard error estimates for the + individual pool rows. The ``TOTAL_ECOSYSTEM`` summary row does + NOT receive a combined SE — see Notes below. + most_recent : bool, default False + If True, automatically filter to the most recent evaluation. + + Returns + ------- + pl.DataFrame + One row per pool plus a ``TOTAL_ECOSYSTEM`` row (one per group, if + ``grp_by`` is set). Columns: POOL, CARBON_ACRE, CARBON_TOTAL + (if totals), and SE columns (if variance, on pool rows only). + + Notes + ----- + Standard error is intentionally NOT computed for the ``TOTAL_ECOSYSTEM`` + row. The naive ``sqrt(sum(SE_pool^2))`` assumes independence between + pools, which is incorrect: pool estimates share plot- and stratum-level + sampling variance through the common post-stratification. Reporting a + combined SE without modelling that covariance would overstate + precision. Callers needing a combined SE should compute one explicitly + against their own assumptions, or use ``stock_change`` for paired + plot-level inference. + + See Also + -------- + live_tree : Live tree carbon. + standing_dead : Standing dead tree carbon. + understory : Understory vegetation carbon. + downed_dead : Downed dead wood carbon. + litter : Litter carbon. + soil_organic : Soil organic carbon. + + Examples + -------- + >>> results = total_ecosystem(db) + >>> total_row = results.filter(pl.col("POOL") == "TOTAL_ECOSYSTEM") + >>> print(f"Total: {total_row['CARBON_ACRE'][0]:.2f} tons/acre") + + >>> grouped = total_ecosystem(db, grp_by="FORTYPCD") + >>> totals = grouped.filter(pl.col("POOL") == "TOTAL_ECOSYSTEM") + """ + from ..validation import ( + validate_boolean, + validate_domain_expression, + validate_grp_by, + validate_land_type, + ) + from .downed_dead import downed_dead + from .litter import litter + from .live_tree import live_tree + from .soil_organic import soil_organic + from .standing_dead import standing_dead + from .understory import understory + + # ----- Validate inputs ----- + land_type = validate_land_type(land_type) + area_domain = validate_domain_expression(area_domain, "area_domain") + plot_domain = validate_domain_expression(plot_domain, "plot_domain") + totals = validate_boolean(totals, "totals") + variance = validate_boolean(variance, "variance") + most_recent = validate_boolean(most_recent, "most_recent") + grp_by_norm = validate_grp_by(grp_by) + # validate_grp_by returns None for empty input; normalize to list form + grp_cols: list[str] = ( + [grp_by_norm] + if isinstance(grp_by_norm, str) + else list(grp_by_norm) + if grp_by_norm + else [] + ) + + # ----- Resolve db + EVALID ----- + db, owns_db = ensure_fia_instance(db) + if most_recent and db.evalid is None: + db.clip_most_recent(eval_type="VOL") + else: + ensure_evalid_set(db, eval_type="VOL", estimator_name="total_ecosystem") + + common_kwargs: dict = { + "grp_by": grp_by_norm, + "land_type": land_type, + "area_domain": area_domain, + "plot_domain": plot_domain, + "totals": totals, + "variance": variance, + } + + try: + pool_specs: list[tuple[str, Callable[..., pl.DataFrame]]] = [ + ("LIVE_TREE", live_tree), + ("STANDING_DEAD", standing_dead), + ("UNDERSTORY", understory), + ("DOWNED_DEAD", downed_dead), + ("LITTER", litter), + ("SOIL_ORGANIC", soil_organic), + ] + + pool_results: list[pl.DataFrame] = [] + for label, fn in pool_specs: + frame = fn(db, pool="total", **common_kwargs) + frame = frame.with_columns(pl.lit(label).alias("POOL")) + pool_results.append(frame) + + # Guard against empty results (e.g. filters eliminate all data). + non_empty = [r for r in pool_results if len(r) > 0] + if not non_empty: + return pl.DataFrame({"POOL": ["TOTAL_ECOSYSTEM"], "CARBON_ACRE": [0.0]}) + + sum_cols = ["CARBON_ACRE"] + if totals: + sum_cols.append("CARBON_TOTAL") + + # Build the TOTAL_ECOSYSTEM row(s) by summing across pools. + # Stack only the columns we need so disparate per-pool extras + # (N_TREES, by_species columns, etc.) don't interfere with the sum. + keep_cols = grp_cols + [ + c for c in sum_cols if all(c in r.columns for r in non_empty) + ] + if not keep_cols or keep_cols == grp_cols: + # No sum columns present — fall back to fallback row. + return pl.DataFrame({"POOL": ["TOTAL_ECOSYSTEM"], "CARBON_ACRE": [0.0]}) + + stacked = pl.concat( + [r.select(keep_cols) for r in non_empty], + how="vertical_relaxed", + ) + + if grp_cols: + total_df = stacked.group_by(grp_cols).agg( + [pl.col(c).sum().alias(c) for c in sum_cols if c in stacked.columns] + ) + else: + total_df = stacked.select( + [pl.col(c).sum().alias(c) for c in sum_cols if c in stacked.columns] + ) + + total_df = total_df.with_columns(pl.lit("TOTAL_ECOSYSTEM").alias("POOL")) + + # Propagate YEAR from first non-empty pool, when present. + if "YEAR" in non_empty[0].columns and "YEAR" not in grp_cols: + year_val = non_empty[0]["YEAR"][0] + total_df = total_df.with_columns(pl.lit(year_val).alias("YEAR")) + + # Normalize columns across all pool results and total + all_frames = pool_results + [total_df] + all_cols: set[str] = set() + for frame in all_frames: + all_cols.update(frame.columns) + + normalized = [] + for frame in all_frames: + for col in all_cols: + if col not in frame.columns: + frame = frame.with_columns(pl.lit(None).alias(col)) + normalized.append(frame) + + result = pl.concat(normalized, how="diagonal_relaxed") + + # Order columns: grp_by columns first, then standard layout. + col_order = grp_cols + [ + "YEAR", + "POOL", + "CARBON_ACRE", + "CARBON_TOTAL", + "CARBON_ACRE_SE", + "CARBON_TOTAL_SE", + "N_PLOTS", + "N_TREES", + ] + final_cols = [c for c in col_order if c in result.columns] + for c in result.columns: + if c not in final_cols: + final_cols.append(c) + return result.select(final_cols) + + finally: + if owns_db and hasattr(db, "close"): + db.close() diff --git a/src/pyfia/carbon/understory.py b/src/pyfia/carbon/understory.py new file mode 100644 index 00000000..2cadf99c --- /dev/null +++ b/src/pyfia/carbon/understory.py @@ -0,0 +1,468 @@ +""" +Understory vegetation carbon estimation from FIADB condition-level attributes. + +Understory vegetation — all biomass of undergrowth plants in a forest, +including woody shrubs and trees < 2.54 cm DBH — is +not directly measured on FIA plots. Instead, the FIADB pre-computes +condition-level carbon density using the Smith & Heath (2008) model, which +is parameterised on geographic area, forest type, and (for most stand +types) live tree carbon density. + +This estimator reads ``COND.CARBON_UNDERSTORY_AG`` and +``COND.CARBON_UNDERSTORY_BG`` (short tons per acre), then runs them +through pyFIA's post-stratified aggregation pipeline to produce per-acre +and population estimates that match EVALIDator. + +The Smith & Heath model descends from the Birdsey (1996) ratios published +in *Forests and Global Change Vol. 2* (Sampson & Hair eds.) and refined in +GTR-NE-343 (Smith et al. 2006, Appendix A). The AG/BG split follows +Smith et al. (2006): 10 % of total understory carbon is belowground. + +National magnitude: ~735 Tg C total understory, ~0.8 % of forest +ecosystem carbon (GTR-NRS-154, Table 2). + +Public API: :func:`understory`. See its docstring for parameters, +examples, and the pool semantics. + +References +---------- +- Birdsey, R.A. (1996). In *Forests and Global Change Vol. 2*, Sampson & + Hair eds., American Forests. (Operational ratios.) +- Smith, J.E.; Heath, L.S.; Skog, K.E.; Birdsey, R.A. (2006). GTR-NE-343. + DOI: 10.2737/NE-GTR-343. (Appendix A carbon yield tables.) +- Smith, J.E.; Heath, L.S. (2008). GTR-NRS-13. (FIADB implementation.) +- USEPA (2024). NGHGI Annex 3.13. (Current operational methodology.) +""" + +from __future__ import annotations + +import logging + +import polars as pl + +from ..core import FIA +from ..estimation.base import AggregationResult, BaseEstimator +from ..estimation.columns import get_cond_columns as _get_cond_columns +from ..estimation.utils import ( + ensure_evalid_set, + ensure_fia_instance, + validate_aggregation_result, + validate_required_columns, +) + +logger = logging.getLogger(__name__) + + +class UnderstoryEstimator(BaseEstimator): + """Understory vegetation carbon estimator. + + Reads pre-computed ``COND.CARBON_UNDERSTORY_AG`` and + ``COND.CARBON_UNDERSTORY_BG`` from the FIADB and aggregates via the + standard post-stratified estimation pipeline. + + Unlike :class:`LiveTreeEstimator` and :class:`StandingDeadEstimator`, + this is a **condition-level** estimator — there is no tree-level data. + The TREE table is not loaded; data comes from ``COND × PLOT`` only. + The adjustment factor is ``ADJ_FACTOR_SUBP`` (subplot-level), + matching FIADB/EVALIDator conventions for condition-level carbon + attributes. + """ + + _estimator_label = "Understory" + + # ------------------------------------------------------------------ + # Table / column requirements + # ------------------------------------------------------------------ + + def get_required_tables(self) -> list[str]: + # Condition-level estimator — no TREE table needed. + return ["COND", "PLOT", "POP_PLOT_STRATUM_ASSGN", "POP_STRATUM"] + + def get_tree_columns(self) -> list[str]: + return [] + + def get_cond_columns(self) -> list[str]: + cols = _get_cond_columns( + land_type=self.config.get("land_type", "forest"), + grp_by=self.config.get("grp_by"), + include_prop_basis=False, + ) + for c in ("CARBON_UNDERSTORY_AG", "CARBON_UNDERSTORY_BG"): + if c not in cols: + cols.append(c) + return cols + + # ------------------------------------------------------------------ + # Override apply_filters for condition-level data + # ------------------------------------------------------------------ + + def apply_filters(self, data: pl.LazyFrame) -> pl.LazyFrame: + """Apply condition/area filters only (no tree-type filtering).""" + from ..filtering import apply_area_filters, get_land_domain_indicator + + columns = data.collect_schema().names() + + if self.config.get("area_domain"): + data = apply_area_filters(data, area_domain=self.config["area_domain"]) + + land_type = self.config.get("land_type", "forest") + if land_type and land_type != "all" and "COND_STATUS_CD" in columns: + data = data.filter(get_land_domain_indicator(land_type)) + + return data + + # ------------------------------------------------------------------ + # Core estimation logic + # ------------------------------------------------------------------ + + def calculate_values(self, data: pl.LazyFrame) -> pl.LazyFrame: + """Select the understory carbon column and set CARBON_ACRE. + + The FIADB columns are already in short tons per acre per + condition — just pick the right pool and alias to CARBON_ACRE. + """ + pool = self.config.get("pool", "ag").lower() + + ag_col = pl.col("CARBON_UNDERSTORY_AG").cast(pl.Float64).fill_null(0.0) + bg_col = pl.col("CARBON_UNDERSTORY_BG").cast(pl.Float64).fill_null(0.0) + + if pool == "ag": + cond_carbon = ag_col + elif pool == "bg": + cond_carbon = bg_col + else: # total + cond_carbon = ag_col + bg_col + + return data.with_columns(cond_carbon.alias("CARBON_ACRE")) + + # ------------------------------------------------------------------ + # Aggregation — condition-level with ADJ_FACTOR_SUBP + # ------------------------------------------------------------------ + + def aggregate_results(self, data: pl.LazyFrame | None) -> AggregationResult: + if data is None: + return AggregationResult( + results=pl.DataFrame(), + plot_tree_data=pl.DataFrame(), + group_cols=[], + ) + + validate_required_columns( + data, ["PLT_CN", "CARBON_ACRE"], "understory carbon data" + ) + + strat_data = self._get_stratification_data() + data_with_strat = data.join(strat_data, on="PLT_CN", how="inner") + + # Condition-level attribute: CARBON_ACRE is a density (tons/acre). + # The two-stage pipeline denominator is sum(CONDPROP_UNADJ * EXPNS), + # so the numerator must include CONDPROP_UNADJ to give the + # condition's contribution proportional to its area on the plot. + # ADJ_FACTOR_SUBP corrects for nonresponse at the subplot level. + data_with_strat = data_with_strat.with_columns( + pl.col("ADJ_FACTOR_SUBP").cast(pl.Float64).alias("ADJ_FACTOR") + ) + + data_with_strat = data_with_strat.with_columns( + ( + pl.col("CARBON_ACRE") + * pl.col("CONDPROP_UNADJ").cast(pl.Float64) + * pl.col("ADJ_FACTOR") + ).alias("CARBON_ADJ") + ) + + group_cols = self._setup_grouping() + + plot_tree_data, data_with_strat = self._preserve_plot_tree_data( + data_with_strat, + metric_cols=["CARBON_ADJ"], + group_cols=group_cols, + ) + + results = self._apply_two_stage_aggregation( + data_with_strat=data_with_strat, + metric_mappings={"CARBON_ADJ": "CONDITION_CARBON"}, + group_cols=group_cols, + use_grm_adjustment=False, + ) + + if not self.config.get("totals", True): + if "CARBON_TOTAL" in results.columns: + results = results.drop("CARBON_TOTAL") + + return AggregationResult( + results=results, + plot_tree_data=plot_tree_data, + group_cols=group_cols, + ) + + # ------------------------------------------------------------------ + # Variance and output formatting + # ------------------------------------------------------------------ + + def calculate_variance(self, agg_result: AggregationResult) -> pl.DataFrame: + validate_aggregation_result(agg_result, self._estimator_label) + metric_configs = [ + { + "adjusted_col": "CARBON_ADJ", + "acre_se_col": "CARBON_ACRE_SE", + "total_se_col": "CARBON_TOTAL_SE", + }, + ] + return self._calculate_variance_for_metrics(agg_result, metric_configs) + + def format_output(self, results: pl.DataFrame) -> pl.DataFrame: + year = self._extract_evaluation_year() + results = results.with_columns(pl.lit(year).alias("YEAR")) + + pool = self.config.get("pool", "ag").upper() + results = results.with_columns(pl.lit(pool).alias("POOL")) + + col_order = [ + "YEAR", + "POOL", + "CARBON_ACRE", + "CARBON_TOTAL", + "CARBON_ACRE_SE", + "CARBON_TOTAL_SE", + "N_PLOTS", + "N_TREES", + ] + + for col in results.columns: + if col not in col_order: + col_order.insert(1, col) + + final_cols = [col for col in col_order if col in results.columns] + return results.select(final_cols) + + +# ====================================================================== +# Public API +# ====================================================================== + + +def understory( + db: str | FIA, + pool: str = "ag", + grp_by: str | list[str] | None = None, + land_type: str = "forest", + area_domain: str | None = None, + plot_domain: str | None = None, + totals: bool = True, + variance: bool = False, + most_recent: bool = False, +) -> pl.DataFrame: + """ + Estimate understory vegetation carbon from FIA data. + + Understory vegetation encompasses all biomass of undergrowth plants + in a forest, including woody shrubs and trees less than 2.54 cm DBH. + Carbon density is estimated from the Smith & Heath (2008) model, + parameterised on geographic area, forest type, and live tree carbon + density. The model descends from the Birdsey (1996) ratios and is + the same model USDA FIA uses to populate the FIADB + ``COND.CARBON_UNDERSTORY_AG`` and ``COND.CARBON_UNDERSTORY_BG`` + columns used in the EPA NGHGI. + + Nationally, understory carbon totals ~735 Tg C, representing ~0.8 % + of total forest ecosystem carbon (GTR-NRS-154, Table 2). Per-acre + values are typically 0.5–2.5 short tons/acre depending on forest type + and region. + + Parameters + ---------- + db : str | FIA + Database connection or path to FIA database. Can be either a path + string to a DuckDB/SQLite file or an existing FIA connection object. + pool : {'ag', 'bg', 'total'}, default 'ag' + Understory carbon pool to estimate: + + - 'ag': Above-ground understory carbon (seedlings + woody shrubs + above-ground portions). Approximately 90 % of total understory + carbon. + - 'bg': Below-ground understory carbon (fine roots of understory + vegetation). Approximately 10 % of total understory carbon + (Smith et al. 2006). + - 'total': ``'ag' + 'bg'``. + grp_by : str or list of str, optional + Column name(s) to group results by. Can be any column from the + FIA COND or PLOT tables. Common grouping columns include: + + - 'FORTYPCD': Forest type code + - 'FORTYPGRPCD': Forest type group code + - 'OWNGRPCD': Ownership group + - 'STATECD': State FIPS code + - 'COUNTYCD': County code + + For complete column descriptions, see USDA FIA Database User Guide. + land_type : {'forest', 'timber', 'all'}, default 'forest' + Land type to include in estimation: + + - 'forest': All forestland + - 'timber': Productive timberland only (unreserved, productive) + - 'all': All land conditions + area_domain : str, optional + SQL-like filter expression for area/condition-level filtering. + Example: ``"OWNGRPCD == 40 AND FORTYPCD == 161"``. + plot_domain : str, optional + SQL-like filter expression for plot-level filtering. + totals : bool, default True + If True, include population-level total estimates in addition to + per-acre values. + variance : bool, default False + If True, calculate and include variance and standard error + estimates following Bechtold & Patterson (2005). + most_recent : bool, default False + If True, automatically filter to the most recent EXPVOL evaluation + for each state in the database before estimation. + + Returns + ------- + pl.DataFrame + Understory carbon estimates with the following columns: + + - **YEAR** : int + Evaluation reference year from EVALID. + - **POOL** : str + Pool identifier — one of ``'AG'``, ``'BG'``, ``'TOTAL'``. + - **CARBON_ACRE** : float + Carbon per acre in short tons. + - **CARBON_TOTAL** : float (if ``totals=True``) + Total carbon in short tons expanded to population level. + - **CARBON_ACRE_SE** : float (if ``variance=True``) + Standard error of the per-acre estimate. + - **CARBON_TOTAL_SE** : float (if ``variance=True`` and ``totals=True``) + Standard error of the population total. + - **N_PLOTS** : int + Number of FIA plots included in the estimation. + - **N_TREES** : int + Number of tree records (used for distribution weighting). + - **[grouping columns]** : various + Any columns specified in ``grp_by``. + + See Also + -------- + live_tree : Estimate live tree carbon using the NSVB framework. + standing_dead : Estimate standing dead tree carbon. + pyfia.carbon : Overview of all carbon pool estimators. + + Notes + ----- + **Methodology** + + Understory vegetation is not directly measured on FIA plots. The + FIADB pre-computes carbon density per condition using a model based on + geographic area, forest type, and (for most stand types) live tree + carbon density (Smith & Heath 2008; GTR-NRS-13). For nonstocked and + pinyon-juniper stands, the model uses geographic area and forest type + alone. + + This estimator reads those pre-computed values directly from the COND + table and runs them through the standard post-stratified aggregation + pipeline, ensuring exact agreement with EVALIDator estimates. + + The underlying model traces back to Birdsey (1996), which established + understory-to-overstory carbon ratios by forest type group and region. + The ratios typically range from 0.5–3 % of live tree carbon for mature + stands, with higher ratios in young stands and in Southern pine types. + + **AG/BG Split** + + 10 % of total understory carbon is assigned to belowground (Smith et + al. 2006). This is a fixed proportion applied uniformly across all + forest types. + + **EVALID Handling** + + If no EVALID is set on the database and ``most_recent=True``, the + function auto-selects the most recent EXPVOL evaluation. + + Examples + -------- + Above-ground understory carbon per acre on forestland: + + >>> results = understory(db, pool="ag") + >>> print(f"Carbon: {results['CARBON_ACRE'][0]:.3f} tons/acre") + + Total understory carbon (AG + BG) by forest type group: + + >>> results = understory(db, pool="total", grp_by="FORTYPGRPCD") + >>> for row in results.iter_rows(named=True): + ... print(f"FTGRP {row['FORTYPGRPCD']}: {row['CARBON_ACRE']:.3f} tons/acre") + + Understory carbon on timberland with standard errors: + + >>> results = understory( + ... db, + ... pool="total", + ... land_type="timber", + ... variance=True, + ... ) + + References + ---------- + .. [1] Birdsey, R.A. (1996). Carbon storage for major forest types and + regions in the coterminous United States. In: Sampson, R.N.; Hair, D., + eds. *Forests and Global Change, Volume 2*. American Forests, + Washington, DC. pp. 1-26. + .. [2] Smith, J.E.; Heath, L.S.; Skog, K.E.; Birdsey, R.A. (2006). + Methods for calculating forest ecosystem and harvested carbon with + standard estimates for forest types of the United States. + Gen. Tech. Rep. NE-343. Newtown Square, PA: USDA Forest Service. + .. [3] Smith, J.E.; Heath, L.S. (2008). Carbon stocks and stock changes + in U.S. forests. In: USDA Forest Service GTR-NRS-13. + .. [4] USEPA (2024). Inventory of U.S. Greenhouse Gas Emissions and + Sinks, Chapter 6 and Annex 3.13. + """ + from ..validation import ( + validate_boolean, + validate_domain_expression, + validate_grp_by, + validate_land_type, + ) + + # ----- Validate pool ----- + pool = pool.lower() + valid_pools = {"ag", "bg", "total"} + if pool not in valid_pools: + raise ValueError( + f"Invalid pool '{pool}'. Must be one of: {sorted(valid_pools)}" + ) + + # ----- Validate standard inputs ----- + land_type = validate_land_type(land_type) + grp_by = validate_grp_by(grp_by) + area_domain = validate_domain_expression(area_domain, "area_domain") + plot_domain = validate_domain_expression(plot_domain, "plot_domain") + totals = validate_boolean(totals, "totals") + variance = validate_boolean(variance, "variance") + most_recent = validate_boolean(most_recent, "most_recent") + + # ----- Resolve db + EVALID ----- + db, owns_db = ensure_fia_instance(db) + if most_recent and db.evalid is None: + db.clip_most_recent(eval_type="VOL") + else: + ensure_evalid_set(db, eval_type="VOL", estimator_name="understory") + + # ----- Build config and run estimator ----- + config = { + "pool": pool, + "grp_by": grp_by, + "by_species": False, + "by_size_class": False, + "land_type": land_type, + "area_domain": area_domain, + "plot_domain": plot_domain, + "totals": totals, + "variance": variance, + "most_recent": most_recent, + } + + try: + estimator = UnderstoryEstimator(db, config) + return estimator.estimate() + finally: + if owns_db and hasattr(db, "close"): + db.close() diff --git a/src/pyfia/core/fia.py b/src/pyfia/core/fia.py index c96a1fa4..385e3b42 100755 --- a/src/pyfia/core/fia.py +++ b/src/pyfia/core/fia.py @@ -30,7 +30,6 @@ logger = logging.getLogger(__name__) - class FIA: """ Main FIA database class for working with Forest Inventory and Analysis data. @@ -1304,6 +1303,86 @@ def area_change(self, **kwargs) -> pl.DataFrame: return area_change(self, **kwargs) + def live_tree(self, **kwargs) -> pl.DataFrame: + """ + Estimate live tree carbon using the NSVB framework. + + See live_tree() function for full parameter documentation. + """ + from pyfia.carbon import live_tree + + return live_tree(self, **kwargs) + + def standing_dead(self, **kwargs) -> pl.DataFrame: + """ + Estimate standing dead tree carbon using the NSVB framework. + + See standing_dead() function for full parameter documentation. + """ + from pyfia.carbon import standing_dead + + return standing_dead(self, **kwargs) + + def understory(self, **kwargs) -> pl.DataFrame: + """ + Estimate understory vegetation carbon. + + See understory() function for full parameter documentation. + """ + from pyfia.carbon import understory + + return understory(self, **kwargs) + + def downed_dead(self, **kwargs) -> pl.DataFrame: + """ + Estimate downed dead wood carbon. + + See downed_dead() function for full parameter documentation. + """ + from pyfia.carbon import downed_dead + + return downed_dead(self, **kwargs) + + def litter(self, **kwargs) -> pl.DataFrame: + """ + Estimate litter carbon. + + See litter() function for full parameter documentation. + """ + from pyfia.carbon import litter + + return litter(self, **kwargs) + + def soil_organic(self, **kwargs) -> pl.DataFrame: + """ + Estimate soil organic carbon. + + See soil_organic() function for full parameter documentation. + """ + from pyfia.carbon import soil_organic + + return soil_organic(self, **kwargs) + + def total_ecosystem(self, **kwargs) -> pl.DataFrame: + """ + Estimate total ecosystem carbon across all six pools. + + See total_ecosystem() function for full parameter documentation. + """ + from pyfia.carbon.total_ecosystem import total_ecosystem + + return total_ecosystem(self, **kwargs) + + def stock_change(self, **kwargs) -> pl.DataFrame: + """ + Estimate carbon stock change between inventory periods. + + See stock_change() function for full parameter documentation. + """ + from pyfia.carbon.stock_change import stock_change + + return stock_change(self, **kwargs) + class MotherDuckFIA(FIA): """ diff --git a/src/pyfia/downloader/tables.py b/src/pyfia/downloader/tables.py index 4c4a465f..9fd106e5 100644 --- a/src/pyfia/downloader/tables.py +++ b/src/pyfia/downloader/tables.py @@ -13,12 +13,16 @@ from __future__ import annotations # Common tables required for pyFIA estimation functions -# These match the rFIA "common=TRUE" default tables +# These match the rFIA "common=TRUE" default tables, plus pyfia-specific +# additions that the estimators need. COMMON_TABLES: list[str] = [ "COND", # Condition data "COND_DWM_CALC", # Down woody material calculations "INVASIVE_SUBPLOT_SPP", # Invasive species subplot data "PLOT", # Plot-level data + "PLOTGEOM", # Plot geometry & ECOSUBCD (Bailey ecoprovince, used by + # pyfia.carbon.live_tree for the Phase 1.5+ DIVISION coefficient + # lookup; not in rFIA common tables). "POP_ESTN_UNIT", # Population estimation units "POP_EVAL", # Population evaluations "POP_EVAL_GRP", # Population evaluation groups diff --git a/tests/conftest.py b/tests/conftest.py index 05321b35..bb50e777 100755 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -75,7 +75,11 @@ def georgia_db(georgia_db_path): ) db = FIA(db_path) yield db - db.close() + # FIA class cleanup is handled by FIADataReader; only MotherDuckFIA + # exposes an explicit close(). Guard so session teardown doesn't raise + # AttributeError on the common DuckDB file path. + if hasattr(db, "close"): + db.close() @pytest.fixture @@ -86,7 +90,11 @@ def fia_db(georgia_db_path): ) db = FIA(db_path) yield db - db.close() + # FIA class cleanup is handled by FIADataReader; only MotherDuckFIA + # exposes an explicit close(). Guard so session teardown doesn't raise + # AttributeError on the common DuckDB file path. + if hasattr(db, "close"): + db.close() @pytest.fixture diff --git a/tests/property/test_nsvb_properties.py b/tests/property/test_nsvb_properties.py new file mode 100644 index 00000000..18aac602 --- /dev/null +++ b/tests/property/test_nsvb_properties.py @@ -0,0 +1,316 @@ +""" +Property-based tests for the NSVB equation library. + +Uses Hypothesis to generate random valid inputs (DBH, height, coefficient +shapes, etc.) and verifies invariants that must hold for all valid inputs: + +1. **Harmonization additivity**: ``wood_h + bark_h + branch_h == agb_predicted`` + to floating-point precision, for any positive component values. +2. **Carbon fraction bounds**: every value returned by + ``get_carbon_fraction_live`` is in [0.30, 0.65] (the actual S10a range). +3. **Model 1 monotonicity in D**: for fixed H, ``model_1`` is monotonically + increasing in D when b > 0 (which it always is in the NSVB tables). +4. **Model 1 monotonicity in H**: for fixed D, ``model_1`` is monotonically + increasing in H when c > 0 (the common case). +5. **Model 4 reduces to Model 1 when b1=0**: an algebraic invariant that + pins the exp factor to 1 in the degenerate case. +6. **Pipeline produces positive AGB for any valid tree**: smoke test that + ``predict_tree_biomass`` doesn't return negative or NaN values for the + Douglas-fir coefficient bundle across the full DBH range [1, 60]. +""" + +from __future__ import annotations + +import math + +from hypothesis import assume, given, settings +from hypothesis import strategies as st + +from pyfia.carbon.nsvb.carbon_fractions import ( + get_carbon_fraction_live, + load_carbon_fractions_live, +) +from pyfia.carbon.nsvb.equations import ( + Coefficients, + harmonize_components, + model_1, + model_4, + predict_tree_biomass, +) + +# Reuse the Douglas-fir coefficients from the equations test as a stable +# coefficient bundle for pipeline property tests. +_DOUGFIR_COEFS = Coefficients( + volib={ + "model": 2, + "a": 0.001929099661, + "a1": 0.0, + "b": 2.162413104203, + "b1": 1.690400253097, + "c": 0.985444005253, + "c1": 0.0, + "source": "test", + }, + volbk={ + "model": 1, + "a": 0.000031886237, + "a1": 0.0, + "b": 1.21260513951, + "b1": 0.0, + "c": 1.978577263767, + "c1": 0.0, + "source": "test", + }, + bark_bio={ + "model": 1, + "a": 0.009106538193, + "a1": 0.0, + "b": 1.437894424586, + "b1": 0.0, + "c": 1.336514272981, + "c1": 0.0, + "source": "test", + }, + branch_bio={ + "model": 1, + "a": 9.521330809106, + "a1": 0.0, + "b": 1.762316117442, + "b1": 0.0, + "c": -0.40574259177, + "c1": 0.0, + "source": "test", + }, + total_agb={ + "model": 1, + "a": 0.135206506787, + "a1": 0.0, + "b": 1.713527048035, + "b1": 0.0, + "c": 1.047613377046, + "c1": 0.0, + "source": "test", + }, +) + + +# --------------------------------------------------------------------------- +# Harmonization invariant +# --------------------------------------------------------------------------- + + +@given( + agb=st.floats(min_value=0.1, max_value=1e6, allow_nan=False, allow_infinity=False), + w_wood=st.floats( + min_value=0.01, max_value=1e6, allow_nan=False, allow_infinity=False + ), + w_bark=st.floats( + min_value=0.01, max_value=1e6, allow_nan=False, allow_infinity=False + ), + w_branch=st.floats( + min_value=0.01, max_value=1e6, allow_nan=False, allow_infinity=False + ), +) +@settings(max_examples=200) +def test_harmonization_additivity(agb, w_wood, w_bark, w_branch): + """Harmonized components must sum exactly to the predicted AGB. + + This is the load-bearing invariant for any downstream code that needs + additive decomposition (e.g., merchantable subdivisions in Phase 2+). + """ + wood_h, bark_h, branch_h = harmonize_components(agb, w_wood, w_bark, w_branch) + total = wood_h + bark_h + branch_h + assert math.isclose(total, agb, rel_tol=1e-12) + + +@given( + agb=st.floats(min_value=1.0, max_value=1e5), + w_wood=st.floats(min_value=1.0, max_value=1e5), + w_bark=st.floats(min_value=1.0, max_value=1e5), + w_branch=st.floats(min_value=1.0, max_value=1e5), +) +@settings(max_examples=200) +def test_harmonization_preserves_relative_ratios(agb, w_wood, w_bark, w_branch): + """The harmonized components preserve the relative ratios of the inputs. + + If raw wood:bark = 4:2, harmonized wood:bark must also be 4:2. + """ + wood_h, bark_h, _ = harmonize_components(agb, w_wood, w_bark, w_branch) + raw_ratio = w_wood / w_bark + harmonized_ratio = wood_h / bark_h + assert math.isclose(raw_ratio, harmonized_ratio, rel_tol=1e-12) + + +# --------------------------------------------------------------------------- +# Carbon fraction bounds +# --------------------------------------------------------------------------- + + +def test_all_carbon_fractions_in_decimal_range(): + """Every value in S10a must be in the decimal range, not percent. + + This is a smoke test of the percent→decimal normalization. We use a + direct iteration rather than hypothesis because we want to check + every species, not a random sample. + """ + table = load_carbon_fractions_live() + for spcd, frac in table.items(): + assert 0.30 <= frac <= 0.65, f"SPCD={spcd} carbon fraction {frac} out of range" + + +@given(spcd=st.sampled_from(sorted(load_carbon_fractions_live().keys()))) +@settings(max_examples=200) +def test_get_carbon_fraction_live_returns_known_value(spcd): + """For any SPCD known to S10a, the lookup returns the table value + (not the fallback).""" + table = load_carbon_fractions_live() + result = get_carbon_fraction_live(spcd) + assert result == table[spcd] + + +# --------------------------------------------------------------------------- +# Model 1 monotonicity +# --------------------------------------------------------------------------- + + +@given( + d_lo=st.floats(min_value=1.0, max_value=30.0), + d_hi=st.floats(min_value=1.0, max_value=30.0), + h=st.floats(min_value=10.0, max_value=200.0), + a=st.floats(min_value=1e-6, max_value=10.0), + b=st.floats(min_value=0.5, max_value=3.0), + c=st.floats(min_value=0.5, max_value=2.0), +) +@settings(max_examples=200) +def test_model_1_monotonic_in_d(d_lo, d_hi, h, a, b, c): + """For fixed H and positive coefficients, Model 1 is increasing in D. + + Models with b > 0 (the universal case in the NSVB tables) must satisfy + ``y(D_lo, H) < y(D_hi, H)`` whenever ``D_lo < D_hi``. + + Note on the 1e-6 gap: the strict-inequality assertion holds mathematically + but fails at floating-point ULP-level differences (e.g., d_lo=1.0 and + d_hi=1.0000000000000002 map to indistinguishable f64 outputs). We only + test the property when the two diameters differ by at least 1e-6 inches, + which is well below any meaningful tree-measurement precision. + """ + assume(abs(d_hi - d_lo) > 1e-6) + lo, hi = sorted([d_lo, d_hi]) + y_lo = model_1(lo, h, a, b, c) + y_hi = model_1(hi, h, a, b, c) + assert y_lo < y_hi + + +@given( + d=st.floats(min_value=1.0, max_value=30.0), + h_lo=st.floats(min_value=10.0, max_value=200.0), + h_hi=st.floats(min_value=10.0, max_value=200.0), + a=st.floats(min_value=1e-6, max_value=10.0), + b=st.floats(min_value=0.5, max_value=3.0), + c=st.floats(min_value=0.5, max_value=2.0), +) +@settings(max_examples=200) +def test_model_1_monotonic_in_h_when_c_positive(d, h_lo, h_hi, a, b, c): + """For fixed D and positive c, Model 1 is increasing in H. + + Note that c can be negative for some components (e.g., branch biomass + has c≈-0.41 — branch mass declines with height for fixed DBH). This test + only covers the positive-c case; the negative-c case is the dual. + + Uses a 1e-6 ft minimum gap between heights for the same f64-ULP reason + documented in ``test_model_1_monotonic_in_d``. + """ + assume(abs(h_hi - h_lo) > 1e-6) + lo, hi = sorted([h_lo, h_hi]) + y_lo = model_1(d, lo, a, b, c) + y_hi = model_1(d, hi, a, b, c) + assert y_lo < y_hi + + +# --------------------------------------------------------------------------- +# Model 4 algebraic invariant +# --------------------------------------------------------------------------- + + +@given( + d=st.floats(min_value=1.0, max_value=30.0), + h=st.floats(min_value=10.0, max_value=200.0), + a=st.floats(min_value=1e-6, max_value=10.0), + b=st.floats(min_value=0.5, max_value=3.0), + c=st.floats(min_value=-1.0, max_value=2.0), +) +@settings(max_examples=200) +def test_model_4_reduces_to_model_1_when_b1_zero(d, h, a, b, c): + """When b1 == 0, exp(0) == 1 and Model 4 collapses to Model 1.""" + m1 = model_1(d, h, a, b, c) + m4 = model_4(d, h, a, b, b1=0.0, c=c) + assert math.isclose(m1, m4, rel_tol=1e-12) + + +# --------------------------------------------------------------------------- +# Pipeline robustness +# --------------------------------------------------------------------------- + + +@given( + dia=st.floats(min_value=1.0, max_value=60.0), + ht=st.floats(min_value=10.0, max_value=200.0), +) +@settings(max_examples=100) +def test_pipeline_returns_positive_agb_for_valid_inputs(dia, ht): + """Pipeline must return positive, finite AGB across the practical + DBH and height range for the Douglas-fir coefficient bundle.""" + result = predict_tree_biomass( + spcd=202, + dia=dia, + ht=ht, + coefficients=_DOUGFIR_COEFS, + wdsg=0.45, + hw_sw="softwood", + cull=0.0, + ) + assert result.agb > 0 + assert math.isfinite(result.agb) + # Component sum must equal AGB to floating-point precision + assert math.isclose( + result.w_wood + result.w_bark + result.w_branch, + result.agb, + rel_tol=1e-12, + ) + + +@given( + dia=st.floats(min_value=5.0, max_value=40.0), + ht=st.floats(min_value=20.0, max_value=150.0), + cull=st.floats(min_value=0.0, max_value=50.0), +) +@settings(max_examples=100) +def test_cull_reduces_agb(dia, ht, cull): + """Increasing cull percentage must monotonically reduce the harmonized AGB. + + For live trees, cull only deducts from wood weight (not bark or branch), + so AGBReduce < 1 whenever CULL > 0. + """ + no_cull = predict_tree_biomass( + spcd=202, + dia=dia, + ht=ht, + coefficients=_DOUGFIR_COEFS, + wdsg=0.45, + hw_sw="softwood", + cull=0.0, + ) + with_cull = predict_tree_biomass( + spcd=202, + dia=dia, + ht=ht, + coefficients=_DOUGFIR_COEFS, + wdsg=0.45, + hw_sw="softwood", + cull=cull, + ) + if cull == 0.0: + assert math.isclose(no_cull.agb, with_cull.agb, rel_tol=1e-12) + else: + # Cull must reduce or hold AGB equal (it never increases it) + assert with_cull.agb <= no_cull.agb diff --git a/tests/unit/test_carbon_fractions.py b/tests/unit/test_carbon_fractions.py new file mode 100644 index 00000000..ab9b3b42 --- /dev/null +++ b/tests/unit/test_carbon_fractions.py @@ -0,0 +1,216 @@ +""" +Unit tests for the NSVB carbon fraction lookup module. + +Verifies: + +1. S10a CSV loads correctly with the trimmed schema (SPCD, hw_sw, fia_wood_c) +2. Percent → decimal normalization (the source CSV stores e.g. 48.04, not 0.4804) +3. SPCD=202 (Douglas-fir) and SPCD=316 (red maple) lookups match worked-example values +4. Unknown SPCD falls back to the national mean and emits exactly one warning +5. S10b dead carbon fractions load with the (hw_sw, decaycd) keying +""" + +from __future__ import annotations + +import logging + +import polars as pl + +from pyfia.carbon.nsvb import carbon_fractions +from pyfia.carbon.nsvb.carbon_fractions import ( + DEFAULT_LIVE_CARBON_FRACTION, + get_carbon_fraction_dead, + get_carbon_fraction_live, + load_carbon_fractions_dead, + load_carbon_fractions_live, + load_carbon_fractions_live_df, +) + + +class TestLoadCarbonFractionsLive: + """S10a loader: ``carbon_fraction_live.csv`` → dict[SPCD] -> float.""" + + def test_returns_dict(self): + result = load_carbon_fractions_live() + assert isinstance(result, dict) + + def test_has_2676_species(self): + """The trimmed S10a has 2676 species rows.""" + result = load_carbon_fractions_live() + assert len(result) == 2676 + + def test_lru_cache(self): + first = load_carbon_fractions_live() + second = load_carbon_fractions_live() + assert first is second + + def test_values_in_decimal_form(self): + """All values must be in decimal form, not percent. + + Bounds are [0.30, 0.65] — wide enough to capture the actual S10a + range (min 0.365, max 0.609 across the 2676 species) but tight + enough to detect a percent-vs-decimal unit error (which would put + values at 30-65 instead of 0.30-0.65). The spec section 3.1.2 + cited "~0.474 mean" but the per-species range is wider than I + initially assumed — measured directly from the vendored CSV. + """ + result = load_carbon_fractions_live() + for spcd, frac in result.items(): + assert 0.30 <= frac <= 0.65, f"SPCD={spcd} has out-of-range fraction {frac}" + + def test_mean_close_to_population_mean(self): + """The S10a population mean is ~0.474 per spec section 3.1.2.""" + result = load_carbon_fractions_live() + mean = sum(result.values()) / len(result) + assert 0.46 <= mean <= 0.49, ( + f"Population mean {mean:.4f} outside expected range" + ) + + +class TestGetCarbonFractionLive: + """Per-species lookup with fallback.""" + + def test_douglas_fir_matches_worked_example(self): + """SPCD=202 should be ~0.5156 per S10a (worked example line 680).""" + result = get_carbon_fraction_live(202) + # Expected: 51.55958333 / 100 = 0.5155958333 + assert abs(result - 0.5155958333) < 1e-6 + + def test_red_maple_matches_s10a(self): + """SPCD=316 red maple should be ~0.4857 per S10a.""" + result = get_carbon_fraction_live(316) + # Expected: 48.57333333 / 100 = 0.4857333333 + assert abs(result - 0.4857333333) < 1e-6 + + def test_unknown_spcd_returns_fallback(self): + """SPCD with no S10a entry returns the national-mean default.""" + result = get_carbon_fraction_live(99999) + assert result == DEFAULT_LIVE_CARBON_FRACTION + + def test_unknown_spcd_warns_only_once(self, caplog): + """First lookup of an unknown SPCD logs a warning; subsequent + lookups for the same SPCD do not (to avoid log spam).""" + # Reset the warned-set so this test is reproducible + carbon_fractions._warned_unknown_spcds.clear() + + with caplog.at_level( + logging.WARNING, logger="pyfia.carbon.nsvb.carbon_fractions" + ): + get_carbon_fraction_live(99998) + get_carbon_fraction_live(99998) + get_carbon_fraction_live(99998) + + warning_count = sum(1 for r in caplog.records if "99998" in r.message) + assert warning_count == 1 + + def test_default_fallback_value(self): + """The default fallback is the S10a arithmetic mean. + + Computed lazily from the vendored S10a table on first access (PEP 562 + ``__getattr__``). The value tracks the CSV so a future re-vendor + automatically updates the constant — this is the regression guard + against the hardcoded 0.4716 value that had drifted from the actual + table mean (~0.4741). + """ + # Matches GTR-WO-104 "live ~47.4% mean" notation. + assert 0.47 < DEFAULT_LIVE_CARBON_FRACTION < 0.48 + # Must exactly equal the arithmetic mean of the loaded dict. + table = load_carbon_fractions_live() + expected = sum(table.values()) / len(table) + assert DEFAULT_LIVE_CARBON_FRACTION == expected + + def test_custom_fallback(self): + """Caller can override the fallback value.""" + result = get_carbon_fraction_live(99997, fallback=0.50) + assert result == 0.50 + + +class TestLoadCarbonFractionsLiveDf: + """S10a polars loader: the join-ready DataFrame used by the vectorized path.""" + + def test_returns_polars_dataframe(self): + result = load_carbon_fractions_live_df() + assert isinstance(result, pl.DataFrame) + + def test_columns(self): + """Schema is exactly (SPCD Int64, CARBON_FRAC_LIVE Float64).""" + result = load_carbon_fractions_live_df() + assert result.columns == ["SPCD", "CARBON_FRAC_LIVE"] + assert result.schema["SPCD"] == pl.Int64 + assert result.schema["CARBON_FRAC_LIVE"] == pl.Float64 + + def test_matches_dict_loader(self): + """The DataFrame loader must agree with the dict loader row-for-row. + + Both loaders consume the same vendored CSV, so every SPCD in one + must appear in the other with the same float value. This catches + drift if someone modifies one loader without the other. + """ + df = load_carbon_fractions_live_df() + table = load_carbon_fractions_live() + assert df.height == len(table) + for row in df.iter_rows(named=True): + assert row["SPCD"] in table + assert abs(row["CARBON_FRAC_LIVE"] - table[row["SPCD"]]) < 1e-12 + + def test_percent_to_decimal_conversion(self): + """Values are in decimal form (e.g., 0.4804), not percent (48.04).""" + df = load_carbon_fractions_live_df() + # All fractions should be in the [0.3, 0.7] range + assert df["CARBON_FRAC_LIVE"].min() > 0.3 + assert df["CARBON_FRAC_LIVE"].max() < 0.7 + + def test_lru_cache_returns_same_instance(self): + first = load_carbon_fractions_live_df() + second = load_carbon_fractions_live_df() + assert first is second + + def test_join_ready(self): + """The loader output is suitable for a left join to a trees frame.""" + df = load_carbon_fractions_live_df() + trees = pl.DataFrame({"SPCD": [202, 316, 99999]}) + joined = trees.join(df, on="SPCD", how="left") + # Douglas-fir and red maple should have non-null CARBON_FRAC_LIVE + assert joined.filter(pl.col("SPCD") == 202)["CARBON_FRAC_LIVE"][0] is not None + assert joined.filter(pl.col("SPCD") == 316)["CARBON_FRAC_LIVE"][0] is not None + # Unknown SPCD should have null (caller fills with DEFAULT) + assert joined.filter(pl.col("SPCD") == 99999)["CARBON_FRAC_LIVE"][0] is None + + +class TestLoadCarbonFractionsDead: + """S10b loader: dead tree carbon fractions by hw/sw × decay class.""" + + def test_returns_dict(self): + result = load_carbon_fractions_dead() + assert isinstance(result, dict) + + def test_ten_rows(self): + """S10b has 10 entries: 2 hw/sw × 5 decay classes.""" + result = load_carbon_fractions_dead() + assert len(result) == 10 + + def test_lookup_hardwood_decay_3(self): + """Verify the hardwood/decay-3 entry exists and is reasonable.""" + result = get_carbon_fraction_dead("hardwood", 3) + # Expected: ~0.473 (47.3%) per S10b + assert 0.46 <= result <= 0.49 + + def test_lookup_softwood_decay_5(self): + """Verify the softwood/decay-5 entry exists.""" + result = get_carbon_fraction_dead("softwood", 5) + assert 0.50 <= result <= 0.55 + + def test_case_insensitive_hw_sw(self): + """``Hardwood`` and ``hardwood`` should both work.""" + a = get_carbon_fraction_dead("Hardwood", 1) + b = get_carbon_fraction_dead("hardwood", 1) + assert a == b + + def test_all_decay_classes_present(self): + """Every decay class 1-5 must exist for both hardwood and softwood.""" + for hw_sw in ("hardwood", "softwood"): + for decay in range(1, 6): + result = get_carbon_fraction_dead(hw_sw, decay) + assert 0.40 <= result <= 0.55, ( + f"({hw_sw}, decay={decay}) -> {result} out of range" + ) diff --git a/tests/unit/test_downed_dead_estimator.py b/tests/unit/test_downed_dead_estimator.py new file mode 100644 index 00000000..97a103e7 --- /dev/null +++ b/tests/unit/test_downed_dead_estimator.py @@ -0,0 +1,143 @@ +"""Unit tests for DownedDeadEstimator class. + +Tests the DownedDeadEstimator methods in isolation using mock data. +No database connection required. +""" + +import polars as pl +import pytest + +from pyfia.carbon.downed_dead import DownedDeadEstimator, downed_dead + + +class MockDB: + """Mock database for testing estimator methods in isolation.""" + + def __init__(self): + self.db_path = "/fake/path" + self.tables = {} + self.evalid = None + self.evalids = None + self._state_filter = None + + +class TestGetRequiredTables: + """Tests for get_required_tables method.""" + + def test_returns_condition_level_tables(self): + config = {"pool": "total", "land_type": "forest"} + estimator = DownedDeadEstimator(MockDB(), config) + tables = estimator.get_required_tables() + + assert "COND" in tables + assert "PLOT" in tables + assert "POP_PLOT_STRATUM_ASSGN" in tables + assert "POP_STRATUM" in tables + + def test_tree_table_not_required(self): + config = {"pool": "total", "land_type": "forest"} + estimator = DownedDeadEstimator(MockDB(), config) + tables = estimator.get_required_tables() + assert "TREE" not in tables + + +class TestGetTreeColumns: + def test_returns_empty_list(self): + config = {"pool": "total"} + estimator = DownedDeadEstimator(MockDB(), config) + cols = estimator.get_tree_columns() + assert cols == [] + + +class TestGetCondColumns: + def test_includes_carbon_down_dead_column(self): + config = {"land_type": "forest"} + estimator = DownedDeadEstimator(MockDB(), config) + cols = estimator.get_cond_columns() + + assert "CARBON_DOWN_DEAD" in cols + assert "PLT_CN" in cols + assert "CONDID" in cols + assert "CONDPROP_UNADJ" in cols + assert "COND_STATUS_CD" in cols + + +class TestCalculateValues: + """Tests for calculate_values method.""" + + @pytest.fixture + def mock_db(self): + return MockDB() + + def test_total_pool(self, mock_db): + config = {"pool": "total"} + estimator = DownedDeadEstimator(mock_db, config) + + data = pl.DataFrame( + {"CARBON_DOWN_DEAD": [2.5, 1.3]} + ).lazy() + + result = estimator.calculate_values(data).collect() + assert abs(result["CARBON_ACRE"][0] - 2.5) < 1e-10 + assert abs(result["CARBON_ACRE"][1] - 1.3) < 1e-10 + + def test_null_values_treated_as_zero(self, mock_db): + config = {"pool": "total"} + estimator = DownedDeadEstimator(mock_db, config) + + data = pl.DataFrame( + {"CARBON_DOWN_DEAD": [None, 1.0]} + ).lazy() + + result = estimator.calculate_values(data).collect() + assert result["CARBON_ACRE"][0] == 0.0 + assert abs(result["CARBON_ACRE"][1] - 1.0) < 1e-10 + + def test_all_null_gives_zero(self, mock_db): + config = {"pool": "total"} + estimator = DownedDeadEstimator(mock_db, config) + + data = pl.DataFrame( + {"CARBON_DOWN_DEAD": [None]} + ).lazy() + + result = estimator.calculate_values(data).collect() + assert result["CARBON_ACRE"][0] == 0.0 + + def test_empty_dataframe(self, mock_db): + config = {"pool": "total"} + estimator = DownedDeadEstimator(mock_db, config) + + data = pl.DataFrame( + {"CARBON_DOWN_DEAD": pl.Series([], dtype=pl.Float64)} + ).lazy() + + result = estimator.calculate_values(data).collect() + assert len(result) == 0 + + +class TestPoolValidation: + def test_invalid_pool_raises_error(self): + with pytest.raises(ValueError, match="Invalid pool"): + downed_dead(MockDB(), pool="ag") + + def test_bg_pool_raises_error(self): + with pytest.raises(ValueError, match="no AG/BG split"): + downed_dead(MockDB(), pool="bg") + + def test_total_pool_accepted(self): + # Should not raise ValueError on pool validation + # (will fail on DB access later) + with pytest.raises(Exception): + downed_dead(MockDB(), pool="total") + + def test_case_insensitive_total(self): + with pytest.raises(Exception): + downed_dead(MockDB(), pool="TOTAL") + + +class TestEstimatorLabel: + def test_label_is_downed_dead(self): + config = {"pool": "total"} + estimator = DownedDeadEstimator(MockDB(), config) + assert estimator._estimator_label == "DownedDead" diff --git a/tests/unit/test_litter_estimator.py b/tests/unit/test_litter_estimator.py new file mode 100644 index 00000000..16bfb2f7 --- /dev/null +++ b/tests/unit/test_litter_estimator.py @@ -0,0 +1,141 @@ +"""Unit tests for LitterEstimator class. + +Tests the LitterEstimator methods in isolation using mock data. +No database connection required. +""" + +import polars as pl +import pytest + +from pyfia.carbon.litter import LitterEstimator, litter + + +class MockDB: + """Mock database for testing estimator methods in isolation.""" + + def __init__(self): + self.db_path = "/fake/path" + self.tables = {} + self.evalid = None + self.evalids = None + self._state_filter = None + + +class TestGetRequiredTables: + """Tests for get_required_tables method.""" + + def test_returns_condition_level_tables(self): + config = {"pool": "total", "land_type": "forest"} + estimator = LitterEstimator(MockDB(), config) + tables = estimator.get_required_tables() + + assert "COND" in tables + assert "PLOT" in tables + assert "POP_PLOT_STRATUM_ASSGN" in tables + assert "POP_STRATUM" in tables + + def test_tree_table_not_required(self): + config = {"pool": "total", "land_type": "forest"} + estimator = LitterEstimator(MockDB(), config) + tables = estimator.get_required_tables() + assert "TREE" not in tables + + +class TestGetTreeColumns: + def test_returns_empty_list(self): + config = {"pool": "total"} + estimator = LitterEstimator(MockDB(), config) + cols = estimator.get_tree_columns() + assert cols == [] + + +class TestGetCondColumns: + def test_includes_carbon_litter_column(self): + config = {"land_type": "forest"} + estimator = LitterEstimator(MockDB(), config) + cols = estimator.get_cond_columns() + + assert "CARBON_LITTER" in cols + assert "PLT_CN" in cols + assert "CONDID" in cols + assert "CONDPROP_UNADJ" in cols + assert "COND_STATUS_CD" in cols + + +class TestCalculateValues: + """Tests for calculate_values method.""" + + @pytest.fixture + def mock_db(self): + return MockDB() + + def test_total_pool(self, mock_db): + config = {"pool": "total"} + estimator = LitterEstimator(mock_db, config) + + data = pl.DataFrame( + {"CARBON_LITTER": [5.2, 3.8]} + ).lazy() + + result = estimator.calculate_values(data).collect() + assert abs(result["CARBON_ACRE"][0] - 5.2) < 1e-10 + assert abs(result["CARBON_ACRE"][1] - 3.8) < 1e-10 + + def test_null_values_treated_as_zero(self, mock_db): + config = {"pool": "total"} + estimator = LitterEstimator(mock_db, config) + + data = pl.DataFrame( + {"CARBON_LITTER": [None, 4.0]} + ).lazy() + + result = estimator.calculate_values(data).collect() + assert result["CARBON_ACRE"][0] == 0.0 + assert abs(result["CARBON_ACRE"][1] - 4.0) < 1e-10 + + def test_all_null_gives_zero(self, mock_db): + config = {"pool": "total"} + estimator = LitterEstimator(mock_db, config) + + data = pl.DataFrame( + {"CARBON_LITTER": [None]} + ).lazy() + + result = estimator.calculate_values(data).collect() + assert result["CARBON_ACRE"][0] == 0.0 + + def test_empty_dataframe(self, mock_db): + config = {"pool": "total"} + estimator = LitterEstimator(mock_db, config) + + data = pl.DataFrame( + {"CARBON_LITTER": pl.Series([], dtype=pl.Float64)} + ).lazy() + + result = estimator.calculate_values(data).collect() + assert len(result) == 0 + + +class TestPoolValidation: + def test_invalid_pool_raises_error(self): + with pytest.raises(ValueError, match="Invalid pool"): + litter(MockDB(), pool="ag") + + def test_bg_pool_raises_error(self): + with pytest.raises(ValueError, match="no AG/BG split"): + litter(MockDB(), pool="bg") + + def test_total_pool_accepted(self): + with pytest.raises(Exception): + litter(MockDB(), pool="total") + + def test_case_insensitive_total(self): + with pytest.raises(Exception): + litter(MockDB(), pool="TOTAL") + + +class TestEstimatorLabel: + def test_label_is_litter(self): + config = {"pool": "total"} + estimator = LitterEstimator(MockDB(), config) + assert estimator._estimator_label == "Litter" diff --git a/tests/unit/test_live_tree_estimator.py b/tests/unit/test_live_tree_estimator.py new file mode 100644 index 00000000..2db84e2b --- /dev/null +++ b/tests/unit/test_live_tree_estimator.py @@ -0,0 +1,250 @@ +"""Unit tests for the LiveTreeEstimator class and live_tree() public function. + +Two layers of coverage: + +1. **Isolated unit tests** (the bulk of this file) — exercise config wiring, + column selection, pool dispatch, and input validation against a MockDB + with no real FIA database. These run in CI unconditionally. + +2. **Database smoke test** (``test_live_tree_smoke_ag_pool``) — exercises + the full template-method pipeline against the Georgia test database + if available (via the ``georgia_db`` fixture from ``tests/conftest.py``). + Skipped automatically when no database is present. + +The equivalence between the vectorized NSVB pipeline and the scalar oracle +is tested in ``tests/unit/test_nsvb_vectorized.py`` — this file does not +re-check the math, only the estimator plumbing. +""" + +from __future__ import annotations + +import pytest + +from pyfia.carbon.live_tree import LiveTreeEstimator, live_tree + + +class MockDB: + """Mock database for testing estimator methods in isolation. + + Matches the pattern used in ``test_carbon_pools_estimator.py``. The + attributes below are the ones ``BaseEstimator.__init__`` touches, plus + ``_reader`` as a stub so :meth:`LiveTreeEstimator._load_ref_species` + can be monkey-patched in tests that need it. + """ + + def __init__(self): + self.db_path = "/fake/path" + self.tables = {} + self.evalid = None + self.evalids = None + self._state_filter = None + self._reader = None + + +# --------------------------------------------------------------------------- +# Isolated estimator config tests +# --------------------------------------------------------------------------- + + +class TestGetRequiredTables: + def test_returns_five_core_tables(self): + """REF_SPECIES is loaded separately in calculate_values, not via the loader.""" + estimator = LiveTreeEstimator(MockDB(), {"pool": "ag", "land_type": "forest"}) + tables = estimator.get_required_tables() + assert "TREE" in tables + assert "COND" in tables + assert "PLOT" in tables + assert "POP_PLOT_STRATUM_ASSGN" in tables + assert "POP_STRATUM" in tables + assert len(tables) == 5 + + def test_tables_consistent_across_pools(self): + for pool in ("ag", "bg", "total"): + estimator = LiveTreeEstimator(MockDB(), {"pool": pool}) + tables = estimator.get_required_tables() + assert set(tables) == { + "TREE", + "COND", + "PLOT", + "POP_PLOT_STRATUM_ASSGN", + "POP_STRATUM", + } + + +class TestGetTreeColumns: + def test_nsvb_and_bg_bridge_columns(self): + """All tree columns the NSVB pipeline + BG bridge need are requested.""" + estimator = LiveTreeEstimator(MockDB(), {"pool": "total"}) + cols = estimator.get_tree_columns() + assert "SPCD" in cols + assert "DIA" in cols + assert "HT" in cols + assert "CULL" in cols + assert "CARBON_BG" in cols + assert "TPA_UNADJ" in cols + # Standard framework columns added by _get_tree_columns() + assert "CN" in cols + assert "PLT_CN" in cols + assert "CONDID" in cols + assert "STATUSCD" in cols # live filter + # ACTUALHT is not loaded in Phase 1 — intact-top assumption + assert "ACTUALHT" not in cols + + def test_tree_columns_include_grouping(self): + """grp_by columns are added to the tree column list.""" + estimator = LiveTreeEstimator(MockDB(), {"pool": "ag", "grp_by": "SPGRPCD"}) + cols = estimator.get_tree_columns() + assert "SPGRPCD" in cols + + +class TestLiveTreeFunctionValidation: + """Input validation for the public ``live_tree()`` function.""" + + def test_invalid_pool_raises(self): + with pytest.raises(ValueError, match="Invalid pool"): + live_tree(MockDB(), pool="xyz") + + def test_pool_case_insensitive(self, monkeypatch): + """pool='AG', 'Ag', 'ag' are all accepted. + + Uses monkeypatch to prevent the estimator from touching a real DB — + we only want to verify that validation accepts the input. + """ + # Short-circuit ensure_fia_instance + ensure_evalid_set so we don't + # actually run an estimation. We just need the validation path to + # pass without raising for the case variants. + # importlib is used because ``pyfia.carbon.__init__`` re-exports the + # ``live_tree`` function, shadowing the submodule name under normal + # attribute access. + import importlib + + live_tree_module = importlib.import_module("pyfia.carbon.live_tree") + + def fake_ensure_fia_instance(db): + return (MockDB(), False) + + def fake_ensure_evalid_set(*args, **kwargs): + pass + + class ShortCircuitError(Exception): + pass + + def fake_estimate(self): + raise ShortCircuitError() + + monkeypatch.setattr( + live_tree_module, "ensure_fia_instance", fake_ensure_fia_instance + ) + monkeypatch.setattr( + live_tree_module, "ensure_evalid_set", fake_ensure_evalid_set + ) + monkeypatch.setattr(LiveTreeEstimator, "estimate", fake_estimate) + + for variant in ("ag", "AG", "Ag", "bg", "BG", "total", "TOTAL"): + with pytest.raises(ShortCircuitError): + live_tree(MockDB(), pool=variant) + + def test_total_is_valid_pool(self, monkeypatch): + """'total' is a valid pool value (AG + BG bridge).""" + # Validation path should accept 'total' without raising. + import importlib + + live_tree_module = importlib.import_module("pyfia.carbon.live_tree") + + def fake_ensure_fia_instance(db): + return (MockDB(), False) + + def fake_ensure_evalid_set(*args, **kwargs): + pass + + class ShortCircuitError(Exception): + pass + + def fake_estimate(self): + raise ShortCircuitError() + + monkeypatch.setattr( + live_tree_module, "ensure_fia_instance", fake_ensure_fia_instance + ) + monkeypatch.setattr( + live_tree_module, "ensure_evalid_set", fake_ensure_evalid_set + ) + monkeypatch.setattr(LiveTreeEstimator, "estimate", fake_estimate) + + with pytest.raises(ShortCircuitError): + live_tree(MockDB(), pool="total") + + +# --------------------------------------------------------------------------- +# Full end-to-end smoke test (skipped if no FIA database present) +# --------------------------------------------------------------------------- + + +class TestLiveTreeEndToEnd: + """End-to-end smoke test using the Georgia test database fixture. + + Skipped automatically when no database is available — the + ``georgia_db_path`` fixture in ``tests/conftest.py`` calls + ``pytest.skip(...)`` if it can't find a DuckDB file or a MotherDuck token. + """ + + def test_live_tree_ag_pool_runs_to_completion(self, georgia_db): + """``live_tree(db, pool='ag', most_recent=True)`` returns a non-empty, + positively-valued DataFrame with the expected column layout.""" + result = live_tree(georgia_db, pool="ag", most_recent=True) + + assert result.height > 0, "live_tree returned an empty DataFrame" + + # Canonical column layout + assert "YEAR" in result.columns + assert "POOL" in result.columns + assert "CARBON_ACRE" in result.columns + assert "CARBON_TOTAL" in result.columns + assert "N_PLOTS" in result.columns + assert "N_TREES" in result.columns + + # POOL column tags the estimate + assert result["POOL"][0] == "AG" + + # Per-acre carbon should be positive and within a plausible range + # (forestland typically runs 10-50 short tons/acre of live tree AG carbon) + carbon_acre = result["CARBON_ACRE"][0] + assert carbon_acre > 0, f"Non-positive CARBON_ACRE: {carbon_acre}" + assert carbon_acre < 500, f"Implausibly large CARBON_ACRE: {carbon_acre}" + + def test_live_tree_bg_pool_uses_fiadb_bridge(self, georgia_db): + """``pool='bg'`` returns belowground carbon via the FIADB bridge.""" + result = live_tree(georgia_db, pool="bg", most_recent=True) + + assert result.height > 0 + assert result["POOL"][0] == "BG" + carbon_acre = result["CARBON_ACRE"][0] + assert carbon_acre > 0 + # BG is typically 15-25% of AG, so should be smaller in absolute terms. + assert carbon_acre < 100 + + def test_live_tree_total_is_sum_of_components(self, georgia_db): + """``pool='total'`` ≈ ``pool='ag'`` + ``pool='bg'`` at the per-acre level. + + Not strictly additive to floating-point precision because of + ratio-of-means aggregation, but should agree to within a fraction + of a percent. + """ + ag = live_tree(georgia_db, pool="ag", most_recent=True) + bg = live_tree(georgia_db, pool="bg", most_recent=True) + total = live_tree(georgia_db, pool="total", most_recent=True) + + ag_acre = ag["CARBON_ACRE"][0] + bg_acre = bg["CARBON_ACRE"][0] + total_acre = total["CARBON_ACRE"][0] + + # Relative difference should be <1% + assert abs(total_acre - (ag_acre + bg_acre)) / total_acre < 0.01, ( + f"total={total_acre}, ag+bg={ag_acre + bg_acre}" + ) + + def test_live_tree_by_species_groups(self, georgia_db): + """``by_species=True`` produces multiple rows, one per species.""" + result = live_tree(georgia_db, pool="ag", by_species=True, most_recent=True) + assert result.height > 1 + assert "SPCD" in result.columns diff --git a/tests/unit/test_nsvb_coefficients.py b/tests/unit/test_nsvb_coefficients.py new file mode 100644 index 00000000..499239aa --- /dev/null +++ b/tests/unit/test_nsvb_coefficients.py @@ -0,0 +1,519 @@ +""" +Unit tests for NSVB coefficient table loaders and lookup precedence. + +Verifies: + +1. CSV bundle loads via ``importlib.resources`` (wheel-safe) +2. All 10 SPCD/Jenkins tables are present with reasonable row counts +3. The ``@lru_cache`` returns the same instance on repeat calls +4. ``lookup_coefficients`` walks the precedence levels in the documented order +5. Species-level fallback works for SPCDs that lack DIVISION-specific rows +6. Jenkins fallback works for the level-4 case +7. KeyError is raised for SPCDs with no coverage at any level +""" + +from __future__ import annotations + +import polars as pl +import pytest + +from pyfia.carbon.nsvb.coefficients import ( + CoefficientTables, + VectorizedLookupTables, + build_division_lookup, + build_jenkins_lookup, + build_species_level_lookup, + ecosubcd_to_division, + get_vectorized_lookup_tables, + load_nsvb_coefficients, + lookup_coefficients, +) + + +class TestLoadNSVBCoefficients: + """CSV bundle loading via importlib.resources.""" + + def test_returns_coefficient_tables_dataclass(self): + coefs = load_nsvb_coefficients() + assert isinstance(coefs, CoefficientTables) + + def test_lru_cache_returns_same_instance(self): + """Repeat calls hit the cache and return the same object.""" + first = load_nsvb_coefficients() + second = load_nsvb_coefficients() + assert first is second + + def test_all_ten_tables_present(self): + coefs = load_nsvb_coefficients() + assert isinstance(coefs.volib_spcd, pl.DataFrame) + assert isinstance(coefs.volib_jenkins, pl.DataFrame) + assert isinstance(coefs.volbk_spcd, pl.DataFrame) + assert isinstance(coefs.volbk_jenkins, pl.DataFrame) + assert isinstance(coefs.bark_biomass_spcd, pl.DataFrame) + assert isinstance(coefs.bark_biomass_jenkins, pl.DataFrame) + assert isinstance(coefs.branch_biomass_spcd, pl.DataFrame) + assert isinstance(coefs.branch_biomass_jenkins, pl.DataFrame) + assert isinstance(coefs.total_biomass_spcd, pl.DataFrame) + assert isinstance(coefs.total_biomass_jenkins, pl.DataFrame) + + def test_table_row_counts(self): + """Verify the vendored tables have the expected number of rows. + + These counts come directly from the WO-104 supplementary archive + and serve as a regression check that we haven't accidentally + replaced or corrupted a CSV during vendoring. + """ + coefs = load_nsvb_coefficients() + assert coefs.volib_spcd.height == 406 + assert coefs.volib_jenkins.height == 9 + assert coefs.volbk_spcd.height == 339 + assert coefs.volbk_jenkins.height == 9 + assert coefs.bark_biomass_spcd.height == 206 + assert coefs.bark_biomass_jenkins.height == 9 + assert coefs.branch_biomass_spcd.height == 175 + assert coefs.branch_biomass_jenkins.height == 9 + assert coefs.total_biomass_spcd.height == 173 + assert coefs.total_biomass_jenkins.height == 9 + + def test_spcd_table_schema(self): + """All ``*_spcd`` tables share the same column structure.""" + coefs = load_nsvb_coefficients() + expected = { + "SPCD", + "DIVISION", + "STDORGCD", + "model", + "a", + "a1", + "b", + "b1", + "c", + "c1", + } + for tbl in ( + coefs.volib_spcd, + coefs.volbk_spcd, + coefs.total_biomass_spcd, + ): + cols = set(tbl.columns) + assert expected.issubset(cols), f"Missing columns: {expected - cols}" + + def test_spcd_table_explicit_dtypes(self): + """All ``*_spcd`` tables are loaded with explicit schemas. + + Regression test for the fix that replaced ``infer_schema_length=10_000`` + with ``schema_overrides``. DIVISION must be ``Utf8`` (ecoprovince codes + like "M240") and STDORGCD must be ``Int64`` — the upcoming vectorized + lookup joins rely on these dtypes being stable regardless of which + rows happen to be at the top of the CSV on a future re-vendor. + + Note: the bark/branch biomass tables have a narrower schema + (``a, b, b1, c`` only, no ``a1`` / ``c1``) than volume/total biomass. + """ + coefs = load_nsvb_coefficients() + for tbl in ( + coefs.volib_spcd, + coefs.volbk_spcd, + coefs.bark_biomass_spcd, + coefs.branch_biomass_spcd, + coefs.total_biomass_spcd, + ): + assert tbl.schema["SPCD"] == pl.Int64 + assert tbl.schema["DIVISION"] == pl.Utf8 + assert tbl.schema["STDORGCD"] == pl.Int64 + assert tbl.schema["model"] == pl.Int64 + # Common numeric columns present in every table + for col in ("a", "b", "b1", "c"): + assert tbl.schema[col] == pl.Float64, ( + f"{col} in {tbl.columns}: expected Float64, got {tbl.schema[col]}" + ) + # a1 / c1 only exist in volib_spcd, volbk_spcd, total_biomass_spcd + for col in ("a1", "c1"): + if col in tbl.columns: + assert tbl.schema[col] == pl.Float64 + + def test_jenkins_table_explicit_dtypes(self): + """All ``*_jenkins`` tables are loaded with explicit schemas.""" + coefs = load_nsvb_coefficients() + for tbl in ( + coefs.volib_jenkins, + coefs.volbk_jenkins, + coefs.bark_biomass_jenkins, + coefs.branch_biomass_jenkins, + coefs.total_biomass_jenkins, + ): + assert tbl.schema["JENKINS_SPGRPCD"] == pl.Int64 + assert tbl.schema["model"] == pl.Int64 + assert tbl.schema["a"] == pl.Float64 + assert tbl.schema["b"] == pl.Float64 + assert tbl.schema["c"] == pl.Float64 + + +class TestLookupCoefficients: + """NSVB coefficient lookup precedence (4 levels).""" + + def test_spcd_only_fallback_douglas_fir(self): + """SPCD=202 species-level row exists in S1a (volib). + + With ``division=None``, lookup should hit Level 3 and return the + species-level row. + """ + coefs = load_nsvb_coefficients() + result = lookup_coefficients( + coefs.volib_spcd, + coefs.volib_jenkins, + spcd=202, + jenkins_spgrpcd=10, # arbitrary, won't be used + ) + assert result["source"] == "spcd" + assert result["model"] == 2 # Douglas-fir uses Model 2 for stem wood vol + # Coefficients should be non-zero — exact values depend on the species-level row + assert result["a"] > 0 + assert result["b"] > 0 + + def test_spcd_only_fallback_red_maple(self): + """SPCD=316 species-level row exists in S1a (volib). + + Red maple uses Model 1 for stem wood volume at the species level. + """ + coefs = load_nsvb_coefficients() + result = lookup_coefficients( + coefs.volib_spcd, + coefs.volib_jenkins, + spcd=316, + jenkins_spgrpcd=8, # arbitrary + ) + assert result["source"] == "spcd" + assert result["model"] == 1 + + def test_division_lookup_when_division_provided(self): + """When DIVISION is provided AND a row exists, prefer Level 2.""" + coefs = load_nsvb_coefficients() + result = lookup_coefficients( + coefs.volib_spcd, + coefs.volib_jenkins, + spcd=202, + division="240", # Marine division — Douglas-fir has a row here + jenkins_spgrpcd=10, + ) + assert result["source"] == "spcd_division" + assert result["model"] == 2 + + def test_division_lookup_falls_back_to_species_when_missing(self): + """When DIVISION is provided BUT no row exists, fall through to Level 3. + + Per the red maple worked example: SPCD=316 has no DIVISION=M210 row + in S1a, so lookup falls back to the species-level row. + """ + coefs = load_nsvb_coefficients() + result = lookup_coefficients( + coefs.volib_spcd, + coefs.volib_jenkins, + spcd=316, + division="M210", # No row exists for 316/M210 — should fall back + jenkins_spgrpcd=8, + ) + assert result["source"] == "spcd" + assert result["model"] == 1 # red maple species-level uses Model 1 + + def test_total_biomass_red_maple_uses_model_4(self): + """Critical regression: SPCD=316 in S8a uses Model 4, not Model 1. + + This pins the Model 4 wiring — if someone accidentally changes the + equation dispatch, this test will catch it. + """ + coefs = load_nsvb_coefficients() + result = lookup_coefficients( + coefs.total_biomass_spcd, + coefs.total_biomass_jenkins, + spcd=316, + jenkins_spgrpcd=8, + ) + assert result["source"] == "spcd" + assert result["model"] == 4 + # Model 4 uses b1; the species-level row should have a non-zero b1 + assert result["b1"] != 0.0 + + def test_keyerror_for_unsupported_spcd(self): + """SPCD with no coverage at any level should raise KeyError.""" + coefs = load_nsvb_coefficients() + with pytest.raises(KeyError, match="No NSVB coefficients"): + lookup_coefficients( + coefs.volib_spcd, + coefs.volib_jenkins, + spcd=99999, # impossible SPCD + jenkins_spgrpcd=None, + ) + + def test_jenkins_fallback_when_jenkins_table_keyed(self): + """Level 4 Jenkins fallback path. + + Note: the Jenkins tables in the vendored CSVs use ``JENKINS_SPGRPCD`` + as the key column. We verify the fallback returns a Jenkins-tagged + result when a non-existent SPCD is paired with a valid Jenkins group. + """ + coefs = load_nsvb_coefficients() + # Pick a Jenkins group that exists in the table + jenkins_groups = coefs.volib_jenkins["JENKINS_SPGRPCD"].to_list() + valid_jenkins = jenkins_groups[0] + result = lookup_coefficients( + coefs.volib_spcd, + coefs.volib_jenkins, + spcd=99999, # not in S1a + jenkins_spgrpcd=valid_jenkins, + ) + assert result["source"] == "jenkins" + + +class TestBuildSpeciesLevelLookup: + """Vectorized path helper: species-level row filter for *_spcd tables.""" + + def test_selected_columns(self): + """Returns only the columns the vectorized biomass expression needs.""" + coefs = load_nsvb_coefficients() + result = build_species_level_lookup(coefs.volib_spcd) + assert result.columns == ["SPCD", "model", "a", "b", "b1", "c"] + + def test_keeps_only_species_level_rows(self): + """Rows where DIVISION or STDORGCD is non-null are filtered out. + + Phase 1 has no ECOSUBCD → DIVISION mapping, so DIVISION-specific + rows (Levels 1-2 of the NSVB precedence) are dead code and must be + dropped before the vectorized join runs. + """ + coefs = load_nsvb_coefficients() + result = build_species_level_lookup(coefs.volib_spcd) + # Original table has 406 rows; species-level is a strict subset + assert result.height < coefs.volib_spcd.height + assert result.height > 0 + + def test_douglas_fir_row_present(self): + """SPCD=202 Douglas-fir has a species-level row with Model 2 for volib.""" + coefs = load_nsvb_coefficients() + result = build_species_level_lookup(coefs.volib_spcd) + dougfir = result.filter(pl.col("SPCD") == 202) + assert dougfir.height == 1 + assert dougfir["model"][0] == 2 + + def test_spcd_unique_in_output(self): + """Each SPCD appears at most once after filtering — single row per species.""" + coefs = load_nsvb_coefficients() + for tbl in ( + coefs.volib_spcd, + coefs.volbk_spcd, + coefs.bark_biomass_spcd, + coefs.branch_biomass_spcd, + coefs.total_biomass_spcd, + ): + result = build_species_level_lookup(tbl) + assert result["SPCD"].is_unique().all() + + def test_works_on_narrow_biomass_tables(self): + """bark_biomass_spcd / branch_biomass_spcd have no a1/c1 but still + expose (a, b, b1, c) — the builder must handle both schemas.""" + coefs = load_nsvb_coefficients() + for tbl in (coefs.bark_biomass_spcd, coefs.branch_biomass_spcd): + result = build_species_level_lookup(tbl) + assert result.columns == ["SPCD", "model", "a", "b", "b1", "c"] + assert result.height > 0 + + +class TestBuildJenkinsLookup: + """Vectorized path helper: Jenkins fallback table for *_jenkins tables.""" + + def test_selected_columns(self): + """Returns uniform columns including synthesized b1=0.""" + coefs = load_nsvb_coefficients() + result = build_jenkins_lookup(coefs.volib_jenkins) + assert result.columns == [ + "JENKINS_SPGRPCD", + "model", + "a", + "b", + "b1", + "c", + ] + + def test_b1_is_always_zero(self): + """Jenkins tables have no b1 column; the synthesized value is 0.0. + + Model 5 (the only form Jenkins rows dispatch to) does not use b1, + so any value is correct; 0.0 keeps the coalesce in the orchestrator + from emitting a null when the downstream expression reads b1. + """ + coefs = load_nsvb_coefficients() + result = build_jenkins_lookup(coefs.volib_jenkins) + assert (result["b1"] == 0.0).all() + assert result.schema["b1"] == pl.Float64 + + def test_all_jenkins_rows_preserved(self): + """No filtering — Jenkins tables are used as-is (all 9 groups).""" + coefs = load_nsvb_coefficients() + result = build_jenkins_lookup(coefs.volib_jenkins) + assert result.height == coefs.volib_jenkins.height + assert result.height == 9 + + +class TestGetVectorizedLookupTables: + """Cached bundle of all 5 component lookup pairs.""" + + def test_returns_vectorized_bundle(self): + bundle = get_vectorized_lookup_tables() + assert isinstance(bundle, VectorizedLookupTables) + + def test_lru_cache_returns_same_instance(self): + first = get_vectorized_lookup_tables() + second = get_vectorized_lookup_tables() + assert first is second + + def test_all_ten_tables_present_and_ready_to_join(self): + """Each table has the vectorized-path column layout.""" + bundle = get_vectorized_lookup_tables() + + spcd_expected = ["SPCD", "model", "a", "b", "b1", "c"] + jen_expected = ["JENKINS_SPGRPCD", "model", "a", "b", "b1", "c"] + + for name in ( + "volib_spcd", + "volbk_spcd", + "bark_bio_spcd", + "branch_bio_spcd", + "total_agb_spcd", + ): + tbl = getattr(bundle, name) + assert tbl.columns == spcd_expected, f"{name} has wrong columns" + assert tbl.height > 0 + + for name in ( + "volib_jen", + "volbk_jen", + "bark_bio_jen", + "branch_bio_jen", + "total_agb_jen", + ): + tbl = getattr(bundle, name) + assert tbl.columns == jen_expected, f"{name} has wrong columns" + assert tbl.height == 9 # 9 Jenkins species groups + + def test_douglas_fir_volib_model_2(self): + """Regression sentinel: SPCD=202 volib row is Model 2 (stem wood k=9).""" + bundle = get_vectorized_lookup_tables() + row = bundle.volib_spcd.filter(pl.col("SPCD") == 202).to_dicts()[0] + assert row["model"] == 2 + + def test_red_maple_total_agb_model_4(self): + """Regression sentinel: SPCD=316 total_agb row is Model 4.""" + bundle = get_vectorized_lookup_tables() + row = bundle.total_agb_spcd.filter(pl.col("SPCD") == 316).to_dicts()[0] + assert row["model"] == 4 + + def test_division_lookups_present(self): + """Each component now has a DIVISION-keyed lookup (Level 2).""" + bundle = get_vectorized_lookup_tables() + div_expected = ["SPCD", "DIVISION", "model", "a", "b", "b1", "c"] + for name in ( + "volib_div", + "volbk_div", + "bark_bio_div", + "branch_bio_div", + "total_agb_div", + ): + tbl = getattr(bundle, name) + assert tbl.columns == div_expected, f"{name} has wrong columns" + # Every division row has a non-null DIVISION code + assert tbl["DIVISION"].null_count() == 0 + # Every division row has a non-null SPCD + assert tbl["SPCD"].null_count() == 0 + + def test_division_230_has_georgia_species(self): + """DIVISION 230 (Subtropical, covering Georgia) has species-level rows + in the S8a (total_agb) table, including loblolly pine (SPCD=131).""" + bundle = get_vectorized_lookup_tables() + div_230 = bundle.total_agb_div.filter(pl.col("DIVISION") == "230") + spcds = set(div_230["SPCD"].to_list()) + # Loblolly pine (131), slash pine (111), and longleaf pine (121) are + # the dominant southern pines in Georgia — if the Phase 1.5 fix is + # to land, these need a DIVISION=230 row. + assert 131 in spcds or 121 in spcds or 111 in spcds, ( + f"no southern pines in DIVISION 230 S8a rows: {sorted(spcds)}" + ) + + +class TestBuildDivisionLookup: + """Vectorized path helper: DIVISION-keyed row filter for *_spcd tables.""" + + def test_selected_columns(self): + """Returns a (SPCD, DIVISION, model, a, b, b1, c) lookup.""" + coefs = load_nsvb_coefficients() + result = build_division_lookup(coefs.volib_spcd) + assert result.columns == ["SPCD", "DIVISION", "model", "a", "b", "b1", "c"] + + def test_only_division_rows(self): + """Rows where DIVISION is null are filtered out.""" + coefs = load_nsvb_coefficients() + result = build_division_lookup(coefs.volib_spcd) + assert result["DIVISION"].null_count() == 0 + # Volib_spcd has 256 DIVISION rows in the raw table; the defensive + # Model 2/4 null-b1 filter may drop a few, but we should still have + # most of them. + assert result.height > 200 + + def test_stdorgcd_null_only(self): + """Phase 1.5 skips Level 1 (STDORGCD-specific rows).""" + coefs = load_nsvb_coefficients() + raw_div = coefs.volib_spcd.filter(pl.col("DIVISION").is_not_null()) + lookup_div = build_division_lookup(coefs.volib_spcd) + # lookup should have <= raw (Level 1 rows dropped by STDORGCD.is_null()) + assert lookup_div.height <= raw_div.height + + def test_composite_key_uniqueness(self): + """Each (SPCD, DIVISION) appears at most once after filtering.""" + coefs = load_nsvb_coefficients() + result = build_division_lookup(coefs.volib_spcd) + pairs = result.select(["SPCD", "DIVISION"]) + assert pairs.n_unique() == pairs.height + + +class TestEcosubcdToDivision: + """Bailey ECOSUBCD → DIVISION crosswalk.""" + + def test_southeastern_mixed_forest(self): + """SE Mixed Forest provinces 231, 232 → Division 230 (Subtropical).""" + assert ecosubcd_to_division("231Ae") == "230" + assert ecosubcd_to_division("231Aa") == "230" + assert ecosubcd_to_division("232Bh") == "230" + assert ecosubcd_to_division("232Ba") == "230" + + def test_mountain_prefix(self): + """Mountain prefix M is preserved: M231Aa → M230.""" + assert ecosubcd_to_division("M231Aa") == "M230" + assert ecosubcd_to_division("M261Ea") == "M260" + assert ecosubcd_to_division("M220Ad") == "M220" + + def test_non_subtropical_divisions(self): + """Sanity-check other domains.""" + assert ecosubcd_to_division("211Aa") == "210" # Warm Continental + assert ecosubcd_to_division("221Aa") == "220" # Hot Continental + assert ecosubcd_to_division("242Bb") == "240" # Marine + + def test_null_and_empty_inputs(self): + """Null, empty, or whitespace input → None.""" + assert ecosubcd_to_division(None) is None + assert ecosubcd_to_division("") is None + assert ecosubcd_to_division(" ") is None + + def test_malformed_input(self): + """Non-digit or too-short prefix → None.""" + assert ecosubcd_to_division("XYZ") is None + assert ecosubcd_to_division("M") is None + assert ecosubcd_to_division("12") is None + assert ecosubcd_to_division("A23") is None + + def test_case_insensitive(self): + """Lowercase input is normalized to uppercase (M prefix preserved).""" + assert ecosubcd_to_division("m231aa") == "M230" + assert ecosubcd_to_division("231ae") == "230" + + def test_whitespace_stripped(self): + """Leading/trailing whitespace is stripped.""" + assert ecosubcd_to_division(" 231Ae ") == "230" diff --git a/tests/unit/test_nsvb_equations.py b/tests/unit/test_nsvb_equations.py new file mode 100644 index 00000000..7b2853df --- /dev/null +++ b/tests/unit/test_nsvb_equations.py @@ -0,0 +1,801 @@ +""" +Unit tests for the NSVB equation library. + +These tests verify the pure-math equation forms (Models 1, 2, 4, 5) and the +``predict_tree_biomass`` orchestrator against the worked numerical examples in +the GTR-WO-104 source PDF (transcribed in +``references_md/tier1_fcaf/gtr_wo104_westfall2023.md`` lines 394-960). + +Two worked examples are reproduced: + +1. **Douglas-fir** (SPCD=202, D=20", H=110', no cull, DIVISION=240) — exercises + Model 2 (stem wood vol, k=9 softwood) and Model 1 (everything else). + Expected total AGB: 3154.5539926725 lb. Carbon: 1626.4748946459 lb. + +2. **Red maple** (SPCD=316, D=11.1", H=38', CULL=3, DIVISION=M210→species-level + fallback) — exercises Model 1 (volib), Model 2 (volbk, k=11 hardwood), + Model 4 (total AGB with exp(-b1*D) factor), and the cull-reduction path. + Expected total AGB after cull-adjusted harmonization: 528.135964525863 lb. + +Coefficients are hand-coded from the worked-example prose to high precision +(12+ significant figures). The CSV-loaded coefficients in +``src/pyfia/carbon/nsvb/data/`` are stored at ~9 significant figures and would +introduce rounding error at the 7th-8th decimal — the regression tests need +the prose precision to verify the equation math itself, separate from CSV +fidelity. +""" + +from __future__ import annotations + +import math + +import pytest + +from pyfia.carbon.nsvb.coefficients import ( + load_nsvb_coefficients, + lookup_coefficients, +) +from pyfia.carbon.nsvb.equations import ( + Coefficients, + _model_k, + harmonize_components, + model_1, + model_2, + model_4, + model_5_jenkins, + predict_tree_biomass, +) + +# --------------------------------------------------------------------------- +# Hand-coded high-precision coefficients from the WO-104 worked examples +# --------------------------------------------------------------------------- + + +# Douglas-fir SPCD=202, DIVISION=240, no cull (worked example lines 394-680) +DOUGFIR_INPUTS = { + "spcd": 202, + "dia": 20.0, + "ht": 110.0, + "wdsg": 0.45, + "hw_sw": "softwood", + "cull": 0.0, +} + +DOUGFIR_COEFS = Coefficients( + volib={ + "model": 2, + "a": 0.001929099661, + "a1": 0.0, + "b": 2.162413104203, + "b1": 1.690400253097, + "c": 0.985444005253, + "c1": 0.0, + "source": "test", + }, + volbk={ + "model": 1, + "a": 0.000031886237, + "a1": 0.0, + "b": 1.21260513951, + "b1": 0.0, + "c": 1.978577263767, + "c1": 0.0, + "source": "test", + }, + bark_bio={ + "model": 1, + "a": 0.009106538193, + "a1": 0.0, + "b": 1.437894424586, + "b1": 0.0, + "c": 1.336514272981, + "c1": 0.0, + "source": "test", + }, + branch_bio={ + "model": 1, + "a": 9.521330809106, + "a1": 0.0, + "b": 1.762316117442, + "b1": 0.0, + "c": -0.40574259177, + "c1": 0.0, + "source": "test", + }, + total_agb={ + "model": 1, + "a": 0.135206506787, + "a1": 0.0, + "b": 1.713527048035, + "b1": 0.0, + "c": 1.047613377046, + "c1": 0.0, + "source": "test", + }, +) + +DOUGFIR_EXPECTED = { + "v_wood_ib": 88.452275544288, + "v_bark": 13.191436232306, + "w_wood_gross": 2483.739897283610, + "w_bark_gross": 361.782496100100, + "w_branch_gross": 277.487756904646, + "agb_predicted": 3154.5539926725, + "agb_component": 3123.010150288360, + "wood_harmonized": 2508.826815376370, + "bark_harmonized": 365.436666110811, + "branch_harmonized": 280.290511185328, + "carbon_total": 1626.474894645920, + "carbon_fraction": 0.515595833333, +} + +# Red maple SPCD=316, species-level fallback, CULL=3 (worked example lines 682-960) +REDMAPLE_INPUTS = { + "spcd": 316, + "dia": 11.1, + "ht": 38.0, + "wdsg": 0.49, + "hw_sw": "hardwood", + "cull": 3.0, +} + +REDMAPLE_COEFS = Coefficients( + volib={ + "model": 1, + "a": 0.001983918881, + "a1": 0.0, + "b": 1.810559393287, + "b1": 0.0, + "c": 1.129417635145, + "c1": 0.0, + "source": "test", + }, + volbk={ + "model": 2, + "a": 0.003743084443, + "a1": 0.0, + "b": 2.226890355309, + "b1": 1.685993125661, + "c": 0.275066356213, + "c1": 0.0, + "source": "test", + }, + bark_bio={ + "model": 1, + "a": 0.061595466174, + "a1": 0.0, + "b": 1.818642599217, + "b1": 0.0, + "c": 0.654020672095, + "c1": 0.0, + "source": "test", + }, + branch_bio={ + "model": 1, + "a": 0.011144618401, + "a1": 0.0, + "b": 3.269520661293, + "b1": 0.0, + "c": 0.421304343724, + "c1": 0.0, + "source": "test", + }, + total_agb={ + "model": 4, + "a": 0.31573027567, + "a1": 0.0, + "b": 1.853839844372, + "b1": -0.024745684975, + "c": 0.740557378679, + "c1": 0.0, + "source": "test", + }, +) + +REDMAPLE_EXPECTED = { + "v_wood_ib": 9.427112777611, + "v_bark": 2.155106401987, + "w_wood_gross": 288.243400288234, + "w_wood_red": 284.265641364256, + "w_bark": 52.945466015848, + "w_branch": 135.001927997271, + "agb_predicted": 532.584798820042, + "agb_component_red": 472.213035377375, + "agb_reduce": 0.991646711840, + "agb_predicted_red": 528.135964525863, + "wood_harmonized": 317.930462388645, + "bark_harmonized": 59.215656211618, + "branch_harmonized": 150.989845925600, +} + +# Tolerance: 1e-9 relative error matches the worked-example precision +TOL = 1e-9 + + +def _close(actual: float, expected: float, tol: float = TOL) -> bool: + if expected == 0: + return abs(actual) < tol + return abs(actual - expected) / abs(expected) < tol + + +# --------------------------------------------------------------------------- +# Model 1 — power form +# --------------------------------------------------------------------------- + + +class TestModel1: + """Pure power form: ``y = a * D^b * H^c``.""" + + def test_douglas_fir_stem_bark_volume(self): + """S2a Model 1, line 424 of worked example. + + Vtot_bk_Gross = 0.000031886237 * 20^1.21260513951 * 110^1.978577263767 + = 13.191436232306 + """ + result = model_1(20.0, 110.0, 0.000031886237, 1.21260513951, 1.978577263767) + assert _close(result, 13.191436232306) + + def test_douglas_fir_stem_bark_biomass(self): + """S6a Model 1, line 560 of worked example. + + Wtot_bk = 0.009106538193 * 20^1.437894424586 * 110^1.336514272981 + = 361.782496100100 + """ + result = model_1(20.0, 110.0, 0.009106538193, 1.437894424586, 1.336514272981) + assert _close(result, 361.782496100100) + + def test_douglas_fir_branch_biomass(self): + """S7a Model 1, line 570 of worked example. + + Wbranch = 9.521330809106 * 20^1.762316117442 * 110^-0.40574259177 + = 277.487756904646 + + Note negative exponent on H — branch biomass declines with height + for trees of fixed diameter (taller trees have less branch mass). + """ + result = model_1(20.0, 110.0, 9.521330809106, 1.762316117442, -0.40574259177) + assert _close(result, 277.487756904646) + + def test_douglas_fir_total_agb(self): + """S8a Model 1, line 578 of worked example. + + AGB_Predicted = 0.135206506787 * 20^1.713527048035 * 110^1.047613377046 + = 3154.5539926725 + """ + result = model_1(20.0, 110.0, 0.135206506787, 1.713527048035, 1.047613377046) + assert _close(result, 3154.5539926725) + + +# --------------------------------------------------------------------------- +# Model 2 — power form with k constant +# --------------------------------------------------------------------------- + + +class TestModel2: + """Power form with species-class base constant k. + + Verifies the k=9 (softwood) and k=11 (hardwood) discovery from the + worked examples. + """ + + def test_softwood_k_constant(self): + """SPCD < 300 → k = 9.0.""" + assert _model_k(202) == 9.0 + assert _model_k(1) == 9.0 + assert _model_k(299) == 9.0 + + def test_hardwood_k_constant(self): + """SPCD >= 300 → k = 11.0.""" + assert _model_k(316) == 11.0 + assert _model_k(300) == 11.0 + assert _model_k(998) == 11.0 + + def test_douglas_fir_stem_wood_volume(self): + """S1a Model 2, line 418 of worked example. SPCD=202 (softwood, k=9). + + Vtot_ib_Gross = 0.001929099661 * 9^(2.162413104203 - 1.690400253097) + * 20^1.690400253097 * 110^0.985444005253 + = 88.452275544288 + """ + result = model_2( + d=20.0, + h=110.0, + a=0.001929099661, + b=2.162413104203, + b1=1.690400253097, + c=0.985444005253, + k=9.0, + ) + assert _close(result, 88.452275544288) + + def test_red_maple_stem_bark_volume(self): + """S2a Model 2, line 698 of worked example. SPCD=316 (hardwood, k=11). + + This is the test that pinned down k=11 for hardwoods. With k=9 the + result would be 1.94, not 2.16. + """ + result = model_2( + d=11.1, + h=38.0, + a=0.003743084443, + b=2.226890355309, + b1=1.685993125661, + c=0.275066356213, + k=11.0, + ) + assert _close(result, 2.155106401987) + + +# --------------------------------------------------------------------------- +# Model 4 — exp-modulated power form +# --------------------------------------------------------------------------- + + +class TestModel4: + """Form: ``y = a * D^b * H^c * exp(-b1 * D)``. + + Discovered from the red maple S8a worked example (line 860). The + sidecar's documentation of Model 4 was incomplete. + """ + + def test_red_maple_total_agb(self): + """S8a Model 4, line 862 of worked example. + + AGB_Predicted = 0.31573027567 * 11.1^1.853839844372 * 38^0.740557378679 + * exp(-(-0.024745684975 * 11.1)) + = 532.584798820042 + + The negative b1 means the exp factor is > 1. + """ + result = model_4( + d=11.1, + h=38.0, + a=0.31573027567, + b=1.853839844372, + b1=-0.024745684975, + c=0.740557378679, + ) + assert _close(result, 532.584798820042) + + def test_model_4_reduces_to_model_1_when_b1_zero(self): + """When b1=0, exp(0)=1, so Model 4 should equal Model 1.""" + m1 = model_1(15.0, 80.0, 0.1, 1.5, 1.2) + m4 = model_4(15.0, 80.0, 0.1, 1.5, 0.0, 1.2) + assert _close(m4, m1) + + +# --------------------------------------------------------------------------- +# Model 5 — Jenkins fallback +# --------------------------------------------------------------------------- + + +class TestModel5Jenkins: + """Form: ``y = a * D^b * H^c * WDSG``.""" + + def test_jenkins_form_multiplies_by_wdsg(self): + """The only difference from Model 1 is the WDSG multiplication.""" + m1 = model_1(15.0, 80.0, 0.1, 1.5, 1.2) + m5 = model_5_jenkins(15.0, 80.0, 0.1, 1.5, 1.2, wdsg=0.55) + assert _close(m5, m1 * 0.55) + + +# --------------------------------------------------------------------------- +# Harmonization +# --------------------------------------------------------------------------- + + +class TestHarmonization: + """Component proportional redistribution to sum exactly to predicted AGB.""" + + def test_douglas_fir_harmonization(self): + """No-cull harmonization, line 602-612 of worked example. + + AGB_predicted = 3154.5539926725 + Components (gross, no cull): 2483.739897283610, 361.782496100100, 277.487756904646 + Component sum: 3123.010150288360 + Expected harmonized: 2508.826815376370, 365.436666110811, 280.290511185328 + """ + wood_h, bark_h, branch_h = harmonize_components( + agb_predicted=3154.5539926725, + w_wood=2483.739897283610, + w_bark=361.782496100100, + w_branch=277.487756904646, + ) + assert _close(wood_h, 2508.826815376370) + assert _close(bark_h, 365.436666110811) + assert _close(branch_h, 280.290511185328) + + def test_harmonization_invariant(self): + """wood_h + bark_h + branch_h must equal agb_predicted exactly.""" + wood_h, bark_h, branch_h = harmonize_components( + agb_predicted=1000.0, + w_wood=400.0, + w_bark=200.0, + w_branch=300.0, + ) + assert math.isclose(wood_h + bark_h + branch_h, 1000.0, rel_tol=1e-15) + + def test_harmonization_handles_zero_components(self): + """Degenerate case: all components zero. Should return predicted in wood slot.""" + wood_h, bark_h, branch_h = harmonize_components( + agb_predicted=500.0, + w_wood=0.0, + w_bark=0.0, + w_branch=0.0, + ) + assert wood_h == 500.0 + assert bark_h == 0.0 + assert branch_h == 0.0 + + def test_harmonization_preserves_relative_ratios(self): + """If components are 4:2:3 in raw form, harmonized should also be 4:2:3.""" + wood_h, bark_h, branch_h = harmonize_components( + agb_predicted=900.0, + w_wood=400.0, + w_bark=200.0, + w_branch=300.0, + ) + assert math.isclose(wood_h / bark_h, 400.0 / 200.0, rel_tol=1e-12) + assert math.isclose(bark_h / branch_h, 200.0 / 300.0, rel_tol=1e-12) + + +# --------------------------------------------------------------------------- +# Full pipeline orchestrator +# --------------------------------------------------------------------------- + + +class TestPredictTreeBiomass: + """End-to-end NSVB pipeline regression vs the worked examples.""" + + def test_douglas_fir_full_pipeline(self): + """No-cull case (CULL=0). AGBReduce=1.0, simple harmonization.""" + result = predict_tree_biomass( + coefficients=DOUGFIR_COEFS, + **DOUGFIR_INPUTS, + ) + assert _close(result.v_wood_ib, DOUGFIR_EXPECTED["v_wood_ib"]) + assert _close(result.v_bark, DOUGFIR_EXPECTED["v_bark"]) + assert _close(result.w_wood, DOUGFIR_EXPECTED["wood_harmonized"]) + assert _close(result.w_bark, DOUGFIR_EXPECTED["bark_harmonized"]) + assert _close(result.w_branch, DOUGFIR_EXPECTED["branch_harmonized"]) + assert _close(result.agb, DOUGFIR_EXPECTED["agb_predicted"]) + + def test_red_maple_full_pipeline_with_cull(self): + """With-cull case (CULL=3). AGBReduce<1, cull-adjusted harmonization.""" + result = predict_tree_biomass( + coefficients=REDMAPLE_COEFS, + **REDMAPLE_INPUTS, + ) + assert _close(result.v_wood_ib, REDMAPLE_EXPECTED["v_wood_ib"]) + assert _close(result.v_bark, REDMAPLE_EXPECTED["v_bark"]) + assert _close(result.w_wood, REDMAPLE_EXPECTED["wood_harmonized"]) + assert _close(result.w_bark, REDMAPLE_EXPECTED["bark_harmonized"]) + assert _close(result.w_branch, REDMAPLE_EXPECTED["branch_harmonized"]) + assert _close(result.agb, REDMAPLE_EXPECTED["agb_predicted_red"]) + + def test_pipeline_harmonization_invariant(self): + """Component sum equals total AGB to floating-point precision.""" + result = predict_tree_biomass( + coefficients=DOUGFIR_COEFS, + **DOUGFIR_INPUTS, + ) + assert math.isclose( + result.w_wood + result.w_bark + result.w_branch, + result.agb, + rel_tol=1e-12, + ) + + def test_pipeline_carbon_conversion(self): + """Multiplying AGB by the species carbon fraction reproduces the + worked-example carbon value (line 680).""" + result = predict_tree_biomass( + coefficients=DOUGFIR_COEFS, + **DOUGFIR_INPUTS, + ) + carbon = result.agb * DOUGFIR_EXPECTED["carbon_fraction"] + assert _close(carbon, DOUGFIR_EXPECTED["carbon_total"]) + + def test_unsupported_model_raises(self): + """Model 3 and Model 6 are not implemented in Phase 1.""" + bad_coefs = Coefficients( + volib={ + "model": 3, # Model 3 not implemented + "a": 0.001, + "a1": 0.0, + "b": 1.5, + "b1": 0.0, + "c": 1.0, + "c1": 0.0, + "source": "test", + }, + volbk=DOUGFIR_COEFS.volbk, + bark_bio=DOUGFIR_COEFS.bark_bio, + branch_bio=DOUGFIR_COEFS.branch_bio, + total_agb=DOUGFIR_COEFS.total_agb, + ) + with pytest.raises(ValueError, match="model 3 not supported"): + predict_tree_biomass( + coefficients=bad_coefs, + **DOUGFIR_INPUTS, + ) + + def test_small_tree_uses_same_pipeline(self): + """Per WO-104 line 1624, saplings (DBH < 5) use the same pipeline. + + We don't have a worked example to compare against, but we can verify + that the pipeline runs without raising and returns a positive AGB + for a 2.5" sapling. + """ + result = predict_tree_biomass( + spcd=202, + dia=2.5, # sapling, well below 5" + ht=20.0, + coefficients=DOUGFIR_COEFS, + wdsg=0.45, + hw_sw="softwood", + cull=0.0, + ) + assert result.agb > 0 + # Sapling AGB should be much smaller than the 20" mature tree AGB + assert result.agb < 100 # 20" tree was 3154 lb; 2.5" sapling should be << 100 + + def test_hw_sw_casing_normalized(self): + """hw_sw is accepted case-insensitively. + + Addresses PR 1 review item: previously ``"Hardwood"`` raised KeyError + inside ``_CULL_DENS_PROP[hw_sw]``. Now normalized to lowercase at the + function boundary so callers don't have to think about casing. + """ + # "Softwood" (title case) should produce the same result as "softwood" + base = predict_tree_biomass(coefficients=DOUGFIR_COEFS, **DOUGFIR_INPUTS) + title_inputs = dict(DOUGFIR_INPUTS) + title_inputs["hw_sw"] = "Softwood" + titled = predict_tree_biomass(coefficients=DOUGFIR_COEFS, **title_inputs) + assert titled.agb == base.agb + + upper_inputs = dict(DOUGFIR_INPUTS) + upper_inputs["hw_sw"] = "SOFTWOOD" + upper = predict_tree_biomass(coefficients=DOUGFIR_COEFS, **upper_inputs) + assert upper.agb == base.agb + + def test_invalid_hw_sw_raises_clear_error(self): + """An invalid hw_sw string raises ValueError with a clear message.""" + bad_inputs = dict(DOUGFIR_INPUTS) + bad_inputs["hw_sw"] = "conifer" + with pytest.raises(ValueError, match="hw_sw must be"): + predict_tree_biomass(coefficients=DOUGFIR_COEFS, **bad_inputs) + + def test_dia_below_one_raises_clear_error(self): + """dia < 1.0 raises ValueError with a clear message. + + Addresses PR 1 review item: previously dia<1 could produce complex + numbers or cryptic TypeErrors inside the Model 1 power form. + """ + small_inputs = dict(DOUGFIR_INPUTS) + small_inputs["dia"] = 0.5 + with pytest.raises(ValueError, match="dia must be >= 1.0"): + predict_tree_biomass(coefficients=DOUGFIR_COEFS, **small_inputs) + + +# --------------------------------------------------------------------------- +# CSV → pipeline regression sentinels (the production data path) +# --------------------------------------------------------------------------- + +# Species-level (DIVISION=null) expected pipeline outputs for the worked-example +# trees, computed once from the vendored CSVs. These constants are sentinels +# for the 5 component CSVs — any change to volib_spcd, volbk_spcd, +# bark_biomass_spcd, branch_biomass_spcd, or total_biomass_spcd that affects +# SPCD=202 or SPCD=316 will perturb one of these values and break the test. +# +# Locked at f64-tight precision (math.isclose rel_tol=1e-9). The CSV stores +# coefficients at ~9 significant figures, but the pipeline output is purely +# deterministic given the CSV — same CSV in, same float out. If a future +# re-vendor changes the CSV intentionally, the failure shows the exact diff +# and the maintainer updates the constants here. +# +# Note: harmonization rescales (w_wood, w_bark, w_branch) proportionally to +# hit the directly-predicted total AGB, so individual component CSV +# corruption only shows up via these per-component checks — the AGB total +# alone cannot detect it because harmonization compensates. v_wood_ib and +# v_bark are the only signals for volib_spcd and volbk_spcd corruption since +# volbk feeds nothing in the Phase 1 AGB path (its value is stored for +# Phase 2+ adjusted-density use). +DOUGFIR_CSV_EXPECTED = { + "agb": 3151.7844814148, + "w_wood": 2442.4991973909, + "w_bark": 387.9115217065, + "w_branch": 321.3737623174, + "v_wood_ib": 83.6844822743, # locks volib_spcd.csv (SPCD=202 species-level row) + "v_bark": 18.1718836702, # locks volbk_spcd.csv (SPCD=202 species-level row) +} + +REDMAPLE_CSV_EXPECTED = { + "agb": 528.1359652182, + "w_wood": 317.9304736164, + "w_bark": 59.2156546044, + "w_branch": 150.9898369973, + "v_wood_ib": 9.4271133316, # locks volib_spcd.csv (SPCD=316 species-level row) + "v_bark": 2.1551061437, # locks volbk_spcd.csv (SPCD=316 species-level row) +} + + +def _bundle_via_csv(spcd: int, jenkins_spgrpcd: int) -> Coefficients: + """Build a ``Coefficients`` bundle by walking the real CSV lookup path. + + This is exactly what PR 2's ``LiveTreeEstimator`` will reach for at + runtime (modulo the planned vectorization). Reusing it here ensures the + regression sentinels exercise the same code path the production estimator + will hit, not a hand-coded shortcut. + """ + tables = load_nsvb_coefficients() + return Coefficients( + volib=lookup_coefficients( + tables.volib_spcd, + tables.volib_jenkins, + spcd=spcd, + jenkins_spgrpcd=jenkins_spgrpcd, + ), + volbk=lookup_coefficients( + tables.volbk_spcd, + tables.volbk_jenkins, + spcd=spcd, + jenkins_spgrpcd=jenkins_spgrpcd, + ), + bark_bio=lookup_coefficients( + tables.bark_biomass_spcd, + tables.bark_biomass_jenkins, + spcd=spcd, + jenkins_spgrpcd=jenkins_spgrpcd, + ), + branch_bio=lookup_coefficients( + tables.branch_biomass_spcd, + tables.branch_biomass_jenkins, + spcd=spcd, + jenkins_spgrpcd=jenkins_spgrpcd, + ), + total_agb=lookup_coefficients( + tables.total_biomass_spcd, + tables.total_biomass_jenkins, + spcd=spcd, + jenkins_spgrpcd=jenkins_spgrpcd, + ), + ) + + +class TestPipelineViaCSV: + """End-to-end NSVB pipeline regression vs the worked examples using the + actual CSV-loaded coefficients (the production data path). + + Unlike :class:`TestPredictTreeBiomass` above — which uses hand-coded + high-precision coefficients to verify the equation form — these tests + walk the full ``lookup_coefficients`` → ``predict_tree_biomass`` path so + a CSV corruption, schema drift, or species-level row recalibration in + *any* of the five vendored coefficient tables will fail the suite. + + Each test makes assertions at two layers: + + 1. **Worked-example agreement** (``agb`` vs WO-104, ~0.5% tolerance) — + documents the per-PR contract that Phase 1 reproduces the GTR-WO-104 + worked-example AGB to within 0.5% even with the deferred ECOSUBCD + lookup. Currently measured: Douglas-fir 0.09%, red maple 0.00%. + + 2. **CSV regression sentinels** (``v_wood_ib``, ``v_bark``, ``w_wood``, + ``w_bark``, ``w_branch``, ``agb`` vs the species-level CSV expected + constants, f64-tight ``rel_tol=1e-9``) — locks each of the five + vendored coefficient CSVs against unintended modification. Without + per-component locks, the harmonization step washes out corruption to + four of the five tables (it proportionally rescales components to hit + the directly-predicted total AGB), so a single AGB assertion would + only catch ``total_biomass_spcd.csv`` regressions. + + See ``pyfia/carbon/__init__.py`` "Items deferred from PR 1 review" for + why the species-level fallback is used in Phase 1. + """ + + def test_douglas_fir_pipeline_via_csv(self): + """Worked example expected AGB: 3154.55 lb (DIVISION=240). + + With Phase 1's species-level fallback the CSV pipeline lands at + 3151.78 lb — within 0.09% of the worked example. AGB asserted at + 0.5% tolerance against WO-104; full result vector locked at + f64-tight tolerance against the species-level CSV expected values. + + Also asserts every component of the bundle was resolved at the + ``"spcd"`` precedence level (not Jenkins fallback) — catches + mis-routing through Level 4 if the SPCD-keyed tables get truncated. + """ + bundle = _bundle_via_csv(spcd=202, jenkins_spgrpcd=10) + # Every component must hit the species-level row, not the Jenkins fallback + assert bundle.volib["source"] == "spcd" + assert bundle.volbk["source"] == "spcd" + assert bundle.bark_bio["source"] == "spcd" + assert bundle.branch_bio["source"] == "spcd" + assert bundle.total_agb["source"] == "spcd" + + result = predict_tree_biomass(coefficients=bundle, **DOUGFIR_INPUTS) + + # Layer 1: worked-example agreement (the PR contract) + expected_worked = DOUGFIR_EXPECTED["agb_predicted"] + rel_err = abs(result.agb - expected_worked) / expected_worked + assert rel_err < 0.005, ( + f"CSV pipeline AGB={result.agb:.4f} differs from worked-example " + f"{expected_worked:.4f} by {rel_err * 100:.3f}% (>0.5% tolerance)" + ) + + # Layer 2: per-CSV regression sentinels (locks all 5 component tables) + assert math.isclose(result.agb, DOUGFIR_CSV_EXPECTED["agb"], rel_tol=1e-9), ( + f"agb={result.agb} drifted from CSV-expected {DOUGFIR_CSV_EXPECTED['agb']}" + ) + assert math.isclose( + result.w_wood, DOUGFIR_CSV_EXPECTED["w_wood"], rel_tol=1e-9 + ), ( + f"w_wood={result.w_wood} drifted — bark_biomass_spcd or " + f"branch_biomass_spcd CSV may have changed" + ) + assert math.isclose( + result.w_bark, DOUGFIR_CSV_EXPECTED["w_bark"], rel_tol=1e-9 + ), f"w_bark={result.w_bark} drifted — bark_biomass_spcd CSV may have changed" + assert math.isclose( + result.w_branch, DOUGFIR_CSV_EXPECTED["w_branch"], rel_tol=1e-9 + ), ( + f"w_branch={result.w_branch} drifted — branch_biomass_spcd CSV " + f"may have changed" + ) + assert math.isclose( + result.v_wood_ib, DOUGFIR_CSV_EXPECTED["v_wood_ib"], rel_tol=1e-9 + ), f"v_wood_ib={result.v_wood_ib} drifted — volib_spcd CSV may have changed" + assert math.isclose( + result.v_bark, DOUGFIR_CSV_EXPECTED["v_bark"], rel_tol=1e-9 + ), f"v_bark={result.v_bark} drifted — volbk_spcd CSV may have changed" + + def test_red_maple_pipeline_via_csv(self): + """Worked example expected AGB (cull-reduced): 528.14 lb. + + The red maple worked example uses the species-level fallback for + both volib (Model 1) and total_agb (Model 4), so this test exercises + the cull-reduction path AND the Model 4 dispatch via the real CSV + lookup — not just the equation form. Full result vector is locked + against the species-level CSV expected constants at f64-tight + precision. + """ + bundle = _bundle_via_csv(spcd=316, jenkins_spgrpcd=8) + assert bundle.volib["source"] == "spcd" + assert bundle.volbk["source"] == "spcd" + assert bundle.bark_bio["source"] == "spcd" + assert bundle.branch_bio["source"] == "spcd" + assert bundle.total_agb["source"] == "spcd" + # Critical regression: total_agb for SPCD=316 must dispatch to Model 4 + assert bundle.total_agb["model"] == 4 + + result = predict_tree_biomass(coefficients=bundle, **REDMAPLE_INPUTS) + + # Layer 1: worked-example agreement (the PR contract) + expected_worked = REDMAPLE_EXPECTED["agb_predicted_red"] + rel_err = abs(result.agb - expected_worked) / expected_worked + assert rel_err < 0.005, ( + f"CSV pipeline AGB={result.agb:.4f} differs from worked-example " + f"{expected_worked:.4f} by {rel_err * 100:.3f}% (>0.5% tolerance)" + ) + + # Layer 2: per-CSV regression sentinels (locks all 5 component tables) + assert math.isclose(result.agb, REDMAPLE_CSV_EXPECTED["agb"], rel_tol=1e-9), ( + f"agb={result.agb} drifted from CSV-expected {REDMAPLE_CSV_EXPECTED['agb']}" + ) + assert math.isclose( + result.w_wood, REDMAPLE_CSV_EXPECTED["w_wood"], rel_tol=1e-9 + ), f"w_wood={result.w_wood} drifted from CSV-expected" + assert math.isclose( + result.w_bark, REDMAPLE_CSV_EXPECTED["w_bark"], rel_tol=1e-9 + ), f"w_bark={result.w_bark} drifted — bark_biomass_spcd CSV may have changed" + assert math.isclose( + result.w_branch, REDMAPLE_CSV_EXPECTED["w_branch"], rel_tol=1e-9 + ), ( + f"w_branch={result.w_branch} drifted — branch_biomass_spcd CSV " + f"may have changed" + ) + assert math.isclose( + result.v_wood_ib, REDMAPLE_CSV_EXPECTED["v_wood_ib"], rel_tol=1e-9 + ), f"v_wood_ib={result.v_wood_ib} drifted — volib_spcd CSV may have changed" + assert math.isclose( + result.v_bark, REDMAPLE_CSV_EXPECTED["v_bark"], rel_tol=1e-9 + ), f"v_bark={result.v_bark} drifted — volbk_spcd CSV may have changed" diff --git a/tests/unit/test_nsvb_vectorized.py b/tests/unit/test_nsvb_vectorized.py new file mode 100644 index 00000000..2f7ccded --- /dev/null +++ b/tests/unit/test_nsvb_vectorized.py @@ -0,0 +1,525 @@ +""" +Equivalence tests for the vectorized NSVB biomass pipeline. + +The scalar reference pipeline (``predict_tree_biomass`` + ``lookup_coefficients``) +is locked against the GTR-WO-104 worked examples in ``test_nsvb_equations.py``. +The vectorized pipeline in ``pyfia.carbon.nsvb.equations.compute_nsvb_biomass`` +must produce identical (f64-tight) outputs for every tree, because it is +intentionally a polars-expression rewrite of the same math. + +This module is the cross-check: for a diverse synthetic tree dataset covering +Models 1, 2, and 4, hardwoods and softwoods, SPCDs that hit the species-level +lookup and SPCDs that fall through to the Jenkins fallback, and trees with +and without cull, each tree's vectorized output must equal its scalar oracle +output at ``rel_tol=1e-9`` (six columns: ``v_wood_ib``, ``v_bark``, +``w_wood``, ``w_bark``, ``w_branch``, ``agb``). + +Any divergence breaks the test with exact per-tree diagnostics, which is the +fastest way to track down a bug introduced into the vectorized pipeline — +finer-grained than comparing aggregated per-acre estimates. + +See ``pyfia/carbon/__init__.py`` "Architectural rules" rule 2 for why the +scalar path stays around as an oracle even though the vectorized path is +the production data path. +""" + +from __future__ import annotations + +import math +import random + +import polars as pl +import pytest + +from pyfia.carbon.nsvb.coefficients import ( + load_nsvb_coefficients, + lookup_coefficients, +) +from pyfia.carbon.nsvb.equations import ( + Coefficients, + compute_nsvb_biomass, + predict_tree_biomass, +) + +# --------------------------------------------------------------------------- +# Synthetic tree generator +# --------------------------------------------------------------------------- + +# SPCDs with species-level rows in ALL 5 coefficient tables. Picked to span +# the Model 1, 2, 4 dispatches and both hardwood/softwood branches. +# +# (SPCD, WDSG_proxy, JENKINS_SPGRPCD_proxy, hw_sw): +# SPCD=110 shortleaf pine (softwood, Model 1/2 for volib) +# SPCD=202 Douglas-fir (softwood, Model 2 for volib — worked example 1) +# SPCD=121 longleaf pine (softwood, species-level) +# SPCD=131 loblolly pine (softwood, species-level) +# SPCD=316 red maple (hardwood, Model 1 volib, Model 4 total_agb — worked example 2) +# SPCD=802 white oak (hardwood, species-level) +# SPCD=833 chestnut oak (hardwood, species-level) +# SPCD=621 yellow-poplar (hardwood, species-level) +# +# The Jenkins fallback is exercised by injecting a few trees with an SPCD that +# isn't in the species-level tables (e.g., 9999 → falls through to Jenkins). +_SPECIES_LEVEL_SPCDS: list[tuple[int, float, int]] = [ + # (SPCD, WDSG, JENKINS_SPGRPCD) — WDSG and JENKINS are arbitrary but must + # be consistent between the vectorized and scalar runs. + (110, 0.46, 4), # shortleaf pine + (202, 0.45, 3), # Douglas-fir + (121, 0.54, 4), # longleaf pine + (131, 0.47, 4), # loblolly pine + (316, 0.49, 8), # red maple + (802, 0.60, 6), # white oak + (833, 0.57, 6), # chestnut oak + (621, 0.40, 8), # yellow-poplar +] + +# At least one Jenkins-fallback SPCD so the (Level 4) path is exercised. +# 9999 has no row in any *_spcd table, so the lookup cascades to the +# Jenkins group provided. Group 3 is "soft maple/birch" — arbitrary. +_JENKINS_FALLBACK_SPCDS: list[tuple[int, float, int]] = [ + (9999, 0.50, 3), +] + + +def _build_synthetic_trees(n: int, seed: int = 42) -> pl.DataFrame: + """Build a synthetic tree DataFrame for equivalence testing. + + Each tree gets random DIA (1.0-30.0 in), HT (5-130 ft), CULL (0 or 0-20), + sampled uniformly, with SPCD drawn from ``_SPECIES_LEVEL_SPCDS`` plus a + small fraction of Jenkins-fallback SPCDs. WDSG and JENKINS_SPGRPCD are + carried along unchanged so both the vectorized and scalar evaluators + see identical inputs. + + Also carries a pre-computed ``hw_sw`` column derived from SPCD<300 — + this matches the rule used by both ``_model_k`` and the vectorized + orchestrator, keeping the scalar oracle call consistent with the + vectorized path. + """ + rng = random.Random(seed) + all_species = _SPECIES_LEVEL_SPCDS + _JENKINS_FALLBACK_SPCDS + + rows: list[dict] = [] + for _ in range(n): + spcd, wdsg, jen = rng.choice(all_species) + dia = rng.uniform(1.0, 30.0) + ht = rng.uniform(5.0, 130.0) + # 30% of trees get nonzero cull (0-20%), 10% get NULL cull, rest 0. + r = rng.random() + if r < 0.1: + cull = None # test null handling + elif r < 0.4: + cull = rng.uniform(0.0, 20.0) + else: + cull = 0.0 + rows.append( + { + "SPCD": spcd, + "DIA": dia, + "HT": ht, + "CULL": cull, + "WDSG": wdsg, + "JENKINS_SPGRPCD": jen, + } + ) + + return pl.DataFrame( + rows, + schema={ + "SPCD": pl.Int64, + "DIA": pl.Float64, + "HT": pl.Float64, + "CULL": pl.Float64, + "WDSG": pl.Float64, + "JENKINS_SPGRPCD": pl.Int64, + }, + ) + + +# --------------------------------------------------------------------------- +# Scalar oracle helpers +# --------------------------------------------------------------------------- + + +def _bundle_via_csv(spcd: int, jenkins_spgrpcd: int) -> Coefficients: + """Build a scalar ``Coefficients`` bundle by walking the real CSV lookup. + + Mirrors the helper of the same name in ``test_nsvb_equations.py``. Duplicated + here so this file is self-contained and doesn't depend on test-module import + order. + """ + tables = load_nsvb_coefficients() + return Coefficients( + volib=lookup_coefficients( + tables.volib_spcd, + tables.volib_jenkins, + spcd=spcd, + jenkins_spgrpcd=jenkins_spgrpcd, + ), + volbk=lookup_coefficients( + tables.volbk_spcd, + tables.volbk_jenkins, + spcd=spcd, + jenkins_spgrpcd=jenkins_spgrpcd, + ), + bark_bio=lookup_coefficients( + tables.bark_biomass_spcd, + tables.bark_biomass_jenkins, + spcd=spcd, + jenkins_spgrpcd=jenkins_spgrpcd, + ), + branch_bio=lookup_coefficients( + tables.branch_biomass_spcd, + tables.branch_biomass_jenkins, + spcd=spcd, + jenkins_spgrpcd=jenkins_spgrpcd, + ), + total_agb=lookup_coefficients( + tables.total_biomass_spcd, + tables.total_biomass_jenkins, + spcd=spcd, + jenkins_spgrpcd=jenkins_spgrpcd, + ), + ) + + +def _scalar_per_tree(trees: pl.DataFrame) -> pl.DataFrame: + """Run the scalar oracle pipeline tree-by-tree and return a DataFrame. + + Matches the vectorized output schema: ``v_wood_ib``, ``v_bark``, + ``w_wood``, ``w_bark``, ``w_branch``, ``agb``. + """ + out = [] + for row in trees.iter_rows(named=True): + bundle = _bundle_via_csv(row["SPCD"], row["JENKINS_SPGRPCD"]) + hw_sw = "hardwood" if row["SPCD"] >= 300 else "softwood" + cull = row["CULL"] if row["CULL"] is not None else 0.0 + result = predict_tree_biomass( + spcd=row["SPCD"], + dia=row["DIA"], + ht=row["HT"], + coefficients=bundle, + wdsg=row["WDSG"], + hw_sw=hw_sw, + cull=cull, + ) + out.append( + { + "SPCD": row["SPCD"], + "v_wood_ib": result.v_wood_ib, + "v_bark": result.v_bark, + "w_wood": result.w_wood, + "w_bark": result.w_bark, + "w_branch": result.w_branch, + "agb": result.agb, + } + ) + return pl.DataFrame(out) + + +# --------------------------------------------------------------------------- +# Equivalence tests +# --------------------------------------------------------------------------- + + +class TestVectorizedMatchesScalarOracle: + """The vectorized pipeline must match the scalar oracle tree-for-tree.""" + + @pytest.mark.parametrize("n_trees", [50, 500]) + def test_equivalence_on_synthetic_trees(self, n_trees): + """For a diverse random tree set, vectorized == scalar at f64 precision. + + Covers Models 1, 2, 4 (via SPCDs that dispatch to each), hardwood + and softwood paths, trees with and without cull, nullable cull + values, and at least one Jenkins-fallback SPCD per batch. + + ``rel_tol=1e-9`` is tighter than the existing ``TestPipelineViaCSV`` + sentinels and is achievable because polars and the scalar math use + the same IEEE-754 operations on the same coefficient floats — there + is no semantic reordering. + """ + trees = _build_synthetic_trees(n=n_trees, seed=2026) + + vec = compute_nsvb_biomass(trees.lazy()).collect() + scalar = _scalar_per_tree(trees) + + # Row counts must match (nothing is filtered out) + assert vec.height == scalar.height == n_trees + + # Element-wise equivalence on all six output columns. + # We preserve row order on both sides (neither pipeline reorders), + # so positional comparison is valid. + for col in ("v_wood_ib", "v_bark", "w_wood", "w_bark", "w_branch", "agb"): + vec_vals = vec[col].to_list() + sc_vals = scalar[col].to_list() + for i, (v, s) in enumerate(zip(vec_vals, sc_vals)): + assert math.isclose(v, s, rel_tol=1e-9), ( + f"{col} divergence at row {i}: vectorized={v} scalar={s} " + f"(SPCD={trees['SPCD'][i]}, DIA={trees['DIA'][i]}, " + f"HT={trees['HT'][i]}, CULL={trees['CULL'][i]})" + ) + + def test_douglas_fir_worked_example_exact(self): + """Worked example 1: Douglas-fir intact tree matches scalar sentinel. + + This is the same tree locked in ``TestPipelineViaCSV`` for the scalar + path. The vectorized path must hit the same f64-tight sentinel values. + """ + trees = pl.DataFrame( + { + "SPCD": [202], + "DIA": [20.0], + "HT": [110.0], + "CULL": [0.0], + "WDSG": [0.45], + "JENKINS_SPGRPCD": [10], + } + ) + result = compute_nsvb_biomass(trees.lazy()).collect() + # Sentinels from DOUGFIR_CSV_EXPECTED in test_nsvb_equations.py + assert math.isclose(result["agb"][0], 3151.7844814148, rel_tol=1e-9) + assert math.isclose(result["w_wood"][0], 2442.4991973909, rel_tol=1e-9) + assert math.isclose(result["w_bark"][0], 387.9115217065, rel_tol=1e-9) + assert math.isclose(result["w_branch"][0], 321.3737623174, rel_tol=1e-9) + assert math.isclose(result["v_wood_ib"][0], 83.6844822743, rel_tol=1e-9) + assert math.isclose(result["v_bark"][0], 18.1718836702, rel_tol=1e-9) + + def test_red_maple_worked_example_exact(self): + """Worked example 2: red maple with 3% cull, exercises Model 4 + cull path. + + Same tree as in the scalar ``TestPipelineViaCSV`` regression sentinels. + Exercises the full cull-reduction + harmonization arithmetic under + the vectorized path. + """ + trees = pl.DataFrame( + { + "SPCD": [316], + "DIA": [11.1], + "HT": [38.0], + "CULL": [3.0], + "WDSG": [0.49], + "JENKINS_SPGRPCD": [8], + } + ) + result = compute_nsvb_biomass(trees.lazy()).collect() + # Sentinels from REDMAPLE_CSV_EXPECTED in test_nsvb_equations.py + assert math.isclose(result["agb"][0], 528.1359652182, rel_tol=1e-9) + assert math.isclose(result["w_wood"][0], 317.9304736164, rel_tol=1e-9) + assert math.isclose(result["w_bark"][0], 59.2156546044, rel_tol=1e-9) + assert math.isclose(result["w_branch"][0], 150.9898369973, rel_tol=1e-9) + assert math.isclose(result["v_wood_ib"][0], 9.4271133316, rel_tol=1e-9) + assert math.isclose(result["v_bark"][0], 2.1551061437, rel_tol=1e-9) + + def test_agb_equals_component_sum(self): + """Harmonization invariant: agb == w_wood + w_bark + w_branch per tree.""" + trees = _build_synthetic_trees(n=100, seed=99) + result = compute_nsvb_biomass(trees.lazy()).collect() + summed = result["w_wood"] + result["w_bark"] + result["w_branch"] + for i, (agb, s) in enumerate(zip(result["agb"].to_list(), summed.to_list())): + assert math.isclose(agb, s, rel_tol=1e-12), ( + f"row {i}: agb={agb} != sum of components={s}" + ) + + def test_cull_nulls_treated_as_zero(self): + """A null CULL is equivalent to CULL=0. + + The vectorized path calls ``.fill_null(0.0)`` on CULL; asserting that + here prevents silent behavior change if the null-handling is removed. + """ + trees = pl.DataFrame( + { + "SPCD": [202, 202], + "DIA": [20.0, 20.0], + "HT": [110.0, 110.0], + "CULL": [None, 0.0], + "WDSG": [0.45, 0.45], + "JENKINS_SPGRPCD": [3, 3], + }, + schema={ + "SPCD": pl.Int64, + "DIA": pl.Float64, + "HT": pl.Float64, + "CULL": pl.Float64, + "WDSG": pl.Float64, + "JENKINS_SPGRPCD": pl.Int64, + }, + ) + result = compute_nsvb_biomass(trees.lazy()).collect() + # Both rows should produce identical outputs + assert result["agb"][0] == result["agb"][1] + assert result["w_wood"][0] == result["w_wood"][1] + + def test_jenkins_fallback_tree(self): + """A tree with an SPCD absent from all *_spcd tables uses Jenkins fallback. + + The vectorized orchestrator's species-level join will return null + coefficients for SPCD=9999, and the coalesce with the Jenkins join + must fill them in from the JENKINS_SPGRPCD match. If this path is + broken, the output will be null, and the final AGB will also be null. + """ + trees = pl.DataFrame( + { + "SPCD": [9999], # no species-level row anywhere + "DIA": [15.0], + "HT": [60.0], + "CULL": [0.0], + "WDSG": [0.5], + "JENKINS_SPGRPCD": [3], # soft maple/birch group + } + ) + result = compute_nsvb_biomass(trees.lazy()).collect() + # Must not be null — Jenkins fallback resolved all 5 components + assert result["agb"][0] is not None + assert result["agb"][0] > 0 + # Match the scalar oracle exactly + scalar = _scalar_per_tree(trees) + assert math.isclose(result["agb"][0], scalar["agb"][0], rel_tol=1e-9) + + +class TestDivisionLookupPath: + """Level 2 (DIVISION-specific) lookup path. + + The synthetic-tree equivalence suite above covers only Levels 3-4 + because the scalar oracle ``predict_tree_biomass`` (via its + ``lookup_coefficients`` backend) always passes ``division=None``. + These tests exercise the new DIVISION coalesce path directly. + """ + + def test_division_lookup_produces_non_null_agb(self): + """A tree with a DIVISION that has a matching S1a/S6a/S7a/S8a row + should still produce a valid AGB (proving the 3-way coalesce + doesn't silently emit nulls when DIVISION is set).""" + # SPCD=131 (loblolly pine, the dominant Georgia species) in + # DIVISION 230 (Subtropical). + trees = pl.DataFrame( + { + "SPCD": [131], + "DIA": [15.0], + "HT": [80.0], + "CULL": [0.0], + "WDSG": [0.47], + "JENKINS_SPGRPCD": [4], + "DIVISION": ["230"], + } + ) + result = compute_nsvb_biomass(trees.lazy()).collect() + assert result["agb"][0] is not None + assert result["agb"][0] > 0 + # Harmonization invariant still holds + assert math.isclose( + result["agb"][0], + result["w_wood"][0] + result["w_bark"][0] + result["w_branch"][0], + rel_tol=1e-12, + ) + + def test_division_null_falls_back_to_species_level(self): + """A tree with DIVISION=null should produce the same result as a + tree without the DIVISION column at all — falling back to the + species-level lookup via coalesce.""" + trees_with_null = pl.DataFrame( + { + "SPCD": [131], + "DIA": [15.0], + "HT": [80.0], + "CULL": [0.0], + "WDSG": [0.47], + "JENKINS_SPGRPCD": [4], + "DIVISION": [None], + }, + schema={ + "SPCD": pl.Int64, + "DIA": pl.Float64, + "HT": pl.Float64, + "CULL": pl.Float64, + "WDSG": pl.Float64, + "JENKINS_SPGRPCD": pl.Int64, + "DIVISION": pl.Utf8, + }, + ) + trees_without = trees_with_null.drop("DIVISION") + + with_null = compute_nsvb_biomass(trees_with_null.lazy()).collect() + without = compute_nsvb_biomass(trees_without.lazy()).collect() + + for col in ("v_wood_ib", "v_bark", "w_wood", "w_bark", "w_branch", "agb"): + assert math.isclose(with_null[col][0], without[col][0], rel_tol=1e-12), ( + f"{col}: with_null={with_null[col][0]} != without={without[col][0]}" + ) + + def test_division_changes_result_for_species_with_division_row(self): + """Positive control: for a species with a DIVISION-specific row, + specifying the DIVISION should change the output vs the + species-level fallback. Proves the DIVISION lookup actually takes + effect rather than silently falling through to species-level. + + Uses SPCD=316 (red maple) in DIVISION=230 — verified via a direct + diff of ``bundle.total_agb_div`` vs ``bundle.total_agb_spcd`` that + this SPCD has a DIVISION=230 S8a row with coefficients differing + from the species-level row by a material amount (total_diff + ~0.36 across a, b, c). + """ + base_data = { + "SPCD": [316], + "DIA": [11.1], + "HT": [38.0], + "CULL": [0.0], + "WDSG": [0.49], + "JENKINS_SPGRPCD": [8], + } + trees_species_level = pl.DataFrame(base_data) + trees_division = pl.DataFrame({**base_data, "DIVISION": ["230"]}) + + species_result = compute_nsvb_biomass(trees_species_level.lazy()).collect() + division_result = compute_nsvb_biomass(trees_division.lazy()).collect() + + # At least one of the 5 components should differ materially + # (confirming the DIVISION lookup produced a different coefficient). + # If they all match exactly, the DIVISION row doesn't exist or the + # coalesce is silently bypassing it. + differs = any( + not math.isclose( + species_result[col][0], division_result[col][0], rel_tol=1e-6 + ) + for col in ("v_wood_ib", "v_bark", "w_wood", "w_bark", "w_branch", "agb") + ) + assert differs, ( + "species-level and DIVISION=230 results are identical — " + "SPCD=316 should have a differing DIVISION=230 row or the " + "DIVISION coalesce is not taking effect" + ) + + # Also assert that the directly-predicted total AGB actually + # responds to DIVISION (not just a noise-level component change). + assert not math.isclose( + species_result["agb"][0], division_result["agb"][0], rel_tol=1e-3 + ), ( + f"AGB essentially unchanged by DIVISION lookup: " + f"species={species_result['agb'][0]:.4f}, " + f"division={division_result['agb'][0]:.4f}" + ) + + def test_division_with_no_matching_row_falls_through(self): + """A tree with DIVISION set to a value that doesn't have a matching + row should fall through to species-level, matching a tree without + DIVISION set at all.""" + trees_bogus = pl.DataFrame( + { + "SPCD": [131], + "DIA": [15.0], + "HT": [80.0], + "CULL": [0.0], + "WDSG": [0.47], + "JENKINS_SPGRPCD": [4], + "DIVISION": ["NOPE"], + } + ) + trees_no_div = trees_bogus.drop("DIVISION") + + bogus_result = compute_nsvb_biomass(trees_bogus.lazy()).collect() + no_div_result = compute_nsvb_biomass(trees_no_div.lazy()).collect() + + for col in ("v_wood_ib", "v_bark", "w_wood", "w_bark", "w_branch", "agb"): + assert math.isclose( + bogus_result[col][0], no_div_result[col][0], rel_tol=1e-12 + ), ( + f"{col}: bogus_div={bogus_result[col][0]} != no_div={no_div_result[col][0]}" + ) diff --git a/tests/unit/test_soil_organic_estimator.py b/tests/unit/test_soil_organic_estimator.py new file mode 100644 index 00000000..0912e32e --- /dev/null +++ b/tests/unit/test_soil_organic_estimator.py @@ -0,0 +1,141 @@ +"""Unit tests for SoilOrganicEstimator class. + +Tests the SoilOrganicEstimator methods in isolation using mock data. +No database connection required. +""" + +import polars as pl +import pytest + +from pyfia.carbon.soil_organic import SoilOrganicEstimator, soil_organic + + +class MockDB: + """Mock database for testing estimator methods in isolation.""" + + def __init__(self): + self.db_path = "/fake/path" + self.tables = {} + self.evalid = None + self.evalids = None + self._state_filter = None + + +class TestGetRequiredTables: + """Tests for get_required_tables method.""" + + def test_returns_condition_level_tables(self): + config = {"pool": "total", "land_type": "forest"} + estimator = SoilOrganicEstimator(MockDB(), config) + tables = estimator.get_required_tables() + + assert "COND" in tables + assert "PLOT" in tables + assert "POP_PLOT_STRATUM_ASSGN" in tables + assert "POP_STRATUM" in tables + + def test_tree_table_not_required(self): + config = {"pool": "total", "land_type": "forest"} + estimator = SoilOrganicEstimator(MockDB(), config) + tables = estimator.get_required_tables() + assert "TREE" not in tables + + +class TestGetTreeColumns: + def test_returns_empty_list(self): + config = {"pool": "total"} + estimator = SoilOrganicEstimator(MockDB(), config) + cols = estimator.get_tree_columns() + assert cols == [] + + +class TestGetCondColumns: + def test_includes_carbon_soil_org_column(self): + config = {"land_type": "forest"} + estimator = SoilOrganicEstimator(MockDB(), config) + cols = estimator.get_cond_columns() + + assert "CARBON_SOIL_ORG" in cols + assert "PLT_CN" in cols + assert "CONDID" in cols + assert "CONDPROP_UNADJ" in cols + assert "COND_STATUS_CD" in cols + + +class TestCalculateValues: + """Tests for calculate_values method.""" + + @pytest.fixture + def mock_db(self): + return MockDB() + + def test_total_pool(self, mock_db): + config = {"pool": "total"} + estimator = SoilOrganicEstimator(mock_db, config) + + data = pl.DataFrame( + {"CARBON_SOIL_ORG": [25.4, 18.7]} + ).lazy() + + result = estimator.calculate_values(data).collect() + assert abs(result["CARBON_ACRE"][0] - 25.4) < 1e-10 + assert abs(result["CARBON_ACRE"][1] - 18.7) < 1e-10 + + def test_null_values_treated_as_zero(self, mock_db): + config = {"pool": "total"} + estimator = SoilOrganicEstimator(mock_db, config) + + data = pl.DataFrame( + {"CARBON_SOIL_ORG": [None, 20.0]} + ).lazy() + + result = estimator.calculate_values(data).collect() + assert result["CARBON_ACRE"][0] == 0.0 + assert abs(result["CARBON_ACRE"][1] - 20.0) < 1e-10 + + def test_all_null_gives_zero(self, mock_db): + config = {"pool": "total"} + estimator = SoilOrganicEstimator(mock_db, config) + + data = pl.DataFrame( + {"CARBON_SOIL_ORG": [None]} + ).lazy() + + result = estimator.calculate_values(data).collect() + assert result["CARBON_ACRE"][0] == 0.0 + + def test_empty_dataframe(self, mock_db): + config = {"pool": "total"} + estimator = SoilOrganicEstimator(mock_db, config) + + data = pl.DataFrame( + {"CARBON_SOIL_ORG": pl.Series([], dtype=pl.Float64)} + ).lazy() + + result = estimator.calculate_values(data).collect() + assert len(result) == 0 + + +class TestPoolValidation: + def test_invalid_pool_raises_error(self): + with pytest.raises(ValueError, match="Invalid pool"): + soil_organic(MockDB(), pool="ag") + + def test_bg_pool_raises_error(self): + with pytest.raises(ValueError, match="no AG/BG split"): + soil_organic(MockDB(), pool="bg") + + def test_total_pool_accepted(self): + with pytest.raises(Exception): + soil_organic(MockDB(), pool="total") + + def test_case_insensitive_total(self): + with pytest.raises(Exception): + soil_organic(MockDB(), pool="TOTAL") + + +class TestEstimatorLabel: + def test_label_is_soil_organic(self): + config = {"pool": "total"} + estimator = SoilOrganicEstimator(MockDB(), config) + assert estimator._estimator_label == "SoilOrganic" diff --git a/tests/unit/test_stock_change.py b/tests/unit/test_stock_change.py new file mode 100644 index 00000000..18f46867 --- /dev/null +++ b/tests/unit/test_stock_change.py @@ -0,0 +1,216 @@ +"""Unit tests for CarbonStockChangeEstimator and stock_change function. + +Tests the stock-change logic in isolation using mock data. +No database connection required. +""" + +import polars as pl +import pytest + +from pyfia.carbon.stock_change import ( + CarbonStockChangeEstimator, + _POOL_COLUMNS, + _VALID_POOLS, + stock_change, +) + + +class MockDB: + """Mock database for testing estimator methods in isolation.""" + + def __init__(self): + self.db_path = "/fake/path" + self.tables = {} + self.evalid = None + self.evalids = None + self._state_filter = None + + +class TestGetRequiredTables: + def test_returns_condition_level_tables(self): + config = {"pool": "downed_dead", "land_type": "forest"} + estimator = CarbonStockChangeEstimator(MockDB(), config) + tables = estimator.get_required_tables() + + assert "COND" in tables + assert "PLOT" in tables + assert "POP_PLOT_STRATUM_ASSGN" in tables + assert "POP_STRATUM" in tables + + def test_tree_table_not_required(self): + config = {"pool": "litter", "land_type": "forest"} + estimator = CarbonStockChangeEstimator(MockDB(), config) + assert "TREE" not in estimator.get_required_tables() + + +class TestGetTreeColumns: + def test_returns_empty_list(self): + config = {"pool": "soil_organic"} + estimator = CarbonStockChangeEstimator(MockDB(), config) + assert estimator.get_tree_columns() == [] + + +class TestGetCondColumns: + def test_includes_carbon_column_for_pool(self): + for pool, expected_cols in _POOL_COLUMNS.items(): + config = {"pool": pool, "land_type": "forest"} + estimator = CarbonStockChangeEstimator(MockDB(), config) + cols = estimator.get_cond_columns() + for ec in expected_cols: + assert ec in cols, f"{ec} missing for pool {pool}" + + +class TestCalculateValues: + """Tests for the delta calculation logic.""" + + @pytest.fixture + def mock_db(self): + return MockDB() + + def test_basic_delta(self, mock_db): + config = {"pool": "downed_dead", "annualize": False} + estimator = CarbonStockChangeEstimator(mock_db, config) + + data = pl.DataFrame( + { + "t2_CARBON_DOWN_DEAD": [5.0, 3.0], + "t1_CARBON_DOWN_DEAD": [4.0, 4.0], + "REMPER": [5.0, 5.0], + } + ).lazy() + + result = estimator.calculate_values(data).collect() + assert abs(result["CARBON_CHANGE_ACRE"][0] - 1.0) < 1e-10 + assert abs(result["CARBON_CHANGE_ACRE"][1] - (-1.0)) < 1e-10 + + def test_annualized_delta(self, mock_db): + config = {"pool": "litter", "annualize": True} + estimator = CarbonStockChangeEstimator(mock_db, config) + + data = pl.DataFrame( + { + "t2_CARBON_LITTER": [10.0], + "t1_CARBON_LITTER": [5.0], + "REMPER": [5.0], + } + ).lazy() + + result = estimator.calculate_values(data).collect() + # (10 - 5) / 5 = 1.0 + assert abs(result["CARBON_CHANGE_ACRE"][0] - 1.0) < 1e-10 + + def test_understory_sums_ag_bg(self, mock_db): + config = {"pool": "understory", "annualize": False} + estimator = CarbonStockChangeEstimator(mock_db, config) + + data = pl.DataFrame( + { + "t2_CARBON_UNDERSTORY_AG": [1.2], + "t2_CARBON_UNDERSTORY_BG": [0.3], + "t1_CARBON_UNDERSTORY_AG": [1.0], + "t1_CARBON_UNDERSTORY_BG": [0.2], + "REMPER": [5.0], + } + ).lazy() + + result = estimator.calculate_values(data).collect() + # (1.2+0.3) - (1.0+0.2) = 0.3 + assert abs(result["CARBON_CHANGE_ACRE"][0] - 0.3) < 1e-10 + + def test_null_t2_treated_as_zero(self, mock_db): + config = {"pool": "soil_organic", "annualize": False} + estimator = CarbonStockChangeEstimator(mock_db, config) + + data = pl.DataFrame( + { + "t2_CARBON_SOIL_ORG": [None], + "t1_CARBON_SOIL_ORG": [20.0], + "REMPER": [5.0], + } + ).lazy() + + result = estimator.calculate_values(data).collect() + assert abs(result["CARBON_CHANGE_ACRE"][0] - (-20.0)) < 1e-10 + + def test_null_t1_treated_as_zero(self, mock_db): + config = {"pool": "soil_organic", "annualize": False} + estimator = CarbonStockChangeEstimator(mock_db, config) + + data = pl.DataFrame( + { + "t2_CARBON_SOIL_ORG": [25.0], + "t1_CARBON_SOIL_ORG": [None], + "REMPER": [5.0], + } + ).lazy() + + result = estimator.calculate_values(data).collect() + assert abs(result["CARBON_CHANGE_ACRE"][0] - 25.0) < 1e-10 + + def test_negative_change(self, mock_db): + """Carbon loss produces negative values.""" + config = {"pool": "downed_dead", "annualize": True} + estimator = CarbonStockChangeEstimator(mock_db, config) + + data = pl.DataFrame( + { + "t2_CARBON_DOWN_DEAD": [2.0], + "t1_CARBON_DOWN_DEAD": [5.0], + "REMPER": [6.0], + } + ).lazy() + + result = estimator.calculate_values(data).collect() + # (2 - 5) / 6 = -0.5 + assert abs(result["CARBON_CHANGE_ACRE"][0] - (-0.5)) < 1e-10 + + +class TestPoolValidation: + def test_live_tree_raises_error(self): + with pytest.raises(ValueError, match="not yet implemented"): + stock_change(MockDB(), pool="live_tree") + + def test_standing_dead_raises_error(self): + with pytest.raises(ValueError, match="not yet implemented"): + stock_change(MockDB(), pool="standing_dead") + + def test_invalid_pool_raises_error(self): + with pytest.raises(ValueError, match="Invalid pool"): + stock_change(MockDB(), pool="invalid") + + def test_valid_pools_accepted(self): + """Valid pools should pass pool validation (fail later on DB access).""" + for pool in _VALID_POOLS: + with pytest.raises(Exception): + stock_change(MockDB(), pool=pool) + + def test_all_pool_accepted(self): + with pytest.raises(Exception): + stock_change(MockDB(), pool="all") + + def test_list_of_pools_accepted(self): + with pytest.raises(Exception): + stock_change(MockDB(), pool=["downed_dead", "litter"]) + + def test_list_with_tree_pool_raises(self): + with pytest.raises(ValueError, match="not yet implemented"): + stock_change(MockDB(), pool=["downed_dead", "live_tree"]) + + +class TestEstimatorLabel: + def test_label(self): + config = {"pool": "downed_dead"} + estimator = CarbonStockChangeEstimator(MockDB(), config) + assert estimator._estimator_label == "CarbonStockChange" + + +class TestPoolColumnMapping: + def test_all_valid_pools_have_columns(self): + for pool in _VALID_POOLS: + assert pool in _POOL_COLUMNS + assert len(_POOL_COLUMNS[pool]) > 0 + + def test_understory_has_ag_and_bg(self): + cols = _POOL_COLUMNS["understory"] + assert "CARBON_UNDERSTORY_AG" in cols + assert "CARBON_UNDERSTORY_BG" in cols diff --git a/tests/unit/test_total_ecosystem.py b/tests/unit/test_total_ecosystem.py new file mode 100644 index 00000000..5f4c73a7 --- /dev/null +++ b/tests/unit/test_total_ecosystem.py @@ -0,0 +1,262 @@ +"""Unit tests for total_ecosystem function. + +Tests the pool-summing logic in isolation using monkeypatched pool functions. +No database connection required. +""" + +from unittest.mock import patch + +import polars as pl +import pytest + +from pyfia.carbon.total_ecosystem import total_ecosystem + + +class MockDB: + """Mock database for testing.""" + + def __init__(self): + self.db_path = "/fake/path" + self.tables = {} + self.evalid = [132301] + self.evalids = None + self._state_filter = None + + +def _make_pool_result( + carbon_acre: float, carbon_total: float, n_plots: int = 100, year: int = 2023 +) -> pl.DataFrame: + """Build a minimal pool result DataFrame.""" + return pl.DataFrame( + { + "YEAR": [year], + "POOL": ["TOTAL"], + "CARBON_ACRE": [carbon_acre], + "CARBON_TOTAL": [carbon_total], + "N_PLOTS": [n_plots], + "N_TREES": [0], + } + ) + + +def _make_empty_result() -> pl.DataFrame: + return pl.DataFrame( + { + "YEAR": pl.Series([], dtype=pl.Int64), + "POOL": pl.Series([], dtype=pl.Utf8), + "CARBON_ACRE": pl.Series([], dtype=pl.Float64), + "CARBON_TOTAL": pl.Series([], dtype=pl.Float64), + "N_PLOTS": pl.Series([], dtype=pl.Int64), + "N_TREES": pl.Series([], dtype=pl.Int64), + } + ) + + +# Patch at the source modules where the functions are defined +_POOL_PATCHES = { + "live_tree": "pyfia.carbon.live_tree.live_tree", + "standing_dead": "pyfia.carbon.standing_dead.standing_dead", + "understory": "pyfia.carbon.understory.understory", + "downed_dead": "pyfia.carbon.downed_dead.downed_dead", + "litter": "pyfia.carbon.litter.litter", + "soil_organic": "pyfia.carbon.soil_organic.soil_organic", +} + + +def _start_pool_patches(pool_results: dict) -> list: + """Start patches for all six pool functions. Returns list of patchers.""" + patchers = [] + for name, target in _POOL_PATCHES.items(): + p = patch(target, return_value=pool_results.get(name, _make_pool_result(1.0, 1000.0))) + p.start() + patchers.append(p) + return patchers + + +def _stop_pool_patches(patchers: list): + for p in patchers: + p.stop() + + +@pytest.fixture +def mock_infra(): + """Patch ensure_fia_instance and ensure_evalid_set.""" + with patch( + "pyfia.carbon.total_ecosystem.ensure_fia_instance", + return_value=(MockDB(), False), + ), patch("pyfia.carbon.total_ecosystem.ensure_evalid_set"): + yield + + +class TestTotalEcosystemSumming: + """Tests for the pool-summing logic.""" + + def test_sums_carbon_acre_correctly(self, mock_infra): + results = { + "live_tree": _make_pool_result(10.0, 100000.0), + "standing_dead": _make_pool_result(2.0, 20000.0), + "understory": _make_pool_result(1.0, 10000.0), + "downed_dead": _make_pool_result(3.0, 30000.0), + "litter": _make_pool_result(4.0, 40000.0), + "soil_organic": _make_pool_result(20.0, 200000.0), + } + patchers = _start_pool_patches(results) + try: + result = total_ecosystem(MockDB()) + total_row = result.filter(pl.col("POOL") == "TOTAL_ECOSYSTEM") + assert abs(float(total_row["CARBON_ACRE"][0]) - 40.0) < 1e-10 + assert abs(float(total_row["CARBON_TOTAL"][0]) - 400000.0) < 1e-10 + finally: + _stop_pool_patches(patchers) + + def test_result_has_seven_rows(self, mock_infra): + """6 pool rows + 1 TOTAL_ECOSYSTEM row.""" + results = { + name: _make_pool_result(1.0, 1000.0) + for name in _POOL_PATCHES + } + patchers = _start_pool_patches(results) + try: + result = total_ecosystem(MockDB()) + assert len(result) == 7 + pools = result["POOL"].to_list() + for expected in [ + "TOTAL_ECOSYSTEM", "LIVE_TREE", "STANDING_DEAD", + "UNDERSTORY", "DOWNED_DEAD", "LITTER", "SOIL_ORGANIC", + ]: + assert expected in pools + finally: + _stop_pool_patches(patchers) + + def test_handles_all_empty_results(self, mock_infra): + """If all pools return empty, total_ecosystem returns a fallback row.""" + results = {name: _make_empty_result() for name in _POOL_PATCHES} + patchers = _start_pool_patches(results) + try: + result = total_ecosystem(MockDB()) + assert len(result) > 0 + assert result["CARBON_ACRE"][0] == 0.0 + finally: + _stop_pool_patches(patchers) + + def test_year_propagates(self, mock_infra): + results = { + name: _make_pool_result(1.0, 1000.0, year=2023) + for name in _POOL_PATCHES + } + patchers = _start_pool_patches(results) + try: + result = total_ecosystem(MockDB()) + total_row = result.filter(pl.col("POOL") == "TOTAL_ECOSYSTEM") + assert int(total_row["YEAR"][0]) == 2023 + finally: + _stop_pool_patches(patchers) + + +class TestTotalEcosystemSignature: + def test_no_pool_parameter(self): + """total_ecosystem has no pool parameter.""" + import inspect + sig = inspect.signature(total_ecosystem) + assert "pool" not in sig.parameters + + def test_has_grp_by_parameter(self): + """total_ecosystem accepts grp_by, matching sibling estimators.""" + import inspect + sig = inspect.signature(total_ecosystem) + assert "grp_by" in sig.parameters + assert sig.parameters["grp_by"].default is None + + +def _make_grouped_pool_result( + rows: list[tuple], + group_col: str = "FORTYPCD", + year: int = 2023, +) -> pl.DataFrame: + """Build a multi-row pool result keyed by a grouping column. + + Each ``rows`` entry is ``(group_value, carbon_acre, carbon_total)``. + """ + return pl.DataFrame( + { + "YEAR": [year] * len(rows), + group_col: [r[0] for r in rows], + "POOL": ["TOTAL"] * len(rows), + "CARBON_ACRE": [r[1] for r in rows], + "CARBON_TOTAL": [r[2] for r in rows], + "N_PLOTS": [100] * len(rows), + "N_TREES": [0] * len(rows), + } + ) + + +class TestTotalEcosystemGrpBy: + """Tests for grp_by forwarding and per-group summing.""" + + def test_grp_by_forwarded_to_each_pool(self, mock_infra): + """grp_by must be passed through to every pool estimator.""" + result_df = _make_grouped_pool_result([("A", 1.0, 100.0)]) + mocks: dict[str, object] = {} + patchers = [] + try: + for name, target in _POOL_PATCHES.items(): + p = patch(target, return_value=result_df) + mocks[name] = p.start() + patchers.append(p) + total_ecosystem(MockDB(), grp_by="FORTYPCD") + for name, mock in mocks.items(): + assert mock.call_args is not None, f"pool {name} was not called" + kwargs = mock.call_args.kwargs + assert kwargs.get("grp_by") == "FORTYPCD", ( + f"pool {name} did not receive grp_by; got {kwargs}" + ) + finally: + for p in patchers: + p.stop() + + def test_grp_by_sums_per_group(self, mock_infra): + """TOTAL_ECOSYSTEM row is summed within each group, not collapsed.""" + # Two forest type codes, each pool reports both. + results = { + name: _make_grouped_pool_result( + [("A", 1.0, 100.0), ("B", 2.0, 200.0)] + ) + for name in _POOL_PATCHES + } + patchers = _start_pool_patches(results) + try: + result = total_ecosystem(MockDB(), grp_by="FORTYPCD") + totals = result.filter(pl.col("POOL") == "TOTAL_ECOSYSTEM").sort( + "FORTYPCD" + ) + # 2 groups × 1 TOTAL row each + assert len(totals) == 2 + assert totals["FORTYPCD"].to_list() == ["A", "B"] + # 6 pools × 1.0 = 6.0 for group A, 6 × 2.0 = 12.0 for group B + assert abs(float(totals["CARBON_ACRE"][0]) - 6.0) < 1e-10 + assert abs(float(totals["CARBON_ACRE"][1]) - 12.0) < 1e-10 + # 6 × 100 = 600, 6 × 200 = 1200 + assert abs(float(totals["CARBON_TOTAL"][0]) - 600.0) < 1e-10 + assert abs(float(totals["CARBON_TOTAL"][1]) - 1200.0) < 1e-10 + finally: + _stop_pool_patches(patchers) + + def test_grp_by_preserves_pool_rows(self, mock_infra): + """Pool rows survive grp_by — output has pool rows + TOTAL rows per group.""" + results = { + name: _make_grouped_pool_result( + [("A", 1.0, 100.0), ("B", 2.0, 200.0)] + ) + for name in _POOL_PATCHES + } + patchers = _start_pool_patches(results) + try: + result = total_ecosystem(MockDB(), grp_by="FORTYPCD") + # 6 pools × 2 groups + 2 TOTAL rows = 14 rows + assert len(result) == 14 + assert "FORTYPCD" in result.columns + # Each pool appears for each group + pool_a = result.filter(pl.col("FORTYPCD") == "A") + assert len(pool_a) == 7 # 6 pools + 1 TOTAL + finally: + _stop_pool_patches(patchers) diff --git a/tests/unit/test_understory_estimator.py b/tests/unit/test_understory_estimator.py new file mode 100644 index 00000000..93d3fffb --- /dev/null +++ b/tests/unit/test_understory_estimator.py @@ -0,0 +1,210 @@ +"""Unit tests for UnderstoryEstimator class. + +Tests the UnderstoryEstimator methods in isolation using mock data. +No database connection required. +""" + +import polars as pl +import pytest + +from pyfia.carbon.understory import UnderstoryEstimator, understory + + +class MockDB: + """Mock database for testing estimator methods in isolation.""" + + def __init__(self): + self.db_path = "/fake/path" + self.tables = {} + self.evalid = None + self.evalids = None + self._state_filter = None + + +class TestGetRequiredTables: + """Tests for get_required_tables method.""" + + def test_returns_condition_level_tables(self): + config = {"pool": "ag", "land_type": "forest"} + estimator = UnderstoryEstimator(MockDB(), config) + tables = estimator.get_required_tables() + + assert "COND" in tables + assert "PLOT" in tables + assert "POP_PLOT_STRATUM_ASSGN" in tables + assert "POP_STRATUM" in tables + + def test_tree_table_not_required(self): + config = {"pool": "ag", "land_type": "forest"} + estimator = UnderstoryEstimator(MockDB(), config) + tables = estimator.get_required_tables() + assert "TREE" not in tables + + def test_tables_consistent_across_pools(self): + for pool in ["ag", "bg", "total"]: + config = {"pool": pool} + estimator = UnderstoryEstimator(MockDB(), config) + tables = estimator.get_required_tables() + assert "TREE" not in tables + assert "COND" in tables + + +class TestGetTreeColumns: + def test_returns_empty_list(self): + config = {"pool": "ag"} + estimator = UnderstoryEstimator(MockDB(), config) + cols = estimator.get_tree_columns() + assert cols == [] + + +class TestGetCondColumns: + def test_includes_understory_columns(self): + config = {"land_type": "forest"} + estimator = UnderstoryEstimator(MockDB(), config) + cols = estimator.get_cond_columns() + + assert "CARBON_UNDERSTORY_AG" in cols + assert "CARBON_UNDERSTORY_BG" in cols + assert "PLT_CN" in cols + assert "CONDID" in cols + assert "CONDPROP_UNADJ" in cols + assert "COND_STATUS_CD" in cols + + +class TestCalculateValues: + """Tests for calculate_values method.""" + + @pytest.fixture + def mock_db(self): + return MockDB() + + def test_ag_pool(self, mock_db): + config = {"pool": "ag"} + estimator = UnderstoryEstimator(mock_db, config) + + data = pl.DataFrame( + { + "CARBON_UNDERSTORY_AG": [1.5, 0.8], + "CARBON_UNDERSTORY_BG": [0.15, 0.08], + } + ).lazy() + + result = estimator.calculate_values(data).collect() + assert abs(result["CARBON_ACRE"][0] - 1.5) < 1e-10 + assert abs(result["CARBON_ACRE"][1] - 0.8) < 1e-10 + + def test_bg_pool(self, mock_db): + config = {"pool": "bg"} + estimator = UnderstoryEstimator(mock_db, config) + + data = pl.DataFrame( + { + "CARBON_UNDERSTORY_AG": [1.5, 0.8], + "CARBON_UNDERSTORY_BG": [0.15, 0.08], + } + ).lazy() + + result = estimator.calculate_values(data).collect() + assert abs(result["CARBON_ACRE"][0] - 0.15) < 1e-10 + assert abs(result["CARBON_ACRE"][1] - 0.08) < 1e-10 + + def test_total_pool(self, mock_db): + config = {"pool": "total"} + estimator = UnderstoryEstimator(mock_db, config) + + data = pl.DataFrame( + { + "CARBON_UNDERSTORY_AG": [1.5, 0.8], + "CARBON_UNDERSTORY_BG": [0.15, 0.08], + } + ).lazy() + + result = estimator.calculate_values(data).collect() + assert abs(result["CARBON_ACRE"][0] - 1.65) < 1e-10 + assert abs(result["CARBON_ACRE"][1] - 0.88) < 1e-10 + + def test_null_values_treated_as_zero(self, mock_db): + config = {"pool": "total"} + estimator = UnderstoryEstimator(mock_db, config) + + data = pl.DataFrame( + { + "CARBON_UNDERSTORY_AG": [None, 1.0], + "CARBON_UNDERSTORY_BG": [0.1, None], + } + ).lazy() + + result = estimator.calculate_values(data).collect() + assert abs(result["CARBON_ACRE"][0] - 0.1) < 1e-10 + assert abs(result["CARBON_ACRE"][1] - 1.0) < 1e-10 + + def test_both_null_gives_zero(self, mock_db): + config = {"pool": "total"} + estimator = UnderstoryEstimator(mock_db, config) + + data = pl.DataFrame( + { + "CARBON_UNDERSTORY_AG": [None], + "CARBON_UNDERSTORY_BG": [None], + } + ).lazy() + + result = estimator.calculate_values(data).collect() + assert result["CARBON_ACRE"][0] == 0.0 + + def test_empty_dataframe(self, mock_db): + config = {"pool": "ag"} + estimator = UnderstoryEstimator(mock_db, config) + + data = pl.DataFrame( + { + "CARBON_UNDERSTORY_AG": pl.Series([], dtype=pl.Float64), + "CARBON_UNDERSTORY_BG": pl.Series([], dtype=pl.Float64), + } + ).lazy() + + result = estimator.calculate_values(data).collect() + assert len(result) == 0 + + def test_bg_is_ten_percent_of_total(self, mock_db): + """BG should be ~10% of total per the Smith et al. 2006 convention.""" + # Using realistic values where BG ≈ AG / 9 + ag = 1.08 + bg = 0.12 # 10% of total (ag + bg = 1.2) + config_total = {"pool": "total"} + config_bg = {"pool": "bg"} + + estimator_total = UnderstoryEstimator(mock_db, config_total) + estimator_bg = UnderstoryEstimator(mock_db, config_bg) + + data = pl.DataFrame( + { + "CARBON_UNDERSTORY_AG": [ag], + "CARBON_UNDERSTORY_BG": [bg], + } + ).lazy() + + total = estimator_total.calculate_values(data).collect()["CARBON_ACRE"][0] + bg_val = estimator_bg.calculate_values(data).collect()["CARBON_ACRE"][0] + assert abs(bg_val / total - 0.1) < 1e-10 + + +class TestPoolValidation: + def test_invalid_pool_raises_error(self): + with pytest.raises(ValueError, match="Invalid pool"): + understory(MockDB(), pool="invalid") + + def test_valid_pools_accepted(self): + # These should not raise ValueError on pool validation + # (they'll fail on DB access later) + for pool in ["ag", "bg", "total", "AG", "BG", "TOTAL"]: + with pytest.raises(Exception): + # Will fail on DB access, but pool validation passes + understory(MockDB(), pool=pool) + + +class TestEstimatorLabel: + def test_label_is_understory(self): + config = {"pool": "ag"} + estimator = UnderstoryEstimator(MockDB(), config) + assert estimator._estimator_label == "Understory" diff --git a/tests/validation/test_live_tree_nsvb.py b/tests/validation/test_live_tree_nsvb.py new file mode 100644 index 00000000..cab541fe --- /dev/null +++ b/tests/validation/test_live_tree_nsvb.py @@ -0,0 +1,431 @@ +"""NSVB live tree carbon parity validation against FIADB TREE.CARBON_AG. + +This is the PR 2+ validation gate — see PR 2 (``pyfia.carbon.live_tree``) +and the ongoing PR 3 closure work. The test answers the question the +in-repo unit/equivalence suite cannot: *how closely does the NSVB +pipeline agree with FIADB's pre-computed carbon values on real inventory +data for the official Georgia 2024 EXPVOL evaluation (EVALID 132401)?* + +History of the ratchet thresholds in this file: + +**Phase 1 baseline** (species-level fallback only, no EVALID filter, +measured before the ECOSUBCD lookup landed — commit e1f0254): + +- Biomass ratio (pyfia NSVB AGB / FIADB DRYBIO_AG): + median **1.030**, mean **1.093**, p95 **1.411** +- ~50% of trees agree within 5%; ~34% within 1%; ~29% within 0.1% +- Rooted in the PR 2 Phase 1 choice to skip DIVISION-specific + coefficient rows (S1a-S8a columns keyed on Bailey ecoprovince) + +**Phase 1.5 closure** (no EVALID filter, ECOSUBCD → DIVISION lookup +wired in — commit adf3635): + +- Joins ``PLOTGEOM.ECOSUBCD`` → derives the Bailey DIVISION via + :func:`pyfia.carbon.nsvb.coefficients.ecosubcd_to_division` → + passes the ``DIVISION`` column through ``compute_nsvb_biomass``, which + then activates the Level 2 coalesce. +- median rel_err 4.87% → 3.55%, biomass ratio median 1.0179 → 1.0000. + +**Phase 1.6 finding — validation scope correction** (this commit): + +- The Phase 1 / 1.5 numbers above were measured on the FULL Georgia DB + (1.46M live trees), which includes 575k pre-1989 periodic-inventory + trees (1972, 1982, 1989 panels) NOT in the current annual evaluation. +- Those periodic-era trees have FIADB ``CARBON_AG`` / ``DRYBIO_AG`` + computed via the legacy Component Ratio Method (CRM, flat 0.5 carbon + fraction; see Appendix K of FIADB User Guide v9.1, K-1) rather than + NSVB. Comparing pyfia's NSVB recompute against legacy-CRM data was + producing spurious 1,000-12,000% rel_err outliers — almost all + CULL ≥ 90 TREECLCD=4 hardwoods from the periodic panels. +- The Phase 1.6 task as originally framed (TREECLCD=4 cull formula + investigation) was misdiagnosed: the pyfia cull formula + ``(1 - (1 - DENSITY_PROP) * CULL/100) * Stem Wood`` matches Appendix K + exactly, with no TREECLCD dispatch. The actual fix is to scope the + validation test to the EVALID set, which we now do via + ``JOIN POP_PLOT_STRATUM_ASSGN ppsa ON t.PLT_CN = ppsa.PLT_CN + AND ppsa.EVALID = 132401`` in the SQL above. +- Massive measured improvement on the EVALID-filtered set + (130,952 trees): median rel_err **3.55% → 0.085%** (42x), within-1% + **43% → 63%**, within-0.1% **39% → 58%**, max rel_err + **12,425% → 478%**, mean **9.6% → 4.4%**. + +FIADB implied carbon fraction (``CARBON_AG / DRYBIO_AG``) on the +EVALID-filtered set is **0.42-0.53** with median 0.477, exactly the +S10a range — confirming the EVALID 132401 trees are all NSVB-era and +the fraction layer is not the source of any residual gap. + +**Requires** the ``PLOTGEOM`` table in the test database. ``PLOTGEOM`` +is in ``pyfia.downloader.COMMON_TABLES`` as of the Phase 1.5 PR, so any +fresh ``pyfia.download(state)`` call pulls it automatically. Existing +test databases downloaded *before* that change need a one-off import: + +.. code-block:: python + + # Only needed for test databases downloaded before PLOTGEOM was + # added to COMMON_TABLES. New downloads include it automatically. + from pyfia.downloader.client import DataMartClient + import duckdb, tempfile + from pathlib import Path + + client = DataMartClient() + with tempfile.TemporaryDirectory() as tmp: + p = Path(tmp) + client.download_tables("GA", tables=["PLOTGEOM"], common=False, dest_dir=p) + conn = duckdb.connect("data/georgia.duckdb") + csv = next(p.glob("*.csv")) + conn.execute(f\"\"\" + CREATE OR REPLACE TABLE PLOTGEOM AS + SELECT * FROM read_csv_auto('{csv}', header=true, ignore_errors=true) + \"\"\") + conn.close() + +If ``PLOTGEOM`` is missing the test skips with a helpful message. +""" + +from __future__ import annotations + +import duckdb +import polars as pl +import pytest + +from pyfia.carbon.nsvb.carbon_fractions import ( + _compute_default_live_carbon_fraction, + load_carbon_fractions_live_df, +) +from pyfia.carbon.nsvb.coefficients import ecosubcd_to_division_expr +from pyfia.carbon.nsvb.equations import compute_nsvb_biomass + +# Ratchet thresholds — locked at the Phase 1.6 baseline (EVALID-filtered +# scope, ECOSUBCD → DIVISION lookup active). Any commit that LOOSENS these +# is a regression; any commit that tightens them is a measurable improvement. +# +# Phase 1 baseline (no EVALID filter, before DIVISION lookup, commit e1f0254): +# median rel_err = 4.87%, p99 = 65.23%, within 5% = 50.36%, +# biomass ratio median = 1.0179 +# +# Phase 1.5 baseline (no EVALID filter, DIVISION lookup wired, commit adf3635): +# median rel_err = 3.55% (-27%), p99 = 65.55% (~unchanged), within 5% = 53.90% +# biomass ratio median = 1.0000, within 1% = 43.07%, within 0.1% = 38.83% +# +# Phase 1.6 baseline (EVALID 132401 filter applied, this commit): +# The unfiltered baselines above were polluted by 575k pre-1989 +# periodic-inventory trees that FIADB computed via legacy CRM. Filtering +# to the official EVALID 132401 evaluation (130,952 NSVB-era trees) +# reveals the real Phase 1.5/1.7 quality: +# median rel_err = 0.0846% (42x better), p99 = 40.37% (1.6x better), +# within 5% = 73.32%, within 1% = 62.91%, within 0.1% = 58.20%, +# max rel_err = 478.62% (down from 12,425%), biomass ratio median = 1.0000 +_BASELINE_MEDIAN_REL_ERR = 0.005 # Phase 1.6: 0.000846 +_BASELINE_P99_REL_ERR = 0.50 # Phase 1.6: 0.4037 +_BASELINE_WITHIN_5PCT_FRAC = 0.70 # Phase 1.6: 0.7332 (lower bound) +_BASELINE_WITHIN_1PCT_FRAC = 0.60 # Phase 1.6: 0.6291 (lower bound) +_BASELINE_WITHIN_0P1PCT_FRAC = 0.55 # Phase 1.6: 0.5820 (lower bound) +_BASELINE_BIOMASS_RATIO_MEDIAN_MIN = 0.999 # Phase 1.6: 1.0000 +_BASELINE_BIOMASS_RATIO_MEDIAN_MAX = 1.001 # Phase 1.6: 1.0000 + +# Trees with SPCD not covered by any NSVB coefficient path (species-level +# OR Jenkins fallback) will get ``agb=None`` from compute_nsvb_biomass. +# Allow a little headroom — the EVALID-filtered set has even fewer null +# trees than the unfiltered set since it's all annual-inventory data. +_MAX_NULL_FRAC = 0.005 # Phase 1.6 EVALID-filtered: well below 0.001 + + +class TestLiveTreeNSVBParity: + """Per-tree NSVB AG vs FIADB ``TREE.CARBON_AG`` on real Georgia data. + + The baseline locks PR 2's species-level-only Phase 1 behavior. Each + subsequent commit that improves the NSVB pipeline's fidelity (DIVISION + lookup, missing SPCD coverage, etc.) should tighten the thresholds + below. + """ + + def test_per_tree_ag_agrees_with_fiadb(self, fia_db): + """Per-tree AG carbon parity between PR 2 and FIADB. + + Runs the vectorized NSVB pipeline on every eligible live Georgia + tree, converts to carbon via species-specific S10a fractions, and + diffs against FIADB's pre-computed ``TREE.CARBON_AG`` column. + + Reports the relative-error distribution plus the biomass ratio + (pyfia / FIADB) and the FIADB-implied carbon fraction as separate + diagnostic layers — if future validation reveals a regression, + the layered stats localize which layer moved. + """ + # Load every eligible live tree in one go, joined to PLOTGEOM to + # get ECOSUBCD for the Phase 1.5 DIVISION lookup. Skip the test + # gracefully when PLOTGEOM is missing so CI runs without it don't + # fail — this file is gated on the ``validation`` marker and is + # opt-in by design. + conn = duckdb.connect(fia_db, read_only=True) + try: + existing = set( + conn.execute("SELECT table_name FROM information_schema.tables") + .pl()["table_name"] + .to_list() + ) + if "PLOTGEOM" not in existing: + pytest.skip( + "PLOTGEOM table missing from the test database. " + "Phase 1.5 validation requires ECOSUBCD. As of the " + "Phase 1.5 PR, PLOTGEOM is in pyfia.downloader" + ".COMMON_TABLES and new pyfia.download() calls " + "include it automatically. For older test databases, " + "see this file's module docstring for the one-off " + "PLOTGEOM import script." + ) + + # Phase 1.6 fix: filter to EVALID 132401 (the official Georgia + # 2024 EXPVOL evaluation, ~131k trees). Without this filter, the + # query pulled in 575k pre-1989 periodic-inventory trees from + # 1972/1982/1989 panels — those have FIADB CARBON_AG/DRYBIO_AG + # computed via the legacy Component Ratio Method (CRM, flat 0.5 + # carbon fraction; see Appendix K of FIADB User Guide v9.1, K-1) + # rather than NSVB. Comparing pyfia's NSVB recompute against + # legacy-CRM data was producing spurious 1,000-12,000% rel_err + # outliers (almost all CULL ≥ 90 TREECLCD=4 hardwoods from the + # periodic inventory) that masked the real Phase 1.5/1.7 quality. + # The DISTINCT is required because POP_PLOT_STRATUM_ASSGN can + # have duplicate (PLT_CN, EVALID) rows in some states. + trees = conn.execute(""" + SELECT DISTINCT + t.CN, + t.SPCD, + t.DIA, + t.HT, + t.CULL, + t.CARBON_AG AS FIADB_CARBON_AG, + t.DRYBIO_AG AS FIADB_DRYBIO_AG, + rs.JENKINS_SPGRPCD, + rs.WOOD_SPGR_GREENVOL_DRYWT AS WDSG, + pg.ECOSUBCD + FROM TREE t + JOIN POP_PLOT_STRATUM_ASSGN ppsa + ON t.PLT_CN = ppsa.PLT_CN + LEFT JOIN REF_SPECIES rs ON t.SPCD = rs.SPCD + LEFT JOIN PLOTGEOM pg ON t.PLT_CN = pg.CN + WHERE ppsa.EVALID = 132401 + AND t.STATUSCD = 1 + AND t.DIA IS NOT NULL AND t.DIA >= 1.0 + AND t.HT IS NOT NULL + AND t.SPCD IS NOT NULL + AND t.CARBON_AG IS NOT NULL + AND rs.JENKINS_SPGRPCD IS NOT NULL + AND rs.WOOD_SPGR_GREENVOL_DRYWT IS NOT NULL + """).pl() + finally: + conn.close() + + n_total = trees.height + assert n_total > 0, f"no eligible trees found in {fia_db}" + + # Derive the Bailey DIVISION from ECOSUBCD. ``ecosubcd_to_division`` + # returns None for null/malformed ECOSUBCDs, which the downstream + # coalesce handles correctly by falling through to species-level + # and Jenkins. + trees = trees.with_columns( + ecosubcd_to_division_expr("ECOSUBCD").alias("DIVISION") + ) + n_with_division = int(trees["DIVISION"].is_not_null().sum()) + + # Normalize dtypes — see PR 2 commit 12a87c9 for why SPCD must be + # cast to Int64 here (CSV-loaded TREE.SPCD lands as Float64). + trees = trees.with_columns( + [ + pl.col("SPCD").cast(pl.Int64), + pl.col("JENKINS_SPGRPCD").cast(pl.Int64), + pl.col("DIA").cast(pl.Float64), + pl.col("HT").cast(pl.Float64), + pl.col("CULL").cast(pl.Float64), + pl.col("WDSG").cast(pl.Float64), + pl.col("FIADB_CARBON_AG").cast(pl.Float64), + pl.col("FIADB_DRYBIO_AG").cast(pl.Float64), + ] + ) + + # Run the vectorized NSVB biomass pipeline (same entry point + # LiveTreeEstimator.calculate_values uses). + result = compute_nsvb_biomass(trees.lazy()).collect() + + # Count trees with null agb (SPCD coverage gaps). Report then drop. + n_null = int(result["agb"].null_count()) + null_frac = n_null / n_total + print(f"\n=== Live tree NSVB parity vs FIADB (Georgia, {n_total:,} trees) ===") + print( + f" trees with DIVISION resolved from ECOSUBCD: " + f"{n_with_division:,} ({n_with_division / n_total:.2%})" + ) + print( + f" trees with null NSVB agb (SPCD coverage gap): " + f"{n_null:,} ({null_frac:.3%})" + ) + + result = result.filter(pl.col("agb").is_not_null()) + + # Apply species-specific S10a carbon fractions with default-mean + # fallback, matching LiveTreeEstimator.calculate_values exactly. + cf_df = load_carbon_fractions_live_df() + default_frac = _compute_default_live_carbon_fraction() + result = result.join(cf_df, on="SPCD", how="left") + result = result.with_columns( + [ + pl.col("CARBON_FRAC_LIVE") + .fill_null(default_frac) + .alias("CARBON_FRAC_LIVE"), + ] + ) + result = result.with_columns( + [ + (pl.col("agb") * pl.col("CARBON_FRAC_LIVE")).alias("pyfia_CARBON_AG"), + # Biomass ratio: localizes whether disagreement is in the + # biomass layer or the carbon-fraction layer. + (pl.col("agb") / pl.col("FIADB_DRYBIO_AG")).alias("biomass_ratio"), + # FIADB implied carbon fraction — should be in the S10a + # range [0.40, 0.55]. If it's flat 0.50, FIADB is pre-NSVB. + (pl.col("FIADB_CARBON_AG") / pl.col("FIADB_DRYBIO_AG")).alias( + "fiadb_implied_frac" + ), + ] + ) + + # Only compute rel-error stats on trees with valid pyfia output and + # positive FIADB CARBON_AG (a zero denominator is degenerate). + result = result.filter( + (pl.col("pyfia_CARBON_AG").is_not_null()) & (pl.col("FIADB_CARBON_AG") > 0) + ) + n_compared = result.height + + result = result.with_columns( + [ + ( + (pl.col("pyfia_CARBON_AG") - pl.col("FIADB_CARBON_AG")).abs() + / pl.col("FIADB_CARBON_AG").abs() + ).alias("rel_error"), + ] + ) + + rel = result["rel_error"] + n_within_0p1 = int((rel < 0.001).sum()) + n_within_1 = int((rel < 0.01).sum()) + n_within_5 = int((rel < 0.05).sum()) + + stats = { + "mean": float(rel.mean()), + "median": float(rel.median()), + "p95": float(rel.quantile(0.95)), + "p99": float(rel.quantile(0.99)), + "max": float(rel.max()), + } + br = result["biomass_ratio"] + biomass_stats = { + "median": float(br.median()), + "mean": float(br.mean()), + "p5": float(br.quantile(0.05)), + "p95": float(br.quantile(0.95)), + } + frac_stats = { + "median": float(result["fiadb_implied_frac"].median()), + "min": float(result["fiadb_implied_frac"].min()), + "max": float(result["fiadb_implied_frac"].max()), + } + + print( + f" trees compared (non-null pyfia, positive FIADB_CARBON_AG): " + f"{n_compared:,}" + ) + print() + print(" === per-tree carbon rel_error ===") + print(f" mean : {stats['mean']:.4%}") + print(f" median : {stats['median']:.4%}") + print(f" p95 : {stats['p95']:.4%}") + print(f" p99 : {stats['p99']:.4%}") + print(f" max : {stats['max']:.4%}") + print( + f" within 0.1% : {n_within_0p1:>10,} / {n_compared:,} " + f"({n_within_0p1 / n_compared:.2%})" + ) + print( + f" within 1% : {n_within_1:>10,} / {n_compared:,} " + f"({n_within_1 / n_compared:.2%})" + ) + print( + f" within 5% : {n_within_5:>10,} / {n_compared:,} " + f"({n_within_5 / n_compared:.2%})" + ) + print() + print(" === biomass ratio (pyfia_NSVB_AGB / FIADB_DRYBIO_AG) ===") + print(f" median : {biomass_stats['median']:.4f} (target: 1.000)") + print(f" mean : {biomass_stats['mean']:.4f}") + print(f" p5 : {biomass_stats['p5']:.4f}") + print(f" p95 : {biomass_stats['p95']:.4f}") + print() + print(" === FIADB implied carbon fraction (CARBON_AG / DRYBIO_AG) ===") + print( + f" median : {frac_stats['median']:.4f} " + f"(S10a range 0.40-0.55 → NSVB, flat 0.50 → pre-NSVB)" + ) + print(f" min : {frac_stats['min']:.4f}") + print(f" max : {frac_stats['max']:.4f}") + + # Top-10 worst offenders for diagnosis, filtering out any lingering + # nulls (shouldn't be any after the earlier filter, but belt-and- + # suspenders against NaN pass-through). + worst = ( + result.filter(pl.col("rel_error").is_not_null()) + .sort("rel_error", descending=True) + .head(10) + ) + if worst.height > 0: + print("\n Top 10 worst disagreements:") + print( + " SPCD DIA HT CULL pyfia_AG FIADB_AG rel_err" + ) + for row in worst.iter_rows(named=True): + cull_val = row["CULL"] if row["CULL"] is not None else 0.0 + print( + f" {row['SPCD']:>4} {row['DIA']:>5.1f} " + f"{row['HT']:>4.1f} {cull_val:>5.1f} " + f"{row['pyfia_CARBON_AG']:>12.2f} " + f"{row['FIADB_CARBON_AG']:>12.2f} " + f"{row['rel_error']:>8.2%}" + ) + + # === Ratchet assertions — locked at the Phase 1.5 baseline. === + # Each assertion failure should be interpreted as either a + # regression (if the violation is on the wrong side) or an + # opportunity to tighten the constant (if the new measurement + # beats the baseline). + assert null_frac < _MAX_NULL_FRAC, ( + f"SPCD coverage null rate {null_frac:.3%} exceeds " + f"{_MAX_NULL_FRAC:.1%} — new SPCDs falling out of the " + "coefficient tables?" + ) + assert stats["median"] < _BASELINE_MEDIAN_REL_ERR, ( + f"median rel error {stats['median']:.4%} exceeds baseline " + f"{_BASELINE_MEDIAN_REL_ERR:.2%} — PR 2 NSVB regression?" + ) + assert stats["p99"] < _BASELINE_P99_REL_ERR, ( + f"p99 rel error {stats['p99']:.4%} exceeds baseline " + f"{_BASELINE_P99_REL_ERR:.2%} — regression in the tail?" + ) + assert n_within_5 / n_compared > _BASELINE_WITHIN_5PCT_FRAC, ( + f"only {n_within_5 / n_compared:.2%} of trees agree within 5% " + f"(baseline: ≥{_BASELINE_WITHIN_5PCT_FRAC:.0%}) — regression?" + ) + assert n_within_1 / n_compared > _BASELINE_WITHIN_1PCT_FRAC, ( + f"only {n_within_1 / n_compared:.2%} of trees agree within 1% " + f"(baseline: ≥{_BASELINE_WITHIN_1PCT_FRAC:.0%}) — regression?" + ) + assert n_within_0p1 / n_compared > _BASELINE_WITHIN_0P1PCT_FRAC, ( + f"only {n_within_0p1 / n_compared:.2%} of trees agree within " + f"0.1% (baseline: ≥{_BASELINE_WITHIN_0P1PCT_FRAC:.0%}) — " + "regression?" + ) + assert ( + _BASELINE_BIOMASS_RATIO_MEDIAN_MIN + <= biomass_stats["median"] + <= _BASELINE_BIOMASS_RATIO_MEDIAN_MAX + ), ( + f"biomass ratio median {biomass_stats['median']:.4f} outside " + f"baseline window [{_BASELINE_BIOMASS_RATIO_MEDIAN_MIN:.3f}, " + f"{_BASELINE_BIOMASS_RATIO_MEDIAN_MAX:.3f}] — systematic shift?" + ) diff --git a/tests/validation/test_standing_dead_nsvb.py b/tests/validation/test_standing_dead_nsvb.py new file mode 100644 index 00000000..9d92c242 --- /dev/null +++ b/tests/validation/test_standing_dead_nsvb.py @@ -0,0 +1,429 @@ +"""NSVB standing dead carbon parity validation against FIADB TREE.CARBON_AG. + +Phase 2.5 validation gate for ``pyfia.carbon.standing_dead`` — the dead-tree +analogue of ``test_live_tree_nsvb.py``. The test answers the question the +in-repo unit/equivalence suite cannot: *how closely does the NSVB dead-tree +pipeline + broken-top corrections + decay reductions + S10b carbon fractions +agree with FIADB's pre-computed CARBON_AG values on real Georgia inventory +data for the official 2024 EXPVOL evaluation (EVALID 132401)?* + +**Validation scope** (locked from PR 3 Phase 1.6 lessons): + +The query joins ``POP_PLOT_STRATUM_ASSGN`` and filters to ``EVALID = +132401`` from the start. Without that filter, the query would pull in +pre-1989 periodic-inventory standing dead trees (1972/1982/1989 panels) +that have FIADB ``CARBON_AG`` / ``DRYBIO_AG`` computed via the legacy +Component Ratio Method (CRM, flat 0.5 carbon fraction; see Appendix K of +FIADB User Guide v9.1, K-1) rather than NSVB. See PR 3 commit ``4ae5bd0`` +for the full rationale. + +**Population filter** for standing dead trees: + +- ``STATUSCD = 2`` (dead) +- ``STANDING_DEAD_CD = '1'`` (excludes downed dead trees, which are part + of the down dead wood pool, and dead saplings, which FIADB tracks but + doesn't compute biomass for) +- ``DECAYCD IS NOT NULL`` (the join key for ``REF_TREE_DECAY_PROP``) +- ``DIA >= 1.0`` (NSVB Models 1-5 are not parameterized below 1.0") + +This filter recovers the ~6,870 EVALID 132401 standing dead Georgia trees +that have a FIADB ``CARBON_AG`` value to compare against. + +**Broken-top corrections (Phase 2.5):** + +~75% (5,132 / 6,870) of EVALID 132401 standing dead Georgia trees have +``ACTUALHT < HT`` (broken top). The pipeline applies two adjustments: + +- Branch biomass is multiplied by ``Broken_crn_prop`` — the fraction of + the intact crown remaining below the break, using the mean intact crown + ratio from Table S11 (``REF_TREE_STND_DEAD_CR_PROP``) keyed on Bailey + ecoregion province × hw/sw. +- Wood and bark are multiplied by ``(ACTUALHT/HT)^(2/3)`` — a paraboloid + taper approximation of the stem volume ratio below the break. This + replaces the Schumacher-Hall Model 6 volume-ratio computation that + FIADB uses. + +**Phase 2.5 measurement (with broken-top corrections):** + +- 6,870 trees compared (100% with DIVISION resolved from ECOSUBCD) +- median rel_err: **10.89%**, mean: 12.14%, p95: 27.5%, p99: 36.5% +- biomass ratio (pyfia/FIADB) median: **0.905** (~10% under-estimate) +- within 1%: 15.02%, within 10%: 47.74%, within 50%: 99.88% +- 0 null trees (full SPCD/DECAYCD coverage on the EVALID 132401 set) + +The remaining ~10% under-estimate (biomass ratio 0.905) comes from the +paraboloid taper approximation vs FIADB's Model 6. Top-10 worst offenders +are now small trees with peculiar HT/ACTUALHT configurations, not the +extreme broken-top snags that dominated the Phase 2 tail. + +**Requires** the ``PLOTGEOM`` table in the test database for the +DIVISION lookup. Same setup as ``test_live_tree_nsvb.py`` — see that +file's docstring for the one-off PLOTGEOM import script for older test +databases. ``PLOTGEOM`` ships in fresh ``pyfia.download()`` outputs as +of the Phase 1.5 ``COMMON_TABLES`` PR. +""" + +from __future__ import annotations + +import duckdb +import polars as pl +import pytest + +from pyfia.carbon.nsvb.carbon_fractions import ( + load_carbon_fractions_dead_df, + load_dead_cr_prop_df, + load_dead_decay_proportions_df, +) +from pyfia.carbon.nsvb.coefficients import ecosubcd_to_division_expr +from pyfia.carbon.nsvb.equations import compute_nsvb_dead_biomass + +# Ratchet thresholds — tightened for Phase 2.5 broken-top corrections. +# +# Phase 2.5 measurement (Georgia EVALID 132401, 6,870 trees, with +# Appendix K broken-top corrections — crown proportion + paraboloid +# volume ratio): +# median rel_err = 10.89%, mean = 12.14%, p95 = 27.51%, p99 = 36.49%, +# max = 90.59%, within 1% = 15.02%, within 10% = 47.74%, +# within 50% = 99.88%, biomass ratio median = 0.905, +# null fraction = 0.000% +# +# Improvement over Phase 2 baseline (intact-HT, no corrections): +# median 17.89% → 10.89% (-39%) +# p99 441.5% → 36.5% (-92%) +# within-10% 36.8% → 47.7% (+30%) +# within-50% 71.6% → 99.9% (+39%) +# biomass ratio 1.174 → 0.905 (moved from 17% over to 10% under) +# +# The remaining ~10% under-estimate (biomass ratio 0.905) is the +# paraboloid taper approximation (exponent 2/3) vs FIADB's Model 6 +# Schumacher-Hall volume-ratio model. Top-10 worst offenders are now +# small trees with peculiar HT/ACTUALHT configurations rather than the +# extreme broken-top snags that dominated the Phase 2 tail. +_BASELINE_MEDIAN_REL_ERR = 0.12 # Phase 2.5: 0.1089 +_BASELINE_P99_REL_ERR = 0.40 # Phase 2.5: 0.3649 +_BASELINE_WITHIN_50PCT_FRAC = 0.99 # Phase 2.5: 0.9988 (lower bound) +_BASELINE_WITHIN_10PCT_FRAC = 0.45 # Phase 2.5: 0.4774 (lower bound) +_BASELINE_WITHIN_1PCT_FRAC = 0.13 # Phase 2.5: 0.1502 (lower bound) +_BASELINE_BIOMASS_RATIO_MEDIAN_MIN = 0.85 # Phase 2.5: 0.9052 +_BASELINE_BIOMASS_RATIO_MEDIAN_MAX = 0.95 # Phase 2.5: 0.9052 + +# Trees with SPCD not covered by any NSVB coefficient path (species-level +# OR Jenkins fallback) get null agb. Allow a little headroom — the +# EVALID-filtered dead set is small (~6,870 trees) so a few nulls are OK. +_MAX_NULL_FRAC = 0.005 + + +class TestStandingDeadNSVBParity: + """Per-tree NSVB AG dead vs FIADB ``TREE.CARBON_AG`` on real Georgia data. + + The Phase 2 baseline locks the intact-HT approximation behavior. Each + subsequent commit that improves the NSVB dead pipeline's fidelity + (broken-top corrections, etc.) should tighten the thresholds below. + """ + + def test_per_tree_ag_agrees_with_fiadb(self, fia_db): + """Per-tree AG dead carbon parity between pyfia and FIADB. + + Runs the vectorized NSVB dead pipeline (intact-HT + decay + reductions + S10b fractions) on every eligible standing dead + Georgia tree and diffs against FIADB's pre-computed + ``TREE.CARBON_AG`` column. + + Reports the relative-error distribution plus the biomass ratio + (pyfia / FIADB) and the FIADB-implied dead carbon fraction as + separate diagnostic layers — if future validation reveals a + regression, the layered stats localize which layer moved. + """ + conn = duckdb.connect(fia_db, read_only=True) + try: + existing = set( + conn.execute("SELECT table_name FROM information_schema.tables") + .pl()["table_name"] + .to_list() + ) + if "PLOTGEOM" not in existing: + pytest.skip( + "PLOTGEOM table missing from the test database. The " + "DIVISION lookup requires ECOSUBCD. As of the Phase 1.5 " + "PR, PLOTGEOM is in pyfia.downloader.COMMON_TABLES and " + "new pyfia.download() calls include it automatically. " + "For older test databases, see the live-tree validation " + "test's module docstring for the one-off PLOTGEOM " + "import script." + ) + + # Phase 2 SD-specific filter: + # STATUSCD = 2 (dead) + # STANDING_DEAD_CD='1' (excludes downed dead + dead saplings) + # DECAYCD IS NOT NULL (the REF_TREE_DECAY_PROP join key) + # DIA >= 1.0 (NSVB Models 1-5 floor) + # CARBON_AG > 0 (FIADB has a value to compare against) + # Plus the same EVALID 132401 scope filter the live-tree test + # uses (see PR 3 Phase 1.6 commit ``4ae5bd0`` for why). + trees = conn.execute(""" + SELECT DISTINCT + t.CN, + t.SPCD, + t.DIA, + t.HT, + t.ACTUALHT, + t.DECAYCD, + t.STANDING_DEAD_CD, + t.CARBON_AG AS FIADB_CARBON_AG, + t.DRYBIO_AG AS FIADB_DRYBIO_AG, + rs.JENKINS_SPGRPCD, + rs.WOOD_SPGR_GREENVOL_DRYWT AS WDSG, + pg.ECOSUBCD + FROM TREE t + JOIN POP_PLOT_STRATUM_ASSGN ppsa + ON t.PLT_CN = ppsa.PLT_CN + LEFT JOIN REF_SPECIES rs ON t.SPCD = rs.SPCD + LEFT JOIN PLOTGEOM pg ON t.PLT_CN = pg.CN + WHERE ppsa.EVALID = 132401 + AND t.STATUSCD = 2 + AND t.STANDING_DEAD_CD = '1' + AND t.DECAYCD IS NOT NULL + AND t.DIA IS NOT NULL AND t.DIA >= 1.0 + AND t.HT IS NOT NULL + AND t.SPCD IS NOT NULL + AND t.CARBON_AG IS NOT NULL AND t.CARBON_AG > 0 + AND rs.JENKINS_SPGRPCD IS NOT NULL + AND rs.WOOD_SPGR_GREENVOL_DRYWT IS NOT NULL + """).pl() + finally: + conn.close() + + n_total = trees.height + assert n_total > 0, f"no eligible standing dead trees found in {fia_db}" + + # Derive Bailey DIVISION from ECOSUBCD (Level 2 NSVB lookup). + trees = trees.with_columns( + ecosubcd_to_division_expr("ECOSUBCD").alias("DIVISION") + ) + n_with_division = int(trees["DIVISION"].is_not_null().sum()) + + # Normalize dtypes. DECAYCD comes off DuckDB as Utf8 because the + # source CSV column has nulls; cast to Int64 for the join. ACTUALHT + # is cast to Float64 for the broken-top correction pipeline. + trees = trees.with_columns( + [ + pl.col("SPCD").cast(pl.Int64), + pl.col("JENKINS_SPGRPCD").cast(pl.Int64), + pl.col("DIA").cast(pl.Float64), + pl.col("HT").cast(pl.Float64), + pl.col("ACTUALHT").cast(pl.Float64, strict=False), + pl.col("DECAYCD").cast(pl.Int64, strict=False), + pl.col("WDSG").cast(pl.Float64), + pl.col("FIADB_CARBON_AG").cast(pl.Float64), + pl.col("FIADB_DRYBIO_AG").cast(pl.Float64), + ] + ) + + # Diagnostic: how many of the SDs are broken-top vs intact? + broken_top_count = int( + trees.filter( + pl.col("ACTUALHT").cast(pl.Float64, strict=False) < pl.col("HT") + ).height + ) + + # Run the vectorized NSVB dead biomass pipeline with broken-top + # corrections (Phase 2.5). + decay_props = load_dead_decay_proportions_df() + cr_prop_table = load_dead_cr_prop_df() + result = compute_nsvb_dead_biomass( + trees.lazy(), decay_props, cr_prop_table=cr_prop_table + ).collect() + + # Count trees with null agb (SPCD coverage gaps OR DECAYCD outside 1-5). + n_null = int(result["agb"].null_count()) + null_frac = n_null / n_total + print( + f"\n=== Standing dead NSVB parity vs FIADB " + f"(Georgia EVALID 132401, {n_total:,} trees) ===" + ) + print( + f" trees with DIVISION resolved from ECOSUBCD: " + f"{n_with_division:,} ({n_with_division / n_total:.2%})" + ) + print( + f" trees with broken top (ACTUALHT < HT): " + f"{broken_top_count:,} ({broken_top_count / n_total:.2%})" + ) + print( + f" trees with null NSVB agb (SPCD/DECAYCD coverage gap): " + f"{n_null:,} ({null_frac:.3%})" + ) + + result = result.filter(pl.col("agb").is_not_null()) + + # Apply S10b dead carbon fractions joined on (hw_sw, DECAYCD). + # The hw_sw expression mirrors the SPCD<300 rule used inside + # compute_nsvb_dead_biomass for consistency. + cf_df = load_carbon_fractions_dead_df() + result = result.with_columns( + [ + pl.when(pl.col("SPCD") >= 300) + .then(pl.lit("hardwood")) + .otherwise(pl.lit("softwood")) + .alias("_hw_sw_cf"), + ] + ) + result = result.join( + cf_df.rename({"hw_sw": "_hw_sw_cf"}), + on=["_hw_sw_cf", "DECAYCD"], + how="left", + ) + result = result.with_columns( + [ + (pl.col("agb") * pl.col("CARBON_FRAC_DEAD")).alias("pyfia_CARBON_AG"), + # Biomass ratio: localizes whether disagreement is in the + # biomass layer or the carbon-fraction layer. + (pl.col("agb") / pl.col("FIADB_DRYBIO_AG")).alias("biomass_ratio"), + # FIADB implied dead carbon fraction — should be in the S10b + # range (0.47 hardwood / 0.50-0.53 softwood). If it's flat 0.50, + # FIADB is using legacy CRM rather than NSVB S10b. + (pl.col("FIADB_CARBON_AG") / pl.col("FIADB_DRYBIO_AG")).alias( + "fiadb_implied_frac" + ), + ] + ) + + # Only compute rel-error stats on trees with valid pyfia output and + # positive FIADB CARBON_AG (a zero denominator is degenerate). + result = result.filter( + (pl.col("pyfia_CARBON_AG").is_not_null()) & (pl.col("FIADB_CARBON_AG") > 0) + ) + n_compared = result.height + + result = result.with_columns( + [ + ( + (pl.col("pyfia_CARBON_AG") - pl.col("FIADB_CARBON_AG")).abs() + / pl.col("FIADB_CARBON_AG").abs() + ).alias("rel_error"), + ] + ) + + rel = result["rel_error"] + n_within_1 = int((rel < 0.01).sum()) + n_within_10 = int((rel < 0.10).sum()) + n_within_50 = int((rel < 0.50).sum()) + + stats = { + "mean": float(rel.mean()), + "median": float(rel.median()), + "p95": float(rel.quantile(0.95)), + "p99": float(rel.quantile(0.99)), + "max": float(rel.max()), + } + br = result["biomass_ratio"] + biomass_stats = { + "median": float(br.median()), + "mean": float(br.mean()), + "p5": float(br.quantile(0.05)), + "p95": float(br.quantile(0.95)), + } + frac_stats = { + "median": float(result["fiadb_implied_frac"].median()), + "min": float(result["fiadb_implied_frac"].min()), + "max": float(result["fiadb_implied_frac"].max()), + } + + print( + f" trees compared (non-null pyfia, positive FIADB_CARBON_AG): " + f"{n_compared:,}" + ) + print() + print(" === per-tree carbon rel_error ===") + print(f" mean : {stats['mean']:.4%}") + print(f" median : {stats['median']:.4%}") + print(f" p95 : {stats['p95']:.4%}") + print(f" p99 : {stats['p99']:.4%}") + print(f" max : {stats['max']:.4%}") + print( + f" within 1% : {n_within_1:>6,} / {n_compared:,} " + f"({n_within_1 / n_compared:.2%})" + ) + print( + f" within 10% : {n_within_10:>6,} / {n_compared:,} " + f"({n_within_10 / n_compared:.2%})" + ) + print( + f" within 50% : {n_within_50:>6,} / {n_compared:,} " + f"({n_within_50 / n_compared:.2%})" + ) + print() + print(" === biomass ratio (pyfia_NSVB_dead_AGB / FIADB_DRYBIO_AG) ===") + print(f" median : {biomass_stats['median']:.4f} (target: 1.000)") + print(f" mean : {biomass_stats['mean']:.4f}") + print(f" p5 : {biomass_stats['p5']:.4f}") + print(f" p95 : {biomass_stats['p95']:.4f}") + print() + print(" === FIADB implied dead carbon fraction (CARBON_AG / DRYBIO_AG) ===") + print( + f" median : {frac_stats['median']:.4f} " + f"(S10b range 0.47-0.53 → NSVB, flat 0.50 → pre-NSVB CRM)" + ) + print(f" min : {frac_stats['min']:.4f}") + print(f" max : {frac_stats['max']:.4f}") + + # Top-10 worst offenders for diagnosis. The intact-HT approximation + # should make broken-top trees the worst offenders — they should + # cluster at the high end with pyfia / FIADB ratios well above 1. + worst = ( + result.filter(pl.col("rel_error").is_not_null()) + .sort("rel_error", descending=True) + .head(10) + ) + if worst.height > 0: + print("\n Top 10 worst disagreements:") + print(" SPCD DIA HT ACTHT DC pyfia_AG FIADB_AG rel_err") + for row in worst.iter_rows(named=True): + actht_str = row["ACTUALHT"] if row["ACTUALHT"] else "-" + print( + f" {row['SPCD']:>4} {row['DIA']:>5.1f} " + f"{row['HT']:>4.0f} {actht_str:>5} " + f"{row['DECAYCD']:>2} " + f"{row['pyfia_CARBON_AG']:>10.2f} " + f"{row['FIADB_CARBON_AG']:>10.2f} " + f"{row['rel_error']:>8.2%}" + ) + + # === Ratchet assertions — locked at the Phase 2 baseline === + # See the comment block at the top of this file for the full + # rationale on why these are loose. Future commits that improve + # broken-top handling should tighten them. + assert null_frac < _MAX_NULL_FRAC, ( + f"SPCD/DECAYCD coverage null rate {null_frac:.3%} exceeds " + f"{_MAX_NULL_FRAC:.1%} — new SPCDs falling out of the " + "coefficient tables, or new DECAYCD outside the 1-5 range?" + ) + assert stats["median"] < _BASELINE_MEDIAN_REL_ERR, ( + f"median rel error {stats['median']:.4%} exceeds baseline " + f"{_BASELINE_MEDIAN_REL_ERR:.2%} — Phase 2 NSVB dead regression?" + ) + assert stats["p99"] < _BASELINE_P99_REL_ERR, ( + f"p99 rel error {stats['p99']:.4%} exceeds baseline " + f"{_BASELINE_P99_REL_ERR:.2%} — regression in the tail?" + ) + assert n_within_50 / n_compared > _BASELINE_WITHIN_50PCT_FRAC, ( + f"only {n_within_50 / n_compared:.2%} of trees agree within 50% " + f"(baseline: ≥{_BASELINE_WITHIN_50PCT_FRAC:.0%}) — regression?" + ) + assert n_within_10 / n_compared > _BASELINE_WITHIN_10PCT_FRAC, ( + f"only {n_within_10 / n_compared:.2%} of trees agree within 10% " + f"(baseline: ≥{_BASELINE_WITHIN_10PCT_FRAC:.0%}) — regression?" + ) + assert n_within_1 / n_compared > _BASELINE_WITHIN_1PCT_FRAC, ( + f"only {n_within_1 / n_compared:.2%} of trees agree within 1% " + f"(baseline: ≥{_BASELINE_WITHIN_1PCT_FRAC:.0%}) — regression?" + ) + assert ( + _BASELINE_BIOMASS_RATIO_MEDIAN_MIN + <= biomass_stats["median"] + <= _BASELINE_BIOMASS_RATIO_MEDIAN_MAX + ), ( + f"biomass ratio median {biomass_stats['median']:.4f} outside " + f"baseline window [{_BASELINE_BIOMASS_RATIO_MEDIAN_MIN:.3f}, " + f"{_BASELINE_BIOMASS_RATIO_MEDIAN_MAX:.3f}] — systematic shift?" + ) diff --git a/uv.lock b/uv.lock index 0bfd5802..998a0d78 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 1 +revision = 3 requires-python = ">=3.11" resolution-markers = [ "python_full_version >= '3.12'", @@ -10,123 +10,123 @@ resolution-markers = [ name = "annotated-types" version = "0.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, ] [[package]] name = "babel" version = "2.17.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852 } +sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537 }, + { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" }, ] [[package]] name = "backrefs" version = "6.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/86/e3/bb3a439d5cb255c4774724810ad8073830fac9c9dee123555820c1bcc806/backrefs-6.1.tar.gz", hash = "sha256:3bba1749aafe1db9b915f00e0dd166cba613b6f788ffd63060ac3485dc9be231", size = 7011962 } +sdist = { url = "https://files.pythonhosted.org/packages/86/e3/bb3a439d5cb255c4774724810ad8073830fac9c9dee123555820c1bcc806/backrefs-6.1.tar.gz", hash = "sha256:3bba1749aafe1db9b915f00e0dd166cba613b6f788ffd63060ac3485dc9be231", size = 7011962, upload-time = "2025-11-15T14:52:08.323Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/ee/c216d52f58ea75b5e1841022bbae24438b19834a29b163cb32aa3a2a7c6e/backrefs-6.1-py310-none-any.whl", hash = "sha256:2a2ccb96302337ce61ee4717ceacfbf26ba4efb1d55af86564b8bbaeda39cac1", size = 381059 }, - { url = "https://files.pythonhosted.org/packages/e6/9a/8da246d988ded941da96c7ed945d63e94a445637eaad985a0ed88787cb89/backrefs-6.1-py311-none-any.whl", hash = "sha256:e82bba3875ee4430f4de4b6db19429a27275d95a5f3773c57e9e18abc23fd2b7", size = 392854 }, - { url = "https://files.pythonhosted.org/packages/37/c9/fd117a6f9300c62bbc33bc337fd2b3c6bfe28b6e9701de336b52d7a797ad/backrefs-6.1-py312-none-any.whl", hash = "sha256:c64698c8d2269343d88947c0735cb4b78745bd3ba590e10313fbf3f78c34da5a", size = 398770 }, - { url = "https://files.pythonhosted.org/packages/eb/95/7118e935b0b0bd3f94dfec2d852fd4e4f4f9757bdb49850519acd245cd3a/backrefs-6.1-py313-none-any.whl", hash = "sha256:4c9d3dc1e2e558965202c012304f33d4e0e477e1c103663fd2c3cc9bb18b0d05", size = 400726 }, - { url = "https://files.pythonhosted.org/packages/1d/72/6296bad135bfafd3254ae3648cd152980a424bd6fed64a101af00cc7ba31/backrefs-6.1-py314-none-any.whl", hash = "sha256:13eafbc9ccd5222e9c1f0bec563e6d2a6d21514962f11e7fc79872fd56cbc853", size = 412584 }, - { url = "https://files.pythonhosted.org/packages/02/e3/a4fa1946722c4c7b063cc25043a12d9ce9b4323777f89643be74cef2993c/backrefs-6.1-py39-none-any.whl", hash = "sha256:a9e99b8a4867852cad177a6430e31b0f6e495d65f8c6c134b68c14c3c95bf4b0", size = 381058 }, + { url = "https://files.pythonhosted.org/packages/3b/ee/c216d52f58ea75b5e1841022bbae24438b19834a29b163cb32aa3a2a7c6e/backrefs-6.1-py310-none-any.whl", hash = "sha256:2a2ccb96302337ce61ee4717ceacfbf26ba4efb1d55af86564b8bbaeda39cac1", size = 381059, upload-time = "2025-11-15T14:51:59.758Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9a/8da246d988ded941da96c7ed945d63e94a445637eaad985a0ed88787cb89/backrefs-6.1-py311-none-any.whl", hash = "sha256:e82bba3875ee4430f4de4b6db19429a27275d95a5f3773c57e9e18abc23fd2b7", size = 392854, upload-time = "2025-11-15T14:52:01.194Z" }, + { url = "https://files.pythonhosted.org/packages/37/c9/fd117a6f9300c62bbc33bc337fd2b3c6bfe28b6e9701de336b52d7a797ad/backrefs-6.1-py312-none-any.whl", hash = "sha256:c64698c8d2269343d88947c0735cb4b78745bd3ba590e10313fbf3f78c34da5a", size = 398770, upload-time = "2025-11-15T14:52:02.584Z" }, + { url = "https://files.pythonhosted.org/packages/eb/95/7118e935b0b0bd3f94dfec2d852fd4e4f4f9757bdb49850519acd245cd3a/backrefs-6.1-py313-none-any.whl", hash = "sha256:4c9d3dc1e2e558965202c012304f33d4e0e477e1c103663fd2c3cc9bb18b0d05", size = 400726, upload-time = "2025-11-15T14:52:04.093Z" }, + { url = "https://files.pythonhosted.org/packages/1d/72/6296bad135bfafd3254ae3648cd152980a424bd6fed64a101af00cc7ba31/backrefs-6.1-py314-none-any.whl", hash = "sha256:13eafbc9ccd5222e9c1f0bec563e6d2a6d21514962f11e7fc79872fd56cbc853", size = 412584, upload-time = "2025-11-15T14:52:05.233Z" }, + { url = "https://files.pythonhosted.org/packages/02/e3/a4fa1946722c4c7b063cc25043a12d9ce9b4323777f89643be74cef2993c/backrefs-6.1-py39-none-any.whl", hash = "sha256:a9e99b8a4867852cad177a6430e31b0f6e495d65f8c6c134b68c14c3c95bf4b0", size = 381058, upload-time = "2025-11-15T14:52:06.698Z" }, ] [[package]] name = "certifi" version = "2025.11.12" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538 } +sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438 }, + { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" }, ] [[package]] name = "cfgv" version = "3.5.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334 } +sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445 }, + { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, ] [[package]] name = "charset-normalizer" version = "3.4.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988 }, - { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324 }, - { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742 }, - { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863 }, - { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837 }, - { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550 }, - { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162 }, - { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019 }, - { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310 }, - { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022 }, - { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383 }, - { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098 }, - { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991 }, - { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456 }, - { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978 }, - { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969 }, - { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425 }, - { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162 }, - { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558 }, - { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497 }, - { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240 }, - { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471 }, - { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864 }, - { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647 }, - { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110 }, - { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839 }, - { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667 }, - { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535 }, - { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816 }, - { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694 }, - { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131 }, - { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390 }, - { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091 }, - { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936 }, - { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180 }, - { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346 }, - { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874 }, - { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076 }, - { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601 }, - { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376 }, - { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825 }, - { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583 }, - { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366 }, - { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300 }, - { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465 }, - { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404 }, - { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092 }, - { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408 }, - { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746 }, - { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889 }, - { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641 }, - { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779 }, - { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035 }, - { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542 }, - { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524 }, - { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395 }, - { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680 }, - { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045 }, - { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687 }, - { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014 }, - { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044 }, - { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940 }, - { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104 }, - { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743 }, - { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402 }, +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, + { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, + { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, + { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, + { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, + { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, + { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, + { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, + { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, + { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, ] [[package]] @@ -136,18 +136,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065 } +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274 }, + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, ] [[package]] name = "colorama" version = "0.4.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] [[package]] @@ -155,108 +155,108 @@ name = "connectorx" version = "0.4.4" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/96/be/648fdee47cacbc5d9073429adbc41395c10a9070d74898fd1638e6fe379f/connectorx-0.4.4-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:7a2cb89637a3ff2f361c6ecc47f8f8280f86bc079fe7b4e60948e37e8acb6384", size = 37965363 }, - { url = "https://files.pythonhosted.org/packages/6b/f3/da48b9a86ab173bcacbe62fb8dd2f744fb57c8dc3a129a9c478a9a802383/connectorx-0.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:40ad3e3130474e19c885adb26d57c056e86179fcc567210e9c1be7593c1355ab", size = 36170915 }, - { url = "https://files.pythonhosted.org/packages/55/a5/43447d5a1e8c4d3233a30132b874d7dda05b9a21a87ea0e625b6b268268e/connectorx-0.4.4-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:7321f83f1aa180922c142df8106e4cf6f44123bc9d7be900b126d9e2dc3566cc", size = 43713561 }, - { url = "https://files.pythonhosted.org/packages/26/b6/cb39941b97e2a26e54316f4169d5d0b3aa5dd1077a85bc855d366c68d7d8/connectorx-0.4.4-cp311-cp311-manylinux_2_35_aarch64.whl", hash = "sha256:c7265ab1f9e12f767e9b5523f497147ff82f7d079ace692069d08b98d06e3843", size = 40936017 }, - { url = "https://files.pythonhosted.org/packages/fb/bb/56a5e5eed72f5a5d929036505f36f0ed4dc9af1faf63cf9165a09e5f29d8/connectorx-0.4.4-cp311-none-win_amd64.whl", hash = "sha256:ca59b6d594e652700840564a05b6f0cb626c3dbaba928bc84e1c2ebafd46c077", size = 34558954 }, - { url = "https://files.pythonhosted.org/packages/52/76/939ccafc8b5822637974764ae261f39519e1d3118acbd6f9be9ebb8cdaf0/connectorx-0.4.4-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:11aab42c72bd4e8913db815b9c93f5e2ee27646ad3243bae0d0f3866759e75d4", size = 37967316 }, - { url = "https://files.pythonhosted.org/packages/ee/9e/368cf802ec46852fbf59e02fb842edd50fb19f7e52bfc4462e1e00b062f5/connectorx-0.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:848fd58e696e7291280e1b58e15a6a036f07b53cf8dd014d04732255d7d6985e", size = 36171795 }, - { url = "https://files.pythonhosted.org/packages/46/25/83392512c7dafd6b940a18ce0349784c9df0a339e4277be77e1babcdf33a/connectorx-0.4.4-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:9f458f521dd143d37ddc2d6614f8014191b736b13cb5824d3aef8a0f487ea8fa", size = 43712778 }, - { url = "https://files.pythonhosted.org/packages/0a/85/1dbbada0f38d9a48be4be7b52c27aaf49673ca4818c1519a18647b170af9/connectorx-0.4.4-cp312-cp312-manylinux_2_35_aarch64.whl", hash = "sha256:c30ccbaf5b2549c47fda96092622568a47b73e9db31dd3d385a472f2b6d07629", size = 40945247 }, - { url = "https://files.pythonhosted.org/packages/08/d0/2af09c4077e0d357f33384e4d6fc2c34a3d33e473ae7f939a6c58769774d/connectorx-0.4.4-cp312-none-win_amd64.whl", hash = "sha256:dcf4fb9d1e94ebe0bb4b72a18aeba119895d2fa66b4fe69a8ece97942748c3b0", size = 34561589 }, - { url = "https://files.pythonhosted.org/packages/24/d5/406fe74dba7e608d2edde3a345c407b1362c59f7044f077b3195e37c2658/connectorx-0.4.4-cp313-cp313-macosx_10_7_x86_64.whl", hash = "sha256:3bf1498e662a5461ba27f8a90d04ff67fddc9bf9d8d9d9687db037a89202fcf7", size = 37965958 }, - { url = "https://files.pythonhosted.org/packages/bf/c9/db8c9d153eb0a8472997f2e746cd9b1e0da39b6eaac734f40de098333e0a/connectorx-0.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cf4c94bc74db19262b6e8fa72f4df92c5ad7249212863400be66300564f89f09", size = 36168586 }, - { url = "https://files.pythonhosted.org/packages/52/e4/0e066245624967ba403b83887d362e730a00981cdfe38a89b49fc305efd2/connectorx-0.4.4-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:66a07c5215aed34a101137e7cc8c4509939974cad60faebb19972d73c1960d82", size = 43710796 }, - { url = "https://files.pythonhosted.org/packages/c3/0f/36ae620a0ab457eb90d7a4ffd2cf5a7b42bc234f3bd47b167602603bed23/connectorx-0.4.4-cp313-cp313-manylinux_2_35_aarch64.whl", hash = "sha256:f1b4fe628623db5dd99713e9195c8cd9cc63a6d2aff206a47ff87d719806836e", size = 40922979 }, - { url = "https://files.pythonhosted.org/packages/9e/19/f4751510ad196e2894205d68d029c1699e67b4026ed655aee5487528cb5a/connectorx-0.4.4-cp313-none-win_amd64.whl", hash = "sha256:ef6ce2e95d04163862c1a2e5a673ae6371d74a3ca66a4e3fec4a7c281944b102", size = 34557019 }, + { url = "https://files.pythonhosted.org/packages/96/be/648fdee47cacbc5d9073429adbc41395c10a9070d74898fd1638e6fe379f/connectorx-0.4.4-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:7a2cb89637a3ff2f361c6ecc47f8f8280f86bc079fe7b4e60948e37e8acb6384", size = 37965363, upload-time = "2025-08-19T05:37:18.428Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f3/da48b9a86ab173bcacbe62fb8dd2f744fb57c8dc3a129a9c478a9a802383/connectorx-0.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:40ad3e3130474e19c885adb26d57c056e86179fcc567210e9c1be7593c1355ab", size = 36170915, upload-time = "2025-08-19T05:37:35.711Z" }, + { url = "https://files.pythonhosted.org/packages/55/a5/43447d5a1e8c4d3233a30132b874d7dda05b9a21a87ea0e625b6b268268e/connectorx-0.4.4-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:7321f83f1aa180922c142df8106e4cf6f44123bc9d7be900b126d9e2dc3566cc", size = 43713561, upload-time = "2025-08-19T05:37:53.291Z" }, + { url = "https://files.pythonhosted.org/packages/26/b6/cb39941b97e2a26e54316f4169d5d0b3aa5dd1077a85bc855d366c68d7d8/connectorx-0.4.4-cp311-cp311-manylinux_2_35_aarch64.whl", hash = "sha256:c7265ab1f9e12f767e9b5523f497147ff82f7d079ace692069d08b98d06e3843", size = 40936017, upload-time = "2025-08-19T05:37:01.76Z" }, + { url = "https://files.pythonhosted.org/packages/fb/bb/56a5e5eed72f5a5d929036505f36f0ed4dc9af1faf63cf9165a09e5f29d8/connectorx-0.4.4-cp311-none-win_amd64.whl", hash = "sha256:ca59b6d594e652700840564a05b6f0cb626c3dbaba928bc84e1c2ebafd46c077", size = 34558954, upload-time = "2025-08-19T05:38:10.805Z" }, + { url = "https://files.pythonhosted.org/packages/52/76/939ccafc8b5822637974764ae261f39519e1d3118acbd6f9be9ebb8cdaf0/connectorx-0.4.4-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:11aab42c72bd4e8913db815b9c93f5e2ee27646ad3243bae0d0f3866759e75d4", size = 37967316, upload-time = "2025-08-19T05:37:23.108Z" }, + { url = "https://files.pythonhosted.org/packages/ee/9e/368cf802ec46852fbf59e02fb842edd50fb19f7e52bfc4462e1e00b062f5/connectorx-0.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:848fd58e696e7291280e1b58e15a6a036f07b53cf8dd014d04732255d7d6985e", size = 36171795, upload-time = "2025-08-19T05:37:40.094Z" }, + { url = "https://files.pythonhosted.org/packages/46/25/83392512c7dafd6b940a18ce0349784c9df0a339e4277be77e1babcdf33a/connectorx-0.4.4-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:9f458f521dd143d37ddc2d6614f8014191b736b13cb5824d3aef8a0f487ea8fa", size = 43712778, upload-time = "2025-08-19T05:37:58.245Z" }, + { url = "https://files.pythonhosted.org/packages/0a/85/1dbbada0f38d9a48be4be7b52c27aaf49673ca4818c1519a18647b170af9/connectorx-0.4.4-cp312-cp312-manylinux_2_35_aarch64.whl", hash = "sha256:c30ccbaf5b2549c47fda96092622568a47b73e9db31dd3d385a472f2b6d07629", size = 40945247, upload-time = "2025-08-19T05:37:06.042Z" }, + { url = "https://files.pythonhosted.org/packages/08/d0/2af09c4077e0d357f33384e4d6fc2c34a3d33e473ae7f939a6c58769774d/connectorx-0.4.4-cp312-none-win_amd64.whl", hash = "sha256:dcf4fb9d1e94ebe0bb4b72a18aeba119895d2fa66b4fe69a8ece97942748c3b0", size = 34561589, upload-time = "2025-08-19T05:38:14.81Z" }, + { url = "https://files.pythonhosted.org/packages/24/d5/406fe74dba7e608d2edde3a345c407b1362c59f7044f077b3195e37c2658/connectorx-0.4.4-cp313-cp313-macosx_10_7_x86_64.whl", hash = "sha256:3bf1498e662a5461ba27f8a90d04ff67fddc9bf9d8d9d9687db037a89202fcf7", size = 37965958, upload-time = "2025-08-19T05:37:27.414Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c9/db8c9d153eb0a8472997f2e746cd9b1e0da39b6eaac734f40de098333e0a/connectorx-0.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cf4c94bc74db19262b6e8fa72f4df92c5ad7249212863400be66300564f89f09", size = 36168586, upload-time = "2025-08-19T05:37:44.241Z" }, + { url = "https://files.pythonhosted.org/packages/52/e4/0e066245624967ba403b83887d362e730a00981cdfe38a89b49fc305efd2/connectorx-0.4.4-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:66a07c5215aed34a101137e7cc8c4509939974cad60faebb19972d73c1960d82", size = 43710796, upload-time = "2025-08-19T05:38:02.845Z" }, + { url = "https://files.pythonhosted.org/packages/c3/0f/36ae620a0ab457eb90d7a4ffd2cf5a7b42bc234f3bd47b167602603bed23/connectorx-0.4.4-cp313-cp313-manylinux_2_35_aarch64.whl", hash = "sha256:f1b4fe628623db5dd99713e9195c8cd9cc63a6d2aff206a47ff87d719806836e", size = 40922979, upload-time = "2025-08-19T05:37:10.312Z" }, + { url = "https://files.pythonhosted.org/packages/9e/19/f4751510ad196e2894205d68d029c1699e67b4026ed655aee5487528cb5a/connectorx-0.4.4-cp313-none-win_amd64.whl", hash = "sha256:ef6ce2e95d04163862c1a2e5a673ae6371d74a3ca66a4e3fec4a7c281944b102", size = 34557019, upload-time = "2025-08-19T05:38:18.784Z" }, ] [[package]] name = "coverage" version = "7.12.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/89/26/4a96807b193b011588099c3b5c89fbb05294e5b90e71018e065465f34eb6/coverage-7.12.0.tar.gz", hash = "sha256:fc11e0a4e372cb5f282f16ef90d4a585034050ccda536451901abfb19a57f40c", size = 819341 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/0c/0dfe7f0487477d96432e4815537263363fb6dd7289743a796e8e51eabdf2/coverage-7.12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aa124a3683d2af98bd9d9c2bfa7a5076ca7e5ab09fdb96b81fa7d89376ae928f", size = 217535 }, - { url = "https://files.pythonhosted.org/packages/9b/f5/f9a4a053a5bbff023d3bec259faac8f11a1e5a6479c2ccf586f910d8dac7/coverage-7.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d93fbf446c31c0140208dcd07c5d882029832e8ed7891a39d6d44bd65f2316c3", size = 218044 }, - { url = "https://files.pythonhosted.org/packages/95/c5/84fc3697c1fa10cd8571919bf9693f693b7373278daaf3b73e328d502bc8/coverage-7.12.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:52ca620260bd8cd6027317bdd8b8ba929be1d741764ee765b42c4d79a408601e", size = 248440 }, - { url = "https://files.pythonhosted.org/packages/f4/36/2d93fbf6a04670f3874aed397d5a5371948a076e3249244a9e84fb0e02d6/coverage-7.12.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f3433ffd541380f3a0e423cff0f4926d55b0cc8c1d160fdc3be24a4c03aa65f7", size = 250361 }, - { url = "https://files.pythonhosted.org/packages/5d/49/66dc65cc456a6bfc41ea3d0758c4afeaa4068a2b2931bf83be6894cf1058/coverage-7.12.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7bbb321d4adc9f65e402c677cd1c8e4c2d0105d3ce285b51b4d87f1d5db5245", size = 252472 }, - { url = "https://files.pythonhosted.org/packages/35/1f/ebb8a18dffd406db9fcd4b3ae42254aedcaf612470e8712f12041325930f/coverage-7.12.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22a7aade354a72dff3b59c577bfd18d6945c61f97393bc5fb7bd293a4237024b", size = 248592 }, - { url = "https://files.pythonhosted.org/packages/da/a8/67f213c06e5ea3b3d4980df7dc344d7fea88240b5fe878a5dcbdfe0e2315/coverage-7.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3ff651dcd36d2fea66877cd4a82de478004c59b849945446acb5baf9379a1b64", size = 250167 }, - { url = "https://files.pythonhosted.org/packages/f0/00/e52aef68154164ea40cc8389c120c314c747fe63a04b013a5782e989b77f/coverage-7.12.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:31b8b2e38391a56e3cea39d22a23faaa7c3fc911751756ef6d2621d2a9daf742", size = 248238 }, - { url = "https://files.pythonhosted.org/packages/1f/a4/4d88750bcf9d6d66f77865e5a05a20e14db44074c25fd22519777cb69025/coverage-7.12.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:297bc2da28440f5ae51c845a47c8175a4db0553a53827886e4fb25c66633000c", size = 247964 }, - { url = "https://files.pythonhosted.org/packages/a7/6b/b74693158899d5b47b0bf6238d2c6722e20ba749f86b74454fac0696bb00/coverage-7.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6ff7651cc01a246908eac162a6a86fc0dbab6de1ad165dfb9a1e2ec660b44984", size = 248862 }, - { url = "https://files.pythonhosted.org/packages/18/de/6af6730227ce0e8ade307b1cc4a08e7f51b419a78d02083a86c04ccceb29/coverage-7.12.0-cp311-cp311-win32.whl", hash = "sha256:313672140638b6ddb2c6455ddeda41c6a0b208298034544cfca138978c6baed6", size = 220033 }, - { url = "https://files.pythonhosted.org/packages/e2/a1/e7f63021a7c4fe20994359fcdeae43cbef4a4d0ca36a5a1639feeea5d9e1/coverage-7.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:a1783ed5bd0d5938d4435014626568dc7f93e3cb99bc59188cc18857c47aa3c4", size = 220966 }, - { url = "https://files.pythonhosted.org/packages/77/e8/deae26453f37c20c3aa0c4433a1e32cdc169bf415cce223a693117aa3ddd/coverage-7.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:4648158fd8dd9381b5847622df1c90ff314efbfc1df4550092ab6013c238a5fc", size = 219637 }, - { url = "https://files.pythonhosted.org/packages/02/bf/638c0427c0f0d47638242e2438127f3c8ee3cfc06c7fdeb16778ed47f836/coverage-7.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:29644c928772c78512b48e14156b81255000dcfd4817574ff69def189bcb3647", size = 217704 }, - { url = "https://files.pythonhosted.org/packages/08/e1/706fae6692a66c2d6b871a608bbde0da6281903fa0e9f53a39ed441da36a/coverage-7.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8638cbb002eaa5d7c8d04da667813ce1067080b9a91099801a0053086e52b736", size = 218064 }, - { url = "https://files.pythonhosted.org/packages/a9/8b/eb0231d0540f8af3ffda39720ff43cb91926489d01524e68f60e961366e4/coverage-7.12.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:083631eeff5eb9992c923e14b810a179798bb598e6a0dd60586819fc23be6e60", size = 249560 }, - { url = "https://files.pythonhosted.org/packages/e9/a1/67fb52af642e974d159b5b379e4d4c59d0ebe1288677fbd04bbffe665a82/coverage-7.12.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:99d5415c73ca12d558e07776bd957c4222c687b9f1d26fa0e1b57e3598bdcde8", size = 252318 }, - { url = "https://files.pythonhosted.org/packages/41/e5/38228f31b2c7665ebf9bdfdddd7a184d56450755c7e43ac721c11a4b8dab/coverage-7.12.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e949ebf60c717c3df63adb4a1a366c096c8d7fd8472608cd09359e1bd48ef59f", size = 253403 }, - { url = "https://files.pythonhosted.org/packages/ec/4b/df78e4c8188f9960684267c5a4897836f3f0f20a20c51606ee778a1d9749/coverage-7.12.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d907ddccbca819afa2cd014bc69983b146cca2735a0b1e6259b2a6c10be1e70", size = 249984 }, - { url = "https://files.pythonhosted.org/packages/ba/51/bb163933d195a345c6f63eab9e55743413d064c291b6220df754075c2769/coverage-7.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b1518ecbad4e6173f4c6e6c4a46e49555ea5679bf3feda5edb1b935c7c44e8a0", size = 251339 }, - { url = "https://files.pythonhosted.org/packages/15/40/c9b29cdb8412c837cdcbc2cfa054547dd83affe6cbbd4ce4fdb92b6ba7d1/coverage-7.12.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:51777647a749abdf6f6fd8c7cffab12de68ab93aab15efc72fbbb83036c2a068", size = 249489 }, - { url = "https://files.pythonhosted.org/packages/c8/da/b3131e20ba07a0de4437a50ef3b47840dfabf9293675b0cd5c2c7f66dd61/coverage-7.12.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:42435d46d6461a3b305cdfcad7cdd3248787771f53fe18305548cba474e6523b", size = 249070 }, - { url = "https://files.pythonhosted.org/packages/70/81/b653329b5f6302c08d683ceff6785bc60a34be9ae92a5c7b63ee7ee7acec/coverage-7.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5bcead88c8423e1855e64b8057d0544e33e4080b95b240c2a355334bb7ced937", size = 250929 }, - { url = "https://files.pythonhosted.org/packages/a3/00/250ac3bca9f252a5fb1338b5ad01331ebb7b40223f72bef5b1b2cb03aa64/coverage-7.12.0-cp312-cp312-win32.whl", hash = "sha256:dcbb630ab034e86d2a0f79aefd2be07e583202f41e037602d438c80044957baa", size = 220241 }, - { url = "https://files.pythonhosted.org/packages/64/1c/77e79e76d37ce83302f6c21980b45e09f8aa4551965213a10e62d71ce0ab/coverage-7.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:2fd8354ed5d69775ac42986a691fbf68b4084278710cee9d7c3eaa0c28fa982a", size = 221051 }, - { url = "https://files.pythonhosted.org/packages/31/f5/641b8a25baae564f9e52cac0e2667b123de961985709a004e287ee7663cc/coverage-7.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:737c3814903be30695b2de20d22bcc5428fdae305c61ba44cdc8b3252984c49c", size = 219692 }, - { url = "https://files.pythonhosted.org/packages/b8/14/771700b4048774e48d2c54ed0c674273702713c9ee7acdfede40c2666747/coverage-7.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:47324fffca8d8eae7e185b5bb20c14645f23350f870c1649003618ea91a78941", size = 217725 }, - { url = "https://files.pythonhosted.org/packages/17/a7/3aa4144d3bcb719bf67b22d2d51c2d577bf801498c13cb08f64173e80497/coverage-7.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ccf3b2ede91decd2fb53ec73c1f949c3e034129d1e0b07798ff1d02ea0c8fa4a", size = 218098 }, - { url = "https://files.pythonhosted.org/packages/fc/9c/b846bbc774ff81091a12a10203e70562c91ae71badda00c5ae5b613527b1/coverage-7.12.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b365adc70a6936c6b0582dc38746b33b2454148c02349345412c6e743efb646d", size = 249093 }, - { url = "https://files.pythonhosted.org/packages/76/b6/67d7c0e1f400b32c883e9342de4a8c2ae7c1a0b57c5de87622b7262e2309/coverage-7.12.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bc13baf85cd8a4cfcf4a35c7bc9d795837ad809775f782f697bf630b7e200211", size = 251686 }, - { url = "https://files.pythonhosted.org/packages/cc/75/b095bd4b39d49c3be4bffbb3135fea18a99a431c52dd7513637c0762fecb/coverage-7.12.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:099d11698385d572ceafb3288a5b80fe1fc58bf665b3f9d362389de488361d3d", size = 252930 }, - { url = "https://files.pythonhosted.org/packages/6e/f3/466f63015c7c80550bead3093aacabf5380c1220a2a93c35d374cae8f762/coverage-7.12.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:473dc45d69694069adb7680c405fb1e81f60b2aff42c81e2f2c3feaf544d878c", size = 249296 }, - { url = "https://files.pythonhosted.org/packages/27/86/eba2209bf2b7e28c68698fc13437519a295b2d228ba9e0ec91673e09fa92/coverage-7.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:583f9adbefd278e9de33c33d6846aa8f5d164fa49b47144180a0e037f0688bb9", size = 251068 }, - { url = "https://files.pythonhosted.org/packages/ec/55/ca8ae7dbba962a3351f18940b359b94c6bafdd7757945fdc79ec9e452dc7/coverage-7.12.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b2089cc445f2dc0af6f801f0d1355c025b76c24481935303cf1af28f636688f0", size = 249034 }, - { url = "https://files.pythonhosted.org/packages/7a/d7/39136149325cad92d420b023b5fd900dabdd1c3a0d1d5f148ef4a8cedef5/coverage-7.12.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:950411f1eb5d579999c5f66c62a40961f126fc71e5e14419f004471957b51508", size = 248853 }, - { url = "https://files.pythonhosted.org/packages/fe/b6/76e1add8b87ef60e00643b0b7f8f7bb73d4bf5249a3be19ebefc5793dd25/coverage-7.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b1aab7302a87bafebfe76b12af681b56ff446dc6f32ed178ff9c092ca776e6bc", size = 250619 }, - { url = "https://files.pythonhosted.org/packages/95/87/924c6dc64f9203f7a3c1832a6a0eee5a8335dbe5f1bdadcc278d6f1b4d74/coverage-7.12.0-cp313-cp313-win32.whl", hash = "sha256:d7e0d0303c13b54db495eb636bc2465b2fb8475d4c8bcec8fe4b5ca454dfbae8", size = 220261 }, - { url = "https://files.pythonhosted.org/packages/91/77/dd4aff9af16ff776bf355a24d87eeb48fc6acde54c907cc1ea89b14a8804/coverage-7.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:ce61969812d6a98a981d147d9ac583a36ac7db7766f2e64a9d4d059c2fe29d07", size = 221072 }, - { url = "https://files.pythonhosted.org/packages/70/49/5c9dc46205fef31b1b226a6e16513193715290584317fd4df91cdaf28b22/coverage-7.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:bcec6f47e4cb8a4c2dc91ce507f6eefc6a1b10f58df32cdc61dff65455031dfc", size = 219702 }, - { url = "https://files.pythonhosted.org/packages/9b/62/f87922641c7198667994dd472a91e1d9b829c95d6c29529ceb52132436ad/coverage-7.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:459443346509476170d553035e4a3eed7b860f4fe5242f02de1010501956ce87", size = 218420 }, - { url = "https://files.pythonhosted.org/packages/85/dd/1cc13b2395ef15dbb27d7370a2509b4aee77890a464fb35d72d428f84871/coverage-7.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:04a79245ab2b7a61688958f7a855275997134bc84f4a03bc240cf64ff132abf6", size = 218773 }, - { url = "https://files.pythonhosted.org/packages/74/40/35773cc4bb1e9d4658d4fb669eb4195b3151bef3bbd6f866aba5cd5dac82/coverage-7.12.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:09a86acaaa8455f13d6a99221d9654df249b33937b4e212b4e5a822065f12aa7", size = 260078 }, - { url = "https://files.pythonhosted.org/packages/ec/ee/231bb1a6ffc2905e396557585ebc6bdc559e7c66708376d245a1f1d330fc/coverage-7.12.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:907e0df1b71ba77463687a74149c6122c3f6aac56c2510a5d906b2f368208560", size = 262144 }, - { url = "https://files.pythonhosted.org/packages/28/be/32f4aa9f3bf0b56f3971001b56508352c7753915345d45fab4296a986f01/coverage-7.12.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b57e2d0ddd5f0582bae5437c04ee71c46cd908e7bc5d4d0391f9a41e812dd12", size = 264574 }, - { url = "https://files.pythonhosted.org/packages/68/7c/00489fcbc2245d13ab12189b977e0cf06ff3351cb98bc6beba8bd68c5902/coverage-7.12.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:58c1c6aa677f3a1411fe6fb28ec3a942e4f665df036a3608816e0847fad23296", size = 259298 }, - { url = "https://files.pythonhosted.org/packages/96/b4/f0760d65d56c3bea95b449e02570d4abd2549dc784bf39a2d4721a2d8ceb/coverage-7.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4c589361263ab2953e3c4cd2a94db94c4ad4a8e572776ecfbad2389c626e4507", size = 262150 }, - { url = "https://files.pythonhosted.org/packages/c5/71/9a9314df00f9326d78c1e5a910f520d599205907432d90d1c1b7a97aa4b1/coverage-7.12.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:91b810a163ccad2e43b1faa11d70d3cf4b6f3d83f9fd5f2df82a32d47b648e0d", size = 259763 }, - { url = "https://files.pythonhosted.org/packages/10/34/01a0aceed13fbdf925876b9a15d50862eb8845454301fe3cdd1df08b2182/coverage-7.12.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:40c867af715f22592e0d0fb533a33a71ec9e0f73a6945f722a0c85c8c1cbe3a2", size = 258653 }, - { url = "https://files.pythonhosted.org/packages/8d/04/81d8fd64928acf1574bbb0181f66901c6c1c6279c8ccf5f84259d2c68ae9/coverage-7.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:68b0d0a2d84f333de875666259dadf28cc67858bc8fd8b3f1eae84d3c2bec455", size = 260856 }, - { url = "https://files.pythonhosted.org/packages/f2/76/fa2a37bfaeaf1f766a2d2360a25a5297d4fb567098112f6517475eee120b/coverage-7.12.0-cp313-cp313t-win32.whl", hash = "sha256:73f9e7fbd51a221818fd11b7090eaa835a353ddd59c236c57b2199486b116c6d", size = 220936 }, - { url = "https://files.pythonhosted.org/packages/f9/52/60f64d932d555102611c366afb0eb434b34266b1d9266fc2fe18ab641c47/coverage-7.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:24cff9d1f5743f67db7ba46ff284018a6e9aeb649b67aa1e70c396aa1b7cb23c", size = 222001 }, - { url = "https://files.pythonhosted.org/packages/77/df/c303164154a5a3aea7472bf323b7c857fed93b26618ed9fc5c2955566bb0/coverage-7.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:c87395744f5c77c866d0f5a43d97cc39e17c7f1cb0115e54a2fe67ca75c5d14d", size = 220273 }, - { url = "https://files.pythonhosted.org/packages/bf/2e/fc12db0883478d6e12bbd62d481210f0c8daf036102aa11434a0c5755825/coverage-7.12.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a1c59b7dc169809a88b21a936eccf71c3895a78f5592051b1af8f4d59c2b4f92", size = 217777 }, - { url = "https://files.pythonhosted.org/packages/1f/c1/ce3e525d223350c6ec16b9be8a057623f54226ef7f4c2fee361ebb6a02b8/coverage-7.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8787b0f982e020adb732b9f051f3e49dd5054cebbc3f3432061278512a2b1360", size = 218100 }, - { url = "https://files.pythonhosted.org/packages/15/87/113757441504aee3808cb422990ed7c8bcc2d53a6779c66c5adef0942939/coverage-7.12.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5ea5a9f7dc8877455b13dd1effd3202e0bca72f6f3ab09f9036b1bcf728f69ac", size = 249151 }, - { url = "https://files.pythonhosted.org/packages/d9/1d/9529d9bd44049b6b05bb319c03a3a7e4b0a8a802d28fa348ad407e10706d/coverage-7.12.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fdba9f15849534594f60b47c9a30bc70409b54947319a7c4fd0e8e3d8d2f355d", size = 251667 }, - { url = "https://files.pythonhosted.org/packages/11/bb/567e751c41e9c03dc29d3ce74b8c89a1e3396313e34f255a2a2e8b9ebb56/coverage-7.12.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a00594770eb715854fb1c57e0dea08cce6720cfbc531accdb9850d7c7770396c", size = 253003 }, - { url = "https://files.pythonhosted.org/packages/e4/b3/c2cce2d8526a02fb9e9ca14a263ca6fc074449b33a6afa4892838c903528/coverage-7.12.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5560c7e0d82b42eb1951e4f68f071f8017c824ebfd5a6ebe42c60ac16c6c2434", size = 249185 }, - { url = "https://files.pythonhosted.org/packages/0e/a7/967f93bb66e82c9113c66a8d0b65ecf72fc865adfba5a145f50c7af7e58d/coverage-7.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d6c2e26b481c9159c2773a37947a9718cfdc58893029cdfb177531793e375cfc", size = 251025 }, - { url = "https://files.pythonhosted.org/packages/b9/b2/f2f6f56337bc1af465d5b2dc1ee7ee2141b8b9272f3bf6213fcbc309a836/coverage-7.12.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6e1a8c066dabcde56d5d9fed6a66bc19a2883a3fe051f0c397a41fc42aedd4cc", size = 248979 }, - { url = "https://files.pythonhosted.org/packages/f4/7a/bf4209f45a4aec09d10a01a57313a46c0e0e8f4c55ff2965467d41a92036/coverage-7.12.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f7ba9da4726e446d8dd8aae5a6cd872511184a5d861de80a86ef970b5dacce3e", size = 248800 }, - { url = "https://files.pythonhosted.org/packages/b8/b7/1e01b8696fb0521810f60c5bbebf699100d6754183e6cc0679bf2ed76531/coverage-7.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e0f483ab4f749039894abaf80c2f9e7ed77bbf3c737517fb88c8e8e305896a17", size = 250460 }, - { url = "https://files.pythonhosted.org/packages/71/ae/84324fb9cb46c024760e706353d9b771a81b398d117d8c1fe010391c186f/coverage-7.12.0-cp314-cp314-win32.whl", hash = "sha256:76336c19a9ef4a94b2f8dc79f8ac2da3f193f625bb5d6f51a328cd19bfc19933", size = 220533 }, - { url = "https://files.pythonhosted.org/packages/e2/71/1033629deb8460a8f97f83e6ac4ca3b93952e2b6f826056684df8275e015/coverage-7.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:7c1059b600aec6ef090721f8f633f60ed70afaffe8ecab85b59df748f24b31fe", size = 221348 }, - { url = "https://files.pythonhosted.org/packages/0a/5f/ac8107a902f623b0c251abdb749be282dc2ab61854a8a4fcf49e276fce2f/coverage-7.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:172cf3a34bfef42611963e2b661302a8931f44df31629e5b1050567d6b90287d", size = 219922 }, - { url = "https://files.pythonhosted.org/packages/79/6e/f27af2d4da367f16077d21ef6fe796c874408219fa6dd3f3efe7751bd910/coverage-7.12.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:aa7d48520a32cb21c7a9b31f81799e8eaec7239db36c3b670be0fa2403828d1d", size = 218511 }, - { url = "https://files.pythonhosted.org/packages/67/dd/65fd874aa460c30da78f9d259400d8e6a4ef457d61ab052fd248f0050558/coverage-7.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:90d58ac63bc85e0fb919f14d09d6caa63f35a5512a2205284b7816cafd21bb03", size = 218771 }, - { url = "https://files.pythonhosted.org/packages/55/e0/7c6b71d327d8068cb79c05f8f45bf1b6145f7a0de23bbebe63578fe5240a/coverage-7.12.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ca8ecfa283764fdda3eae1bdb6afe58bf78c2c3ec2b2edcb05a671f0bba7b3f9", size = 260151 }, - { url = "https://files.pythonhosted.org/packages/49/ce/4697457d58285b7200de6b46d606ea71066c6e674571a946a6ea908fb588/coverage-7.12.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:874fe69a0785d96bd066059cd4368022cebbec1a8958f224f0016979183916e6", size = 262257 }, - { url = "https://files.pythonhosted.org/packages/2f/33/acbc6e447aee4ceba88c15528dbe04a35fb4d67b59d393d2e0d6f1e242c1/coverage-7.12.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5b3c889c0b8b283a24d721a9eabc8ccafcfc3aebf167e4cd0d0e23bf8ec4e339", size = 264671 }, - { url = "https://files.pythonhosted.org/packages/87/ec/e2822a795c1ed44d569980097be839c5e734d4c0c1119ef8e0a073496a30/coverage-7.12.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8bb5b894b3ec09dcd6d3743229dc7f2c42ef7787dc40596ae04c0edda487371e", size = 259231 }, - { url = "https://files.pythonhosted.org/packages/72/c5/a7ec5395bb4a49c9b7ad97e63f0c92f6bf4a9e006b1393555a02dae75f16/coverage-7.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:79a44421cd5fba96aa57b5e3b5a4d3274c449d4c622e8f76882d76635501fd13", size = 262137 }, - { url = "https://files.pythonhosted.org/packages/67/0c/02c08858b764129f4ecb8e316684272972e60777ae986f3865b10940bdd6/coverage-7.12.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:33baadc0efd5c7294f436a632566ccc1f72c867f82833eb59820ee37dc811c6f", size = 259745 }, - { url = "https://files.pythonhosted.org/packages/5a/04/4fd32b7084505f3829a8fe45c1a74a7a728cb251aaadbe3bec04abcef06d/coverage-7.12.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c406a71f544800ef7e9e0000af706b88465f3573ae8b8de37e5f96c59f689ad1", size = 258570 }, - { url = "https://files.pythonhosted.org/packages/48/35/2365e37c90df4f5342c4fa202223744119fe31264ee2924f09f074ea9b6d/coverage-7.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e71bba6a40883b00c6d571599b4627f50c360b3d0d02bfc658168936be74027b", size = 260899 }, - { url = "https://files.pythonhosted.org/packages/05/56/26ab0464ca733fa325e8e71455c58c1c374ce30f7c04cebb88eabb037b18/coverage-7.12.0-cp314-cp314t-win32.whl", hash = "sha256:9157a5e233c40ce6613dead4c131a006adfda70e557b6856b97aceed01b0e27a", size = 221313 }, - { url = "https://files.pythonhosted.org/packages/da/1c/017a3e1113ed34d998b27d2c6dba08a9e7cb97d362f0ec988fcd873dcf81/coverage-7.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e84da3a0fd233aeec797b981c51af1cabac74f9bd67be42458365b30d11b5291", size = 222423 }, - { url = "https://files.pythonhosted.org/packages/4c/36/bcc504fdd5169301b52568802bb1b9cdde2e27a01d39fbb3b4b508ab7c2c/coverage-7.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:01d24af36fedda51c2b1aca56e4330a3710f83b02a5ff3743a6b015ffa7c9384", size = 220459 }, - { url = "https://files.pythonhosted.org/packages/ce/a3/43b749004e3c09452e39bb56347a008f0a0668aad37324a99b5c8ca91d9e/coverage-7.12.0-py3-none-any.whl", hash = "sha256:159d50c0b12e060b15ed3d39f87ed43d4f7f7ad40b8a534f4dd331adbb51104a", size = 209503 }, +sdist = { url = "https://files.pythonhosted.org/packages/89/26/4a96807b193b011588099c3b5c89fbb05294e5b90e71018e065465f34eb6/coverage-7.12.0.tar.gz", hash = "sha256:fc11e0a4e372cb5f282f16ef90d4a585034050ccda536451901abfb19a57f40c", size = 819341, upload-time = "2025-11-18T13:34:20.766Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/0c/0dfe7f0487477d96432e4815537263363fb6dd7289743a796e8e51eabdf2/coverage-7.12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aa124a3683d2af98bd9d9c2bfa7a5076ca7e5ab09fdb96b81fa7d89376ae928f", size = 217535, upload-time = "2025-11-18T13:32:08.812Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f5/f9a4a053a5bbff023d3bec259faac8f11a1e5a6479c2ccf586f910d8dac7/coverage-7.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d93fbf446c31c0140208dcd07c5d882029832e8ed7891a39d6d44bd65f2316c3", size = 218044, upload-time = "2025-11-18T13:32:10.329Z" }, + { url = "https://files.pythonhosted.org/packages/95/c5/84fc3697c1fa10cd8571919bf9693f693b7373278daaf3b73e328d502bc8/coverage-7.12.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:52ca620260bd8cd6027317bdd8b8ba929be1d741764ee765b42c4d79a408601e", size = 248440, upload-time = "2025-11-18T13:32:12.536Z" }, + { url = "https://files.pythonhosted.org/packages/f4/36/2d93fbf6a04670f3874aed397d5a5371948a076e3249244a9e84fb0e02d6/coverage-7.12.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f3433ffd541380f3a0e423cff0f4926d55b0cc8c1d160fdc3be24a4c03aa65f7", size = 250361, upload-time = "2025-11-18T13:32:13.852Z" }, + { url = "https://files.pythonhosted.org/packages/5d/49/66dc65cc456a6bfc41ea3d0758c4afeaa4068a2b2931bf83be6894cf1058/coverage-7.12.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7bbb321d4adc9f65e402c677cd1c8e4c2d0105d3ce285b51b4d87f1d5db5245", size = 252472, upload-time = "2025-11-18T13:32:15.068Z" }, + { url = "https://files.pythonhosted.org/packages/35/1f/ebb8a18dffd406db9fcd4b3ae42254aedcaf612470e8712f12041325930f/coverage-7.12.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22a7aade354a72dff3b59c577bfd18d6945c61f97393bc5fb7bd293a4237024b", size = 248592, upload-time = "2025-11-18T13:32:16.328Z" }, + { url = "https://files.pythonhosted.org/packages/da/a8/67f213c06e5ea3b3d4980df7dc344d7fea88240b5fe878a5dcbdfe0e2315/coverage-7.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3ff651dcd36d2fea66877cd4a82de478004c59b849945446acb5baf9379a1b64", size = 250167, upload-time = "2025-11-18T13:32:17.687Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/e52aef68154164ea40cc8389c120c314c747fe63a04b013a5782e989b77f/coverage-7.12.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:31b8b2e38391a56e3cea39d22a23faaa7c3fc911751756ef6d2621d2a9daf742", size = 248238, upload-time = "2025-11-18T13:32:19.2Z" }, + { url = "https://files.pythonhosted.org/packages/1f/a4/4d88750bcf9d6d66f77865e5a05a20e14db44074c25fd22519777cb69025/coverage-7.12.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:297bc2da28440f5ae51c845a47c8175a4db0553a53827886e4fb25c66633000c", size = 247964, upload-time = "2025-11-18T13:32:21.027Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6b/b74693158899d5b47b0bf6238d2c6722e20ba749f86b74454fac0696bb00/coverage-7.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6ff7651cc01a246908eac162a6a86fc0dbab6de1ad165dfb9a1e2ec660b44984", size = 248862, upload-time = "2025-11-18T13:32:22.304Z" }, + { url = "https://files.pythonhosted.org/packages/18/de/6af6730227ce0e8ade307b1cc4a08e7f51b419a78d02083a86c04ccceb29/coverage-7.12.0-cp311-cp311-win32.whl", hash = "sha256:313672140638b6ddb2c6455ddeda41c6a0b208298034544cfca138978c6baed6", size = 220033, upload-time = "2025-11-18T13:32:23.714Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a1/e7f63021a7c4fe20994359fcdeae43cbef4a4d0ca36a5a1639feeea5d9e1/coverage-7.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:a1783ed5bd0d5938d4435014626568dc7f93e3cb99bc59188cc18857c47aa3c4", size = 220966, upload-time = "2025-11-18T13:32:25.599Z" }, + { url = "https://files.pythonhosted.org/packages/77/e8/deae26453f37c20c3aa0c4433a1e32cdc169bf415cce223a693117aa3ddd/coverage-7.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:4648158fd8dd9381b5847622df1c90ff314efbfc1df4550092ab6013c238a5fc", size = 219637, upload-time = "2025-11-18T13:32:27.265Z" }, + { url = "https://files.pythonhosted.org/packages/02/bf/638c0427c0f0d47638242e2438127f3c8ee3cfc06c7fdeb16778ed47f836/coverage-7.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:29644c928772c78512b48e14156b81255000dcfd4817574ff69def189bcb3647", size = 217704, upload-time = "2025-11-18T13:32:28.906Z" }, + { url = "https://files.pythonhosted.org/packages/08/e1/706fae6692a66c2d6b871a608bbde0da6281903fa0e9f53a39ed441da36a/coverage-7.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8638cbb002eaa5d7c8d04da667813ce1067080b9a91099801a0053086e52b736", size = 218064, upload-time = "2025-11-18T13:32:30.161Z" }, + { url = "https://files.pythonhosted.org/packages/a9/8b/eb0231d0540f8af3ffda39720ff43cb91926489d01524e68f60e961366e4/coverage-7.12.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:083631eeff5eb9992c923e14b810a179798bb598e6a0dd60586819fc23be6e60", size = 249560, upload-time = "2025-11-18T13:32:31.835Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a1/67fb52af642e974d159b5b379e4d4c59d0ebe1288677fbd04bbffe665a82/coverage-7.12.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:99d5415c73ca12d558e07776bd957c4222c687b9f1d26fa0e1b57e3598bdcde8", size = 252318, upload-time = "2025-11-18T13:32:33.178Z" }, + { url = "https://files.pythonhosted.org/packages/41/e5/38228f31b2c7665ebf9bdfdddd7a184d56450755c7e43ac721c11a4b8dab/coverage-7.12.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e949ebf60c717c3df63adb4a1a366c096c8d7fd8472608cd09359e1bd48ef59f", size = 253403, upload-time = "2025-11-18T13:32:34.45Z" }, + { url = "https://files.pythonhosted.org/packages/ec/4b/df78e4c8188f9960684267c5a4897836f3f0f20a20c51606ee778a1d9749/coverage-7.12.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d907ddccbca819afa2cd014bc69983b146cca2735a0b1e6259b2a6c10be1e70", size = 249984, upload-time = "2025-11-18T13:32:35.747Z" }, + { url = "https://files.pythonhosted.org/packages/ba/51/bb163933d195a345c6f63eab9e55743413d064c291b6220df754075c2769/coverage-7.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b1518ecbad4e6173f4c6e6c4a46e49555ea5679bf3feda5edb1b935c7c44e8a0", size = 251339, upload-time = "2025-11-18T13:32:37.352Z" }, + { url = "https://files.pythonhosted.org/packages/15/40/c9b29cdb8412c837cdcbc2cfa054547dd83affe6cbbd4ce4fdb92b6ba7d1/coverage-7.12.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:51777647a749abdf6f6fd8c7cffab12de68ab93aab15efc72fbbb83036c2a068", size = 249489, upload-time = "2025-11-18T13:32:39.212Z" }, + { url = "https://files.pythonhosted.org/packages/c8/da/b3131e20ba07a0de4437a50ef3b47840dfabf9293675b0cd5c2c7f66dd61/coverage-7.12.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:42435d46d6461a3b305cdfcad7cdd3248787771f53fe18305548cba474e6523b", size = 249070, upload-time = "2025-11-18T13:32:40.598Z" }, + { url = "https://files.pythonhosted.org/packages/70/81/b653329b5f6302c08d683ceff6785bc60a34be9ae92a5c7b63ee7ee7acec/coverage-7.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5bcead88c8423e1855e64b8057d0544e33e4080b95b240c2a355334bb7ced937", size = 250929, upload-time = "2025-11-18T13:32:42.915Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/250ac3bca9f252a5fb1338b5ad01331ebb7b40223f72bef5b1b2cb03aa64/coverage-7.12.0-cp312-cp312-win32.whl", hash = "sha256:dcbb630ab034e86d2a0f79aefd2be07e583202f41e037602d438c80044957baa", size = 220241, upload-time = "2025-11-18T13:32:44.665Z" }, + { url = "https://files.pythonhosted.org/packages/64/1c/77e79e76d37ce83302f6c21980b45e09f8aa4551965213a10e62d71ce0ab/coverage-7.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:2fd8354ed5d69775ac42986a691fbf68b4084278710cee9d7c3eaa0c28fa982a", size = 221051, upload-time = "2025-11-18T13:32:46.008Z" }, + { url = "https://files.pythonhosted.org/packages/31/f5/641b8a25baae564f9e52cac0e2667b123de961985709a004e287ee7663cc/coverage-7.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:737c3814903be30695b2de20d22bcc5428fdae305c61ba44cdc8b3252984c49c", size = 219692, upload-time = "2025-11-18T13:32:47.372Z" }, + { url = "https://files.pythonhosted.org/packages/b8/14/771700b4048774e48d2c54ed0c674273702713c9ee7acdfede40c2666747/coverage-7.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:47324fffca8d8eae7e185b5bb20c14645f23350f870c1649003618ea91a78941", size = 217725, upload-time = "2025-11-18T13:32:49.22Z" }, + { url = "https://files.pythonhosted.org/packages/17/a7/3aa4144d3bcb719bf67b22d2d51c2d577bf801498c13cb08f64173e80497/coverage-7.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ccf3b2ede91decd2fb53ec73c1f949c3e034129d1e0b07798ff1d02ea0c8fa4a", size = 218098, upload-time = "2025-11-18T13:32:50.78Z" }, + { url = "https://files.pythonhosted.org/packages/fc/9c/b846bbc774ff81091a12a10203e70562c91ae71badda00c5ae5b613527b1/coverage-7.12.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b365adc70a6936c6b0582dc38746b33b2454148c02349345412c6e743efb646d", size = 249093, upload-time = "2025-11-18T13:32:52.554Z" }, + { url = "https://files.pythonhosted.org/packages/76/b6/67d7c0e1f400b32c883e9342de4a8c2ae7c1a0b57c5de87622b7262e2309/coverage-7.12.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bc13baf85cd8a4cfcf4a35c7bc9d795837ad809775f782f697bf630b7e200211", size = 251686, upload-time = "2025-11-18T13:32:54.862Z" }, + { url = "https://files.pythonhosted.org/packages/cc/75/b095bd4b39d49c3be4bffbb3135fea18a99a431c52dd7513637c0762fecb/coverage-7.12.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:099d11698385d572ceafb3288a5b80fe1fc58bf665b3f9d362389de488361d3d", size = 252930, upload-time = "2025-11-18T13:32:56.417Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f3/466f63015c7c80550bead3093aacabf5380c1220a2a93c35d374cae8f762/coverage-7.12.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:473dc45d69694069adb7680c405fb1e81f60b2aff42c81e2f2c3feaf544d878c", size = 249296, upload-time = "2025-11-18T13:32:58.074Z" }, + { url = "https://files.pythonhosted.org/packages/27/86/eba2209bf2b7e28c68698fc13437519a295b2d228ba9e0ec91673e09fa92/coverage-7.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:583f9adbefd278e9de33c33d6846aa8f5d164fa49b47144180a0e037f0688bb9", size = 251068, upload-time = "2025-11-18T13:32:59.646Z" }, + { url = "https://files.pythonhosted.org/packages/ec/55/ca8ae7dbba962a3351f18940b359b94c6bafdd7757945fdc79ec9e452dc7/coverage-7.12.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b2089cc445f2dc0af6f801f0d1355c025b76c24481935303cf1af28f636688f0", size = 249034, upload-time = "2025-11-18T13:33:01.481Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d7/39136149325cad92d420b023b5fd900dabdd1c3a0d1d5f148ef4a8cedef5/coverage-7.12.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:950411f1eb5d579999c5f66c62a40961f126fc71e5e14419f004471957b51508", size = 248853, upload-time = "2025-11-18T13:33:02.935Z" }, + { url = "https://files.pythonhosted.org/packages/fe/b6/76e1add8b87ef60e00643b0b7f8f7bb73d4bf5249a3be19ebefc5793dd25/coverage-7.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b1aab7302a87bafebfe76b12af681b56ff446dc6f32ed178ff9c092ca776e6bc", size = 250619, upload-time = "2025-11-18T13:33:04.336Z" }, + { url = "https://files.pythonhosted.org/packages/95/87/924c6dc64f9203f7a3c1832a6a0eee5a8335dbe5f1bdadcc278d6f1b4d74/coverage-7.12.0-cp313-cp313-win32.whl", hash = "sha256:d7e0d0303c13b54db495eb636bc2465b2fb8475d4c8bcec8fe4b5ca454dfbae8", size = 220261, upload-time = "2025-11-18T13:33:06.493Z" }, + { url = "https://files.pythonhosted.org/packages/91/77/dd4aff9af16ff776bf355a24d87eeb48fc6acde54c907cc1ea89b14a8804/coverage-7.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:ce61969812d6a98a981d147d9ac583a36ac7db7766f2e64a9d4d059c2fe29d07", size = 221072, upload-time = "2025-11-18T13:33:07.926Z" }, + { url = "https://files.pythonhosted.org/packages/70/49/5c9dc46205fef31b1b226a6e16513193715290584317fd4df91cdaf28b22/coverage-7.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:bcec6f47e4cb8a4c2dc91ce507f6eefc6a1b10f58df32cdc61dff65455031dfc", size = 219702, upload-time = "2025-11-18T13:33:09.631Z" }, + { url = "https://files.pythonhosted.org/packages/9b/62/f87922641c7198667994dd472a91e1d9b829c95d6c29529ceb52132436ad/coverage-7.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:459443346509476170d553035e4a3eed7b860f4fe5242f02de1010501956ce87", size = 218420, upload-time = "2025-11-18T13:33:11.153Z" }, + { url = "https://files.pythonhosted.org/packages/85/dd/1cc13b2395ef15dbb27d7370a2509b4aee77890a464fb35d72d428f84871/coverage-7.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:04a79245ab2b7a61688958f7a855275997134bc84f4a03bc240cf64ff132abf6", size = 218773, upload-time = "2025-11-18T13:33:12.569Z" }, + { url = "https://files.pythonhosted.org/packages/74/40/35773cc4bb1e9d4658d4fb669eb4195b3151bef3bbd6f866aba5cd5dac82/coverage-7.12.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:09a86acaaa8455f13d6a99221d9654df249b33937b4e212b4e5a822065f12aa7", size = 260078, upload-time = "2025-11-18T13:33:14.037Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ee/231bb1a6ffc2905e396557585ebc6bdc559e7c66708376d245a1f1d330fc/coverage-7.12.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:907e0df1b71ba77463687a74149c6122c3f6aac56c2510a5d906b2f368208560", size = 262144, upload-time = "2025-11-18T13:33:15.601Z" }, + { url = "https://files.pythonhosted.org/packages/28/be/32f4aa9f3bf0b56f3971001b56508352c7753915345d45fab4296a986f01/coverage-7.12.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b57e2d0ddd5f0582bae5437c04ee71c46cd908e7bc5d4d0391f9a41e812dd12", size = 264574, upload-time = "2025-11-18T13:33:17.354Z" }, + { url = "https://files.pythonhosted.org/packages/68/7c/00489fcbc2245d13ab12189b977e0cf06ff3351cb98bc6beba8bd68c5902/coverage-7.12.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:58c1c6aa677f3a1411fe6fb28ec3a942e4f665df036a3608816e0847fad23296", size = 259298, upload-time = "2025-11-18T13:33:18.958Z" }, + { url = "https://files.pythonhosted.org/packages/96/b4/f0760d65d56c3bea95b449e02570d4abd2549dc784bf39a2d4721a2d8ceb/coverage-7.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4c589361263ab2953e3c4cd2a94db94c4ad4a8e572776ecfbad2389c626e4507", size = 262150, upload-time = "2025-11-18T13:33:20.644Z" }, + { url = "https://files.pythonhosted.org/packages/c5/71/9a9314df00f9326d78c1e5a910f520d599205907432d90d1c1b7a97aa4b1/coverage-7.12.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:91b810a163ccad2e43b1faa11d70d3cf4b6f3d83f9fd5f2df82a32d47b648e0d", size = 259763, upload-time = "2025-11-18T13:33:22.189Z" }, + { url = "https://files.pythonhosted.org/packages/10/34/01a0aceed13fbdf925876b9a15d50862eb8845454301fe3cdd1df08b2182/coverage-7.12.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:40c867af715f22592e0d0fb533a33a71ec9e0f73a6945f722a0c85c8c1cbe3a2", size = 258653, upload-time = "2025-11-18T13:33:24.239Z" }, + { url = "https://files.pythonhosted.org/packages/8d/04/81d8fd64928acf1574bbb0181f66901c6c1c6279c8ccf5f84259d2c68ae9/coverage-7.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:68b0d0a2d84f333de875666259dadf28cc67858bc8fd8b3f1eae84d3c2bec455", size = 260856, upload-time = "2025-11-18T13:33:26.365Z" }, + { url = "https://files.pythonhosted.org/packages/f2/76/fa2a37bfaeaf1f766a2d2360a25a5297d4fb567098112f6517475eee120b/coverage-7.12.0-cp313-cp313t-win32.whl", hash = "sha256:73f9e7fbd51a221818fd11b7090eaa835a353ddd59c236c57b2199486b116c6d", size = 220936, upload-time = "2025-11-18T13:33:28.165Z" }, + { url = "https://files.pythonhosted.org/packages/f9/52/60f64d932d555102611c366afb0eb434b34266b1d9266fc2fe18ab641c47/coverage-7.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:24cff9d1f5743f67db7ba46ff284018a6e9aeb649b67aa1e70c396aa1b7cb23c", size = 222001, upload-time = "2025-11-18T13:33:29.656Z" }, + { url = "https://files.pythonhosted.org/packages/77/df/c303164154a5a3aea7472bf323b7c857fed93b26618ed9fc5c2955566bb0/coverage-7.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:c87395744f5c77c866d0f5a43d97cc39e17c7f1cb0115e54a2fe67ca75c5d14d", size = 220273, upload-time = "2025-11-18T13:33:31.415Z" }, + { url = "https://files.pythonhosted.org/packages/bf/2e/fc12db0883478d6e12bbd62d481210f0c8daf036102aa11434a0c5755825/coverage-7.12.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a1c59b7dc169809a88b21a936eccf71c3895a78f5592051b1af8f4d59c2b4f92", size = 217777, upload-time = "2025-11-18T13:33:32.86Z" }, + { url = "https://files.pythonhosted.org/packages/1f/c1/ce3e525d223350c6ec16b9be8a057623f54226ef7f4c2fee361ebb6a02b8/coverage-7.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8787b0f982e020adb732b9f051f3e49dd5054cebbc3f3432061278512a2b1360", size = 218100, upload-time = "2025-11-18T13:33:34.532Z" }, + { url = "https://files.pythonhosted.org/packages/15/87/113757441504aee3808cb422990ed7c8bcc2d53a6779c66c5adef0942939/coverage-7.12.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5ea5a9f7dc8877455b13dd1effd3202e0bca72f6f3ab09f9036b1bcf728f69ac", size = 249151, upload-time = "2025-11-18T13:33:36.135Z" }, + { url = "https://files.pythonhosted.org/packages/d9/1d/9529d9bd44049b6b05bb319c03a3a7e4b0a8a802d28fa348ad407e10706d/coverage-7.12.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fdba9f15849534594f60b47c9a30bc70409b54947319a7c4fd0e8e3d8d2f355d", size = 251667, upload-time = "2025-11-18T13:33:37.996Z" }, + { url = "https://files.pythonhosted.org/packages/11/bb/567e751c41e9c03dc29d3ce74b8c89a1e3396313e34f255a2a2e8b9ebb56/coverage-7.12.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a00594770eb715854fb1c57e0dea08cce6720cfbc531accdb9850d7c7770396c", size = 253003, upload-time = "2025-11-18T13:33:39.553Z" }, + { url = "https://files.pythonhosted.org/packages/e4/b3/c2cce2d8526a02fb9e9ca14a263ca6fc074449b33a6afa4892838c903528/coverage-7.12.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5560c7e0d82b42eb1951e4f68f071f8017c824ebfd5a6ebe42c60ac16c6c2434", size = 249185, upload-time = "2025-11-18T13:33:42.086Z" }, + { url = "https://files.pythonhosted.org/packages/0e/a7/967f93bb66e82c9113c66a8d0b65ecf72fc865adfba5a145f50c7af7e58d/coverage-7.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d6c2e26b481c9159c2773a37947a9718cfdc58893029cdfb177531793e375cfc", size = 251025, upload-time = "2025-11-18T13:33:43.634Z" }, + { url = "https://files.pythonhosted.org/packages/b9/b2/f2f6f56337bc1af465d5b2dc1ee7ee2141b8b9272f3bf6213fcbc309a836/coverage-7.12.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6e1a8c066dabcde56d5d9fed6a66bc19a2883a3fe051f0c397a41fc42aedd4cc", size = 248979, upload-time = "2025-11-18T13:33:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7a/bf4209f45a4aec09d10a01a57313a46c0e0e8f4c55ff2965467d41a92036/coverage-7.12.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f7ba9da4726e446d8dd8aae5a6cd872511184a5d861de80a86ef970b5dacce3e", size = 248800, upload-time = "2025-11-18T13:33:47.546Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b7/1e01b8696fb0521810f60c5bbebf699100d6754183e6cc0679bf2ed76531/coverage-7.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e0f483ab4f749039894abaf80c2f9e7ed77bbf3c737517fb88c8e8e305896a17", size = 250460, upload-time = "2025-11-18T13:33:49.537Z" }, + { url = "https://files.pythonhosted.org/packages/71/ae/84324fb9cb46c024760e706353d9b771a81b398d117d8c1fe010391c186f/coverage-7.12.0-cp314-cp314-win32.whl", hash = "sha256:76336c19a9ef4a94b2f8dc79f8ac2da3f193f625bb5d6f51a328cd19bfc19933", size = 220533, upload-time = "2025-11-18T13:33:51.16Z" }, + { url = "https://files.pythonhosted.org/packages/e2/71/1033629deb8460a8f97f83e6ac4ca3b93952e2b6f826056684df8275e015/coverage-7.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:7c1059b600aec6ef090721f8f633f60ed70afaffe8ecab85b59df748f24b31fe", size = 221348, upload-time = "2025-11-18T13:33:52.776Z" }, + { url = "https://files.pythonhosted.org/packages/0a/5f/ac8107a902f623b0c251abdb749be282dc2ab61854a8a4fcf49e276fce2f/coverage-7.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:172cf3a34bfef42611963e2b661302a8931f44df31629e5b1050567d6b90287d", size = 219922, upload-time = "2025-11-18T13:33:54.316Z" }, + { url = "https://files.pythonhosted.org/packages/79/6e/f27af2d4da367f16077d21ef6fe796c874408219fa6dd3f3efe7751bd910/coverage-7.12.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:aa7d48520a32cb21c7a9b31f81799e8eaec7239db36c3b670be0fa2403828d1d", size = 218511, upload-time = "2025-11-18T13:33:56.343Z" }, + { url = "https://files.pythonhosted.org/packages/67/dd/65fd874aa460c30da78f9d259400d8e6a4ef457d61ab052fd248f0050558/coverage-7.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:90d58ac63bc85e0fb919f14d09d6caa63f35a5512a2205284b7816cafd21bb03", size = 218771, upload-time = "2025-11-18T13:33:57.966Z" }, + { url = "https://files.pythonhosted.org/packages/55/e0/7c6b71d327d8068cb79c05f8f45bf1b6145f7a0de23bbebe63578fe5240a/coverage-7.12.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ca8ecfa283764fdda3eae1bdb6afe58bf78c2c3ec2b2edcb05a671f0bba7b3f9", size = 260151, upload-time = "2025-11-18T13:33:59.597Z" }, + { url = "https://files.pythonhosted.org/packages/49/ce/4697457d58285b7200de6b46d606ea71066c6e674571a946a6ea908fb588/coverage-7.12.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:874fe69a0785d96bd066059cd4368022cebbec1a8958f224f0016979183916e6", size = 262257, upload-time = "2025-11-18T13:34:01.166Z" }, + { url = "https://files.pythonhosted.org/packages/2f/33/acbc6e447aee4ceba88c15528dbe04a35fb4d67b59d393d2e0d6f1e242c1/coverage-7.12.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5b3c889c0b8b283a24d721a9eabc8ccafcfc3aebf167e4cd0d0e23bf8ec4e339", size = 264671, upload-time = "2025-11-18T13:34:02.795Z" }, + { url = "https://files.pythonhosted.org/packages/87/ec/e2822a795c1ed44d569980097be839c5e734d4c0c1119ef8e0a073496a30/coverage-7.12.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8bb5b894b3ec09dcd6d3743229dc7f2c42ef7787dc40596ae04c0edda487371e", size = 259231, upload-time = "2025-11-18T13:34:04.397Z" }, + { url = "https://files.pythonhosted.org/packages/72/c5/a7ec5395bb4a49c9b7ad97e63f0c92f6bf4a9e006b1393555a02dae75f16/coverage-7.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:79a44421cd5fba96aa57b5e3b5a4d3274c449d4c622e8f76882d76635501fd13", size = 262137, upload-time = "2025-11-18T13:34:06.068Z" }, + { url = "https://files.pythonhosted.org/packages/67/0c/02c08858b764129f4ecb8e316684272972e60777ae986f3865b10940bdd6/coverage-7.12.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:33baadc0efd5c7294f436a632566ccc1f72c867f82833eb59820ee37dc811c6f", size = 259745, upload-time = "2025-11-18T13:34:08.04Z" }, + { url = "https://files.pythonhosted.org/packages/5a/04/4fd32b7084505f3829a8fe45c1a74a7a728cb251aaadbe3bec04abcef06d/coverage-7.12.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c406a71f544800ef7e9e0000af706b88465f3573ae8b8de37e5f96c59f689ad1", size = 258570, upload-time = "2025-11-18T13:34:09.676Z" }, + { url = "https://files.pythonhosted.org/packages/48/35/2365e37c90df4f5342c4fa202223744119fe31264ee2924f09f074ea9b6d/coverage-7.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e71bba6a40883b00c6d571599b4627f50c360b3d0d02bfc658168936be74027b", size = 260899, upload-time = "2025-11-18T13:34:11.259Z" }, + { url = "https://files.pythonhosted.org/packages/05/56/26ab0464ca733fa325e8e71455c58c1c374ce30f7c04cebb88eabb037b18/coverage-7.12.0-cp314-cp314t-win32.whl", hash = "sha256:9157a5e233c40ce6613dead4c131a006adfda70e557b6856b97aceed01b0e27a", size = 221313, upload-time = "2025-11-18T13:34:12.863Z" }, + { url = "https://files.pythonhosted.org/packages/da/1c/017a3e1113ed34d998b27d2c6dba08a9e7cb97d362f0ec988fcd873dcf81/coverage-7.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e84da3a0fd233aeec797b981c51af1cabac74f9bd67be42458365b30d11b5291", size = 222423, upload-time = "2025-11-18T13:34:15.14Z" }, + { url = "https://files.pythonhosted.org/packages/4c/36/bcc504fdd5169301b52568802bb1b9cdde2e27a01d39fbb3b4b508ab7c2c/coverage-7.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:01d24af36fedda51c2b1aca56e4330a3710f83b02a5ff3743a6b015ffa7c9384", size = 220459, upload-time = "2025-11-18T13:34:17.222Z" }, + { url = "https://files.pythonhosted.org/packages/ce/a3/43b749004e3c09452e39bb56347a008f0a0668aad37324a99b5c8ca91d9e/coverage-7.12.0-py3-none-any.whl", hash = "sha256:159d50c0b12e060b15ed3d39f87ed43d4f7f7ad40b8a534f4dd331adbb51104a", size = 209503, upload-time = "2025-11-18T13:34:18.892Z" }, ] [package.optional-dependencies] @@ -268,50 +268,50 @@ toml = [ name = "distlib" version = "0.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605 } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047 }, + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, ] [[package]] name = "duckdb" version = "1.4.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/81/99/ac6c105118751cc3ccae980b12e44847273f3402e647ec3197aff2251e23/duckdb-1.4.2.tar.gz", hash = "sha256:df81acee3b15ecb2c72eb8f8579fb5922f6f56c71f5c8892ea3bc6fab39aa2c4", size = 18469786 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1a/76/5b79eac0abcb239806da1d26f20515882a8392d0729a031af9e61d494dd4/duckdb-1.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b2d882672b61bc6117a2c524cf64ea519d2e829295951d214f04e126f1549b09", size = 29005908 }, - { url = "https://files.pythonhosted.org/packages/73/1a/324d7787fdb0de96872ff7b48524830930494b45abf9501875be7456faa2/duckdb-1.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:995ec9c1fc3ce5fbfe5950b980ede2a9d51b35fdf2e3f873ce94c22fc3355fdc", size = 15398994 }, - { url = "https://files.pythonhosted.org/packages/ad/c6/a2a072ca73f91a32c0db1254dd84fec30f4d673f9d57d853802aedf867fa/duckdb-1.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:19d2c2f3cdf0242cad42e803602bbc2636706fc1d2d260ffac815ea2e3a018e8", size = 13727492 }, - { url = "https://files.pythonhosted.org/packages/d6/d5/8f84b3685a8730f47e68bce46dbce789cb85c915a8c6aafdf85830589eb3/duckdb-1.4.2-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a496a04458590dcec8e928122ebe2ecbb42c3e1de4119f5461f7bf547acbe79", size = 18456479 }, - { url = "https://files.pythonhosted.org/packages/30/7c/709a80e72a3bf013fa890fc767d2959a8a2a15abee4088559ddabcb9399f/duckdb-1.4.2-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c2315b693f201787c9892f31eb9a0484d3c648edb3578a86dc8c1284dd2873a", size = 20458319 }, - { url = "https://files.pythonhosted.org/packages/93/ff/e0b0dd10e6da48a262f3e054378a3781febf28af3381c0e1e901d0390b3c/duckdb-1.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:bdd2d808806ceeeec33ba89665a0bb707af8815f2ca40e6c4c581966c0628ba1", size = 12320864 }, - { url = "https://files.pythonhosted.org/packages/c9/29/2f68c57e7c4242fedbf4b3fdc24fce2ffcf60640c936621d8a645593a161/duckdb-1.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9356fe17af2711e0a5ace4b20a0373e03163545fd7516e0c3c40428f44597052", size = 29015814 }, - { url = "https://files.pythonhosted.org/packages/34/b7/030cc278a4ae788800a833b2901b9a7da7a6993121053c4155c359328531/duckdb-1.4.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:946a8374c0252db3fa41165ab9952b48adc8de06561a6b5fd62025ac700e492f", size = 15403892 }, - { url = "https://files.pythonhosted.org/packages/f7/a2/67f4798a7a29bd0813f8a1e94a83e857e57f5d1ba14cf3edc5551aad0095/duckdb-1.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:389fa9abe4ca37d091332a2f8c3ebd713f18e87dc4cb5e8efd3e5aa8ddf8885f", size = 13733622 }, - { url = "https://files.pythonhosted.org/packages/6e/ac/d0d0e3feae9663334b2336f15785d280b54a56c3ffa10334e20a51a87ecd/duckdb-1.4.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be8c0c40f2264b91500b89c688f743e1c7764966e988f680b1f19416b00052e", size = 18470220 }, - { url = "https://files.pythonhosted.org/packages/a5/52/7570a50430cbffc8bd702443ac28a446b0fa4f77747a3821d4b37a852b15/duckdb-1.4.2-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6a21732dd52a76f1e61484c06d65800b18f57fe29e8102a7466c201a2221604", size = 20481138 }, - { url = "https://files.pythonhosted.org/packages/95/5e/be05f46a290ea27630c112ff9e01fd01f585e599967fc52fe2edc7bc2039/duckdb-1.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:769440f4507c20542ae2e5b87f6c6c6d3f148c0aa8f912528f6c97e9aedf6a21", size = 12330737 }, - { url = "https://files.pythonhosted.org/packages/70/c4/5054dbe79cf570b0c97db0c2eba7eb541cc561037360479059a3b57e4a32/duckdb-1.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:de646227fc2c53101ac84e86e444e7561aa077387aca8b37052f3803ee690a17", size = 29015784 }, - { url = "https://files.pythonhosted.org/packages/2c/b8/97f4f07d9459f5d262751cccfb2f4256debb8fe5ca92370cebe21aab1ee2/duckdb-1.4.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f1fac31babda2045d4cdefe6d0fd2ebdd8d4c2a333fbcc11607cfeaec202d18d", size = 15403788 }, - { url = "https://files.pythonhosted.org/packages/a4/ea/112f33ace03682bafd4aaf0a3336da689b9834663e7032b3d678fd2902c9/duckdb-1.4.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:43ac632f40ab1aede9b4ce3c09ea043f26f3db97b83c07c632c84ebd7f7c0f4a", size = 13733603 }, - { url = "https://files.pythonhosted.org/packages/34/83/8d6f845a9a946e8b47b6253b9edb084c45670763e815feed6cfefc957e89/duckdb-1.4.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77db030b48321bf785767b7b1800bf657dd2584f6df0a77e05201ecd22017da2", size = 18473725 }, - { url = "https://files.pythonhosted.org/packages/82/29/153d1b4fc14c68e6766d7712d35a7ab6272a801c52160126ac7df681f758/duckdb-1.4.2-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a456adbc3459c9dcd99052fad20bd5f8ef642be5b04d09590376b2eb3eb84f5c", size = 20481971 }, - { url = "https://files.pythonhosted.org/packages/58/b7/8d3a58b5ebfb9e79ed4030a0f2fbd7e404c52602e977b1e7ab51651816c7/duckdb-1.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:2f7c61617d2b1da3da5d7e215be616ad45aa3221c4b9e2c4d1c28ed09bc3c1c4", size = 12330535 }, - { url = "https://files.pythonhosted.org/packages/25/46/0f316e4d0d6bada350b9da06691a2537c329c8948c78e8b5e0c4874bc5e2/duckdb-1.4.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:422be8c6bdc98366c97f464b204b81b892bf962abeae6b0184104b8233da4f19", size = 29028616 }, - { url = "https://files.pythonhosted.org/packages/82/ab/e04a8f97865251b544aee9501088d4f0cb8e8b37339bd465c0d33857d411/duckdb-1.4.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:459b1855bd06a226a2838da4f14c8863fd87a62e63d414a7f7f682a7c616511a", size = 15410382 }, - { url = "https://files.pythonhosted.org/packages/47/ec/b8229517c2f9fe88a38bb1a172a2da4d0ff34996d319d74554fda80b6358/duckdb-1.4.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20c45b4ead1ea4d23a1be1cd4f1dfc635e58b55f0dd11e38781369be6c549903", size = 13737588 }, - { url = "https://files.pythonhosted.org/packages/f2/9a/63d26da9011890a5b893e0c21845c0c0b43c634bf263af3bbca64be0db76/duckdb-1.4.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e552451054534970dc999e69ca5ae5c606458548c43fb66d772117760485096", size = 18477886 }, - { url = "https://files.pythonhosted.org/packages/23/35/b1fae4c5245697837f6f63e407fa81e7ccc7948f6ef2b124cd38736f4d1d/duckdb-1.4.2-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:128c97dab574a438d7c8d020670b21c68792267d88e65a7773667b556541fa9b", size = 20483292 }, - { url = "https://files.pythonhosted.org/packages/25/5e/6f5ebaabc12c6db62f471f86b5c9c8debd57f11aa1b2acbbcc4c68683238/duckdb-1.4.2-cp314-cp314-win_amd64.whl", hash = "sha256:dfcc56a83420c0dec0b83e97a6b33addac1b7554b8828894f9d203955591218c", size = 12830520 }, +sdist = { url = "https://files.pythonhosted.org/packages/81/99/ac6c105118751cc3ccae980b12e44847273f3402e647ec3197aff2251e23/duckdb-1.4.2.tar.gz", hash = "sha256:df81acee3b15ecb2c72eb8f8579fb5922f6f56c71f5c8892ea3bc6fab39aa2c4", size = 18469786, upload-time = "2025-11-12T13:18:04.203Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/76/5b79eac0abcb239806da1d26f20515882a8392d0729a031af9e61d494dd4/duckdb-1.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b2d882672b61bc6117a2c524cf64ea519d2e829295951d214f04e126f1549b09", size = 29005908, upload-time = "2025-11-12T13:16:44.454Z" }, + { url = "https://files.pythonhosted.org/packages/73/1a/324d7787fdb0de96872ff7b48524830930494b45abf9501875be7456faa2/duckdb-1.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:995ec9c1fc3ce5fbfe5950b980ede2a9d51b35fdf2e3f873ce94c22fc3355fdc", size = 15398994, upload-time = "2025-11-12T13:16:46.802Z" }, + { url = "https://files.pythonhosted.org/packages/ad/c6/a2a072ca73f91a32c0db1254dd84fec30f4d673f9d57d853802aedf867fa/duckdb-1.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:19d2c2f3cdf0242cad42e803602bbc2636706fc1d2d260ffac815ea2e3a018e8", size = 13727492, upload-time = "2025-11-12T13:16:49.097Z" }, + { url = "https://files.pythonhosted.org/packages/d6/d5/8f84b3685a8730f47e68bce46dbce789cb85c915a8c6aafdf85830589eb3/duckdb-1.4.2-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a496a04458590dcec8e928122ebe2ecbb42c3e1de4119f5461f7bf547acbe79", size = 18456479, upload-time = "2025-11-12T13:16:51.66Z" }, + { url = "https://files.pythonhosted.org/packages/30/7c/709a80e72a3bf013fa890fc767d2959a8a2a15abee4088559ddabcb9399f/duckdb-1.4.2-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c2315b693f201787c9892f31eb9a0484d3c648edb3578a86dc8c1284dd2873a", size = 20458319, upload-time = "2025-11-12T13:16:54.24Z" }, + { url = "https://files.pythonhosted.org/packages/93/ff/e0b0dd10e6da48a262f3e054378a3781febf28af3381c0e1e901d0390b3c/duckdb-1.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:bdd2d808806ceeeec33ba89665a0bb707af8815f2ca40e6c4c581966c0628ba1", size = 12320864, upload-time = "2025-11-12T13:16:56.798Z" }, + { url = "https://files.pythonhosted.org/packages/c9/29/2f68c57e7c4242fedbf4b3fdc24fce2ffcf60640c936621d8a645593a161/duckdb-1.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9356fe17af2711e0a5ace4b20a0373e03163545fd7516e0c3c40428f44597052", size = 29015814, upload-time = "2025-11-12T13:16:59.329Z" }, + { url = "https://files.pythonhosted.org/packages/34/b7/030cc278a4ae788800a833b2901b9a7da7a6993121053c4155c359328531/duckdb-1.4.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:946a8374c0252db3fa41165ab9952b48adc8de06561a6b5fd62025ac700e492f", size = 15403892, upload-time = "2025-11-12T13:17:02.141Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a2/67f4798a7a29bd0813f8a1e94a83e857e57f5d1ba14cf3edc5551aad0095/duckdb-1.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:389fa9abe4ca37d091332a2f8c3ebd713f18e87dc4cb5e8efd3e5aa8ddf8885f", size = 13733622, upload-time = "2025-11-12T13:17:04.502Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ac/d0d0e3feae9663334b2336f15785d280b54a56c3ffa10334e20a51a87ecd/duckdb-1.4.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be8c0c40f2264b91500b89c688f743e1c7764966e988f680b1f19416b00052e", size = 18470220, upload-time = "2025-11-12T13:17:07.049Z" }, + { url = "https://files.pythonhosted.org/packages/a5/52/7570a50430cbffc8bd702443ac28a446b0fa4f77747a3821d4b37a852b15/duckdb-1.4.2-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6a21732dd52a76f1e61484c06d65800b18f57fe29e8102a7466c201a2221604", size = 20481138, upload-time = "2025-11-12T13:17:09.459Z" }, + { url = "https://files.pythonhosted.org/packages/95/5e/be05f46a290ea27630c112ff9e01fd01f585e599967fc52fe2edc7bc2039/duckdb-1.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:769440f4507c20542ae2e5b87f6c6c6d3f148c0aa8f912528f6c97e9aedf6a21", size = 12330737, upload-time = "2025-11-12T13:17:12.02Z" }, + { url = "https://files.pythonhosted.org/packages/70/c4/5054dbe79cf570b0c97db0c2eba7eb541cc561037360479059a3b57e4a32/duckdb-1.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:de646227fc2c53101ac84e86e444e7561aa077387aca8b37052f3803ee690a17", size = 29015784, upload-time = "2025-11-12T13:17:14.409Z" }, + { url = "https://files.pythonhosted.org/packages/2c/b8/97f4f07d9459f5d262751cccfb2f4256debb8fe5ca92370cebe21aab1ee2/duckdb-1.4.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f1fac31babda2045d4cdefe6d0fd2ebdd8d4c2a333fbcc11607cfeaec202d18d", size = 15403788, upload-time = "2025-11-12T13:17:16.864Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ea/112f33ace03682bafd4aaf0a3336da689b9834663e7032b3d678fd2902c9/duckdb-1.4.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:43ac632f40ab1aede9b4ce3c09ea043f26f3db97b83c07c632c84ebd7f7c0f4a", size = 13733603, upload-time = "2025-11-12T13:17:20.884Z" }, + { url = "https://files.pythonhosted.org/packages/34/83/8d6f845a9a946e8b47b6253b9edb084c45670763e815feed6cfefc957e89/duckdb-1.4.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77db030b48321bf785767b7b1800bf657dd2584f6df0a77e05201ecd22017da2", size = 18473725, upload-time = "2025-11-12T13:17:23.074Z" }, + { url = "https://files.pythonhosted.org/packages/82/29/153d1b4fc14c68e6766d7712d35a7ab6272a801c52160126ac7df681f758/duckdb-1.4.2-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a456adbc3459c9dcd99052fad20bd5f8ef642be5b04d09590376b2eb3eb84f5c", size = 20481971, upload-time = "2025-11-12T13:17:26.703Z" }, + { url = "https://files.pythonhosted.org/packages/58/b7/8d3a58b5ebfb9e79ed4030a0f2fbd7e404c52602e977b1e7ab51651816c7/duckdb-1.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:2f7c61617d2b1da3da5d7e215be616ad45aa3221c4b9e2c4d1c28ed09bc3c1c4", size = 12330535, upload-time = "2025-11-12T13:17:29.175Z" }, + { url = "https://files.pythonhosted.org/packages/25/46/0f316e4d0d6bada350b9da06691a2537c329c8948c78e8b5e0c4874bc5e2/duckdb-1.4.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:422be8c6bdc98366c97f464b204b81b892bf962abeae6b0184104b8233da4f19", size = 29028616, upload-time = "2025-11-12T13:17:31.599Z" }, + { url = "https://files.pythonhosted.org/packages/82/ab/e04a8f97865251b544aee9501088d4f0cb8e8b37339bd465c0d33857d411/duckdb-1.4.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:459b1855bd06a226a2838da4f14c8863fd87a62e63d414a7f7f682a7c616511a", size = 15410382, upload-time = "2025-11-12T13:17:34.14Z" }, + { url = "https://files.pythonhosted.org/packages/47/ec/b8229517c2f9fe88a38bb1a172a2da4d0ff34996d319d74554fda80b6358/duckdb-1.4.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20c45b4ead1ea4d23a1be1cd4f1dfc635e58b55f0dd11e38781369be6c549903", size = 13737588, upload-time = "2025-11-12T13:17:36.515Z" }, + { url = "https://files.pythonhosted.org/packages/f2/9a/63d26da9011890a5b893e0c21845c0c0b43c634bf263af3bbca64be0db76/duckdb-1.4.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e552451054534970dc999e69ca5ae5c606458548c43fb66d772117760485096", size = 18477886, upload-time = "2025-11-12T13:17:39.136Z" }, + { url = "https://files.pythonhosted.org/packages/23/35/b1fae4c5245697837f6f63e407fa81e7ccc7948f6ef2b124cd38736f4d1d/duckdb-1.4.2-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:128c97dab574a438d7c8d020670b21c68792267d88e65a7773667b556541fa9b", size = 20483292, upload-time = "2025-11-12T13:17:41.501Z" }, + { url = "https://files.pythonhosted.org/packages/25/5e/6f5ebaabc12c6db62f471f86b5c9c8debd57f11aa1b2acbbcc4c68683238/duckdb-1.4.2-cp314-cp314-win_amd64.whl", hash = "sha256:dfcc56a83420c0dec0b83e97a6b33addac1b7554b8828894f9d203955591218c", size = 12830520, upload-time = "2025-11-12T13:17:43.93Z" }, ] [[package]] name = "filelock" version = "3.20.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/58/46/0028a82567109b5ef6e4d2a1f04a583fb513e6cf9527fcdd09afd817deeb/filelock-3.20.0.tar.gz", hash = "sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4", size = 18922 } +sdist = { url = "https://files.pythonhosted.org/packages/58/46/0028a82567109b5ef6e4d2a1f04a583fb513e6cf9527fcdd09afd817deeb/filelock-3.20.0.tar.gz", hash = "sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4", size = 18922, upload-time = "2025-10-08T18:03:50.056Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054 }, + { url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054, upload-time = "2025-10-08T18:03:48.35Z" }, ] [[package]] @@ -326,9 +326,9 @@ dependencies = [ { name = "pyproj" }, { name = "shapely" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8c/76/e1960ba846f153ab109575242abf89dc98f8e057faa32f3decf4cce9247a/geopandas-1.1.1.tar.gz", hash = "sha256:1745713f64d095c43e72e08e753dbd271678254b24f2e01db8cdb8debe1d293d", size = 332655 } +sdist = { url = "https://files.pythonhosted.org/packages/8c/76/e1960ba846f153ab109575242abf89dc98f8e057faa32f3decf4cce9247a/geopandas-1.1.1.tar.gz", hash = "sha256:1745713f64d095c43e72e08e753dbd271678254b24f2e01db8cdb8debe1d293d", size = 332655, upload-time = "2025-06-26T21:04:56.57Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/70/d5cd0696eff08e62fdbdebe5b46527facb4e7220eabe0ac6225efab50168/geopandas-1.1.1-py3-none-any.whl", hash = "sha256:589e61aaf39b19828843df16cb90234e72897e2579be236f10eee0d052ad98e8", size = 338365 }, + { url = "https://files.pythonhosted.org/packages/0b/70/d5cd0696eff08e62fdbdebe5b46527facb4e7220eabe0ac6225efab50168/geopandas-1.1.1-py3-none-any.whl", hash = "sha256:589e61aaf39b19828843df16cb90234e72897e2579be236f10eee0d052ad98e8", size = 338365, upload-time = "2025-06-26T21:04:55.139Z" }, ] [[package]] @@ -338,9 +338,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "python-dateutil" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943 } +sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034 }, + { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, ] [[package]] @@ -350,9 +350,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "sortedcontainers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e1/3d/41da3727e5f3e6b0c79b9657946c742e2f61d24edcde3e1660e337509586/hypothesis-6.148.3.tar.gz", hash = "sha256:bd81221740d8658473060ad900dc831f889f156fdb41210ba2f47cfad10a66ed", size = 469896 } +sdist = { url = "https://files.pythonhosted.org/packages/e1/3d/41da3727e5f3e6b0c79b9657946c742e2f61d24edcde3e1660e337509586/hypothesis-6.148.3.tar.gz", hash = "sha256:bd81221740d8658473060ad900dc831f889f156fdb41210ba2f47cfad10a66ed", size = 469896, upload-time = "2025-11-27T06:34:09.419Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/61/8c9fd9397eb46ac54d974be8b9e619c386d6b47a462d8df962ebb79980f9/hypothesis-6.148.3-py3-none-any.whl", hash = "sha256:e7dd193da9800234ec5e1541c1eddde4bddff49b53faf690ba68a0af55a7abb3", size = 536925 }, + { url = "https://files.pythonhosted.org/packages/0a/61/8c9fd9397eb46ac54d974be8b9e619c386d6b47a462d8df962ebb79980f9/hypothesis-6.148.3-py3-none-any.whl", hash = "sha256:e7dd193da9800234ec5e1541c1eddde4bddff49b53faf690ba68a0af55a7abb3", size = 536925, upload-time = "2025-11-27T06:34:06.978Z" }, ] [package.optional-dependencies] @@ -364,27 +364,27 @@ numpy = [ name = "identify" version = "2.6.15" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ff/e7/685de97986c916a6d93b3876139e00eef26ad5bbbd61925d670ae8013449/identify-2.6.15.tar.gz", hash = "sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf", size = 99311 } +sdist = { url = "https://files.pythonhosted.org/packages/ff/e7/685de97986c916a6d93b3876139e00eef26ad5bbbd61925d670ae8013449/identify-2.6.15.tar.gz", hash = "sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf", size = 99311, upload-time = "2025-10-02T17:43:40.631Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl", hash = "sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757", size = 99183 }, + { url = "https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl", hash = "sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757", size = 99183, upload-time = "2025-10-02T17:43:39.137Z" }, ] [[package]] name = "idna" version = "3.11" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582 } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008 }, + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] [[package]] name = "iniconfig" version = "2.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503 } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484 }, + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] [[package]] @@ -394,81 +394,81 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115 } +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 }, + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] [[package]] name = "librt" version = "0.6.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/37/c3/cdff3c10e2e608490dc0a310ccf11ba777b3943ad4fcead2a2ade98c21e1/librt-0.6.3.tar.gz", hash = "sha256:c724a884e642aa2bbad52bb0203ea40406ad742368a5f90da1b220e970384aae", size = 54209 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/80/bc60fd16fe24910bf5974fb914778a2e8540cef55385ab2cb04a0dfe42c4/librt-0.6.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:61348cc488b18d1b1ff9f3e5fcd5ac43ed22d3e13e862489d2267c2337285c08", size = 27285 }, - { url = "https://files.pythonhosted.org/packages/88/3c/26335536ed9ba097c79cffcee148393592e55758fe76d99015af3e47a6d0/librt-0.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:64645b757d617ad5f98c08e07620bc488d4bced9ced91c6279cec418f16056fa", size = 27629 }, - { url = "https://files.pythonhosted.org/packages/af/fd/2dcedeacfedee5d2eda23e7a49c1c12ce6221b5d58a13555f053203faafc/librt-0.6.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:26b8026393920320bb9a811b691d73c5981385d537ffc5b6e22e53f7b65d4122", size = 82039 }, - { url = "https://files.pythonhosted.org/packages/48/ff/6aa11914b83b0dc2d489f7636942a8e3322650d0dba840db9a1b455f3caa/librt-0.6.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d998b432ed9ffccc49b820e913c8f327a82026349e9c34fa3690116f6b70770f", size = 86560 }, - { url = "https://files.pythonhosted.org/packages/76/a1/d25af61958c2c7eb978164aeba0350719f615179ba3f428b682b9a5fdace/librt-0.6.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e18875e17ef69ba7dfa9623f2f95f3eda6f70b536079ee6d5763ecdfe6cc9040", size = 86494 }, - { url = "https://files.pythonhosted.org/packages/7d/4b/40e75d3b258c801908e64b39788f9491635f9554f8717430a491385bd6f2/librt-0.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a218f85081fc3f70cddaed694323a1ad7db5ca028c379c214e3a7c11c0850523", size = 88914 }, - { url = "https://files.pythonhosted.org/packages/97/6d/0070c81aba8a169224301c75fb5fb6c3c25ca67e6ced086584fc130d5a67/librt-0.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1ef42ff4edd369e84433ce9b188a64df0837f4f69e3d34d3b34d4955c599d03f", size = 86944 }, - { url = "https://files.pythonhosted.org/packages/a6/94/809f38887941b7726692e0b5a083dbdc87dbb8cf893e3b286550c5f0b129/librt-0.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0e0f2b79993fec23a685b3e8107ba5f8675eeae286675a216da0b09574fa1e47", size = 89852 }, - { url = "https://files.pythonhosted.org/packages/58/a3/b0e5b1cda675b91f1111d8ba941da455d8bfaa22f4d2d8963ba96ccb5b12/librt-0.6.3-cp311-cp311-win32.whl", hash = "sha256:fd98cacf4e0fabcd4005c452cb8a31750258a85cab9a59fb3559e8078da408d7", size = 19948 }, - { url = "https://files.pythonhosted.org/packages/cc/73/70011c2b37e3be3ece3affd3abc8ebe5cda482b03fd6b3397906321a901e/librt-0.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:e17b5b42c8045867ca9d1f54af00cc2275198d38de18545edaa7833d7e9e4ac8", size = 21406 }, - { url = "https://files.pythonhosted.org/packages/91/ee/119aa759290af6ca0729edf513ca390c1afbeae60f3ecae9b9d56f25a8a9/librt-0.6.3-cp311-cp311-win_arm64.whl", hash = "sha256:87597e3d57ec0120a3e1d857a708f80c02c42ea6b00227c728efbc860f067c45", size = 20875 }, - { url = "https://files.pythonhosted.org/packages/b4/2c/b59249c566f98fe90e178baf59e83f628d6c38fb8bc78319301fccda0b5e/librt-0.6.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:74418f718083009108dc9a42c21bf2e4802d49638a1249e13677585fcc9ca176", size = 27841 }, - { url = "https://files.pythonhosted.org/packages/40/e8/9db01cafcd1a2872b76114c858f81cc29ce7ad606bc102020d6dabf470fb/librt-0.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:514f3f363d1ebc423357d36222c37e5c8e6674b6eae8d7195ac9a64903722057", size = 27844 }, - { url = "https://files.pythonhosted.org/packages/59/4d/da449d3a7d83cc853af539dee42adc37b755d7eea4ad3880bacfd84b651d/librt-0.6.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cf1115207a5049d1f4b7b4b72de0e52f228d6c696803d94843907111cbf80610", size = 84091 }, - { url = "https://files.pythonhosted.org/packages/ea/6c/f90306906fb6cc6eaf4725870f0347115de05431e1f96d35114392d31fda/librt-0.6.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad8ba80cdcea04bea7b78fcd4925bfbf408961e9d8397d2ee5d3ec121e20c08c", size = 88239 }, - { url = "https://files.pythonhosted.org/packages/e7/ae/473ce7b423cfac2cb503851a89d9d2195bf615f534d5912bf86feeebbee7/librt-0.6.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4018904c83eab49c814e2494b4e22501a93cdb6c9f9425533fe693c3117126f9", size = 88815 }, - { url = "https://files.pythonhosted.org/packages/c4/6d/934df738c87fb9617cabefe4891eece585a06abe6def25b4bca3b174429d/librt-0.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8983c5c06ac9c990eac5eb97a9f03fe41dc7e9d7993df74d9e8682a1056f596c", size = 90598 }, - { url = "https://files.pythonhosted.org/packages/72/89/eeaa124f5e0f431c2b39119550378ae817a4b1a3c93fd7122f0639336fff/librt-0.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d7769c579663a6f8dbf34878969ac71befa42067ce6bf78e6370bf0d1194997c", size = 88603 }, - { url = "https://files.pythonhosted.org/packages/4d/ed/c60b3c1cfc27d709bc0288af428ce58543fcb5053cf3eadbc773c24257f5/librt-0.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d3c9a07eafdc70556f8c220da4a538e715668c0c63cabcc436a026e4e89950bf", size = 92112 }, - { url = "https://files.pythonhosted.org/packages/c1/ab/f56169be5f716ef4ab0277be70bcb1874b4effc262e655d85b505af4884d/librt-0.6.3-cp312-cp312-win32.whl", hash = "sha256:38320386a48a15033da295df276aea93a92dfa94a862e06893f75ea1d8bbe89d", size = 20127 }, - { url = "https://files.pythonhosted.org/packages/ff/8d/222750ce82bf95125529eaab585ac7e2829df252f3cfc05d68792fb1dd2c/librt-0.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:c0ecf4786ad0404b072196b5df774b1bb23c8aacdcacb6c10b4128bc7b00bd01", size = 21545 }, - { url = "https://files.pythonhosted.org/packages/72/c9/f731ddcfb72f446a92a8674c6b8e1e2242773cce43a04f41549bd8b958ff/librt-0.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:9f2a6623057989ebc469cd9cc8fe436c40117a0147627568d03f84aef7854c55", size = 20946 }, - { url = "https://files.pythonhosted.org/packages/dd/aa/3055dd440f8b8b3b7e8624539a0749dd8e1913e978993bcca9ce7e306231/librt-0.6.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9e716f9012148a81f02f46a04fc4c663420c6fbfeacfac0b5e128cf43b4413d3", size = 27874 }, - { url = "https://files.pythonhosted.org/packages/ef/93/226d7dd455eaa4c26712b5ccb2dfcca12831baa7f898c8ffd3a831e29fda/librt-0.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:669ff2495728009a96339c5ad2612569c6d8be4474e68f3f3ac85d7c3261f5f5", size = 27852 }, - { url = "https://files.pythonhosted.org/packages/4e/8b/db9d51191aef4e4cc06285250affe0bb0ad8b2ed815f7ca77951655e6f02/librt-0.6.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:349b6873ebccfc24c9efd244e49da9f8a5c10f60f07575e248921aae2123fc42", size = 84264 }, - { url = "https://files.pythonhosted.org/packages/8d/53/297c96bda3b5a73bdaf748f1e3ae757edd29a0a41a956b9c10379f193417/librt-0.6.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c74c26736008481c9f6d0adf1aedb5a52aff7361fea98276d1f965c0256ee70", size = 88432 }, - { url = "https://files.pythonhosted.org/packages/54/3a/c005516071123278e340f22de72fa53d51e259d49215295c212da16c4dc2/librt-0.6.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:408a36ddc75e91918cb15b03460bdc8a015885025d67e68c6f78f08c3a88f522", size = 89014 }, - { url = "https://files.pythonhosted.org/packages/8e/9b/ea715f818d926d17b94c80a12d81a79e95c44f52848e61e8ca1ff29bb9a9/librt-0.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e61ab234624c9ffca0248a707feffe6fac2343758a36725d8eb8a6efef0f8c30", size = 90807 }, - { url = "https://files.pythonhosted.org/packages/f0/fc/4e2e4c87e002fa60917a8e474fd13c4bac9a759df82be3778573bb1ab954/librt-0.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:324462fe7e3896d592b967196512491ec60ca6e49c446fe59f40743d08c97917", size = 88890 }, - { url = "https://files.pythonhosted.org/packages/70/7f/c7428734fbdfd4db3d5b9237fc3a857880b2ace66492836f6529fef25d92/librt-0.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:36b2ec8c15030002c7f688b4863e7be42820d7c62d9c6eece3db54a2400f0530", size = 92300 }, - { url = "https://files.pythonhosted.org/packages/f9/0c/738c4824fdfe74dc0f95d5e90ef9e759d4ecf7fd5ba964d54a7703322251/librt-0.6.3-cp313-cp313-win32.whl", hash = "sha256:25b1b60cb059471c0c0c803e07d0dfdc79e41a0a122f288b819219ed162672a3", size = 20159 }, - { url = "https://files.pythonhosted.org/packages/f2/95/93d0e61bc617306ecf4c54636b5cbde4947d872563565c4abdd9d07a39d3/librt-0.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:10a95ad074e2a98c9e4abc7f5b7d40e5ecbfa84c04c6ab8a70fabf59bd429b88", size = 21484 }, - { url = "https://files.pythonhosted.org/packages/10/23/abd7ace79ab54d1dbee265f13529266f686a7ce2d21ab59a992f989009b6/librt-0.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:17000df14f552e86877d67e4ab7966912224efc9368e998c96a6974a8d609bf9", size = 20935 }, - { url = "https://files.pythonhosted.org/packages/83/14/c06cb31152182798ed98be73f54932ab984894f5a8fccf9b73130897a938/librt-0.6.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8e695f25d1a425ad7a272902af8ab8c8d66c1998b177e4b5f5e7b4e215d0c88a", size = 27566 }, - { url = "https://files.pythonhosted.org/packages/0c/b1/ce83ca7b057b06150519152f53a0b302d7c33c8692ce2f01f669b5a819d9/librt-0.6.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3e84a4121a7ae360ca4da436548a9c1ca8ca134a5ced76c893cc5944426164bd", size = 27753 }, - { url = "https://files.pythonhosted.org/packages/3b/ec/739a885ef0a2839b6c25f1b01c99149d2cb6a34e933ffc8c051fcd22012e/librt-0.6.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:05f385a414de3f950886ea0aad8f109650d4b712cf9cc14cc17f5f62a9ab240b", size = 83178 }, - { url = "https://files.pythonhosted.org/packages/db/bd/dc18bb1489d48c0911b9f4d72eae2d304ea264e215ba80f1e6ba4a9fc41d/librt-0.6.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36a8e337461150b05ca2c7bdedb9e591dfc262c5230422cea398e89d0c746cdc", size = 87266 }, - { url = "https://files.pythonhosted.org/packages/94/f3/d0c5431b39eef15e48088b2d739ad84b17c2f1a22c0345c6d4c4a42b135e/librt-0.6.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dcbe48f6a03979384f27086484dc2a14959be1613cb173458bd58f714f2c48f3", size = 87623 }, - { url = "https://files.pythonhosted.org/packages/3b/15/9a52e90834e4bd6ee16cdbaf551cb32227cbaad27398391a189c489318bc/librt-0.6.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4bca9e4c260233fba37b15c4ec2f78aa99c1a79fbf902d19dd4a763c5c3fb751", size = 89436 }, - { url = "https://files.pythonhosted.org/packages/c3/8a/a7e78e46e8486e023c50f21758930ef4793999115229afd65de69e94c9cc/librt-0.6.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:760c25ed6ac968e24803eb5f7deb17ce026902d39865e83036bacbf5cf242aa8", size = 87540 }, - { url = "https://files.pythonhosted.org/packages/49/01/93799044a1cccac31f1074b07c583e181829d240539657e7f305ae63ae2a/librt-0.6.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4aa4a93a353ccff20df6e34fa855ae8fd788832c88f40a9070e3ddd3356a9f0e", size = 90597 }, - { url = "https://files.pythonhosted.org/packages/a7/29/00c7f58b8f8eb1bad6529ffb6c9cdcc0890a27dac59ecda04f817ead5277/librt-0.6.3-cp314-cp314-win32.whl", hash = "sha256:cb92741c2b4ea63c09609b064b26f7f5d9032b61ae222558c55832ec3ad0bcaf", size = 18955 }, - { url = "https://files.pythonhosted.org/packages/d7/13/2739e6e197a9f751375a37908a6a5b0bff637b81338497a1bcb5817394da/librt-0.6.3-cp314-cp314-win_amd64.whl", hash = "sha256:fdcd095b1b812d756fa5452aca93b962cf620694c0cadb192cec2bb77dcca9a2", size = 20263 }, - { url = "https://files.pythonhosted.org/packages/e1/73/393868fc2158705ea003114a24e73bb10b03bda31e9ad7b5c5ec6575338b/librt-0.6.3-cp314-cp314-win_arm64.whl", hash = "sha256:822ca79e28720a76a935c228d37da6579edef048a17cd98d406a2484d10eda78", size = 19575 }, - { url = "https://files.pythonhosted.org/packages/48/6d/3c8ff3dec21bf804a205286dd63fd28dcdbe00b8dd7eb7ccf2e21a40a0b0/librt-0.6.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:078cd77064d1640cb7b0650871a772956066174d92c8aeda188a489b58495179", size = 28732 }, - { url = "https://files.pythonhosted.org/packages/f4/90/e214b8b4aa34ed3d3f1040719c06c4d22472c40c5ef81a922d5af7876eb4/librt-0.6.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5cc22f7f5c0cc50ed69f4b15b9c51d602aabc4500b433aaa2ddd29e578f452f7", size = 29065 }, - { url = "https://files.pythonhosted.org/packages/ab/90/ef61ed51f0a7770cc703422d907a757bbd8811ce820c333d3db2fd13542a/librt-0.6.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:14b345eb7afb61b9fdcdfda6738946bd11b8e0f6be258666b0646af3b9bb5916", size = 93703 }, - { url = "https://files.pythonhosted.org/packages/a8/ae/c30bb119c35962cbe9a908a71da99c168056fc3f6e9bbcbc157d0b724d89/librt-0.6.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d46aa46aa29b067f0b8b84f448fd9719aaf5f4c621cc279164d76a9dc9ab3e8", size = 98890 }, - { url = "https://files.pythonhosted.org/packages/d1/96/47a4a78d252d36f072b79d592df10600d379a895c3880c8cbd2ac699f0ad/librt-0.6.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1b51ba7d9d5d9001494769eca8c0988adce25d0a970c3ba3f2eb9df9d08036fc", size = 98255 }, - { url = "https://files.pythonhosted.org/packages/e5/28/779b5cc3cd9987683884eb5f5672e3251676bebaaae6b7da1cf366eb1da1/librt-0.6.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ced0925a18fddcff289ef54386b2fc230c5af3c83b11558571124bfc485b8c07", size = 100769 }, - { url = "https://files.pythonhosted.org/packages/28/d7/771755e57c375cb9d25a4e106f570607fd856e2cb91b02418db1db954796/librt-0.6.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:6bac97e51f66da2ca012adddbe9fd656b17f7368d439de30898f24b39512f40f", size = 98580 }, - { url = "https://files.pythonhosted.org/packages/d0/ec/8b157eb8fbc066339a2f34b0aceb2028097d0ed6150a52e23284a311eafe/librt-0.6.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b2922a0e8fa97395553c304edc3bd36168d8eeec26b92478e292e5d4445c1ef0", size = 101706 }, - { url = "https://files.pythonhosted.org/packages/82/a8/4aaead9a06c795a318282aebf7d3e3e578fa889ff396e1b640c3be4c7806/librt-0.6.3-cp314-cp314t-win32.whl", hash = "sha256:f33462b19503ba68d80dac8a1354402675849259fb3ebf53b67de86421735a3a", size = 19465 }, - { url = "https://files.pythonhosted.org/packages/3a/61/b7e6a02746c1731670c19ba07d86da90b1ae45d29e405c0b5615abf97cde/librt-0.6.3-cp314-cp314t-win_amd64.whl", hash = "sha256:04f8ce401d4f6380cfc42af0f4e67342bf34c820dae01343f58f472dbac75dcf", size = 21042 }, - { url = "https://files.pythonhosted.org/packages/0e/3d/72cc9ec90bb80b5b1a65f0bb74a0f540195837baaf3b98c7fa4a7aa9718e/librt-0.6.3-cp314-cp314t-win_arm64.whl", hash = "sha256:afb39550205cc5e5c935762c6bf6a2bb34f7d21a68eadb25e2db7bf3593fecc0", size = 20246 }, +sdist = { url = "https://files.pythonhosted.org/packages/37/c3/cdff3c10e2e608490dc0a310ccf11ba777b3943ad4fcead2a2ade98c21e1/librt-0.6.3.tar.gz", hash = "sha256:c724a884e642aa2bbad52bb0203ea40406ad742368a5f90da1b220e970384aae", size = 54209, upload-time = "2025-11-29T14:01:56.058Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/80/bc60fd16fe24910bf5974fb914778a2e8540cef55385ab2cb04a0dfe42c4/librt-0.6.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:61348cc488b18d1b1ff9f3e5fcd5ac43ed22d3e13e862489d2267c2337285c08", size = 27285, upload-time = "2025-11-29T14:00:46.626Z" }, + { url = "https://files.pythonhosted.org/packages/88/3c/26335536ed9ba097c79cffcee148393592e55758fe76d99015af3e47a6d0/librt-0.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:64645b757d617ad5f98c08e07620bc488d4bced9ced91c6279cec418f16056fa", size = 27629, upload-time = "2025-11-29T14:00:47.863Z" }, + { url = "https://files.pythonhosted.org/packages/af/fd/2dcedeacfedee5d2eda23e7a49c1c12ce6221b5d58a13555f053203faafc/librt-0.6.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:26b8026393920320bb9a811b691d73c5981385d537ffc5b6e22e53f7b65d4122", size = 82039, upload-time = "2025-11-29T14:00:49.131Z" }, + { url = "https://files.pythonhosted.org/packages/48/ff/6aa11914b83b0dc2d489f7636942a8e3322650d0dba840db9a1b455f3caa/librt-0.6.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d998b432ed9ffccc49b820e913c8f327a82026349e9c34fa3690116f6b70770f", size = 86560, upload-time = "2025-11-29T14:00:50.403Z" }, + { url = "https://files.pythonhosted.org/packages/76/a1/d25af61958c2c7eb978164aeba0350719f615179ba3f428b682b9a5fdace/librt-0.6.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e18875e17ef69ba7dfa9623f2f95f3eda6f70b536079ee6d5763ecdfe6cc9040", size = 86494, upload-time = "2025-11-29T14:00:51.383Z" }, + { url = "https://files.pythonhosted.org/packages/7d/4b/40e75d3b258c801908e64b39788f9491635f9554f8717430a491385bd6f2/librt-0.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a218f85081fc3f70cddaed694323a1ad7db5ca028c379c214e3a7c11c0850523", size = 88914, upload-time = "2025-11-29T14:00:52.688Z" }, + { url = "https://files.pythonhosted.org/packages/97/6d/0070c81aba8a169224301c75fb5fb6c3c25ca67e6ced086584fc130d5a67/librt-0.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1ef42ff4edd369e84433ce9b188a64df0837f4f69e3d34d3b34d4955c599d03f", size = 86944, upload-time = "2025-11-29T14:00:53.768Z" }, + { url = "https://files.pythonhosted.org/packages/a6/94/809f38887941b7726692e0b5a083dbdc87dbb8cf893e3b286550c5f0b129/librt-0.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0e0f2b79993fec23a685b3e8107ba5f8675eeae286675a216da0b09574fa1e47", size = 89852, upload-time = "2025-11-29T14:00:54.71Z" }, + { url = "https://files.pythonhosted.org/packages/58/a3/b0e5b1cda675b91f1111d8ba941da455d8bfaa22f4d2d8963ba96ccb5b12/librt-0.6.3-cp311-cp311-win32.whl", hash = "sha256:fd98cacf4e0fabcd4005c452cb8a31750258a85cab9a59fb3559e8078da408d7", size = 19948, upload-time = "2025-11-29T14:00:55.989Z" }, + { url = "https://files.pythonhosted.org/packages/cc/73/70011c2b37e3be3ece3affd3abc8ebe5cda482b03fd6b3397906321a901e/librt-0.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:e17b5b42c8045867ca9d1f54af00cc2275198d38de18545edaa7833d7e9e4ac8", size = 21406, upload-time = "2025-11-29T14:00:56.874Z" }, + { url = "https://files.pythonhosted.org/packages/91/ee/119aa759290af6ca0729edf513ca390c1afbeae60f3ecae9b9d56f25a8a9/librt-0.6.3-cp311-cp311-win_arm64.whl", hash = "sha256:87597e3d57ec0120a3e1d857a708f80c02c42ea6b00227c728efbc860f067c45", size = 20875, upload-time = "2025-11-29T14:00:57.752Z" }, + { url = "https://files.pythonhosted.org/packages/b4/2c/b59249c566f98fe90e178baf59e83f628d6c38fb8bc78319301fccda0b5e/librt-0.6.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:74418f718083009108dc9a42c21bf2e4802d49638a1249e13677585fcc9ca176", size = 27841, upload-time = "2025-11-29T14:00:58.925Z" }, + { url = "https://files.pythonhosted.org/packages/40/e8/9db01cafcd1a2872b76114c858f81cc29ce7ad606bc102020d6dabf470fb/librt-0.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:514f3f363d1ebc423357d36222c37e5c8e6674b6eae8d7195ac9a64903722057", size = 27844, upload-time = "2025-11-29T14:01:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/59/4d/da449d3a7d83cc853af539dee42adc37b755d7eea4ad3880bacfd84b651d/librt-0.6.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cf1115207a5049d1f4b7b4b72de0e52f228d6c696803d94843907111cbf80610", size = 84091, upload-time = "2025-11-29T14:01:01.118Z" }, + { url = "https://files.pythonhosted.org/packages/ea/6c/f90306906fb6cc6eaf4725870f0347115de05431e1f96d35114392d31fda/librt-0.6.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad8ba80cdcea04bea7b78fcd4925bfbf408961e9d8397d2ee5d3ec121e20c08c", size = 88239, upload-time = "2025-11-29T14:01:02.11Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ae/473ce7b423cfac2cb503851a89d9d2195bf615f534d5912bf86feeebbee7/librt-0.6.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4018904c83eab49c814e2494b4e22501a93cdb6c9f9425533fe693c3117126f9", size = 88815, upload-time = "2025-11-29T14:01:03.114Z" }, + { url = "https://files.pythonhosted.org/packages/c4/6d/934df738c87fb9617cabefe4891eece585a06abe6def25b4bca3b174429d/librt-0.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8983c5c06ac9c990eac5eb97a9f03fe41dc7e9d7993df74d9e8682a1056f596c", size = 90598, upload-time = "2025-11-29T14:01:04.071Z" }, + { url = "https://files.pythonhosted.org/packages/72/89/eeaa124f5e0f431c2b39119550378ae817a4b1a3c93fd7122f0639336fff/librt-0.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d7769c579663a6f8dbf34878969ac71befa42067ce6bf78e6370bf0d1194997c", size = 88603, upload-time = "2025-11-29T14:01:05.02Z" }, + { url = "https://files.pythonhosted.org/packages/4d/ed/c60b3c1cfc27d709bc0288af428ce58543fcb5053cf3eadbc773c24257f5/librt-0.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d3c9a07eafdc70556f8c220da4a538e715668c0c63cabcc436a026e4e89950bf", size = 92112, upload-time = "2025-11-29T14:01:06.304Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ab/f56169be5f716ef4ab0277be70bcb1874b4effc262e655d85b505af4884d/librt-0.6.3-cp312-cp312-win32.whl", hash = "sha256:38320386a48a15033da295df276aea93a92dfa94a862e06893f75ea1d8bbe89d", size = 20127, upload-time = "2025-11-29T14:01:07.283Z" }, + { url = "https://files.pythonhosted.org/packages/ff/8d/222750ce82bf95125529eaab585ac7e2829df252f3cfc05d68792fb1dd2c/librt-0.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:c0ecf4786ad0404b072196b5df774b1bb23c8aacdcacb6c10b4128bc7b00bd01", size = 21545, upload-time = "2025-11-29T14:01:08.184Z" }, + { url = "https://files.pythonhosted.org/packages/72/c9/f731ddcfb72f446a92a8674c6b8e1e2242773cce43a04f41549bd8b958ff/librt-0.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:9f2a6623057989ebc469cd9cc8fe436c40117a0147627568d03f84aef7854c55", size = 20946, upload-time = "2025-11-29T14:01:09.384Z" }, + { url = "https://files.pythonhosted.org/packages/dd/aa/3055dd440f8b8b3b7e8624539a0749dd8e1913e978993bcca9ce7e306231/librt-0.6.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9e716f9012148a81f02f46a04fc4c663420c6fbfeacfac0b5e128cf43b4413d3", size = 27874, upload-time = "2025-11-29T14:01:10.615Z" }, + { url = "https://files.pythonhosted.org/packages/ef/93/226d7dd455eaa4c26712b5ccb2dfcca12831baa7f898c8ffd3a831e29fda/librt-0.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:669ff2495728009a96339c5ad2612569c6d8be4474e68f3f3ac85d7c3261f5f5", size = 27852, upload-time = "2025-11-29T14:01:11.535Z" }, + { url = "https://files.pythonhosted.org/packages/4e/8b/db9d51191aef4e4cc06285250affe0bb0ad8b2ed815f7ca77951655e6f02/librt-0.6.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:349b6873ebccfc24c9efd244e49da9f8a5c10f60f07575e248921aae2123fc42", size = 84264, upload-time = "2025-11-29T14:01:12.461Z" }, + { url = "https://files.pythonhosted.org/packages/8d/53/297c96bda3b5a73bdaf748f1e3ae757edd29a0a41a956b9c10379f193417/librt-0.6.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c74c26736008481c9f6d0adf1aedb5a52aff7361fea98276d1f965c0256ee70", size = 88432, upload-time = "2025-11-29T14:01:13.405Z" }, + { url = "https://files.pythonhosted.org/packages/54/3a/c005516071123278e340f22de72fa53d51e259d49215295c212da16c4dc2/librt-0.6.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:408a36ddc75e91918cb15b03460bdc8a015885025d67e68c6f78f08c3a88f522", size = 89014, upload-time = "2025-11-29T14:01:14.373Z" }, + { url = "https://files.pythonhosted.org/packages/8e/9b/ea715f818d926d17b94c80a12d81a79e95c44f52848e61e8ca1ff29bb9a9/librt-0.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e61ab234624c9ffca0248a707feffe6fac2343758a36725d8eb8a6efef0f8c30", size = 90807, upload-time = "2025-11-29T14:01:15.377Z" }, + { url = "https://files.pythonhosted.org/packages/f0/fc/4e2e4c87e002fa60917a8e474fd13c4bac9a759df82be3778573bb1ab954/librt-0.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:324462fe7e3896d592b967196512491ec60ca6e49c446fe59f40743d08c97917", size = 88890, upload-time = "2025-11-29T14:01:16.633Z" }, + { url = "https://files.pythonhosted.org/packages/70/7f/c7428734fbdfd4db3d5b9237fc3a857880b2ace66492836f6529fef25d92/librt-0.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:36b2ec8c15030002c7f688b4863e7be42820d7c62d9c6eece3db54a2400f0530", size = 92300, upload-time = "2025-11-29T14:01:17.658Z" }, + { url = "https://files.pythonhosted.org/packages/f9/0c/738c4824fdfe74dc0f95d5e90ef9e759d4ecf7fd5ba964d54a7703322251/librt-0.6.3-cp313-cp313-win32.whl", hash = "sha256:25b1b60cb059471c0c0c803e07d0dfdc79e41a0a122f288b819219ed162672a3", size = 20159, upload-time = "2025-11-29T14:01:18.61Z" }, + { url = "https://files.pythonhosted.org/packages/f2/95/93d0e61bc617306ecf4c54636b5cbde4947d872563565c4abdd9d07a39d3/librt-0.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:10a95ad074e2a98c9e4abc7f5b7d40e5ecbfa84c04c6ab8a70fabf59bd429b88", size = 21484, upload-time = "2025-11-29T14:01:19.506Z" }, + { url = "https://files.pythonhosted.org/packages/10/23/abd7ace79ab54d1dbee265f13529266f686a7ce2d21ab59a992f989009b6/librt-0.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:17000df14f552e86877d67e4ab7966912224efc9368e998c96a6974a8d609bf9", size = 20935, upload-time = "2025-11-29T14:01:20.415Z" }, + { url = "https://files.pythonhosted.org/packages/83/14/c06cb31152182798ed98be73f54932ab984894f5a8fccf9b73130897a938/librt-0.6.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8e695f25d1a425ad7a272902af8ab8c8d66c1998b177e4b5f5e7b4e215d0c88a", size = 27566, upload-time = "2025-11-29T14:01:21.609Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b1/ce83ca7b057b06150519152f53a0b302d7c33c8692ce2f01f669b5a819d9/librt-0.6.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3e84a4121a7ae360ca4da436548a9c1ca8ca134a5ced76c893cc5944426164bd", size = 27753, upload-time = "2025-11-29T14:01:22.558Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ec/739a885ef0a2839b6c25f1b01c99149d2cb6a34e933ffc8c051fcd22012e/librt-0.6.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:05f385a414de3f950886ea0aad8f109650d4b712cf9cc14cc17f5f62a9ab240b", size = 83178, upload-time = "2025-11-29T14:01:23.555Z" }, + { url = "https://files.pythonhosted.org/packages/db/bd/dc18bb1489d48c0911b9f4d72eae2d304ea264e215ba80f1e6ba4a9fc41d/librt-0.6.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36a8e337461150b05ca2c7bdedb9e591dfc262c5230422cea398e89d0c746cdc", size = 87266, upload-time = "2025-11-29T14:01:24.532Z" }, + { url = "https://files.pythonhosted.org/packages/94/f3/d0c5431b39eef15e48088b2d739ad84b17c2f1a22c0345c6d4c4a42b135e/librt-0.6.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dcbe48f6a03979384f27086484dc2a14959be1613cb173458bd58f714f2c48f3", size = 87623, upload-time = "2025-11-29T14:01:25.798Z" }, + { url = "https://files.pythonhosted.org/packages/3b/15/9a52e90834e4bd6ee16cdbaf551cb32227cbaad27398391a189c489318bc/librt-0.6.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4bca9e4c260233fba37b15c4ec2f78aa99c1a79fbf902d19dd4a763c5c3fb751", size = 89436, upload-time = "2025-11-29T14:01:26.769Z" }, + { url = "https://files.pythonhosted.org/packages/c3/8a/a7e78e46e8486e023c50f21758930ef4793999115229afd65de69e94c9cc/librt-0.6.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:760c25ed6ac968e24803eb5f7deb17ce026902d39865e83036bacbf5cf242aa8", size = 87540, upload-time = "2025-11-29T14:01:27.756Z" }, + { url = "https://files.pythonhosted.org/packages/49/01/93799044a1cccac31f1074b07c583e181829d240539657e7f305ae63ae2a/librt-0.6.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4aa4a93a353ccff20df6e34fa855ae8fd788832c88f40a9070e3ddd3356a9f0e", size = 90597, upload-time = "2025-11-29T14:01:29.35Z" }, + { url = "https://files.pythonhosted.org/packages/a7/29/00c7f58b8f8eb1bad6529ffb6c9cdcc0890a27dac59ecda04f817ead5277/librt-0.6.3-cp314-cp314-win32.whl", hash = "sha256:cb92741c2b4ea63c09609b064b26f7f5d9032b61ae222558c55832ec3ad0bcaf", size = 18955, upload-time = "2025-11-29T14:01:30.325Z" }, + { url = "https://files.pythonhosted.org/packages/d7/13/2739e6e197a9f751375a37908a6a5b0bff637b81338497a1bcb5817394da/librt-0.6.3-cp314-cp314-win_amd64.whl", hash = "sha256:fdcd095b1b812d756fa5452aca93b962cf620694c0cadb192cec2bb77dcca9a2", size = 20263, upload-time = "2025-11-29T14:01:31.287Z" }, + { url = "https://files.pythonhosted.org/packages/e1/73/393868fc2158705ea003114a24e73bb10b03bda31e9ad7b5c5ec6575338b/librt-0.6.3-cp314-cp314-win_arm64.whl", hash = "sha256:822ca79e28720a76a935c228d37da6579edef048a17cd98d406a2484d10eda78", size = 19575, upload-time = "2025-11-29T14:01:32.229Z" }, + { url = "https://files.pythonhosted.org/packages/48/6d/3c8ff3dec21bf804a205286dd63fd28dcdbe00b8dd7eb7ccf2e21a40a0b0/librt-0.6.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:078cd77064d1640cb7b0650871a772956066174d92c8aeda188a489b58495179", size = 28732, upload-time = "2025-11-29T14:01:33.165Z" }, + { url = "https://files.pythonhosted.org/packages/f4/90/e214b8b4aa34ed3d3f1040719c06c4d22472c40c5ef81a922d5af7876eb4/librt-0.6.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5cc22f7f5c0cc50ed69f4b15b9c51d602aabc4500b433aaa2ddd29e578f452f7", size = 29065, upload-time = "2025-11-29T14:01:34.088Z" }, + { url = "https://files.pythonhosted.org/packages/ab/90/ef61ed51f0a7770cc703422d907a757bbd8811ce820c333d3db2fd13542a/librt-0.6.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:14b345eb7afb61b9fdcdfda6738946bd11b8e0f6be258666b0646af3b9bb5916", size = 93703, upload-time = "2025-11-29T14:01:35.057Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ae/c30bb119c35962cbe9a908a71da99c168056fc3f6e9bbcbc157d0b724d89/librt-0.6.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d46aa46aa29b067f0b8b84f448fd9719aaf5f4c621cc279164d76a9dc9ab3e8", size = 98890, upload-time = "2025-11-29T14:01:36.031Z" }, + { url = "https://files.pythonhosted.org/packages/d1/96/47a4a78d252d36f072b79d592df10600d379a895c3880c8cbd2ac699f0ad/librt-0.6.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1b51ba7d9d5d9001494769eca8c0988adce25d0a970c3ba3f2eb9df9d08036fc", size = 98255, upload-time = "2025-11-29T14:01:37.058Z" }, + { url = "https://files.pythonhosted.org/packages/e5/28/779b5cc3cd9987683884eb5f5672e3251676bebaaae6b7da1cf366eb1da1/librt-0.6.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ced0925a18fddcff289ef54386b2fc230c5af3c83b11558571124bfc485b8c07", size = 100769, upload-time = "2025-11-29T14:01:38.413Z" }, + { url = "https://files.pythonhosted.org/packages/28/d7/771755e57c375cb9d25a4e106f570607fd856e2cb91b02418db1db954796/librt-0.6.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:6bac97e51f66da2ca012adddbe9fd656b17f7368d439de30898f24b39512f40f", size = 98580, upload-time = "2025-11-29T14:01:39.459Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ec/8b157eb8fbc066339a2f34b0aceb2028097d0ed6150a52e23284a311eafe/librt-0.6.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b2922a0e8fa97395553c304edc3bd36168d8eeec26b92478e292e5d4445c1ef0", size = 101706, upload-time = "2025-11-29T14:01:40.474Z" }, + { url = "https://files.pythonhosted.org/packages/82/a8/4aaead9a06c795a318282aebf7d3e3e578fa889ff396e1b640c3be4c7806/librt-0.6.3-cp314-cp314t-win32.whl", hash = "sha256:f33462b19503ba68d80dac8a1354402675849259fb3ebf53b67de86421735a3a", size = 19465, upload-time = "2025-11-29T14:01:41.77Z" }, + { url = "https://files.pythonhosted.org/packages/3a/61/b7e6a02746c1731670c19ba07d86da90b1ae45d29e405c0b5615abf97cde/librt-0.6.3-cp314-cp314t-win_amd64.whl", hash = "sha256:04f8ce401d4f6380cfc42af0f4e67342bf34c820dae01343f58f472dbac75dcf", size = 21042, upload-time = "2025-11-29T14:01:42.865Z" }, + { url = "https://files.pythonhosted.org/packages/0e/3d/72cc9ec90bb80b5b1a65f0bb74a0f540195837baaf3b98c7fa4a7aa9718e/librt-0.6.3-cp314-cp314t-win_arm64.whl", hash = "sha256:afb39550205cc5e5c935762c6bf6a2bb34f7d21a68eadb25e2db7bf3593fecc0", size = 20246, upload-time = "2025-11-29T14:01:44.13Z" }, ] [[package]] name = "markdown" version = "3.10" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/7dd27d9d863b3376fcf23a5a13cb5d024aed1db46f963f1b5735ae43b3be/markdown-3.10.tar.gz", hash = "sha256:37062d4f2aa4b2b6b32aefb80faa300f82cc790cb949a35b8caede34f2b68c0e", size = 364931 } +sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/7dd27d9d863b3376fcf23a5a13cb5d024aed1db46f963f1b5735ae43b3be/markdown-3.10.tar.gz", hash = "sha256:37062d4f2aa4b2b6b32aefb80faa300f82cc790cb949a35b8caede34f2b68c0e", size = 364931, upload-time = "2025-11-03T19:51:15.007Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/70/81/54e3ce63502cd085a0c556652a4e1b919c45a446bd1e5300e10c44c8c521/markdown-3.10-py3-none-any.whl", hash = "sha256:b5b99d6951e2e4948d939255596523444c0e677c669700b1d17aa4a8a464cb7c", size = 107678 }, + { url = "https://files.pythonhosted.org/packages/70/81/54e3ce63502cd085a0c556652a4e1b919c45a446bd1e5300e10c44c8c521/markdown-3.10-py3-none-any.whl", hash = "sha256:b5b99d6951e2e4948d939255596523444c0e677c669700b1d17aa4a8a464cb7c", size = 107678, upload-time = "2025-11-03T19:51:13.887Z" }, ] [[package]] @@ -478,101 +478,101 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070 } +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321 }, + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, ] [[package]] name = "markupsafe" version = "3.0.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631 }, - { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058 }, - { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287 }, - { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940 }, - { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887 }, - { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692 }, - { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471 }, - { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923 }, - { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572 }, - { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077 }, - { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876 }, - { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615 }, - { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020 }, - { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332 }, - { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947 }, - { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962 }, - { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760 }, - { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529 }, - { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015 }, - { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540 }, - { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105 }, - { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906 }, - { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622 }, - { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029 }, - { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374 }, - { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980 }, - { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990 }, - { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784 }, - { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588 }, - { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041 }, - { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543 }, - { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113 }, - { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911 }, - { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658 }, - { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066 }, - { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639 }, - { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569 }, - { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284 }, - { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801 }, - { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769 }, - { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642 }, - { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612 }, - { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200 }, - { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973 }, - { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619 }, - { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029 }, - { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408 }, - { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005 }, - { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048 }, - { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821 }, - { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606 }, - { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043 }, - { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747 }, - { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341 }, - { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073 }, - { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661 }, - { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069 }, - { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670 }, - { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598 }, - { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261 }, - { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835 }, - { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733 }, - { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672 }, - { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819 }, - { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426 }, - { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146 }, +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, ] [[package]] name = "mdurl" version = "0.1.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] [[package]] name = "mergedeep" version = "1.3.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661 } +sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354 }, + { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, ] [[package]] @@ -594,9 +594,9 @@ dependencies = [ { name = "pyyaml-env-tag" }, { name = "watchdog" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159 } +sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451 }, + { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" }, ] [[package]] @@ -608,9 +608,9 @@ dependencies = [ { name = "platformdirs" }, { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/98/f5/ed29cd50067784976f25ed0ed6fcd3c2ce9eb90650aa3b2796ddf7b6870b/mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", size = 10239 } +sdist = { url = "https://files.pythonhosted.org/packages/98/f5/ed29cd50067784976f25ed0ed6fcd3c2ce9eb90650aa3b2796ddf7b6870b/mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", size = 10239, upload-time = "2023-11-20T17:51:09.981Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521 }, + { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521, upload-time = "2023-11-20T17:51:08.587Z" }, ] [[package]] @@ -630,18 +630,18 @@ dependencies = [ { name = "pymdown-extensions" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9c/3b/111b84cd6ff28d9e955b5f799ef217a17bc1684ac346af333e6100e413cb/mkdocs_material-9.7.0.tar.gz", hash = "sha256:602b359844e906ee402b7ed9640340cf8a474420d02d8891451733b6b02314ec", size = 4094546 } +sdist = { url = "https://files.pythonhosted.org/packages/9c/3b/111b84cd6ff28d9e955b5f799ef217a17bc1684ac346af333e6100e413cb/mkdocs_material-9.7.0.tar.gz", hash = "sha256:602b359844e906ee402b7ed9640340cf8a474420d02d8891451733b6b02314ec", size = 4094546, upload-time = "2025-11-11T08:49:09.73Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/87/eefe8d5e764f4cf50ed91b943f8e8f96b5efd65489d8303b7a36e2e79834/mkdocs_material-9.7.0-py3-none-any.whl", hash = "sha256:da2866ea53601125ff5baa8aa06404c6e07af3c5ce3d5de95e3b52b80b442887", size = 9283770 }, + { url = "https://files.pythonhosted.org/packages/04/87/eefe8d5e764f4cf50ed91b943f8e8f96b5efd65489d8303b7a36e2e79834/mkdocs_material-9.7.0-py3-none-any.whl", hash = "sha256:da2866ea53601125ff5baa8aa06404c6e07af3c5ce3d5de95e3b52b80b442887", size = 9283770, upload-time = "2025-11-11T08:49:06.26Z" }, ] [[package]] name = "mkdocs-material-extensions" version = "1.3.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847 } +sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847, upload-time = "2023-11-22T19:09:45.208Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728 }, + { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" }, ] [[package]] @@ -654,150 +654,150 @@ dependencies = [ { name = "pathspec" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f9/b5/b58cdc25fadd424552804bf410855d52324183112aa004f0732c5f6324cf/mypy-1.19.0.tar.gz", hash = "sha256:f6b874ca77f733222641e5c46e4711648c4037ea13646fd0cdc814c2eaec2528", size = 3579025 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/d2/010fb171ae5ac4a01cc34fbacd7544531e5ace95c35ca166dd8fd1b901d0/mypy-1.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a31e4c28e8ddb042c84c5e977e28a21195d086aaffaf08b016b78e19c9ef8106", size = 13010563 }, - { url = "https://files.pythonhosted.org/packages/41/6b/63f095c9f1ce584fdeb595d663d49e0980c735a1d2004720ccec252c5d47/mypy-1.19.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34ec1ac66d31644f194b7c163d7f8b8434f1b49719d403a5d26c87fff7e913f7", size = 12077037 }, - { url = "https://files.pythonhosted.org/packages/d7/83/6cb93d289038d809023ec20eb0b48bbb1d80af40511fa077da78af6ff7c7/mypy-1.19.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cb64b0ba5980466a0f3f9990d1c582bcab8db12e29815ecb57f1408d99b4bff7", size = 12680255 }, - { url = "https://files.pythonhosted.org/packages/99/db/d217815705987d2cbace2edd9100926196d6f85bcb9b5af05058d6e3c8ad/mypy-1.19.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:120cffe120cca5c23c03c77f84abc0c14c5d2e03736f6c312480020082f1994b", size = 13421472 }, - { url = "https://files.pythonhosted.org/packages/4e/51/d2beaca7c497944b07594f3f8aad8d2f0e8fc53677059848ae5d6f4d193e/mypy-1.19.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7a500ab5c444268a70565e374fc803972bfd1f09545b13418a5174e29883dab7", size = 13651823 }, - { url = "https://files.pythonhosted.org/packages/aa/d1/7883dcf7644db3b69490f37b51029e0870aac4a7ad34d09ceae709a3df44/mypy-1.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:c14a98bc63fd867530e8ec82f217dae29d0550c86e70debc9667fff1ec83284e", size = 10049077 }, - { url = "https://files.pythonhosted.org/packages/11/7e/1afa8fb188b876abeaa14460dc4983f909aaacaa4bf5718c00b2c7e0b3d5/mypy-1.19.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0fb3115cb8fa7c5f887c8a8d81ccdcb94cff334684980d847e5a62e926910e1d", size = 13207728 }, - { url = "https://files.pythonhosted.org/packages/b2/13/f103d04962bcbefb1644f5ccb235998b32c337d6c13145ea390b9da47f3e/mypy-1.19.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3e19e3b897562276bb331074d64c076dbdd3e79213f36eed4e592272dabd760", size = 12202945 }, - { url = "https://files.pythonhosted.org/packages/e4/93/a86a5608f74a22284a8ccea8592f6e270b61f95b8588951110ad797c2ddd/mypy-1.19.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9d491295825182fba01b6ffe2c6fe4e5a49dbf4e2bb4d1217b6ced3b4797bc6", size = 12718673 }, - { url = "https://files.pythonhosted.org/packages/3d/58/cf08fff9ced0423b858f2a7495001fda28dc058136818ee9dffc31534ea9/mypy-1.19.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6016c52ab209919b46169651b362068f632efcd5eb8ef9d1735f6f86da7853b2", size = 13608336 }, - { url = "https://files.pythonhosted.org/packages/64/ed/9c509105c5a6d4b73bb08733102a3ea62c25bc02c51bca85e3134bf912d3/mypy-1.19.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f188dcf16483b3e59f9278c4ed939ec0254aa8a60e8fc100648d9ab5ee95a431", size = 13833174 }, - { url = "https://files.pythonhosted.org/packages/cd/71/01939b66e35c6f8cb3e6fdf0b657f0fd24de2f8ba5e523625c8e72328208/mypy-1.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:0e3c3d1e1d62e678c339e7ade72746a9e0325de42cd2cccc51616c7b2ed1a018", size = 10112208 }, - { url = "https://files.pythonhosted.org/packages/cb/0d/a1357e6bb49e37ce26fcf7e3cc55679ce9f4ebee0cd8b6ee3a0e301a9210/mypy-1.19.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7686ed65dbabd24d20066f3115018d2dce030d8fa9db01aa9f0a59b6813e9f9e", size = 13191993 }, - { url = "https://files.pythonhosted.org/packages/5d/75/8e5d492a879ec4490e6ba664b5154e48c46c85b5ac9785792a5ec6a4d58f/mypy-1.19.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fd4a985b2e32f23bead72e2fb4bbe5d6aceee176be471243bd831d5b2644672d", size = 12174411 }, - { url = "https://files.pythonhosted.org/packages/71/31/ad5dcee9bfe226e8eaba777e9d9d251c292650130f0450a280aec3485370/mypy-1.19.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fc51a5b864f73a3a182584b1ac75c404396a17eced54341629d8bdcb644a5bba", size = 12727751 }, - { url = "https://files.pythonhosted.org/packages/77/06/b6b8994ce07405f6039701f4b66e9d23f499d0b41c6dd46ec28f96d57ec3/mypy-1.19.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:37af5166f9475872034b56c5efdcf65ee25394e9e1d172907b84577120714364", size = 13593323 }, - { url = "https://files.pythonhosted.org/packages/68/b1/126e274484cccdf099a8e328d4fda1c7bdb98a5e888fa6010b00e1bbf330/mypy-1.19.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:510c014b722308c9bd377993bcbf9a07d7e0692e5fa8fc70e639c1eb19fc6bee", size = 13818032 }, - { url = "https://files.pythonhosted.org/packages/f8/56/53a8f70f562dfc466c766469133a8a4909f6c0012d83993143f2a9d48d2d/mypy-1.19.0-cp313-cp313-win_amd64.whl", hash = "sha256:cabbee74f29aa9cd3b444ec2f1e4fa5a9d0d746ce7567a6a609e224429781f53", size = 10120644 }, - { url = "https://files.pythonhosted.org/packages/b0/f4/7751f32f56916f7f8c229fe902cbdba3e4dd3f3ea9e8b872be97e7fc546d/mypy-1.19.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f2e36bed3c6d9b5f35d28b63ca4b727cb0228e480826ffc8953d1892ddc8999d", size = 13185236 }, - { url = "https://files.pythonhosted.org/packages/35/31/871a9531f09e78e8d145032355890384f8a5b38c95a2c7732d226b93242e/mypy-1.19.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a18d8abdda14035c5718acb748faec09571432811af129bf0d9e7b2d6699bf18", size = 12213902 }, - { url = "https://files.pythonhosted.org/packages/58/b8/af221910dd40eeefa2077a59107e611550167b9994693fc5926a0b0f87c0/mypy-1.19.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f75e60aca3723a23511948539b0d7ed514dda194bc3755eae0bfc7a6b4887aa7", size = 12738600 }, - { url = "https://files.pythonhosted.org/packages/11/9f/c39e89a3e319c1d9c734dedec1183b2cc3aefbab066ec611619002abb932/mypy-1.19.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f44f2ae3c58421ee05fe609160343c25f70e3967f6e32792b5a78006a9d850f", size = 13592639 }, - { url = "https://files.pythonhosted.org/packages/97/6d/ffaf5f01f5e284d9033de1267e6c1b8f3783f2cf784465378a86122e884b/mypy-1.19.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:63ea6a00e4bd6822adbfc75b02ab3653a17c02c4347f5bb0cf1d5b9df3a05835", size = 13799132 }, - { url = "https://files.pythonhosted.org/packages/fe/b0/c33921e73aaa0106224e5a34822411bea38046188eb781637f5a5b07e269/mypy-1.19.0-cp314-cp314-win_amd64.whl", hash = "sha256:3ad925b14a0bb99821ff6f734553294aa6a3440a8cb082fe1f5b84dfb662afb1", size = 10269832 }, - { url = "https://files.pythonhosted.org/packages/09/0e/fe228ed5aeab470c6f4eb82481837fadb642a5aa95cc8215fd2214822c10/mypy-1.19.0-py3-none-any.whl", hash = "sha256:0c01c99d626380752e527d5ce8e69ffbba2046eb8a060db0329690849cf9b6f9", size = 2469714 }, +sdist = { url = "https://files.pythonhosted.org/packages/f9/b5/b58cdc25fadd424552804bf410855d52324183112aa004f0732c5f6324cf/mypy-1.19.0.tar.gz", hash = "sha256:f6b874ca77f733222641e5c46e4711648c4037ea13646fd0cdc814c2eaec2528", size = 3579025, upload-time = "2025-11-28T15:49:01.26Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/d2/010fb171ae5ac4a01cc34fbacd7544531e5ace95c35ca166dd8fd1b901d0/mypy-1.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a31e4c28e8ddb042c84c5e977e28a21195d086aaffaf08b016b78e19c9ef8106", size = 13010563, upload-time = "2025-11-28T15:48:23.975Z" }, + { url = "https://files.pythonhosted.org/packages/41/6b/63f095c9f1ce584fdeb595d663d49e0980c735a1d2004720ccec252c5d47/mypy-1.19.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34ec1ac66d31644f194b7c163d7f8b8434f1b49719d403a5d26c87fff7e913f7", size = 12077037, upload-time = "2025-11-28T15:47:51.582Z" }, + { url = "https://files.pythonhosted.org/packages/d7/83/6cb93d289038d809023ec20eb0b48bbb1d80af40511fa077da78af6ff7c7/mypy-1.19.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cb64b0ba5980466a0f3f9990d1c582bcab8db12e29815ecb57f1408d99b4bff7", size = 12680255, upload-time = "2025-11-28T15:46:57.628Z" }, + { url = "https://files.pythonhosted.org/packages/99/db/d217815705987d2cbace2edd9100926196d6f85bcb9b5af05058d6e3c8ad/mypy-1.19.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:120cffe120cca5c23c03c77f84abc0c14c5d2e03736f6c312480020082f1994b", size = 13421472, upload-time = "2025-11-28T15:47:59.655Z" }, + { url = "https://files.pythonhosted.org/packages/4e/51/d2beaca7c497944b07594f3f8aad8d2f0e8fc53677059848ae5d6f4d193e/mypy-1.19.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7a500ab5c444268a70565e374fc803972bfd1f09545b13418a5174e29883dab7", size = 13651823, upload-time = "2025-11-28T15:45:29.318Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d1/7883dcf7644db3b69490f37b51029e0870aac4a7ad34d09ceae709a3df44/mypy-1.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:c14a98bc63fd867530e8ec82f217dae29d0550c86e70debc9667fff1ec83284e", size = 10049077, upload-time = "2025-11-28T15:45:39.818Z" }, + { url = "https://files.pythonhosted.org/packages/11/7e/1afa8fb188b876abeaa14460dc4983f909aaacaa4bf5718c00b2c7e0b3d5/mypy-1.19.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0fb3115cb8fa7c5f887c8a8d81ccdcb94cff334684980d847e5a62e926910e1d", size = 13207728, upload-time = "2025-11-28T15:46:26.463Z" }, + { url = "https://files.pythonhosted.org/packages/b2/13/f103d04962bcbefb1644f5ccb235998b32c337d6c13145ea390b9da47f3e/mypy-1.19.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3e19e3b897562276bb331074d64c076dbdd3e79213f36eed4e592272dabd760", size = 12202945, upload-time = "2025-11-28T15:48:49.143Z" }, + { url = "https://files.pythonhosted.org/packages/e4/93/a86a5608f74a22284a8ccea8592f6e270b61f95b8588951110ad797c2ddd/mypy-1.19.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9d491295825182fba01b6ffe2c6fe4e5a49dbf4e2bb4d1217b6ced3b4797bc6", size = 12718673, upload-time = "2025-11-28T15:47:37.193Z" }, + { url = "https://files.pythonhosted.org/packages/3d/58/cf08fff9ced0423b858f2a7495001fda28dc058136818ee9dffc31534ea9/mypy-1.19.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6016c52ab209919b46169651b362068f632efcd5eb8ef9d1735f6f86da7853b2", size = 13608336, upload-time = "2025-11-28T15:48:32.625Z" }, + { url = "https://files.pythonhosted.org/packages/64/ed/9c509105c5a6d4b73bb08733102a3ea62c25bc02c51bca85e3134bf912d3/mypy-1.19.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f188dcf16483b3e59f9278c4ed939ec0254aa8a60e8fc100648d9ab5ee95a431", size = 13833174, upload-time = "2025-11-28T15:45:48.091Z" }, + { url = "https://files.pythonhosted.org/packages/cd/71/01939b66e35c6f8cb3e6fdf0b657f0fd24de2f8ba5e523625c8e72328208/mypy-1.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:0e3c3d1e1d62e678c339e7ade72746a9e0325de42cd2cccc51616c7b2ed1a018", size = 10112208, upload-time = "2025-11-28T15:46:41.702Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0d/a1357e6bb49e37ce26fcf7e3cc55679ce9f4ebee0cd8b6ee3a0e301a9210/mypy-1.19.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7686ed65dbabd24d20066f3115018d2dce030d8fa9db01aa9f0a59b6813e9f9e", size = 13191993, upload-time = "2025-11-28T15:47:22.336Z" }, + { url = "https://files.pythonhosted.org/packages/5d/75/8e5d492a879ec4490e6ba664b5154e48c46c85b5ac9785792a5ec6a4d58f/mypy-1.19.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fd4a985b2e32f23bead72e2fb4bbe5d6aceee176be471243bd831d5b2644672d", size = 12174411, upload-time = "2025-11-28T15:44:55.492Z" }, + { url = "https://files.pythonhosted.org/packages/71/31/ad5dcee9bfe226e8eaba777e9d9d251c292650130f0450a280aec3485370/mypy-1.19.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fc51a5b864f73a3a182584b1ac75c404396a17eced54341629d8bdcb644a5bba", size = 12727751, upload-time = "2025-11-28T15:44:14.169Z" }, + { url = "https://files.pythonhosted.org/packages/77/06/b6b8994ce07405f6039701f4b66e9d23f499d0b41c6dd46ec28f96d57ec3/mypy-1.19.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:37af5166f9475872034b56c5efdcf65ee25394e9e1d172907b84577120714364", size = 13593323, upload-time = "2025-11-28T15:46:34.699Z" }, + { url = "https://files.pythonhosted.org/packages/68/b1/126e274484cccdf099a8e328d4fda1c7bdb98a5e888fa6010b00e1bbf330/mypy-1.19.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:510c014b722308c9bd377993bcbf9a07d7e0692e5fa8fc70e639c1eb19fc6bee", size = 13818032, upload-time = "2025-11-28T15:46:18.286Z" }, + { url = "https://files.pythonhosted.org/packages/f8/56/53a8f70f562dfc466c766469133a8a4909f6c0012d83993143f2a9d48d2d/mypy-1.19.0-cp313-cp313-win_amd64.whl", hash = "sha256:cabbee74f29aa9cd3b444ec2f1e4fa5a9d0d746ce7567a6a609e224429781f53", size = 10120644, upload-time = "2025-11-28T15:47:43.99Z" }, + { url = "https://files.pythonhosted.org/packages/b0/f4/7751f32f56916f7f8c229fe902cbdba3e4dd3f3ea9e8b872be97e7fc546d/mypy-1.19.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f2e36bed3c6d9b5f35d28b63ca4b727cb0228e480826ffc8953d1892ddc8999d", size = 13185236, upload-time = "2025-11-28T15:45:20.696Z" }, + { url = "https://files.pythonhosted.org/packages/35/31/871a9531f09e78e8d145032355890384f8a5b38c95a2c7732d226b93242e/mypy-1.19.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a18d8abdda14035c5718acb748faec09571432811af129bf0d9e7b2d6699bf18", size = 12213902, upload-time = "2025-11-28T15:46:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/58/b8/af221910dd40eeefa2077a59107e611550167b9994693fc5926a0b0f87c0/mypy-1.19.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f75e60aca3723a23511948539b0d7ed514dda194bc3755eae0bfc7a6b4887aa7", size = 12738600, upload-time = "2025-11-28T15:44:22.521Z" }, + { url = "https://files.pythonhosted.org/packages/11/9f/c39e89a3e319c1d9c734dedec1183b2cc3aefbab066ec611619002abb932/mypy-1.19.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f44f2ae3c58421ee05fe609160343c25f70e3967f6e32792b5a78006a9d850f", size = 13592639, upload-time = "2025-11-28T15:48:08.55Z" }, + { url = "https://files.pythonhosted.org/packages/97/6d/ffaf5f01f5e284d9033de1267e6c1b8f3783f2cf784465378a86122e884b/mypy-1.19.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:63ea6a00e4bd6822adbfc75b02ab3653a17c02c4347f5bb0cf1d5b9df3a05835", size = 13799132, upload-time = "2025-11-28T15:47:06.032Z" }, + { url = "https://files.pythonhosted.org/packages/fe/b0/c33921e73aaa0106224e5a34822411bea38046188eb781637f5a5b07e269/mypy-1.19.0-cp314-cp314-win_amd64.whl", hash = "sha256:3ad925b14a0bb99821ff6f734553294aa6a3440a8cb082fe1f5b84dfb662afb1", size = 10269832, upload-time = "2025-11-28T15:47:29.392Z" }, + { url = "https://files.pythonhosted.org/packages/09/0e/fe228ed5aeab470c6f4eb82481837fadb642a5aa95cc8215fd2214822c10/mypy-1.19.0-py3-none-any.whl", hash = "sha256:0c01c99d626380752e527d5ce8e69ffbba2046eb8a060db0329690849cf9b6f9", size = 2469714, upload-time = "2025-11-28T15:45:33.22Z" }, ] [[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 } +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 }, + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] [[package]] name = "nodeenv" version = "1.9.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, ] [[package]] name = "numpy" version = "2.3.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/76/65/21b3bc86aac7b8f2862db1e808f1ea22b028e30a225a34a5ede9bf8678f2/numpy-2.3.5.tar.gz", hash = "sha256:784db1dcdab56bf0517743e746dfb0f885fc68d948aba86eeec2cba234bdf1c0", size = 20584950 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/43/77/84dd1d2e34d7e2792a236ba180b5e8fcc1e3e414e761ce0253f63d7f572e/numpy-2.3.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:de5672f4a7b200c15a4127042170a694d4df43c992948f5e1af57f0174beed10", size = 17034641 }, - { url = "https://files.pythonhosted.org/packages/2a/ea/25e26fa5837106cde46ae7d0b667e20f69cbbc0efd64cba8221411ab26ae/numpy-2.3.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:acfd89508504a19ed06ef963ad544ec6664518c863436306153e13e94605c218", size = 12528324 }, - { url = "https://files.pythonhosted.org/packages/4d/1a/e85f0eea4cf03d6a0228f5c0256b53f2df4bc794706e7df019fc622e47f1/numpy-2.3.5-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:ffe22d2b05504f786c867c8395de703937f934272eb67586817b46188b4ded6d", size = 5356872 }, - { url = "https://files.pythonhosted.org/packages/5c/bb/35ef04afd567f4c989c2060cde39211e4ac5357155c1833bcd1166055c61/numpy-2.3.5-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:872a5cf366aec6bb1147336480fef14c9164b154aeb6542327de4970282cd2f5", size = 6893148 }, - { url = "https://files.pythonhosted.org/packages/f2/2b/05bbeb06e2dff5eab512dfc678b1cc5ee94d8ac5956a0885c64b6b26252b/numpy-2.3.5-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3095bdb8dd297e5920b010e96134ed91d852d81d490e787beca7e35ae1d89cf7", size = 14557282 }, - { url = "https://files.pythonhosted.org/packages/65/fb/2b23769462b34398d9326081fad5655198fcf18966fcb1f1e49db44fbf31/numpy-2.3.5-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8cba086a43d54ca804ce711b2a940b16e452807acebe7852ff327f1ecd49b0d4", size = 16897903 }, - { url = "https://files.pythonhosted.org/packages/ac/14/085f4cf05fc3f1e8aa95e85404e984ffca9b2275a5dc2b1aae18a67538b8/numpy-2.3.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6cf9b429b21df6b99f4dee7a1218b8b7ffbbe7df8764dc0bd60ce8a0708fed1e", size = 16341672 }, - { url = "https://files.pythonhosted.org/packages/6f/3b/1f73994904142b2aa290449b3bb99772477b5fd94d787093e4f24f5af763/numpy-2.3.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:396084a36abdb603546b119d96528c2f6263921c50df3c8fd7cb28873a237748", size = 18838896 }, - { url = "https://files.pythonhosted.org/packages/cd/b9/cf6649b2124f288309ffc353070792caf42ad69047dcc60da85ee85fea58/numpy-2.3.5-cp311-cp311-win32.whl", hash = "sha256:b0c7088a73aef3d687c4deef8452a3ac7c1be4e29ed8bf3b366c8111128ac60c", size = 6563608 }, - { url = "https://files.pythonhosted.org/packages/aa/44/9fe81ae1dcc29c531843852e2874080dc441338574ccc4306b39e2ff6e59/numpy-2.3.5-cp311-cp311-win_amd64.whl", hash = "sha256:a414504bef8945eae5f2d7cb7be2d4af77c5d1cb5e20b296c2c25b61dff2900c", size = 13078442 }, - { url = "https://files.pythonhosted.org/packages/6d/a7/f99a41553d2da82a20a2f22e93c94f928e4490bb447c9ff3c4ff230581d3/numpy-2.3.5-cp311-cp311-win_arm64.whl", hash = "sha256:0cd00b7b36e35398fa2d16af7b907b65304ef8bb4817a550e06e5012929830fa", size = 10458555 }, - { url = "https://files.pythonhosted.org/packages/44/37/e669fe6cbb2b96c62f6bbedc6a81c0f3b7362f6a59230b23caa673a85721/numpy-2.3.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:74ae7b798248fe62021dbf3c914245ad45d1a6b0cb4a29ecb4b31d0bfbc4cc3e", size = 16733873 }, - { url = "https://files.pythonhosted.org/packages/c5/65/df0db6c097892c9380851ab9e44b52d4f7ba576b833996e0080181c0c439/numpy-2.3.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee3888d9ff7c14604052b2ca5535a30216aa0a58e948cdd3eeb8d3415f638769", size = 12259838 }, - { url = "https://files.pythonhosted.org/packages/5b/e1/1ee06e70eb2136797abe847d386e7c0e830b67ad1d43f364dd04fa50d338/numpy-2.3.5-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:612a95a17655e213502f60cfb9bf9408efdc9eb1d5f50535cc6eb365d11b42b5", size = 5088378 }, - { url = "https://files.pythonhosted.org/packages/6d/9c/1ca85fb86708724275103b81ec4cf1ac1d08f465368acfc8da7ab545bdae/numpy-2.3.5-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3101e5177d114a593d79dd79658650fe28b5a0d8abeb8ce6f437c0e6df5be1a4", size = 6628559 }, - { url = "https://files.pythonhosted.org/packages/74/78/fcd41e5a0ce4f3f7b003da85825acddae6d7ecb60cf25194741b036ca7d6/numpy-2.3.5-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b973c57ff8e184109db042c842423ff4f60446239bd585a5131cc47f06f789d", size = 14250702 }, - { url = "https://files.pythonhosted.org/packages/b6/23/2a1b231b8ff672b4c450dac27164a8b2ca7d9b7144f9c02d2396518352eb/numpy-2.3.5-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d8163f43acde9a73c2a33605353a4f1bc4798745a8b1d73183b28e5b435ae28", size = 16606086 }, - { url = "https://files.pythonhosted.org/packages/a0/c5/5ad26fbfbe2012e190cc7d5003e4d874b88bb18861d0829edc140a713021/numpy-2.3.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:51c1e14eb1e154ebd80e860722f9e6ed6ec89714ad2db2d3aa33c31d7c12179b", size = 16025985 }, - { url = "https://files.pythonhosted.org/packages/d2/fa/dd48e225c46c819288148d9d060b047fd2a6fb1eb37eae25112ee4cb4453/numpy-2.3.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b46b4ec24f7293f23adcd2d146960559aaf8020213de8ad1909dba6c013bf89c", size = 18542976 }, - { url = "https://files.pythonhosted.org/packages/05/79/ccbd23a75862d95af03d28b5c6901a1b7da4803181513d52f3b86ed9446e/numpy-2.3.5-cp312-cp312-win32.whl", hash = "sha256:3997b5b3c9a771e157f9aae01dd579ee35ad7109be18db0e85dbdbe1de06e952", size = 6285274 }, - { url = "https://files.pythonhosted.org/packages/2d/57/8aeaf160312f7f489dea47ab61e430b5cb051f59a98ae68b7133ce8fa06a/numpy-2.3.5-cp312-cp312-win_amd64.whl", hash = "sha256:86945f2ee6d10cdfd67bcb4069c1662dd711f7e2a4343db5cecec06b87cf31aa", size = 12782922 }, - { url = "https://files.pythonhosted.org/packages/78/a6/aae5cc2ca78c45e64b9ef22f089141d661516856cf7c8a54ba434576900d/numpy-2.3.5-cp312-cp312-win_arm64.whl", hash = "sha256:f28620fe26bee16243be2b7b874da327312240a7cdc38b769a697578d2100013", size = 10194667 }, - { url = "https://files.pythonhosted.org/packages/db/69/9cde09f36da4b5a505341180a3f2e6fadc352fd4d2b7096ce9778db83f1a/numpy-2.3.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d0f23b44f57077c1ede8c5f26b30f706498b4862d3ff0a7298b8411dd2f043ff", size = 16728251 }, - { url = "https://files.pythonhosted.org/packages/79/fb/f505c95ceddd7027347b067689db71ca80bd5ecc926f913f1a23e65cf09b/numpy-2.3.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa5bc7c5d59d831d9773d1170acac7893ce3a5e130540605770ade83280e7188", size = 12254652 }, - { url = "https://files.pythonhosted.org/packages/78/da/8c7738060ca9c31b30e9301ee0cf6c5ffdbf889d9593285a1cead337f9a5/numpy-2.3.5-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:ccc933afd4d20aad3c00bcef049cb40049f7f196e0397f1109dba6fed63267b0", size = 5083172 }, - { url = "https://files.pythonhosted.org/packages/a4/b4/ee5bb2537fb9430fd2ef30a616c3672b991a4129bb1c7dcc42aa0abbe5d7/numpy-2.3.5-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:afaffc4393205524af9dfa400fa250143a6c3bc646c08c9f5e25a9f4b4d6a903", size = 6622990 }, - { url = "https://files.pythonhosted.org/packages/95/03/dc0723a013c7d7c19de5ef29e932c3081df1c14ba582b8b86b5de9db7f0f/numpy-2.3.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c75442b2209b8470d6d5d8b1c25714270686f14c749028d2199c54e29f20b4d", size = 14248902 }, - { url = "https://files.pythonhosted.org/packages/f5/10/ca162f45a102738958dcec8023062dad0cbc17d1ab99d68c4e4a6c45fb2b/numpy-2.3.5-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11e06aa0af8c0f05104d56450d6093ee639e15f24ecf62d417329d06e522e017", size = 16597430 }, - { url = "https://files.pythonhosted.org/packages/2a/51/c1e29be863588db58175175f057286900b4b3327a1351e706d5e0f8dd679/numpy-2.3.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ed89927b86296067b4f81f108a2271d8926467a8868e554eaf370fc27fa3ccaf", size = 16024551 }, - { url = "https://files.pythonhosted.org/packages/83/68/8236589d4dbb87253d28259d04d9b814ec0ecce7cb1c7fed29729f4c3a78/numpy-2.3.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51c55fe3451421f3a6ef9a9c1439e82101c57a2c9eab9feb196a62b1a10b58ce", size = 18533275 }, - { url = "https://files.pythonhosted.org/packages/40/56/2932d75b6f13465239e3b7b7e511be27f1b8161ca2510854f0b6e521c395/numpy-2.3.5-cp313-cp313-win32.whl", hash = "sha256:1978155dd49972084bd6ef388d66ab70f0c323ddee6f693d539376498720fb7e", size = 6277637 }, - { url = "https://files.pythonhosted.org/packages/0c/88/e2eaa6cffb115b85ed7c7c87775cb8bcf0816816bc98ca8dbfa2ee33fe6e/numpy-2.3.5-cp313-cp313-win_amd64.whl", hash = "sha256:00dc4e846108a382c5869e77c6ed514394bdeb3403461d25a829711041217d5b", size = 12779090 }, - { url = "https://files.pythonhosted.org/packages/8f/88/3f41e13a44ebd4034ee17baa384acac29ba6a4fcc2aca95f6f08ca0447d1/numpy-2.3.5-cp313-cp313-win_arm64.whl", hash = "sha256:0472f11f6ec23a74a906a00b48a4dcf3849209696dff7c189714511268d103ae", size = 10194710 }, - { url = "https://files.pythonhosted.org/packages/13/cb/71744144e13389d577f867f745b7df2d8489463654a918eea2eeb166dfc9/numpy-2.3.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:414802f3b97f3c1eef41e530aaba3b3c1620649871d8cb38c6eaff034c2e16bd", size = 16827292 }, - { url = "https://files.pythonhosted.org/packages/71/80/ba9dc6f2a4398e7f42b708a7fdc841bb638d353be255655498edbf9a15a8/numpy-2.3.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5ee6609ac3604fa7780e30a03e5e241a7956f8e2fcfe547d51e3afa5247ac47f", size = 12378897 }, - { url = "https://files.pythonhosted.org/packages/2e/6d/db2151b9f64264bcceccd51741aa39b50150de9b602d98ecfe7e0c4bff39/numpy-2.3.5-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:86d835afea1eaa143012a2d7a3f45a3adce2d7adc8b4961f0b362214d800846a", size = 5207391 }, - { url = "https://files.pythonhosted.org/packages/80/ae/429bacace5ccad48a14c4ae5332f6aa8ab9f69524193511d60ccdfdc65fa/numpy-2.3.5-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:30bc11310e8153ca664b14c5f1b73e94bd0503681fcf136a163de856f3a50139", size = 6721275 }, - { url = "https://files.pythonhosted.org/packages/74/5b/1919abf32d8722646a38cd527bc3771eb229a32724ee6ba340ead9b92249/numpy-2.3.5-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1062fde1dcf469571705945b0f221b73928f34a20c904ffb45db101907c3454e", size = 14306855 }, - { url = "https://files.pythonhosted.org/packages/a5/87/6831980559434973bebc30cd9c1f21e541a0f2b0c280d43d3afd909b66d0/numpy-2.3.5-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce581db493ea1a96c0556360ede6607496e8bf9b3a8efa66e06477267bc831e9", size = 16657359 }, - { url = "https://files.pythonhosted.org/packages/dd/91/c797f544491ee99fd00495f12ebb7802c440c1915811d72ac5b4479a3356/numpy-2.3.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:cc8920d2ec5fa99875b670bb86ddeb21e295cb07aa331810d9e486e0b969d946", size = 16093374 }, - { url = "https://files.pythonhosted.org/packages/74/a6/54da03253afcbe7a72785ec4da9c69fb7a17710141ff9ac5fcb2e32dbe64/numpy-2.3.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9ee2197ef8c4f0dfe405d835f3b6a14f5fee7782b5de51ba06fb65fc9b36e9f1", size = 18594587 }, - { url = "https://files.pythonhosted.org/packages/80/e9/aff53abbdd41b0ecca94285f325aff42357c6b5abc482a3fcb4994290b18/numpy-2.3.5-cp313-cp313t-win32.whl", hash = "sha256:70b37199913c1bd300ff6e2693316c6f869c7ee16378faf10e4f5e3275b299c3", size = 6405940 }, - { url = "https://files.pythonhosted.org/packages/d5/81/50613fec9d4de5480de18d4f8ef59ad7e344d497edbef3cfd80f24f98461/numpy-2.3.5-cp313-cp313t-win_amd64.whl", hash = "sha256:b501b5fa195cc9e24fe102f21ec0a44dffc231d2af79950b451e0d99cea02234", size = 12920341 }, - { url = "https://files.pythonhosted.org/packages/bb/ab/08fd63b9a74303947f34f0bd7c5903b9c5532c2d287bead5bdf4c556c486/numpy-2.3.5-cp313-cp313t-win_arm64.whl", hash = "sha256:a80afd79f45f3c4a7d341f13acbe058d1ca8ac017c165d3fa0d3de6bc1a079d7", size = 10262507 }, - { url = "https://files.pythonhosted.org/packages/ba/97/1a914559c19e32d6b2e233cf9a6a114e67c856d35b1d6babca571a3e880f/numpy-2.3.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:bf06bc2af43fa8d32d30fae16ad965663e966b1a3202ed407b84c989c3221e82", size = 16735706 }, - { url = "https://files.pythonhosted.org/packages/57/d4/51233b1c1b13ecd796311216ae417796b88b0616cfd8a33ae4536330748a/numpy-2.3.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:052e8c42e0c49d2575621c158934920524f6c5da05a1d3b9bab5d8e259e045f0", size = 12264507 }, - { url = "https://files.pythonhosted.org/packages/45/98/2fe46c5c2675b8306d0b4a3ec3494273e93e1226a490f766e84298576956/numpy-2.3.5-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:1ed1ec893cff7040a02c8aa1c8611b94d395590d553f6b53629a4461dc7f7b63", size = 5093049 }, - { url = "https://files.pythonhosted.org/packages/ce/0e/0698378989bb0ac5f1660c81c78ab1fe5476c1a521ca9ee9d0710ce54099/numpy-2.3.5-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:2dcd0808a421a482a080f89859a18beb0b3d1e905b81e617a188bd80422d62e9", size = 6626603 }, - { url = "https://files.pythonhosted.org/packages/5e/a6/9ca0eecc489640615642a6cbc0ca9e10df70df38c4d43f5a928ff18d8827/numpy-2.3.5-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:727fd05b57df37dc0bcf1a27767a3d9a78cbbc92822445f32cc3436ba797337b", size = 14262696 }, - { url = "https://files.pythonhosted.org/packages/c8/f6/07ec185b90ec9d7217a00eeeed7383b73d7e709dae2a9a021b051542a708/numpy-2.3.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fffe29a1ef00883599d1dc2c51aa2e5d80afe49523c261a74933df395c15c520", size = 16597350 }, - { url = "https://files.pythonhosted.org/packages/75/37/164071d1dde6a1a84c9b8e5b414fa127981bad47adf3a6b7e23917e52190/numpy-2.3.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8f7f0e05112916223d3f438f293abf0727e1181b5983f413dfa2fefc4098245c", size = 16040190 }, - { url = "https://files.pythonhosted.org/packages/08/3c/f18b82a406b04859eb026d204e4e1773eb41c5be58410f41ffa511d114ae/numpy-2.3.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2e2eb32ddb9ccb817d620ac1d8dae7c3f641c1e5f55f531a33e8ab97960a75b8", size = 18536749 }, - { url = "https://files.pythonhosted.org/packages/40/79/f82f572bf44cf0023a2fe8588768e23e1592585020d638999f15158609e1/numpy-2.3.5-cp314-cp314-win32.whl", hash = "sha256:66f85ce62c70b843bab1fb14a05d5737741e74e28c7b8b5a064de10142fad248", size = 6335432 }, - { url = "https://files.pythonhosted.org/packages/a3/2e/235b4d96619931192c91660805e5e49242389742a7a82c27665021db690c/numpy-2.3.5-cp314-cp314-win_amd64.whl", hash = "sha256:e6a0bc88393d65807d751a614207b7129a310ca4fe76a74e5c7da5fa5671417e", size = 12919388 }, - { url = "https://files.pythonhosted.org/packages/07/2b/29fd75ce45d22a39c61aad74f3d718e7ab67ccf839ca8b60866054eb15f8/numpy-2.3.5-cp314-cp314-win_arm64.whl", hash = "sha256:aeffcab3d4b43712bb7a60b65f6044d444e75e563ff6180af8f98dd4b905dfd2", size = 10476651 }, - { url = "https://files.pythonhosted.org/packages/17/e1/f6a721234ebd4d87084cfa68d081bcba2f5cfe1974f7de4e0e8b9b2a2ba1/numpy-2.3.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:17531366a2e3a9e30762c000f2c43a9aaa05728712e25c11ce1dbe700c53ad41", size = 16834503 }, - { url = "https://files.pythonhosted.org/packages/5c/1c/baf7ffdc3af9c356e1c135e57ab7cf8d247931b9554f55c467efe2c69eff/numpy-2.3.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d21644de1b609825ede2f48be98dfde4656aefc713654eeee280e37cadc4e0ad", size = 12381612 }, - { url = "https://files.pythonhosted.org/packages/74/91/f7f0295151407ddc9ba34e699013c32c3c91944f9b35fcf9281163dc1468/numpy-2.3.5-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:c804e3a5aba5460c73955c955bdbd5c08c354954e9270a2c1565f62e866bdc39", size = 5210042 }, - { url = "https://files.pythonhosted.org/packages/2e/3b/78aebf345104ec50dd50a4d06ddeb46a9ff5261c33bcc58b1c4f12f85ec2/numpy-2.3.5-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:cc0a57f895b96ec78969c34f682c602bf8da1a0270b09bc65673df2e7638ec20", size = 6724502 }, - { url = "https://files.pythonhosted.org/packages/02/c6/7c34b528740512e57ef1b7c8337ab0b4f0bddf34c723b8996c675bc2bc91/numpy-2.3.5-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:900218e456384ea676e24ea6a0417f030a3b07306d29d7ad843957b40a9d8d52", size = 14308962 }, - { url = "https://files.pythonhosted.org/packages/80/35/09d433c5262bc32d725bafc619e095b6a6651caf94027a03da624146f655/numpy-2.3.5-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:09a1bea522b25109bf8e6f3027bd810f7c1085c64a0c7ce050c1676ad0ba010b", size = 16655054 }, - { url = "https://files.pythonhosted.org/packages/7a/ab/6a7b259703c09a88804fa2430b43d6457b692378f6b74b356155283566ac/numpy-2.3.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:04822c00b5fd0323c8166d66c701dc31b7fbd252c100acd708c48f763968d6a3", size = 16091613 }, - { url = "https://files.pythonhosted.org/packages/c2/88/330da2071e8771e60d1038166ff9d73f29da37b01ec3eb43cb1427464e10/numpy-2.3.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d6889ec4ec662a1a37eb4b4fb26b6100841804dac55bd9df579e326cdc146227", size = 18591147 }, - { url = "https://files.pythonhosted.org/packages/51/41/851c4b4082402d9ea860c3626db5d5df47164a712cb23b54be028b184c1c/numpy-2.3.5-cp314-cp314t-win32.whl", hash = "sha256:93eebbcf1aafdf7e2ddd44c2923e2672e1010bddc014138b229e49725b4d6be5", size = 6479806 }, - { url = "https://files.pythonhosted.org/packages/90/30/d48bde1dfd93332fa557cff1972fbc039e055a52021fbef4c2c4b1eefd17/numpy-2.3.5-cp314-cp314t-win_amd64.whl", hash = "sha256:c8a9958e88b65c3b27e22ca2a076311636850b612d6bbfb76e8d156aacde2aaf", size = 13105760 }, - { url = "https://files.pythonhosted.org/packages/2d/fd/4b5eb0b3e888d86aee4d198c23acec7d214baaf17ea93c1adec94c9518b9/numpy-2.3.5-cp314-cp314t-win_arm64.whl", hash = "sha256:6203fdf9f3dc5bdaed7319ad8698e685c7a3be10819f41d32a0723e611733b42", size = 10545459 }, - { url = "https://files.pythonhosted.org/packages/c6/65/f9dea8e109371ade9c782b4e4756a82edf9d3366bca495d84d79859a0b79/numpy-2.3.5-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f0963b55cdd70fad460fa4c1341f12f976bb26cb66021a5580329bd498988310", size = 16910689 }, - { url = "https://files.pythonhosted.org/packages/00/4f/edb00032a8fb92ec0a679d3830368355da91a69cab6f3e9c21b64d0bb986/numpy-2.3.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f4255143f5160d0de972d28c8f9665d882b5f61309d8362fdd3e103cf7bf010c", size = 12457053 }, - { url = "https://files.pythonhosted.org/packages/16/a4/e8a53b5abd500a63836a29ebe145fc1ab1f2eefe1cfe59276020373ae0aa/numpy-2.3.5-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:a4b9159734b326535f4dd01d947f919c6eefd2d9827466a696c44ced82dfbc18", size = 5285635 }, - { url = "https://files.pythonhosted.org/packages/a3/2f/37eeb9014d9c8b3e9c55bc599c68263ca44fdbc12a93e45a21d1d56df737/numpy-2.3.5-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:2feae0d2c91d46e59fcd62784a3a83b3fb677fead592ce51b5a6fbb4f95965ff", size = 6801770 }, - { url = "https://files.pythonhosted.org/packages/7d/e4/68d2f474df2cb671b2b6c2986a02e520671295647dad82484cde80ca427b/numpy-2.3.5-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffac52f28a7849ad7576293c0cb7b9f08304e8f7d738a8cb8a90ec4c55a998eb", size = 14391768 }, - { url = "https://files.pythonhosted.org/packages/b8/50/94ccd8a2b141cb50651fddd4f6a48874acb3c91c8f0842b08a6afc4b0b21/numpy-2.3.5-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63c0e9e7eea69588479ebf4a8a270d5ac22763cc5854e9a7eae952a3908103f7", size = 16729263 }, - { url = "https://files.pythonhosted.org/packages/2d/ee/346fa473e666fe14c52fcdd19ec2424157290a032d4c41f98127bfb31ac7/numpy-2.3.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f16417ec91f12f814b10bafe79ef77e70113a2f5f7018640e7425ff979253425", size = 12967213 }, +sdist = { url = "https://files.pythonhosted.org/packages/76/65/21b3bc86aac7b8f2862db1e808f1ea22b028e30a225a34a5ede9bf8678f2/numpy-2.3.5.tar.gz", hash = "sha256:784db1dcdab56bf0517743e746dfb0f885fc68d948aba86eeec2cba234bdf1c0", size = 20584950, upload-time = "2025-11-16T22:52:42.067Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/77/84dd1d2e34d7e2792a236ba180b5e8fcc1e3e414e761ce0253f63d7f572e/numpy-2.3.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:de5672f4a7b200c15a4127042170a694d4df43c992948f5e1af57f0174beed10", size = 17034641, upload-time = "2025-11-16T22:49:19.336Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ea/25e26fa5837106cde46ae7d0b667e20f69cbbc0efd64cba8221411ab26ae/numpy-2.3.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:acfd89508504a19ed06ef963ad544ec6664518c863436306153e13e94605c218", size = 12528324, upload-time = "2025-11-16T22:49:22.582Z" }, + { url = "https://files.pythonhosted.org/packages/4d/1a/e85f0eea4cf03d6a0228f5c0256b53f2df4bc794706e7df019fc622e47f1/numpy-2.3.5-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:ffe22d2b05504f786c867c8395de703937f934272eb67586817b46188b4ded6d", size = 5356872, upload-time = "2025-11-16T22:49:25.408Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bb/35ef04afd567f4c989c2060cde39211e4ac5357155c1833bcd1166055c61/numpy-2.3.5-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:872a5cf366aec6bb1147336480fef14c9164b154aeb6542327de4970282cd2f5", size = 6893148, upload-time = "2025-11-16T22:49:27.549Z" }, + { url = "https://files.pythonhosted.org/packages/f2/2b/05bbeb06e2dff5eab512dfc678b1cc5ee94d8ac5956a0885c64b6b26252b/numpy-2.3.5-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3095bdb8dd297e5920b010e96134ed91d852d81d490e787beca7e35ae1d89cf7", size = 14557282, upload-time = "2025-11-16T22:49:30.964Z" }, + { url = "https://files.pythonhosted.org/packages/65/fb/2b23769462b34398d9326081fad5655198fcf18966fcb1f1e49db44fbf31/numpy-2.3.5-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8cba086a43d54ca804ce711b2a940b16e452807acebe7852ff327f1ecd49b0d4", size = 16897903, upload-time = "2025-11-16T22:49:34.191Z" }, + { url = "https://files.pythonhosted.org/packages/ac/14/085f4cf05fc3f1e8aa95e85404e984ffca9b2275a5dc2b1aae18a67538b8/numpy-2.3.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6cf9b429b21df6b99f4dee7a1218b8b7ffbbe7df8764dc0bd60ce8a0708fed1e", size = 16341672, upload-time = "2025-11-16T22:49:37.2Z" }, + { url = "https://files.pythonhosted.org/packages/6f/3b/1f73994904142b2aa290449b3bb99772477b5fd94d787093e4f24f5af763/numpy-2.3.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:396084a36abdb603546b119d96528c2f6263921c50df3c8fd7cb28873a237748", size = 18838896, upload-time = "2025-11-16T22:49:39.727Z" }, + { url = "https://files.pythonhosted.org/packages/cd/b9/cf6649b2124f288309ffc353070792caf42ad69047dcc60da85ee85fea58/numpy-2.3.5-cp311-cp311-win32.whl", hash = "sha256:b0c7088a73aef3d687c4deef8452a3ac7c1be4e29ed8bf3b366c8111128ac60c", size = 6563608, upload-time = "2025-11-16T22:49:42.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/44/9fe81ae1dcc29c531843852e2874080dc441338574ccc4306b39e2ff6e59/numpy-2.3.5-cp311-cp311-win_amd64.whl", hash = "sha256:a414504bef8945eae5f2d7cb7be2d4af77c5d1cb5e20b296c2c25b61dff2900c", size = 13078442, upload-time = "2025-11-16T22:49:43.99Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a7/f99a41553d2da82a20a2f22e93c94f928e4490bb447c9ff3c4ff230581d3/numpy-2.3.5-cp311-cp311-win_arm64.whl", hash = "sha256:0cd00b7b36e35398fa2d16af7b907b65304ef8bb4817a550e06e5012929830fa", size = 10458555, upload-time = "2025-11-16T22:49:47.092Z" }, + { url = "https://files.pythonhosted.org/packages/44/37/e669fe6cbb2b96c62f6bbedc6a81c0f3b7362f6a59230b23caa673a85721/numpy-2.3.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:74ae7b798248fe62021dbf3c914245ad45d1a6b0cb4a29ecb4b31d0bfbc4cc3e", size = 16733873, upload-time = "2025-11-16T22:49:49.84Z" }, + { url = "https://files.pythonhosted.org/packages/c5/65/df0db6c097892c9380851ab9e44b52d4f7ba576b833996e0080181c0c439/numpy-2.3.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee3888d9ff7c14604052b2ca5535a30216aa0a58e948cdd3eeb8d3415f638769", size = 12259838, upload-time = "2025-11-16T22:49:52.863Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e1/1ee06e70eb2136797abe847d386e7c0e830b67ad1d43f364dd04fa50d338/numpy-2.3.5-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:612a95a17655e213502f60cfb9bf9408efdc9eb1d5f50535cc6eb365d11b42b5", size = 5088378, upload-time = "2025-11-16T22:49:55.055Z" }, + { url = "https://files.pythonhosted.org/packages/6d/9c/1ca85fb86708724275103b81ec4cf1ac1d08f465368acfc8da7ab545bdae/numpy-2.3.5-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3101e5177d114a593d79dd79658650fe28b5a0d8abeb8ce6f437c0e6df5be1a4", size = 6628559, upload-time = "2025-11-16T22:49:57.371Z" }, + { url = "https://files.pythonhosted.org/packages/74/78/fcd41e5a0ce4f3f7b003da85825acddae6d7ecb60cf25194741b036ca7d6/numpy-2.3.5-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b973c57ff8e184109db042c842423ff4f60446239bd585a5131cc47f06f789d", size = 14250702, upload-time = "2025-11-16T22:49:59.632Z" }, + { url = "https://files.pythonhosted.org/packages/b6/23/2a1b231b8ff672b4c450dac27164a8b2ca7d9b7144f9c02d2396518352eb/numpy-2.3.5-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d8163f43acde9a73c2a33605353a4f1bc4798745a8b1d73183b28e5b435ae28", size = 16606086, upload-time = "2025-11-16T22:50:02.127Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c5/5ad26fbfbe2012e190cc7d5003e4d874b88bb18861d0829edc140a713021/numpy-2.3.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:51c1e14eb1e154ebd80e860722f9e6ed6ec89714ad2db2d3aa33c31d7c12179b", size = 16025985, upload-time = "2025-11-16T22:50:04.536Z" }, + { url = "https://files.pythonhosted.org/packages/d2/fa/dd48e225c46c819288148d9d060b047fd2a6fb1eb37eae25112ee4cb4453/numpy-2.3.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b46b4ec24f7293f23adcd2d146960559aaf8020213de8ad1909dba6c013bf89c", size = 18542976, upload-time = "2025-11-16T22:50:07.557Z" }, + { url = "https://files.pythonhosted.org/packages/05/79/ccbd23a75862d95af03d28b5c6901a1b7da4803181513d52f3b86ed9446e/numpy-2.3.5-cp312-cp312-win32.whl", hash = "sha256:3997b5b3c9a771e157f9aae01dd579ee35ad7109be18db0e85dbdbe1de06e952", size = 6285274, upload-time = "2025-11-16T22:50:10.746Z" }, + { url = "https://files.pythonhosted.org/packages/2d/57/8aeaf160312f7f489dea47ab61e430b5cb051f59a98ae68b7133ce8fa06a/numpy-2.3.5-cp312-cp312-win_amd64.whl", hash = "sha256:86945f2ee6d10cdfd67bcb4069c1662dd711f7e2a4343db5cecec06b87cf31aa", size = 12782922, upload-time = "2025-11-16T22:50:12.811Z" }, + { url = "https://files.pythonhosted.org/packages/78/a6/aae5cc2ca78c45e64b9ef22f089141d661516856cf7c8a54ba434576900d/numpy-2.3.5-cp312-cp312-win_arm64.whl", hash = "sha256:f28620fe26bee16243be2b7b874da327312240a7cdc38b769a697578d2100013", size = 10194667, upload-time = "2025-11-16T22:50:16.16Z" }, + { url = "https://files.pythonhosted.org/packages/db/69/9cde09f36da4b5a505341180a3f2e6fadc352fd4d2b7096ce9778db83f1a/numpy-2.3.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d0f23b44f57077c1ede8c5f26b30f706498b4862d3ff0a7298b8411dd2f043ff", size = 16728251, upload-time = "2025-11-16T22:50:19.013Z" }, + { url = "https://files.pythonhosted.org/packages/79/fb/f505c95ceddd7027347b067689db71ca80bd5ecc926f913f1a23e65cf09b/numpy-2.3.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa5bc7c5d59d831d9773d1170acac7893ce3a5e130540605770ade83280e7188", size = 12254652, upload-time = "2025-11-16T22:50:21.487Z" }, + { url = "https://files.pythonhosted.org/packages/78/da/8c7738060ca9c31b30e9301ee0cf6c5ffdbf889d9593285a1cead337f9a5/numpy-2.3.5-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:ccc933afd4d20aad3c00bcef049cb40049f7f196e0397f1109dba6fed63267b0", size = 5083172, upload-time = "2025-11-16T22:50:24.562Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b4/ee5bb2537fb9430fd2ef30a616c3672b991a4129bb1c7dcc42aa0abbe5d7/numpy-2.3.5-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:afaffc4393205524af9dfa400fa250143a6c3bc646c08c9f5e25a9f4b4d6a903", size = 6622990, upload-time = "2025-11-16T22:50:26.47Z" }, + { url = "https://files.pythonhosted.org/packages/95/03/dc0723a013c7d7c19de5ef29e932c3081df1c14ba582b8b86b5de9db7f0f/numpy-2.3.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c75442b2209b8470d6d5d8b1c25714270686f14c749028d2199c54e29f20b4d", size = 14248902, upload-time = "2025-11-16T22:50:28.861Z" }, + { url = "https://files.pythonhosted.org/packages/f5/10/ca162f45a102738958dcec8023062dad0cbc17d1ab99d68c4e4a6c45fb2b/numpy-2.3.5-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11e06aa0af8c0f05104d56450d6093ee639e15f24ecf62d417329d06e522e017", size = 16597430, upload-time = "2025-11-16T22:50:31.56Z" }, + { url = "https://files.pythonhosted.org/packages/2a/51/c1e29be863588db58175175f057286900b4b3327a1351e706d5e0f8dd679/numpy-2.3.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ed89927b86296067b4f81f108a2271d8926467a8868e554eaf370fc27fa3ccaf", size = 16024551, upload-time = "2025-11-16T22:50:34.242Z" }, + { url = "https://files.pythonhosted.org/packages/83/68/8236589d4dbb87253d28259d04d9b814ec0ecce7cb1c7fed29729f4c3a78/numpy-2.3.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51c55fe3451421f3a6ef9a9c1439e82101c57a2c9eab9feb196a62b1a10b58ce", size = 18533275, upload-time = "2025-11-16T22:50:37.651Z" }, + { url = "https://files.pythonhosted.org/packages/40/56/2932d75b6f13465239e3b7b7e511be27f1b8161ca2510854f0b6e521c395/numpy-2.3.5-cp313-cp313-win32.whl", hash = "sha256:1978155dd49972084bd6ef388d66ab70f0c323ddee6f693d539376498720fb7e", size = 6277637, upload-time = "2025-11-16T22:50:40.11Z" }, + { url = "https://files.pythonhosted.org/packages/0c/88/e2eaa6cffb115b85ed7c7c87775cb8bcf0816816bc98ca8dbfa2ee33fe6e/numpy-2.3.5-cp313-cp313-win_amd64.whl", hash = "sha256:00dc4e846108a382c5869e77c6ed514394bdeb3403461d25a829711041217d5b", size = 12779090, upload-time = "2025-11-16T22:50:42.503Z" }, + { url = "https://files.pythonhosted.org/packages/8f/88/3f41e13a44ebd4034ee17baa384acac29ba6a4fcc2aca95f6f08ca0447d1/numpy-2.3.5-cp313-cp313-win_arm64.whl", hash = "sha256:0472f11f6ec23a74a906a00b48a4dcf3849209696dff7c189714511268d103ae", size = 10194710, upload-time = "2025-11-16T22:50:44.971Z" }, + { url = "https://files.pythonhosted.org/packages/13/cb/71744144e13389d577f867f745b7df2d8489463654a918eea2eeb166dfc9/numpy-2.3.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:414802f3b97f3c1eef41e530aaba3b3c1620649871d8cb38c6eaff034c2e16bd", size = 16827292, upload-time = "2025-11-16T22:50:47.715Z" }, + { url = "https://files.pythonhosted.org/packages/71/80/ba9dc6f2a4398e7f42b708a7fdc841bb638d353be255655498edbf9a15a8/numpy-2.3.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5ee6609ac3604fa7780e30a03e5e241a7956f8e2fcfe547d51e3afa5247ac47f", size = 12378897, upload-time = "2025-11-16T22:50:51.327Z" }, + { url = "https://files.pythonhosted.org/packages/2e/6d/db2151b9f64264bcceccd51741aa39b50150de9b602d98ecfe7e0c4bff39/numpy-2.3.5-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:86d835afea1eaa143012a2d7a3f45a3adce2d7adc8b4961f0b362214d800846a", size = 5207391, upload-time = "2025-11-16T22:50:54.542Z" }, + { url = "https://files.pythonhosted.org/packages/80/ae/429bacace5ccad48a14c4ae5332f6aa8ab9f69524193511d60ccdfdc65fa/numpy-2.3.5-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:30bc11310e8153ca664b14c5f1b73e94bd0503681fcf136a163de856f3a50139", size = 6721275, upload-time = "2025-11-16T22:50:56.794Z" }, + { url = "https://files.pythonhosted.org/packages/74/5b/1919abf32d8722646a38cd527bc3771eb229a32724ee6ba340ead9b92249/numpy-2.3.5-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1062fde1dcf469571705945b0f221b73928f34a20c904ffb45db101907c3454e", size = 14306855, upload-time = "2025-11-16T22:50:59.208Z" }, + { url = "https://files.pythonhosted.org/packages/a5/87/6831980559434973bebc30cd9c1f21e541a0f2b0c280d43d3afd909b66d0/numpy-2.3.5-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce581db493ea1a96c0556360ede6607496e8bf9b3a8efa66e06477267bc831e9", size = 16657359, upload-time = "2025-11-16T22:51:01.991Z" }, + { url = "https://files.pythonhosted.org/packages/dd/91/c797f544491ee99fd00495f12ebb7802c440c1915811d72ac5b4479a3356/numpy-2.3.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:cc8920d2ec5fa99875b670bb86ddeb21e295cb07aa331810d9e486e0b969d946", size = 16093374, upload-time = "2025-11-16T22:51:05.291Z" }, + { url = "https://files.pythonhosted.org/packages/74/a6/54da03253afcbe7a72785ec4da9c69fb7a17710141ff9ac5fcb2e32dbe64/numpy-2.3.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9ee2197ef8c4f0dfe405d835f3b6a14f5fee7782b5de51ba06fb65fc9b36e9f1", size = 18594587, upload-time = "2025-11-16T22:51:08.585Z" }, + { url = "https://files.pythonhosted.org/packages/80/e9/aff53abbdd41b0ecca94285f325aff42357c6b5abc482a3fcb4994290b18/numpy-2.3.5-cp313-cp313t-win32.whl", hash = "sha256:70b37199913c1bd300ff6e2693316c6f869c7ee16378faf10e4f5e3275b299c3", size = 6405940, upload-time = "2025-11-16T22:51:11.541Z" }, + { url = "https://files.pythonhosted.org/packages/d5/81/50613fec9d4de5480de18d4f8ef59ad7e344d497edbef3cfd80f24f98461/numpy-2.3.5-cp313-cp313t-win_amd64.whl", hash = "sha256:b501b5fa195cc9e24fe102f21ec0a44dffc231d2af79950b451e0d99cea02234", size = 12920341, upload-time = "2025-11-16T22:51:14.312Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ab/08fd63b9a74303947f34f0bd7c5903b9c5532c2d287bead5bdf4c556c486/numpy-2.3.5-cp313-cp313t-win_arm64.whl", hash = "sha256:a80afd79f45f3c4a7d341f13acbe058d1ca8ac017c165d3fa0d3de6bc1a079d7", size = 10262507, upload-time = "2025-11-16T22:51:16.846Z" }, + { url = "https://files.pythonhosted.org/packages/ba/97/1a914559c19e32d6b2e233cf9a6a114e67c856d35b1d6babca571a3e880f/numpy-2.3.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:bf06bc2af43fa8d32d30fae16ad965663e966b1a3202ed407b84c989c3221e82", size = 16735706, upload-time = "2025-11-16T22:51:19.558Z" }, + { url = "https://files.pythonhosted.org/packages/57/d4/51233b1c1b13ecd796311216ae417796b88b0616cfd8a33ae4536330748a/numpy-2.3.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:052e8c42e0c49d2575621c158934920524f6c5da05a1d3b9bab5d8e259e045f0", size = 12264507, upload-time = "2025-11-16T22:51:22.492Z" }, + { url = "https://files.pythonhosted.org/packages/45/98/2fe46c5c2675b8306d0b4a3ec3494273e93e1226a490f766e84298576956/numpy-2.3.5-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:1ed1ec893cff7040a02c8aa1c8611b94d395590d553f6b53629a4461dc7f7b63", size = 5093049, upload-time = "2025-11-16T22:51:25.171Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0e/0698378989bb0ac5f1660c81c78ab1fe5476c1a521ca9ee9d0710ce54099/numpy-2.3.5-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:2dcd0808a421a482a080f89859a18beb0b3d1e905b81e617a188bd80422d62e9", size = 6626603, upload-time = "2025-11-16T22:51:27Z" }, + { url = "https://files.pythonhosted.org/packages/5e/a6/9ca0eecc489640615642a6cbc0ca9e10df70df38c4d43f5a928ff18d8827/numpy-2.3.5-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:727fd05b57df37dc0bcf1a27767a3d9a78cbbc92822445f32cc3436ba797337b", size = 14262696, upload-time = "2025-11-16T22:51:29.402Z" }, + { url = "https://files.pythonhosted.org/packages/c8/f6/07ec185b90ec9d7217a00eeeed7383b73d7e709dae2a9a021b051542a708/numpy-2.3.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fffe29a1ef00883599d1dc2c51aa2e5d80afe49523c261a74933df395c15c520", size = 16597350, upload-time = "2025-11-16T22:51:32.167Z" }, + { url = "https://files.pythonhosted.org/packages/75/37/164071d1dde6a1a84c9b8e5b414fa127981bad47adf3a6b7e23917e52190/numpy-2.3.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8f7f0e05112916223d3f438f293abf0727e1181b5983f413dfa2fefc4098245c", size = 16040190, upload-time = "2025-11-16T22:51:35.403Z" }, + { url = "https://files.pythonhosted.org/packages/08/3c/f18b82a406b04859eb026d204e4e1773eb41c5be58410f41ffa511d114ae/numpy-2.3.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2e2eb32ddb9ccb817d620ac1d8dae7c3f641c1e5f55f531a33e8ab97960a75b8", size = 18536749, upload-time = "2025-11-16T22:51:39.698Z" }, + { url = "https://files.pythonhosted.org/packages/40/79/f82f572bf44cf0023a2fe8588768e23e1592585020d638999f15158609e1/numpy-2.3.5-cp314-cp314-win32.whl", hash = "sha256:66f85ce62c70b843bab1fb14a05d5737741e74e28c7b8b5a064de10142fad248", size = 6335432, upload-time = "2025-11-16T22:51:42.476Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2e/235b4d96619931192c91660805e5e49242389742a7a82c27665021db690c/numpy-2.3.5-cp314-cp314-win_amd64.whl", hash = "sha256:e6a0bc88393d65807d751a614207b7129a310ca4fe76a74e5c7da5fa5671417e", size = 12919388, upload-time = "2025-11-16T22:51:45.275Z" }, + { url = "https://files.pythonhosted.org/packages/07/2b/29fd75ce45d22a39c61aad74f3d718e7ab67ccf839ca8b60866054eb15f8/numpy-2.3.5-cp314-cp314-win_arm64.whl", hash = "sha256:aeffcab3d4b43712bb7a60b65f6044d444e75e563ff6180af8f98dd4b905dfd2", size = 10476651, upload-time = "2025-11-16T22:51:47.749Z" }, + { url = "https://files.pythonhosted.org/packages/17/e1/f6a721234ebd4d87084cfa68d081bcba2f5cfe1974f7de4e0e8b9b2a2ba1/numpy-2.3.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:17531366a2e3a9e30762c000f2c43a9aaa05728712e25c11ce1dbe700c53ad41", size = 16834503, upload-time = "2025-11-16T22:51:50.443Z" }, + { url = "https://files.pythonhosted.org/packages/5c/1c/baf7ffdc3af9c356e1c135e57ab7cf8d247931b9554f55c467efe2c69eff/numpy-2.3.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d21644de1b609825ede2f48be98dfde4656aefc713654eeee280e37cadc4e0ad", size = 12381612, upload-time = "2025-11-16T22:51:53.609Z" }, + { url = "https://files.pythonhosted.org/packages/74/91/f7f0295151407ddc9ba34e699013c32c3c91944f9b35fcf9281163dc1468/numpy-2.3.5-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:c804e3a5aba5460c73955c955bdbd5c08c354954e9270a2c1565f62e866bdc39", size = 5210042, upload-time = "2025-11-16T22:51:56.213Z" }, + { url = "https://files.pythonhosted.org/packages/2e/3b/78aebf345104ec50dd50a4d06ddeb46a9ff5261c33bcc58b1c4f12f85ec2/numpy-2.3.5-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:cc0a57f895b96ec78969c34f682c602bf8da1a0270b09bc65673df2e7638ec20", size = 6724502, upload-time = "2025-11-16T22:51:58.584Z" }, + { url = "https://files.pythonhosted.org/packages/02/c6/7c34b528740512e57ef1b7c8337ab0b4f0bddf34c723b8996c675bc2bc91/numpy-2.3.5-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:900218e456384ea676e24ea6a0417f030a3b07306d29d7ad843957b40a9d8d52", size = 14308962, upload-time = "2025-11-16T22:52:01.698Z" }, + { url = "https://files.pythonhosted.org/packages/80/35/09d433c5262bc32d725bafc619e095b6a6651caf94027a03da624146f655/numpy-2.3.5-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:09a1bea522b25109bf8e6f3027bd810f7c1085c64a0c7ce050c1676ad0ba010b", size = 16655054, upload-time = "2025-11-16T22:52:04.267Z" }, + { url = "https://files.pythonhosted.org/packages/7a/ab/6a7b259703c09a88804fa2430b43d6457b692378f6b74b356155283566ac/numpy-2.3.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:04822c00b5fd0323c8166d66c701dc31b7fbd252c100acd708c48f763968d6a3", size = 16091613, upload-time = "2025-11-16T22:52:08.651Z" }, + { url = "https://files.pythonhosted.org/packages/c2/88/330da2071e8771e60d1038166ff9d73f29da37b01ec3eb43cb1427464e10/numpy-2.3.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d6889ec4ec662a1a37eb4b4fb26b6100841804dac55bd9df579e326cdc146227", size = 18591147, upload-time = "2025-11-16T22:52:11.453Z" }, + { url = "https://files.pythonhosted.org/packages/51/41/851c4b4082402d9ea860c3626db5d5df47164a712cb23b54be028b184c1c/numpy-2.3.5-cp314-cp314t-win32.whl", hash = "sha256:93eebbcf1aafdf7e2ddd44c2923e2672e1010bddc014138b229e49725b4d6be5", size = 6479806, upload-time = "2025-11-16T22:52:14.641Z" }, + { url = "https://files.pythonhosted.org/packages/90/30/d48bde1dfd93332fa557cff1972fbc039e055a52021fbef4c2c4b1eefd17/numpy-2.3.5-cp314-cp314t-win_amd64.whl", hash = "sha256:c8a9958e88b65c3b27e22ca2a076311636850b612d6bbfb76e8d156aacde2aaf", size = 13105760, upload-time = "2025-11-16T22:52:17.975Z" }, + { url = "https://files.pythonhosted.org/packages/2d/fd/4b5eb0b3e888d86aee4d198c23acec7d214baaf17ea93c1adec94c9518b9/numpy-2.3.5-cp314-cp314t-win_arm64.whl", hash = "sha256:6203fdf9f3dc5bdaed7319ad8698e685c7a3be10819f41d32a0723e611733b42", size = 10545459, upload-time = "2025-11-16T22:52:20.55Z" }, + { url = "https://files.pythonhosted.org/packages/c6/65/f9dea8e109371ade9c782b4e4756a82edf9d3366bca495d84d79859a0b79/numpy-2.3.5-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f0963b55cdd70fad460fa4c1341f12f976bb26cb66021a5580329bd498988310", size = 16910689, upload-time = "2025-11-16T22:52:23.247Z" }, + { url = "https://files.pythonhosted.org/packages/00/4f/edb00032a8fb92ec0a679d3830368355da91a69cab6f3e9c21b64d0bb986/numpy-2.3.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f4255143f5160d0de972d28c8f9665d882b5f61309d8362fdd3e103cf7bf010c", size = 12457053, upload-time = "2025-11-16T22:52:26.367Z" }, + { url = "https://files.pythonhosted.org/packages/16/a4/e8a53b5abd500a63836a29ebe145fc1ab1f2eefe1cfe59276020373ae0aa/numpy-2.3.5-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:a4b9159734b326535f4dd01d947f919c6eefd2d9827466a696c44ced82dfbc18", size = 5285635, upload-time = "2025-11-16T22:52:29.266Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2f/37eeb9014d9c8b3e9c55bc599c68263ca44fdbc12a93e45a21d1d56df737/numpy-2.3.5-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:2feae0d2c91d46e59fcd62784a3a83b3fb677fead592ce51b5a6fbb4f95965ff", size = 6801770, upload-time = "2025-11-16T22:52:31.421Z" }, + { url = "https://files.pythonhosted.org/packages/7d/e4/68d2f474df2cb671b2b6c2986a02e520671295647dad82484cde80ca427b/numpy-2.3.5-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffac52f28a7849ad7576293c0cb7b9f08304e8f7d738a8cb8a90ec4c55a998eb", size = 14391768, upload-time = "2025-11-16T22:52:33.593Z" }, + { url = "https://files.pythonhosted.org/packages/b8/50/94ccd8a2b141cb50651fddd4f6a48874acb3c91c8f0842b08a6afc4b0b21/numpy-2.3.5-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63c0e9e7eea69588479ebf4a8a270d5ac22763cc5854e9a7eae952a3908103f7", size = 16729263, upload-time = "2025-11-16T22:52:36.369Z" }, + { url = "https://files.pythonhosted.org/packages/2d/ee/346fa473e666fe14c52fcdd19ec2424157290a032d4c41f98127bfb31ac7/numpy-2.3.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f16417ec91f12f814b10bafe79ef77e70113a2f5f7018640e7425ff979253425", size = 12967213, upload-time = "2025-11-16T22:52:39.38Z" }, ] [[package]] name = "packaging" version = "25.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727 } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 }, + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, ] [[package]] name = "paginate" version = "0.5.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252 } +sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252, upload-time = "2024-08-25T14:17:24.139Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746 }, + { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" }, ] [[package]] @@ -810,75 +810,75 @@ dependencies = [ { name = "pytz" }, { name = "tzdata" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/fa/7ac648108144a095b4fb6aa3de1954689f7af60a14cf25583f4960ecb878/pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523", size = 11578790 }, - { url = "https://files.pythonhosted.org/packages/9b/35/74442388c6cf008882d4d4bdfc4109be87e9b8b7ccd097ad1e7f006e2e95/pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45", size = 10833831 }, - { url = "https://files.pythonhosted.org/packages/fe/e4/de154cbfeee13383ad58d23017da99390b91d73f8c11856f2095e813201b/pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66", size = 12199267 }, - { url = "https://files.pythonhosted.org/packages/bf/c9/63f8d545568d9ab91476b1818b4741f521646cbdd151c6efebf40d6de6f7/pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b", size = 12789281 }, - { url = "https://files.pythonhosted.org/packages/f2/00/a5ac8c7a0e67fd1a6059e40aa08fa1c52cc00709077d2300e210c3ce0322/pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791", size = 13240453 }, - { url = "https://files.pythonhosted.org/packages/27/4d/5c23a5bc7bd209231618dd9e606ce076272c9bc4f12023a70e03a86b4067/pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151", size = 13890361 }, - { url = "https://files.pythonhosted.org/packages/8e/59/712db1d7040520de7a4965df15b774348980e6df45c129b8c64d0dbe74ef/pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c", size = 11348702 }, - { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846 }, - { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618 }, - { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212 }, - { url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693 }, - { url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002 }, - { url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971 }, - { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722 }, - { url = "https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", size = 11544671 }, - { url = "https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", size = 10680807 }, - { url = "https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", size = 11709872 }, - { url = "https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", size = 12306371 }, - { url = "https://files.pythonhosted.org/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", size = 12765333 }, - { url = "https://files.pythonhosted.org/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", size = 13418120 }, - { url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991 }, - { url = "https://files.pythonhosted.org/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", size = 12048227 }, - { url = "https://files.pythonhosted.org/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", size = 11411056 }, - { url = "https://files.pythonhosted.org/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", size = 11645189 }, - { url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912 }, - { url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160 }, - { url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233 }, - { url = "https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0", size = 11540635 }, - { url = "https://files.pythonhosted.org/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593", size = 10759079 }, - { url = "https://files.pythonhosted.org/packages/ca/05/d01ef80a7a3a12b2f8bbf16daba1e17c98a2f039cbc8e2f77a2c5a63d382/pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c", size = 11814049 }, - { url = "https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b", size = 12332638 }, - { url = "https://files.pythonhosted.org/packages/c5/33/dd70400631b62b9b29c3c93d2feee1d0964dc2bae2e5ad7a6c73a7f25325/pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6", size = 12886834 }, - { url = "https://files.pythonhosted.org/packages/d3/18/b5d48f55821228d0d2692b34fd5034bb185e854bdb592e9c640f6290e012/pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3", size = 13409925 }, - { url = "https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", size = 11109071 }, - { url = "https://files.pythonhosted.org/packages/89/9c/0e21c895c38a157e0faa1fb64587a9226d6dd46452cac4532d80c3c4a244/pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec", size = 12048504 }, - { url = "https://files.pythonhosted.org/packages/d7/82/b69a1c95df796858777b68fbe6a81d37443a33319761d7c652ce77797475/pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7", size = 11410702 }, - { url = "https://files.pythonhosted.org/packages/f9/88/702bde3ba0a94b8c73a0181e05144b10f13f29ebfc2150c3a79062a8195d/pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450", size = 11634535 }, - { url = "https://files.pythonhosted.org/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", size = 12121582 }, - { url = "https://files.pythonhosted.org/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", size = 12699963 }, - { 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 }, +sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/fa/7ac648108144a095b4fb6aa3de1954689f7af60a14cf25583f4960ecb878/pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523", size = 11578790, upload-time = "2025-09-29T23:18:30.065Z" }, + { url = "https://files.pythonhosted.org/packages/9b/35/74442388c6cf008882d4d4bdfc4109be87e9b8b7ccd097ad1e7f006e2e95/pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45", size = 10833831, upload-time = "2025-09-29T23:38:56.071Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e4/de154cbfeee13383ad58d23017da99390b91d73f8c11856f2095e813201b/pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66", size = 12199267, upload-time = "2025-09-29T23:18:41.627Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c9/63f8d545568d9ab91476b1818b4741f521646cbdd151c6efebf40d6de6f7/pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b", size = 12789281, upload-time = "2025-09-29T23:18:56.834Z" }, + { url = "https://files.pythonhosted.org/packages/f2/00/a5ac8c7a0e67fd1a6059e40aa08fa1c52cc00709077d2300e210c3ce0322/pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791", size = 13240453, upload-time = "2025-09-29T23:19:09.247Z" }, + { url = "https://files.pythonhosted.org/packages/27/4d/5c23a5bc7bd209231618dd9e606ce076272c9bc4f12023a70e03a86b4067/pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151", size = 13890361, upload-time = "2025-09-29T23:19:25.342Z" }, + { url = "https://files.pythonhosted.org/packages/8e/59/712db1d7040520de7a4965df15b774348980e6df45c129b8c64d0dbe74ef/pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c", size = 11348702, upload-time = "2025-09-29T23:19:38.296Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846, upload-time = "2025-09-29T23:19:48.856Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618, upload-time = "2025-09-29T23:39:08.659Z" }, + { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212, upload-time = "2025-09-29T23:19:59.765Z" }, + { url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693, upload-time = "2025-09-29T23:20:14.098Z" }, + { url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002, upload-time = "2025-09-29T23:20:26.76Z" }, + { url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971, upload-time = "2025-09-29T23:20:41.344Z" }, + { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722, upload-time = "2025-09-29T23:20:54.139Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", size = 11544671, upload-time = "2025-09-29T23:21:05.024Z" }, + { url = "https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", size = 10680807, upload-time = "2025-09-29T23:21:15.979Z" }, + { url = "https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", size = 11709872, upload-time = "2025-09-29T23:21:27.165Z" }, + { url = "https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", size = 12306371, upload-time = "2025-09-29T23:21:40.532Z" }, + { url = "https://files.pythonhosted.org/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", size = 12765333, upload-time = "2025-09-29T23:21:55.77Z" }, + { url = "https://files.pythonhosted.org/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", size = 13418120, upload-time = "2025-09-29T23:22:10.109Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991, upload-time = "2025-09-29T23:25:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", size = 12048227, upload-time = "2025-09-29T23:22:24.343Z" }, + { url = "https://files.pythonhosted.org/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", size = 11411056, upload-time = "2025-09-29T23:22:37.762Z" }, + { url = "https://files.pythonhosted.org/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", size = 11645189, upload-time = "2025-09-29T23:22:51.688Z" }, + { url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912, upload-time = "2025-09-29T23:23:05.042Z" }, + { url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160, upload-time = "2025-09-29T23:23:28.57Z" }, + { url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233, upload-time = "2025-09-29T23:24:24.876Z" }, + { url = "https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0", size = 11540635, upload-time = "2025-09-29T23:25:52.486Z" }, + { url = "https://files.pythonhosted.org/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593", size = 10759079, upload-time = "2025-09-29T23:26:33.204Z" }, + { url = "https://files.pythonhosted.org/packages/ca/05/d01ef80a7a3a12b2f8bbf16daba1e17c98a2f039cbc8e2f77a2c5a63d382/pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c", size = 11814049, upload-time = "2025-09-29T23:27:15.384Z" }, + { url = "https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b", size = 12332638, upload-time = "2025-09-29T23:27:51.625Z" }, + { url = "https://files.pythonhosted.org/packages/c5/33/dd70400631b62b9b29c3c93d2feee1d0964dc2bae2e5ad7a6c73a7f25325/pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6", size = 12886834, upload-time = "2025-09-29T23:28:21.289Z" }, + { url = "https://files.pythonhosted.org/packages/d3/18/b5d48f55821228d0d2692b34fd5034bb185e854bdb592e9c640f6290e012/pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3", size = 13409925, upload-time = "2025-09-29T23:28:58.261Z" }, + { url = "https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", size = 11109071, upload-time = "2025-09-29T23:32:27.484Z" }, + { url = "https://files.pythonhosted.org/packages/89/9c/0e21c895c38a157e0faa1fb64587a9226d6dd46452cac4532d80c3c4a244/pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec", size = 12048504, upload-time = "2025-09-29T23:29:31.47Z" }, + { url = "https://files.pythonhosted.org/packages/d7/82/b69a1c95df796858777b68fbe6a81d37443a33319761d7c652ce77797475/pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7", size = 11410702, upload-time = "2025-09-29T23:29:54.591Z" }, + { url = "https://files.pythonhosted.org/packages/f9/88/702bde3ba0a94b8c73a0181e05144b10f13f29ebfc2150c3a79062a8195d/pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450", size = 11634535, upload-time = "2025-09-29T23:30:21.003Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", size = 12121582, upload-time = "2025-09-29T23:30:43.391Z" }, + { url = "https://files.pythonhosted.org/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", size = 12699963, upload-time = "2025-09-29T23:31:10.009Z" }, + { 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 = "pathspec" version = "0.12.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 }, + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, ] [[package]] name = "platformdirs" version = "4.5.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/61/33/9611380c2bdb1225fdef633e2a9610622310fed35ab11dac9620972ee088/platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312", size = 21632 } +sdist = { url = "https://files.pythonhosted.org/packages/61/33/9611380c2bdb1225fdef633e2a9610622310fed35ab11dac9620972ee088/platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312", size = 21632, upload-time = "2025-10-08T17:44:48.791Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3", size = 18651 }, + { url = "https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3", size = 18651, upload-time = "2025-10-08T17:44:47.223Z" }, ] [[package]] name = "pluggy" version = "1.6.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412 } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 }, + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] [[package]] @@ -888,23 +888,23 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "polars-runtime-32" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fa/43/09d4738aa24394751cb7e5d1fc4b5ef461d796efcadd9d00c79578332063/polars-1.35.2.tar.gz", hash = "sha256:ae458b05ca6e7ca2c089342c70793f92f1103c502dc1b14b56f0a04f2cc1d205", size = 694895 } +sdist = { url = "https://files.pythonhosted.org/packages/fa/43/09d4738aa24394751cb7e5d1fc4b5ef461d796efcadd9d00c79578332063/polars-1.35.2.tar.gz", hash = "sha256:ae458b05ca6e7ca2c089342c70793f92f1103c502dc1b14b56f0a04f2cc1d205", size = 694895, upload-time = "2025-11-09T13:20:05.921Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/9a/24e4b890c7ee4358964aa92c4d1865df0e8831f7df6abaa3a39914521724/polars-1.35.2-py3-none-any.whl", hash = "sha256:5e8057c8289ac148c793478323b726faea933d9776bd6b8a554b0ab7c03db87e", size = 783597 }, + { url = "https://files.pythonhosted.org/packages/b4/9a/24e4b890c7ee4358964aa92c4d1865df0e8831f7df6abaa3a39914521724/polars-1.35.2-py3-none-any.whl", hash = "sha256:5e8057c8289ac148c793478323b726faea933d9776bd6b8a554b0ab7c03db87e", size = 783597, upload-time = "2025-11-09T13:18:51.361Z" }, ] [[package]] name = "polars-runtime-32" version = "1.35.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cb/75/ac1256ace28c832a0997b20ba9d10a9d3739bd4d457c1eb1e7d196b6f88b/polars_runtime_32-1.35.2.tar.gz", hash = "sha256:6e6e35733ec52abe54b7d30d245e6586b027d433315d20edfb4a5d162c79fe90", size = 2694387 } +sdist = { url = "https://files.pythonhosted.org/packages/cb/75/ac1256ace28c832a0997b20ba9d10a9d3739bd4d457c1eb1e7d196b6f88b/polars_runtime_32-1.35.2.tar.gz", hash = "sha256:6e6e35733ec52abe54b7d30d245e6586b027d433315d20edfb4a5d162c79fe90", size = 2694387, upload-time = "2025-11-09T13:20:07.624Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/66/de/a532b81e68e636483a5dd764d72e106215543f3ef49a142272b277ada8fe/polars_runtime_32-1.35.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e465d12a29e8df06ea78947e50bd361cdf77535cd904fd562666a8a9374e7e3a", size = 40524507 }, - { url = "https://files.pythonhosted.org/packages/2d/0b/679751ea6aeaa7b3e33a70ba17f9c8150310792583f3ecf9bb1ce15fe15c/polars_runtime_32-1.35.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ef2b029b78f64fb53f126654c0bfa654045c7546bd0de3009d08bd52d660e8cc", size = 36700154 }, - { url = "https://files.pythonhosted.org/packages/e2/c8/fd9f48dd6b89ae9cff53d896b51d08579ef9c739e46ea87a647b376c8ca2/polars_runtime_32-1.35.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85dda0994b5dff7f456bb2f4bbd22be9a9e5c5e28670e23fedb13601ec99a46d", size = 41317788 }, - { url = "https://files.pythonhosted.org/packages/67/89/e09d9897a70b607e22a36c9eae85a5b829581108fd1e3d4292e5c0f52939/polars_runtime_32-1.35.2-cp39-abi3-manylinux_2_24_aarch64.whl", hash = "sha256:3b9006902fc51b768ff747c0f74bd4ce04005ee8aeb290ce9c07ce1cbe1b58a9", size = 37850590 }, - { url = "https://files.pythonhosted.org/packages/dc/40/96a808ca5cc8707894e196315227f04a0c82136b7fb25570bc51ea33b88d/polars_runtime_32-1.35.2-cp39-abi3-win_amd64.whl", hash = "sha256:ddc015fac39735592e2e7c834c02193ba4d257bb4c8c7478b9ebe440b0756b84", size = 41290019 }, - { url = "https://files.pythonhosted.org/packages/f4/d1/8d1b28d007da43c750367c8bf5cb0f22758c16b1104b2b73b9acadb2d17a/polars_runtime_32-1.35.2-cp39-abi3-win_arm64.whl", hash = "sha256:6861145aa321a44eda7cc6694fb7751cb7aa0f21026df51b5faa52e64f9dc39b", size = 36955684 }, + { url = "https://files.pythonhosted.org/packages/66/de/a532b81e68e636483a5dd764d72e106215543f3ef49a142272b277ada8fe/polars_runtime_32-1.35.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e465d12a29e8df06ea78947e50bd361cdf77535cd904fd562666a8a9374e7e3a", size = 40524507, upload-time = "2025-11-09T13:18:55.727Z" }, + { url = "https://files.pythonhosted.org/packages/2d/0b/679751ea6aeaa7b3e33a70ba17f9c8150310792583f3ecf9bb1ce15fe15c/polars_runtime_32-1.35.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ef2b029b78f64fb53f126654c0bfa654045c7546bd0de3009d08bd52d660e8cc", size = 36700154, upload-time = "2025-11-09T13:18:59.78Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c8/fd9f48dd6b89ae9cff53d896b51d08579ef9c739e46ea87a647b376c8ca2/polars_runtime_32-1.35.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85dda0994b5dff7f456bb2f4bbd22be9a9e5c5e28670e23fedb13601ec99a46d", size = 41317788, upload-time = "2025-11-09T13:19:03.949Z" }, + { url = "https://files.pythonhosted.org/packages/67/89/e09d9897a70b607e22a36c9eae85a5b829581108fd1e3d4292e5c0f52939/polars_runtime_32-1.35.2-cp39-abi3-manylinux_2_24_aarch64.whl", hash = "sha256:3b9006902fc51b768ff747c0f74bd4ce04005ee8aeb290ce9c07ce1cbe1b58a9", size = 37850590, upload-time = "2025-11-09T13:19:08.154Z" }, + { url = "https://files.pythonhosted.org/packages/dc/40/96a808ca5cc8707894e196315227f04a0c82136b7fb25570bc51ea33b88d/polars_runtime_32-1.35.2-cp39-abi3-win_amd64.whl", hash = "sha256:ddc015fac39735592e2e7c834c02193ba4d257bb4c8c7478b9ebe440b0756b84", size = 41290019, upload-time = "2025-11-09T13:19:12.214Z" }, + { url = "https://files.pythonhosted.org/packages/f4/d1/8d1b28d007da43c750367c8bf5cb0f22758c16b1104b2b73b9acadb2d17a/polars_runtime_32-1.35.2-cp39-abi3-win_arm64.whl", hash = "sha256:6861145aa321a44eda7cc6694fb7751cb7aa0f21026df51b5faa52e64f9dc39b", size = 36955684, upload-time = "2025-11-09T13:19:15.666Z" }, ] [[package]] @@ -918,59 +918,59 @@ dependencies = [ { name = "pyyaml" }, { name = "virtualenv" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f4/9b/6a4ffb4ed980519da959e1cf3122fc6cb41211daa58dbae1c73c0e519a37/pre_commit-4.5.0.tar.gz", hash = "sha256:dc5a065e932b19fc1d4c653c6939068fe54325af8e741e74e88db4d28a4dd66b", size = 198428 } +sdist = { url = "https://files.pythonhosted.org/packages/f4/9b/6a4ffb4ed980519da959e1cf3122fc6cb41211daa58dbae1c73c0e519a37/pre_commit-4.5.0.tar.gz", hash = "sha256:dc5a065e932b19fc1d4c653c6939068fe54325af8e741e74e88db4d28a4dd66b", size = 198428, upload-time = "2025-11-22T21:02:42.304Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/c4/b2d28e9d2edf4f1713eb3c29307f1a63f3d67cf09bdda29715a36a68921a/pre_commit-4.5.0-py2.py3-none-any.whl", hash = "sha256:25e2ce09595174d9c97860a95609f9f852c0614ba602de3561e267547f2335e1", size = 226429 }, + { url = "https://files.pythonhosted.org/packages/5d/c4/b2d28e9d2edf4f1713eb3c29307f1a63f3d67cf09bdda29715a36a68921a/pre_commit-4.5.0-py2.py3-none-any.whl", hash = "sha256:25e2ce09595174d9c97860a95609f9f852c0614ba602de3561e267547f2335e1", size = 226429, upload-time = "2025-11-22T21:02:40.836Z" }, ] [[package]] name = "pyarrow" version = "22.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/30/53/04a7fdc63e6056116c9ddc8b43bc28c12cdd181b85cbeadb79278475f3ae/pyarrow-22.0.0.tar.gz", hash = "sha256:3d600dc583260d845c7d8a6db540339dd883081925da2bd1c5cb808f720b3cd9", size = 1151151 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/b7/18f611a8cdc43417f9394a3ccd3eace2f32183c08b9eddc3d17681819f37/pyarrow-22.0.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:3e294c5eadfb93d78b0763e859a0c16d4051fc1c5231ae8956d61cb0b5666f5a", size = 34272022 }, - { url = "https://files.pythonhosted.org/packages/26/5c/f259e2526c67eb4b9e511741b19870a02363a47a35edbebc55c3178db22d/pyarrow-22.0.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:69763ab2445f632d90b504a815a2a033f74332997052b721002298ed6de40f2e", size = 35995834 }, - { url = "https://files.pythonhosted.org/packages/50/8d/281f0f9b9376d4b7f146913b26fac0aa2829cd1ee7e997f53a27411bbb92/pyarrow-22.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:b41f37cabfe2463232684de44bad753d6be08a7a072f6a83447eeaf0e4d2a215", size = 45030348 }, - { url = "https://files.pythonhosted.org/packages/f5/e5/53c0a1c428f0976bf22f513d79c73000926cb00b9c138d8e02daf2102e18/pyarrow-22.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:35ad0f0378c9359b3f297299c3309778bb03b8612f987399a0333a560b43862d", size = 47699480 }, - { url = "https://files.pythonhosted.org/packages/95/e1/9dbe4c465c3365959d183e6345d0a8d1dc5b02ca3f8db4760b3bc834cf25/pyarrow-22.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8382ad21458075c2e66a82a29d650f963ce51c7708c7c0ff313a8c206c4fd5e8", size = 48011148 }, - { url = "https://files.pythonhosted.org/packages/c5/b4/7caf5d21930061444c3cf4fa7535c82faf5263e22ce43af7c2759ceb5b8b/pyarrow-22.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1a812a5b727bc09c3d7ea072c4eebf657c2f7066155506ba31ebf4792f88f016", size = 50276964 }, - { url = "https://files.pythonhosted.org/packages/ae/f3/cec89bd99fa3abf826f14d4e53d3d11340ce6f6af4d14bdcd54cd83b6576/pyarrow-22.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:ec5d40dd494882704fb876c16fa7261a69791e784ae34e6b5992e977bd2e238c", size = 28106517 }, - { url = "https://files.pythonhosted.org/packages/af/63/ba23862d69652f85b615ca14ad14f3bcfc5bf1b99ef3f0cd04ff93fdad5a/pyarrow-22.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:bea79263d55c24a32b0d79c00a1c58bb2ee5f0757ed95656b01c0fb310c5af3d", size = 34211578 }, - { url = "https://files.pythonhosted.org/packages/b1/d0/f9ad86fe809efd2bcc8be32032fa72e8b0d112b01ae56a053006376c5930/pyarrow-22.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:12fe549c9b10ac98c91cf791d2945e878875d95508e1a5d14091a7aaa66d9cf8", size = 35989906 }, - { url = "https://files.pythonhosted.org/packages/b4/a8/f910afcb14630e64d673f15904ec27dd31f1e009b77033c365c84e8c1e1d/pyarrow-22.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:334f900ff08ce0423407af97e6c26ad5d4e3b0763645559ece6fbf3747d6a8f5", size = 45021677 }, - { url = "https://files.pythonhosted.org/packages/13/95/aec81f781c75cd10554dc17a25849c720d54feafb6f7847690478dcf5ef8/pyarrow-22.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:c6c791b09c57ed76a18b03f2631753a4960eefbbca80f846da8baefc6491fcfe", size = 47726315 }, - { url = "https://files.pythonhosted.org/packages/bb/d4/74ac9f7a54cfde12ee42734ea25d5a3c9a45db78f9def949307a92720d37/pyarrow-22.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c3200cb41cdbc65156e5f8c908d739b0dfed57e890329413da2748d1a2cd1a4e", size = 47990906 }, - { url = "https://files.pythonhosted.org/packages/2e/71/fedf2499bf7a95062eafc989ace56572f3343432570e1c54e6599d5b88da/pyarrow-22.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ac93252226cf288753d8b46280f4edf3433bf9508b6977f8dd8526b521a1bbb9", size = 50306783 }, - { url = "https://files.pythonhosted.org/packages/68/ed/b202abd5a5b78f519722f3d29063dda03c114711093c1995a33b8e2e0f4b/pyarrow-22.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:44729980b6c50a5f2bfcc2668d36c569ce17f8b17bccaf470c4313dcbbf13c9d", size = 27972883 }, - { url = "https://files.pythonhosted.org/packages/a6/d6/d0fac16a2963002fc22c8fa75180a838737203d558f0ed3b564c4a54eef5/pyarrow-22.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:e6e95176209257803a8b3d0394f21604e796dadb643d2f7ca21b66c9c0b30c9a", size = 34204629 }, - { url = "https://files.pythonhosted.org/packages/c6/9c/1d6357347fbae062ad3f17082f9ebc29cc733321e892c0d2085f42a2212b/pyarrow-22.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:001ea83a58024818826a9e3f89bf9310a114f7e26dfe404a4c32686f97bd7901", size = 35985783 }, - { url = "https://files.pythonhosted.org/packages/ff/c0/782344c2ce58afbea010150df07e3a2f5fdad299cd631697ae7bd3bac6e3/pyarrow-22.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:ce20fe000754f477c8a9125543f1936ea5b8867c5406757c224d745ed033e691", size = 45020999 }, - { url = "https://files.pythonhosted.org/packages/1b/8b/5362443737a5307a7b67c1017c42cd104213189b4970bf607e05faf9c525/pyarrow-22.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e0a15757fccb38c410947df156f9749ae4a3c89b2393741a50521f39a8cf202a", size = 47724601 }, - { url = "https://files.pythonhosted.org/packages/69/4d/76e567a4fc2e190ee6072967cb4672b7d9249ac59ae65af2d7e3047afa3b/pyarrow-22.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cedb9dd9358e4ea1d9bce3665ce0797f6adf97ff142c8e25b46ba9cdd508e9b6", size = 48001050 }, - { url = "https://files.pythonhosted.org/packages/01/5e/5653f0535d2a1aef8223cee9d92944cb6bccfee5cf1cd3f462d7cb022790/pyarrow-22.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:252be4a05f9d9185bb8c18e83764ebcfea7185076c07a7a662253af3a8c07941", size = 50307877 }, - { url = "https://files.pythonhosted.org/packages/2d/f8/1d0bd75bf9328a3b826e24a16e5517cd7f9fbf8d34a3184a4566ef5a7f29/pyarrow-22.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:a4893d31e5ef780b6edcaf63122df0f8d321088bb0dee4c8c06eccb1ca28d145", size = 27977099 }, - { url = "https://files.pythonhosted.org/packages/90/81/db56870c997805bf2b0f6eeeb2d68458bf4654652dccdcf1bf7a42d80903/pyarrow-22.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:f7fe3dbe871294ba70d789be16b6e7e52b418311e166e0e3cba9522f0f437fb1", size = 34336685 }, - { url = "https://files.pythonhosted.org/packages/1c/98/0727947f199aba8a120f47dfc229eeb05df15bcd7a6f1b669e9f882afc58/pyarrow-22.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:ba95112d15fd4f1105fb2402c4eab9068f0554435e9b7085924bcfaac2cc306f", size = 36032158 }, - { url = "https://files.pythonhosted.org/packages/96/b4/9babdef9c01720a0785945c7cf550e4acd0ebcd7bdd2e6f0aa7981fa85e2/pyarrow-22.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:c064e28361c05d72eed8e744c9605cbd6d2bb7481a511c74071fd9b24bc65d7d", size = 44892060 }, - { url = "https://files.pythonhosted.org/packages/f8/ca/2f8804edd6279f78a37062d813de3f16f29183874447ef6d1aadbb4efa0f/pyarrow-22.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:6f9762274496c244d951c819348afbcf212714902742225f649cf02823a6a10f", size = 47504395 }, - { url = "https://files.pythonhosted.org/packages/b9/f0/77aa5198fd3943682b2e4faaf179a674f0edea0d55d326d83cb2277d9363/pyarrow-22.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a9d9ffdc2ab696f6b15b4d1f7cec6658e1d788124418cb30030afbae31c64746", size = 48066216 }, - { url = "https://files.pythonhosted.org/packages/79/87/a1937b6e78b2aff18b706d738c9e46ade5bfcf11b294e39c87706a0089ac/pyarrow-22.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ec1a15968a9d80da01e1d30349b2b0d7cc91e96588ee324ce1b5228175043e95", size = 50288552 }, - { url = "https://files.pythonhosted.org/packages/60/ae/b5a5811e11f25788ccfdaa8f26b6791c9807119dffcf80514505527c384c/pyarrow-22.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:bba208d9c7decf9961998edf5c65e3ea4355d5818dd6cd0f6809bec1afb951cc", size = 28262504 }, - { url = "https://files.pythonhosted.org/packages/bd/b0/0fa4d28a8edb42b0a7144edd20befd04173ac79819547216f8a9f36f9e50/pyarrow-22.0.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:9bddc2cade6561f6820d4cd73f99a0243532ad506bc510a75a5a65a522b2d74d", size = 34224062 }, - { url = "https://files.pythonhosted.org/packages/0f/a8/7a719076b3c1be0acef56a07220c586f25cd24de0e3f3102b438d18ae5df/pyarrow-22.0.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:e70ff90c64419709d38c8932ea9fe1cc98415c4f87ea8da81719e43f02534bc9", size = 35990057 }, - { url = "https://files.pythonhosted.org/packages/89/3c/359ed54c93b47fb6fe30ed16cdf50e3f0e8b9ccfb11b86218c3619ae50a8/pyarrow-22.0.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:92843c305330aa94a36e706c16209cd4df274693e777ca47112617db7d0ef3d7", size = 45068002 }, - { url = "https://files.pythonhosted.org/packages/55/fc/4945896cc8638536ee787a3bd6ce7cec8ec9acf452d78ec39ab328efa0a1/pyarrow-22.0.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:6dda1ddac033d27421c20d7a7943eec60be44e0db4e079f33cc5af3b8280ccde", size = 47737765 }, - { url = "https://files.pythonhosted.org/packages/cd/5e/7cb7edeb2abfaa1f79b5d5eb89432356155c8426f75d3753cbcb9592c0fd/pyarrow-22.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:84378110dd9a6c06323b41b56e129c504d157d1a983ce8f5443761eb5256bafc", size = 48048139 }, - { url = "https://files.pythonhosted.org/packages/88/c6/546baa7c48185f5e9d6e59277c4b19f30f48c94d9dd938c2a80d4d6b067c/pyarrow-22.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:854794239111d2b88b40b6ef92aa478024d1e5074f364033e73e21e3f76b25e0", size = 50314244 }, - { url = "https://files.pythonhosted.org/packages/3c/79/755ff2d145aafec8d347bf18f95e4e81c00127f06d080135dfc86aea417c/pyarrow-22.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:b883fe6fd85adad7932b3271c38ac289c65b7337c2c132e9569f9d3940620730", size = 28757501 }, - { url = "https://files.pythonhosted.org/packages/0e/d2/237d75ac28ced3147912954e3c1a174df43a95f4f88e467809118a8165e0/pyarrow-22.0.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:7a820d8ae11facf32585507c11f04e3f38343c1e784c9b5a8b1da5c930547fe2", size = 34355506 }, - { url = "https://files.pythonhosted.org/packages/1e/2c/733dfffe6d3069740f98e57ff81007809067d68626c5faef293434d11bd6/pyarrow-22.0.0-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:c6ec3675d98915bf1ec8b3c7986422682f7232ea76cad276f4c8abd5b7319b70", size = 36047312 }, - { url = "https://files.pythonhosted.org/packages/7c/2b/29d6e3782dc1f299727462c1543af357a0f2c1d3c160ce199950d9ca51eb/pyarrow-22.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:3e739edd001b04f654b166204fc7a9de896cf6007eaff33409ee9e50ceaff754", size = 45081609 }, - { url = "https://files.pythonhosted.org/packages/8d/42/aa9355ecc05997915af1b7b947a7f66c02dcaa927f3203b87871c114ba10/pyarrow-22.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:7388ac685cab5b279a41dfe0a6ccd99e4dbf322edfb63e02fc0443bf24134e91", size = 47703663 }, - { url = "https://files.pythonhosted.org/packages/ee/62/45abedde480168e83a1de005b7b7043fd553321c1e8c5a9a114425f64842/pyarrow-22.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f633074f36dbc33d5c05b5dc75371e5660f1dbf9c8b1d95669def05e5425989c", size = 48066543 }, - { url = "https://files.pythonhosted.org/packages/84/e9/7878940a5b072e4f3bf998770acafeae13b267f9893af5f6d4ab3904b67e/pyarrow-22.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4c19236ae2402a8663a2c8f21f1870a03cc57f0bef7e4b6eb3238cc82944de80", size = 50288838 }, - { url = "https://files.pythonhosted.org/packages/7b/03/f335d6c52b4a4761bcc83499789a1e2e16d9d201a58c327a9b5cc9a41bd9/pyarrow-22.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0c34fe18094686194f204a3b1787a27456897d8a2d62caf84b61e8dfbc0252ae", size = 29185594 }, +sdist = { url = "https://files.pythonhosted.org/packages/30/53/04a7fdc63e6056116c9ddc8b43bc28c12cdd181b85cbeadb79278475f3ae/pyarrow-22.0.0.tar.gz", hash = "sha256:3d600dc583260d845c7d8a6db540339dd883081925da2bd1c5cb808f720b3cd9", size = 1151151, upload-time = "2025-10-24T12:30:00.762Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/b7/18f611a8cdc43417f9394a3ccd3eace2f32183c08b9eddc3d17681819f37/pyarrow-22.0.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:3e294c5eadfb93d78b0763e859a0c16d4051fc1c5231ae8956d61cb0b5666f5a", size = 34272022, upload-time = "2025-10-24T10:04:28.973Z" }, + { url = "https://files.pythonhosted.org/packages/26/5c/f259e2526c67eb4b9e511741b19870a02363a47a35edbebc55c3178db22d/pyarrow-22.0.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:69763ab2445f632d90b504a815a2a033f74332997052b721002298ed6de40f2e", size = 35995834, upload-time = "2025-10-24T10:04:35.467Z" }, + { url = "https://files.pythonhosted.org/packages/50/8d/281f0f9b9376d4b7f146913b26fac0aa2829cd1ee7e997f53a27411bbb92/pyarrow-22.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:b41f37cabfe2463232684de44bad753d6be08a7a072f6a83447eeaf0e4d2a215", size = 45030348, upload-time = "2025-10-24T10:04:43.366Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e5/53c0a1c428f0976bf22f513d79c73000926cb00b9c138d8e02daf2102e18/pyarrow-22.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:35ad0f0378c9359b3f297299c3309778bb03b8612f987399a0333a560b43862d", size = 47699480, upload-time = "2025-10-24T10:04:51.486Z" }, + { url = "https://files.pythonhosted.org/packages/95/e1/9dbe4c465c3365959d183e6345d0a8d1dc5b02ca3f8db4760b3bc834cf25/pyarrow-22.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8382ad21458075c2e66a82a29d650f963ce51c7708c7c0ff313a8c206c4fd5e8", size = 48011148, upload-time = "2025-10-24T10:04:59.585Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b4/7caf5d21930061444c3cf4fa7535c82faf5263e22ce43af7c2759ceb5b8b/pyarrow-22.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1a812a5b727bc09c3d7ea072c4eebf657c2f7066155506ba31ebf4792f88f016", size = 50276964, upload-time = "2025-10-24T10:05:08.175Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f3/cec89bd99fa3abf826f14d4e53d3d11340ce6f6af4d14bdcd54cd83b6576/pyarrow-22.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:ec5d40dd494882704fb876c16fa7261a69791e784ae34e6b5992e977bd2e238c", size = 28106517, upload-time = "2025-10-24T10:05:14.314Z" }, + { url = "https://files.pythonhosted.org/packages/af/63/ba23862d69652f85b615ca14ad14f3bcfc5bf1b99ef3f0cd04ff93fdad5a/pyarrow-22.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:bea79263d55c24a32b0d79c00a1c58bb2ee5f0757ed95656b01c0fb310c5af3d", size = 34211578, upload-time = "2025-10-24T10:05:21.583Z" }, + { url = "https://files.pythonhosted.org/packages/b1/d0/f9ad86fe809efd2bcc8be32032fa72e8b0d112b01ae56a053006376c5930/pyarrow-22.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:12fe549c9b10ac98c91cf791d2945e878875d95508e1a5d14091a7aaa66d9cf8", size = 35989906, upload-time = "2025-10-24T10:05:29.485Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a8/f910afcb14630e64d673f15904ec27dd31f1e009b77033c365c84e8c1e1d/pyarrow-22.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:334f900ff08ce0423407af97e6c26ad5d4e3b0763645559ece6fbf3747d6a8f5", size = 45021677, upload-time = "2025-10-24T10:05:38.274Z" }, + { url = "https://files.pythonhosted.org/packages/13/95/aec81f781c75cd10554dc17a25849c720d54feafb6f7847690478dcf5ef8/pyarrow-22.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:c6c791b09c57ed76a18b03f2631753a4960eefbbca80f846da8baefc6491fcfe", size = 47726315, upload-time = "2025-10-24T10:05:47.314Z" }, + { url = "https://files.pythonhosted.org/packages/bb/d4/74ac9f7a54cfde12ee42734ea25d5a3c9a45db78f9def949307a92720d37/pyarrow-22.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c3200cb41cdbc65156e5f8c908d739b0dfed57e890329413da2748d1a2cd1a4e", size = 47990906, upload-time = "2025-10-24T10:05:58.254Z" }, + { url = "https://files.pythonhosted.org/packages/2e/71/fedf2499bf7a95062eafc989ace56572f3343432570e1c54e6599d5b88da/pyarrow-22.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ac93252226cf288753d8b46280f4edf3433bf9508b6977f8dd8526b521a1bbb9", size = 50306783, upload-time = "2025-10-24T10:06:08.08Z" }, + { url = "https://files.pythonhosted.org/packages/68/ed/b202abd5a5b78f519722f3d29063dda03c114711093c1995a33b8e2e0f4b/pyarrow-22.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:44729980b6c50a5f2bfcc2668d36c569ce17f8b17bccaf470c4313dcbbf13c9d", size = 27972883, upload-time = "2025-10-24T10:06:14.204Z" }, + { url = "https://files.pythonhosted.org/packages/a6/d6/d0fac16a2963002fc22c8fa75180a838737203d558f0ed3b564c4a54eef5/pyarrow-22.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:e6e95176209257803a8b3d0394f21604e796dadb643d2f7ca21b66c9c0b30c9a", size = 34204629, upload-time = "2025-10-24T10:06:20.274Z" }, + { url = "https://files.pythonhosted.org/packages/c6/9c/1d6357347fbae062ad3f17082f9ebc29cc733321e892c0d2085f42a2212b/pyarrow-22.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:001ea83a58024818826a9e3f89bf9310a114f7e26dfe404a4c32686f97bd7901", size = 35985783, upload-time = "2025-10-24T10:06:27.301Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c0/782344c2ce58afbea010150df07e3a2f5fdad299cd631697ae7bd3bac6e3/pyarrow-22.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:ce20fe000754f477c8a9125543f1936ea5b8867c5406757c224d745ed033e691", size = 45020999, upload-time = "2025-10-24T10:06:35.387Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8b/5362443737a5307a7b67c1017c42cd104213189b4970bf607e05faf9c525/pyarrow-22.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e0a15757fccb38c410947df156f9749ae4a3c89b2393741a50521f39a8cf202a", size = 47724601, upload-time = "2025-10-24T10:06:43.551Z" }, + { url = "https://files.pythonhosted.org/packages/69/4d/76e567a4fc2e190ee6072967cb4672b7d9249ac59ae65af2d7e3047afa3b/pyarrow-22.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cedb9dd9358e4ea1d9bce3665ce0797f6adf97ff142c8e25b46ba9cdd508e9b6", size = 48001050, upload-time = "2025-10-24T10:06:52.284Z" }, + { url = "https://files.pythonhosted.org/packages/01/5e/5653f0535d2a1aef8223cee9d92944cb6bccfee5cf1cd3f462d7cb022790/pyarrow-22.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:252be4a05f9d9185bb8c18e83764ebcfea7185076c07a7a662253af3a8c07941", size = 50307877, upload-time = "2025-10-24T10:07:02.405Z" }, + { url = "https://files.pythonhosted.org/packages/2d/f8/1d0bd75bf9328a3b826e24a16e5517cd7f9fbf8d34a3184a4566ef5a7f29/pyarrow-22.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:a4893d31e5ef780b6edcaf63122df0f8d321088bb0dee4c8c06eccb1ca28d145", size = 27977099, upload-time = "2025-10-24T10:08:07.259Z" }, + { url = "https://files.pythonhosted.org/packages/90/81/db56870c997805bf2b0f6eeeb2d68458bf4654652dccdcf1bf7a42d80903/pyarrow-22.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:f7fe3dbe871294ba70d789be16b6e7e52b418311e166e0e3cba9522f0f437fb1", size = 34336685, upload-time = "2025-10-24T10:07:11.47Z" }, + { url = "https://files.pythonhosted.org/packages/1c/98/0727947f199aba8a120f47dfc229eeb05df15bcd7a6f1b669e9f882afc58/pyarrow-22.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:ba95112d15fd4f1105fb2402c4eab9068f0554435e9b7085924bcfaac2cc306f", size = 36032158, upload-time = "2025-10-24T10:07:18.626Z" }, + { url = "https://files.pythonhosted.org/packages/96/b4/9babdef9c01720a0785945c7cf550e4acd0ebcd7bdd2e6f0aa7981fa85e2/pyarrow-22.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:c064e28361c05d72eed8e744c9605cbd6d2bb7481a511c74071fd9b24bc65d7d", size = 44892060, upload-time = "2025-10-24T10:07:26.002Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ca/2f8804edd6279f78a37062d813de3f16f29183874447ef6d1aadbb4efa0f/pyarrow-22.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:6f9762274496c244d951c819348afbcf212714902742225f649cf02823a6a10f", size = 47504395, upload-time = "2025-10-24T10:07:34.09Z" }, + { url = "https://files.pythonhosted.org/packages/b9/f0/77aa5198fd3943682b2e4faaf179a674f0edea0d55d326d83cb2277d9363/pyarrow-22.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a9d9ffdc2ab696f6b15b4d1f7cec6658e1d788124418cb30030afbae31c64746", size = 48066216, upload-time = "2025-10-24T10:07:43.528Z" }, + { url = "https://files.pythonhosted.org/packages/79/87/a1937b6e78b2aff18b706d738c9e46ade5bfcf11b294e39c87706a0089ac/pyarrow-22.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ec1a15968a9d80da01e1d30349b2b0d7cc91e96588ee324ce1b5228175043e95", size = 50288552, upload-time = "2025-10-24T10:07:53.519Z" }, + { url = "https://files.pythonhosted.org/packages/60/ae/b5a5811e11f25788ccfdaa8f26b6791c9807119dffcf80514505527c384c/pyarrow-22.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:bba208d9c7decf9961998edf5c65e3ea4355d5818dd6cd0f6809bec1afb951cc", size = 28262504, upload-time = "2025-10-24T10:08:00.932Z" }, + { url = "https://files.pythonhosted.org/packages/bd/b0/0fa4d28a8edb42b0a7144edd20befd04173ac79819547216f8a9f36f9e50/pyarrow-22.0.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:9bddc2cade6561f6820d4cd73f99a0243532ad506bc510a75a5a65a522b2d74d", size = 34224062, upload-time = "2025-10-24T10:08:14.101Z" }, + { url = "https://files.pythonhosted.org/packages/0f/a8/7a719076b3c1be0acef56a07220c586f25cd24de0e3f3102b438d18ae5df/pyarrow-22.0.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:e70ff90c64419709d38c8932ea9fe1cc98415c4f87ea8da81719e43f02534bc9", size = 35990057, upload-time = "2025-10-24T10:08:21.842Z" }, + { url = "https://files.pythonhosted.org/packages/89/3c/359ed54c93b47fb6fe30ed16cdf50e3f0e8b9ccfb11b86218c3619ae50a8/pyarrow-22.0.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:92843c305330aa94a36e706c16209cd4df274693e777ca47112617db7d0ef3d7", size = 45068002, upload-time = "2025-10-24T10:08:29.034Z" }, + { url = "https://files.pythonhosted.org/packages/55/fc/4945896cc8638536ee787a3bd6ce7cec8ec9acf452d78ec39ab328efa0a1/pyarrow-22.0.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:6dda1ddac033d27421c20d7a7943eec60be44e0db4e079f33cc5af3b8280ccde", size = 47737765, upload-time = "2025-10-24T10:08:38.559Z" }, + { url = "https://files.pythonhosted.org/packages/cd/5e/7cb7edeb2abfaa1f79b5d5eb89432356155c8426f75d3753cbcb9592c0fd/pyarrow-22.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:84378110dd9a6c06323b41b56e129c504d157d1a983ce8f5443761eb5256bafc", size = 48048139, upload-time = "2025-10-24T10:08:46.784Z" }, + { url = "https://files.pythonhosted.org/packages/88/c6/546baa7c48185f5e9d6e59277c4b19f30f48c94d9dd938c2a80d4d6b067c/pyarrow-22.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:854794239111d2b88b40b6ef92aa478024d1e5074f364033e73e21e3f76b25e0", size = 50314244, upload-time = "2025-10-24T10:08:55.771Z" }, + { url = "https://files.pythonhosted.org/packages/3c/79/755ff2d145aafec8d347bf18f95e4e81c00127f06d080135dfc86aea417c/pyarrow-22.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:b883fe6fd85adad7932b3271c38ac289c65b7337c2c132e9569f9d3940620730", size = 28757501, upload-time = "2025-10-24T10:09:59.891Z" }, + { url = "https://files.pythonhosted.org/packages/0e/d2/237d75ac28ced3147912954e3c1a174df43a95f4f88e467809118a8165e0/pyarrow-22.0.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:7a820d8ae11facf32585507c11f04e3f38343c1e784c9b5a8b1da5c930547fe2", size = 34355506, upload-time = "2025-10-24T10:09:02.953Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/733dfffe6d3069740f98e57ff81007809067d68626c5faef293434d11bd6/pyarrow-22.0.0-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:c6ec3675d98915bf1ec8b3c7986422682f7232ea76cad276f4c8abd5b7319b70", size = 36047312, upload-time = "2025-10-24T10:09:10.334Z" }, + { url = "https://files.pythonhosted.org/packages/7c/2b/29d6e3782dc1f299727462c1543af357a0f2c1d3c160ce199950d9ca51eb/pyarrow-22.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:3e739edd001b04f654b166204fc7a9de896cf6007eaff33409ee9e50ceaff754", size = 45081609, upload-time = "2025-10-24T10:09:18.61Z" }, + { url = "https://files.pythonhosted.org/packages/8d/42/aa9355ecc05997915af1b7b947a7f66c02dcaa927f3203b87871c114ba10/pyarrow-22.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:7388ac685cab5b279a41dfe0a6ccd99e4dbf322edfb63e02fc0443bf24134e91", size = 47703663, upload-time = "2025-10-24T10:09:27.369Z" }, + { url = "https://files.pythonhosted.org/packages/ee/62/45abedde480168e83a1de005b7b7043fd553321c1e8c5a9a114425f64842/pyarrow-22.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f633074f36dbc33d5c05b5dc75371e5660f1dbf9c8b1d95669def05e5425989c", size = 48066543, upload-time = "2025-10-24T10:09:34.908Z" }, + { url = "https://files.pythonhosted.org/packages/84/e9/7878940a5b072e4f3bf998770acafeae13b267f9893af5f6d4ab3904b67e/pyarrow-22.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4c19236ae2402a8663a2c8f21f1870a03cc57f0bef7e4b6eb3238cc82944de80", size = 50288838, upload-time = "2025-10-24T10:09:44.394Z" }, + { url = "https://files.pythonhosted.org/packages/7b/03/f335d6c52b4a4761bcc83499789a1e2e16d9d201a58c327a9b5cc9a41bd9/pyarrow-22.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0c34fe18094686194f204a3b1787a27456897d8a2d62caf84b61e8dfbc0252ae", size = 29185594, upload-time = "2025-10-24T10:09:53.111Z" }, ] [[package]] @@ -983,9 +983,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591 } +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580 }, + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, ] [[package]] @@ -995,94 +995,94 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873 }, - { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826 }, - { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869 }, - { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890 }, - { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740 }, - { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021 }, - { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378 }, - { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761 }, - { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303 }, - { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355 }, - { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875 }, - { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549 }, - { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305 }, - { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902 }, - { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990 }, - { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003 }, - { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200 }, - { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578 }, - { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504 }, - { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816 }, - { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366 }, - { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698 }, - { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603 }, - { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591 }, - { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068 }, - { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908 }, - { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145 }, - { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179 }, - { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403 }, - { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206 }, - { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307 }, - { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258 }, - { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917 }, - { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186 }, - { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164 }, - { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146 }, - { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788 }, - { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133 }, - { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852 }, - { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679 }, - { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766 }, - { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005 }, - { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622 }, - { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725 }, - { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040 }, - { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691 }, - { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897 }, - { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302 }, - { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877 }, - { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680 }, - { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960 }, - { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102 }, - { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039 }, - { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126 }, - { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489 }, - { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288 }, - { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255 }, - { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760 }, - { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092 }, - { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385 }, - { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832 }, - { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585 }, - { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078 }, - { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914 }, - { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560 }, - { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244 }, - { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955 }, - { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906 }, - { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607 }, - { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769 }, - { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441 }, - { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291 }, - { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632 }, - { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905 }, - { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495 }, - { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388 }, - { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879 }, - { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017 }, - { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980 }, - { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865 }, - { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256 }, - { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762 }, - { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141 }, - { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317 }, - { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992 }, - { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302 }, +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, ] [[package]] @@ -1094,14 +1094,14 @@ dependencies = [ { name = "python-dotenv" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/43/4b/ac7e0aae12027748076d72a8764ff1c9d82ca75a7a52622e67ed3f765c54/pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0", size = 194184 } +sdist = { url = "https://files.pythonhosted.org/packages/43/4b/ac7e0aae12027748076d72a8764ff1c9d82ca75a7a52622e67ed3f765c54/pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0", size = 194184, upload-time = "2025-11-10T14:25:47.013Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", size = 51880 }, + { url = "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", size = 51880, upload-time = "2025-11-10T14:25:45.546Z" }, ] [[package]] name = "pyfia" -version = "1.2.3" +version = "1.3.0" source = { editable = "." } dependencies = [ { name = "connectorx" }, @@ -1188,9 +1188,9 @@ dev = [] name = "pygments" version = "2.19.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631 } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217 }, + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] [[package]] @@ -1201,9 +1201,9 @@ dependencies = [ { name = "markdown" }, { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/25/6d/af5378dbdb379fddd9a277f8b9888c027db480cde70028669ebd009d642a/pymdown_extensions-10.17.2.tar.gz", hash = "sha256:26bb3d7688e651606260c90fb46409fbda70bf9fdc3623c7868643a1aeee4713", size = 847344 } +sdist = { url = "https://files.pythonhosted.org/packages/25/6d/af5378dbdb379fddd9a277f8b9888c027db480cde70028669ebd009d642a/pymdown_extensions-10.17.2.tar.gz", hash = "sha256:26bb3d7688e651606260c90fb46409fbda70bf9fdc3623c7868643a1aeee4713", size = 847344, upload-time = "2025-11-26T15:43:57.004Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/93/78/b93cb80bd673bdc9f6ede63d8eb5b4646366953df15667eb3603be57a2b1/pymdown_extensions-10.17.2-py3-none-any.whl", hash = "sha256:bffae79a2e8b9e44aef0d813583a8fea63457b7a23643a43988055b7b79b4992", size = 266556 }, + { url = "https://files.pythonhosted.org/packages/93/78/b93cb80bd673bdc9f6ede63d8eb5b4646366953df15667eb3603be57a2b1/pymdown_extensions-10.17.2-py3-none-any.whl", hash = "sha256:bffae79a2e8b9e44aef0d813583a8fea63457b7a23643a43988055b7b79b4992", size = 266556, upload-time = "2025-11-26T15:43:55.162Z" }, ] [[package]] @@ -1215,44 +1215,44 @@ dependencies = [ { name = "numpy" }, { name = "packaging" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/49/d4/12f86b1ed09721363da4c09622464b604c851a9223fc0c6b393fb2012208/pyogrio-0.12.1.tar.gz", hash = "sha256:e548ab705bb3e5383693717de1e6c76da97f3762ab92522cb310f93128a75ff1", size = 303289 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/02/46/b2c2dcdfd88759b56f103365905fffb85e8b08c1db1ec7c8f8b4c4c26016/pyogrio-0.12.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:01b322dac2a258d24b024d1028dcaa03c9bb6d9c3988b86d298a64873d10dc65", size = 23670744 }, - { url = "https://files.pythonhosted.org/packages/d9/21/b69f1bc51d805c00dd7c484a18e1fd2e75b41da1d9f5b8591d7d9d4a7d2f/pyogrio-0.12.1-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:e10087abcbd6b7e8212560a7002984e5078ac7b3a969ddc2c9929044dbb0d403", size = 25246184 }, - { url = "https://files.pythonhosted.org/packages/19/8c/b6aae08e8fcc4f2a903da5f6bd8f888d2b6d7290e54dde5abe15b4cca8df/pyogrio-0.12.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1f6c621972b09fd81a32317e742c69ff4a7763a803da211361a78317f9577765", size = 31434449 }, - { url = "https://files.pythonhosted.org/packages/70/f9/9538fa893c29a3fdfeddf3b4c9f8db77f2d4134bc766587929fec8405ebf/pyogrio-0.12.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:c38253427b688464caad5316d4ebcec116b5e13f1f02cc4e3588502f136ca1b4", size = 30987586 }, - { url = "https://files.pythonhosted.org/packages/89/a4/0aef5837b4e11840f501e48e01c31242838476c4f4aff9c05e228a083982/pyogrio-0.12.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:5f47787251de7ce13cc06038da93a1214dc283cbccf816be6e03c080358226c8", size = 32534386 }, - { url = "https://files.pythonhosted.org/packages/34/97/e8f2ed8a339152b86f8403c258ae5d5f23ab32d690eeb0545bb3473d0c69/pyogrio-0.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:c1d756cf2da4cdf5609779f260d1e1e89be023184225855d6f3dcd33bbe17cb0", size = 22941718 }, - { url = "https://files.pythonhosted.org/packages/ad/e0/656b6536549d41b5aec57e0deca1f269b4f17532f0636836f587e581603a/pyogrio-0.12.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:7a0d5ca39184030aec4cde30f4258f75b227a854530d2659babc8189d76e657d", size = 23661857 }, - { url = "https://files.pythonhosted.org/packages/14/78/313259e40da728bdb60106ffdc7ea8224d164498cb838ecb79b634aab967/pyogrio-0.12.1-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:feaff42bbe8087ca0b30e33b09d1ce049ca55fe83ad83db1139ef37d1d04f30c", size = 25237106 }, - { url = "https://files.pythonhosted.org/packages/8f/ca/5368571a8b00b941ccfbe6ea29a5566aaffd45d4eb1553b956f7755af43e/pyogrio-0.12.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:81096a5139532de5a8003ef02b41d5d2444cb382a9aecd1165b447eb549180d3", size = 31417048 }, - { url = "https://files.pythonhosted.org/packages/ef/85/6eeb875f27bf498d657eb5dab9f58e4c48b36c9037122787abee9a1ba4ba/pyogrio-0.12.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:41b78863f782f7a113ed0d36a5dc74d59735bd3a82af53510899bb02a18b06bb", size = 30952115 }, - { url = "https://files.pythonhosted.org/packages/36/f7/cf8bec9024625947e1a71441906f60a5fa6f9e4c441c4428037e73b1fcc8/pyogrio-0.12.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:8b65be8c4258b27cc8f919b21929cecdadda4c353e3637fa30850339ef4d15c5", size = 32537246 }, - { url = "https://files.pythonhosted.org/packages/ab/10/7c9f5e428273574e69f217eba3a6c0c42936188ad4dcd9e2c41ebb711188/pyogrio-0.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:1291b866c2c81d991bda15021b08b3621709b40ee3a85689229929e9465788bf", size = 22933980 }, - { url = "https://files.pythonhosted.org/packages/be/56/f56e79f71b84aa9bea25fdde39fab3846841bd7926be96f623eb7253b7e1/pyogrio-0.12.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:ec0e47a5a704e575092b2fd5c83fa0472a1d421e590f94093eb837bb0a11125d", size = 23658483 }, - { url = "https://files.pythonhosted.org/packages/66/ac/5559f8a35d58a16cbb2dd7602dd11936ff8796d8c9bf789f14da88764ec3/pyogrio-0.12.1-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:b4c888fc08f388be4dd99dfca5e84a5cdc5994deeec0230cc45144d3460e2b21", size = 25232737 }, - { url = "https://files.pythonhosted.org/packages/59/58/925f1c129ddd7cbba8dea4e7609797cea7a76dbc863ac9afd318a679c4b9/pyogrio-0.12.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:73a88436f9962750d782853727897ac2722cac5900d920e39fab3e56d7a6a7f1", size = 31377986 }, - { url = "https://files.pythonhosted.org/packages/18/5f/c87034e92847b1844d0e8492a6a8e3301147d32c5e57909397ce64dbedf5/pyogrio-0.12.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:b5d248a0d59fe9bbf9a35690b70004c67830ee0ebe7d4f7bb8ffd8659f684b3a", size = 30915791 }, - { url = "https://files.pythonhosted.org/packages/46/35/b874f79d03e9f900012cf609f7fff97b77164f2e14ee5aac282f8a999c1b/pyogrio-0.12.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:0622bc1a186421547660271083079b38d42e6f868802936d8538c0b379f1ab6b", size = 32499754 }, - { url = "https://files.pythonhosted.org/packages/c3/c4/705678c9c4200130290b3a104b45c0cc10aaa48fcef3b2585b34e34ab3e1/pyogrio-0.12.1-cp313-cp313-win_amd64.whl", hash = "sha256:207bd60c7ffbcea84584596e3637653aa7095e9ee20fa408f90c7f9460392613", size = 22933945 }, - { url = "https://files.pythonhosted.org/packages/f9/e0/d92d4944001330bc87742d43f112d63d12fc89378b6187e62ff3fc1e8e85/pyogrio-0.12.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:1511b39a283fa27cda906cd187a791578942a87a40b6a06697d9b43bb8ac80b0", size = 23692697 }, - { url = "https://files.pythonhosted.org/packages/e5/d7/40acbe06d1b1140e3bb27b79e9163776469c1dc785f1be7d9a7fc7b95c87/pyogrio-0.12.1-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:e486cd6aa9ea8a15394a5f84e019d61ec18f257eeeb642348bd68c3d1e57280b", size = 25258083 }, - { url = "https://files.pythonhosted.org/packages/87/a1/39fefd9cddd95986700524f43d3093b4350f6e4fc200623c3838424a5080/pyogrio-0.12.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d3f1a19f63bfd1d3042e45f37ad1d6598123a5a604b6c4ba3f38b419273486cd", size = 31368995 }, - { url = "https://files.pythonhosted.org/packages/18/d7/da88c566e67d741a03851eb8d01358949d52e0b0fc2cd953582dc6d89ff8/pyogrio-0.12.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:f3dcc59b3316b8a0f59346bcc638a4d69997864a4d21da839192f50c4c92369a", size = 31035589 }, - { url = "https://files.pythonhosted.org/packages/11/ac/8f0199f0d31b8ddbc4b4ea1918df8070fdf3e0a63100b898633ec9396224/pyogrio-0.12.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:a0643e041dee3e8e038fce69f52a915ecb486e6d7b674c0f9919f3c9e9629689", size = 32487973 }, - { url = "https://files.pythonhosted.org/packages/bd/64/8541a27e9635a335835d234dfaeb19d6c26097fd88224eda7791f83ca98d/pyogrio-0.12.1-cp313-cp313t-win_amd64.whl", hash = "sha256:5881017f29e110d3613819667657844d8e961b747f2d35cf92f273c27af6d068", size = 22987374 }, - { url = "https://files.pythonhosted.org/packages/f4/6f/b4d5e285e08c0c60bcc23b50d73038ddc7335d8de79cc25678cd486a3db0/pyogrio-0.12.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:5a1b0453d1c9e7b03715dd57296c8f3790acb8b50d7e3b5844b3074a18f50709", size = 23660673 }, - { url = "https://files.pythonhosted.org/packages/8d/75/4b29e71489c5551aa1a1c5ca8c5160a60203c94f2f68c87c0e3614d58965/pyogrio-0.12.1-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:e7ee560422239dd09ca7f8284cc8483a8919c30d25f3049bb0249bff4c38dec4", size = 25232194 }, - { url = "https://files.pythonhosted.org/packages/89/6e/e9929d2261a07c36301983de2767bcde90d441ab5bf1d767ce56dd07f8b4/pyogrio-0.12.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:648c6f7f5f214d30e6cf493b4af1d59782907ac068af9119ca35f18153d6865a", size = 31336936 }, - { url = "https://files.pythonhosted.org/packages/1d/9e/c59941d734ed936d4e5c89b4b99cb5541307cc42b3fd466ee78a1850c177/pyogrio-0.12.1-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:58042584f3fd4cabb0f55d26c1405053f656be8a5c266c38140316a1e981aca0", size = 30902210 }, - { url = "https://files.pythonhosted.org/packages/d1/68/cc07320a63f9c2586e60bf11d148b00e12d0e707673bffe609bbdcb7e754/pyogrio-0.12.1-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:b438e38e4ccbaedaa5cb5824ff5de5539315d9b2fde6547c1e816576924ee8ca", size = 32461674 }, - { url = "https://files.pythonhosted.org/packages/13/bc/e4522f429c45a3b6ad28185849dd76e5c8718b780883c4795e7ee41841ae/pyogrio-0.12.1-cp314-cp314-win_amd64.whl", hash = "sha256:f1d8d8a2fea3781dc2a05982c050259261ebc0f6c5e03732d6d79d582adf9363", size = 23550575 }, - { url = "https://files.pythonhosted.org/packages/bd/ac/34f0664d0e391994a7b68529ae07a96432b2b4926dbac173ddc4ec94d310/pyogrio-0.12.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:9fe7286946f35a73e6370dc5855bc7a5e8e7babf9e4a8bad7a3279a1d94c7ea9", size = 23694285 }, - { url = "https://files.pythonhosted.org/packages/8a/93/873255529faff1da09d0b27287e85ec805a318c60c0c74fd7df77f94e557/pyogrio-0.12.1-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:2c50345b382f1be801d654ec22c70ee974d6057d4ba7afe984b55f2192bc94ee", size = 25259825 }, - { url = "https://files.pythonhosted.org/packages/27/95/4d4c3644695d99c6fa0b0b42f0d6266ae9dfaf64478a3371eaac950bdd02/pyogrio-0.12.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0db95765ac0ca935c7fe579e29451294e3ab19c317b0c59c31fbe92a69155e0", size = 31371995 }, - { url = "https://files.pythonhosted.org/packages/4c/6f/71f6bcca8754c8bf55a4b7153c61c91f8ac5ba992568e9fa3e54a0ee76fd/pyogrio-0.12.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:fc882779075982b93064b3bf3d8642514a6df00d9dd752493b104817072cfb01", size = 31035498 }, - { url = "https://files.pythonhosted.org/packages/fd/47/75c1aa165a988347317afab9b938a01ad25dbca559b582ea34473703dc38/pyogrio-0.12.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:806f620e0c54b54dbdd65e9b6368d24f344cda84c9343364b40a57eb3e1c4dca", size = 32496390 }, - { url = "https://files.pythonhosted.org/packages/31/93/4641dc5d952f6bdb71dabad2c50e3f8a5d58396cdea6ff8f8a08bfd4f4a6/pyogrio-0.12.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5399f66730978d8852ef5f44dbafa0f738e7f28f4f784349f36830b69a9d2134", size = 23620996 }, +sdist = { url = "https://files.pythonhosted.org/packages/49/d4/12f86b1ed09721363da4c09622464b604c851a9223fc0c6b393fb2012208/pyogrio-0.12.1.tar.gz", hash = "sha256:e548ab705bb3e5383693717de1e6c76da97f3762ab92522cb310f93128a75ff1", size = 303289, upload-time = "2025-11-28T19:04:53.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/46/b2c2dcdfd88759b56f103365905fffb85e8b08c1db1ec7c8f8b4c4c26016/pyogrio-0.12.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:01b322dac2a258d24b024d1028dcaa03c9bb6d9c3988b86d298a64873d10dc65", size = 23670744, upload-time = "2025-11-28T19:03:11.299Z" }, + { url = "https://files.pythonhosted.org/packages/d9/21/b69f1bc51d805c00dd7c484a18e1fd2e75b41da1d9f5b8591d7d9d4a7d2f/pyogrio-0.12.1-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:e10087abcbd6b7e8212560a7002984e5078ac7b3a969ddc2c9929044dbb0d403", size = 25246184, upload-time = "2025-11-28T19:03:13.997Z" }, + { url = "https://files.pythonhosted.org/packages/19/8c/b6aae08e8fcc4f2a903da5f6bd8f888d2b6d7290e54dde5abe15b4cca8df/pyogrio-0.12.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1f6c621972b09fd81a32317e742c69ff4a7763a803da211361a78317f9577765", size = 31434449, upload-time = "2025-11-28T19:03:16.777Z" }, + { url = "https://files.pythonhosted.org/packages/70/f9/9538fa893c29a3fdfeddf3b4c9f8db77f2d4134bc766587929fec8405ebf/pyogrio-0.12.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:c38253427b688464caad5316d4ebcec116b5e13f1f02cc4e3588502f136ca1b4", size = 30987586, upload-time = "2025-11-28T19:03:19.586Z" }, + { url = "https://files.pythonhosted.org/packages/89/a4/0aef5837b4e11840f501e48e01c31242838476c4f4aff9c05e228a083982/pyogrio-0.12.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:5f47787251de7ce13cc06038da93a1214dc283cbccf816be6e03c080358226c8", size = 32534386, upload-time = "2025-11-28T19:03:22.292Z" }, + { url = "https://files.pythonhosted.org/packages/34/97/e8f2ed8a339152b86f8403c258ae5d5f23ab32d690eeb0545bb3473d0c69/pyogrio-0.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:c1d756cf2da4cdf5609779f260d1e1e89be023184225855d6f3dcd33bbe17cb0", size = 22941718, upload-time = "2025-11-28T19:03:24.82Z" }, + { url = "https://files.pythonhosted.org/packages/ad/e0/656b6536549d41b5aec57e0deca1f269b4f17532f0636836f587e581603a/pyogrio-0.12.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:7a0d5ca39184030aec4cde30f4258f75b227a854530d2659babc8189d76e657d", size = 23661857, upload-time = "2025-11-28T19:03:27.744Z" }, + { url = "https://files.pythonhosted.org/packages/14/78/313259e40da728bdb60106ffdc7ea8224d164498cb838ecb79b634aab967/pyogrio-0.12.1-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:feaff42bbe8087ca0b30e33b09d1ce049ca55fe83ad83db1139ef37d1d04f30c", size = 25237106, upload-time = "2025-11-28T19:03:30.018Z" }, + { url = "https://files.pythonhosted.org/packages/8f/ca/5368571a8b00b941ccfbe6ea29a5566aaffd45d4eb1553b956f7755af43e/pyogrio-0.12.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:81096a5139532de5a8003ef02b41d5d2444cb382a9aecd1165b447eb549180d3", size = 31417048, upload-time = "2025-11-28T19:03:32.572Z" }, + { url = "https://files.pythonhosted.org/packages/ef/85/6eeb875f27bf498d657eb5dab9f58e4c48b36c9037122787abee9a1ba4ba/pyogrio-0.12.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:41b78863f782f7a113ed0d36a5dc74d59735bd3a82af53510899bb02a18b06bb", size = 30952115, upload-time = "2025-11-28T19:03:35.332Z" }, + { url = "https://files.pythonhosted.org/packages/36/f7/cf8bec9024625947e1a71441906f60a5fa6f9e4c441c4428037e73b1fcc8/pyogrio-0.12.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:8b65be8c4258b27cc8f919b21929cecdadda4c353e3637fa30850339ef4d15c5", size = 32537246, upload-time = "2025-11-28T19:03:37.969Z" }, + { url = "https://files.pythonhosted.org/packages/ab/10/7c9f5e428273574e69f217eba3a6c0c42936188ad4dcd9e2c41ebb711188/pyogrio-0.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:1291b866c2c81d991bda15021b08b3621709b40ee3a85689229929e9465788bf", size = 22933980, upload-time = "2025-11-28T19:03:41.047Z" }, + { url = "https://files.pythonhosted.org/packages/be/56/f56e79f71b84aa9bea25fdde39fab3846841bd7926be96f623eb7253b7e1/pyogrio-0.12.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:ec0e47a5a704e575092b2fd5c83fa0472a1d421e590f94093eb837bb0a11125d", size = 23658483, upload-time = "2025-11-28T19:03:43.567Z" }, + { url = "https://files.pythonhosted.org/packages/66/ac/5559f8a35d58a16cbb2dd7602dd11936ff8796d8c9bf789f14da88764ec3/pyogrio-0.12.1-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:b4c888fc08f388be4dd99dfca5e84a5cdc5994deeec0230cc45144d3460e2b21", size = 25232737, upload-time = "2025-11-28T19:03:45.92Z" }, + { url = "https://files.pythonhosted.org/packages/59/58/925f1c129ddd7cbba8dea4e7609797cea7a76dbc863ac9afd318a679c4b9/pyogrio-0.12.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:73a88436f9962750d782853727897ac2722cac5900d920e39fab3e56d7a6a7f1", size = 31377986, upload-time = "2025-11-28T19:03:48.495Z" }, + { url = "https://files.pythonhosted.org/packages/18/5f/c87034e92847b1844d0e8492a6a8e3301147d32c5e57909397ce64dbedf5/pyogrio-0.12.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:b5d248a0d59fe9bbf9a35690b70004c67830ee0ebe7d4f7bb8ffd8659f684b3a", size = 30915791, upload-time = "2025-11-28T19:03:51.267Z" }, + { url = "https://files.pythonhosted.org/packages/46/35/b874f79d03e9f900012cf609f7fff97b77164f2e14ee5aac282f8a999c1b/pyogrio-0.12.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:0622bc1a186421547660271083079b38d42e6f868802936d8538c0b379f1ab6b", size = 32499754, upload-time = "2025-11-28T19:03:58.776Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c4/705678c9c4200130290b3a104b45c0cc10aaa48fcef3b2585b34e34ab3e1/pyogrio-0.12.1-cp313-cp313-win_amd64.whl", hash = "sha256:207bd60c7ffbcea84584596e3637653aa7095e9ee20fa408f90c7f9460392613", size = 22933945, upload-time = "2025-11-28T19:04:01.551Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e0/d92d4944001330bc87742d43f112d63d12fc89378b6187e62ff3fc1e8e85/pyogrio-0.12.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:1511b39a283fa27cda906cd187a791578942a87a40b6a06697d9b43bb8ac80b0", size = 23692697, upload-time = "2025-11-28T19:04:04.208Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d7/40acbe06d1b1140e3bb27b79e9163776469c1dc785f1be7d9a7fc7b95c87/pyogrio-0.12.1-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:e486cd6aa9ea8a15394a5f84e019d61ec18f257eeeb642348bd68c3d1e57280b", size = 25258083, upload-time = "2025-11-28T19:04:07.121Z" }, + { url = "https://files.pythonhosted.org/packages/87/a1/39fefd9cddd95986700524f43d3093b4350f6e4fc200623c3838424a5080/pyogrio-0.12.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d3f1a19f63bfd1d3042e45f37ad1d6598123a5a604b6c4ba3f38b419273486cd", size = 31368995, upload-time = "2025-11-28T19:04:09.88Z" }, + { url = "https://files.pythonhosted.org/packages/18/d7/da88c566e67d741a03851eb8d01358949d52e0b0fc2cd953582dc6d89ff8/pyogrio-0.12.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:f3dcc59b3316b8a0f59346bcc638a4d69997864a4d21da839192f50c4c92369a", size = 31035589, upload-time = "2025-11-28T19:04:12.993Z" }, + { url = "https://files.pythonhosted.org/packages/11/ac/8f0199f0d31b8ddbc4b4ea1918df8070fdf3e0a63100b898633ec9396224/pyogrio-0.12.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:a0643e041dee3e8e038fce69f52a915ecb486e6d7b674c0f9919f3c9e9629689", size = 32487973, upload-time = "2025-11-28T19:04:16.103Z" }, + { url = "https://files.pythonhosted.org/packages/bd/64/8541a27e9635a335835d234dfaeb19d6c26097fd88224eda7791f83ca98d/pyogrio-0.12.1-cp313-cp313t-win_amd64.whl", hash = "sha256:5881017f29e110d3613819667657844d8e961b747f2d35cf92f273c27af6d068", size = 22987374, upload-time = "2025-11-28T19:04:18.91Z" }, + { url = "https://files.pythonhosted.org/packages/f4/6f/b4d5e285e08c0c60bcc23b50d73038ddc7335d8de79cc25678cd486a3db0/pyogrio-0.12.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:5a1b0453d1c9e7b03715dd57296c8f3790acb8b50d7e3b5844b3074a18f50709", size = 23660673, upload-time = "2025-11-28T19:04:21.662Z" }, + { url = "https://files.pythonhosted.org/packages/8d/75/4b29e71489c5551aa1a1c5ca8c5160a60203c94f2f68c87c0e3614d58965/pyogrio-0.12.1-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:e7ee560422239dd09ca7f8284cc8483a8919c30d25f3049bb0249bff4c38dec4", size = 25232194, upload-time = "2025-11-28T19:04:23.975Z" }, + { url = "https://files.pythonhosted.org/packages/89/6e/e9929d2261a07c36301983de2767bcde90d441ab5bf1d767ce56dd07f8b4/pyogrio-0.12.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:648c6f7f5f214d30e6cf493b4af1d59782907ac068af9119ca35f18153d6865a", size = 31336936, upload-time = "2025-11-28T19:04:26.594Z" }, + { url = "https://files.pythonhosted.org/packages/1d/9e/c59941d734ed936d4e5c89b4b99cb5541307cc42b3fd466ee78a1850c177/pyogrio-0.12.1-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:58042584f3fd4cabb0f55d26c1405053f656be8a5c266c38140316a1e981aca0", size = 30902210, upload-time = "2025-11-28T19:04:29.143Z" }, + { url = "https://files.pythonhosted.org/packages/d1/68/cc07320a63f9c2586e60bf11d148b00e12d0e707673bffe609bbdcb7e754/pyogrio-0.12.1-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:b438e38e4ccbaedaa5cb5824ff5de5539315d9b2fde6547c1e816576924ee8ca", size = 32461674, upload-time = "2025-11-28T19:04:31.792Z" }, + { url = "https://files.pythonhosted.org/packages/13/bc/e4522f429c45a3b6ad28185849dd76e5c8718b780883c4795e7ee41841ae/pyogrio-0.12.1-cp314-cp314-win_amd64.whl", hash = "sha256:f1d8d8a2fea3781dc2a05982c050259261ebc0f6c5e03732d6d79d582adf9363", size = 23550575, upload-time = "2025-11-28T19:04:34.556Z" }, + { url = "https://files.pythonhosted.org/packages/bd/ac/34f0664d0e391994a7b68529ae07a96432b2b4926dbac173ddc4ec94d310/pyogrio-0.12.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:9fe7286946f35a73e6370dc5855bc7a5e8e7babf9e4a8bad7a3279a1d94c7ea9", size = 23694285, upload-time = "2025-11-28T19:04:37.833Z" }, + { url = "https://files.pythonhosted.org/packages/8a/93/873255529faff1da09d0b27287e85ec805a318c60c0c74fd7df77f94e557/pyogrio-0.12.1-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:2c50345b382f1be801d654ec22c70ee974d6057d4ba7afe984b55f2192bc94ee", size = 25259825, upload-time = "2025-11-28T19:04:40.125Z" }, + { url = "https://files.pythonhosted.org/packages/27/95/4d4c3644695d99c6fa0b0b42f0d6266ae9dfaf64478a3371eaac950bdd02/pyogrio-0.12.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0db95765ac0ca935c7fe579e29451294e3ab19c317b0c59c31fbe92a69155e0", size = 31371995, upload-time = "2025-11-28T19:04:42.736Z" }, + { url = "https://files.pythonhosted.org/packages/4c/6f/71f6bcca8754c8bf55a4b7153c61c91f8ac5ba992568e9fa3e54a0ee76fd/pyogrio-0.12.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:fc882779075982b93064b3bf3d8642514a6df00d9dd752493b104817072cfb01", size = 31035498, upload-time = "2025-11-28T19:04:45.79Z" }, + { url = "https://files.pythonhosted.org/packages/fd/47/75c1aa165a988347317afab9b938a01ad25dbca559b582ea34473703dc38/pyogrio-0.12.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:806f620e0c54b54dbdd65e9b6368d24f344cda84c9343364b40a57eb3e1c4dca", size = 32496390, upload-time = "2025-11-28T19:04:48.786Z" }, + { url = "https://files.pythonhosted.org/packages/31/93/4641dc5d952f6bdb71dabad2c50e3f8a5d58396cdea6ff8f8a08bfd4f4a6/pyogrio-0.12.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5399f66730978d8852ef5f44dbafa0f738e7f28f4f784349f36830b69a9d2134", size = 23620996, upload-time = "2025-11-28T19:04:51.132Z" }, ] [[package]] @@ -1262,62 +1262,62 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/04/90/67bd7260b4ea9b8b20b4f58afef6c223ecb3abf368eb4ec5bc2cdef81b49/pyproj-3.7.2.tar.gz", hash = "sha256:39a0cf1ecc7e282d1d30f36594ebd55c9fae1fda8a2622cee5d100430628f88c", size = 226279 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a6/bd/f205552cd1713b08f93b09e39a3ec99edef0b3ebbbca67b486fdf1abe2de/pyproj-3.7.2-cp311-cp311-macosx_13_0_x86_64.whl", hash = "sha256:2514d61f24c4e0bb9913e2c51487ecdaeca5f8748d8313c933693416ca41d4d5", size = 6227022 }, - { url = "https://files.pythonhosted.org/packages/75/4c/9a937e659b8b418ab573c6d340d27e68716928953273e0837e7922fcac34/pyproj-3.7.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:8693ca3892d82e70de077701ee76dd13d7bca4ae1c9d1e739d72004df015923a", size = 4625810 }, - { url = "https://files.pythonhosted.org/packages/c0/7d/a9f41e814dc4d1dc54e95b2ccaf0b3ebe3eb18b1740df05fe334724c3d89/pyproj-3.7.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:5e26484d80fea56273ed1555abaea161e9661d81a6c07815d54b8e883d4ceb25", size = 9638694 }, - { url = "https://files.pythonhosted.org/packages/ad/ab/9bdb4a6216b712a1f9aab1c0fcbee5d3726f34a366f29c3e8c08a78d6b70/pyproj-3.7.2-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:281cb92847814e8018010c48b4069ff858a30236638631c1a91dd7bfa68f8a8a", size = 9493977 }, - { url = "https://files.pythonhosted.org/packages/c9/db/2db75b1b6190f1137b1c4e8ef6a22e1c338e46320f6329bfac819143e063/pyproj-3.7.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9c8577f0b7bb09118ec2e57e3babdc977127dd66326d6c5d755c76b063e6d9dc", size = 10841151 }, - { url = "https://files.pythonhosted.org/packages/89/f7/989643394ba23a286e9b7b3f09981496172f9e0d4512457ffea7dc47ffc7/pyproj-3.7.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a23f59904fac3a5e7364b3aa44d288234af267ca041adb2c2b14a903cd5d3ac5", size = 10751585 }, - { url = "https://files.pythonhosted.org/packages/53/6d/ad928fe975a6c14a093c92e6a319ca18f479f3336bb353a740bdba335681/pyproj-3.7.2-cp311-cp311-win32.whl", hash = "sha256:f2af4ed34b2cf3e031a2d85b067a3ecbd38df073c567e04b52fa7a0202afde8a", size = 5908533 }, - { url = "https://files.pythonhosted.org/packages/79/e0/b95584605cec9ed50b7ebaf7975d1c4ddeec5a86b7a20554ed8b60042bd7/pyproj-3.7.2-cp311-cp311-win_amd64.whl", hash = "sha256:0b7cb633565129677b2a183c4d807c727d1c736fcb0568a12299383056e67433", size = 6320742 }, - { url = "https://files.pythonhosted.org/packages/b7/4d/536e8f93bca808175c2d0a5ac9fdf69b960d8ab6b14f25030dccb07464d7/pyproj-3.7.2-cp311-cp311-win_arm64.whl", hash = "sha256:38b08d85e3a38e455625b80e9eb9f78027c8e2649a21dec4df1f9c3525460c71", size = 6245772 }, - { url = "https://files.pythonhosted.org/packages/8d/ab/9893ea9fb066be70ed9074ae543914a618c131ed8dff2da1e08b3a4df4db/pyproj-3.7.2-cp312-cp312-macosx_13_0_x86_64.whl", hash = "sha256:0a9bb26a6356fb5b033433a6d1b4542158fb71e3c51de49b4c318a1dff3aeaab", size = 6219832 }, - { url = "https://files.pythonhosted.org/packages/53/78/4c64199146eed7184eb0e85bedec60a4aa8853b6ffe1ab1f3a8b962e70a0/pyproj-3.7.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:567caa03021178861fad27fabde87500ec6d2ee173dd32f3e2d9871e40eebd68", size = 4620650 }, - { url = "https://files.pythonhosted.org/packages/b6/ac/14a78d17943898a93ef4f8c6a9d4169911c994e3161e54a7cedeba9d8dde/pyproj-3.7.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:c203101d1dc3c038a56cff0447acc515dd29d6e14811406ac539c21eed422b2a", size = 9667087 }, - { url = "https://files.pythonhosted.org/packages/b8/be/212882c450bba74fc8d7d35cbd57e4af84792f0a56194819d98106b075af/pyproj-3.7.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:1edc34266c0c23ced85f95a1ee8b47c9035eae6aca5b6b340327250e8e281630", size = 9552797 }, - { url = "https://files.pythonhosted.org/packages/ba/c0/c0f25c87b5d2a8686341c53c1792a222a480d6c9caf60311fec12c99ec26/pyproj-3.7.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aa9f26c21bc0e2dc3d224cb1eb4020cf23e76af179a7c66fea49b828611e4260", size = 10837036 }, - { url = "https://files.pythonhosted.org/packages/5d/37/5cbd6772addde2090c91113332623a86e8c7d583eccb2ad02ea634c4a89f/pyproj-3.7.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9428b318530625cb389b9ddc9c51251e172808a4af79b82809376daaeabe5e9", size = 10775952 }, - { url = "https://files.pythonhosted.org/packages/69/a1/dc250e3cf83eb4b3b9a2cf86fdb5e25288bd40037ae449695550f9e96b2f/pyproj-3.7.2-cp312-cp312-win32.whl", hash = "sha256:b3d99ed57d319da042f175f4554fc7038aa4bcecc4ac89e217e350346b742c9d", size = 5898872 }, - { url = "https://files.pythonhosted.org/packages/4a/a6/6fe724b72b70f2b00152d77282e14964d60ab092ec225e67c196c9b463e5/pyproj-3.7.2-cp312-cp312-win_amd64.whl", hash = "sha256:11614a054cd86a2ed968a657d00987a86eeb91fdcbd9ad3310478685dc14a128", size = 6312176 }, - { url = "https://files.pythonhosted.org/packages/5d/68/915cc32c02a91e76d02c8f55d5a138d6ef9e47a0d96d259df98f4842e558/pyproj-3.7.2-cp312-cp312-win_arm64.whl", hash = "sha256:509a146d1398bafe4f53273398c3bb0b4732535065fa995270e52a9d3676bca3", size = 6233452 }, - { url = "https://files.pythonhosted.org/packages/be/14/faf1b90d267cea68d7e70662e7f88cefdb1bc890bd596c74b959e0517a72/pyproj-3.7.2-cp313-cp313-macosx_13_0_x86_64.whl", hash = "sha256:19466e529b1b15eeefdf8ff26b06fa745856c044f2f77bf0edbae94078c1dfa1", size = 6214580 }, - { url = "https://files.pythonhosted.org/packages/35/48/da9a45b184d375f62667f62eba0ca68569b0bd980a0bb7ffcc1d50440520/pyproj-3.7.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:c79b9b84c4a626c5dc324c0d666be0bfcebd99f7538d66e8898c2444221b3da7", size = 4615388 }, - { url = "https://files.pythonhosted.org/packages/5e/e7/d2b459a4a64bca328b712c1b544e109df88e5c800f7c143cfbc404d39bfb/pyproj-3.7.2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:ceecf374cacca317bc09e165db38ac548ee3cad07c3609442bd70311c59c21aa", size = 9628455 }, - { url = "https://files.pythonhosted.org/packages/f8/85/c2b1706e51942de19076eff082f8495e57d5151364e78b5bef4af4a1d94a/pyproj-3.7.2-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5141a538ffdbe4bfd157421828bb2e07123a90a7a2d6f30fa1462abcfb5ce681", size = 9514269 }, - { url = "https://files.pythonhosted.org/packages/34/38/07a9b89ae7467872f9a476883a5bad9e4f4d1219d31060f0f2b282276cbe/pyproj-3.7.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f000841e98ea99acbb7b8ca168d67773b0191de95187228a16110245c5d954d5", size = 10808437 }, - { url = "https://files.pythonhosted.org/packages/12/56/fda1daeabbd39dec5b07f67233d09f31facb762587b498e6fc4572be9837/pyproj-3.7.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8115faf2597f281a42ab608ceac346b4eb1383d3b45ab474fd37341c4bf82a67", size = 10745540 }, - { url = "https://files.pythonhosted.org/packages/0d/90/c793182cbba65a39a11db2ac6b479fe76c59e6509ae75e5744c344a0da9d/pyproj-3.7.2-cp313-cp313-win32.whl", hash = "sha256:f18c0579dd6be00b970cb1a6719197fceecc407515bab37da0066f0184aafdf3", size = 5896506 }, - { url = "https://files.pythonhosted.org/packages/be/0f/747974129cf0d800906f81cd25efd098c96509026e454d4b66868779ab04/pyproj-3.7.2-cp313-cp313-win_amd64.whl", hash = "sha256:bb41c29d5f60854b1075853fe80c58950b398d4ebb404eb532536ac8d2834ed7", size = 6310195 }, - { url = "https://files.pythonhosted.org/packages/82/64/fc7598a53172c4931ec6edf5228280663063150625d3f6423b4c20f9daff/pyproj-3.7.2-cp313-cp313-win_arm64.whl", hash = "sha256:2b617d573be4118c11cd96b8891a0b7f65778fa7733ed8ecdb297a447d439100", size = 6230748 }, - { url = "https://files.pythonhosted.org/packages/aa/f0/611dd5cddb0d277f94b7af12981f56e1441bf8d22695065d4f0df5218498/pyproj-3.7.2-cp313-cp313t-macosx_13_0_x86_64.whl", hash = "sha256:d27b48f0e81beeaa2b4d60c516c3a1cfbb0c7ff6ef71256d8e9c07792f735279", size = 6241729 }, - { url = "https://files.pythonhosted.org/packages/15/93/40bd4a6c523ff9965e480870611aed7eda5aa2c6128c6537345a2b77b542/pyproj-3.7.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:55a3610d75023c7b1c6e583e48ef8f62918e85a2ae81300569d9f104d6684bb6", size = 4652497 }, - { url = "https://files.pythonhosted.org/packages/1b/ae/7150ead53c117880b35e0d37960d3138fe640a235feb9605cb9386f50bb0/pyproj-3.7.2-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:8d7349182fa622696787cc9e195508d2a41a64765da9b8a6bee846702b9e6220", size = 9942610 }, - { url = "https://files.pythonhosted.org/packages/d8/17/7a4a7eafecf2b46ab64e5c08176c20ceb5844b503eaa551bf12ccac77322/pyproj-3.7.2-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:d230b186eb876ed4f29a7c5ee310144c3a0e44e89e55f65fb3607e13f6db337c", size = 9692390 }, - { url = "https://files.pythonhosted.org/packages/c3/55/ae18f040f6410f0ea547a21ada7ef3e26e6c82befa125b303b02759c0e9d/pyproj-3.7.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:237499c7862c578d0369e2b8ac56eec550e391a025ff70e2af8417139dabb41c", size = 11047596 }, - { url = "https://files.pythonhosted.org/packages/e6/2e/d3fff4d2909473f26ae799f9dda04caa322c417a51ff3b25763f7d03b233/pyproj-3.7.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8c225f5978abd506fd9a78eaaf794435e823c9156091cabaab5374efb29d7f69", size = 10896975 }, - { url = "https://files.pythonhosted.org/packages/f2/bc/8fc7d3963d87057b7b51ebe68c1e7c51c23129eee5072ba6b86558544a46/pyproj-3.7.2-cp313-cp313t-win32.whl", hash = "sha256:2da731876d27639ff9d2d81c151f6ab90a1546455fabd93368e753047be344a2", size = 5953057 }, - { url = "https://files.pythonhosted.org/packages/cc/27/ea9809966cc47d2d51e6d5ae631ea895f7c7c7b9b3c29718f900a8f7d197/pyproj-3.7.2-cp313-cp313t-win_amd64.whl", hash = "sha256:f54d91ae18dd23b6c0ab48126d446820e725419da10617d86a1b69ada6d881d3", size = 6375414 }, - { url = "https://files.pythonhosted.org/packages/5b/f8/1ef0129fba9a555c658e22af68989f35e7ba7b9136f25758809efec0cd6e/pyproj-3.7.2-cp313-cp313t-win_arm64.whl", hash = "sha256:fc52ba896cfc3214dc9f9ca3c0677a623e8fdd096b257c14a31e719d21ff3fdd", size = 6262501 }, - { url = "https://files.pythonhosted.org/packages/42/17/c2b050d3f5b71b6edd0d96ae16c990fdc42a5f1366464a5c2772146de33a/pyproj-3.7.2-cp314-cp314-macosx_13_0_x86_64.whl", hash = "sha256:2aaa328605ace41db050d06bac1adc11f01b71fe95c18661497763116c3a0f02", size = 6214541 }, - { url = "https://files.pythonhosted.org/packages/03/68/68ada9c8aea96ded09a66cfd9bf87aa6db8c2edebe93f5bf9b66b0143fbc/pyproj-3.7.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:35dccbce8201313c596a970fde90e33605248b66272595c061b511c8100ccc08", size = 4617456 }, - { url = "https://files.pythonhosted.org/packages/81/e4/4c50ceca7d0e937977866b02cb64e6ccf4df979a5871e521f9e255df6073/pyproj-3.7.2-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:25b0b7cb0042444c29a164b993c45c1b8013d6c48baa61dc1160d834a277e83b", size = 9615590 }, - { url = "https://files.pythonhosted.org/packages/05/1e/ada6fb15a1d75b5bd9b554355a69a798c55a7dcc93b8d41596265c1772e3/pyproj-3.7.2-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:85def3a6388e9ba51f964619aa002a9d2098e77c6454ff47773bb68871024281", size = 9474960 }, - { url = "https://files.pythonhosted.org/packages/51/07/9d48ad0a8db36e16f842f2c8a694c1d9d7dcf9137264846bef77585a71f3/pyproj-3.7.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b1bccefec3875ab81eabf49059e2b2ea77362c178b66fd3528c3e4df242f1516", size = 10799478 }, - { url = "https://files.pythonhosted.org/packages/85/cf/2f812b529079f72f51ff2d6456b7fef06c01735e5cfd62d54ffb2b548028/pyproj-3.7.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d5371ca114d6990b675247355a801925814eca53e6c4b2f1b5c0a956336ee36e", size = 10710030 }, - { url = "https://files.pythonhosted.org/packages/99/9b/4626a19e1f03eba4c0e77b91a6cf0f73aa9cb5d51a22ee385c22812bcc2c/pyproj-3.7.2-cp314-cp314-win32.whl", hash = "sha256:77f066626030f41be543274f5ac79f2a511fe89860ecd0914f22131b40a0ec25", size = 5991181 }, - { url = "https://files.pythonhosted.org/packages/04/b2/5a6610554306a83a563080c2cf2c57565563eadd280e15388efa00fb5b33/pyproj-3.7.2-cp314-cp314-win_amd64.whl", hash = "sha256:5a964da1696b8522806f4276ab04ccfff8f9eb95133a92a25900697609d40112", size = 6434721 }, - { url = "https://files.pythonhosted.org/packages/ae/ce/6c910ea2e1c74ef673c5d48c482564b8a7824a44c4e35cca2e765b68cfcc/pyproj-3.7.2-cp314-cp314-win_arm64.whl", hash = "sha256:e258ab4dbd3cf627809067c0ba8f9884ea76c8e5999d039fb37a1619c6c3e1f6", size = 6363821 }, - { url = "https://files.pythonhosted.org/packages/e4/e4/5532f6f7491812ba782a2177fe9de73fd8e2912b59f46a1d056b84b9b8f2/pyproj-3.7.2-cp314-cp314t-macosx_13_0_x86_64.whl", hash = "sha256:bbbac2f930c6d266f70ec75df35ef851d96fdb3701c674f42fd23a9314573b37", size = 6241773 }, - { url = "https://files.pythonhosted.org/packages/20/1f/0938c3f2bbbef1789132d1726d9b0e662f10cfc22522743937f421ad664e/pyproj-3.7.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:b7544e0a3d6339dc9151e9c8f3ea62a936ab7cc446a806ec448bbe86aebb979b", size = 4652537 }, - { url = "https://files.pythonhosted.org/packages/c7/a8/488b1ed47d25972f33874f91f09ca8f2227902f05f63a2b80dc73e7b1c97/pyproj-3.7.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:f7f5133dca4c703e8acadf6f30bc567d39a42c6af321e7f81975c2518f3ed357", size = 9940864 }, - { url = "https://files.pythonhosted.org/packages/c7/cc/7f4c895d0cb98e47b6a85a6d79eaca03eb266129eed2f845125c09cf31ff/pyproj-3.7.2-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:5aff3343038d7426aa5076f07feb88065f50e0502d1b0d7c22ddfdd2c75a3f81", size = 9688868 }, - { url = "https://files.pythonhosted.org/packages/b2/b7/c7e306b8bb0f071d9825b753ee4920f066c40fbfcce9372c4f3cfb2fc4ed/pyproj-3.7.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b0552178c61f2ac1c820d087e8ba6e62b29442debddbb09d51c4bf8acc84d888", size = 11045910 }, - { url = "https://files.pythonhosted.org/packages/42/fb/538a4d2df695980e2dde5c04d965fbdd1fe8c20a3194dc4aaa3952a4d1be/pyproj-3.7.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:47d87db2d2c436c5fd0409b34d70bb6cdb875cca2ebe7a9d1c442367b0ab8d59", size = 10895724 }, - { url = "https://files.pythonhosted.org/packages/e8/8b/a3f0618b03957de9db5489a04558a8826f43906628bb0b766033aa3b5548/pyproj-3.7.2-cp314-cp314t-win32.whl", hash = "sha256:c9b6f1d8ad3e80a0ee0903a778b6ece7dca1d1d40f6d114ae01bc8ddbad971aa", size = 6056848 }, - { url = "https://files.pythonhosted.org/packages/bc/56/413240dd5149dd3291eda55aa55a659da4431244a2fd1319d0ae89407cfb/pyproj-3.7.2-cp314-cp314t-win_amd64.whl", hash = "sha256:1914e29e27933ba6f9822663ee0600f169014a2859f851c054c88cf5ea8a333c", size = 6517676 }, - { url = "https://files.pythonhosted.org/packages/15/73/a7141a1a0559bf1a7aa42a11c879ceb19f02f5c6c371c6d57fd86cefd4d1/pyproj-3.7.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d9d25bae416a24397e0d85739f84d323b55f6511e45a522dd7d7eae70d10c7e4", size = 6391844 }, +sdist = { url = "https://files.pythonhosted.org/packages/04/90/67bd7260b4ea9b8b20b4f58afef6c223ecb3abf368eb4ec5bc2cdef81b49/pyproj-3.7.2.tar.gz", hash = "sha256:39a0cf1ecc7e282d1d30f36594ebd55c9fae1fda8a2622cee5d100430628f88c", size = 226279, upload-time = "2025-08-14T12:05:42.18Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/bd/f205552cd1713b08f93b09e39a3ec99edef0b3ebbbca67b486fdf1abe2de/pyproj-3.7.2-cp311-cp311-macosx_13_0_x86_64.whl", hash = "sha256:2514d61f24c4e0bb9913e2c51487ecdaeca5f8748d8313c933693416ca41d4d5", size = 6227022, upload-time = "2025-08-14T12:03:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/75/4c/9a937e659b8b418ab573c6d340d27e68716928953273e0837e7922fcac34/pyproj-3.7.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:8693ca3892d82e70de077701ee76dd13d7bca4ae1c9d1e739d72004df015923a", size = 4625810, upload-time = "2025-08-14T12:03:53.808Z" }, + { url = "https://files.pythonhosted.org/packages/c0/7d/a9f41e814dc4d1dc54e95b2ccaf0b3ebe3eb18b1740df05fe334724c3d89/pyproj-3.7.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:5e26484d80fea56273ed1555abaea161e9661d81a6c07815d54b8e883d4ceb25", size = 9638694, upload-time = "2025-08-14T12:03:55.669Z" }, + { url = "https://files.pythonhosted.org/packages/ad/ab/9bdb4a6216b712a1f9aab1c0fcbee5d3726f34a366f29c3e8c08a78d6b70/pyproj-3.7.2-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:281cb92847814e8018010c48b4069ff858a30236638631c1a91dd7bfa68f8a8a", size = 9493977, upload-time = "2025-08-14T12:03:57.937Z" }, + { url = "https://files.pythonhosted.org/packages/c9/db/2db75b1b6190f1137b1c4e8ef6a22e1c338e46320f6329bfac819143e063/pyproj-3.7.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9c8577f0b7bb09118ec2e57e3babdc977127dd66326d6c5d755c76b063e6d9dc", size = 10841151, upload-time = "2025-08-14T12:04:00.271Z" }, + { url = "https://files.pythonhosted.org/packages/89/f7/989643394ba23a286e9b7b3f09981496172f9e0d4512457ffea7dc47ffc7/pyproj-3.7.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a23f59904fac3a5e7364b3aa44d288234af267ca041adb2c2b14a903cd5d3ac5", size = 10751585, upload-time = "2025-08-14T12:04:02.228Z" }, + { url = "https://files.pythonhosted.org/packages/53/6d/ad928fe975a6c14a093c92e6a319ca18f479f3336bb353a740bdba335681/pyproj-3.7.2-cp311-cp311-win32.whl", hash = "sha256:f2af4ed34b2cf3e031a2d85b067a3ecbd38df073c567e04b52fa7a0202afde8a", size = 5908533, upload-time = "2025-08-14T12:04:04.821Z" }, + { url = "https://files.pythonhosted.org/packages/79/e0/b95584605cec9ed50b7ebaf7975d1c4ddeec5a86b7a20554ed8b60042bd7/pyproj-3.7.2-cp311-cp311-win_amd64.whl", hash = "sha256:0b7cb633565129677b2a183c4d807c727d1c736fcb0568a12299383056e67433", size = 6320742, upload-time = "2025-08-14T12:04:06.357Z" }, + { url = "https://files.pythonhosted.org/packages/b7/4d/536e8f93bca808175c2d0a5ac9fdf69b960d8ab6b14f25030dccb07464d7/pyproj-3.7.2-cp311-cp311-win_arm64.whl", hash = "sha256:38b08d85e3a38e455625b80e9eb9f78027c8e2649a21dec4df1f9c3525460c71", size = 6245772, upload-time = "2025-08-14T12:04:08.365Z" }, + { url = "https://files.pythonhosted.org/packages/8d/ab/9893ea9fb066be70ed9074ae543914a618c131ed8dff2da1e08b3a4df4db/pyproj-3.7.2-cp312-cp312-macosx_13_0_x86_64.whl", hash = "sha256:0a9bb26a6356fb5b033433a6d1b4542158fb71e3c51de49b4c318a1dff3aeaab", size = 6219832, upload-time = "2025-08-14T12:04:10.264Z" }, + { url = "https://files.pythonhosted.org/packages/53/78/4c64199146eed7184eb0e85bedec60a4aa8853b6ffe1ab1f3a8b962e70a0/pyproj-3.7.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:567caa03021178861fad27fabde87500ec6d2ee173dd32f3e2d9871e40eebd68", size = 4620650, upload-time = "2025-08-14T12:04:11.978Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ac/14a78d17943898a93ef4f8c6a9d4169911c994e3161e54a7cedeba9d8dde/pyproj-3.7.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:c203101d1dc3c038a56cff0447acc515dd29d6e14811406ac539c21eed422b2a", size = 9667087, upload-time = "2025-08-14T12:04:13.964Z" }, + { url = "https://files.pythonhosted.org/packages/b8/be/212882c450bba74fc8d7d35cbd57e4af84792f0a56194819d98106b075af/pyproj-3.7.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:1edc34266c0c23ced85f95a1ee8b47c9035eae6aca5b6b340327250e8e281630", size = 9552797, upload-time = "2025-08-14T12:04:16.624Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c0/c0f25c87b5d2a8686341c53c1792a222a480d6c9caf60311fec12c99ec26/pyproj-3.7.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aa9f26c21bc0e2dc3d224cb1eb4020cf23e76af179a7c66fea49b828611e4260", size = 10837036, upload-time = "2025-08-14T12:04:18.733Z" }, + { url = "https://files.pythonhosted.org/packages/5d/37/5cbd6772addde2090c91113332623a86e8c7d583eccb2ad02ea634c4a89f/pyproj-3.7.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9428b318530625cb389b9ddc9c51251e172808a4af79b82809376daaeabe5e9", size = 10775952, upload-time = "2025-08-14T12:04:20.709Z" }, + { url = "https://files.pythonhosted.org/packages/69/a1/dc250e3cf83eb4b3b9a2cf86fdb5e25288bd40037ae449695550f9e96b2f/pyproj-3.7.2-cp312-cp312-win32.whl", hash = "sha256:b3d99ed57d319da042f175f4554fc7038aa4bcecc4ac89e217e350346b742c9d", size = 5898872, upload-time = "2025-08-14T12:04:22.485Z" }, + { url = "https://files.pythonhosted.org/packages/4a/a6/6fe724b72b70f2b00152d77282e14964d60ab092ec225e67c196c9b463e5/pyproj-3.7.2-cp312-cp312-win_amd64.whl", hash = "sha256:11614a054cd86a2ed968a657d00987a86eeb91fdcbd9ad3310478685dc14a128", size = 6312176, upload-time = "2025-08-14T12:04:24.736Z" }, + { url = "https://files.pythonhosted.org/packages/5d/68/915cc32c02a91e76d02c8f55d5a138d6ef9e47a0d96d259df98f4842e558/pyproj-3.7.2-cp312-cp312-win_arm64.whl", hash = "sha256:509a146d1398bafe4f53273398c3bb0b4732535065fa995270e52a9d3676bca3", size = 6233452, upload-time = "2025-08-14T12:04:27.287Z" }, + { url = "https://files.pythonhosted.org/packages/be/14/faf1b90d267cea68d7e70662e7f88cefdb1bc890bd596c74b959e0517a72/pyproj-3.7.2-cp313-cp313-macosx_13_0_x86_64.whl", hash = "sha256:19466e529b1b15eeefdf8ff26b06fa745856c044f2f77bf0edbae94078c1dfa1", size = 6214580, upload-time = "2025-08-14T12:04:28.804Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/da9a45b184d375f62667f62eba0ca68569b0bd980a0bb7ffcc1d50440520/pyproj-3.7.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:c79b9b84c4a626c5dc324c0d666be0bfcebd99f7538d66e8898c2444221b3da7", size = 4615388, upload-time = "2025-08-14T12:04:30.553Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e7/d2b459a4a64bca328b712c1b544e109df88e5c800f7c143cfbc404d39bfb/pyproj-3.7.2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:ceecf374cacca317bc09e165db38ac548ee3cad07c3609442bd70311c59c21aa", size = 9628455, upload-time = "2025-08-14T12:04:32.435Z" }, + { url = "https://files.pythonhosted.org/packages/f8/85/c2b1706e51942de19076eff082f8495e57d5151364e78b5bef4af4a1d94a/pyproj-3.7.2-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5141a538ffdbe4bfd157421828bb2e07123a90a7a2d6f30fa1462abcfb5ce681", size = 9514269, upload-time = "2025-08-14T12:04:34.599Z" }, + { url = "https://files.pythonhosted.org/packages/34/38/07a9b89ae7467872f9a476883a5bad9e4f4d1219d31060f0f2b282276cbe/pyproj-3.7.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f000841e98ea99acbb7b8ca168d67773b0191de95187228a16110245c5d954d5", size = 10808437, upload-time = "2025-08-14T12:04:36.485Z" }, + { url = "https://files.pythonhosted.org/packages/12/56/fda1daeabbd39dec5b07f67233d09f31facb762587b498e6fc4572be9837/pyproj-3.7.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8115faf2597f281a42ab608ceac346b4eb1383d3b45ab474fd37341c4bf82a67", size = 10745540, upload-time = "2025-08-14T12:04:38.568Z" }, + { url = "https://files.pythonhosted.org/packages/0d/90/c793182cbba65a39a11db2ac6b479fe76c59e6509ae75e5744c344a0da9d/pyproj-3.7.2-cp313-cp313-win32.whl", hash = "sha256:f18c0579dd6be00b970cb1a6719197fceecc407515bab37da0066f0184aafdf3", size = 5896506, upload-time = "2025-08-14T12:04:41.059Z" }, + { url = "https://files.pythonhosted.org/packages/be/0f/747974129cf0d800906f81cd25efd098c96509026e454d4b66868779ab04/pyproj-3.7.2-cp313-cp313-win_amd64.whl", hash = "sha256:bb41c29d5f60854b1075853fe80c58950b398d4ebb404eb532536ac8d2834ed7", size = 6310195, upload-time = "2025-08-14T12:04:42.974Z" }, + { url = "https://files.pythonhosted.org/packages/82/64/fc7598a53172c4931ec6edf5228280663063150625d3f6423b4c20f9daff/pyproj-3.7.2-cp313-cp313-win_arm64.whl", hash = "sha256:2b617d573be4118c11cd96b8891a0b7f65778fa7733ed8ecdb297a447d439100", size = 6230748, upload-time = "2025-08-14T12:04:44.491Z" }, + { url = "https://files.pythonhosted.org/packages/aa/f0/611dd5cddb0d277f94b7af12981f56e1441bf8d22695065d4f0df5218498/pyproj-3.7.2-cp313-cp313t-macosx_13_0_x86_64.whl", hash = "sha256:d27b48f0e81beeaa2b4d60c516c3a1cfbb0c7ff6ef71256d8e9c07792f735279", size = 6241729, upload-time = "2025-08-14T12:04:46.274Z" }, + { url = "https://files.pythonhosted.org/packages/15/93/40bd4a6c523ff9965e480870611aed7eda5aa2c6128c6537345a2b77b542/pyproj-3.7.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:55a3610d75023c7b1c6e583e48ef8f62918e85a2ae81300569d9f104d6684bb6", size = 4652497, upload-time = "2025-08-14T12:04:48.203Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ae/7150ead53c117880b35e0d37960d3138fe640a235feb9605cb9386f50bb0/pyproj-3.7.2-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:8d7349182fa622696787cc9e195508d2a41a64765da9b8a6bee846702b9e6220", size = 9942610, upload-time = "2025-08-14T12:04:49.652Z" }, + { url = "https://files.pythonhosted.org/packages/d8/17/7a4a7eafecf2b46ab64e5c08176c20ceb5844b503eaa551bf12ccac77322/pyproj-3.7.2-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:d230b186eb876ed4f29a7c5ee310144c3a0e44e89e55f65fb3607e13f6db337c", size = 9692390, upload-time = "2025-08-14T12:04:51.731Z" }, + { url = "https://files.pythonhosted.org/packages/c3/55/ae18f040f6410f0ea547a21ada7ef3e26e6c82befa125b303b02759c0e9d/pyproj-3.7.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:237499c7862c578d0369e2b8ac56eec550e391a025ff70e2af8417139dabb41c", size = 11047596, upload-time = "2025-08-14T12:04:53.748Z" }, + { url = "https://files.pythonhosted.org/packages/e6/2e/d3fff4d2909473f26ae799f9dda04caa322c417a51ff3b25763f7d03b233/pyproj-3.7.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8c225f5978abd506fd9a78eaaf794435e823c9156091cabaab5374efb29d7f69", size = 10896975, upload-time = "2025-08-14T12:04:55.875Z" }, + { url = "https://files.pythonhosted.org/packages/f2/bc/8fc7d3963d87057b7b51ebe68c1e7c51c23129eee5072ba6b86558544a46/pyproj-3.7.2-cp313-cp313t-win32.whl", hash = "sha256:2da731876d27639ff9d2d81c151f6ab90a1546455fabd93368e753047be344a2", size = 5953057, upload-time = "2025-08-14T12:04:58.466Z" }, + { url = "https://files.pythonhosted.org/packages/cc/27/ea9809966cc47d2d51e6d5ae631ea895f7c7c7b9b3c29718f900a8f7d197/pyproj-3.7.2-cp313-cp313t-win_amd64.whl", hash = "sha256:f54d91ae18dd23b6c0ab48126d446820e725419da10617d86a1b69ada6d881d3", size = 6375414, upload-time = "2025-08-14T12:04:59.861Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f8/1ef0129fba9a555c658e22af68989f35e7ba7b9136f25758809efec0cd6e/pyproj-3.7.2-cp313-cp313t-win_arm64.whl", hash = "sha256:fc52ba896cfc3214dc9f9ca3c0677a623e8fdd096b257c14a31e719d21ff3fdd", size = 6262501, upload-time = "2025-08-14T12:05:01.39Z" }, + { url = "https://files.pythonhosted.org/packages/42/17/c2b050d3f5b71b6edd0d96ae16c990fdc42a5f1366464a5c2772146de33a/pyproj-3.7.2-cp314-cp314-macosx_13_0_x86_64.whl", hash = "sha256:2aaa328605ace41db050d06bac1adc11f01b71fe95c18661497763116c3a0f02", size = 6214541, upload-time = "2025-08-14T12:05:03.166Z" }, + { url = "https://files.pythonhosted.org/packages/03/68/68ada9c8aea96ded09a66cfd9bf87aa6db8c2edebe93f5bf9b66b0143fbc/pyproj-3.7.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:35dccbce8201313c596a970fde90e33605248b66272595c061b511c8100ccc08", size = 4617456, upload-time = "2025-08-14T12:05:04.563Z" }, + { url = "https://files.pythonhosted.org/packages/81/e4/4c50ceca7d0e937977866b02cb64e6ccf4df979a5871e521f9e255df6073/pyproj-3.7.2-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:25b0b7cb0042444c29a164b993c45c1b8013d6c48baa61dc1160d834a277e83b", size = 9615590, upload-time = "2025-08-14T12:05:06.094Z" }, + { url = "https://files.pythonhosted.org/packages/05/1e/ada6fb15a1d75b5bd9b554355a69a798c55a7dcc93b8d41596265c1772e3/pyproj-3.7.2-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:85def3a6388e9ba51f964619aa002a9d2098e77c6454ff47773bb68871024281", size = 9474960, upload-time = "2025-08-14T12:05:07.973Z" }, + { url = "https://files.pythonhosted.org/packages/51/07/9d48ad0a8db36e16f842f2c8a694c1d9d7dcf9137264846bef77585a71f3/pyproj-3.7.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b1bccefec3875ab81eabf49059e2b2ea77362c178b66fd3528c3e4df242f1516", size = 10799478, upload-time = "2025-08-14T12:05:14.102Z" }, + { url = "https://files.pythonhosted.org/packages/85/cf/2f812b529079f72f51ff2d6456b7fef06c01735e5cfd62d54ffb2b548028/pyproj-3.7.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d5371ca114d6990b675247355a801925814eca53e6c4b2f1b5c0a956336ee36e", size = 10710030, upload-time = "2025-08-14T12:05:16.317Z" }, + { url = "https://files.pythonhosted.org/packages/99/9b/4626a19e1f03eba4c0e77b91a6cf0f73aa9cb5d51a22ee385c22812bcc2c/pyproj-3.7.2-cp314-cp314-win32.whl", hash = "sha256:77f066626030f41be543274f5ac79f2a511fe89860ecd0914f22131b40a0ec25", size = 5991181, upload-time = "2025-08-14T12:05:19.492Z" }, + { url = "https://files.pythonhosted.org/packages/04/b2/5a6610554306a83a563080c2cf2c57565563eadd280e15388efa00fb5b33/pyproj-3.7.2-cp314-cp314-win_amd64.whl", hash = "sha256:5a964da1696b8522806f4276ab04ccfff8f9eb95133a92a25900697609d40112", size = 6434721, upload-time = "2025-08-14T12:05:21.022Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ce/6c910ea2e1c74ef673c5d48c482564b8a7824a44c4e35cca2e765b68cfcc/pyproj-3.7.2-cp314-cp314-win_arm64.whl", hash = "sha256:e258ab4dbd3cf627809067c0ba8f9884ea76c8e5999d039fb37a1619c6c3e1f6", size = 6363821, upload-time = "2025-08-14T12:05:22.627Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e4/5532f6f7491812ba782a2177fe9de73fd8e2912b59f46a1d056b84b9b8f2/pyproj-3.7.2-cp314-cp314t-macosx_13_0_x86_64.whl", hash = "sha256:bbbac2f930c6d266f70ec75df35ef851d96fdb3701c674f42fd23a9314573b37", size = 6241773, upload-time = "2025-08-14T12:05:24.577Z" }, + { url = "https://files.pythonhosted.org/packages/20/1f/0938c3f2bbbef1789132d1726d9b0e662f10cfc22522743937f421ad664e/pyproj-3.7.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:b7544e0a3d6339dc9151e9c8f3ea62a936ab7cc446a806ec448bbe86aebb979b", size = 4652537, upload-time = "2025-08-14T12:05:26.391Z" }, + { url = "https://files.pythonhosted.org/packages/c7/a8/488b1ed47d25972f33874f91f09ca8f2227902f05f63a2b80dc73e7b1c97/pyproj-3.7.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:f7f5133dca4c703e8acadf6f30bc567d39a42c6af321e7f81975c2518f3ed357", size = 9940864, upload-time = "2025-08-14T12:05:27.985Z" }, + { url = "https://files.pythonhosted.org/packages/c7/cc/7f4c895d0cb98e47b6a85a6d79eaca03eb266129eed2f845125c09cf31ff/pyproj-3.7.2-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:5aff3343038d7426aa5076f07feb88065f50e0502d1b0d7c22ddfdd2c75a3f81", size = 9688868, upload-time = "2025-08-14T12:05:30.425Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b7/c7e306b8bb0f071d9825b753ee4920f066c40fbfcce9372c4f3cfb2fc4ed/pyproj-3.7.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b0552178c61f2ac1c820d087e8ba6e62b29442debddbb09d51c4bf8acc84d888", size = 11045910, upload-time = "2025-08-14T12:05:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/42/fb/538a4d2df695980e2dde5c04d965fbdd1fe8c20a3194dc4aaa3952a4d1be/pyproj-3.7.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:47d87db2d2c436c5fd0409b34d70bb6cdb875cca2ebe7a9d1c442367b0ab8d59", size = 10895724, upload-time = "2025-08-14T12:05:35.465Z" }, + { url = "https://files.pythonhosted.org/packages/e8/8b/a3f0618b03957de9db5489a04558a8826f43906628bb0b766033aa3b5548/pyproj-3.7.2-cp314-cp314t-win32.whl", hash = "sha256:c9b6f1d8ad3e80a0ee0903a778b6ece7dca1d1d40f6d114ae01bc8ddbad971aa", size = 6056848, upload-time = "2025-08-14T12:05:37.553Z" }, + { url = "https://files.pythonhosted.org/packages/bc/56/413240dd5149dd3291eda55aa55a659da4431244a2fd1319d0ae89407cfb/pyproj-3.7.2-cp314-cp314t-win_amd64.whl", hash = "sha256:1914e29e27933ba6f9822663ee0600f169014a2859f851c054c88cf5ea8a333c", size = 6517676, upload-time = "2025-08-14T12:05:39.126Z" }, + { url = "https://files.pythonhosted.org/packages/15/73/a7141a1a0559bf1a7aa42a11c879ceb19f02f5c6c371c6d57fd86cefd4d1/pyproj-3.7.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d9d25bae416a24397e0d85739f84d323b55f6511e45a522dd7d7eae70d10c7e4", size = 6391844, upload-time = "2025-08-14T12:05:40.745Z" }, ] [[package]] @@ -1331,9 +1331,9 @@ dependencies = [ { name = "pluggy" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/07/56/f013048ac4bc4c1d9be45afd4ab209ea62822fb1598f40687e6bf45dcea4/pytest-9.0.1.tar.gz", hash = "sha256:3e9c069ea73583e255c3b21cf46b8d3c56f6e3a1a8f6da94ccb0fcf57b9d73c8", size = 1564125 } +sdist = { url = "https://files.pythonhosted.org/packages/07/56/f013048ac4bc4c1d9be45afd4ab209ea62822fb1598f40687e6bf45dcea4/pytest-9.0.1.tar.gz", hash = "sha256:3e9c069ea73583e255c3b21cf46b8d3c56f6e3a1a8f6da94ccb0fcf57b9d73c8", size = 1564125, upload-time = "2025-11-12T13:05:09.333Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/8b/6300fb80f858cda1c51ffa17075df5d846757081d11ab4aa35cef9e6258b/pytest-9.0.1-py3-none-any.whl", hash = "sha256:67be0030d194df2dfa7b556f2e56fb3c3315bd5c8822c6951162b92b32ce7dad", size = 373668 }, + { url = "https://files.pythonhosted.org/packages/0b/8b/6300fb80f858cda1c51ffa17075df5d846757081d11ab4aa35cef9e6258b/pytest-9.0.1-py3-none-any.whl", hash = "sha256:67be0030d194df2dfa7b556f2e56fb3c3315bd5c8822c6951162b92b32ce7dad", size = 373668, upload-time = "2025-11-12T13:05:07.379Z" }, ] [[package]] @@ -1345,9 +1345,9 @@ dependencies = [ { name = "pluggy" }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328 } +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424 }, + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, ] [[package]] @@ -1357,82 +1357,82 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "six" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] [[package]] name = "python-dotenv" version = "1.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221 } +sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230 }, + { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, ] [[package]] name = "pytz" version = "2025.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884 } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225 }, + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, ] [[package]] name = "pyyaml" version = "6.0.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826 }, - { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577 }, - { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556 }, - { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114 }, - { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638 }, - { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463 }, - { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986 }, - { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543 }, - { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763 }, - { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063 }, - { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973 }, - { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116 }, - { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011 }, - { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870 }, - { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089 }, - { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181 }, - { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658 }, - { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003 }, - { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344 }, - { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669 }, - { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252 }, - { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081 }, - { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159 }, - { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626 }, - { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613 }, - { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115 }, - { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427 }, - { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090 }, - { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246 }, - { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814 }, - { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809 }, - { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454 }, - { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355 }, - { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175 }, - { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228 }, - { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194 }, - { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429 }, - { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912 }, - { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108 }, - { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641 }, - { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901 }, - { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132 }, - { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261 }, - { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272 }, - { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923 }, - { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062 }, - { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341 }, +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] [[package]] @@ -1442,9 +1442,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737 } +sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722 }, + { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, ] [[package]] @@ -1457,9 +1457,9 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517 } +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738 }, + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, ] [[package]] @@ -1470,35 +1470,35 @@ dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990 } +sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393 }, + { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" }, ] [[package]] name = "ruff" version = "0.14.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b7/5b/dd7406afa6c95e3d8fa9d652b6d6dd17dd4a6bf63cb477014e8ccd3dcd46/ruff-0.14.7.tar.gz", hash = "sha256:3417deb75d23bd14a722b57b0a1435561db65f0ad97435b4cf9f85ffcef34ae5", size = 5727324 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8c/b1/7ea5647aaf90106f6d102230e5df874613da43d1089864da1553b899ba5e/ruff-0.14.7-py3-none-linux_armv6l.whl", hash = "sha256:b9d5cb5a176c7236892ad7224bc1e63902e4842c460a0b5210701b13e3de4fca", size = 13414475 }, - { url = "https://files.pythonhosted.org/packages/af/19/fddb4cd532299db9cdaf0efdc20f5c573ce9952a11cb532d3b859d6d9871/ruff-0.14.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3f64fe375aefaf36ca7d7250292141e39b4cea8250427482ae779a2aa5d90015", size = 13634613 }, - { url = "https://files.pythonhosted.org/packages/40/2b/469a66e821d4f3de0440676ed3e04b8e2a1dc7575cf6fa3ba6d55e3c8557/ruff-0.14.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:93e83bd3a9e1a3bda64cb771c0d47cda0e0d148165013ae2d3554d718632d554", size = 12765458 }, - { url = "https://files.pythonhosted.org/packages/f1/05/0b001f734fe550bcfde4ce845948ac620ff908ab7241a39a1b39bb3c5f49/ruff-0.14.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3838948e3facc59a6070795de2ae16e5786861850f78d5914a03f12659e88f94", size = 13236412 }, - { url = "https://files.pythonhosted.org/packages/11/36/8ed15d243f011b4e5da75cd56d6131c6766f55334d14ba31cce5461f28aa/ruff-0.14.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:24c8487194d38b6d71cd0fd17a5b6715cda29f59baca1defe1e3a03240f851d1", size = 13182949 }, - { url = "https://files.pythonhosted.org/packages/3b/cf/fcb0b5a195455729834f2a6eadfe2e4519d8ca08c74f6d2b564a4f18f553/ruff-0.14.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:79c73db6833f058a4be8ffe4a0913b6d4ad41f6324745179bd2aa09275b01d0b", size = 13816470 }, - { url = "https://files.pythonhosted.org/packages/7f/5d/34a4748577ff7a5ed2f2471456740f02e86d1568a18c9faccfc73bd9ca3f/ruff-0.14.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:12eb7014fccff10fc62d15c79d8a6be4d0c2d60fe3f8e4d169a0d2def75f5dad", size = 15289621 }, - { url = "https://files.pythonhosted.org/packages/53/53/0a9385f047a858ba133d96f3f8e3c9c66a31cc7c4b445368ef88ebeac209/ruff-0.14.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c623bbdc902de7ff715a93fa3bb377a4e42dd696937bf95669118773dbf0c50", size = 14975817 }, - { url = "https://files.pythonhosted.org/packages/a8/d7/2f1c32af54c3b46e7fadbf8006d8b9bcfbea535c316b0bd8813d6fb25e5d/ruff-0.14.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f53accc02ed2d200fa621593cdb3c1ae06aa9b2c3cae70bc96f72f0000ae97a9", size = 14284549 }, - { url = "https://files.pythonhosted.org/packages/92/05/434ddd86becd64629c25fb6b4ce7637dd52a45cc4a4415a3008fe61c27b9/ruff-0.14.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:281f0e61a23fcdcffca210591f0f53aafaa15f9025b5b3f9706879aaa8683bc4", size = 14071389 }, - { url = "https://files.pythonhosted.org/packages/ff/50/fdf89d4d80f7f9d4f420d26089a79b3bb1538fe44586b148451bc2ba8d9c/ruff-0.14.7-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:dbbaa5e14148965b91cb090236931182ee522a5fac9bc5575bafc5c07b9f9682", size = 14202679 }, - { url = "https://files.pythonhosted.org/packages/77/54/87b34988984555425ce967f08a36df0ebd339bb5d9d0e92a47e41151eafc/ruff-0.14.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1464b6e54880c0fe2f2d6eaefb6db15373331414eddf89d6b903767ae2458143", size = 13147677 }, - { url = "https://files.pythonhosted.org/packages/67/29/f55e4d44edfe053918a16a3299e758e1c18eef216b7a7092550d7a9ec51c/ruff-0.14.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f217ed871e4621ea6128460df57b19ce0580606c23aeab50f5de425d05226784", size = 13151392 }, - { url = "https://files.pythonhosted.org/packages/36/69/47aae6dbd4f1d9b4f7085f4d9dcc84e04561ee7ad067bf52e0f9b02e3209/ruff-0.14.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6be02e849440ed3602d2eb478ff7ff07d53e3758f7948a2a598829660988619e", size = 13412230 }, - { url = "https://files.pythonhosted.org/packages/b7/4b/6e96cb6ba297f2ba502a231cd732ed7c3de98b1a896671b932a5eefa3804/ruff-0.14.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:19a0f116ee5e2b468dfe80c41c84e2bbd6b74f7b719bee86c2ecde0a34563bcc", size = 14195397 }, - { url = "https://files.pythonhosted.org/packages/69/82/251d5f1aa4dcad30aed491b4657cecd9fb4274214da6960ffec144c260f7/ruff-0.14.7-py3-none-win32.whl", hash = "sha256:e33052c9199b347c8937937163b9b149ef6ab2e4bb37b042e593da2e6f6cccfa", size = 13126751 }, - { url = "https://files.pythonhosted.org/packages/a8/b5/d0b7d145963136b564806f6584647af45ab98946660d399ec4da79cae036/ruff-0.14.7-py3-none-win_amd64.whl", hash = "sha256:e17a20ad0d3fad47a326d773a042b924d3ac31c6ca6deb6c72e9e6b5f661a7c6", size = 14531726 }, - { url = "https://files.pythonhosted.org/packages/1d/d2/1637f4360ada6a368d3265bf39f2cf737a0aaab15ab520fc005903e883f8/ruff-0.14.7-py3-none-win_arm64.whl", hash = "sha256:be4d653d3bea1b19742fcc6502354e32f65cd61ff2fbdb365803ef2c2aec6228", size = 13609215 }, +sdist = { url = "https://files.pythonhosted.org/packages/b7/5b/dd7406afa6c95e3d8fa9d652b6d6dd17dd4a6bf63cb477014e8ccd3dcd46/ruff-0.14.7.tar.gz", hash = "sha256:3417deb75d23bd14a722b57b0a1435561db65f0ad97435b4cf9f85ffcef34ae5", size = 5727324, upload-time = "2025-11-28T20:55:10.525Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/b1/7ea5647aaf90106f6d102230e5df874613da43d1089864da1553b899ba5e/ruff-0.14.7-py3-none-linux_armv6l.whl", hash = "sha256:b9d5cb5a176c7236892ad7224bc1e63902e4842c460a0b5210701b13e3de4fca", size = 13414475, upload-time = "2025-11-28T20:54:54.569Z" }, + { url = "https://files.pythonhosted.org/packages/af/19/fddb4cd532299db9cdaf0efdc20f5c573ce9952a11cb532d3b859d6d9871/ruff-0.14.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3f64fe375aefaf36ca7d7250292141e39b4cea8250427482ae779a2aa5d90015", size = 13634613, upload-time = "2025-11-28T20:55:17.54Z" }, + { url = "https://files.pythonhosted.org/packages/40/2b/469a66e821d4f3de0440676ed3e04b8e2a1dc7575cf6fa3ba6d55e3c8557/ruff-0.14.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:93e83bd3a9e1a3bda64cb771c0d47cda0e0d148165013ae2d3554d718632d554", size = 12765458, upload-time = "2025-11-28T20:55:26.128Z" }, + { url = "https://files.pythonhosted.org/packages/f1/05/0b001f734fe550bcfde4ce845948ac620ff908ab7241a39a1b39bb3c5f49/ruff-0.14.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3838948e3facc59a6070795de2ae16e5786861850f78d5914a03f12659e88f94", size = 13236412, upload-time = "2025-11-28T20:55:28.602Z" }, + { url = "https://files.pythonhosted.org/packages/11/36/8ed15d243f011b4e5da75cd56d6131c6766f55334d14ba31cce5461f28aa/ruff-0.14.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:24c8487194d38b6d71cd0fd17a5b6715cda29f59baca1defe1e3a03240f851d1", size = 13182949, upload-time = "2025-11-28T20:55:33.265Z" }, + { url = "https://files.pythonhosted.org/packages/3b/cf/fcb0b5a195455729834f2a6eadfe2e4519d8ca08c74f6d2b564a4f18f553/ruff-0.14.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:79c73db6833f058a4be8ffe4a0913b6d4ad41f6324745179bd2aa09275b01d0b", size = 13816470, upload-time = "2025-11-28T20:55:08.203Z" }, + { url = "https://files.pythonhosted.org/packages/7f/5d/34a4748577ff7a5ed2f2471456740f02e86d1568a18c9faccfc73bd9ca3f/ruff-0.14.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:12eb7014fccff10fc62d15c79d8a6be4d0c2d60fe3f8e4d169a0d2def75f5dad", size = 15289621, upload-time = "2025-11-28T20:55:30.837Z" }, + { url = "https://files.pythonhosted.org/packages/53/53/0a9385f047a858ba133d96f3f8e3c9c66a31cc7c4b445368ef88ebeac209/ruff-0.14.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c623bbdc902de7ff715a93fa3bb377a4e42dd696937bf95669118773dbf0c50", size = 14975817, upload-time = "2025-11-28T20:55:24.107Z" }, + { url = "https://files.pythonhosted.org/packages/a8/d7/2f1c32af54c3b46e7fadbf8006d8b9bcfbea535c316b0bd8813d6fb25e5d/ruff-0.14.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f53accc02ed2d200fa621593cdb3c1ae06aa9b2c3cae70bc96f72f0000ae97a9", size = 14284549, upload-time = "2025-11-28T20:55:06.08Z" }, + { url = "https://files.pythonhosted.org/packages/92/05/434ddd86becd64629c25fb6b4ce7637dd52a45cc4a4415a3008fe61c27b9/ruff-0.14.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:281f0e61a23fcdcffca210591f0f53aafaa15f9025b5b3f9706879aaa8683bc4", size = 14071389, upload-time = "2025-11-28T20:55:35.617Z" }, + { url = "https://files.pythonhosted.org/packages/ff/50/fdf89d4d80f7f9d4f420d26089a79b3bb1538fe44586b148451bc2ba8d9c/ruff-0.14.7-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:dbbaa5e14148965b91cb090236931182ee522a5fac9bc5575bafc5c07b9f9682", size = 14202679, upload-time = "2025-11-28T20:55:01.472Z" }, + { url = "https://files.pythonhosted.org/packages/77/54/87b34988984555425ce967f08a36df0ebd339bb5d9d0e92a47e41151eafc/ruff-0.14.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1464b6e54880c0fe2f2d6eaefb6db15373331414eddf89d6b903767ae2458143", size = 13147677, upload-time = "2025-11-28T20:55:19.933Z" }, + { url = "https://files.pythonhosted.org/packages/67/29/f55e4d44edfe053918a16a3299e758e1c18eef216b7a7092550d7a9ec51c/ruff-0.14.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f217ed871e4621ea6128460df57b19ce0580606c23aeab50f5de425d05226784", size = 13151392, upload-time = "2025-11-28T20:55:21.967Z" }, + { url = "https://files.pythonhosted.org/packages/36/69/47aae6dbd4f1d9b4f7085f4d9dcc84e04561ee7ad067bf52e0f9b02e3209/ruff-0.14.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6be02e849440ed3602d2eb478ff7ff07d53e3758f7948a2a598829660988619e", size = 13412230, upload-time = "2025-11-28T20:55:12.749Z" }, + { url = "https://files.pythonhosted.org/packages/b7/4b/6e96cb6ba297f2ba502a231cd732ed7c3de98b1a896671b932a5eefa3804/ruff-0.14.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:19a0f116ee5e2b468dfe80c41c84e2bbd6b74f7b719bee86c2ecde0a34563bcc", size = 14195397, upload-time = "2025-11-28T20:54:56.896Z" }, + { url = "https://files.pythonhosted.org/packages/69/82/251d5f1aa4dcad30aed491b4657cecd9fb4274214da6960ffec144c260f7/ruff-0.14.7-py3-none-win32.whl", hash = "sha256:e33052c9199b347c8937937163b9b149ef6ab2e4bb37b042e593da2e6f6cccfa", size = 13126751, upload-time = "2025-11-28T20:55:03.47Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b5/d0b7d145963136b564806f6584647af45ab98946660d399ec4da79cae036/ruff-0.14.7-py3-none-win_amd64.whl", hash = "sha256:e17a20ad0d3fad47a326d773a042b924d3ac31c6ca6deb6c72e9e6b5f661a7c6", size = 14531726, upload-time = "2025-11-28T20:54:59.121Z" }, + { url = "https://files.pythonhosted.org/packages/1d/d2/1637f4360ada6a368d3265bf39f2cf737a0aaab15ab520fc005903e883f8/ruff-0.14.7-py3-none-win_arm64.whl", hash = "sha256:be4d653d3bea1b19742fcc6502354e32f65cd61ff2fbdb365803ef2c2aec6228", size = 13609215, upload-time = "2025-11-28T20:55:15.375Z" }, ] [[package]] @@ -1508,123 +1508,123 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4d/bc/0989043118a27cccb4e906a46b7565ce36ca7b57f5a18b78f4f1b0f72d9d/shapely-2.1.2.tar.gz", hash = "sha256:2ed4ecb28320a433db18a5bf029986aa8afcfd740745e78847e330d5d94922a9", size = 315489 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/8d/1ff672dea9ec6a7b5d422eb6d095ed886e2e523733329f75fdcb14ee1149/shapely-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:91121757b0a36c9aac3427a651a7e6567110a4a67c97edf04f8d55d4765f6618", size = 1820038 }, - { url = "https://files.pythonhosted.org/packages/4f/ce/28fab8c772ce5db23a0d86bf0adaee0c4c79d5ad1db766055fa3dab442e2/shapely-2.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:16a9c722ba774cf50b5d4541242b4cce05aafd44a015290c82ba8a16931ff63d", size = 1626039 }, - { url = "https://files.pythonhosted.org/packages/70/8b/868b7e3f4982f5006e9395c1e12343c66a8155c0374fdc07c0e6a1ab547d/shapely-2.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cc4f7397459b12c0b196c9efe1f9d7e92463cbba142632b4cc6d8bbbbd3e2b09", size = 3001519 }, - { url = "https://files.pythonhosted.org/packages/13/02/58b0b8d9c17c93ab6340edd8b7308c0c5a5b81f94ce65705819b7416dba5/shapely-2.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:136ab87b17e733e22f0961504d05e77e7be8c9b5a8184f685b4a91a84efe3c26", size = 3110842 }, - { url = "https://files.pythonhosted.org/packages/af/61/8e389c97994d5f331dcffb25e2fa761aeedfb52b3ad9bcdd7b8671f4810a/shapely-2.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:16c5d0fc45d3aa0a69074979f4f1928ca2734fb2e0dde8af9611e134e46774e7", size = 4021316 }, - { url = "https://files.pythonhosted.org/packages/d3/d4/9b2a9fe6039f9e42ccf2cb3e84f219fd8364b0c3b8e7bbc857b5fbe9c14c/shapely-2.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6ddc759f72b5b2b0f54a7e7cde44acef680a55019eb52ac63a7af2cf17cb9cd2", size = 4178586 }, - { url = "https://files.pythonhosted.org/packages/16/f6/9840f6963ed4decf76b08fd6d7fed14f8779fb7a62cb45c5617fa8ac6eab/shapely-2.1.2-cp311-cp311-win32.whl", hash = "sha256:2fa78b49485391224755a856ed3b3bd91c8455f6121fee0db0e71cefb07d0ef6", size = 1543961 }, - { url = "https://files.pythonhosted.org/packages/38/1e/3f8ea46353c2a33c1669eb7327f9665103aa3a8dfe7f2e4ef714c210b2c2/shapely-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:c64d5c97b2f47e3cd9b712eaced3b061f2b71234b3fc263e0fcf7d889c6559dc", size = 1722856 }, - { url = "https://files.pythonhosted.org/packages/24/c0/f3b6453cf2dfa99adc0ba6675f9aaff9e526d2224cbd7ff9c1a879238693/shapely-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fe2533caae6a91a543dec62e8360fe86ffcdc42a7c55f9dfd0128a977a896b94", size = 1833550 }, - { url = "https://files.pythonhosted.org/packages/86/07/59dee0bc4b913b7ab59ab1086225baca5b8f19865e6101db9ebb7243e132/shapely-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ba4d1333cc0bc94381d6d4308d2e4e008e0bd128bdcff5573199742ee3634359", size = 1643556 }, - { url = "https://files.pythonhosted.org/packages/26/29/a5397e75b435b9895cd53e165083faed5d12fd9626eadec15a83a2411f0f/shapely-2.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0bd308103340030feef6c111d3eb98d50dc13feea33affc8a6f9fa549e9458a3", size = 2988308 }, - { url = "https://files.pythonhosted.org/packages/b9/37/e781683abac55dde9771e086b790e554811a71ed0b2b8a1e789b7430dd44/shapely-2.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e7d4d7ad262a48bb44277ca12c7c78cb1b0f56b32c10734ec9a1d30c0b0c54b", size = 3099844 }, - { url = "https://files.pythonhosted.org/packages/d8/f3/9876b64d4a5a321b9dc482c92bb6f061f2fa42131cba643c699f39317cb9/shapely-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e9eddfe513096a71896441a7c37db72da0687b34752c4e193577a145c71736fc", size = 3988842 }, - { url = "https://files.pythonhosted.org/packages/d1/a0/704c7292f7014c7e74ec84eddb7b109e1fbae74a16deae9c1504b1d15565/shapely-2.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:980c777c612514c0cf99bc8a9de6d286f5e186dcaf9091252fcd444e5638193d", size = 4152714 }, - { url = "https://files.pythonhosted.org/packages/53/46/319c9dc788884ad0785242543cdffac0e6530e4d0deb6c4862bc4143dcf3/shapely-2.1.2-cp312-cp312-win32.whl", hash = "sha256:9111274b88e4d7b54a95218e243282709b330ef52b7b86bc6aaf4f805306f454", size = 1542745 }, - { url = "https://files.pythonhosted.org/packages/ec/bf/cb6c1c505cb31e818e900b9312d514f381fbfa5c4363edfce0fcc4f8c1a4/shapely-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:743044b4cfb34f9a67205cee9279feaf60ba7d02e69febc2afc609047cb49179", size = 1722861 }, - { url = "https://files.pythonhosted.org/packages/c3/90/98ef257c23c46425dc4d1d31005ad7c8d649fe423a38b917db02c30f1f5a/shapely-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b510dda1a3672d6879beb319bc7c5fd302c6c354584690973c838f46ec3e0fa8", size = 1832644 }, - { url = "https://files.pythonhosted.org/packages/6d/ab/0bee5a830d209adcd3a01f2d4b70e587cdd9fd7380d5198c064091005af8/shapely-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8cff473e81017594d20ec55d86b54bc635544897e13a7cfc12e36909c5309a2a", size = 1642887 }, - { url = "https://files.pythonhosted.org/packages/2d/5e/7d7f54ba960c13302584c73704d8c4d15404a51024631adb60b126a4ae88/shapely-2.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe7b77dc63d707c09726b7908f575fc04ff1d1ad0f3fb92aec212396bc6cfe5e", size = 2970931 }, - { url = "https://files.pythonhosted.org/packages/f2/a2/83fc37e2a58090e3d2ff79175a95493c664bcd0b653dd75cb9134645a4e5/shapely-2.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ed1a5bbfb386ee8332713bf7508bc24e32d24b74fc9a7b9f8529a55db9f4ee6", size = 3082855 }, - { url = "https://files.pythonhosted.org/packages/44/2b/578faf235a5b09f16b5f02833c53822294d7f21b242f8e2d0cf03fb64321/shapely-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a84e0582858d841d54355246ddfcbd1fce3179f185da7470f41ce39d001ee1af", size = 3979960 }, - { url = "https://files.pythonhosted.org/packages/4d/04/167f096386120f692cc4ca02f75a17b961858997a95e67a3cb6a7bbd6b53/shapely-2.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc3487447a43d42adcdf52d7ac73804f2312cbfa5d433a7d2c506dcab0033dfd", size = 4142851 }, - { url = "https://files.pythonhosted.org/packages/48/74/fb402c5a6235d1c65a97348b48cdedb75fb19eca2b1d66d04969fc1c6091/shapely-2.1.2-cp313-cp313-win32.whl", hash = "sha256:9c3a3c648aedc9f99c09263b39f2d8252f199cb3ac154fadc173283d7d111350", size = 1541890 }, - { url = "https://files.pythonhosted.org/packages/41/47/3647fe7ad990af60ad98b889657a976042c9988c2807cf322a9d6685f462/shapely-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:ca2591bff6645c216695bdf1614fca9c82ea1144d4a7591a466fef64f28f0715", size = 1722151 }, - { url = "https://files.pythonhosted.org/packages/3c/49/63953754faa51ffe7d8189bfbe9ca34def29f8c0e34c67cbe2a2795f269d/shapely-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2d93d23bdd2ed9dc157b46bc2f19b7da143ca8714464249bef6771c679d5ff40", size = 1834130 }, - { url = "https://files.pythonhosted.org/packages/7f/ee/dce001c1984052970ff60eb4727164892fb2d08052c575042a47f5a9e88f/shapely-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01d0d304b25634d60bd7cf291828119ab55a3bab87dc4af1e44b07fb225f188b", size = 1642802 }, - { url = "https://files.pythonhosted.org/packages/da/e7/fc4e9a19929522877fa602f705706b96e78376afb7fad09cad5b9af1553c/shapely-2.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8d8382dd120d64b03698b7298b89611a6ea6f55ada9d39942838b79c9bc89801", size = 3018460 }, - { url = "https://files.pythonhosted.org/packages/a1/18/7519a25db21847b525696883ddc8e6a0ecaa36159ea88e0fef11466384d0/shapely-2.1.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:19efa3611eef966e776183e338b2d7ea43569ae99ab34f8d17c2c054d3205cc0", size = 3095223 }, - { url = "https://files.pythonhosted.org/packages/48/de/b59a620b1f3a129c3fecc2737104a0a7e04e79335bd3b0a1f1609744cf17/shapely-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:346ec0c1a0fcd32f57f00e4134d1200e14bf3f5ae12af87ba83ca275c502498c", size = 4030760 }, - { url = "https://files.pythonhosted.org/packages/96/b3/c6655ee7232b417562bae192ae0d3ceaadb1cc0ffc2088a2ddf415456cc2/shapely-2.1.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6305993a35989391bd3476ee538a5c9a845861462327efe00dd11a5c8c709a99", size = 4170078 }, - { url = "https://files.pythonhosted.org/packages/a0/8e/605c76808d73503c9333af8f6cbe7e1354d2d238bda5f88eea36bfe0f42a/shapely-2.1.2-cp313-cp313t-win32.whl", hash = "sha256:c8876673449f3401f278c86eb33224c5764582f72b653a415d0e6672fde887bf", size = 1559178 }, - { url = "https://files.pythonhosted.org/packages/36/f7/d317eb232352a1f1444d11002d477e54514a4a6045536d49d0c59783c0da/shapely-2.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:4a44bc62a10d84c11a7a3d7c1c4fe857f7477c3506e24c9062da0db0ae0c449c", size = 1739756 }, - { url = "https://files.pythonhosted.org/packages/fc/c4/3ce4c2d9b6aabd27d26ec988f08cb877ba9e6e96086eff81bfea93e688c7/shapely-2.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:9a522f460d28e2bf4e12396240a5fc1518788b2fcd73535166d748399ef0c223", size = 1831290 }, - { url = "https://files.pythonhosted.org/packages/17/b9/f6ab8918fc15429f79cb04afa9f9913546212d7fb5e5196132a2af46676b/shapely-2.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ff629e00818033b8d71139565527ced7d776c269a49bd78c9df84e8f852190c", size = 1641463 }, - { url = "https://files.pythonhosted.org/packages/a5/57/91d59ae525ca641e7ac5551c04c9503aee6f29b92b392f31790fcb1a4358/shapely-2.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f67b34271dedc3c653eba4e3d7111aa421d5be9b4c4c7d38d30907f796cb30df", size = 2970145 }, - { url = "https://files.pythonhosted.org/packages/8a/cb/4948be52ee1da6927831ab59e10d4c29baa2a714f599f1f0d1bc747f5777/shapely-2.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:21952dc00df38a2c28375659b07a3979d22641aeb104751e769c3ee825aadecf", size = 3073806 }, - { url = "https://files.pythonhosted.org/packages/03/83/f768a54af775eb41ef2e7bec8a0a0dbe7d2431c3e78c0a8bdba7ab17e446/shapely-2.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1f2f33f486777456586948e333a56ae21f35ae273be99255a191f5c1fa302eb4", size = 3980803 }, - { url = "https://files.pythonhosted.org/packages/9f/cb/559c7c195807c91c79d38a1f6901384a2878a76fbdf3f1048893a9b7534d/shapely-2.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cf831a13e0d5a7eb519e96f58ec26e049b1fad411fc6fc23b162a7ce04d9cffc", size = 4133301 }, - { url = "https://files.pythonhosted.org/packages/80/cd/60d5ae203241c53ef3abd2ef27c6800e21afd6c94e39db5315ea0cbafb4a/shapely-2.1.2-cp314-cp314-win32.whl", hash = "sha256:61edcd8d0d17dd99075d320a1dd39c0cb9616f7572f10ef91b4b5b00c4aeb566", size = 1583247 }, - { url = "https://files.pythonhosted.org/packages/74/d4/135684f342e909330e50d31d441ace06bf83c7dc0777e11043f99167b123/shapely-2.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:a444e7afccdb0999e203b976adb37ea633725333e5b119ad40b1ca291ecf311c", size = 1773019 }, - { url = "https://files.pythonhosted.org/packages/a3/05/a44f3f9f695fa3ada22786dc9da33c933da1cbc4bfe876fe3a100bafe263/shapely-2.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:5ebe3f84c6112ad3d4632b1fd2290665aa75d4cef5f6c5d77c4c95b324527c6a", size = 1834137 }, - { url = "https://files.pythonhosted.org/packages/52/7e/4d57db45bf314573427b0a70dfca15d912d108e6023f623947fa69f39b72/shapely-2.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5860eb9f00a1d49ebb14e881f5caf6c2cf472c7fd38bd7f253bbd34f934eb076", size = 1642884 }, - { url = "https://files.pythonhosted.org/packages/5a/27/4e29c0a55d6d14ad7422bf86995d7ff3f54af0eba59617eb95caf84b9680/shapely-2.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b705c99c76695702656327b819c9660768ec33f5ce01fa32b2af62b56ba400a1", size = 3018320 }, - { url = "https://files.pythonhosted.org/packages/9f/bb/992e6a3c463f4d29d4cd6ab8963b75b1b1040199edbd72beada4af46bde5/shapely-2.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a1fd0ea855b2cf7c9cddaf25543e914dd75af9de08785f20ca3085f2c9ca60b0", size = 3094931 }, - { url = "https://files.pythonhosted.org/packages/9c/16/82e65e21070e473f0ed6451224ed9fa0be85033d17e0c6e7213a12f59d12/shapely-2.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:df90e2db118c3671a0754f38e36802db75fe0920d211a27481daf50a711fdf26", size = 4030406 }, - { url = "https://files.pythonhosted.org/packages/7c/75/c24ed871c576d7e2b64b04b1fe3d075157f6eb54e59670d3f5ffb36e25c7/shapely-2.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:361b6d45030b4ac64ddd0a26046906c8202eb60d0f9f53085f5179f1d23021a0", size = 4169511 }, - { url = "https://files.pythonhosted.org/packages/b1/f7/b3d1d6d18ebf55236eec1c681ce5e665742aab3c0b7b232720a7d43df7b6/shapely-2.1.2-cp314-cp314t-win32.whl", hash = "sha256:b54df60f1fbdecc8ebc2c5b11870461a6417b3d617f555e5033f1505d36e5735", size = 1602607 }, - { url = "https://files.pythonhosted.org/packages/9a/f6/f09272a71976dfc138129b8faf435d064a811ae2f708cb147dccdf7aacdb/shapely-2.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:0036ac886e0923417932c2e6369b6c52e38e0ff5d9120b90eef5cd9a5fc5cae9", size = 1796682 }, +sdist = { url = "https://files.pythonhosted.org/packages/4d/bc/0989043118a27cccb4e906a46b7565ce36ca7b57f5a18b78f4f1b0f72d9d/shapely-2.1.2.tar.gz", hash = "sha256:2ed4ecb28320a433db18a5bf029986aa8afcfd740745e78847e330d5d94922a9", size = 315489, upload-time = "2025-09-24T13:51:41.432Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/8d/1ff672dea9ec6a7b5d422eb6d095ed886e2e523733329f75fdcb14ee1149/shapely-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:91121757b0a36c9aac3427a651a7e6567110a4a67c97edf04f8d55d4765f6618", size = 1820038, upload-time = "2025-09-24T13:50:15.628Z" }, + { url = "https://files.pythonhosted.org/packages/4f/ce/28fab8c772ce5db23a0d86bf0adaee0c4c79d5ad1db766055fa3dab442e2/shapely-2.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:16a9c722ba774cf50b5d4541242b4cce05aafd44a015290c82ba8a16931ff63d", size = 1626039, upload-time = "2025-09-24T13:50:16.881Z" }, + { url = "https://files.pythonhosted.org/packages/70/8b/868b7e3f4982f5006e9395c1e12343c66a8155c0374fdc07c0e6a1ab547d/shapely-2.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cc4f7397459b12c0b196c9efe1f9d7e92463cbba142632b4cc6d8bbbbd3e2b09", size = 3001519, upload-time = "2025-09-24T13:50:18.606Z" }, + { url = "https://files.pythonhosted.org/packages/13/02/58b0b8d9c17c93ab6340edd8b7308c0c5a5b81f94ce65705819b7416dba5/shapely-2.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:136ab87b17e733e22f0961504d05e77e7be8c9b5a8184f685b4a91a84efe3c26", size = 3110842, upload-time = "2025-09-24T13:50:21.77Z" }, + { url = "https://files.pythonhosted.org/packages/af/61/8e389c97994d5f331dcffb25e2fa761aeedfb52b3ad9bcdd7b8671f4810a/shapely-2.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:16c5d0fc45d3aa0a69074979f4f1928ca2734fb2e0dde8af9611e134e46774e7", size = 4021316, upload-time = "2025-09-24T13:50:23.626Z" }, + { url = "https://files.pythonhosted.org/packages/d3/d4/9b2a9fe6039f9e42ccf2cb3e84f219fd8364b0c3b8e7bbc857b5fbe9c14c/shapely-2.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6ddc759f72b5b2b0f54a7e7cde44acef680a55019eb52ac63a7af2cf17cb9cd2", size = 4178586, upload-time = "2025-09-24T13:50:25.443Z" }, + { url = "https://files.pythonhosted.org/packages/16/f6/9840f6963ed4decf76b08fd6d7fed14f8779fb7a62cb45c5617fa8ac6eab/shapely-2.1.2-cp311-cp311-win32.whl", hash = "sha256:2fa78b49485391224755a856ed3b3bd91c8455f6121fee0db0e71cefb07d0ef6", size = 1543961, upload-time = "2025-09-24T13:50:26.968Z" }, + { url = "https://files.pythonhosted.org/packages/38/1e/3f8ea46353c2a33c1669eb7327f9665103aa3a8dfe7f2e4ef714c210b2c2/shapely-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:c64d5c97b2f47e3cd9b712eaced3b061f2b71234b3fc263e0fcf7d889c6559dc", size = 1722856, upload-time = "2025-09-24T13:50:28.497Z" }, + { url = "https://files.pythonhosted.org/packages/24/c0/f3b6453cf2dfa99adc0ba6675f9aaff9e526d2224cbd7ff9c1a879238693/shapely-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fe2533caae6a91a543dec62e8360fe86ffcdc42a7c55f9dfd0128a977a896b94", size = 1833550, upload-time = "2025-09-24T13:50:30.019Z" }, + { url = "https://files.pythonhosted.org/packages/86/07/59dee0bc4b913b7ab59ab1086225baca5b8f19865e6101db9ebb7243e132/shapely-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ba4d1333cc0bc94381d6d4308d2e4e008e0bd128bdcff5573199742ee3634359", size = 1643556, upload-time = "2025-09-24T13:50:32.291Z" }, + { url = "https://files.pythonhosted.org/packages/26/29/a5397e75b435b9895cd53e165083faed5d12fd9626eadec15a83a2411f0f/shapely-2.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0bd308103340030feef6c111d3eb98d50dc13feea33affc8a6f9fa549e9458a3", size = 2988308, upload-time = "2025-09-24T13:50:33.862Z" }, + { url = "https://files.pythonhosted.org/packages/b9/37/e781683abac55dde9771e086b790e554811a71ed0b2b8a1e789b7430dd44/shapely-2.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e7d4d7ad262a48bb44277ca12c7c78cb1b0f56b32c10734ec9a1d30c0b0c54b", size = 3099844, upload-time = "2025-09-24T13:50:35.459Z" }, + { url = "https://files.pythonhosted.org/packages/d8/f3/9876b64d4a5a321b9dc482c92bb6f061f2fa42131cba643c699f39317cb9/shapely-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e9eddfe513096a71896441a7c37db72da0687b34752c4e193577a145c71736fc", size = 3988842, upload-time = "2025-09-24T13:50:37.478Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a0/704c7292f7014c7e74ec84eddb7b109e1fbae74a16deae9c1504b1d15565/shapely-2.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:980c777c612514c0cf99bc8a9de6d286f5e186dcaf9091252fcd444e5638193d", size = 4152714, upload-time = "2025-09-24T13:50:39.9Z" }, + { url = "https://files.pythonhosted.org/packages/53/46/319c9dc788884ad0785242543cdffac0e6530e4d0deb6c4862bc4143dcf3/shapely-2.1.2-cp312-cp312-win32.whl", hash = "sha256:9111274b88e4d7b54a95218e243282709b330ef52b7b86bc6aaf4f805306f454", size = 1542745, upload-time = "2025-09-24T13:50:41.414Z" }, + { url = "https://files.pythonhosted.org/packages/ec/bf/cb6c1c505cb31e818e900b9312d514f381fbfa5c4363edfce0fcc4f8c1a4/shapely-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:743044b4cfb34f9a67205cee9279feaf60ba7d02e69febc2afc609047cb49179", size = 1722861, upload-time = "2025-09-24T13:50:43.35Z" }, + { url = "https://files.pythonhosted.org/packages/c3/90/98ef257c23c46425dc4d1d31005ad7c8d649fe423a38b917db02c30f1f5a/shapely-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b510dda1a3672d6879beb319bc7c5fd302c6c354584690973c838f46ec3e0fa8", size = 1832644, upload-time = "2025-09-24T13:50:44.886Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ab/0bee5a830d209adcd3a01f2d4b70e587cdd9fd7380d5198c064091005af8/shapely-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8cff473e81017594d20ec55d86b54bc635544897e13a7cfc12e36909c5309a2a", size = 1642887, upload-time = "2025-09-24T13:50:46.735Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5e/7d7f54ba960c13302584c73704d8c4d15404a51024631adb60b126a4ae88/shapely-2.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe7b77dc63d707c09726b7908f575fc04ff1d1ad0f3fb92aec212396bc6cfe5e", size = 2970931, upload-time = "2025-09-24T13:50:48.374Z" }, + { url = "https://files.pythonhosted.org/packages/f2/a2/83fc37e2a58090e3d2ff79175a95493c664bcd0b653dd75cb9134645a4e5/shapely-2.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ed1a5bbfb386ee8332713bf7508bc24e32d24b74fc9a7b9f8529a55db9f4ee6", size = 3082855, upload-time = "2025-09-24T13:50:50.037Z" }, + { url = "https://files.pythonhosted.org/packages/44/2b/578faf235a5b09f16b5f02833c53822294d7f21b242f8e2d0cf03fb64321/shapely-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a84e0582858d841d54355246ddfcbd1fce3179f185da7470f41ce39d001ee1af", size = 3979960, upload-time = "2025-09-24T13:50:51.74Z" }, + { url = "https://files.pythonhosted.org/packages/4d/04/167f096386120f692cc4ca02f75a17b961858997a95e67a3cb6a7bbd6b53/shapely-2.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc3487447a43d42adcdf52d7ac73804f2312cbfa5d433a7d2c506dcab0033dfd", size = 4142851, upload-time = "2025-09-24T13:50:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/48/74/fb402c5a6235d1c65a97348b48cdedb75fb19eca2b1d66d04969fc1c6091/shapely-2.1.2-cp313-cp313-win32.whl", hash = "sha256:9c3a3c648aedc9f99c09263b39f2d8252f199cb3ac154fadc173283d7d111350", size = 1541890, upload-time = "2025-09-24T13:50:55.337Z" }, + { url = "https://files.pythonhosted.org/packages/41/47/3647fe7ad990af60ad98b889657a976042c9988c2807cf322a9d6685f462/shapely-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:ca2591bff6645c216695bdf1614fca9c82ea1144d4a7591a466fef64f28f0715", size = 1722151, upload-time = "2025-09-24T13:50:57.153Z" }, + { url = "https://files.pythonhosted.org/packages/3c/49/63953754faa51ffe7d8189bfbe9ca34def29f8c0e34c67cbe2a2795f269d/shapely-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2d93d23bdd2ed9dc157b46bc2f19b7da143ca8714464249bef6771c679d5ff40", size = 1834130, upload-time = "2025-09-24T13:50:58.49Z" }, + { url = "https://files.pythonhosted.org/packages/7f/ee/dce001c1984052970ff60eb4727164892fb2d08052c575042a47f5a9e88f/shapely-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01d0d304b25634d60bd7cf291828119ab55a3bab87dc4af1e44b07fb225f188b", size = 1642802, upload-time = "2025-09-24T13:50:59.871Z" }, + { url = "https://files.pythonhosted.org/packages/da/e7/fc4e9a19929522877fa602f705706b96e78376afb7fad09cad5b9af1553c/shapely-2.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8d8382dd120d64b03698b7298b89611a6ea6f55ada9d39942838b79c9bc89801", size = 3018460, upload-time = "2025-09-24T13:51:02.08Z" }, + { url = "https://files.pythonhosted.org/packages/a1/18/7519a25db21847b525696883ddc8e6a0ecaa36159ea88e0fef11466384d0/shapely-2.1.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:19efa3611eef966e776183e338b2d7ea43569ae99ab34f8d17c2c054d3205cc0", size = 3095223, upload-time = "2025-09-24T13:51:04.472Z" }, + { url = "https://files.pythonhosted.org/packages/48/de/b59a620b1f3a129c3fecc2737104a0a7e04e79335bd3b0a1f1609744cf17/shapely-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:346ec0c1a0fcd32f57f00e4134d1200e14bf3f5ae12af87ba83ca275c502498c", size = 4030760, upload-time = "2025-09-24T13:51:06.455Z" }, + { url = "https://files.pythonhosted.org/packages/96/b3/c6655ee7232b417562bae192ae0d3ceaadb1cc0ffc2088a2ddf415456cc2/shapely-2.1.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6305993a35989391bd3476ee538a5c9a845861462327efe00dd11a5c8c709a99", size = 4170078, upload-time = "2025-09-24T13:51:08.584Z" }, + { url = "https://files.pythonhosted.org/packages/a0/8e/605c76808d73503c9333af8f6cbe7e1354d2d238bda5f88eea36bfe0f42a/shapely-2.1.2-cp313-cp313t-win32.whl", hash = "sha256:c8876673449f3401f278c86eb33224c5764582f72b653a415d0e6672fde887bf", size = 1559178, upload-time = "2025-09-24T13:51:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/36/f7/d317eb232352a1f1444d11002d477e54514a4a6045536d49d0c59783c0da/shapely-2.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:4a44bc62a10d84c11a7a3d7c1c4fe857f7477c3506e24c9062da0db0ae0c449c", size = 1739756, upload-time = "2025-09-24T13:51:12.105Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c4/3ce4c2d9b6aabd27d26ec988f08cb877ba9e6e96086eff81bfea93e688c7/shapely-2.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:9a522f460d28e2bf4e12396240a5fc1518788b2fcd73535166d748399ef0c223", size = 1831290, upload-time = "2025-09-24T13:51:13.56Z" }, + { url = "https://files.pythonhosted.org/packages/17/b9/f6ab8918fc15429f79cb04afa9f9913546212d7fb5e5196132a2af46676b/shapely-2.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ff629e00818033b8d71139565527ced7d776c269a49bd78c9df84e8f852190c", size = 1641463, upload-time = "2025-09-24T13:51:14.972Z" }, + { url = "https://files.pythonhosted.org/packages/a5/57/91d59ae525ca641e7ac5551c04c9503aee6f29b92b392f31790fcb1a4358/shapely-2.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f67b34271dedc3c653eba4e3d7111aa421d5be9b4c4c7d38d30907f796cb30df", size = 2970145, upload-time = "2025-09-24T13:51:16.961Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cb/4948be52ee1da6927831ab59e10d4c29baa2a714f599f1f0d1bc747f5777/shapely-2.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:21952dc00df38a2c28375659b07a3979d22641aeb104751e769c3ee825aadecf", size = 3073806, upload-time = "2025-09-24T13:51:18.712Z" }, + { url = "https://files.pythonhosted.org/packages/03/83/f768a54af775eb41ef2e7bec8a0a0dbe7d2431c3e78c0a8bdba7ab17e446/shapely-2.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1f2f33f486777456586948e333a56ae21f35ae273be99255a191f5c1fa302eb4", size = 3980803, upload-time = "2025-09-24T13:51:20.37Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/559c7c195807c91c79d38a1f6901384a2878a76fbdf3f1048893a9b7534d/shapely-2.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cf831a13e0d5a7eb519e96f58ec26e049b1fad411fc6fc23b162a7ce04d9cffc", size = 4133301, upload-time = "2025-09-24T13:51:21.887Z" }, + { url = "https://files.pythonhosted.org/packages/80/cd/60d5ae203241c53ef3abd2ef27c6800e21afd6c94e39db5315ea0cbafb4a/shapely-2.1.2-cp314-cp314-win32.whl", hash = "sha256:61edcd8d0d17dd99075d320a1dd39c0cb9616f7572f10ef91b4b5b00c4aeb566", size = 1583247, upload-time = "2025-09-24T13:51:23.401Z" }, + { url = "https://files.pythonhosted.org/packages/74/d4/135684f342e909330e50d31d441ace06bf83c7dc0777e11043f99167b123/shapely-2.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:a444e7afccdb0999e203b976adb37ea633725333e5b119ad40b1ca291ecf311c", size = 1773019, upload-time = "2025-09-24T13:51:24.873Z" }, + { url = "https://files.pythonhosted.org/packages/a3/05/a44f3f9f695fa3ada22786dc9da33c933da1cbc4bfe876fe3a100bafe263/shapely-2.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:5ebe3f84c6112ad3d4632b1fd2290665aa75d4cef5f6c5d77c4c95b324527c6a", size = 1834137, upload-time = "2025-09-24T13:51:26.665Z" }, + { url = "https://files.pythonhosted.org/packages/52/7e/4d57db45bf314573427b0a70dfca15d912d108e6023f623947fa69f39b72/shapely-2.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5860eb9f00a1d49ebb14e881f5caf6c2cf472c7fd38bd7f253bbd34f934eb076", size = 1642884, upload-time = "2025-09-24T13:51:28.029Z" }, + { url = "https://files.pythonhosted.org/packages/5a/27/4e29c0a55d6d14ad7422bf86995d7ff3f54af0eba59617eb95caf84b9680/shapely-2.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b705c99c76695702656327b819c9660768ec33f5ce01fa32b2af62b56ba400a1", size = 3018320, upload-time = "2025-09-24T13:51:29.903Z" }, + { url = "https://files.pythonhosted.org/packages/9f/bb/992e6a3c463f4d29d4cd6ab8963b75b1b1040199edbd72beada4af46bde5/shapely-2.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a1fd0ea855b2cf7c9cddaf25543e914dd75af9de08785f20ca3085f2c9ca60b0", size = 3094931, upload-time = "2025-09-24T13:51:32.699Z" }, + { url = "https://files.pythonhosted.org/packages/9c/16/82e65e21070e473f0ed6451224ed9fa0be85033d17e0c6e7213a12f59d12/shapely-2.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:df90e2db118c3671a0754f38e36802db75fe0920d211a27481daf50a711fdf26", size = 4030406, upload-time = "2025-09-24T13:51:34.189Z" }, + { url = "https://files.pythonhosted.org/packages/7c/75/c24ed871c576d7e2b64b04b1fe3d075157f6eb54e59670d3f5ffb36e25c7/shapely-2.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:361b6d45030b4ac64ddd0a26046906c8202eb60d0f9f53085f5179f1d23021a0", size = 4169511, upload-time = "2025-09-24T13:51:36.297Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f7/b3d1d6d18ebf55236eec1c681ce5e665742aab3c0b7b232720a7d43df7b6/shapely-2.1.2-cp314-cp314t-win32.whl", hash = "sha256:b54df60f1fbdecc8ebc2c5b11870461a6417b3d617f555e5033f1505d36e5735", size = 1602607, upload-time = "2025-09-24T13:51:37.757Z" }, + { url = "https://files.pythonhosted.org/packages/9a/f6/f09272a71976dfc138129b8faf435d064a811ae2f708cb147dccdf7aacdb/shapely-2.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:0036ac886e0923417932c2e6369b6c52e38e0ff5d9120b90eef5cd9a5fc5cae9", size = 1796682, upload-time = "2025-09-24T13:51:39.233Z" }, ] [[package]] name = "six" version = "1.17.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] [[package]] name = "sortedcontainers" version = "2.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594 } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575 }, + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, ] [[package]] name = "tomli" version = "2.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236 }, - { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084 }, - { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832 }, - { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052 }, - { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555 }, - { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128 }, - { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445 }, - { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165 }, - { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891 }, - { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796 }, - { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121 }, - { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070 }, - { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859 }, - { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296 }, - { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124 }, - { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698 }, - { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819 }, - { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766 }, - { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771 }, - { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586 }, - { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792 }, - { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909 }, - { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946 }, - { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705 }, - { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244 }, - { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637 }, - { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925 }, - { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045 }, - { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835 }, - { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109 }, - { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930 }, - { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964 }, - { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065 }, - { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088 }, - { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193 }, - { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488 }, - { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669 }, - { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709 }, - { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563 }, - { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756 }, - { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408 }, +sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" }, + { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" }, + { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" }, + { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" }, + { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" }, + { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" }, + { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" }, + { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" }, + { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" }, + { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" }, + { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" }, + { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" }, + { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" }, + { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586, upload-time = "2025-10-08T22:01:21.164Z" }, + { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792, upload-time = "2025-10-08T22:01:22.417Z" }, + { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909, upload-time = "2025-10-08T22:01:23.859Z" }, + { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" }, + { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244, upload-time = "2025-10-08T22:01:27.06Z" }, + { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637, upload-time = "2025-10-08T22:01:28.059Z" }, + { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925, upload-time = "2025-10-08T22:01:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045, upload-time = "2025-10-08T22:01:31.98Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835, upload-time = "2025-10-08T22:01:32.989Z" }, + { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109, upload-time = "2025-10-08T22:01:34.052Z" }, + { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930, upload-time = "2025-10-08T22:01:35.082Z" }, + { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964, upload-time = "2025-10-08T22:01:36.057Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065, upload-time = "2025-10-08T22:01:37.27Z" }, + { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088, upload-time = "2025-10-08T22:01:38.235Z" }, + { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193, upload-time = "2025-10-08T22:01:39.712Z" }, + { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488, upload-time = "2025-10-08T22:01:40.773Z" }, + { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669, upload-time = "2025-10-08T22:01:41.824Z" }, + { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709, upload-time = "2025-10-08T22:01:43.177Z" }, + { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563, upload-time = "2025-10-08T22:01:44.233Z" }, + { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756, upload-time = "2025-10-08T22:01:45.234Z" }, + { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, ] [[package]] @@ -1634,18 +1634,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/36/27/489922f4505975b11de2b5ad07b4fe1dca0bca9be81a703f26c5f3acfce5/types_requests-2.32.4.20250913.tar.gz", hash = "sha256:abd6d4f9ce3a9383f269775a9835a4c24e5cd6b9f647d64f88aa4613c33def5d", size = 23113 } +sdist = { url = "https://files.pythonhosted.org/packages/36/27/489922f4505975b11de2b5ad07b4fe1dca0bca9be81a703f26c5f3acfce5/types_requests-2.32.4.20250913.tar.gz", hash = "sha256:abd6d4f9ce3a9383f269775a9835a4c24e5cd6b9f647d64f88aa4613c33def5d", size = 23113, upload-time = "2025-09-13T02:40:02.309Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/20/9a227ea57c1285986c4cf78400d0a91615d25b24e257fd9e2969606bdfae/types_requests-2.32.4.20250913-py3-none-any.whl", hash = "sha256:78c9c1fffebbe0fa487a418e0fa5252017e9c60d1a2da394077f1780f655d7e1", size = 20658 }, + { url = "https://files.pythonhosted.org/packages/2a/20/9a227ea57c1285986c4cf78400d0a91615d25b24e257fd9e2969606bdfae/types_requests-2.32.4.20250913-py3-none-any.whl", hash = "sha256:78c9c1fffebbe0fa487a418e0fa5252017e9c60d1a2da394077f1780f655d7e1", size = 20658, upload-time = "2025-09-13T02:40:01.115Z" }, ] [[package]] name = "typing-extensions" version = "4.15.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391 } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614 }, + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] [[package]] @@ -1655,27 +1655,27 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949 } +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611 }, + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] [[package]] name = "tzdata" version = "2025.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380 } +sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839 }, + { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, ] [[package]] name = "urllib3" version = "2.5.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185 } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795 }, + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, ] [[package]] @@ -1687,34 +1687,34 @@ dependencies = [ { name = "filelock" }, { name = "platformdirs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/20/28/e6f1a6f655d620846bd9df527390ecc26b3805a0c5989048c210e22c5ca9/virtualenv-20.35.4.tar.gz", hash = "sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c", size = 6028799 } +sdist = { url = "https://files.pythonhosted.org/packages/20/28/e6f1a6f655d620846bd9df527390ecc26b3805a0c5989048c210e22c5ca9/virtualenv-20.35.4.tar.gz", hash = "sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c", size = 6028799, upload-time = "2025-10-29T06:57:40.511Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl", hash = "sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b", size = 6005095 }, + { url = "https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl", hash = "sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b", size = 6005095, upload-time = "2025-10-29T06:57:37.598Z" }, ] [[package]] name = "watchdog" version = "6.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393 }, - { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392 }, - { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019 }, - { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471 }, - { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449 }, - { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054 }, - { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480 }, - { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451 }, - { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057 }, - { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079 }, - { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078 }, - { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076 }, - { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077 }, - { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078 }, - { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077 }, - { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078 }, - { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065 }, - { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070 }, - { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067 }, +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" }, + { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" }, + { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" }, + { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, + { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, + { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, + { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, ]