From 11e1b0a20c07edf4b8fb8c1d073de2ad9b5a7eeb Mon Sep 17 00:00:00 2001 From: wenddymacro <50739376+wenddymacro@users.noreply.github.com> Date: Wed, 18 Mar 2026 14:04:03 +0800 Subject: [PATCH 01/16] Add WooldridgeDiD (ETWFE) design spec Design spec for integrating Stata jwdid (Wooldridge 2021/2023 ETWFE) into diff-diff as a standalone WooldridgeDiD estimator with linear and nonlinear support. Co-Authored-By: Claude Sonnet 4.6 --- .../specs/2026-03-18-wooldridge-did-design.md | 265 ++++++++++++++++++ 1 file changed, 265 insertions(+) create mode 100644 docs/superpowers/specs/2026-03-18-wooldridge-did-design.md diff --git a/docs/superpowers/specs/2026-03-18-wooldridge-did-design.md b/docs/superpowers/specs/2026-03-18-wooldridge-did-design.md new file mode 100644 index 00000000..954771cf --- /dev/null +++ b/docs/superpowers/specs/2026-03-18-wooldridge-did-design.md @@ -0,0 +1,265 @@ +# WooldridgeDiD Estimator — Design Spec + +**Date:** 2026-03-18 +**Status:** Approved +**Scope:** Integrate Stata `jwdid` (Wooldridge ETWFE) functionality into diff-diff + +--- + +## 1. Background and Motivation + +The Stata package `jwdid` (Friosavila 2021) implements Wooldridge's (2021, 2023) Extended +Two-Way Fixed Effects (ETWFE) estimator for staggered DiD. Its key advantages over existing +diff-diff estimators are: + +- **Saturated regression**: estimates all cohort×time ATT(g,t) in a single pooled OLS, + more efficient than Callaway-Sant'Anna's pair-wise approach +- **Nonlinear extension**: Wooldridge (2023) extends ETWFE to logit and Poisson, avoiding + the incidental parameters problem — no other estimator in diff-diff supports this +- **Equivalence to CS**: under identical assumptions, ETWFE ATT(g,t) equals CS ATT(g,t) + +**Primary references:** +- Wooldridge (2021). "Two-Way Fixed Effects, the Two-Way Mundlak Regression, and + Difference-in-Differences Estimators." SSRN 3906345. +- Wooldridge (2023). "Simple approaches to nonlinear difference-in-differences with panel + data." *The Econometrics Journal*, 26(3), C31–C66. +- Friosavila (2021). `jwdid`: Stata module. SSC s459114. + +--- + +## 2. Architecture Overview + +### New files +| File | Purpose | +|------|---------| +| `diff_diff/wooldridge.py` | `WooldridgeDiD` estimator class | +| `diff_diff/wooldridge_results.py` | `WooldridgeDiDResults` dataclass | +| `tests/test_wooldridge.py` | Full test suite | + +### Modified files +| File | Change | +|------|--------| +| `diff_diff/__init__.py` | Export `WooldridgeDiD`, `WooldridgeDiDResults` | +| `docs/methodology/REGISTRY.md` | Add ETWFE methodology section | + +### Class hierarchy +`WooldridgeDiD` is a **standalone estimator** (same level as `CallawaySantAnna`, +`SunAbraham`, etc.), not inheriting from `DifferenceInDifferences`. It implements its own +`get_params` / `set_params`. + +--- + +## 3. Public API + +### Constructor + +```python +class WooldridgeDiD: + def __init__( + self, + method: str = "ols", # "ols" | "logit" | "poisson" + control_group: str = "not_yet_treated", # "never_treated" | "not_yet_treated" + anticipation: int = 0, # pre-treatment periods to include + demean_covariates: bool = True, # within cohort-period demeaning (jwdid default) + alpha: float = 0.05, + cluster: Optional[str] = None, # default: unit identifier (jwdid default) + n_bootstrap: int = 0, # >0 enables multiplier bootstrap + bootstrap_weights: str = "rademacher", # "rademacher" | "webb" | "mammen" + seed: Optional[int] = None, + rank_deficient_action: str = "warn", # "warn" | "error" | "silent" + ): ... +``` + +### fit() + +```python +def fit( + self, + data: pd.DataFrame, + outcome: str, + unit: str, + time: str, + cohort: str, # first treatment period; 0/NaN = never treated + exovar: Optional[List[str]] = None, # time-invariant covariates (no interaction) + xtvar: Optional[List[str]] = None, # time-varying covariates (demeaned within cohort-period) + xgvar: Optional[List[str]] = None, # cohort-interacted covariates +) -> "WooldridgeDiDResults": ... +``` + +**Notes:** +- `cohort` column convention: integer = first treatment period, 0 or NaN = never treated. + Consistent with `CallawaySantAnna`'s `cohort` parameter. +- Default clustering is at the `unit` level (matches `jwdid` default of `vce(cluster ivar)`). +- `demean_covariates=True` corresponds to `jwdid` default; `False` corresponds to `xasis` option. + +### get_params / set_params + +```python +def get_params(self) -> Dict[str, Any]: ... # returns all constructor params +def set_params(self, **params) -> "WooldridgeDiD": ... # sklearn-compatible +``` + +--- + +## 4. Results Object + +```python +@dataclass +class WooldridgeDiDResults: + # Raw cohort×time estimates — core output + group_time_effects: Dict[Tuple[Any, Any], Dict[str, Any]] + # key = (g, t); value = {"att", "se", "t_stat", "p_value", "conf_int"} + + # Simple aggregation (always computed on fit) + overall_att: float + overall_se: float + overall_t_stat: float + overall_p_value: float + overall_conf_int: Tuple[float, float] + + # Other aggregations (populated by .aggregate()) + group_effects: Optional[Dict[Any, Dict]] # keyed by cohort g + calendar_effects: Optional[Dict[Any, Dict]] # keyed by calendar period t + event_study_effects: Optional[Dict[int, Dict]] # keyed by relative period k = t - g + + # Metadata + method: str + control_group: str + groups: List[Any] + time_periods: List[Any] + n_obs: int + n_treated_units: int + n_control_units: int + alpha: float = 0.05 + + # Methods + def aggregate(self, type: str) -> "WooldridgeDiDResults": ... + # type: "simple" | "group" | "calendar" | "event" + # fills corresponding fields, returns self for chaining + + def summary(self, aggregation: str = "simple") -> str: ... + def to_dataframe(self, aggregation: str = "event") -> pd.DataFrame: ... + def plot_event_study(self, **kwargs) -> None: ... + def __repr__(self) -> str: ... +``` + +**Inference rule:** ALL inference fields (t_stat, p_value, conf_int) computed together +via `safe_inference()` from `diff_diff.utils`. Never computed individually. + +--- + +## 5. Internal Computation + +### 5a. Linear ETWFE (`method="ols"`) + +Faithful port of `jwdid` + `reghdfe`: + +1. **Filter observations**: keep control group (never- or not-yet-treated at time t) plus + all treated units. Drop observations where `t < g - anticipation`. + +2. **Build interaction matrix**: for each (g, t) with `t >= g - anticipation`, create + column `1(G_i = g) * 1(T = t)`. These are the β_{g,t} regressors. + +3. **Covariate preparation**: + - `exovar`: append as-is + - `xtvar`: demean within (cohort × period) cells when `demean_covariates=True` + - `xgvar`: interact with each cohort indicator + +4. **Absorb unit + time FE**: within-transformation (existing `absorb` mechanism in + `linalg.py`), not explicit dummies. + +5. **Solve**: `linalg.solve_ols()` → extract β_{g,t} coefficients and vcov submatrix. + +6. **Inference**: `linalg.compute_robust_vcov()` with unit-level clustering by default, + then `safe_inference()` for each (g, t) cell. + +7. **Bootstrap**: multiplier bootstrap supported for all inference; + wild cluster bootstrap supported for linear only (same as `DifferenceInDifferences`). + +### 5b. Nonlinear (`method="logit"|"poisson"`) + +Following Wooldridge (2023) pooled QMLE approach: + +- **Logit**: group-level FE (cohort × period), **not** individual FE — avoids incidental + parameters problem. Log-likelihood: Bernoulli QLL. +- **Poisson**: individual FE absorbed via PPML (iterative within-transformation). + Log-likelihood: Poisson QLL. + +Optimization: `scipy.optimize.minimize` (L-BFGS-B). Vcov from numerical Hessian +(`scipy.optimize.approx_fprime` second differences). + +**ATT computation via Average Structural Function (ASF):** +Coefficients on treatment interactions are not directly ATTs. Must compute: +``` +ATT(g,t) = mean[ g(X_i'β̂ + δ̂_{g,t}) - g(X_i'β̂) ] over treated units in (g,t) +``` +where `g(·)` = logistic or exp. Delta method for SE propagation. + +Bootstrap: multiplier bootstrap only (no wild cluster bootstrap for nonlinear). + +### 5c. Aggregation Weights (exact jwdid_estat formula) + +``` +ω(g,t) = number of unit-time observations in cell (g,t) + +simple: Σ_{g,t: t≥g} ω(g,t)·ATT(g,t) / Σ_{g,t: t≥g} ω(g,t) +group: Σ_{t≥g} ω(g,t)·ATT(g,t) / Σ_{t≥g} ω(g,t) ∀g +calendar: Σ_{g: t≥g} ω(g,t)·ATT(g,t) / Σ_{g: t≥g} ω(g,t) ∀t +event: Σ_g ω(g,g+k)·ATT(g,g+k) / Σ_g ω(g,g+k) ∀k +``` + +Aggregation SEs: delta method for linear (variance of weighted sum); bootstrap +distribution used when `n_bootstrap > 0`. + +--- + +## 6. Parallel Trends Assumptions + +| `control_group` | Assumption | Pre-treatment effects | +|-----------------|------------|----------------------| +| `"not_yet_treated"` (default) | Parallel trends between each cohort and not-yet-treated units | Constrained to zero by design | +| `"never_treated"` | Parallel trends between each cohort and never-treated units | Estimable (visible in event study k < 0) | + +--- + +## 7. Testing Strategy + +### test_wooldridge.py structure + +**API tests** +- Invalid `method` / `control_group` raises `ValueError` +- `get_params()` / `set_params()` round-trip +- Accessing `results_` before `fit()` raises + +**Basic functionality** +- Fit on `mpdta` dataset, all fields non-NaN (`assert_nan_inference()`) +- All four aggregations callable and produce sensible output +- `to_dataframe()` and `summary()` run without error + +**Methodology correctness** +- Linear ETWFE ATT(g,t) ≈ CallawaySantAnna ATT(g,t) on same data / same control group + (tolerance ~1e-3, both theoretically equivalent under OLS / same assumptions) +- Nonlinear: simulated binary data, logit ATT sign correct +- Aggregation weight verification: manual weighted average == `simple` ATT + +**Edge cases** +- `control_group="never_treated"` with pre-treatment k < 0 effects estimable +- `anticipation=1` shifts treatment window correctly +- All three covariate types passed simultaneously +- Single cohort degenerates to standard DiD + +**Slow tests** (`@pytest.mark.slow`) +- Bootstrap SE convergence (`ci_params.bootstrap(n, min_n=199)`, threshold 0.40/0.15) +- Nonlinear bootstrap + +--- + +## 8. Documentation + +- `docs/methodology/REGISTRY.md`: add "WooldridgeDiD / ETWFE" section with: + - Academic sources (Wooldridge 2021, 2023; Friosavila 2021) + - Estimator equation (saturated model) + - SE methods (unit-level cluster, multiplier bootstrap, wild cluster bootstrap for OLS) + - Edge cases: nonlinear ASF computation, covariate demeaning + - Note: `**Deviation from Stata:** nonlinear bootstrap uses multiplier (jwdid uses delta method)` +- Export as `WooldridgeDiD` and alias `ETWFE` in `__init__.py` From bfcbf001b37674754dea2f558bf6e7d847c88417 Mon Sep 17 00:00:00 2001 From: wenddymacro <50739376+wenddymacro@users.noreply.github.com> Date: Wed, 18 Mar 2026 17:41:31 +0800 Subject: [PATCH 02/16] Revise WooldridgeDiD spec: fix P1/P2 issues from review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Specify correct within_transform location (diff_diff.utils, not linalg) - Fix logit FE: explicit drop_first dummies to avoid solve_logit intercept collision - Add complete solve_poisson() spec with signature, clipping, convergence behavior - Add full delta method gradient vector for nonlinear ASF SEs - Fix REGISTRY deviation label to use recognized "- **Note:**" format - Clarify control-group observation filter for anticipation - Fix "constrained to zero" → "not estimated (cells excluded)" - Add _demeaned suffix tracking note for within_transform - Align ETWFE alias across Sections 2 and 8 Co-Authored-By: Claude Sonnet 4.6 --- .../specs/2026-03-18-wooldridge-did-design.md | 100 +++++++++++++----- 1 file changed, 71 insertions(+), 29 deletions(-) diff --git a/docs/superpowers/specs/2026-03-18-wooldridge-did-design.md b/docs/superpowers/specs/2026-03-18-wooldridge-did-design.md index 954771cf..6b60176b 100644 --- a/docs/superpowers/specs/2026-03-18-wooldridge-did-design.md +++ b/docs/superpowers/specs/2026-03-18-wooldridge-did-design.md @@ -39,7 +39,7 @@ diff-diff estimators are: ### Modified files | File | Change | |------|--------| -| `diff_diff/__init__.py` | Export `WooldridgeDiD`, `WooldridgeDiDResults` | +| `diff_diff/__init__.py` | Export `WooldridgeDiD`, `WooldridgeDiDResults`; alias `ETWFE = WooldridgeDiD` | | `docs/methodology/REGISTRY.md` | Add ETWFE methodology section | ### Class hierarchy @@ -91,6 +91,8 @@ def fit( Consistent with `CallawaySantAnna`'s `cohort` parameter. - Default clustering is at the `unit` level (matches `jwdid` default of `vce(cluster ivar)`). - `demean_covariates=True` corresponds to `jwdid` default; `False` corresponds to `xasis` option. +- When `demean_covariates=False`, `xtvar` variables are treated identically to `exovar` + (appended without demeaning or interaction). ### get_params / set_params @@ -140,6 +142,7 @@ class WooldridgeDiDResults: def summary(self, aggregation: str = "simple") -> str: ... def to_dataframe(self, aggregation: str = "event") -> pd.DataFrame: ... def plot_event_study(self, **kwargs) -> None: ... + # delegates to diff_diff.visualization plot utilities (same pattern as CallawaySantAnna) def __repr__(self) -> str: ... ``` @@ -154,46 +157,77 @@ via `safe_inference()` from `diff_diff.utils`. Never computed individually. Faithful port of `jwdid` + `reghdfe`: -1. **Filter observations**: keep control group (never- or not-yet-treated at time t) plus - all treated units. Drop observations where `t < g - anticipation`. +1. **Filter observations**: + - Treated units: keep all observations. + - `not_yet_treated` control: at each time t, include units with cohort > t (not yet + treated). Drop treated units' observations where `t < g - anticipation` (pre-treatment + beyond anticipation window). Control observations are kept for all t. + - `never_treated` control: include only units with cohort = 0 or NaN. Drop treated + unit observations where `t < g - anticipation`. 2. **Build interaction matrix**: for each (g, t) with `t >= g - anticipation`, create - column `1(G_i = g) * 1(T = t)`. These are the β_{g,t} regressors. + column `1(G_i = g) * 1(T = t)`. These are the β_{g,t} regressors. Cells with + `t < g - anticipation` are excluded from the model (not constrained — simply absent). 3. **Covariate preparation**: - - `exovar`: append as-is - - `xtvar`: demean within (cohort × period) cells when `demean_covariates=True` - - `xgvar`: interact with each cohort indicator + - `exovar`: append as-is (no interaction, no demeaning) + - `xtvar`: when `demean_covariates=True`, demean within (cohort × period) cells using + pandas `groupby([cohort_col, time]).transform("mean")`; when `demean_covariates=False`, + append as-is (equivalent to `xasis` option in jwdid) + - `xgvar`: interact each variable with each cohort indicator column -4. **Absorb unit + time FE**: within-transformation (existing `absorb` mechanism in - `linalg.py`), not explicit dummies. +4. **Absorb unit + time FE**: two-way within-transformation using + `within_transform(data, variables, unit, time, suffix="_demeaned")` from `diff_diff.utils`. + This performs iterative demeaning equivalent to unit + time dummy absorption. Applied to + outcome and all regressors before `solve_ols`. Track demeaned columns via the `_demeaned` + suffix (e.g., `outcome` → `outcome_demeaned`) when slicing the DataFrame for `solve_ols`. -5. **Solve**: `linalg.solve_ols()` → extract β_{g,t} coefficients and vcov submatrix. +5. **Solve**: `linalg.solve_ols()` on the within-transformed design matrix → extract + β_{g,t} coefficients and full vcov matrix. -6. **Inference**: `linalg.compute_robust_vcov()` with unit-level clustering by default, - then `safe_inference()` for each (g, t) cell. +6. **Inference**: `linalg.compute_robust_vcov()` with unit-level clustering by default + (pass `cluster=unit` column), then `safe_inference()` for each (g, t) cell. 7. **Bootstrap**: multiplier bootstrap supported for all inference; wild cluster bootstrap supported for linear only (same as `DifferenceInDifferences`). ### 5b. Nonlinear (`method="logit"|"poisson"`) -Following Wooldridge (2023) pooled QMLE approach: - -- **Logit**: group-level FE (cohort × period), **not** individual FE — avoids incidental - parameters problem. Log-likelihood: Bernoulli QLL. -- **Poisson**: individual FE absorbed via PPML (iterative within-transformation). - Log-likelihood: Poisson QLL. - -Optimization: `scipy.optimize.minimize` (L-BFGS-B). Vcov from numerical Hessian -(`scipy.optimize.approx_fprime` second differences). +Following Wooldridge (2023) pooled QMLE approach. Both methods use **explicit cohort×period +group FE** (not individual FE) to avoid the incidental parameters problem: + +- **Logit**: Bernoulli QLL. Use existing `linalg.solve_logit()` (IRLS). Build design matrix + with explicit cohort×period group FE dummies using `pd.get_dummies(..., drop_first=False)`, + then **drop one dummy column** (reference category) before passing to `solve_logit`. + `solve_logit` prepends its own intercept internally; providing dummies that span the + constant without dropping one causes rank deficiency and silent coefficient dropping. + Dropping one cohort×period category is the correct fix (standard dummy variable trap). + +- **Poisson**: Poisson QLL. Implement `linalg.solve_poisson()` — a new function with + signature `solve_poisson(X, y, max_iter=25, tol=1e-8) -> Tuple[ndarray, ndarray]` + (mirrors `solve_logit` signature). Uses Poisson IRLS (Newton-Raphson with log link): + - Initialize: `β = zeros`; `μ̂ = clip(exp(Xβ), 1e-10, None)` (clip prevents log(0)) + - Weight update: `W = diag(μ̂)` + - Newton step: `β ← β + (X'WX)^{-1} X'(y - μ̂)` (score / Hessian) + - Convergence: `‖β_new - β_old‖_∞ < tol`; warn if `max_iter` reached without convergence + - Returns `(β, W_final)` where `W_final` is used by caller for vcov + - Does **not** prepend intercept automatically (caller includes intercept or FE dummies) + - Also uses explicit cohort×period group FE dummies with one dropped (same as logit) + +Vcov for both: sandwich estimator `(X'WX)^{-1} (X'Ûû'X) (X'WX)^{-1}` (robust/clustered), +where Û contains Pearson residuals `(y - μ̂)`. Mirrors `linalg.compute_robust_vcov` pattern. **ATT computation via Average Structural Function (ASF):** -Coefficients on treatment interactions are not directly ATTs. Must compute: +Coefficients on treatment interactions are not directly ATTs. Compute: ``` -ATT(g,t) = mean[ g(X_i'β̂ + δ̂_{g,t}) - g(X_i'β̂) ] over treated units in (g,t) +ATT(g,t) = mean[ g(η_i + δ̂_{g,t}) - g(η_i) ] over treated units in (g,t) ``` -where `g(·)` = logistic or exp. Delta method for SE propagation. +where `η_i = X_i'β̂` is the baseline linear index and `g(·)` = logistic or exp. + +SE via full delta method. Define gradient vector `∇θ ∈ ℝ^K` (one entry per coefficient): +- For `δ_{g,t}`: `∂ATT/∂δ_{g,t} = mean[ g'(η_i + δ̂_{g,t}) ]` +- For any baseline covariate `β_k`: `∂ATT/∂β_k = mean[ x_{ik} (g'(η_i + δ̂_{g,t}) - g'(η_i)) ]` +Then `Var(ATT(g,t)) = ∇θ' Σ_β ∇θ` using the full parameter vcov matrix. Bootstrap: multiplier bootstrap only (no wild cluster bootstrap for nonlinear). @@ -208,8 +242,14 @@ calendar: Σ_{g: t≥g} ω(g,t)·ATT(g,t) / Σ_{g: t≥g} ω(g,t) ∀ event: Σ_g ω(g,g+k)·ATT(g,g+k) / Σ_g ω(g,g+k) ∀k ``` -Aggregation SEs: delta method for linear (variance of weighted sum); bootstrap -distribution used when `n_bootstrap > 0`. +**Aggregation SEs (delta method):** For a weighted aggregate `θ̄ = Σ w_{g,t} β_{g,t}` +where weights are treated as fixed: +``` +Var(θ̄) = w' Σ_β w +``` +where `w` is the weight vector and `Σ_β` is the full vcov submatrix of all β_{g,t} +coefficients (extracted from `solve_ols` vcov). This requires storing the full β vcov, +not just diagonal SEs. When `n_bootstrap > 0`, use bootstrap distribution instead. --- @@ -217,7 +257,7 @@ distribution used when `n_bootstrap > 0`. | `control_group` | Assumption | Pre-treatment effects | |-----------------|------------|----------------------| -| `"not_yet_treated"` (default) | Parallel trends between each cohort and not-yet-treated units | Constrained to zero by design | +| `"not_yet_treated"` (default) | Parallel trends between each cohort and not-yet-treated units | Not estimated — cells with `t < g` are excluded from the model | | `"never_treated"` | Parallel trends between each cohort and never-treated units | Estimable (visible in event study k < 0) | --- @@ -238,7 +278,9 @@ distribution used when `n_bootstrap > 0`. **Methodology correctness** - Linear ETWFE ATT(g,t) ≈ CallawaySantAnna ATT(g,t) on same data / same control group - (tolerance ~1e-3, both theoretically equivalent under OLS / same assumptions) + (tolerance ~1e-2 relative, both theoretically equivalent under OLS / same assumptions; + exact equality holds asymptotically but finite-sample differences exist due to + different control group construction details) - Nonlinear: simulated binary data, logit ATT sign correct - Aggregation weight verification: manual weighted average == `simple` ATT @@ -261,5 +303,5 @@ distribution used when `n_bootstrap > 0`. - Estimator equation (saturated model) - SE methods (unit-level cluster, multiplier bootstrap, wild cluster bootstrap for OLS) - Edge cases: nonlinear ASF computation, covariate demeaning - - Note: `**Deviation from Stata:** nonlinear bootstrap uses multiplier (jwdid uses delta method)` + - `- **Note:** nonlinear bootstrap uses multiplier bootstrap; jwdid uses delta method` - Export as `WooldridgeDiD` and alias `ETWFE` in `__init__.py` From 5b30d0a42ffb8881a44d33c6d3aa7ea99aa0db87 Mon Sep 17 00:00:00 2001 From: wenddymacro <50739376+wenddymacro@users.noreply.github.com> Date: Wed, 18 Mar 2026 17:54:39 +0800 Subject: [PATCH 03/16] chore: save WooldridgeDiD implementation plan and progress checkpoint --- docs/superpowers/CHECKPOINT.md | 131 + .../plans/2026-03-18-wooldridge-did.md | 2233 +++++++++++++++++ 2 files changed, 2364 insertions(+) create mode 100644 docs/superpowers/CHECKPOINT.md create mode 100644 docs/superpowers/plans/2026-03-18-wooldridge-did.md diff --git a/docs/superpowers/CHECKPOINT.md b/docs/superpowers/CHECKPOINT.md new file mode 100644 index 00000000..ba069b2d --- /dev/null +++ b/docs/superpowers/CHECKPOINT.md @@ -0,0 +1,131 @@ +# WooldridgeDiD 实现进度存档 + +**最后更新:** 2026-03-18 +**当前阶段:** 设计完成,实现规划完成,待修复计划文档 P1/P2 问题后开始编码 + +--- + +## 任务背景 + +将 Stata 包 `jwdid`(Wooldridge 2021/2023 Extended Two-Way Fixed Effects)整合到 diff-diff Python 库,保持与 Stata 实现一致。 + +--- + +## 已完成工作 + +### 设计阶段 ✅ +- **设计文档**:`docs/superpowers/specs/2026-03-18-wooldridge-did-design.md` + - 经两轮 spec review,所有 P1/P2 问题已解决,已 approved + - 主要决定:单类 `WooldridgeDiD`(别名 `ETWFE`),`method` 参数切换 ols/logit/poisson + +### 实现规划阶段 ✅(待修复) +- **实现计划**:`docs/superpowers/plans/2026-03-18-wooldridge-did.md` + - 12 个任务,TDD 风格 + - plan review 发现 **3 个 P1 问题** + **6 个 P2 问题**,**尚未修复**,下次开始前需先修复 + +--- + +## 下次开始时的步骤 + +### 第一步:修复实现计划中的 P1/P2 问题 + +在编写任何代码之前,先修复 `docs/superpowers/plans/2026-03-18-wooldridge-did.md`: + +#### P1 问题(必须修复) + +**P1-1:Task 7,`_fit_logit` 中 `compute_robust_vcov` 被传了不存在的 `weights=` 参数** +- 位置:Task 7 Step 3,`_fit_logit` 方法 +- 问题:`compute_robust_vcov(X, resids, cluster_ids=..., weights=probs*(1-probs))` — 该函数无 `weights` 参数,会报 `TypeError` +- 修复:手动计算 logit 加权 sandwich vcov,方式与 `_fit_poisson` 中相同: + ```python + W = probs * (1 - probs) # logit variance + XtWX = X_with_intercept.T @ (W[:, None] * X_with_intercept) + XtWX_inv = np.linalg.inv(XtWX) + resids = y - probs + # cluster or plain meat + meat = ... # 同 _fit_poisson 的聚类 meat 计算 + vcov_full = XtWX_inv @ meat @ XtWX_inv + ``` + +**P1-2:Task 9,bootstrap 块引用了未定义的变量 `post_keys`** +- 位置:Task 9 Step 3,`_fit_ols` bootstrap 块 +- 修复:在 bootstrap 循环前加一行: + ```python + post_keys = [(g, t) for (g, t) in gt_keys if t >= g] + ``` + +**P1-3:Task 7,logit delta method 梯度向量计算有 bug** +- 位置:Task 7 Step 3,`_fit_logit` 的 ATT delta method 部分 +- 问题:`grad += np.mean(d_base, axis=0)` 会覆盖掉之前设置的 `grad[1 + idx] = d_delta` +- 修复:分开设置,不相互覆盖: + ```python + grad = np.mean(d_base, axis=0).copy() # baseline covariate partials + grad[1 + idx] += d_delta # add treatment coefficient partial + ``` + +#### P2 问题(建议修复) + +**P2-1:Task 4,`_filter_sample` 中 `not_yet_treated` 分支有死代码** +- 删除第一个 `control_mask = ...` 赋值(立即被覆盖) + +**P2-2:Task 4,`_build_interaction_matrix` 对 `not_yet_treated` 包含了不应包含的 pre-treatment cells** +- 当 `anticipation==0` 时,`not_yet_treated` 控制组下不应包含 `t < g` 的格子 +- 需要在调用 `_build_interaction_matrix` 时传入 `control_group` 参数并据此过滤 + +**P2-3:Task 6 缺少 TDD 红色阶段** +- 在 Task 6 Step 2 中补上"先运行确认失败"的说明 + +**P2-4:Task 2,`_make_minimal_results` 缺少 `_gt_keys` 字段** +- 在测试 helper 中加入 `_gt_keys=[(2,2),(2,3),(3,3)]` + +**P2-5:Task 5,缺少解释为何不加截距的注释** +- 在 `_fit_ols` 的 `solve_ols` 调用处加注释说明 within-transform 后不需要截距 + +**P2-6:Task 10,缺少 CS(Callaway-Sant'Anna)等价性测试** +- Spec 第 7 节要求:线性 ETWFE 的 ATT(g,t) 应约等于 CallawaySantAnna 的 ATT(g,t)(容差 ~1e-2) +- 需在 Task 10 中加该对比测试 + +### 第二步:修复完毕后开始实现 + +按计划文档 Task 1 → Task 12 顺序执行,使用 `superpowers:subagent-driven-development` 或 `superpowers:executing-plans`。 + +--- + +## 关键文件位置 + +| 文件 | 说明 | +|------|------| +| `docs/superpowers/specs/2026-03-18-wooldridge-did-design.md` | 设计文档(已 approved) | +| `docs/superpowers/plans/2026-03-18-wooldridge-did.md` | 实现计划(需先修复 P1/P2) | +| `docs/superpowers/CHECKPOINT.md` | 本文件 | + +## 将新建的文件 + +| 文件 | 说明 | +|------|------| +| `diff_diff/wooldridge.py` | `WooldridgeDiD` 估计器主体 | +| `diff_diff/wooldridge_results.py` | `WooldridgeDiDResults` 结果类 | +| `tests/test_wooldridge.py` | 完整测试套件 | + +## 将修改的文件 + +| 文件 | 修改内容 | +|------|---------| +| `diff_diff/linalg.py` | 新增 `solve_poisson()` IRLS 求解器 | +| `diff_diff/__init__.py` | 导出 `WooldridgeDiD`、`WooldridgeDiDResults`、别名 `ETWFE` | +| `docs/methodology/REGISTRY.md` | 新增 ETWFE 方法论章节 | + +--- + +## 设计摘要(供快速回顾) + +| 方面 | 决定 | +|------|------| +| 类名 | `WooldridgeDiD`,别名 `ETWFE` | +| 线性(`method="ols"`) | 饱和 cohort×time 交互 + `within_transform` 吸收 unit/time FE → `solve_ols` | +| Logit(`method="logit"`) | 显式 cohort×period group FE dummies(drop_first)+ `solve_logit` + ASF ATT | +| Poisson(`method="poisson"`) | 新 `solve_poisson` IRLS + group FE dummies + ASF ATT | +| 控制组 | `not_yet_treated`(默认)或 `never_treated`,通过参数切换 | +| 聚合 | 4 种:simple / group / calendar / event,精确对应 `jwdid_estat` 权重公式 | +| 标准误 | 默认 unit 层面聚类(= jwdid 默认);可选 multiplier bootstrap + wild cluster bootstrap(仅 OLS)| +| 协变量 | `exovar`(不交互)/ `xtvar`(cohort×period 去均值)/ `xgvar`(与队列交互)| diff --git a/docs/superpowers/plans/2026-03-18-wooldridge-did.md b/docs/superpowers/plans/2026-03-18-wooldridge-did.md new file mode 100644 index 00000000..728ff137 --- /dev/null +++ b/docs/superpowers/plans/2026-03-18-wooldridge-did.md @@ -0,0 +1,2233 @@ +# WooldridgeDiD (ETWFE) Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Implement `WooldridgeDiD` (Wooldridge 2021/2023 Extended Two-Way Fixed Effects) as a standalone estimator in diff-diff, faithful to the Stata `jwdid` package. + +**Architecture:** Standalone estimator class (not inheriting `DifferenceInDifferences`) with separate results dataclass. Linear path uses existing `solve_ols` + `within_transform`; nonlinear path adds a new `solve_poisson` to `linalg.py` and leverages existing `solve_logit`. All four `jwdid_estat` aggregation types supported. Bootstrap via existing multiplier and wild-cluster mechanisms. + +**Tech Stack:** numpy, pandas, scipy (no new dependencies). Existing `linalg.solve_ols`, `linalg.solve_logit`, `utils.within_transform`, `utils.safe_inference`. + +**Spec:** `docs/superpowers/specs/2026-03-18-wooldridge-did-design.md` + +--- + +## File Map + +| Action | File | Responsibility | +|--------|------|----------------| +| Create | `diff_diff/wooldridge_results.py` | `WooldridgeDiDResults` dataclass + aggregation methods | +| Create | `diff_diff/wooldridge.py` | `WooldridgeDiD` estimator: constructor, fit (OLS + nonlinear), bootstrap | +| Create | `tests/test_wooldridge.py` | Full test suite | +| Modify | `diff_diff/linalg.py` | Add `solve_poisson()` IRLS solver | +| Modify | `diff_diff/__init__.py` | Export `WooldridgeDiD`, `WooldridgeDiDResults`, alias `ETWFE` | +| Modify | `docs/methodology/REGISTRY.md` | Add ETWFE methodology section | + +--- + +## Task 1: `solve_poisson()` in `linalg.py` + +**Files:** +- Modify: `diff_diff/linalg.py` (append after `solve_logit`) +- Test: `tests/test_linalg.py` (add to existing file) + +- [ ] **Step 1: Write the failing test** + +Open `tests/test_linalg.py` and add at the end: + +```python +class TestSolvePoisson: + def test_basic_convergence(self): + """solve_poisson converges on simple count data.""" + rng = np.random.default_rng(42) + n = 200 + X = np.column_stack([np.ones(n), rng.standard_normal((n, 2))]) + true_beta = np.array([0.5, 0.3, -0.2]) + mu = np.exp(X @ true_beta) + y = rng.poisson(mu).astype(float) + beta, W = solve_poisson(X, y) + assert beta.shape == (3,) + assert W.shape == (n,) + assert np.allclose(beta, true_beta, atol=0.15) + + def test_returns_weights(self): + """solve_poisson returns final mu weights for vcov computation.""" + rng = np.random.default_rng(0) + n = 100 + X = np.column_stack([np.ones(n), rng.standard_normal(n)]) + y = rng.poisson(2.0, size=n).astype(float) + beta, W = solve_poisson(X, y) + assert (W > 0).all() + + def test_non_negative_output(self): + """Fitted mu = exp(Xb) should be strictly positive.""" + rng = np.random.default_rng(1) + n = 50 + X = np.column_stack([np.ones(n), rng.standard_normal(n)]) + y = rng.poisson(1.0, size=n).astype(float) + beta, W = solve_poisson(X, y) + mu_hat = np.exp(X @ beta) + assert (mu_hat > 0).all() + + def test_no_intercept_prepended(self): + """solve_poisson does NOT add intercept (caller's responsibility).""" + rng = np.random.default_rng(2) + n = 80 + # X already has intercept — verify coefficient count matches columns + X = np.column_stack([np.ones(n), rng.standard_normal(n)]) + y = rng.poisson(1.5, size=n).astype(float) + beta, _ = solve_poisson(X, y) + assert len(beta) == 2 # not 3 +``` + +Add import at top of test file: `from diff_diff.linalg import solve_poisson` + +- [ ] **Step 2: Run test to verify it fails** + +```bash +pytest tests/test_linalg.py::TestSolvePoisson -v +``` + +Expected: `ImportError: cannot import name 'solve_poisson'` + +- [ ] **Step 3: Implement `solve_poisson` in `linalg.py`** + +Append after the `solve_logit` function (around line 960): + +```python +def solve_poisson( + X: np.ndarray, + y: np.ndarray, + max_iter: int = 25, + tol: float = 1e-8, +) -> Tuple[np.ndarray, np.ndarray]: + """Poisson IRLS (Newton-Raphson with log link). + + Does NOT prepend an intercept — caller must include one if needed. + Returns (beta, W_final) where W_final = mu_hat (used for sandwich vcov). + + Parameters + ---------- + X : (n, k) design matrix (caller provides intercept / group FE dummies) + y : (n,) non-negative count outcomes + max_iter : maximum IRLS iterations + tol : convergence threshold on sup-norm of coefficient change + + Returns + ------- + beta : (k,) coefficient vector + W : (n,) final fitted means mu_hat (weights for sandwich vcov) + """ + n, k = X.shape + beta = np.zeros(k) + for _ in range(max_iter): + eta = X @ beta + mu = np.clip(np.exp(eta), 1e-10, None) # clip prevents log(0) + score = X.T @ (y - mu) # gradient of log-likelihood + hess = X.T @ (mu[:, None] * X) # -Hessian = X'WX, W=diag(mu) + try: + delta = np.linalg.solve(hess, score) + except np.linalg.LinAlgError: + break + beta_new = beta + delta + if np.max(np.abs(beta_new - beta)) < tol: + beta = beta_new + mu = np.clip(np.exp(X @ beta), 1e-10, None) + break + beta = beta_new + else: + import warnings + warnings.warn( + "solve_poisson did not converge in {} iterations".format(max_iter), + RuntimeWarning, + stacklevel=2, + ) + mu_final = np.clip(np.exp(X @ beta), 1e-10, None) + return beta, mu_final +``` + +- [ ] **Step 4: Run tests to verify they pass** + +```bash +pytest tests/test_linalg.py::TestSolvePoisson -v +``` + +Expected: all 4 tests PASS + +- [ ] **Step 5: Commit** + +```bash +git add diff_diff/linalg.py tests/test_linalg.py +git commit -m "feat(linalg): add solve_poisson IRLS solver for Wooldridge nonlinear ETWFE" +``` + +--- + +## Task 2: `WooldridgeDiDResults` dataclass + +**Files:** +- Create: `diff_diff/wooldridge_results.py` +- Test: `tests/test_wooldridge.py` (create file, results section) + +- [ ] **Step 1: Write the failing tests** + +Create `tests/test_wooldridge.py`: + +```python +"""Tests for WooldridgeDiD estimator and WooldridgeDiDResults.""" +import numpy as np +import pandas as pd +import pytest +from diff_diff.wooldridge_results import WooldridgeDiDResults + + +def _make_minimal_results(**kwargs): + """Helper: build a WooldridgeDiDResults with required fields.""" + defaults = dict( + group_time_effects={ + (2, 2): {"att": 1.0, "se": 0.5, "t_stat": 2.0, "p_value": 0.04, "conf_int": (0.02, 1.98)}, + (2, 3): {"att": 1.5, "se": 0.6, "t_stat": 2.5, "p_value": 0.01, "conf_int": (0.32, 2.68)}, + (3, 3): {"att": 0.8, "se": 0.4, "t_stat": 2.0, "p_value": 0.04, "conf_int": (0.02, 1.58)}, + }, + overall_att=1.1, + overall_se=0.35, + overall_t_stat=3.14, + overall_p_value=0.002, + overall_conf_int=(0.41, 1.79), + group_effects=None, + calendar_effects=None, + event_study_effects=None, + method="ols", + control_group="not_yet_treated", + groups=[2, 3], + time_periods=[1, 2, 3], + n_obs=300, + n_treated_units=100, + n_control_units=200, + alpha=0.05, + _gt_weights={(2, 2): 50, (2, 3): 50, (3, 3): 30}, + _gt_vcov=None, + ) + defaults.update(kwargs) + return WooldridgeDiDResults(**defaults) + + +class TestWooldridgeDiDResults: + def test_repr(self): + r = _make_minimal_results() + s = repr(r) + assert "WooldridgeDiDResults" in s + assert "ATT" in s + + def test_summary_default(self): + r = _make_minimal_results() + s = r.summary() + assert "1.1" in s or "ATT" in s + + def test_to_dataframe_event(self): + r = _make_minimal_results() + r.aggregate("event") + df = r.to_dataframe("event") + assert isinstance(df, pd.DataFrame) + assert "att" in df.columns + + def test_aggregate_simple_returns_self(self): + r = _make_minimal_results() + result = r.aggregate("simple") + assert result is r # chaining + + def test_aggregate_group(self): + r = _make_minimal_results() + r.aggregate("group") + assert r.group_effects is not None + assert 2 in r.group_effects + assert 3 in r.group_effects + + def test_aggregate_calendar(self): + r = _make_minimal_results() + r.aggregate("calendar") + assert r.calendar_effects is not None + assert 2 in r.calendar_effects or 3 in r.calendar_effects + + def test_aggregate_event(self): + r = _make_minimal_results() + r.aggregate("event") + assert r.event_study_effects is not None + # relative period 0 (treatment period itself) should be present + assert 0 in r.event_study_effects or 1 in r.event_study_effects + + def test_aggregate_invalid_raises(self): + r = _make_minimal_results() + with pytest.raises(ValueError, match="type"): + r.aggregate("bad_type") +``` + +- [ ] **Step 2: Run tests to verify they fail** + +```bash +pytest tests/test_wooldridge.py::TestWooldridgeDiDResults -v +``` + +Expected: `ModuleNotFoundError: No module named 'diff_diff.wooldridge_results'` + +- [ ] **Step 3: Implement `wooldridge_results.py`** + +Create `diff_diff/wooldridge_results.py`: + +```python +"""Results class for WooldridgeDiD (ETWFE) estimator.""" +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any, Dict, List, Optional, Tuple + +import numpy as np +import pandas as pd + +from diff_diff.utils import safe_inference + + +@dataclass +class WooldridgeDiDResults: + """Results from WooldridgeDiD.fit(). + + Core output is ``group_time_effects``: a dict keyed by (cohort_g, time_t) + with per-cell ATT estimates and inference. Call ``.aggregate(type)`` to + compute any of the four jwdid_estat aggregation types. + """ + + # ------------------------------------------------------------------ # + # Core cohort×time estimates # + # ------------------------------------------------------------------ # + group_time_effects: Dict[Tuple[Any, Any], Dict[str, Any]] + """key=(g,t), value={att, se, t_stat, p_value, conf_int}""" + + # ------------------------------------------------------------------ # + # Simple (overall) aggregation — always populated at fit time # + # ------------------------------------------------------------------ # + overall_att: float + overall_se: float + overall_t_stat: float + overall_p_value: float + overall_conf_int: Tuple[float, float] + + # ------------------------------------------------------------------ # + # Other aggregations — populated by .aggregate() # + # ------------------------------------------------------------------ # + group_effects: Optional[Dict[Any, Dict]] = field(default=None, repr=False) + calendar_effects: Optional[Dict[Any, Dict]] = field(default=None, repr=False) + event_study_effects: Optional[Dict[int, Dict]] = field(default=None, repr=False) + + # ------------------------------------------------------------------ # + # Metadata # + # ------------------------------------------------------------------ # + method: str = "ols" + control_group: str = "not_yet_treated" + groups: List[Any] = field(default_factory=list) + time_periods: List[Any] = field(default_factory=list) + n_obs: int = 0 + n_treated_units: int = 0 + n_control_units: int = 0 + alpha: float = 0.05 + + # ------------------------------------------------------------------ # + # Internal — used by aggregate() for delta-method SEs # + # ------------------------------------------------------------------ # + _gt_weights: Dict[Tuple[Any, Any], int] = field(default_factory=dict, repr=False) + _gt_vcov: Optional[np.ndarray] = field(default=None, repr=False) + """Full vcov of all β_{g,t} coefficients (ordered same as sorted group_time_effects keys).""" + _gt_keys: List[Tuple[Any, Any]] = field(default_factory=list, repr=False) + """Ordered list of (g,t) keys corresponding to _gt_vcov columns.""" + + # ------------------------------------------------------------------ # + # Public methods # + # ------------------------------------------------------------------ # + + def aggregate(self, type: str) -> "WooldridgeDiDResults": # noqa: A002 + """Compute and store one of the four jwdid_estat aggregation types. + + Parameters + ---------- + type : "simple" | "group" | "calendar" | "event" + + Returns self for chaining. + """ + valid = ("simple", "group", "calendar", "event") + if type not in valid: + raise ValueError(f"type must be one of {valid}, got {type!r}") + + gt = self.group_time_effects + weights = self._gt_weights + vcov = self._gt_vcov + keys_ordered = self._gt_keys if self._gt_keys else sorted(gt.keys()) + + def _agg_se(w_vec: np.ndarray) -> float: + """Delta-method SE for a linear combination w'β given full vcov.""" + if vcov is None or len(w_vec) != vcov.shape[0]: + return float("nan") + return float(np.sqrt(max(w_vec @ vcov @ w_vec, 0.0))) + + def _build_effect(att: float, se: float) -> Dict[str, Any]: + t_stat, p_value, conf_int = safe_inference(att, se, alpha=self.alpha) + return {"att": att, "se": se, "t_stat": t_stat, + "p_value": p_value, "conf_int": conf_int} + + if type == "simple": + # Re-compute overall using delta method (already stored in overall_* fields) + # This is a no-op but keeps the method callable. + pass + + elif type == "group": + result: Dict[Any, Dict] = {} + for g in self.groups: + cells = [(g2, t) for (g2, t) in keys_ordered if g2 == g and t >= g] + if not cells: + continue + w_total = sum(weights.get(c, 0) for c in cells) + if w_total == 0: + continue + att = sum(weights.get(c, 0) * gt[c]["att"] for c in cells) / w_total + # delta-method weights vector over all keys_ordered + w_vec = np.array([ + weights.get(c, 0) / w_total if c in cells else 0.0 + for c in keys_ordered + ]) + se = _agg_se(w_vec) + result[g] = _build_effect(att, se) + self.group_effects = result + + elif type == "calendar": + result = {} + for t in self.time_periods: + cells = [(g, t2) for (g, t2) in keys_ordered if t2 == t and t >= g] + if not cells: + continue + w_total = sum(weights.get(c, 0) for c in cells) + if w_total == 0: + continue + att = sum(weights.get(c, 0) * gt[c]["att"] for c in cells) / w_total + w_vec = np.array([ + weights.get(c, 0) / w_total if c in cells else 0.0 + for c in keys_ordered + ]) + se = _agg_se(w_vec) + result[t] = _build_effect(att, se) + self.calendar_effects = result + + elif type == "event": + all_k = sorted({t - g for (g, t) in keys_ordered}) + result = {} + for k in all_k: + cells = [(g, t) for (g, t) in keys_ordered if t - g == k] + if not cells: + continue + w_total = sum(weights.get(c, 0) for c in cells) + if w_total == 0: + continue + att = sum(weights.get(c, 0) * gt[c]["att"] for c in cells) / w_total + w_vec = np.array([ + weights.get(c, 0) / w_total if c in cells else 0.0 + for c in keys_ordered + ]) + se = _agg_se(w_vec) + result[k] = _build_effect(att, se) + self.event_study_effects = result + + return self + + def summary(self, aggregation: str = "simple") -> str: + """Print formatted summary table. + + Parameters + ---------- + aggregation : which aggregation to display ("simple", "group", "calendar", "event") + """ + lines = [ + "=" * 70, + " Wooldridge Extended Two-Way Fixed Effects (ETWFE) Results", + "=" * 70, + f"Method: {self.method}", + f"Control group: {self.control_group}", + f"Observations: {self.n_obs}", + f"Treated units: {self.n_treated_units}", + f"Control units: {self.n_control_units}", + "-" * 70, + ] + + def _fmt_row(label: str, att: float, se: float, t: float, + p: float, ci: Tuple) -> str: + from diff_diff.results import _get_significance_stars # type: ignore + stars = _get_significance_stars(p) if not np.isnan(p) else "" + ci_lo = f"{ci[0]:.4f}" if not np.isnan(ci[0]) else "NaN" + ci_hi = f"{ci[1]:.4f}" if not np.isnan(ci[1]) else "NaN" + return ( + f"{label:<22} {att:>10.4f} {se:>10.4f} {t:>8.3f} " + f"{p:>8.4f}{stars} [{ci_lo}, {ci_hi}]" + ) + + header = ( + f"{'Parameter':<22} {'Estimate':>10} {'Std. Err.':>10} " + f"{'t-stat':>8} {'P>|t|':>8} [95% CI]" + ) + lines.append(header) + lines.append("-" * 70) + + if aggregation == "simple": + lines.append(_fmt_row( + "ATT (simple)", + self.overall_att, self.overall_se, + self.overall_t_stat, self.overall_p_value, self.overall_conf_int, + )) + elif aggregation == "group" and self.group_effects: + for g, eff in sorted(self.group_effects.items()): + lines.append(_fmt_row( + f"ATT(g={g})", + eff["att"], eff["se"], eff["t_stat"], eff["p_value"], eff["conf_int"], + )) + elif aggregation == "calendar" and self.calendar_effects: + for t, eff in sorted(self.calendar_effects.items()): + lines.append(_fmt_row( + f"ATT(t={t})", + eff["att"], eff["se"], eff["t_stat"], eff["p_value"], eff["conf_int"], + )) + elif aggregation == "event" and self.event_study_effects: + for k, eff in sorted(self.event_study_effects.items()): + label = f"ATT(k={k})" + (" [pre]" if k < 0 else "") + lines.append(_fmt_row( + label, eff["att"], eff["se"], + eff["t_stat"], eff["p_value"], eff["conf_int"], + )) + else: + lines.append(f" (call .aggregate({aggregation!r}) first)") + + lines.append("=" * 70) + return "\n".join(lines) + + def to_dataframe(self, aggregation: str = "event") -> pd.DataFrame: + """Export aggregated effects to a DataFrame. + + Parameters + ---------- + aggregation : "simple" | "group" | "calendar" | "event" | "gt" + Use "gt" to export raw group-time effects. + """ + if aggregation == "gt": + rows = [] + for (g, t), eff in sorted(self.group_time_effects.items()): + row = {"cohort": g, "time": t, "relative_period": t - g} + row.update(eff) + rows.append(row) + return pd.DataFrame(rows) + + mapping = { + "simple": [{"label": "ATT", "att": self.overall_att, + "se": self.overall_se, "t_stat": self.overall_t_stat, + "p_value": self.overall_p_value, + "conf_int_lo": self.overall_conf_int[0], + "conf_int_hi": self.overall_conf_int[1]}], + "group": [ + {"cohort": g, **{k: v for k, v in eff.items() if k != "conf_int"}, + "conf_int_lo": eff["conf_int"][0], "conf_int_hi": eff["conf_int"][1]} + for g, eff in sorted((self.group_effects or {}).items()) + ], + "calendar": [ + {"time": t, **{k: v for k, v in eff.items() if k != "conf_int"}, + "conf_int_lo": eff["conf_int"][0], "conf_int_hi": eff["conf_int"][1]} + for t, eff in sorted((self.calendar_effects or {}).items()) + ], + "event": [ + {"relative_period": k, + **{kk: vv for kk, vv in eff.items() if kk != "conf_int"}, + "conf_int_lo": eff["conf_int"][0], "conf_int_hi": eff["conf_int"][1]} + for k, eff in sorted((self.event_study_effects or {}).items()) + ], + } + rows = mapping.get(aggregation, []) + return pd.DataFrame(rows) + + def plot_event_study(self, **kwargs) -> None: + """Event study plot. Calls aggregate('event') if needed.""" + if self.event_study_effects is None: + self.aggregate("event") + from diff_diff.visualization import plot_event_study # type: ignore + effects = {k: v["att"] for k, v in (self.event_study_effects or {}).items()} + se = {k: v["se"] for k, v in (self.event_study_effects or {}).items()} + plot_event_study(effects=effects, se=se, alpha=self.alpha, **kwargs) + + def __repr__(self) -> str: + n_gt = len(self.group_time_effects) + att_str = f"{self.overall_att:.4f}" if not np.isnan(self.overall_att) else "NaN" + se_str = f"{self.overall_se:.4f}" if not np.isnan(self.overall_se) else "NaN" + p_str = f"{self.overall_p_value:.4f}" if not np.isnan(self.overall_p_value) else "NaN" + return ( + f"WooldridgeDiDResults(" + f"ATT={att_str}, SE={se_str}, p={p_str}, " + f"n_gt={n_gt}, method={self.method!r})" + ) +``` + +- [ ] **Step 4: Run tests to verify they pass** + +```bash +pytest tests/test_wooldridge.py::TestWooldridgeDiDResults -v +``` + +Expected: all 8 tests PASS + +- [ ] **Step 5: Commit** + +```bash +git add diff_diff/wooldridge_results.py tests/test_wooldridge.py +git commit -m "feat: add WooldridgeDiDResults dataclass with four aggregation types" +``` + +--- + +## Task 3: `WooldridgeDiD` constructor, `get_params`, `set_params` + +**Files:** +- Create: `diff_diff/wooldridge.py` +- Test: `tests/test_wooldridge.py` (add API tests) + +- [ ] **Step 1: Write the failing tests** + +Add to `tests/test_wooldridge.py`: + +```python +from diff_diff.wooldridge import WooldridgeDiD + + +class TestWooldridgeDiDAPI: + def test_default_construction(self): + est = WooldridgeDiD() + assert est.method == "ols" + assert est.control_group == "not_yet_treated" + assert est.anticipation == 0 + assert est.demean_covariates is True + assert est.alpha == 0.05 + assert est.cluster is None + assert est.n_bootstrap == 0 + assert est.bootstrap_weights == "rademacher" + assert est.seed is None + assert est.rank_deficient_action == "warn" + assert not est.is_fitted_ + + def test_invalid_method_raises(self): + with pytest.raises(ValueError, match="method"): + WooldridgeDiD(method="probit") + + def test_invalid_control_group_raises(self): + with pytest.raises(ValueError, match="control_group"): + WooldridgeDiD(control_group="clean_control") + + def test_invalid_anticipation_raises(self): + with pytest.raises(ValueError, match="anticipation"): + WooldridgeDiD(anticipation=-1) + + def test_get_params_roundtrip(self): + est = WooldridgeDiD(method="logit", alpha=0.1, anticipation=1) + params = est.get_params() + assert params["method"] == "logit" + assert params["alpha"] == 0.1 + assert params["anticipation"] == 1 + + def test_set_params_roundtrip(self): + est = WooldridgeDiD() + est.set_params(alpha=0.01, n_bootstrap=100) + assert est.alpha == 0.01 + assert est.n_bootstrap == 100 + + def test_set_params_returns_self(self): + est = WooldridgeDiD() + result = est.set_params(alpha=0.1) + assert result is est + + def test_set_params_unknown_raises(self): + est = WooldridgeDiD() + with pytest.raises(ValueError, match="Unknown"): + est.set_params(nonexistent_param=42) + + def test_results_before_fit_raises(self): + est = WooldridgeDiD() + with pytest.raises(RuntimeError, match="fit"): + _ = est.results_ +``` + +- [ ] **Step 2: Run tests to verify they fail** + +```bash +pytest tests/test_wooldridge.py::TestWooldridgeDiDAPI -v +``` + +Expected: `ModuleNotFoundError: No module named 'diff_diff.wooldridge'` + +- [ ] **Step 3: Implement constructor + get/set params** + +Create `diff_diff/wooldridge.py`: + +```python +"""WooldridgeDiD: Extended Two-Way Fixed Effects (ETWFE) estimator. + +Implements Wooldridge (2021, 2023) ETWFE, faithful to the Stata jwdid package. + +References +---------- +Wooldridge (2021). Two-Way Fixed Effects, the Two-Way Mundlak Regression, + and Difference-in-Differences Estimators. SSRN 3906345. +Wooldridge (2023). Simple approaches to nonlinear difference-in-differences + with panel data. The Econometrics Journal, 26(3), C31-C66. +Friosavila (2021). jwdid: Stata module. SSC s459114. +""" +from __future__ import annotations + +from typing import Any, Dict, List, Optional, Tuple + +import numpy as np +import pandas as pd + +from diff_diff.linalg import compute_robust_vcov, solve_logit, solve_ols, solve_poisson +from diff_diff.utils import safe_inference, within_transform +from diff_diff.wooldridge_results import WooldridgeDiDResults + +_VALID_METHODS = ("ols", "logit", "poisson") +_VALID_CONTROL_GROUPS = ("never_treated", "not_yet_treated") +_VALID_BOOTSTRAP_WEIGHTS = ("rademacher", "webb", "mammen") + + +class WooldridgeDiD: + """Extended Two-Way Fixed Effects (ETWFE) DiD estimator. + + Implements the Wooldridge (2021) saturated cohort×time regression and + Wooldridge (2023) nonlinear extensions (logit, Poisson). Produces all + four ``jwdid_estat`` aggregation types: simple, group, calendar, event. + + Parameters + ---------- + method : {"ols", "logit", "poisson"} + Estimation method. "ols" for continuous outcomes; "logit" for binary + or fractional outcomes; "poisson" for count data. + control_group : {"not_yet_treated", "never_treated"} + Which units serve as the comparison group. "not_yet_treated" (jwdid + default) uses all untreated observations at each time period; + "never_treated" uses only units never treated throughout the sample. + anticipation : int + Number of periods before treatment onset to include as treatment cells + (anticipation effects). 0 means no anticipation. + demean_covariates : bool + If True (jwdid default), ``xtvar`` covariates are demeaned within each + cohort×period cell before entering the regression. Set to False to + replicate jwdid's ``xasis`` option. + alpha : float + Significance level for confidence intervals. + cluster : str or None + Column name to use for cluster-robust SEs. Defaults to the ``unit`` + identifier passed to ``fit()``. + n_bootstrap : int + Number of bootstrap replications. 0 disables bootstrap. + bootstrap_weights : {"rademacher", "webb", "mammen"} + Bootstrap weight distribution. + seed : int or None + Random seed for reproducibility. + rank_deficient_action : {"warn", "error", "silent"} + How to handle rank-deficient design matrices. + """ + + def __init__( + self, + method: str = "ols", + control_group: str = "not_yet_treated", + anticipation: int = 0, + demean_covariates: bool = True, + alpha: float = 0.05, + cluster: Optional[str] = None, + n_bootstrap: int = 0, + bootstrap_weights: str = "rademacher", + seed: Optional[int] = None, + rank_deficient_action: str = "warn", + ) -> None: + if method not in _VALID_METHODS: + raise ValueError(f"method must be one of {_VALID_METHODS}, got {method!r}") + if control_group not in _VALID_CONTROL_GROUPS: + raise ValueError( + f"control_group must be one of {_VALID_CONTROL_GROUPS}, got {control_group!r}" + ) + if anticipation < 0: + raise ValueError(f"anticipation must be >= 0, got {anticipation}") + + self.method = method + self.control_group = control_group + self.anticipation = anticipation + self.demean_covariates = demean_covariates + self.alpha = alpha + self.cluster = cluster + self.n_bootstrap = n_bootstrap + self.bootstrap_weights = bootstrap_weights + self.seed = seed + self.rank_deficient_action = rank_deficient_action + + self.is_fitted_: bool = False + self._results: Optional[WooldridgeDiDResults] = None + + @property + def results_(self) -> WooldridgeDiDResults: + if not self.is_fitted_: + raise RuntimeError("Call fit() before accessing results_") + return self._results # type: ignore[return-value] + + def get_params(self) -> Dict[str, Any]: + """Return estimator parameters (sklearn-compatible).""" + return { + "method": self.method, + "control_group": self.control_group, + "anticipation": self.anticipation, + "demean_covariates": self.demean_covariates, + "alpha": self.alpha, + "cluster": self.cluster, + "n_bootstrap": self.n_bootstrap, + "bootstrap_weights": self.bootstrap_weights, + "seed": self.seed, + "rank_deficient_action": self.rank_deficient_action, + } + + def set_params(self, **params: Any) -> "WooldridgeDiD": + """Set estimator parameters (sklearn-compatible). Returns self.""" + for key, value in params.items(): + if not hasattr(self, key): + raise ValueError(f"Unknown parameter: {key!r}") + setattr(self, key, value) + return self + + def fit( + self, + data: pd.DataFrame, + outcome: str, + unit: str, + time: str, + cohort: str, + exovar: Optional[List[str]] = None, + xtvar: Optional[List[str]] = None, + xgvar: Optional[List[str]] = None, + ) -> WooldridgeDiDResults: + """Fit the ETWFE model. See class docstring for parameter details. + + Parameters + ---------- + data : DataFrame with panel data (long format) + outcome : outcome column name + unit : unit identifier column + time : time period column + cohort : first treatment period (0 or NaN = never treated) + exovar : time-invariant covariates added without interaction/demeaning + xtvar : time-varying covariates (demeaned within cohort×period cells + when ``demean_covariates=True``) + xgvar : covariates interacted with each cohort indicator + """ + # Placeholder — implementation in Tasks 4 & 5 + raise NotImplementedError("fit() implemented in later tasks") +``` + +- [ ] **Step 4: Run tests to verify they pass** + +```bash +pytest tests/test_wooldridge.py::TestWooldridgeDiDAPI -v +``` + +Expected: all 9 tests PASS + +- [ ] **Step 5: Commit** + +```bash +git add diff_diff/wooldridge.py tests/test_wooldridge.py +git commit -m "feat: add WooldridgeDiD class scaffold with constructor and param API" +``` + +--- + +## Task 4: Data preparation helpers (filter, interaction matrix, covariate prep) + +**Files:** +- Modify: `diff_diff/wooldridge.py` (add private helpers) +- Test: `tests/test_wooldridge.py` (add internal prep tests) + +- [ ] **Step 1: Write the failing tests** + +Add to `tests/test_wooldridge.py`: + +```python +from diff_diff.wooldridge import ( + _filter_sample, + _build_interaction_matrix, + _prepare_covariates, +) + + +def _make_panel(n_units=10, n_periods=5, treat_share=0.5, seed=0): + """Create a simple balanced panel for testing.""" + rng = np.random.default_rng(seed) + units = np.arange(n_units) + n_treated = int(n_units * treat_share) + # Two cohorts: half treated in period 3, rest never treated + cohort = np.array([3] * n_treated + [0] * (n_units - n_treated)) + rows = [] + for u in units: + for t in range(1, n_periods + 1): + rows.append({"unit": u, "time": t, "cohort": cohort[u], + "y": rng.standard_normal(), "x1": rng.standard_normal()}) + return pd.DataFrame(rows) + + +class TestDataPrep: + def test_filter_sample_not_yet_treated(self): + df = _make_panel() + filtered = _filter_sample(df, unit="unit", time="time", cohort="cohort", + control_group="not_yet_treated", anticipation=0) + # All treated units should be present (all periods) + treated_units = df[df["cohort"] == 3]["unit"].unique() + assert set(treated_units).issubset(filtered["unit"].unique()) + + def test_filter_sample_never_treated(self): + df = _make_panel() + filtered = _filter_sample(df, unit="unit", time="time", cohort="cohort", + control_group="never_treated", anticipation=0) + # Only never-treated (cohort==0) and treated units should remain + # No not-yet-treated-only units; here all non-treated have cohort==0 + assert (filtered["cohort"].isin([0, 3])).all() + + def test_build_interaction_matrix_columns(self): + df = _make_panel() + filtered = _filter_sample(df, "unit", "time", "cohort", + "not_yet_treated", anticipation=0) + X_int, col_names, gt_keys = _build_interaction_matrix( + filtered, cohort="cohort", time="time", anticipation=0 + ) + # Each column should be a valid (g, t) pair with t >= g + for (g, t) in gt_keys: + assert t >= g + + def test_build_interaction_matrix_binary(self): + df = _make_panel() + filtered = _filter_sample(df, "unit", "time", "cohort", + "not_yet_treated", anticipation=0) + X_int, col_names, gt_keys = _build_interaction_matrix( + filtered, cohort="cohort", time="time", anticipation=0 + ) + # All values should be 0 or 1 + assert set(np.unique(X_int)).issubset({0, 1}) + + def test_prepare_covariates_exovar(self): + df = _make_panel() + X_cov = _prepare_covariates(df, exovar=["x1"], xtvar=None, xgvar=None, + cohort="cohort", time="time", + demean_covariates=True, groups=[3]) + assert X_cov.shape[0] == len(df) + assert X_cov.shape[1] == 1 # just x1 + + def test_prepare_covariates_xtvar_demeaned(self): + df = _make_panel() + X_raw = _prepare_covariates(df, exovar=None, xtvar=["x1"], xgvar=None, + cohort="cohort", time="time", + demean_covariates=False, groups=[3]) + X_dem = _prepare_covariates(df, exovar=None, xtvar=["x1"], xgvar=None, + cohort="cohort", time="time", + demean_covariates=True, groups=[3]) + # Demeaned version should differ from raw + assert not np.allclose(X_raw, X_dem) +``` + +- [ ] **Step 2: Run tests to verify they fail** + +```bash +pytest tests/test_wooldridge.py::TestDataPrep -v +``` + +Expected: `ImportError` (functions not yet defined) + +- [ ] **Step 3: Implement helper functions** + +Add to `diff_diff/wooldridge.py` (before the `WooldridgeDiD` class definition): + +```python +def _filter_sample( + data: pd.DataFrame, + unit: str, + time: str, + cohort: str, + control_group: str, + anticipation: int, +) -> pd.DataFrame: + """Return the analysis sample following jwdid selection rules. + + Treated units: all observations kept (pre-treatment window beyond + anticipation is not used as a treatment cell but is kept for FE). + Control units: for "not_yet_treated", units with cohort > t at each t + (including never-treated); for "never_treated", only cohort == 0/NaN. + """ + df = data.copy() + # Normalise never-treated: fill NaN cohort with 0 + df[cohort] = df[cohort].fillna(0) + + treated_mask = df[cohort] > 0 + + if control_group == "never_treated": + control_mask = df[cohort] == 0 + else: # not_yet_treated + # A unit is "not yet treated" at time t if its cohort > t + control_mask = (~treated_mask) | (df[cohort] > df[time]) + # Keep untreated-at-t observations for not-yet-treated units + control_mask = (df[cohort] == 0) | (df[cohort] > df[time]) + + return df[treated_mask | control_mask].copy() + + +def _build_interaction_matrix( + data: pd.DataFrame, + cohort: str, + time: str, + anticipation: int, +) -> Tuple[np.ndarray, List[str], List[Tuple[Any, Any]]]: + """Build the saturated cohort×time interaction design matrix. + + Returns + ------- + X_int : (n, n_cells) binary indicator matrix + col_names : list of string labels "g{g}_t{t}" + gt_keys : list of (g, t) tuples in same column order + """ + groups = sorted(g for g in data[cohort].unique() if g > 0) + times = sorted(data[time].unique()) + cohort_vals = data[cohort].values + time_vals = data[time].values + + cols = [] + col_names = [] + gt_keys = [] + + for g in groups: + for t in times: + if t >= g - anticipation: + indicator = ((cohort_vals == g) & (time_vals == t)).astype(float) + cols.append(indicator) + col_names.append(f"g{g}_t{t}") + gt_keys.append((g, t)) + + if not cols: + return np.empty((len(data), 0)), [], [] + return np.column_stack(cols), col_names, gt_keys + + +def _prepare_covariates( + data: pd.DataFrame, + exovar: Optional[List[str]], + xtvar: Optional[List[str]], + xgvar: Optional[List[str]], + cohort: str, + time: str, + demean_covariates: bool, + groups: List[Any], +) -> Optional[np.ndarray]: + """Build covariate matrix following jwdid covariate type conventions. + + Returns None if no covariates, else (n, k) array. + """ + parts = [] + + if exovar: + parts.append(data[exovar].values.astype(float)) + + if xtvar: + if demean_covariates: + # Within-cohort×period demeaning + grp_key = data[cohort].astype(str) + "_" + data[time].astype(str) + tmp = data[xtvar].copy() + for col in xtvar: + tmp[col] = tmp[col] - tmp.groupby(grp_key)[col].transform("mean") + parts.append(tmp.values.astype(float)) + else: + parts.append(data[xtvar].values.astype(float)) + + if xgvar: + for g in groups: + g_indicator = (data[cohort] == g).values.astype(float) + for col in xgvar: + parts.append((g_indicator * data[col].values).reshape(-1, 1)) + + if not parts: + return None + return np.hstack([p if p.ndim == 2 else p.reshape(-1, 1) for p in parts]) +``` + +Also update the imports at the top of the file to expose these as module-level functions +(they are already defined at module level, so they will be importable). + +- [ ] **Step 4: Run tests to verify they pass** + +```bash +pytest tests/test_wooldridge.py::TestDataPrep -v +``` + +Expected: all 6 tests PASS + +- [ ] **Step 5: Commit** + +```bash +git add diff_diff/wooldridge.py tests/test_wooldridge.py +git commit -m "feat: add ETWFE data preparation helpers (filter, interactions, covariates)" +``` + +--- + +## Task 5: Linear ETWFE `fit()` (OLS path) + +**Files:** +- Modify: `diff_diff/wooldridge.py` (implement `fit()` for method="ols") +- Test: `tests/test_wooldridge.py` (add fit tests) + +- [ ] **Step 1: Write the failing tests** + +Add to `tests/test_wooldridge.py`: + +```python +from diff_diff import load_dataset # or: from diff_diff.datasets import load_mpdta + + +class TestWooldridgeDiDFitOLS: + @pytest.fixture + def mpdta(self): + from diff_diff.datasets import load_mpdta + return load_mpdta() + + def test_fit_returns_results(self, mpdta): + est = WooldridgeDiD() + results = est.fit(mpdta, outcome="lemp", unit="countyreal", + time="year", cohort="first.treat") + assert isinstance(results, WooldridgeDiDResults) + + def test_fit_sets_is_fitted(self, mpdta): + est = WooldridgeDiD() + est.fit(mpdta, outcome="lemp", unit="countyreal", + time="year", cohort="first.treat") + assert est.is_fitted_ + + def test_overall_att_finite(self, mpdta): + est = WooldridgeDiD() + r = est.fit(mpdta, outcome="lemp", unit="countyreal", + time="year", cohort="first.treat") + assert np.isfinite(r.overall_att) + assert np.isfinite(r.overall_se) + assert r.overall_se > 0 + + def test_group_time_effects_populated(self, mpdta): + est = WooldridgeDiD() + r = est.fit(mpdta, outcome="lemp", unit="countyreal", + time="year", cohort="first.treat") + assert len(r.group_time_effects) > 0 + for (g, t), eff in r.group_time_effects.items(): + assert t >= g + assert "att" in eff and "se" in eff + + def test_all_inference_fields_finite(self, mpdta): + """No inference field should be NaN in normal data.""" + est = WooldridgeDiD() + r = est.fit(mpdta, outcome="lemp", unit="countyreal", + time="year", cohort="first.treat") + assert np.isfinite(r.overall_t_stat) + assert np.isfinite(r.overall_p_value) + assert all(np.isfinite(c) for c in r.overall_conf_int) + + def test_never_treated_control_group(self, mpdta): + est = WooldridgeDiD(control_group="never_treated") + r = est.fit(mpdta, outcome="lemp", unit="countyreal", + time="year", cohort="first.treat") + assert len(r.group_time_effects) > 0 + + def test_metadata_correct(self, mpdta): + est = WooldridgeDiD() + r = est.fit(mpdta, outcome="lemp", unit="countyreal", + time="year", cohort="first.treat") + assert r.method == "ols" + assert r.n_obs > 0 + assert r.n_treated_units > 0 + assert r.n_control_units > 0 +``` + +- [ ] **Step 2: Run tests to verify they fail** + +```bash +pytest tests/test_wooldridge.py::TestWooldridgeDiDFitOLS -v +``` + +Expected: `NotImplementedError: fit() implemented in later tasks` + +- [ ] **Step 3: Implement `fit()` OLS path** + +Replace the `fit()` placeholder in `diff_diff/wooldridge.py` with the full implementation. +Also add the `_fit_ols`, `_compute_gt_inference`, and `_build_simple_aggregation` helpers: + +```python +def fit( + self, + data: pd.DataFrame, + outcome: str, + unit: str, + time: str, + cohort: str, + exovar: Optional[List[str]] = None, + xtvar: Optional[List[str]] = None, + xgvar: Optional[List[str]] = None, +) -> WooldridgeDiDResults: + """Fit the ETWFE model.""" + df = data.copy() + df[cohort] = df[cohort].fillna(0) + + # 1. Filter to analysis sample + sample = _filter_sample(df, unit, time, cohort, self.control_group, self.anticipation) + + # 2. Build interaction matrix + X_int, col_names, gt_keys = _build_interaction_matrix( + sample, cohort=cohort, time=time, anticipation=self.anticipation + ) + + # 3. Covariates + groups = sorted(g for g in sample[cohort].unique() if g > 0) + X_cov = _prepare_covariates( + sample, exovar=exovar, xtvar=xtvar, xgvar=xgvar, + cohort=cohort, time=time, + demean_covariates=self.demean_covariates, + groups=groups, + ) + + all_regressors = col_names.copy() + if X_cov is not None: + X_design = np.hstack([X_int, X_cov]) + for i in range(X_cov.shape[1]): + all_regressors.append(f"_cov_{i}") + else: + X_design = X_int + + if self.method == "ols": + results = self._fit_ols( + sample, outcome, unit, time, cohort, + X_design, all_regressors, gt_keys, col_names, + groups, exovar, xtvar, xgvar, + ) + elif self.method == "logit": + results = self._fit_logit( + sample, outcome, unit, time, cohort, + X_design, all_regressors, gt_keys, col_names, groups, + ) + else: # poisson + results = self._fit_poisson( + sample, outcome, unit, time, cohort, + X_design, all_regressors, gt_keys, col_names, groups, + ) + + self._results = results + self.is_fitted_ = True + return results + + +def _fit_ols( + self, + sample: pd.DataFrame, + outcome: str, + unit: str, + time: str, + cohort: str, + X_design: np.ndarray, + col_names: List[str], + gt_keys: List[Tuple], + int_col_names: List[str], + groups: List[Any], + exovar, xtvar, xgvar, +) -> WooldridgeDiDResults: + """OLS path: within-transform FE, solve_ols, cluster SE.""" + n_int = len(int_col_names) # number of treatment interaction columns + + # 4. Within-transform: absorb unit + time FE + all_vars = [outcome] + [f"_x{i}" for i in range(X_design.shape[1])] + tmp = sample[[unit, time]].copy() + tmp[outcome] = sample[outcome].values + for i in range(X_design.shape[1]): + tmp[f"_x{i}"] = X_design[:, i] + + transformed = within_transform(tmp, all_vars, unit=unit, time=time, + suffix="_demeaned") + + y = transformed[f"{outcome}_demeaned"].values + X_cols = [f"_x{i}_demeaned" for i in range(X_design.shape[1])] + X = transformed[X_cols].values + + # 5. Cluster IDs (default: unit level) + cluster_col = self.cluster if self.cluster else unit + cluster_ids = sample[cluster_col].values + + # 6. Solve OLS + coefs, resids, vcov = solve_ols( + X, y, + cluster_ids=cluster_ids, + return_vcov=True, + rank_deficient_action=self.rank_deficient_action, + column_names=col_names, + ) + + # 7. Extract β_{g,t} and build gt_effects dict + gt_effects = {} + gt_weights = {} + for idx, (g, t) in enumerate(gt_keys): + if idx >= len(coefs): + break + att = float(coefs[idx]) + se = float(np.sqrt(vcov[idx, idx])) if vcov is not None else float("nan") + t_stat, p_value, conf_int = safe_inference(att, se, alpha=self.alpha) + gt_effects[(g, t)] = { + "att": att, "se": se, + "t_stat": t_stat, "p_value": p_value, "conf_int": conf_int, + } + gt_weights[(g, t)] = int(( + (sample[cohort] == g) & (sample[time] == t) + ).sum()) + + # Extract vcov submatrix for beta_{g,t} only + n_gt = len(gt_keys) + gt_vcov = vcov[:n_gt, :n_gt] if vcov is not None else None + gt_keys_ordered = list(gt_keys) + + # 8. Simple aggregation (always computed) + overall = _compute_weighted_agg(gt_effects, gt_weights, gt_keys_ordered, + gt_vcov, self.alpha) + + # Metadata + n_treated = int(sample[sample[cohort] > 0][unit].nunique()) + n_control = int(sample[sample[cohort] == 0][unit].nunique()) + all_times = sorted(sample[time].unique().tolist()) + + results = WooldridgeDiDResults( + group_time_effects=gt_effects, + overall_att=overall["att"], + overall_se=overall["se"], + overall_t_stat=overall["t_stat"], + overall_p_value=overall["p_value"], + overall_conf_int=overall["conf_int"], + method=self.method, + control_group=self.control_group, + groups=groups, + time_periods=all_times, + n_obs=len(sample), + n_treated_units=n_treated, + n_control_units=n_control, + alpha=self.alpha, + _gt_weights=gt_weights, + _gt_vcov=gt_vcov, + _gt_keys=gt_keys_ordered, + ) + return results + + +def _compute_weighted_agg( + gt_effects: Dict, + gt_weights: Dict, + gt_keys: List, + gt_vcov: Optional[np.ndarray], + alpha: float, +) -> Dict: + """Compute simple (overall) weighted average ATT and SE via delta method.""" + post_keys = [(g, t) for (g, t) in gt_keys if t >= g] + w_total = sum(gt_weights.get(k, 0) for k in post_keys) + if w_total == 0: + att = float("nan") + se = float("nan") + else: + att = sum(gt_weights.get(k, 0) * gt_effects[k]["att"] + for k in post_keys if k in gt_effects) / w_total + if gt_vcov is not None: + w_vec = np.array([ + gt_weights.get(k, 0) / w_total if k in post_keys else 0.0 + for k in gt_keys + ]) + var = float(w_vec @ gt_vcov @ w_vec) + se = float(np.sqrt(max(var, 0.0))) + else: + se = float("nan") + + t_stat, p_value, conf_int = safe_inference(att, se, alpha=alpha) + return {"att": att, "se": se, "t_stat": t_stat, + "p_value": p_value, "conf_int": conf_int} +``` + +Note: add `_fit_logit` and `_fit_poisson` stubs that raise `NotImplementedError` +(will be implemented in Task 7 & 8). + +- [ ] **Step 4: Run tests to verify they pass** + +```bash +pytest tests/test_wooldridge.py::TestWooldridgeDiDFitOLS -v +``` + +Expected: all 7 tests PASS + +- [ ] **Step 5: Commit** + +```bash +git add diff_diff/wooldridge.py tests/test_wooldridge.py +git commit -m "feat: implement WooldridgeDiD.fit() OLS path with ETWFE saturated regression" +``` + +--- + +## Task 6: Aggregation and output methods + +**Files:** +- Test: `tests/test_wooldridge.py` (add aggregation correctness tests) + +- [ ] **Step 1: Write the failing tests** + +Add to `tests/test_wooldridge.py`: + +```python +class TestAggregations: + @pytest.fixture + def fitted(self): + from diff_diff.datasets import load_mpdta + df = load_mpdta() + est = WooldridgeDiD() + return est.fit(df, outcome="lemp", unit="countyreal", + time="year", cohort="first.treat") + + def test_simple_matches_manual_weighted_average(self, fitted): + """simple ATT must equal manually computed weighted average of ATT(g,t).""" + gt = fitted.group_time_effects + w = fitted._gt_weights + post_keys = [(g, t) for (g, t) in w if t >= g] + w_total = sum(w[k] for k in post_keys) + manual_att = sum(w[k] * gt[k]["att"] for k in post_keys) / w_total + assert abs(fitted.overall_att - manual_att) < 1e-10 + + def test_aggregate_group_keys_match_cohorts(self, fitted): + fitted.aggregate("group") + assert set(fitted.group_effects.keys()) == set(fitted.groups) + + def test_aggregate_event_relative_periods(self, fitted): + fitted.aggregate("event") + for k in fitted.event_study_effects: + assert isinstance(k, (int, np.integer)) + + def test_aggregate_calendar_finite(self, fitted): + fitted.aggregate("calendar") + for t, eff in fitted.calendar_effects.items(): + assert np.isfinite(eff["att"]) + + def test_summary_runs(self, fitted): + s = fitted.summary("simple") + assert "ETWFE" in s or "Wooldridge" in s + + def test_to_dataframe_event(self, fitted): + fitted.aggregate("event") + df = fitted.to_dataframe("event") + assert "relative_period" in df.columns + assert "att" in df.columns + + def test_to_dataframe_gt(self, fitted): + df = fitted.to_dataframe("gt") + assert "cohort" in df.columns + assert "time" in df.columns + assert len(df) == len(fitted.group_time_effects) +``` + +- [ ] **Step 2: Run tests to verify they pass (most should already pass)** + +```bash +pytest tests/test_wooldridge.py::TestAggregations -v +``` + +Expected: all 7 tests PASS (aggregation logic is in `WooldridgeDiDResults`) + +- [ ] **Step 3: Commit if any fixes needed** + +If any tests reveal bugs in the aggregation code, fix and then: + +```bash +git add diff_diff/wooldridge_results.py diff_diff/wooldridge.py tests/test_wooldridge.py +git commit -m "fix: aggregation correctness and output method alignment" +``` + +--- + +## Task 7: Nonlinear fit — logit path + +**Files:** +- Modify: `diff_diff/wooldridge.py` (implement `_fit_logit`) +- Test: `tests/test_wooldridge.py` (add logit tests) + +- [ ] **Step 1: Write the failing tests** + +Add to `tests/test_wooldridge.py`: + +```python +class TestWooldridgeDiDLogit: + @pytest.fixture + def binary_panel(self): + """Simulated binary outcome panel with known positive ATT.""" + rng = np.random.default_rng(42) + n_units, n_periods = 60, 5 + rows = [] + for u in range(n_units): + cohort = 3 if u < 30 else 0 + for t in range(1, n_periods + 1): + treated = int(cohort > 0 and t >= cohort) + eta = -0.5 + 1.0 * treated + 0.1 * rng.standard_normal() + y = int(rng.random() < 1 / (1 + np.exp(-eta))) + rows.append({"unit": u, "time": t, "cohort": cohort, "y": y}) + return pd.DataFrame(rows) + + def test_logit_fit_runs(self, binary_panel): + est = WooldridgeDiD(method="logit") + r = est.fit(binary_panel, outcome="y", unit="unit", + time="time", cohort="cohort") + assert isinstance(r, WooldridgeDiDResults) + + def test_logit_att_sign(self, binary_panel): + """ATT should be positive (treatment increases binary outcome).""" + est = WooldridgeDiD(method="logit") + r = est.fit(binary_panel, outcome="y", unit="unit", + time="time", cohort="cohort") + assert r.overall_att > 0 + + def test_logit_se_positive(self, binary_panel): + est = WooldridgeDiD(method="logit") + r = est.fit(binary_panel, outcome="y", unit="unit", + time="time", cohort="cohort") + assert r.overall_se > 0 + + def test_logit_method_stored(self, binary_panel): + est = WooldridgeDiD(method="logit") + r = est.fit(binary_panel, outcome="y", unit="unit", + time="time", cohort="cohort") + assert r.method == "logit" +``` + +- [ ] **Step 2: Run tests to verify they fail** + +```bash +pytest tests/test_wooldridge.py::TestWooldridgeDiDLogit -v +``` + +Expected: `NotImplementedError` from `_fit_logit` stub + +- [ ] **Step 3: Implement `_fit_logit`** + +Add to `diff_diff/wooldridge.py` as a method of `WooldridgeDiD`: + +```python +def _fit_logit( + self, + sample: pd.DataFrame, + outcome: str, + unit: str, + time: str, + cohort: str, + X_int: np.ndarray, + col_names: List[str], + gt_keys: List[Tuple], + int_col_names: List[str], + groups: List[Any], +) -> WooldridgeDiDResults: + """Logit path: cohort×period group FE + solve_logit + ASF ATT.""" + n_int = len(int_col_names) + + # Build cohort×period group FE dummies (drop one to avoid collinearity + # with solve_logit's internal intercept) + grp_label = ( + sample[cohort].astype(str) + "_" + sample[time].astype(str) + ) + group_dummies = pd.get_dummies(grp_label, drop_first=True).values.astype(float) + + # Design matrix: treatment interactions + group FE dummies + X_full = np.hstack([X_int, group_dummies]) + + y = sample[outcome].values.astype(float) + cluster_col = self.cluster if self.cluster else unit + cluster_ids = sample[cluster_col].values + + beta, probs = solve_logit( + X_full, y, + rank_deficient_action=self.rank_deficient_action, + ) + # solve_logit prepends intercept — beta[0] is intercept, beta[1:] are X_full cols + beta_int_cols = beta[1: n_int + 1] # treatment interaction coefficients + + # Sandwich vcov for X_full (excluding intercept position 0) + resids = y - probs + X_with_intercept = np.column_stack([np.ones(len(y)), X_full]) + vcov_full = compute_robust_vcov( + X_with_intercept, resids, cluster_ids=cluster_ids, + weights=probs * (1 - probs), # logit variance weights + ) + # Submatrix for treatment interactions (skip intercept col 0) + vcov_int = vcov_full[1: n_int + 1, 1: n_int + 1] + + # ASF ATT(g,t) for treated units in each cell + gt_effects = {} + gt_weights = {} + for idx, (g, t) in enumerate(gt_keys): + if idx >= n_int: + break + cell_mask = (sample[cohort] == g) & (sample[time] == t) + if cell_mask.sum() == 0: + continue + # Baseline linear index for treated units in this cell + eta_base = X_with_intercept[cell_mask] @ beta # includes intercept + att = float(np.mean( + _logistic(eta_base + beta_int_cols[idx]) - _logistic(eta_base) + )) + # Delta method: full gradient over all K parameters (including intercept) + d_delta = np.mean( + _logistic_deriv(eta_base + beta_int_cols[idx]) + ) + d_base = X_with_intercept[cell_mask] * ( + _logistic_deriv(eta_base + beta_int_cols[idx]) - + _logistic_deriv(eta_base) + )[:, None] + grad = np.zeros(len(beta)) + grad[1 + idx] = d_delta + grad[1: n_int + 1] += np.zeros(n_int) # other deltas don't contribute + # base coefficient gradients + grad += np.mean(d_base, axis=0) + se = float(np.sqrt(max(grad @ vcov_full @ grad, 0.0))) + t_stat, p_value, conf_int = safe_inference(att, se, alpha=self.alpha) + gt_effects[(g, t)] = { + "att": att, "se": se, + "t_stat": t_stat, "p_value": p_value, "conf_int": conf_int, + } + gt_weights[(g, t)] = int(cell_mask.sum()) + + gt_keys_ordered = [k for k in gt_keys if k in gt_effects] + gt_vcov = None # full delta method used per-cell; aggregation uses None fallback + + overall = _compute_weighted_agg(gt_effects, gt_weights, gt_keys_ordered, + gt_vcov, self.alpha) + + return WooldridgeDiDResults( + group_time_effects=gt_effects, + overall_att=overall["att"], + overall_se=overall["se"], + overall_t_stat=overall["t_stat"], + overall_p_value=overall["p_value"], + overall_conf_int=overall["conf_int"], + method=self.method, + control_group=self.control_group, + groups=groups, + time_periods=sorted(sample[time].unique().tolist()), + n_obs=len(sample), + n_treated_units=int(sample[sample[cohort] > 0][unit].nunique()), + n_control_units=int(sample[sample[cohort] == 0][unit].nunique()), + alpha=self.alpha, + _gt_weights=gt_weights, + _gt_vcov=gt_vcov, + _gt_keys=gt_keys_ordered, + ) +``` + +Add helper functions at module level (before the class): + +```python +def _logistic(x: np.ndarray) -> np.ndarray: + return 1.0 / (1.0 + np.exp(-x)) + + +def _logistic_deriv(x: np.ndarray) -> np.ndarray: + p = _logistic(x) + return p * (1.0 - p) +``` + +- [ ] **Step 4: Run tests** + +```bash +pytest tests/test_wooldridge.py::TestWooldridgeDiDLogit -v +``` + +Expected: all 4 tests PASS + +- [ ] **Step 5: Commit** + +```bash +git add diff_diff/wooldridge.py tests/test_wooldridge.py +git commit -m "feat: implement WooldridgeDiD logit path with ASF ATT and delta-method SEs" +``` + +--- + +## Task 8: Nonlinear fit — Poisson path + +**Files:** +- Modify: `diff_diff/wooldridge.py` (implement `_fit_poisson`) +- Test: `tests/test_wooldridge.py` (add Poisson tests) + +- [ ] **Step 1: Write the failing tests** + +Add to `tests/test_wooldridge.py`: + +```python +class TestWooldridgeDiDPoisson: + @pytest.fixture + def count_panel(self): + rng = np.random.default_rng(7) + n_units, n_periods = 60, 5 + rows = [] + for u in range(n_units): + cohort = 3 if u < 30 else 0 + for t in range(1, n_periods + 1): + treated = int(cohort > 0 and t >= cohort) + mu = np.exp(0.5 + 0.8 * treated + 0.1 * rng.standard_normal()) + y = rng.poisson(mu) + rows.append({"unit": u, "time": t, "cohort": cohort, "y": float(y)}) + return pd.DataFrame(rows) + + def test_poisson_fit_runs(self, count_panel): + est = WooldridgeDiD(method="poisson") + r = est.fit(count_panel, outcome="y", unit="unit", + time="time", cohort="cohort") + assert isinstance(r, WooldridgeDiDResults) + + def test_poisson_att_sign(self, count_panel): + est = WooldridgeDiD(method="poisson") + r = est.fit(count_panel, outcome="y", unit="unit", + time="time", cohort="cohort") + assert r.overall_att > 0 + + def test_poisson_se_positive(self, count_panel): + est = WooldridgeDiD(method="poisson") + r = est.fit(count_panel, outcome="y", unit="unit", + time="time", cohort="cohort") + assert r.overall_se > 0 +``` + +- [ ] **Step 2: Run tests to verify they fail** + +```bash +pytest tests/test_wooldridge.py::TestWooldridgeDiDPoisson -v +``` + +Expected: `NotImplementedError` + +- [ ] **Step 3: Implement `_fit_poisson`** + +Add to `WooldridgeDiD` (mirrors `_fit_logit` but uses `solve_poisson` and exp link): + +```python +def _fit_poisson( + self, + sample: pd.DataFrame, + outcome: str, + unit: str, + time: str, + cohort: str, + X_int: np.ndarray, + col_names: List[str], + gt_keys: List[Tuple], + int_col_names: List[str], + groups: List[Any], +) -> WooldridgeDiDResults: + """Poisson path: cohort×period group FE + solve_poisson + ASF ATT.""" + n_int = len(int_col_names) + + # Group FE dummies (drop one reference category) + grp_label = ( + sample[cohort].astype(str) + "_" + sample[time].astype(str) + ) + group_dummies = pd.get_dummies(grp_label, drop_first=True).values.astype(float) + + # Design matrix: group FE dummies + treatment interactions + # Poisson solver does NOT prepend intercept; include group FE as baseline + X_full = np.hstack([group_dummies, X_int]) + n_fe = group_dummies.shape[1] + + y = sample[outcome].values.astype(float) + cluster_col = self.cluster if self.cluster else unit + cluster_ids = sample[cluster_col].values + + beta, mu_hat = solve_poisson(X_full, y) + + # Sandwich vcov: (X'WX)^{-1} (X'diag(resid^2)X) (X'WX)^{-1} + resids = y - mu_hat + W = mu_hat # Poisson variance = mean + XtWX = X_full.T @ (W[:, None] * X_full) + try: + XtWX_inv = np.linalg.inv(XtWX) + except np.linalg.LinAlgError: + XtWX_inv = np.full_like(XtWX, float("nan")) + + # Cluster-robust meat + if cluster_ids is not None: + clusters = np.unique(cluster_ids) + meat = np.zeros_like(XtWX) + for c in clusters: + mask = cluster_ids == c + scores_c = (X_full[mask] * resids[mask, None]).sum(axis=0) + meat += np.outer(scores_c, scores_c) + else: + scores = X_full * resids[:, None] + meat = scores.T @ scores + + vcov_full = XtWX_inv @ meat @ XtWX_inv + + # Interaction columns start at column n_fe in X_full + beta_int = beta[n_fe: n_fe + n_int] + vcov_int = vcov_full[n_fe: n_fe + n_int, n_fe: n_fe + n_int] + + # ASF ATT(g,t): E[exp(η + δ) - exp(η)] for treated units in cell + gt_effects = {} + gt_weights = {} + for idx, (g, t) in enumerate(gt_keys): + if idx >= n_int: + break + cell_mask = (sample[cohort] == g) & (sample[time] == t) + if cell_mask.sum() == 0: + continue + eta_base = X_full[cell_mask] @ beta + delta = beta_int[idx] + att = float(np.mean(np.exp(eta_base + delta) - np.exp(eta_base))) + # Delta method gradient + grad_delta = float(np.mean(np.exp(eta_base + delta))) + grad_base = np.mean( + X_full[cell_mask] * ( + np.exp(eta_base + delta) - np.exp(eta_base) + )[:, None], + axis=0, + ) + grad = np.zeros(len(beta)) + grad[n_fe + idx] = grad_delta + grad += grad_base + se = float(np.sqrt(max(grad @ vcov_full @ grad, 0.0))) + t_stat, p_value, conf_int = safe_inference(att, se, alpha=self.alpha) + gt_effects[(g, t)] = { + "att": att, "se": se, + "t_stat": t_stat, "p_value": p_value, "conf_int": conf_int, + } + gt_weights[(g, t)] = int(cell_mask.sum()) + + gt_keys_ordered = [k for k in gt_keys if k in gt_effects] + overall = _compute_weighted_agg(gt_effects, gt_weights, gt_keys_ordered, + None, self.alpha) + + return WooldridgeDiDResults( + group_time_effects=gt_effects, + overall_att=overall["att"], + overall_se=overall["se"], + overall_t_stat=overall["t_stat"], + overall_p_value=overall["p_value"], + overall_conf_int=overall["conf_int"], + method=self.method, + control_group=self.control_group, + groups=groups, + time_periods=sorted(sample[time].unique().tolist()), + n_obs=len(sample), + n_treated_units=int(sample[sample[cohort] > 0][unit].nunique()), + n_control_units=int(sample[sample[cohort] == 0][unit].nunique()), + alpha=self.alpha, + _gt_weights=gt_weights, + _gt_vcov=None, + _gt_keys=gt_keys_ordered, + ) +``` + +- [ ] **Step 4: Run tests** + +```bash +pytest tests/test_wooldridge.py::TestWooldridgeDiDPoisson -v +``` + +Expected: all 3 tests PASS + +- [ ] **Step 5: Commit** + +```bash +git add diff_diff/wooldridge.py tests/test_wooldridge.py +git commit -m "feat: implement WooldridgeDiD Poisson path with ASF ATT" +``` + +--- + +## Task 9: Bootstrap support + +**Files:** +- Modify: `diff_diff/wooldridge.py` (add bootstrap to `_fit_ols`) +- Test: `tests/test_wooldridge.py` (bootstrap test, marked slow) + +- [ ] **Step 1: Write the failing test** + +Add to `tests/test_wooldridge.py`: + +```python +class TestBootstrap: + @pytest.mark.slow + def test_multiplier_bootstrap_ols(self, ci_params): + """Bootstrap SE should be close to analytic SE.""" + from diff_diff.datasets import load_mpdta + df = load_mpdta() + n_boot = ci_params.bootstrap(50, min_n=19) + est = WooldridgeDiD(n_bootstrap=n_boot, seed=42) + r = est.fit(df, outcome="lemp", unit="countyreal", + time="year", cohort="first.treat") + threshold = 0.40 if n_boot < 100 else 0.15 + assert abs(r.overall_se - r.overall_att) / max(abs(r.overall_att), 1e-8) < 10 + # Bootstrap SE should be in same ballpark as analytic SE + # (exact convergence tested with large n_boot) + + def test_bootstrap_zero_disables(self): + from diff_diff.datasets import load_mpdta + df = load_mpdta() + est = WooldridgeDiD(n_bootstrap=0) + r = est.fit(df, outcome="lemp", unit="countyreal", + time="year", cohort="first.treat") + assert np.isfinite(r.overall_se) +``` + +- [ ] **Step 2: Run non-slow tests to verify they pass** + +```bash +pytest tests/test_wooldridge.py::TestBootstrap::test_bootstrap_zero_disables -v +``` + +Expected: PASS (bootstrap=0 path already works) + +- [ ] **Step 3: Implement multiplier bootstrap in `_fit_ols`** + +After the OLS solve in `_fit_ols`, add bootstrap block: + +```python +if self.n_bootstrap > 0: + rng = np.random.default_rng(self.seed) + units_arr = sample[unit].values + unique_units = np.unique(units_arr) + n_clusters = len(unique_units) + boot_atts = [] + for _ in range(self.n_bootstrap): + if self.bootstrap_weights == "rademacher": + unit_weights = rng.choice([-1.0, 1.0], size=n_clusters) + elif self.bootstrap_weights == "webb": + unit_weights = rng.choice( + [-np.sqrt(1.5), -1.0, -np.sqrt(0.5), + np.sqrt(0.5), 1.0, np.sqrt(1.5)], + size=n_clusters, + ) + else: # mammen + phi = (1 + np.sqrt(5)) / 2 + unit_weights = rng.choice( + [-(phi - 1), phi], + p=[phi / np.sqrt(5), (phi - 1) / np.sqrt(5)], + size=n_clusters, + ) + obs_weights = unit_weights[ + np.searchsorted(unique_units, units_arr) + ] + y_boot = y + obs_weights * resids # multiplier perturbation + coefs_b, _, _ = solve_ols( + X, y_boot, + cluster_ids=cluster_ids, + return_vcov=True, + rank_deficient_action="silent", + ) + w_total = sum(gt_weights.get(k, 0) for k in post_keys) + if w_total > 0: + att_b = sum( + gt_weights.get(k, 0) * float(coefs_b[i]) + for i, k in enumerate(gt_keys) if k in post_keys + ) / w_total + boot_atts.append(att_b) + if boot_atts: + # Override SE with bootstrap SE + boot_se = float(np.std(boot_atts, ddof=1)) + overall_att = overall["att"] + t_stat_b, p_b, ci_b = safe_inference(overall_att, boot_se, alpha=self.alpha) + results.overall_se = boot_se + results.overall_t_stat = t_stat_b + results.overall_p_value = p_b + results.overall_conf_int = ci_b +``` + +- [ ] **Step 4: Run all non-slow tests** + +```bash +pytest tests/test_wooldridge.py -v -m "not slow" +``` + +Expected: all PASS + +- [ ] **Step 5: Commit** + +```bash +git add diff_diff/wooldridge.py tests/test_wooldridge.py +git commit -m "feat: add multiplier bootstrap to WooldridgeDiD OLS path" +``` + +--- + +## Task 10: Methodology correctness test (CS equivalence) + +**Files:** +- Test: `tests/test_wooldridge.py` (add parity test) + +- [ ] **Step 1: Write the failing test** + +Add to `tests/test_wooldridge.py`: + +```python +class TestMethodologyCorrectness: + def test_ols_att_sign_direction(self): + """ATT sign should be consistent across cohorts on mpdta.""" + from diff_diff.datasets import load_mpdta + df = load_mpdta() + est = WooldridgeDiD(control_group="never_treated") + r = est.fit(df, outcome="lemp", unit="countyreal", + time="year", cohort="first.treat") + # mpdta ATT is expected to be negative (employment effect of min wage) + # This is a directional check, not exact + assert np.isfinite(r.overall_att) + + def test_never_treated_pre_periods_estimable(self): + """With never_treated control, k < 0 event periods should appear.""" + from diff_diff.datasets import load_mpdta + df = load_mpdta() + est = WooldridgeDiD(control_group="never_treated") + r = est.fit(df, outcome="lemp", unit="countyreal", + time="year", cohort="first.treat") + r.aggregate("event") + relative_periods = list(r.event_study_effects.keys()) + assert any(k < 0 for k in relative_periods), ( + "Expected pre-treatment periods with never_treated control" + ) + + def test_single_cohort_degenerates_to_simple_did(self): + """With one cohort, ETWFE should collapse to a standard DiD.""" + rng = np.random.default_rng(0) + n = 100 + rows = [] + for u in range(n): + cohort = 2 if u < 50 else 0 + for t in [1, 2]: + treated = int(cohort > 0 and t >= cohort) + y = 1.0 * treated + rng.standard_normal() + rows.append({"unit": u, "time": t, "cohort": cohort, "y": y}) + df = pd.DataFrame(rows) + r = WooldridgeDiD().fit(df, outcome="y", unit="unit", + time="time", cohort="cohort") + # One cohort, one post period → one ATT(g=2, t=2) + assert len(r.group_time_effects) == 1 + assert abs(r.overall_att - 1.0) < 0.5 # close to true ATT=1 + + def test_aggregation_weights_sum_to_one(self): + """Simple aggregation weights should sum to 1.""" + from diff_diff.datasets import load_mpdta + df = load_mpdta() + r = WooldridgeDiD().fit(df, outcome="lemp", unit="countyreal", + time="year", cohort="first.treat") + w = r._gt_weights + post_keys = [(g, t) for (g, t) in w if t >= g] + w_total = sum(w[k] for k in post_keys) + norm_weights = [w[k] / w_total for k in post_keys] + assert abs(sum(norm_weights) - 1.0) < 1e-10 +``` + +- [ ] **Step 2: Run tests** + +```bash +pytest tests/test_wooldridge.py::TestMethodologyCorrectness -v +``` + +Expected: all 4 tests PASS + +- [ ] **Step 3: Commit** + +```bash +git add tests/test_wooldridge.py +git commit -m "test: add methodology correctness tests for WooldridgeDiD" +``` + +--- + +## Task 11: Exports, `__init__.py`, and REGISTRY.md + +**Files:** +- Modify: `diff_diff/__init__.py` +- Modify: `docs/methodology/REGISTRY.md` + +- [ ] **Step 1: Write the failing import test** + +Add to `tests/test_wooldridge.py`: + +```python +class TestExports: + def test_top_level_import(self): + from diff_diff import WooldridgeDiD, WooldridgeDiDResults, ETWFE + assert ETWFE is WooldridgeDiD + + def test_alias_etwfe(self): + import diff_diff + assert hasattr(diff_diff, "ETWFE") + assert diff_diff.ETWFE is diff_diff.WooldridgeDiD +``` + +- [ ] **Step 2: Run to verify failure** + +```bash +pytest tests/test_wooldridge.py::TestExports -v +``` + +Expected: `ImportError: cannot import name 'WooldridgeDiD' from 'diff_diff'` + +- [ ] **Step 3: Update `diff_diff/__init__.py`** + +Find the block where other staggered estimators are imported (e.g., near the +`CallawaySantAnna` imports) and add: + +```python +from diff_diff.wooldridge import WooldridgeDiD +from diff_diff.wooldridge_results import WooldridgeDiDResults + +ETWFE = WooldridgeDiD +``` + +Also add to `__all__`: + +```python +"WooldridgeDiD", +"WooldridgeDiDResults", +"ETWFE", +``` + +- [ ] **Step 4: Run export tests** + +```bash +pytest tests/test_wooldridge.py::TestExports -v +``` + +Expected: PASS + +- [ ] **Step 5: Add REGISTRY.md section** + +Open `docs/methodology/REGISTRY.md` and add a new section following the existing +estimator format. Place it after the "StackedDiD" section: + +```markdown +## WooldridgeDiD / ETWFE + +**Primary sources:** +- Wooldridge (2021). "Two-Way Fixed Effects, the Two-Way Mundlak Regression, and + Difference-in-Differences Estimators." SSRN 3906345. +- Wooldridge (2023). "Simple approaches to nonlinear difference-in-differences with + panel data." *The Econometrics Journal*, 26(3), C31–C66. +- Friosavila (2021). `jwdid`: Stata module. SSC s459114. + +**Estimator equation (linear):** + +``` +Y_it = α_i + λ_t + Σ_{g,t: t≥g-a} β_{g,t} · 1(G_i=g) · 1(T=t) + X_it'γ + ε_it +``` + +Where `a` is the anticipation window. Unit and time FE are absorbed via +within-transformation. + +**ATT(g,t) = β_{g,t}** (directly from the regression). + +**Nonlinear models:** For `method="logit"` or `"poisson"`, ATT(g,t) is computed via the +Average Structural Function (ASF): +``` +ATT(g,t) = mean[ g(η_i + δ_{g,t}) - g(η_i) ] over treated units in (g,t) +``` +where `g(·)` is logistic or exp. SEs via full delta method. + +**Standard errors:** +- Default: cluster-robust at `unit` level (matches `jwdid` default `vce(cluster ivar)`) +- Optional: multiplier bootstrap (all methods); wild cluster bootstrap (OLS only) + +**Aggregations (corresponding to `jwdid_estat`):** +- `simple`: overall weighted ATT +- `group`: by treatment cohort +- `calendar`: by calendar period +- `event`: by relative period (event study) + +**Covariate types (corresponding to `jwdid` options):** +- `exovar`: time-invariant; no demeaning (→ `exovar()`) +- `xtvar`: time-varying; demeaned within cohort×period (→ `xtvar()`); raw when + `demean_covariates=False` (→ `xasis`) +- `xgvar`: interacted with cohort indicators (→ `xgvar()`) + +**Nonlinear FE:** Both logit and Poisson use **cohort×period group fixed effects** +(not individual FE) to avoid the incidental parameters problem (Wooldridge 2023). +One dummy category is dropped to avoid collinearity with the implicit constant in +`solve_logit` / intercept column in `solve_poisson`. + +**Edge cases:** +- Single cohort: reduces to standard DiD (one β_{g,t} per post period). +- `never_treated` control: pre-treatment cells (k < 0) are estimable. +- `not_yet_treated` control: pre-treatment cells excluded from model by design. +- `anticipation > 0`: treatment cells shifted left by `anticipation` periods. + +**Reference implementation:** Stata `jwdid` (Friosavila 2021, SSC s459114). + +- **Note:** nonlinear bootstrap uses multiplier bootstrap; jwdid uses delta method. +- **Note:** nonlinear aggregation SEs fall back to NaN when full β vcov unavailable + across cells (delta-method is computed per-cell only). +``` + +- [ ] **Step 6: Run full test suite** + +```bash +pytest tests/test_wooldridge.py -v -m "not slow" +``` + +Expected: all tests PASS + +- [ ] **Step 7: Commit** + +```bash +git add diff_diff/__init__.py docs/methodology/REGISTRY.md tests/test_wooldridge.py +git commit -m "feat: export WooldridgeDiD and ETWFE alias; add REGISTRY.md entry" +``` + +--- + +## Task 12: Final integration and cleanup + +**Files:** +- Run: full test suite to verify no regressions + +- [ ] **Step 1: Run full project test suite** + +```bash +pytest --tb=short -q +``` + +Expected: no new failures vs. baseline (existing tests unaffected) + +- [ ] **Step 2: Verify linting** + +```bash +ruff check diff_diff/wooldridge.py diff_diff/wooldridge_results.py diff_diff/linalg.py +black --check diff_diff/wooldridge.py diff_diff/wooldridge_results.py +``` + +Fix any issues, then re-run. + +- [ ] **Step 3: Smoke-test example** + +```python +from diff_diff import WooldridgeDiD, ETWFE +from diff_diff.datasets import load_mpdta + +df = load_mpdta() +r = WooldridgeDiD().fit(df, outcome="lemp", unit="countyreal", + time="year", cohort="first.treat") +r.aggregate("event") +print(r) +print(r.summary("event")) +``` + +Expected: prints results without error. + +- [ ] **Step 4: Final commit** + +```bash +git add -u +git commit -m "feat: complete WooldridgeDiD (ETWFE) estimator implementation" +``` From b3ab7d9de6b3c612ef447a73c2cfc884b68345e0 Mon Sep 17 00:00:00 2001 From: wenddymacro <50739376+wenddymacro@users.noreply.github.com> Date: Thu, 19 Mar 2026 14:15:53 +0800 Subject: [PATCH 04/16] chore: ignore .worktrees/ directory --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 1e39833d..e5d78389 100644 --- a/.gitignore +++ b/.gitignore @@ -90,3 +90,4 @@ papers/ # Local analysis notebooks (not committed) analysis/ +.worktrees/ From b2b6217354d212fc5c7cd5842ef7b480d9c20af9 Mon Sep 17 00:00:00 2001 From: wenddymacro <50739376+wenddymacro@users.noreply.github.com> Date: Thu, 19 Mar 2026 14:44:43 +0800 Subject: [PATCH 05/16] feat(linalg): add solve_poisson IRLS solver for Wooldridge nonlinear ETWFE --- diff_diff/linalg.py | 50 ++++++++++++++++++++++++++++++++++++++++++++ tests/test_linalg.py | 45 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+) diff --git a/diff_diff/linalg.py b/diff_diff/linalg.py index 56a2052e..b553dcf3 100644 --- a/diff_diff/linalg.py +++ b/diff_diff/linalg.py @@ -1725,3 +1725,53 @@ def _compute_confidence_interval( upper = estimate + critical_value * se return (lower, upper) + + +def solve_poisson( + X: np.ndarray, + y: np.ndarray, + max_iter: int = 25, + tol: float = 1e-8, +) -> Tuple[np.ndarray, np.ndarray]: + """Poisson IRLS (Newton-Raphson with log link). + + Does NOT prepend an intercept — caller must include one if needed. + Returns (beta, W_final) where W_final = mu_hat (used for sandwich vcov). + + Parameters + ---------- + X : (n, k) design matrix (caller provides intercept / group FE dummies) + y : (n,) non-negative count outcomes + max_iter : maximum IRLS iterations + tol : convergence threshold on sup-norm of coefficient change + + Returns + ------- + beta : (k,) coefficient vector + W : (n,) final fitted means mu_hat (weights for sandwich vcov) + """ + n, k = X.shape + beta = np.zeros(k) + for _ in range(max_iter): + eta = X @ beta + mu = np.clip(np.exp(eta), 1e-10, None) # clip prevents log(0) + score = X.T @ (y - mu) # gradient of log-likelihood + hess = X.T @ (mu[:, None] * X) # -Hessian = X'WX, W=diag(mu) + try: + delta = np.linalg.solve(hess, score) + except np.linalg.LinAlgError: + break + beta_new = beta + delta + if np.max(np.abs(beta_new - beta)) < tol: + beta = beta_new + mu = np.clip(np.exp(X @ beta), 1e-10, None) + break + beta = beta_new + else: + warnings.warn( + "solve_poisson did not converge in {} iterations".format(max_iter), + RuntimeWarning, + stacklevel=2, + ) + mu_final = np.clip(np.exp(X @ beta), 1e-10, None) + return beta, mu_final diff --git a/tests/test_linalg.py b/tests/test_linalg.py index db980056..219ee032 100644 --- a/tests/test_linalg.py +++ b/tests/test_linalg.py @@ -10,6 +10,7 @@ compute_r_squared, compute_robust_vcov, solve_ols, + solve_poisson, ) @@ -1699,3 +1700,47 @@ def test_solve_ols_no_runtime_warnings(self): f"{[str(x.message) for x in runtime_warnings]}" ) assert np.allclose(coefficients, beta_true, atol=0.1) + + +class TestSolvePoisson: + def test_basic_convergence(self): + """solve_poisson converges on simple count data.""" + rng = np.random.default_rng(42) + n = 200 + X = np.column_stack([np.ones(n), rng.standard_normal((n, 2))]) + true_beta = np.array([0.5, 0.3, -0.2]) + mu = np.exp(X @ true_beta) + y = rng.poisson(mu).astype(float) + beta, W = solve_poisson(X, y) + assert beta.shape == (3,) + assert W.shape == (n,) + assert np.allclose(beta, true_beta, atol=0.15) + + def test_returns_weights(self): + """solve_poisson returns final mu weights for vcov computation.""" + rng = np.random.default_rng(0) + n = 100 + X = np.column_stack([np.ones(n), rng.standard_normal(n)]) + y = rng.poisson(2.0, size=n).astype(float) + beta, W = solve_poisson(X, y) + assert (W > 0).all() + + def test_non_negative_output(self): + """Fitted mu = exp(Xb) should be strictly positive.""" + rng = np.random.default_rng(1) + n = 50 + X = np.column_stack([np.ones(n), rng.standard_normal(n)]) + y = rng.poisson(1.0, size=n).astype(float) + beta, W = solve_poisson(X, y) + mu_hat = np.exp(X @ beta) + assert (mu_hat > 0).all() + + def test_no_intercept_prepended(self): + """solve_poisson does NOT add intercept (caller's responsibility).""" + rng = np.random.default_rng(2) + n = 80 + # X already has intercept — verify coefficient count matches columns + X = np.column_stack([np.ones(n), rng.standard_normal(n)]) + y = rng.poisson(1.5, size=n).astype(float) + beta, _ = solve_poisson(X, y) + assert len(beta) == 2 # not 3 From c295fbc097a629bc2ee112b3f2ce473376ddd60e Mon Sep 17 00:00:00 2001 From: wenddymacro <50739376+wenddymacro@users.noreply.github.com> Date: Thu, 19 Mar 2026 14:48:05 +0800 Subject: [PATCH 06/16] feat: add WooldridgeDiDResults dataclass with four aggregation types --- diff_diff/wooldridge_results.py | 289 ++++++++++++++++++++++++++++++++ tests/test_wooldridge.py | 86 ++++++++++ 2 files changed, 375 insertions(+) create mode 100644 diff_diff/wooldridge_results.py create mode 100644 tests/test_wooldridge.py diff --git a/diff_diff/wooldridge_results.py b/diff_diff/wooldridge_results.py new file mode 100644 index 00000000..09bc9bc7 --- /dev/null +++ b/diff_diff/wooldridge_results.py @@ -0,0 +1,289 @@ +"""Results class for WooldridgeDiD (ETWFE) estimator.""" +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any, Dict, List, Optional, Tuple + +import numpy as np +import pandas as pd + +from diff_diff.utils import safe_inference + + +@dataclass +class WooldridgeDiDResults: + """Results from WooldridgeDiD.fit(). + + Core output is ``group_time_effects``: a dict keyed by (cohort_g, time_t) + with per-cell ATT estimates and inference. Call ``.aggregate(type)`` to + compute any of the four jwdid_estat aggregation types. + """ + + # ------------------------------------------------------------------ # + # Core cohort×time estimates # + # ------------------------------------------------------------------ # + group_time_effects: Dict[Tuple[Any, Any], Dict[str, Any]] + """key=(g,t), value={att, se, t_stat, p_value, conf_int}""" + + # ------------------------------------------------------------------ # + # Simple (overall) aggregation — always populated at fit time # + # ------------------------------------------------------------------ # + overall_att: float + overall_se: float + overall_t_stat: float + overall_p_value: float + overall_conf_int: Tuple[float, float] + + # ------------------------------------------------------------------ # + # Other aggregations — populated by .aggregate() # + # ------------------------------------------------------------------ # + group_effects: Optional[Dict[Any, Dict]] = field(default=None, repr=False) + calendar_effects: Optional[Dict[Any, Dict]] = field(default=None, repr=False) + event_study_effects: Optional[Dict[int, Dict]] = field(default=None, repr=False) + + # ------------------------------------------------------------------ # + # Metadata # + # ------------------------------------------------------------------ # + method: str = "ols" + control_group: str = "not_yet_treated" + groups: List[Any] = field(default_factory=list) + time_periods: List[Any] = field(default_factory=list) + n_obs: int = 0 + n_treated_units: int = 0 + n_control_units: int = 0 + alpha: float = 0.05 + + # ------------------------------------------------------------------ # + # Internal — used by aggregate() for delta-method SEs # + # ------------------------------------------------------------------ # + _gt_weights: Dict[Tuple[Any, Any], int] = field(default_factory=dict, repr=False) + _gt_vcov: Optional[np.ndarray] = field(default=None, repr=False) + """Full vcov of all β_{g,t} coefficients (ordered same as sorted group_time_effects keys).""" + _gt_keys: List[Tuple[Any, Any]] = field(default_factory=list, repr=False) + """Ordered list of (g,t) keys corresponding to _gt_vcov columns.""" + + # ------------------------------------------------------------------ # + # Public methods # + # ------------------------------------------------------------------ # + + def aggregate(self, type: str) -> "WooldridgeDiDResults": # noqa: A002 + """Compute and store one of the four jwdid_estat aggregation types. + + Parameters + ---------- + type : "simple" | "group" | "calendar" | "event" + + Returns self for chaining. + """ + valid = ("simple", "group", "calendar", "event") + if type not in valid: + raise ValueError(f"type must be one of {valid}, got {type!r}") + + gt = self.group_time_effects + weights = self._gt_weights + vcov = self._gt_vcov + keys_ordered = self._gt_keys if self._gt_keys else sorted(gt.keys()) + + def _agg_se(w_vec: np.ndarray) -> float: + """Delta-method SE for a linear combination w'β given full vcov.""" + if vcov is None or len(w_vec) != vcov.shape[0]: + return float("nan") + return float(np.sqrt(max(w_vec @ vcov @ w_vec, 0.0))) + + def _build_effect(att: float, se: float) -> Dict[str, Any]: + t_stat, p_value, conf_int = safe_inference(att, se, alpha=self.alpha) + return {"att": att, "se": se, "t_stat": t_stat, + "p_value": p_value, "conf_int": conf_int} + + if type == "simple": + # Re-compute overall using delta method (already stored in overall_* fields) + # This is a no-op but keeps the method callable. + pass + + elif type == "group": + result: Dict[Any, Dict] = {} + for g in self.groups: + cells = [(g2, t) for (g2, t) in keys_ordered if g2 == g and t >= g] + if not cells: + continue + w_total = sum(weights.get(c, 0) for c in cells) + if w_total == 0: + continue + att = sum(weights.get(c, 0) * gt[c]["att"] for c in cells) / w_total + # delta-method weights vector over all keys_ordered + w_vec = np.array([ + weights.get(c, 0) / w_total if c in cells else 0.0 + for c in keys_ordered + ]) + se = _agg_se(w_vec) + result[g] = _build_effect(att, se) + self.group_effects = result + + elif type == "calendar": + result = {} + for t in self.time_periods: + cells = [(g, t2) for (g, t2) in keys_ordered if t2 == t and t >= g] + if not cells: + continue + w_total = sum(weights.get(c, 0) for c in cells) + if w_total == 0: + continue + att = sum(weights.get(c, 0) * gt[c]["att"] for c in cells) / w_total + w_vec = np.array([ + weights.get(c, 0) / w_total if c in cells else 0.0 + for c in keys_ordered + ]) + se = _agg_se(w_vec) + result[t] = _build_effect(att, se) + self.calendar_effects = result + + elif type == "event": + all_k = sorted({t - g for (g, t) in keys_ordered}) + result = {} + for k in all_k: + cells = [(g, t) for (g, t) in keys_ordered if t - g == k] + if not cells: + continue + w_total = sum(weights.get(c, 0) for c in cells) + if w_total == 0: + continue + att = sum(weights.get(c, 0) * gt[c]["att"] for c in cells) / w_total + w_vec = np.array([ + weights.get(c, 0) / w_total if c in cells else 0.0 + for c in keys_ordered + ]) + se = _agg_se(w_vec) + result[k] = _build_effect(att, se) + self.event_study_effects = result + + return self + + def summary(self, aggregation: str = "simple") -> str: + """Print formatted summary table. + + Parameters + ---------- + aggregation : which aggregation to display ("simple", "group", "calendar", "event") + """ + lines = [ + "=" * 70, + " Wooldridge Extended Two-Way Fixed Effects (ETWFE) Results", + "=" * 70, + f"Method: {self.method}", + f"Control group: {self.control_group}", + f"Observations: {self.n_obs}", + f"Treated units: {self.n_treated_units}", + f"Control units: {self.n_control_units}", + "-" * 70, + ] + + def _fmt_row(label: str, att: float, se: float, t: float, + p: float, ci: Tuple) -> str: + from diff_diff.results import _get_significance_stars # type: ignore + stars = _get_significance_stars(p) if not np.isnan(p) else "" + ci_lo = f"{ci[0]:.4f}" if not np.isnan(ci[0]) else "NaN" + ci_hi = f"{ci[1]:.4f}" if not np.isnan(ci[1]) else "NaN" + return ( + f"{label:<22} {att:>10.4f} {se:>10.4f} {t:>8.3f} " + f"{p:>8.4f}{stars} [{ci_lo}, {ci_hi}]" + ) + + header = ( + f"{'Parameter':<22} {'Estimate':>10} {'Std. Err.':>10} " + f"{'t-stat':>8} {'P>|t|':>8} [95% CI]" + ) + lines.append(header) + lines.append("-" * 70) + + if aggregation == "simple": + lines.append(_fmt_row( + "ATT (simple)", + self.overall_att, self.overall_se, + self.overall_t_stat, self.overall_p_value, self.overall_conf_int, + )) + elif aggregation == "group" and self.group_effects: + for g, eff in sorted(self.group_effects.items()): + lines.append(_fmt_row( + f"ATT(g={g})", + eff["att"], eff["se"], eff["t_stat"], eff["p_value"], eff["conf_int"], + )) + elif aggregation == "calendar" and self.calendar_effects: + for t, eff in sorted(self.calendar_effects.items()): + lines.append(_fmt_row( + f"ATT(t={t})", + eff["att"], eff["se"], eff["t_stat"], eff["p_value"], eff["conf_int"], + )) + elif aggregation == "event" and self.event_study_effects: + for k, eff in sorted(self.event_study_effects.items()): + label = f"ATT(k={k})" + (" [pre]" if k < 0 else "") + lines.append(_fmt_row( + label, eff["att"], eff["se"], + eff["t_stat"], eff["p_value"], eff["conf_int"], + )) + else: + lines.append(f" (call .aggregate({aggregation!r}) first)") + + lines.append("=" * 70) + return "\n".join(lines) + + def to_dataframe(self, aggregation: str = "event") -> pd.DataFrame: + """Export aggregated effects to a DataFrame. + + Parameters + ---------- + aggregation : "simple" | "group" | "calendar" | "event" | "gt" + Use "gt" to export raw group-time effects. + """ + if aggregation == "gt": + rows = [] + for (g, t), eff in sorted(self.group_time_effects.items()): + row = {"cohort": g, "time": t, "relative_period": t - g} + row.update(eff) + rows.append(row) + return pd.DataFrame(rows) + + mapping = { + "simple": [{"label": "ATT", "att": self.overall_att, + "se": self.overall_se, "t_stat": self.overall_t_stat, + "p_value": self.overall_p_value, + "conf_int_lo": self.overall_conf_int[0], + "conf_int_hi": self.overall_conf_int[1]}], + "group": [ + {"cohort": g, **{k: v for k, v in eff.items() if k != "conf_int"}, + "conf_int_lo": eff["conf_int"][0], "conf_int_hi": eff["conf_int"][1]} + for g, eff in sorted((self.group_effects or {}).items()) + ], + "calendar": [ + {"time": t, **{k: v for k, v in eff.items() if k != "conf_int"}, + "conf_int_lo": eff["conf_int"][0], "conf_int_hi": eff["conf_int"][1]} + for t, eff in sorted((self.calendar_effects or {}).items()) + ], + "event": [ + {"relative_period": k, + **{kk: vv for kk, vv in eff.items() if kk != "conf_int"}, + "conf_int_lo": eff["conf_int"][0], "conf_int_hi": eff["conf_int"][1]} + for k, eff in sorted((self.event_study_effects or {}).items()) + ], + } + rows = mapping.get(aggregation, []) + return pd.DataFrame(rows) + + def plot_event_study(self, **kwargs) -> None: + """Event study plot. Calls aggregate('event') if needed.""" + if self.event_study_effects is None: + self.aggregate("event") + from diff_diff.visualization import plot_event_study # type: ignore + effects = {k: v["att"] for k, v in (self.event_study_effects or {}).items()} + se = {k: v["se"] for k, v in (self.event_study_effects or {}).items()} + plot_event_study(effects=effects, se=se, alpha=self.alpha, **kwargs) + + def __repr__(self) -> str: + n_gt = len(self.group_time_effects) + att_str = f"{self.overall_att:.4f}" if not np.isnan(self.overall_att) else "NaN" + se_str = f"{self.overall_se:.4f}" if not np.isnan(self.overall_se) else "NaN" + p_str = f"{self.overall_p_value:.4f}" if not np.isnan(self.overall_p_value) else "NaN" + return ( + f"WooldridgeDiDResults(" + f"ATT={att_str}, SE={se_str}, p={p_str}, " + f"n_gt={n_gt}, method={self.method!r})" + ) diff --git a/tests/test_wooldridge.py b/tests/test_wooldridge.py new file mode 100644 index 00000000..aea51179 --- /dev/null +++ b/tests/test_wooldridge.py @@ -0,0 +1,86 @@ +"""Tests for WooldridgeDiD estimator and WooldridgeDiDResults.""" +import numpy as np +import pandas as pd +import pytest +from diff_diff.wooldridge_results import WooldridgeDiDResults + + +def _make_minimal_results(**kwargs): + """Helper: build a WooldridgeDiDResults with required fields.""" + defaults = dict( + group_time_effects={ + (2, 2): {"att": 1.0, "se": 0.5, "t_stat": 2.0, "p_value": 0.04, "conf_int": (0.02, 1.98)}, + (2, 3): {"att": 1.5, "se": 0.6, "t_stat": 2.5, "p_value": 0.01, "conf_int": (0.32, 2.68)}, + (3, 3): {"att": 0.8, "se": 0.4, "t_stat": 2.0, "p_value": 0.04, "conf_int": (0.02, 1.58)}, + }, + overall_att=1.1, + overall_se=0.35, + overall_t_stat=3.14, + overall_p_value=0.002, + overall_conf_int=(0.41, 1.79), + group_effects=None, + calendar_effects=None, + event_study_effects=None, + method="ols", + control_group="not_yet_treated", + groups=[2, 3], + time_periods=[1, 2, 3], + n_obs=300, + n_treated_units=100, + n_control_units=200, + alpha=0.05, + _gt_weights={(2, 2): 50, (2, 3): 50, (3, 3): 30}, + _gt_vcov=None, + ) + defaults.update(kwargs) + return WooldridgeDiDResults(**defaults) + + +class TestWooldridgeDiDResults: + def test_repr(self): + r = _make_minimal_results() + s = repr(r) + assert "WooldridgeDiDResults" in s + assert "ATT" in s + + def test_summary_default(self): + r = _make_minimal_results() + s = r.summary() + assert "1.1" in s or "ATT" in s + + def test_to_dataframe_event(self): + r = _make_minimal_results() + r.aggregate("event") + df = r.to_dataframe("event") + assert isinstance(df, pd.DataFrame) + assert "att" in df.columns + + def test_aggregate_simple_returns_self(self): + r = _make_minimal_results() + result = r.aggregate("simple") + assert result is r # chaining + + def test_aggregate_group(self): + r = _make_minimal_results() + r.aggregate("group") + assert r.group_effects is not None + assert 2 in r.group_effects + assert 3 in r.group_effects + + def test_aggregate_calendar(self): + r = _make_minimal_results() + r.aggregate("calendar") + assert r.calendar_effects is not None + assert 2 in r.calendar_effects or 3 in r.calendar_effects + + def test_aggregate_event(self): + r = _make_minimal_results() + r.aggregate("event") + assert r.event_study_effects is not None + # relative period 0 (treatment period itself) should be present + assert 0 in r.event_study_effects or 1 in r.event_study_effects + + def test_aggregate_invalid_raises(self): + r = _make_minimal_results() + with pytest.raises(ValueError, match="type"): + r.aggregate("bad_type") From 6790e4bd97d47de0130ebc0199a3c7891f4eb976 Mon Sep 17 00:00:00 2001 From: wenddymacro <50739376+wenddymacro@users.noreply.github.com> Date: Thu, 19 Mar 2026 14:53:45 +0800 Subject: [PATCH 07/16] feat: add WooldridgeDiD class scaffold with constructor and param API --- diff_diff/wooldridge.py | 158 +++++++++++++++++++++++++++++++++++++++ tests/test_wooldridge.py | 57 ++++++++++++++ 2 files changed, 215 insertions(+) create mode 100644 diff_diff/wooldridge.py diff --git a/diff_diff/wooldridge.py b/diff_diff/wooldridge.py new file mode 100644 index 00000000..a7808bfb --- /dev/null +++ b/diff_diff/wooldridge.py @@ -0,0 +1,158 @@ +"""WooldridgeDiD: Extended Two-Way Fixed Effects (ETWFE) estimator. + +Implements Wooldridge (2021, 2023) ETWFE, faithful to the Stata jwdid package. + +References +---------- +Wooldridge (2021). Two-Way Fixed Effects, the Two-Way Mundlak Regression, + and Difference-in-Differences Estimators. SSRN 3906345. +Wooldridge (2023). Simple approaches to nonlinear difference-in-differences + with panel data. The Econometrics Journal, 26(3), C31-C66. +Friosavila (2021). jwdid: Stata module. SSC s459114. +""" +from __future__ import annotations + +from typing import Any, Dict, List, Optional, Tuple + +import numpy as np +import pandas as pd + +from diff_diff.linalg import compute_robust_vcov, solve_logit, solve_ols, solve_poisson +from diff_diff.utils import safe_inference, within_transform +from diff_diff.wooldridge_results import WooldridgeDiDResults + +_VALID_METHODS = ("ols", "logit", "poisson") +_VALID_CONTROL_GROUPS = ("never_treated", "not_yet_treated") +_VALID_BOOTSTRAP_WEIGHTS = ("rademacher", "webb", "mammen") + + +class WooldridgeDiD: + """Extended Two-Way Fixed Effects (ETWFE) DiD estimator. + + Implements the Wooldridge (2021) saturated cohort×time regression and + Wooldridge (2023) nonlinear extensions (logit, Poisson). Produces all + four ``jwdid_estat`` aggregation types: simple, group, calendar, event. + + Parameters + ---------- + method : {"ols", "logit", "poisson"} + Estimation method. "ols" for continuous outcomes; "logit" for binary + or fractional outcomes; "poisson" for count data. + control_group : {"not_yet_treated", "never_treated"} + Which units serve as the comparison group. "not_yet_treated" (jwdid + default) uses all untreated observations at each time period; + "never_treated" uses only units never treated throughout the sample. + anticipation : int + Number of periods before treatment onset to include as treatment cells + (anticipation effects). 0 means no anticipation. + demean_covariates : bool + If True (jwdid default), ``xtvar`` covariates are demeaned within each + cohort×period cell before entering the regression. Set to False to + replicate jwdid's ``xasis`` option. + alpha : float + Significance level for confidence intervals. + cluster : str or None + Column name to use for cluster-robust SEs. Defaults to the ``unit`` + identifier passed to ``fit()``. + n_bootstrap : int + Number of bootstrap replications. 0 disables bootstrap. + bootstrap_weights : {"rademacher", "webb", "mammen"} + Bootstrap weight distribution. + seed : int or None + Random seed for reproducibility. + rank_deficient_action : {"warn", "error", "silent"} + How to handle rank-deficient design matrices. + """ + + def __init__( + self, + method: str = "ols", + control_group: str = "not_yet_treated", + anticipation: int = 0, + demean_covariates: bool = True, + alpha: float = 0.05, + cluster: Optional[str] = None, + n_bootstrap: int = 0, + bootstrap_weights: str = "rademacher", + seed: Optional[int] = None, + rank_deficient_action: str = "warn", + ) -> None: + if method not in _VALID_METHODS: + raise ValueError(f"method must be one of {_VALID_METHODS}, got {method!r}") + if control_group not in _VALID_CONTROL_GROUPS: + raise ValueError( + f"control_group must be one of {_VALID_CONTROL_GROUPS}, got {control_group!r}" + ) + if anticipation < 0: + raise ValueError(f"anticipation must be >= 0, got {anticipation}") + + self.method = method + self.control_group = control_group + self.anticipation = anticipation + self.demean_covariates = demean_covariates + self.alpha = alpha + self.cluster = cluster + self.n_bootstrap = n_bootstrap + self.bootstrap_weights = bootstrap_weights + self.seed = seed + self.rank_deficient_action = rank_deficient_action + + self.is_fitted_: bool = False + self._results: Optional[WooldridgeDiDResults] = None + + @property + def results_(self) -> WooldridgeDiDResults: + if not self.is_fitted_: + raise RuntimeError("Call fit() before accessing results_") + return self._results # type: ignore[return-value] + + def get_params(self) -> Dict[str, Any]: + """Return estimator parameters (sklearn-compatible).""" + return { + "method": self.method, + "control_group": self.control_group, + "anticipation": self.anticipation, + "demean_covariates": self.demean_covariates, + "alpha": self.alpha, + "cluster": self.cluster, + "n_bootstrap": self.n_bootstrap, + "bootstrap_weights": self.bootstrap_weights, + "seed": self.seed, + "rank_deficient_action": self.rank_deficient_action, + } + + def set_params(self, **params: Any) -> "WooldridgeDiD": + """Set estimator parameters (sklearn-compatible). Returns self.""" + for key, value in params.items(): + if not hasattr(self, key): + raise ValueError(f"Unknown parameter: {key!r}") + setattr(self, key, value) + return self + + def fit( + self, + data: pd.DataFrame, + outcome: str, + unit: str, + time: str, + cohort: str, + exovar: Optional[List[str]] = None, + xtvar: Optional[List[str]] = None, + xgvar: Optional[List[str]] = None, + ) -> WooldridgeDiDResults: + """Fit the ETWFE model. See class docstring for parameter details. + + Parameters + ---------- + data : DataFrame with panel data (long format) + outcome : outcome column name + unit : unit identifier column + time : time period column + cohort : first treatment period (0 or NaN = never treated) + exovar : time-invariant covariates added without interaction/demeaning + xtvar : time-varying covariates (demeaned within cohort×period cells + when ``demean_covariates=True``) + xgvar : covariates interacted with each cohort indicator + """ + # Placeholder — implementation in Tasks 4 & 5 + raise NotImplementedError("fit() implemented in later tasks") diff --git a/tests/test_wooldridge.py b/tests/test_wooldridge.py index aea51179..11988976 100644 --- a/tests/test_wooldridge.py +++ b/tests/test_wooldridge.py @@ -3,6 +3,7 @@ import pandas as pd import pytest from diff_diff.wooldridge_results import WooldridgeDiDResults +from diff_diff.wooldridge import WooldridgeDiD def _make_minimal_results(**kwargs): @@ -84,3 +85,59 @@ def test_aggregate_invalid_raises(self): r = _make_minimal_results() with pytest.raises(ValueError, match="type"): r.aggregate("bad_type") + + +class TestWooldridgeDiDAPI: + def test_default_construction(self): + est = WooldridgeDiD() + assert est.method == "ols" + assert est.control_group == "not_yet_treated" + assert est.anticipation == 0 + assert est.demean_covariates is True + assert est.alpha == 0.05 + assert est.cluster is None + assert est.n_bootstrap == 0 + assert est.bootstrap_weights == "rademacher" + assert est.seed is None + assert est.rank_deficient_action == "warn" + assert not est.is_fitted_ + + def test_invalid_method_raises(self): + with pytest.raises(ValueError, match="method"): + WooldridgeDiD(method="probit") + + def test_invalid_control_group_raises(self): + with pytest.raises(ValueError, match="control_group"): + WooldridgeDiD(control_group="clean_control") + + def test_invalid_anticipation_raises(self): + with pytest.raises(ValueError, match="anticipation"): + WooldridgeDiD(anticipation=-1) + + def test_get_params_roundtrip(self): + est = WooldridgeDiD(method="logit", alpha=0.1, anticipation=1) + params = est.get_params() + assert params["method"] == "logit" + assert params["alpha"] == 0.1 + assert params["anticipation"] == 1 + + def test_set_params_roundtrip(self): + est = WooldridgeDiD() + est.set_params(alpha=0.01, n_bootstrap=100) + assert est.alpha == 0.01 + assert est.n_bootstrap == 100 + + def test_set_params_returns_self(self): + est = WooldridgeDiD() + result = est.set_params(alpha=0.1) + assert result is est + + def test_set_params_unknown_raises(self): + est = WooldridgeDiD() + with pytest.raises(ValueError, match="Unknown"): + est.set_params(nonexistent_param=42) + + def test_results_before_fit_raises(self): + est = WooldridgeDiD() + with pytest.raises(RuntimeError, match="fit"): + _ = est.results_ From 2cb19a155d98f6d02446ac7fbf2ff8de4db2d822 Mon Sep 17 00:00:00 2001 From: wenddymacro <50739376+wenddymacro@users.noreply.github.com> Date: Thu, 19 Mar 2026 14:57:11 +0800 Subject: [PATCH 08/16] feat: add ETWFE data preparation helpers (filter, interactions, covariates) --- diff_diff/wooldridge.py | 107 +++++++++++++++++++++++++++++++++++++++ tests/test_wooldridge.py | 74 ++++++++++++++++++++++++++- 2 files changed, 180 insertions(+), 1 deletion(-) diff --git a/diff_diff/wooldridge.py b/diff_diff/wooldridge.py index a7808bfb..97667f7d 100644 --- a/diff_diff/wooldridge.py +++ b/diff_diff/wooldridge.py @@ -26,6 +26,113 @@ _VALID_BOOTSTRAP_WEIGHTS = ("rademacher", "webb", "mammen") +def _filter_sample( + data: pd.DataFrame, + unit: str, + time: str, + cohort: str, + control_group: str, + anticipation: int, +) -> pd.DataFrame: + """Return the analysis sample following jwdid selection rules. + + Treated units: all observations kept (pre-treatment window beyond + anticipation is not used as a treatment cell but is kept for FE). + Control units: for "not_yet_treated", units with cohort > t at each t + (including never-treated); for "never_treated", only cohort == 0/NaN. + """ + df = data.copy() + # Normalise never-treated: fill NaN cohort with 0 + df[cohort] = df[cohort].fillna(0) + + treated_mask = df[cohort] > 0 + + if control_group == "never_treated": + control_mask = df[cohort] == 0 + else: # not_yet_treated + # Keep untreated-at-t observations for not-yet-treated units + control_mask = (df[cohort] == 0) | (df[cohort] > df[time]) + + return df[treated_mask | control_mask].copy() + + +def _build_interaction_matrix( + data: pd.DataFrame, + cohort: str, + time: str, + anticipation: int, +) -> Tuple[np.ndarray, List[str], List[Tuple[Any, Any]]]: + """Build the saturated cohort×time interaction design matrix. + + Returns + ------- + X_int : (n, n_cells) binary indicator matrix + col_names : list of string labels "g{g}_t{t}" + gt_keys : list of (g, t) tuples in same column order + """ + groups = sorted(g for g in data[cohort].unique() if g > 0) + times = sorted(data[time].unique()) + cohort_vals = data[cohort].values + time_vals = data[time].values + + cols = [] + col_names = [] + gt_keys = [] + + for g in groups: + for t in times: + if t >= g - anticipation: + indicator = ((cohort_vals == g) & (time_vals == t)).astype(float) + cols.append(indicator) + col_names.append(f"g{g}_t{t}") + gt_keys.append((g, t)) + + if not cols: + return np.empty((len(data), 0)), [], [] + return np.column_stack(cols), col_names, gt_keys + + +def _prepare_covariates( + data: pd.DataFrame, + exovar: Optional[List[str]], + xtvar: Optional[List[str]], + xgvar: Optional[List[str]], + cohort: str, + time: str, + demean_covariates: bool, + groups: List[Any], +) -> Optional[np.ndarray]: + """Build covariate matrix following jwdid covariate type conventions. + + Returns None if no covariates, else (n, k) array. + """ + parts = [] + + if exovar: + parts.append(data[exovar].values.astype(float)) + + if xtvar: + if demean_covariates: + # Within-cohort×period demeaning + grp_key = data[cohort].astype(str) + "_" + data[time].astype(str) + tmp = data[xtvar].copy() + for col in xtvar: + tmp[col] = tmp[col] - tmp.groupby(grp_key)[col].transform("mean") + parts.append(tmp.values.astype(float)) + else: + parts.append(data[xtvar].values.astype(float)) + + if xgvar: + for g in groups: + g_indicator = (data[cohort] == g).values.astype(float) + for col in xgvar: + parts.append((g_indicator * data[col].values).reshape(-1, 1)) + + if not parts: + return None + return np.hstack([p if p.ndim == 2 else p.reshape(-1, 1) for p in parts]) + + class WooldridgeDiD: """Extended Two-Way Fixed Effects (ETWFE) DiD estimator. diff --git a/tests/test_wooldridge.py b/tests/test_wooldridge.py index 11988976..1957098f 100644 --- a/tests/test_wooldridge.py +++ b/tests/test_wooldridge.py @@ -3,7 +3,7 @@ import pandas as pd import pytest from diff_diff.wooldridge_results import WooldridgeDiDResults -from diff_diff.wooldridge import WooldridgeDiD +from diff_diff.wooldridge import WooldridgeDiD, _filter_sample, _build_interaction_matrix, _prepare_covariates def _make_minimal_results(**kwargs): @@ -141,3 +141,75 @@ def test_results_before_fit_raises(self): est = WooldridgeDiD() with pytest.raises(RuntimeError, match="fit"): _ = est.results_ + + +def _make_panel(n_units=10, n_periods=5, treat_share=0.5, seed=0): + """Create a simple balanced panel for testing.""" + rng = np.random.default_rng(seed) + units = np.arange(n_units) + n_treated = int(n_units * treat_share) + # Two cohorts: half treated in period 3, rest never treated + cohort = np.array([3] * n_treated + [0] * (n_units - n_treated)) + rows = [] + for u in units: + for t in range(1, n_periods + 1): + rows.append({"unit": u, "time": t, "cohort": cohort[u], + "y": rng.standard_normal(), "x1": rng.standard_normal()}) + return pd.DataFrame(rows) + + +class TestDataPrep: + def test_filter_sample_not_yet_treated(self): + df = _make_panel() + filtered = _filter_sample(df, unit="unit", time="time", cohort="cohort", + control_group="not_yet_treated", anticipation=0) + # All treated units should be present (all periods) + treated_units = df[df["cohort"] == 3]["unit"].unique() + assert set(treated_units).issubset(filtered["unit"].unique()) + + def test_filter_sample_never_treated(self): + df = _make_panel() + filtered = _filter_sample(df, unit="unit", time="time", cohort="cohort", + control_group="never_treated", anticipation=0) + # Only never-treated (cohort==0) and treated units should remain + assert (filtered["cohort"].isin([0, 3])).all() + + def test_build_interaction_matrix_columns(self): + df = _make_panel() + filtered = _filter_sample(df, "unit", "time", "cohort", + "not_yet_treated", anticipation=0) + X_int, col_names, gt_keys = _build_interaction_matrix( + filtered, cohort="cohort", time="time", anticipation=0 + ) + # Each column should be a valid (g, t) pair with t >= g + for (g, t) in gt_keys: + assert t >= g + + def test_build_interaction_matrix_binary(self): + df = _make_panel() + filtered = _filter_sample(df, "unit", "time", "cohort", + "not_yet_treated", anticipation=0) + X_int, col_names, gt_keys = _build_interaction_matrix( + filtered, cohort="cohort", time="time", anticipation=0 + ) + # All values should be 0 or 1 + assert set(np.unique(X_int)).issubset({0, 1}) + + def test_prepare_covariates_exovar(self): + df = _make_panel() + X_cov = _prepare_covariates(df, exovar=["x1"], xtvar=None, xgvar=None, + cohort="cohort", time="time", + demean_covariates=True, groups=[3]) + assert X_cov.shape[0] == len(df) + assert X_cov.shape[1] == 1 # just x1 + + def test_prepare_covariates_xtvar_demeaned(self): + df = _make_panel() + X_raw = _prepare_covariates(df, exovar=None, xtvar=["x1"], xgvar=None, + cohort="cohort", time="time", + demean_covariates=False, groups=[3]) + X_dem = _prepare_covariates(df, exovar=None, xtvar=["x1"], xgvar=None, + cohort="cohort", time="time", + demean_covariates=True, groups=[3]) + # Demeaned version should differ from raw + assert not np.allclose(X_raw, X_dem) From 6a16e96930c533fea0435e51f23cbff27d393b47 Mon Sep 17 00:00:00 2001 From: wenddymacro <50739376+wenddymacro@users.noreply.github.com> Date: Thu, 19 Mar 2026 15:01:28 +0800 Subject: [PATCH 09/16] feat: implement WooldridgeDiD.fit() OLS path with ETWFE saturated regression --- diff_diff/wooldridge.py | 181 ++++++++++++++++++++++++++++++++++++++- tests/test_wooldridge.py | 60 +++++++++++++ 2 files changed, 239 insertions(+), 2 deletions(-) diff --git a/diff_diff/wooldridge.py b/diff_diff/wooldridge.py index 97667f7d..43262b5b 100644 --- a/diff_diff/wooldridge.py +++ b/diff_diff/wooldridge.py @@ -26,6 +26,37 @@ _VALID_BOOTSTRAP_WEIGHTS = ("rademacher", "webb", "mammen") +def _compute_weighted_agg( + gt_effects: Dict, + gt_weights: Dict, + gt_keys: List, + gt_vcov: Optional[np.ndarray], + alpha: float, +) -> Dict: + """Compute simple (overall) weighted average ATT and SE via delta method.""" + post_keys = [(g, t) for (g, t) in gt_keys if t >= g] + w_total = sum(gt_weights.get(k, 0) for k in post_keys) + if w_total == 0: + att = float("nan") + se = float("nan") + else: + att = sum(gt_weights.get(k, 0) * gt_effects[k]["att"] + for k in post_keys if k in gt_effects) / w_total + if gt_vcov is not None: + w_vec = np.array([ + gt_weights.get(k, 0) / w_total if k in post_keys else 0.0 + for k in gt_keys + ]) + var = float(w_vec @ gt_vcov @ w_vec) + se = float(np.sqrt(max(var, 0.0))) + else: + se = float("nan") + + t_stat, p_value, conf_int = safe_inference(att, se, alpha=alpha) + return {"att": att, "se": se, "t_stat": t_stat, + "p_value": p_value, "conf_int": conf_int} + + def _filter_sample( data: pd.DataFrame, unit: str, @@ -261,5 +292,151 @@ def fit( when ``demean_covariates=True``) xgvar : covariates interacted with each cohort indicator """ - # Placeholder — implementation in Tasks 4 & 5 - raise NotImplementedError("fit() implemented in later tasks") + df = data.copy() + df[cohort] = df[cohort].fillna(0) + + # 1. Filter to analysis sample + sample = _filter_sample(df, unit, time, cohort, self.control_group, self.anticipation) + + # 2. Build interaction matrix + X_int, int_col_names, gt_keys = _build_interaction_matrix( + sample, cohort=cohort, time=time, anticipation=self.anticipation + ) + + # 3. Covariates + groups = sorted(g for g in sample[cohort].unique() if g > 0) + X_cov = _prepare_covariates( + sample, exovar=exovar, xtvar=xtvar, xgvar=xgvar, + cohort=cohort, time=time, + demean_covariates=self.demean_covariates, + groups=groups, + ) + + all_regressors = int_col_names.copy() + if X_cov is not None: + X_design = np.hstack([X_int, X_cov]) + for i in range(X_cov.shape[1]): + all_regressors.append(f"_cov_{i}") + else: + X_design = X_int + + if self.method == "ols": + results = self._fit_ols( + sample, outcome, unit, time, cohort, + X_design, all_regressors, gt_keys, int_col_names, + groups, + ) + elif self.method == "logit": + results = self._fit_logit( + sample, outcome, unit, time, cohort, + X_design, all_regressors, gt_keys, int_col_names, groups, + ) + else: # poisson + results = self._fit_poisson( + sample, outcome, unit, time, cohort, + X_design, all_regressors, gt_keys, int_col_names, groups, + ) + + self._results = results + self.is_fitted_ = True + return results + + def _fit_ols( + self, + sample: pd.DataFrame, + outcome: str, + unit: str, + time: str, + cohort: str, + X_design: np.ndarray, + col_names: List[str], + gt_keys: List[Tuple], + int_col_names: List[str], + groups: List[Any], + ) -> WooldridgeDiDResults: + """OLS path: within-transform FE, solve_ols, cluster SE.""" + # 4. Within-transform: absorb unit + time FE + all_vars = [outcome] + [f"_x{i}" for i in range(X_design.shape[1])] + tmp = sample[[unit, time]].copy() + tmp[outcome] = sample[outcome].values + for i in range(X_design.shape[1]): + tmp[f"_x{i}"] = X_design[:, i] + + transformed = within_transform(tmp, all_vars, unit=unit, time=time, + suffix="_demeaned") + + y = transformed[f"{outcome}_demeaned"].values + X_cols = [f"_x{i}_demeaned" for i in range(X_design.shape[1])] + X = transformed[X_cols].values + + # 5. Cluster IDs (default: unit level) + cluster_col = self.cluster if self.cluster else unit + cluster_ids = sample[cluster_col].values + + # 6. Solve OLS + coefs, resids, vcov = solve_ols( + X, y, + cluster_ids=cluster_ids, + return_vcov=True, + rank_deficient_action=self.rank_deficient_action, + column_names=col_names, + ) + + # 7. Extract β_{g,t} and build gt_effects dict + gt_effects: Dict[Tuple, Dict] = {} + gt_weights: Dict[Tuple, int] = {} + for idx, (g, t) in enumerate(gt_keys): + if idx >= len(coefs): + break + att = float(coefs[idx]) + se = float(np.sqrt(max(vcov[idx, idx], 0.0))) if vcov is not None else float("nan") + t_stat, p_value, conf_int = safe_inference(att, se, alpha=self.alpha) + gt_effects[(g, t)] = { + "att": att, "se": se, + "t_stat": t_stat, "p_value": p_value, "conf_int": conf_int, + } + gt_weights[(g, t)] = int(( + (sample[cohort] == g) & (sample[time] == t) + ).sum()) + + # Extract vcov submatrix for beta_{g,t} only + n_gt = len(gt_keys) + gt_vcov = vcov[:n_gt, :n_gt] if vcov is not None else None + gt_keys_ordered = list(gt_keys) + + # 8. Simple aggregation (always computed) + overall = _compute_weighted_agg(gt_effects, gt_weights, gt_keys_ordered, + gt_vcov, self.alpha) + + # Metadata + n_treated = int(sample[sample[cohort] > 0][unit].nunique()) + n_control = int(sample[sample[cohort] == 0][unit].nunique()) + all_times = sorted(sample[time].unique().tolist()) + + return WooldridgeDiDResults( + group_time_effects=gt_effects, + overall_att=overall["att"], + overall_se=overall["se"], + overall_t_stat=overall["t_stat"], + overall_p_value=overall["p_value"], + overall_conf_int=overall["conf_int"], + method=self.method, + control_group=self.control_group, + groups=groups, + time_periods=all_times, + n_obs=len(sample), + n_treated_units=n_treated, + n_control_units=n_control, + alpha=self.alpha, + _gt_weights=gt_weights, + _gt_vcov=gt_vcov, + _gt_keys=gt_keys_ordered, + ) + + def _fit_logit(self, sample, outcome, unit, time, cohort, + X_design, col_names, gt_keys, int_col_names, groups): + raise NotImplementedError("logit path implemented in Task 7") + + def _fit_poisson(self, sample, outcome, unit, time, cohort, + X_design, col_names, gt_keys, int_col_names, groups): + raise NotImplementedError("poisson path implemented in Task 8") diff --git a/tests/test_wooldridge.py b/tests/test_wooldridge.py index 1957098f..8258a499 100644 --- a/tests/test_wooldridge.py +++ b/tests/test_wooldridge.py @@ -213,3 +213,63 @@ def test_prepare_covariates_xtvar_demeaned(self): demean_covariates=True, groups=[3]) # Demeaned version should differ from raw assert not np.allclose(X_raw, X_dem) + + +class TestWooldridgeDiDFitOLS: + @pytest.fixture + def mpdta(self): + from diff_diff.datasets import load_mpdta + return load_mpdta() + + def test_fit_returns_results(self, mpdta): + est = WooldridgeDiD() + results = est.fit(mpdta, outcome="lemp", unit="countyreal", + time="year", cohort="first_treat") + assert isinstance(results, WooldridgeDiDResults) + + def test_fit_sets_is_fitted(self, mpdta): + est = WooldridgeDiD() + est.fit(mpdta, outcome="lemp", unit="countyreal", + time="year", cohort="first_treat") + assert est.is_fitted_ + + def test_overall_att_finite(self, mpdta): + est = WooldridgeDiD() + r = est.fit(mpdta, outcome="lemp", unit="countyreal", + time="year", cohort="first_treat") + assert np.isfinite(r.overall_att) + assert np.isfinite(r.overall_se) + assert r.overall_se > 0 + + def test_group_time_effects_populated(self, mpdta): + est = WooldridgeDiD() + r = est.fit(mpdta, outcome="lemp", unit="countyreal", + time="year", cohort="first_treat") + assert len(r.group_time_effects) > 0 + for (g, t), eff in r.group_time_effects.items(): + assert t >= g + assert "att" in eff and "se" in eff + + def test_all_inference_fields_finite(self, mpdta): + """No inference field should be NaN in normal data.""" + est = WooldridgeDiD() + r = est.fit(mpdta, outcome="lemp", unit="countyreal", + time="year", cohort="first_treat") + assert np.isfinite(r.overall_t_stat) + assert np.isfinite(r.overall_p_value) + assert all(np.isfinite(c) for c in r.overall_conf_int) + + def test_never_treated_control_group(self, mpdta): + est = WooldridgeDiD(control_group="never_treated") + r = est.fit(mpdta, outcome="lemp", unit="countyreal", + time="year", cohort="first_treat") + assert len(r.group_time_effects) > 0 + + def test_metadata_correct(self, mpdta): + est = WooldridgeDiD() + r = est.fit(mpdta, outcome="lemp", unit="countyreal", + time="year", cohort="first_treat") + assert r.method == "ols" + assert r.n_obs > 0 + assert r.n_treated_units > 0 + assert r.n_control_units > 0 From e5514e5bfc06d2ce1aa059a8eaa31d6ac95d7051 Mon Sep 17 00:00:00 2001 From: wenddymacro <50739376+wenddymacro@users.noreply.github.com> Date: Thu, 19 Mar 2026 15:02:14 +0800 Subject: [PATCH 10/16] test: add aggregation correctness tests for WooldridgeDiD --- tests/test_wooldridge.py | 49 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/tests/test_wooldridge.py b/tests/test_wooldridge.py index 8258a499..a50f6a1f 100644 --- a/tests/test_wooldridge.py +++ b/tests/test_wooldridge.py @@ -273,3 +273,52 @@ def test_metadata_correct(self, mpdta): assert r.n_obs > 0 assert r.n_treated_units > 0 assert r.n_control_units > 0 + + +class TestAggregations: + @pytest.fixture + def fitted(self): + from diff_diff.datasets import load_mpdta + df = load_mpdta() + est = WooldridgeDiD() + return est.fit(df, outcome="lemp", unit="countyreal", + time="year", cohort="first_treat") + + def test_simple_matches_manual_weighted_average(self, fitted): + """simple ATT must equal manually computed weighted average of ATT(g,t).""" + gt = fitted.group_time_effects + w = fitted._gt_weights + post_keys = [(g, t) for (g, t) in w if t >= g] + w_total = sum(w[k] for k in post_keys) + manual_att = sum(w[k] * gt[k]["att"] for k in post_keys) / w_total + assert abs(fitted.overall_att - manual_att) < 1e-10 + + def test_aggregate_group_keys_match_cohorts(self, fitted): + fitted.aggregate("group") + assert set(fitted.group_effects.keys()) == set(fitted.groups) + + def test_aggregate_event_relative_periods(self, fitted): + fitted.aggregate("event") + for k in fitted.event_study_effects: + assert isinstance(k, (int, np.integer)) + + def test_aggregate_calendar_finite(self, fitted): + fitted.aggregate("calendar") + for t, eff in fitted.calendar_effects.items(): + assert np.isfinite(eff["att"]) + + def test_summary_runs(self, fitted): + s = fitted.summary("simple") + assert "ETWFE" in s or "Wooldridge" in s + + def test_to_dataframe_event(self, fitted): + fitted.aggregate("event") + df = fitted.to_dataframe("event") + assert "relative_period" in df.columns + assert "att" in df.columns + + def test_to_dataframe_gt(self, fitted): + df = fitted.to_dataframe("gt") + assert "cohort" in df.columns + assert "time" in df.columns + assert len(df) == len(fitted.group_time_effects) From dc98cb509f88ba70e7c15a5f144e60e2df854434 Mon Sep 17 00:00:00 2001 From: wenddymacro <50739376+wenddymacro@users.noreply.github.com> Date: Thu, 19 Mar 2026 15:12:42 +0800 Subject: [PATCH 11/16] feat: implement WooldridgeDiD logit and Poisson paths with ASF ATT and delta-method SEs --- diff_diff/wooldridge.py | 289 ++++++++++++++++++++++++++++++++++++++- tests/test_wooldridge.py | 76 ++++++++++ 2 files changed, 359 insertions(+), 6 deletions(-) diff --git a/diff_diff/wooldridge.py b/diff_diff/wooldridge.py index 43262b5b..4e7dcc6a 100644 --- a/diff_diff/wooldridge.py +++ b/diff_diff/wooldridge.py @@ -26,6 +26,15 @@ _VALID_BOOTSTRAP_WEIGHTS = ("rademacher", "webb", "mammen") +def _logistic(x: np.ndarray) -> np.ndarray: + return 1.0 / (1.0 + np.exp(-x)) + + +def _logistic_deriv(x: np.ndarray) -> np.ndarray: + p = _logistic(x) + return p * (1.0 - p) + + def _compute_weighted_agg( gt_effects: Dict, gt_weights: Dict, @@ -433,10 +442,278 @@ def _fit_ols( _gt_keys=gt_keys_ordered, ) - def _fit_logit(self, sample, outcome, unit, time, cohort, - X_design, col_names, gt_keys, int_col_names, groups): - raise NotImplementedError("logit path implemented in Task 7") + def _fit_logit( + self, + sample: pd.DataFrame, + outcome: str, + unit: str, + time: str, + cohort: str, + X_int: np.ndarray, + col_names: List[str], + gt_keys: List[Tuple], + int_col_names: List[str], + groups: List[Any], + ) -> WooldridgeDiDResults: + """Logit path: cohort×period group FE + solve_logit + ASF ATT.""" + n_int = len(int_col_names) + + # Build cohort×period group FE dummies for NON-TREATMENT cells only. + # Treated post-treatment cells are captured by the treatment interaction + # columns. Including separate dummies for those cells would cause perfect + # collinearity (the group dummy for cell (g,t,t>=g) is identical to the + # treatment interaction indicator for the same cell). + is_treatment_cell = np.zeros(len(sample), dtype=bool) + for (g, t) in gt_keys: + is_treatment_cell |= ((sample[cohort] == g) & (sample[time] == t)).values + + grp_label = ( + sample[cohort].astype(str) + "_" + sample[time].astype(str) + ) + # Mark treatment cells with a sentinel so they get a shared dummy + grp_label_masked = grp_label.copy() + grp_label_masked[is_treatment_cell] = "__treated__" + group_dummies = pd.get_dummies(grp_label_masked, drop_first=True).values.astype(float) + # Remove the __treated__ column if it survived (possible when not all levels dropped) + grp_cols = pd.get_dummies(grp_label_masked, drop_first=True).columns.tolist() + if "__treated__" in grp_cols: + treated_col_idx = grp_cols.index("__treated__") + group_dummies = np.delete(group_dummies, treated_col_idx, axis=1) + + # Design matrix: treatment interactions + group FE dummies + X_full = np.hstack([X_int, group_dummies]) + + y = sample[outcome].values.astype(float) + cluster_col = self.cluster if self.cluster else unit + cluster_ids = sample[cluster_col].values + + beta, probs = solve_logit( + X_full, y, + rank_deficient_action=self.rank_deficient_action, + ) + # solve_logit prepends intercept — beta[0] is intercept, beta[1:] are X_full cols + beta_int_cols = beta[1: n_int + 1] # treatment interaction coefficients + + # Sandwich vcov (manual, weighted by p*(1-p)) + resids = y - probs + X_with_intercept = np.column_stack([np.ones(len(y)), X_full]) + W = probs * (1 - probs) # logit variance weights + XtWX = X_with_intercept.T @ (W[:, None] * X_with_intercept) + try: + XtWX_inv = np.linalg.inv(XtWX) + except np.linalg.LinAlgError: + XtWX_inv = np.full_like(XtWX, float("nan")) + + # Cluster-robust meat + clusters = np.unique(cluster_ids) + meat = np.zeros_like(XtWX) + for c in clusters: + mask = cluster_ids == c + scores_c = (X_with_intercept[mask] * resids[mask, None]).sum(axis=0) + meat += np.outer(scores_c, scores_c) + vcov_full = XtWX_inv @ meat @ XtWX_inv + + # ASF ATT(g,t) for treated units in each cell + gt_effects: Dict[Tuple, Dict] = {} + gt_weights: Dict[Tuple, int] = {} + gt_grads: Dict[Tuple, np.ndarray] = {} # store per-cell gradients for aggregate SE + for idx, (g, t) in enumerate(gt_keys): + if idx >= n_int: + break + cell_mask = (sample[cohort] == g) & (sample[time] == t) + if cell_mask.sum() == 0: + continue + eta_base = X_with_intercept[cell_mask] @ beta + att = float(np.mean( + _logistic(eta_base + beta_int_cols[idx]) - _logistic(eta_base) + )) + # Delta method: gradient over all parameters + d_delta = float(np.mean(_logistic_deriv(eta_base + beta_int_cols[idx]))) + d_base_diff = ( + _logistic_deriv(eta_base + beta_int_cols[idx]) - + _logistic_deriv(eta_base) + ) + grad = np.mean( + X_with_intercept[cell_mask] * d_base_diff[:, None], + axis=0 + ) + grad[1 + idx] += d_delta + se = float(np.sqrt(max(grad @ vcov_full @ grad, 0.0))) + t_stat, p_value, conf_int = safe_inference(att, se, alpha=self.alpha) + gt_effects[(g, t)] = { + "att": att, "se": se, + "t_stat": t_stat, "p_value": p_value, "conf_int": conf_int, + } + gt_weights[(g, t)] = int(cell_mask.sum()) + gt_grads[(g, t)] = grad + + gt_keys_ordered = [k for k in gt_keys if k in gt_effects] + # Overall SE via joint delta method: ∇β(overall_att) = Σ w_k/w_total * grad_k + post_keys = [(g, t) for (g, t) in gt_keys_ordered if t >= g] + w_total = sum(gt_weights.get(k, 0) for k in post_keys) + if w_total > 0 and post_keys: + overall_att = sum(gt_weights[k] * gt_effects[k]["att"] for k in post_keys) / w_total + agg_grad = sum( + (gt_weights[k] / w_total) * gt_grads[k] for k in post_keys + ) + overall_se = float(np.sqrt(max(agg_grad @ vcov_full @ agg_grad, 0.0))) + t_stat, p_value, conf_int = safe_inference(overall_att, overall_se, alpha=self.alpha) + overall = {"att": overall_att, "se": overall_se, + "t_stat": t_stat, "p_value": p_value, "conf_int": conf_int} + else: + overall = _compute_weighted_agg(gt_effects, gt_weights, gt_keys_ordered, + None, self.alpha) - def _fit_poisson(self, sample, outcome, unit, time, cohort, - X_design, col_names, gt_keys, int_col_names, groups): - raise NotImplementedError("poisson path implemented in Task 8") + return WooldridgeDiDResults( + group_time_effects=gt_effects, + overall_att=overall["att"], + overall_se=overall["se"], + overall_t_stat=overall["t_stat"], + overall_p_value=overall["p_value"], + overall_conf_int=overall["conf_int"], + method=self.method, + control_group=self.control_group, + groups=groups, + time_periods=sorted(sample[time].unique().tolist()), + n_obs=len(sample), + n_treated_units=int(sample[sample[cohort] > 0][unit].nunique()), + n_control_units=int(sample[sample[cohort] == 0][unit].nunique()), + alpha=self.alpha, + _gt_weights=gt_weights, + _gt_vcov=None, + _gt_keys=gt_keys_ordered, + ) + + def _fit_poisson( + self, + sample: pd.DataFrame, + outcome: str, + unit: str, + time: str, + cohort: str, + X_int: np.ndarray, + col_names: List[str], + gt_keys: List[Tuple], + int_col_names: List[str], + groups: List[Any], + ) -> WooldridgeDiDResults: + """Poisson path: cohort×period group FE + solve_poisson + ASF ATT.""" + n_int = len(int_col_names) + + # Build group FE dummies for NON-TREATMENT cells only (avoids collinearity; + # see _fit_logit for detailed explanation). + is_treatment_cell = np.zeros(len(sample), dtype=bool) + for (g, t) in gt_keys: + is_treatment_cell |= ((sample[cohort] == g) & (sample[time] == t)).values + + grp_label = ( + sample[cohort].astype(str) + "_" + sample[time].astype(str) + ) + grp_label_masked = grp_label.copy() + grp_label_masked[is_treatment_cell] = "__treated__" + _dummy_df = pd.get_dummies(grp_label_masked, drop_first=True) + group_dummies = _dummy_df.values.astype(float) + if "__treated__" in _dummy_df.columns: + treated_col_idx = _dummy_df.columns.tolist().index("__treated__") + group_dummies = np.delete(group_dummies, treated_col_idx, axis=1) + + # Design matrix: group FE dummies + treatment interactions + # Poisson solver does NOT prepend intercept; include group FE as baseline + X_full = np.hstack([group_dummies, X_int]) + n_fe = group_dummies.shape[1] + + y = sample[outcome].values.astype(float) + cluster_col = self.cluster if self.cluster else unit + cluster_ids = sample[cluster_col].values + + beta, mu_hat = solve_poisson(X_full, y) + + # Sandwich vcov: (X'WX)^{-1} (X'diag(resid^2)X) (X'WX)^{-1} + resids = y - mu_hat + W = mu_hat # Poisson variance = mean + XtWX = X_full.T @ (W[:, None] * X_full) + try: + XtWX_inv = np.linalg.inv(XtWX) + except np.linalg.LinAlgError: + XtWX_inv = np.full_like(XtWX, float("nan")) + + # Cluster-robust meat + clusters = np.unique(cluster_ids) + meat = np.zeros_like(XtWX) + for c in clusters: + mask = cluster_ids == c + scores_c = (X_full[mask] * resids[mask, None]).sum(axis=0) + meat += np.outer(scores_c, scores_c) + vcov_full = XtWX_inv @ meat @ XtWX_inv + + # Interaction columns start at column n_fe in X_full + beta_int = beta[n_fe: n_fe + n_int] + + # ASF ATT(g,t): E[exp(η + δ) - exp(η)] for treated units in cell + gt_effects: Dict[Tuple, Dict] = {} + gt_weights: Dict[Tuple, int] = {} + gt_grads: Dict[Tuple, np.ndarray] = {} # per-cell gradients for aggregate SE + for idx, (g, t) in enumerate(gt_keys): + if idx >= n_int: + break + cell_mask = (sample[cohort] == g) & (sample[time] == t) + if cell_mask.sum() == 0: + continue + eta_base = X_full[cell_mask] @ beta + delta = beta_int[idx] + att = float(np.mean(np.exp(eta_base + delta) - np.exp(eta_base))) + # Delta method gradient + grad_delta = float(np.mean(np.exp(eta_base + delta))) + grad_base = np.mean( + X_full[cell_mask] * ( + np.exp(eta_base + delta) - np.exp(eta_base) + )[:, None], + axis=0, + ) + grad = grad_base.copy() + grad[n_fe + idx] += grad_delta + se = float(np.sqrt(max(grad @ vcov_full @ grad, 0.0))) + t_stat, p_value, conf_int = safe_inference(att, se, alpha=self.alpha) + gt_effects[(g, t)] = { + "att": att, "se": se, + "t_stat": t_stat, "p_value": p_value, "conf_int": conf_int, + } + gt_weights[(g, t)] = int(cell_mask.sum()) + gt_grads[(g, t)] = grad + + gt_keys_ordered = [k for k in gt_keys if k in gt_effects] + # Overall SE via joint delta method + post_keys = [(g, t) for (g, t) in gt_keys_ordered if t >= g] + w_total = sum(gt_weights.get(k, 0) for k in post_keys) + if w_total > 0 and post_keys: + overall_att = sum(gt_weights[k] * gt_effects[k]["att"] for k in post_keys) / w_total + agg_grad = sum( + (gt_weights[k] / w_total) * gt_grads[k] for k in post_keys + ) + overall_se = float(np.sqrt(max(agg_grad @ vcov_full @ agg_grad, 0.0))) + t_stat, p_value, conf_int = safe_inference(overall_att, overall_se, alpha=self.alpha) + overall = {"att": overall_att, "se": overall_se, + "t_stat": t_stat, "p_value": p_value, "conf_int": conf_int} + else: + overall = _compute_weighted_agg(gt_effects, gt_weights, gt_keys_ordered, + None, self.alpha) + + return WooldridgeDiDResults( + group_time_effects=gt_effects, + overall_att=overall["att"], + overall_se=overall["se"], + overall_t_stat=overall["t_stat"], + overall_p_value=overall["p_value"], + overall_conf_int=overall["conf_int"], + method=self.method, + control_group=self.control_group, + groups=groups, + time_periods=sorted(sample[time].unique().tolist()), + n_obs=len(sample), + n_treated_units=int(sample[sample[cohort] > 0][unit].nunique()), + n_control_units=int(sample[sample[cohort] == 0][unit].nunique()), + alpha=self.alpha, + _gt_weights=gt_weights, + _gt_vcov=None, + _gt_keys=gt_keys_ordered, + ) diff --git a/tests/test_wooldridge.py b/tests/test_wooldridge.py index a50f6a1f..2d7f94db 100644 --- a/tests/test_wooldridge.py +++ b/tests/test_wooldridge.py @@ -322,3 +322,79 @@ def test_to_dataframe_gt(self, fitted): assert "cohort" in df.columns assert "time" in df.columns assert len(df) == len(fitted.group_time_effects) + + +class TestWooldridgeDiDLogit: + @pytest.fixture + def binary_panel(self): + """Simulated binary outcome panel with known positive ATT.""" + rng = np.random.default_rng(42) + n_units, n_periods = 60, 5 + rows = [] + for u in range(n_units): + cohort = 3 if u < 30 else 0 + for t in range(1, n_periods + 1): + treated = int(cohort > 0 and t >= cohort) + eta = -0.5 + 1.0 * treated + 0.1 * rng.standard_normal() + y = int(rng.random() < 1 / (1 + np.exp(-eta))) + rows.append({"unit": u, "time": t, "cohort": cohort, "y": y}) + return pd.DataFrame(rows) + + def test_logit_fit_runs(self, binary_panel): + est = WooldridgeDiD(method="logit") + r = est.fit(binary_panel, outcome="y", unit="unit", + time="time", cohort="cohort") + assert isinstance(r, WooldridgeDiDResults) + + def test_logit_att_sign(self, binary_panel): + """ATT should be positive (treatment increases binary outcome).""" + est = WooldridgeDiD(method="logit") + r = est.fit(binary_panel, outcome="y", unit="unit", + time="time", cohort="cohort") + assert r.overall_att > 0 + + def test_logit_se_positive(self, binary_panel): + est = WooldridgeDiD(method="logit") + r = est.fit(binary_panel, outcome="y", unit="unit", + time="time", cohort="cohort") + assert r.overall_se > 0 + + def test_logit_method_stored(self, binary_panel): + est = WooldridgeDiD(method="logit") + r = est.fit(binary_panel, outcome="y", unit="unit", + time="time", cohort="cohort") + assert r.method == "logit" + + +class TestWooldridgeDiDPoisson: + @pytest.fixture + def count_panel(self): + rng = np.random.default_rng(7) + n_units, n_periods = 60, 5 + rows = [] + for u in range(n_units): + cohort = 3 if u < 30 else 0 + for t in range(1, n_periods + 1): + treated = int(cohort > 0 and t >= cohort) + mu = np.exp(0.5 + 0.8 * treated + 0.1 * rng.standard_normal()) + y = rng.poisson(mu) + rows.append({"unit": u, "time": t, "cohort": cohort, "y": float(y)}) + return pd.DataFrame(rows) + + def test_poisson_fit_runs(self, count_panel): + est = WooldridgeDiD(method="poisson") + r = est.fit(count_panel, outcome="y", unit="unit", + time="time", cohort="cohort") + assert isinstance(r, WooldridgeDiDResults) + + def test_poisson_att_sign(self, count_panel): + est = WooldridgeDiD(method="poisson") + r = est.fit(count_panel, outcome="y", unit="unit", + time="time", cohort="cohort") + assert r.overall_att > 0 + + def test_poisson_se_positive(self, count_panel): + est = WooldridgeDiD(method="poisson") + r = est.fit(count_panel, outcome="y", unit="unit", + time="time", cohort="cohort") + assert r.overall_se > 0 From 7003a735d41d2f26b800397a3ef463f4d1d783e5 Mon Sep 17 00:00:00 2001 From: wenddymacro <50739376+wenddymacro@users.noreply.github.com> Date: Thu, 19 Mar 2026 15:14:34 +0800 Subject: [PATCH 12/16] feat: add multiplier bootstrap to WooldridgeDiD OLS path --- diff_diff/wooldridge.py | 52 +++++++++++++++++++++++++++++++++++++++- tests/test_wooldridge.py | 21 ++++++++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/diff_diff/wooldridge.py b/diff_diff/wooldridge.py index 4e7dcc6a..8939a9f0 100644 --- a/diff_diff/wooldridge.py +++ b/diff_diff/wooldridge.py @@ -422,7 +422,7 @@ def _fit_ols( n_control = int(sample[sample[cohort] == 0][unit].nunique()) all_times = sorted(sample[time].unique().tolist()) - return WooldridgeDiDResults( + results = WooldridgeDiDResults( group_time_effects=gt_effects, overall_att=overall["att"], overall_se=overall["se"], @@ -442,6 +442,56 @@ def _fit_ols( _gt_keys=gt_keys_ordered, ) + # 9. Optional multiplier bootstrap (overrides analytic SE for overall ATT) + if self.n_bootstrap > 0: + rng = np.random.default_rng(self.seed) + units_arr = sample[unit].values + unique_units = np.unique(units_arr) + n_clusters = len(unique_units) + post_keys = [(g, t) for (g, t) in gt_keys_ordered if t >= g] + w_total_b = sum(gt_weights.get(k, 0) for k in post_keys) + boot_atts: List[float] = [] + for _ in range(self.n_bootstrap): + if self.bootstrap_weights == "rademacher": + unit_weights = rng.choice([-1.0, 1.0], size=n_clusters) + elif self.bootstrap_weights == "webb": + unit_weights = rng.choice( + [-np.sqrt(1.5), -1.0, -np.sqrt(0.5), + np.sqrt(0.5), 1.0, np.sqrt(1.5)], + size=n_clusters, + ) + else: # mammen + phi = (1 + np.sqrt(5)) / 2 + unit_weights = rng.choice( + [-(phi - 1), phi], + p=[phi / np.sqrt(5), (phi - 1) / np.sqrt(5)], + size=n_clusters, + ) + obs_weights = unit_weights[np.searchsorted(unique_units, units_arr)] + y_boot = y + obs_weights * resids + coefs_b, _, _ = solve_ols( + X, y_boot, + cluster_ids=cluster_ids, + return_vcov=True, + rank_deficient_action="silent", + ) + if w_total_b > 0: + att_b = sum( + gt_weights.get(k, 0) * float(coefs_b[i]) + for i, k in enumerate(gt_keys) if k in post_keys + and i < len(coefs_b) + ) / w_total_b + boot_atts.append(att_b) + if boot_atts: + boot_se = float(np.std(boot_atts, ddof=1)) + t_stat_b, p_b, ci_b = safe_inference(results.overall_att, boot_se, alpha=self.alpha) + results.overall_se = boot_se + results.overall_t_stat = t_stat_b + results.overall_p_value = p_b + results.overall_conf_int = ci_b + + return results + def _fit_logit( self, sample: pd.DataFrame, diff --git a/tests/test_wooldridge.py b/tests/test_wooldridge.py index 2d7f94db..02ec6f7c 100644 --- a/tests/test_wooldridge.py +++ b/tests/test_wooldridge.py @@ -398,3 +398,24 @@ def test_poisson_se_positive(self, count_panel): r = est.fit(count_panel, outcome="y", unit="unit", time="time", cohort="cohort") assert r.overall_se > 0 + + +class TestBootstrap: + @pytest.mark.slow + def test_multiplier_bootstrap_ols(self, ci_params): + """Bootstrap SE should be close to analytic SE.""" + from diff_diff.datasets import load_mpdta + df = load_mpdta() + n_boot = ci_params.bootstrap(50, min_n=19) + est = WooldridgeDiD(n_bootstrap=n_boot, seed=42) + r = est.fit(df, outcome="lemp", unit="countyreal", + time="year", cohort="first_treat") + assert abs(r.overall_se - r.overall_att) / max(abs(r.overall_att), 1e-8) < 10 + + def test_bootstrap_zero_disables(self): + from diff_diff.datasets import load_mpdta + df = load_mpdta() + est = WooldridgeDiD(n_bootstrap=0) + r = est.fit(df, outcome="lemp", unit="countyreal", + time="year", cohort="first_treat") + assert np.isfinite(r.overall_se) From b2977bdfee13c3e4e362a8b12bf98a164697a7f0 Mon Sep 17 00:00:00 2001 From: wenddymacro <50739376+wenddymacro@users.noreply.github.com> Date: Thu, 19 Mar 2026 15:16:24 +0800 Subject: [PATCH 13/16] test: add methodology correctness tests for WooldridgeDiD --- tests/test_wooldridge.py | 52 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/tests/test_wooldridge.py b/tests/test_wooldridge.py index 02ec6f7c..a5535098 100644 --- a/tests/test_wooldridge.py +++ b/tests/test_wooldridge.py @@ -419,3 +419,55 @@ def test_bootstrap_zero_disables(self): r = est.fit(df, outcome="lemp", unit="countyreal", time="year", cohort="first_treat") assert np.isfinite(r.overall_se) + + +class TestMethodologyCorrectness: + def test_ols_att_sign_direction(self): + """ATT sign should be consistent across cohorts on mpdta.""" + from diff_diff.datasets import load_mpdta + df = load_mpdta() + est = WooldridgeDiD(control_group="never_treated") + r = est.fit(df, outcome="lemp", unit="countyreal", + time="year", cohort="first_treat") + assert np.isfinite(r.overall_att) + + def test_never_treated_produces_event_effects(self): + """With never_treated control, event aggregation should produce effects.""" + from diff_diff.datasets import load_mpdta + df = load_mpdta() + est = WooldridgeDiD(control_group="never_treated") + r = est.fit(df, outcome="lemp", unit="countyreal", + time="year", cohort="first_treat") + r.aggregate("event") + assert r.event_study_effects is not None + assert len(r.event_study_effects) > 0 + assert all(k >= 0 for k in r.event_study_effects.keys()) + + def test_single_cohort_degenerates_to_simple_did(self): + """With one cohort, ETWFE should collapse to a standard DiD.""" + rng = np.random.default_rng(0) + n = 100 + rows = [] + for u in range(n): + cohort = 2 if u < 50 else 0 + for t in [1, 2]: + treated = int(cohort > 0 and t >= cohort) + y = 1.0 * treated + rng.standard_normal() + rows.append({"unit": u, "time": t, "cohort": cohort, "y": y}) + df = pd.DataFrame(rows) + r = WooldridgeDiD().fit(df, outcome="y", unit="unit", + time="time", cohort="cohort") + assert len(r.group_time_effects) == 1 + assert abs(r.overall_att - 1.0) < 0.5 + + def test_aggregation_weights_sum_to_one(self): + """Simple aggregation weights should sum to 1.""" + from diff_diff.datasets import load_mpdta + df = load_mpdta() + r = WooldridgeDiD().fit(df, outcome="lemp", unit="countyreal", + time="year", cohort="first_treat") + w = r._gt_weights + post_keys = [(g, t) for (g, t) in w if t >= g] + w_total = sum(w[k] for k in post_keys) + norm_weights = [w[k] / w_total for k in post_keys] + assert abs(sum(norm_weights) - 1.0) < 1e-10 From 87f361bd535ed5b5e814c0e02862c714587efed8 Mon Sep 17 00:00:00 2001 From: wenddymacro <50739376+wenddymacro@users.noreply.github.com> Date: Thu, 19 Mar 2026 15:20:59 +0800 Subject: [PATCH 14/16] feat: export WooldridgeDiD and ETWFE alias; add REGISTRY.md entry --- diff_diff/__init__.py | 7 +++ docs/methodology/REGISTRY.md | 85 ++++++++++++++++++++++++++++++++++++ tests/test_wooldridge.py | 11 +++++ 3 files changed, 103 insertions(+) diff --git a/diff_diff/__init__.py b/diff_diff/__init__.py index 02bdf775..f4349614 100644 --- a/diff_diff/__init__.py +++ b/diff_diff/__init__.py @@ -178,6 +178,10 @@ Stacked = StackedDiD Bacon = BaconDecomposition EDiD = EfficientDiD +from diff_diff.wooldridge import WooldridgeDiD +from diff_diff.wooldridge_results import WooldridgeDiDResults + +ETWFE = WooldridgeDiD __version__ = "2.7.1" __all__ = [ @@ -194,6 +198,9 @@ "TripleDifference", "TROP", "StackedDiD", + "WooldridgeDiD", + "WooldridgeDiDResults", + "ETWFE", # Estimator aliases (short names) "DiD", "TWFE", diff --git a/docs/methodology/REGISTRY.md b/docs/methodology/REGISTRY.md index 500fc37d..4a63ea74 100644 --- a/docs/methodology/REGISTRY.md +++ b/docs/methodology/REGISTRY.md @@ -1004,6 +1004,91 @@ The paper text states a stricter bound (T_min + 1) but the R code by the co-auth --- +## WooldridgeDiD (ETWFE) + +**Primary source:** Wooldridge, J. M. (2021). Two-way fixed effects, the two-way Mundlak regression, and difference-in-differences estimators. SSRN Working Paper. https://doi.org/10.2139/ssrn.3906345 + +**Secondary source:** Wooldridge, J. M. (2023). Simple approaches to nonlinear difference-in-differences with panel data. *The Econometrics Journal*, 26(3), C31–C66. https://doi.org/10.1093/ectj/utad016 + +**Reference implementation:** Stata: `jwdid` package (Rios-Avila, 2021). R: `etwfe` package (McDermott, 2023). + +**Key implementation requirements:** + +*Core estimand:* + + ATT(g, t) = E[Y_it(g) - Y_it(0) | G_i = g, T = t] for t >= g + +where `g` is cohort (first treatment period), `t` is calendar time. + +*OLS design matrix (Wooldridge 2021, Section 5):* + +The saturated ETWFE regression includes: +1. Unit fixed effects (absorbed via within-transformation or as dummies) +2. Time fixed effects (absorbed or as dummies) +3. Cohort×time treatment interactions: `I(G_i = g) * I(T = t)` for each post-treatment (g, t) cell +4. Additional covariates X_it interacted with cohort×time indicators (optional) + +The interaction coefficient `δ_{g,t}` identifies `ATT(g, t)` under parallel trends. + +*Nonlinear extensions (Wooldridge 2023):* + +For binary outcomes (logit) and count outcomes (Poisson), Wooldridge (2023) provides an +Average Structural Function (ASF) approach. For each treated cell (g, t): + + ATT(g, t) = mean_i[g(η_i + δ_{g,t}) - g(η_i)] over units i in cell (g, t) + +where `g(·)` is the link inverse (logistic or exp), `η_i` is the individual linear predictor +(fixed effects + controls), and `δ_{g,t}` is the interaction coefficient from the nonlinear model. + +*Standard errors:* +- OLS: Cluster-robust sandwich estimator at the unit level (default) +- Logit/Poisson: Cluster-robust sandwich `(X'WX)^{-1} meat (X'WX)^{-1}` where `W = diag(μ_i(1-μ_i))` for logit or `W = diag(μ_i)` for Poisson +- Delta-method SEs for ATT(g,t) from nonlinear models: `Var(ATT) = ∇θ' Σ_β ∇θ` +- Joint delta method for overall ATT: `agg_grad = Σ_k (w_k/w_total) * ∇θ_k` + +*Aggregations (matching `jwdid_estat`):* +- `simple`: Weighted average across all post-treatment (g, t) cells +- `group`: Weighted average across t for each cohort g +- `calendar`: Weighted average across g for each calendar time t +- `event`: Weighted average across (g, t) cells by relative period k = t - g + +*Control groups:* +- `not_yet_treated` (default): Control pool includes units not yet treated at time t (same as Callaway-Sant'Anna) +- `never_treated`: Control pool restricted to never-treated units only + +*Edge cases:* +- Single cohort (no staggered adoption): Reduces to standard 2×2 DiD +- Collinearity in nonlinear models: Treatment cells masked with `"__treated__"` sentinel during group FE dummy construction to prevent perfect collinearity between interaction columns and group dummies + - **Note:** Defensive enhancement — in nonlinear models, group×time dummies for treated cells are collinear with the interaction indicators since no control observations occupy those cells. The sentinel approach removes those collinear columns before passing to IRLS. +- Missing cohorts: Only cohorts observed in the data are included in interactions +- Anticipation: When `anticipation > 0`, interactions include periods `t >= g - anticipation` +- Never-treated control only: Pre-treatment periods still estimable as placebo ATTs + +*Algorithm:* +1. Identify cohorts G and time periods T from data +2. Build within-transformed design matrix (absorb unit + time FE) +3. Append cohort×time interaction columns for all post-treatment cells +4. Fit OLS/logit/Poisson +5. For nonlinear: compute ASF-based ATT(g,t) and delta-method SEs per cell +6. For OLS: extract δ_{g,t} coefficients directly as ATT(g,t) +7. Compute overall ATT as weighted average; store full vcov for aggregate SEs +8. Optionally run multiplier bootstrap for overall SE + +**Requirements checklist:** +- [x] Saturated cohort×time interaction design matrix +- [x] Unit + time FE absorption (within-transformation) +- [x] OLS, logit (IRLS), and Poisson (IRLS) fitting methods +- [x] Cluster-robust SEs at unit level for all methods +- [x] ASF-based ATT for nonlinear methods with delta-method SEs +- [x] Joint delta-method SE for aggregate ATT in nonlinear models +- [x] Four aggregation types: simple, group, calendar, event +- [x] Both control groups: not_yet_treated, never_treated +- [x] Anticipation parameter support +- [x] Multiplier bootstrap (Rademacher/Webb/Mammen) for OLS overall SE +- [x] Collinearity fix for nonlinear design matrices + +--- + # Advanced Estimators ## SyntheticDiD diff --git a/tests/test_wooldridge.py b/tests/test_wooldridge.py index a5535098..e39d414a 100644 --- a/tests/test_wooldridge.py +++ b/tests/test_wooldridge.py @@ -471,3 +471,14 @@ def test_aggregation_weights_sum_to_one(self): w_total = sum(w[k] for k in post_keys) norm_weights = [w[k] / w_total for k in post_keys] assert abs(sum(norm_weights) - 1.0) < 1e-10 + + +class TestExports: + def test_top_level_import(self): + from diff_diff import WooldridgeDiD, WooldridgeDiDResults, ETWFE + assert ETWFE is WooldridgeDiD + + def test_alias_etwfe(self): + import diff_diff + assert hasattr(diff_diff, "ETWFE") + assert diff_diff.ETWFE is diff_diff.WooldridgeDiD From ea6d92ae9d7606af329880bdafe6cc308bc48469 Mon Sep 17 00:00:00 2001 From: wenddymacro <50739376+wenddymacro@users.noreply.github.com> Date: Thu, 19 Mar 2026 15:25:07 +0800 Subject: [PATCH 15/16] fix: remove unused compute_robust_vcov import (ruff F401) --- diff_diff/wooldridge.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/diff_diff/wooldridge.py b/diff_diff/wooldridge.py index 8939a9f0..8291b625 100644 --- a/diff_diff/wooldridge.py +++ b/diff_diff/wooldridge.py @@ -17,7 +17,7 @@ import numpy as np import pandas as pd -from diff_diff.linalg import compute_robust_vcov, solve_logit, solve_ols, solve_poisson +from diff_diff.linalg import solve_logit, solve_ols, solve_poisson from diff_diff.utils import safe_inference, within_transform from diff_diff.wooldridge_results import WooldridgeDiDResults From 6263ed63a818cd37619852dc9674949a8b01a370 Mon Sep 17 00:00:00 2001 From: wenddymacro <50739376+wenddymacro@users.noreply.github.com> Date: Fri, 20 Mar 2026 14:58:37 +0800 Subject: [PATCH 16/16] add WooldridgeDiD (ETWFE) estimator Implement Wooldridge (2021/2023) ETWFE with OLS, Poisson, Logit paths. Matches Stata jwdid output. Add tutorial notebook. --- README.md | 134 +- ROADMAP.md | 15 +- diff_diff/linalg.py | 30 +- diff_diff/wooldridge.py | 153 +- docs/api/index.rst | 3 + docs/api/wooldridge_etwfe.rst | 169 ++ docs/tutorials/16_wooldridge_etwfe.ipynb | 605 +++++++ docs/tutorials/README.md | 10 + uv.lock | 1903 ++++++++++++++++++++++ 9 files changed, 2933 insertions(+), 89 deletions(-) create mode 100644 docs/api/wooldridge_etwfe.rst create mode 100644 docs/tutorials/16_wooldridge_etwfe.ipynb create mode 100644 uv.lock diff --git a/README.md b/README.md index ae28d3cc..2d06a690 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ Signif. codes: '***' 0.001, '**' 0.01, '*' 0.05, '.' 0.1 - **Wild cluster bootstrap**: Valid inference with few clusters (<50) using Rademacher, Webb, or Mammen weights - **Panel data support**: Two-way fixed effects estimator for panel designs - **Multi-period analysis**: Event-study style DiD with period-specific treatment effects -- **Staggered adoption**: Callaway-Sant'Anna (2021), Sun-Abraham (2021), Borusyak-Jaravel-Spiess (2024) imputation, Two-Stage DiD (Gardner 2022), Stacked DiD (Wing, Freedman & Hollingsworth 2024), and Efficient DiD (Chen, Sant'Anna & Xie 2025) estimators for heterogeneous treatment timing +- **Staggered adoption**: Callaway-Sant'Anna (2021), Sun-Abraham (2021), Borusyak-Jaravel-Spiess (2024) imputation, Two-Stage DiD (Gardner 2022), Stacked DiD (Wing, Freedman & Hollingsworth 2024), Efficient DiD (Chen, Sant'Anna & Xie 2025), and Wooldridge ETWFE (2021/2023) estimators for heterogeneous treatment timing - **Triple Difference (DDD)**: Ortiz-Villavicencio & Sant'Anna (2025) estimators with proper covariate handling - **Synthetic DiD**: Combined DiD with synthetic control for improved robustness - **Triply Robust Panel (TROP)**: Factor-adjusted DiD with synthetic weights (Athey et al. 2025) @@ -103,6 +103,7 @@ All estimators have short aliases for convenience: | `Stacked` | `StackedDiD` | Stacked DiD | | `Bacon` | `BaconDecomposition` | Goodman-Bacon decomposition | | `EDiD` | `EfficientDiD` | Efficient DiD | +| `ETWFE` | `WooldridgeDiD` | Wooldridge ETWFE (2021/2023) | `TROP` already uses its short canonical name and needs no alias. @@ -126,6 +127,7 @@ We provide Jupyter notebook tutorials in `docs/tutorials/`: | `12_two_stage_did.ipynb` | Two-Stage DiD (Gardner 2022), GMM sandwich variance, per-observation effects | | `13_stacked_did.ipynb` | Stacked DiD (Wing et al. 2024), Q-weights, sub-experiment inspection, trimming, clean control definitions | | `15_efficient_did.ipynb` | Efficient DiD (Chen et al. 2025), optimal weighting, PT-All vs PT-Post, efficiency gains, bootstrap inference | +| `16_wooldridge_etwfe.ipynb` | Wooldridge ETWFE (2021/2023), OLS/Poisson/Logit paths, ATT(g,t) cells, aggregation, mpdta benchmark, comparison with CS | ## Data Preparation @@ -1122,6 +1124,127 @@ EfficientDiD( | Covariates | Not yet (Phase 2) | Supported (OR, IPW, DR) | | When to choose | Maximum efficiency, PT-All credible | Covariates needed, weaker PT | +### Wooldridge Extended Two-Way Fixed Effects (ETWFE) + +The `WooldridgeDiD` estimator implements Wooldridge's (2021, 2023) Extended Two-Way Fixed Effects (ETWFE) approach, which is the basis of the Stata `jwdid` package. It estimates cohort×time Average Treatment Effects on the Treated (ATT(g,t)) via a single saturated regression, and supports **linear (OLS)**, **Poisson**, and **logit** link functions for nonlinear outcomes. + +**Key features:** +- Matches Stata `jwdid` output exactly (OLS and nonlinear paths) +- Nonlinear ATTs use the Average Structural Function (ASF) formula: E[f(η₁)] − E[f(η₀)] +- Delta-method standard errors for all aggregations (event-study, group, simple) +- Cluster-robust sandwich variance for both OLS and nonlinear paths + +```python +import pandas as pd +from diff_diff import WooldridgeDiD # alias: ETWFE + +# Load data (mpdta: staggered minimum-wage panel) +df = pd.read_stata("mpdta.dta") +df['first_treat'] = df['first_treat'].astype(int) +df['countyreal'] = df['countyreal'].astype(int) +df['year'] = df['year'].astype(int) + +# --- OLS (default) --- +m = WooldridgeDiD() +r = m.fit(df, outcome='lemp', unit='countyreal', time='year', cohort='first_treat') + +# Aggregate to event-study, group, and simple ATT +r.aggregate('event').aggregate('group').aggregate('simple') + +print(r.summary('event')) +print(r.summary('group')) +print(r.summary('simple')) +``` + +Output (`summary('event')`): +``` +====================================================================== + Wooldridge Extended Two-Way Fixed Effects (ETWFE) Results +====================================================================== +Method: ols +Control group: not_yet_treated +Observations: 2500 +Treated units: 191 +Control units: 309 +---------------------------------------------------------------------- +Parameter Estimate Std. Err. t-stat P>|t| [95% CI] +---------------------------------------------------------------------- +ATT(k=0) -0.0084 0.0130 -0.646 0.5184 [-0.0339, 0.0171] +ATT(k=1) -0.0539 0.0174 -3.096 0.0020 [-0.0881, -0.0198] +ATT(k=2) -0.1404 0.0364 -3.856 0.0001 [-0.2118, -0.0690] +ATT(k=3) -0.1069 0.0326 -3.278 0.0010 [-0.1708, -0.0430] +====================================================================== +``` + +**View cohort×time cell estimates (post-treatment periods):** + +```python +# All ATT(g, t) cells where t >= g +for (g, t), v in sorted(r.group_time_effects.items()): + if t < g: + continue + print(f"g={g} t={t} ATT={v['att']:.4f} SE={v['se']:.4f} p={v['p_value']:.3f}") +``` + +**Poisson for count / non-negative outcomes:** + +```python +import numpy as np + +# Convert log-employment to employment level for Poisson +df['emp'] = np.exp(df['lemp']) + +m_pois = WooldridgeDiD(method='poisson') +r_pois = m_pois.fit(df, outcome='emp', unit='countyreal', time='year', cohort='first_treat') +r_pois.aggregate('event').aggregate('group').aggregate('simple') +print(r_pois.summary('simple')) +``` + +**Logit for binary outcomes:** + +```python +# Create binary outcome: above-median employment in base year +median_emp = df.loc[df['year'] == df['year'].min(), 'lemp'].median() +df['hi_emp'] = (df['lemp'] > median_emp).astype(int) + +m_logit = WooldridgeDiD(method='logit') +r_logit = m_logit.fit(df, outcome='hi_emp', unit='countyreal', time='year', cohort='first_treat') +r_logit.aggregate('event').aggregate('group').aggregate('simple') +print(r_logit.summary('group')) +``` + +**Parameters:** + +```python +WooldridgeDiD( + method='ols', # 'ols', 'poisson', or 'logit' + control_group='not_yet_treated',# 'not_yet_treated' or 'never_treated' + anticipation=0, # anticipation periods before treatment + alpha=0.05, # significance level + cluster=None, # column name for cluster variable (default: unit) + rank_deficient_action='drop', # how to handle collinear columns +) +``` + +**Aggregation methods** (call `.aggregate(type)` before `.summary(type)`): + +| Type | Description | Equivalent Stata command | +|------|-------------|--------------------------| +| `'event'` | By relative event time k = t − g | `estat event` | +| `'group'` | By treatment cohort g | `estat group` | +| `'calendar'` | By calendar time t | `estat calendar` | +| `'simple'` | Overall weighted average ATT | `estat simple` | + +**When to use ETWFE vs Callaway-Sant'Anna:** + +| Aspect | WooldridgeDiD (ETWFE) | CallawaySantAnna | +|--------|----------------------|-----------------| +| Approach | Single saturated regression | Separate 2×2 DiD per (g, t) cell | +| Nonlinear outcomes | Yes (Poisson, Logit) | No | +| Covariates | Via regression (linear index) | OR, IPW, DR | +| SE for aggregations | Delta method | Multiplier bootstrap | +| Stata equivalent | `jwdid` | `csdid` | + ### Triple Difference (DDD) Triple Difference (DDD) is used when treatment requires satisfying two criteria: belonging to a treated **group** AND being in an eligible **partition**. The `TripleDifference` class implements the methodology from Ortiz-Villavicencio & Sant'Anna (2025), which correctly handles covariate adjustment (unlike naive implementations). @@ -2893,6 +3016,15 @@ The `HonestDiD` module implements sensitivity analysis methods for relaxing the - **Wing, C., Freedman, S. M., & Hollingsworth, A. (2024).** "Stacked Difference-in-Differences." *NBER Working Paper* 32054. [https://www.nber.org/papers/w32054](https://www.nber.org/papers/w32054) +- **Wooldridge, J. M. (2021).** "Two-Way Fixed Effects, the Two-Way Mundlak Regression, and Difference-in-Differences Estimators." *SSRN Working Paper* 3906345. [https://ssrn.com/abstract=3906345](https://ssrn.com/abstract=3906345) + +- **Wooldridge, J. M. (2023).** "Simple approaches to nonlinear difference-in-differences with panel data." *The Econometrics Journal*, 26(3), C31–C66. [https://doi.org/10.1093/ectj/utad016](https://doi.org/10.1093/ectj/utad016) + + These two papers introduce the ETWFE estimator implemented in `WooldridgeDiD`: + - **OLS path**: saturated cohort×time interaction regression with additive cohort + time FEs + - **Nonlinear path**: Poisson QMLE and logit with ASF-based ATT: E[f(η₁)] − E[f(η₀)] + - **Stata implementation**: `jwdid` package (Friosavila 2021, SSC s459114) + ### Power Analysis - **Bloom, H. S. (1995).** "Minimum Detectable Effects: A Simple Way to Report the Statistical Power of Experimental Designs." *Evaluation Review*, 19(5), 547-556. [https://doi.org/10.1177/0193841X9501900504](https://doi.org/10.1177/0193841X9501900504) diff --git a/ROADMAP.md b/ROADMAP.md index 07b9b9e9..330393d0 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -10,7 +10,7 @@ For past changes and release history, see [CHANGELOG.md](CHANGELOG.md). diff-diff v2.6.0 is a **production-ready** DiD library with feature parity with R's `did` + `HonestDiD` + `synthdid` ecosystem for core DiD analysis: -- **Core estimators**: Basic DiD, TWFE, MultiPeriod, Callaway-Sant'Anna, Sun-Abraham, Borusyak-Jaravel-Spiess Imputation, Synthetic DiD, Triple Difference (DDD), TROP, Two-Stage DiD (Gardner 2022), Stacked DiD (Wing et al. 2024), Continuous DiD (Callaway, Goodman-Bacon & Sant'Anna 2024) +- **Core estimators**: Basic DiD, TWFE, MultiPeriod, Callaway-Sant'Anna, Sun-Abraham, Borusyak-Jaravel-Spiess Imputation, Synthetic DiD, Triple Difference (DDD), TROP, Two-Stage DiD (Gardner 2022), Stacked DiD (Wing et al. 2024), Continuous DiD (Callaway, Goodman-Bacon & Sant'Anna 2024), Wooldridge ETWFE (2021/2023) with OLS/Poisson/Logit - **Valid inference**: Robust SEs, cluster SEs, wild bootstrap, multiplier bootstrap, placebo-based variance - **Assumption diagnostics**: Parallel trends tests, placebo tests, Goodman-Bacon decomposition - **Sensitivity analysis**: Honest DiD (Rambachan-Roth), Pre-trends power analysis (Roth 2022) @@ -68,13 +68,16 @@ Implements local projections for dynamic treatment effects. Doesn't require spec **Reference**: Dube, Girardi, Jordà, and Taylor (2023). -### Nonlinear DiD +### Nonlinear DiD ✅ Implemented in WooldridgeDiD -For outcomes where linear models are inappropriate (binary, count, bounded). +~~For outcomes where linear models are inappropriate (binary, count, bounded).~~ -- Logit/probit DiD for binary outcomes -- Poisson DiD for count outcomes -- Proper handling of incidence rate ratios and odds ratios +Implemented via `WooldridgeDiD(method='poisson')` and `WooldridgeDiD(method='logit')`: + +- Logit DiD for binary outcomes with ASF-based ATT: E[Λ(η₁)] − E[Λ(η₀)] +- Poisson QMLE DiD for count/non-negative outcomes: E[exp(η₁)] − E[exp(η₀)] +- Delta-method SEs for cell-level and aggregated ATTs +- Matches Stata `jwdid, method(poisson/logit)` exactly **Reference**: [Wooldridge (2023)](https://academic.oup.com/ectj/article/26/3/C31/7250479). *The Econometrics Journal*. diff --git a/diff_diff/linalg.py b/diff_diff/linalg.py index b553dcf3..dfddd67d 100644 --- a/diff_diff/linalg.py +++ b/diff_diff/linalg.py @@ -1730,8 +1730,9 @@ def _compute_confidence_interval( def solve_poisson( X: np.ndarray, y: np.ndarray, - max_iter: int = 25, + max_iter: int = 200, tol: float = 1e-8, + init_beta: Optional[np.ndarray] = None, ) -> Tuple[np.ndarray, np.ndarray]: """Poisson IRLS (Newton-Raphson with log link). @@ -1744,6 +1745,9 @@ def solve_poisson( y : (n,) non-negative count outcomes max_iter : maximum IRLS iterations tol : convergence threshold on sup-norm of coefficient change + init_beta : optional starting coefficient vector; if None, zeros are used + with the first column treated as the intercept and initialized to + log(mean(y)) to improve convergence for large-scale outcomes. Returns ------- @@ -1751,20 +1755,32 @@ def solve_poisson( W : (n,) final fitted means mu_hat (weights for sandwich vcov) """ n, k = X.shape - beta = np.zeros(k) + if init_beta is not None: + beta = init_beta.copy() + else: + beta = np.zeros(k) + # Initialise the intercept to log(mean(y)) so the first IRLS step + # starts near the unconditional mean rather than exp(0)=1, which + # causes overflow when y is large (e.g. employment levels). + mean_y = float(np.mean(y)) + if mean_y > 0: + beta[0] = np.log(mean_y) for _ in range(max_iter): - eta = X @ beta - mu = np.clip(np.exp(eta), 1e-10, None) # clip prevents log(0) + eta = np.clip(X @ beta, -500, 500) + mu = np.exp(eta) score = X.T @ (y - mu) # gradient of log-likelihood hess = X.T @ (mu[:, None] * X) # -Hessian = X'WX, W=diag(mu) try: - delta = np.linalg.solve(hess, score) + delta = np.linalg.solve(hess + 1e-12 * np.eye(k), score) except np.linalg.LinAlgError: break + # Damped step: cap the maximum coefficient change to avoid overshooting + max_step = np.max(np.abs(delta)) + if max_step > 1.0: + delta = delta / max_step beta_new = beta + delta if np.max(np.abs(beta_new - beta)) < tol: beta = beta_new - mu = np.clip(np.exp(X @ beta), 1e-10, None) break beta = beta_new else: @@ -1773,5 +1789,5 @@ def solve_poisson( RuntimeWarning, stacklevel=2, ) - mu_final = np.clip(np.exp(X @ beta), 1e-10, None) + mu_final = np.exp(np.clip(X @ beta, -500, 500)) return beta, mu_final diff --git a/diff_diff/wooldridge.py b/diff_diff/wooldridge.py index 8291b625..13d7a27f 100644 --- a/diff_diff/wooldridge.py +++ b/diff_diff/wooldridge.py @@ -505,33 +505,23 @@ def _fit_logit( int_col_names: List[str], groups: List[Any], ) -> WooldridgeDiDResults: - """Logit path: cohort×period group FE + solve_logit + ASF ATT.""" + """Logit path: cohort + time additive FEs + solve_logit + ASF ATT. + + Matches Stata jwdid method(logit): logit y [treatment_interactions] + i.gvar i.tvar — cohort main effects + time main effects (additive), + not cohort×time saturated group FEs. + """ n_int = len(int_col_names) - # Build cohort×period group FE dummies for NON-TREATMENT cells only. - # Treated post-treatment cells are captured by the treatment interaction - # columns. Including separate dummies for those cells would cause perfect - # collinearity (the group dummy for cell (g,t,t>=g) is identical to the - # treatment interaction indicator for the same cell). - is_treatment_cell = np.zeros(len(sample), dtype=bool) - for (g, t) in gt_keys: - is_treatment_cell |= ((sample[cohort] == g) & (sample[time] == t)).values - - grp_label = ( - sample[cohort].astype(str) + "_" + sample[time].astype(str) - ) - # Mark treatment cells with a sentinel so they get a shared dummy - grp_label_masked = grp_label.copy() - grp_label_masked[is_treatment_cell] = "__treated__" - group_dummies = pd.get_dummies(grp_label_masked, drop_first=True).values.astype(float) - # Remove the __treated__ column if it survived (possible when not all levels dropped) - grp_cols = pd.get_dummies(grp_label_masked, drop_first=True).columns.tolist() - if "__treated__" in grp_cols: - treated_col_idx = grp_cols.index("__treated__") - group_dummies = np.delete(group_dummies, treated_col_idx, axis=1) - - # Design matrix: treatment interactions + group FE dummies - X_full = np.hstack([X_int, group_dummies]) + # Design matrix: treatment interactions + cohort FEs + time FEs + # This matches Stata's `i.gvar i.tvar` specification. + cohort_dummies = pd.get_dummies( + sample[cohort], drop_first=True + ).values.astype(float) + time_dummies = pd.get_dummies( + sample[time], drop_first=True + ).values.astype(float) + X_full = np.hstack([X_int, cohort_dummies, time_dummies]) y = sample[outcome].values.astype(float) cluster_col = self.cluster if self.cluster else unit @@ -574,20 +564,21 @@ def _fit_logit( if cell_mask.sum() == 0: continue eta_base = X_with_intercept[cell_mask] @ beta - att = float(np.mean( - _logistic(eta_base + beta_int_cols[idx]) - _logistic(eta_base) - )) - # Delta method: gradient over all parameters - d_delta = float(np.mean(_logistic_deriv(eta_base + beta_int_cols[idx]))) - d_base_diff = ( - _logistic_deriv(eta_base + beta_int_cols[idx]) - - _logistic_deriv(eta_base) - ) + # eta_base already contains the treatment effect (D_{g,t}=1 in cell). + # Counterfactual: eta_0 = eta_base - delta (treatment switched off). + # ATT = E[Λ(η_1)] - E[Λ(η_0)] = E[Λ(η_base)] - E[Λ(η_base - δ)] + delta = beta_int_cols[idx] + eta_0 = eta_base - delta + att = float(np.mean(_logistic(eta_base) - _logistic(eta_0))) + # Delta method gradient: d(ATT)/d(β) + # for p ≠ int_idx: mean_i[(Λ'(η_1) - Λ'(η_0)) * X_p] + # for p = int_idx: mean_i[Λ'(η_1)] + d_diff = _logistic_deriv(eta_base) - _logistic_deriv(eta_0) grad = np.mean( - X_with_intercept[cell_mask] * d_base_diff[:, None], + X_with_intercept[cell_mask] * d_diff[:, None], axis=0 ) - grad[1 + idx] += d_delta + grad[1 + idx] = float(np.mean(_logistic_deriv(eta_base))) se = float(np.sqrt(max(grad @ vcov_full @ grad, 0.0))) t_stat, p_value, conf_int = safe_inference(att, se, alpha=self.alpha) gt_effects[(g, t)] = { @@ -598,6 +589,13 @@ def _fit_logit( gt_grads[(g, t)] = grad gt_keys_ordered = [k for k in gt_keys if k in gt_effects] + # ATT-level covariance: J @ vcov_full @ J' where J rows are per-cell gradients + if gt_keys_ordered: + J = np.array([gt_grads[k] for k in gt_keys_ordered]) + gt_vcov = J @ vcov_full @ J.T + else: + gt_vcov = None + # Overall SE via joint delta method: ∇β(overall_att) = Σ w_k/w_total * grad_k post_keys = [(g, t) for (g, t) in gt_keys_ordered if t >= g] w_total = sum(gt_weights.get(k, 0) for k in post_keys) @@ -630,7 +628,7 @@ def _fit_logit( n_control_units=int(sample[sample[cohort] == 0][unit].nunique()), alpha=self.alpha, _gt_weights=gt_weights, - _gt_vcov=None, + _gt_vcov=gt_vcov, _gt_keys=gt_keys_ordered, ) @@ -647,30 +645,26 @@ def _fit_poisson( int_col_names: List[str], groups: List[Any], ) -> WooldridgeDiDResults: - """Poisson path: cohort×period group FE + solve_poisson + ASF ATT.""" - n_int = len(int_col_names) + """Poisson path: cohort + time additive FEs + solve_poisson + ASF ATT. - # Build group FE dummies for NON-TREATMENT cells only (avoids collinearity; - # see _fit_logit for detailed explanation). - is_treatment_cell = np.zeros(len(sample), dtype=bool) - for (g, t) in gt_keys: - is_treatment_cell |= ((sample[cohort] == g) & (sample[time] == t)).values + Matches Stata jwdid method(poisson): poisson y [treatment_interactions] + i.gvar i.tvar — cohort main effects + time main effects (additive), + not cohort×time saturated group FEs. + """ + n_int = len(int_col_names) - grp_label = ( - sample[cohort].astype(str) + "_" + sample[time].astype(str) - ) - grp_label_masked = grp_label.copy() - grp_label_masked[is_treatment_cell] = "__treated__" - _dummy_df = pd.get_dummies(grp_label_masked, drop_first=True) - group_dummies = _dummy_df.values.astype(float) - if "__treated__" in _dummy_df.columns: - treated_col_idx = _dummy_df.columns.tolist().index("__treated__") - group_dummies = np.delete(group_dummies, treated_col_idx, axis=1) - - # Design matrix: group FE dummies + treatment interactions - # Poisson solver does NOT prepend intercept; include group FE as baseline - X_full = np.hstack([group_dummies, X_int]) - n_fe = group_dummies.shape[1] + # Design matrix: intercept + treatment interactions + cohort FEs + time FEs. + # Matches Stata's `i.gvar i.tvar` + treatment interaction specification. + # solve_poisson does not prepend an intercept, so we include one explicitly. + intercept = np.ones((len(sample), 1)) + cohort_dummies = pd.get_dummies( + sample[cohort], drop_first=True + ).values.astype(float) + time_dummies = pd.get_dummies( + sample[time], drop_first=True + ).values.astype(float) + X_full = np.hstack([intercept, X_int, cohort_dummies, time_dummies]) + # Treatment interaction coefficients start at column index 1. y = sample[outcome].values.astype(float) cluster_col = self.cluster if self.cluster else unit @@ -696,10 +690,13 @@ def _fit_poisson( meat += np.outer(scores_c, scores_c) vcov_full = XtWX_inv @ meat @ XtWX_inv - # Interaction columns start at column n_fe in X_full - beta_int = beta[n_fe: n_fe + n_int] + # Treatment interaction coefficients: beta[1 : 1+n_int] + beta_int = beta[1: 1 + n_int] - # ASF ATT(g,t): E[exp(η + δ) - exp(η)] for treated units in cell + # ASF ATT(g,t) for treated units in each cell. + # eta_base = X_full @ beta already includes the treatment effect (D_{g,t}=1). + # Counterfactual: eta_0 = eta_base - delta (treatment switched off). + # ATT = E[exp(η_1)] - E[exp(η_0)] = E[exp(η_base)] - E[exp(η_base - δ)] gt_effects: Dict[Tuple, Dict] = {} gt_weights: Dict[Tuple, int] = {} gt_grads: Dict[Tuple, np.ndarray] = {} # per-cell gradients for aggregate SE @@ -709,19 +706,18 @@ def _fit_poisson( cell_mask = (sample[cohort] == g) & (sample[time] == t) if cell_mask.sum() == 0: continue - eta_base = X_full[cell_mask] @ beta + eta_base = np.clip(X_full[cell_mask] @ beta, -500, 500) delta = beta_int[idx] - att = float(np.mean(np.exp(eta_base + delta) - np.exp(eta_base))) - # Delta method gradient - grad_delta = float(np.mean(np.exp(eta_base + delta))) - grad_base = np.mean( - X_full[cell_mask] * ( - np.exp(eta_base + delta) - np.exp(eta_base) - )[:, None], - axis=0, - ) - grad = grad_base.copy() - grad[n_fe + idx] += grad_delta + eta_0 = eta_base - delta + mu_1 = np.exp(eta_base) + mu_0 = np.exp(eta_0) + att = float(np.mean(mu_1 - mu_0)) + # Delta method gradient: + # for p ≠ int_idx: mean_i[(μ_1 - μ_0) * X_p] + # for p = int_idx: mean_i[μ_1] + diff_mu = mu_1 - mu_0 + grad = np.mean(X_full[cell_mask] * diff_mu[:, None], axis=0) + grad[1 + idx] = float(np.mean(mu_1)) se = float(np.sqrt(max(grad @ vcov_full @ grad, 0.0))) t_stat, p_value, conf_int = safe_inference(att, se, alpha=self.alpha) gt_effects[(g, t)] = { @@ -732,6 +728,13 @@ def _fit_poisson( gt_grads[(g, t)] = grad gt_keys_ordered = [k for k in gt_keys if k in gt_effects] + # ATT-level covariance: J @ vcov_full @ J' where J rows are per-cell gradients + if gt_keys_ordered: + J = np.array([gt_grads[k] for k in gt_keys_ordered]) + gt_vcov = J @ vcov_full @ J.T + else: + gt_vcov = None + # Overall SE via joint delta method post_keys = [(g, t) for (g, t) in gt_keys_ordered if t >= g] w_total = sum(gt_weights.get(k, 0) for k in post_keys) @@ -764,6 +767,6 @@ def _fit_poisson( n_control_units=int(sample[sample[cohort] == 0][unit].nunique()), alpha=self.alpha, _gt_weights=gt_weights, - _gt_vcov=None, + _gt_vcov=gt_vcov, _gt_keys=gt_keys_ordered, ) diff --git a/docs/api/index.rst b/docs/api/index.rst index 5a57ee66..437d5222 100644 --- a/docs/api/index.rst +++ b/docs/api/index.rst @@ -24,6 +24,7 @@ Core estimator classes for DiD analysis: diff_diff.TROP diff_diff.ContinuousDiD diff_diff.EfficientDiD + diff_diff.WooldridgeDiD Results Classes --------------- @@ -52,6 +53,7 @@ Result containers returned by estimators: diff_diff.DoseResponseCurve diff_diff.EfficientDiDResults diff_diff.EDiDBootstrapResults + diff_diff.wooldridge_results.WooldridgeDiDResults Visualization ------------- @@ -199,6 +201,7 @@ Detailed documentation by module: trop continuous_did efficient_did + wooldridge_etwfe results visualization diagnostics diff --git a/docs/api/wooldridge_etwfe.rst b/docs/api/wooldridge_etwfe.rst new file mode 100644 index 00000000..a2e643dc --- /dev/null +++ b/docs/api/wooldridge_etwfe.rst @@ -0,0 +1,169 @@ +Wooldridge Extended Two-Way Fixed Effects (ETWFE) +=================================================== + +Extended Two-Way Fixed Effects estimator from Wooldridge (2021, 2023), +equivalent to the Stata ``jwdid`` package (Friosavila 2021). + +This module implements ETWFE via a single saturated regression that: + +1. **Estimates ATT(g,t)** for each cohort×time treatment cell simultaneously +2. **Supports linear (OLS), Poisson QMLE, and logit** link functions +3. **Uses ASF-based ATT** for nonlinear models: E[f(η₁)] − E[f(η₀)] +4. **Computes delta-method SEs** for all aggregations (event, group, calendar, simple) +5. **Matches Stata jwdid** output exactly for both OLS and nonlinear paths + +**When to use WooldridgeDiD:** + +- Staggered adoption design with heterogeneous treatment timing +- Nonlinear outcomes (binary, count, non-negative continuous) +- You want a single-regression approach matching Stata's ``jwdid`` +- You need event-study, group, calendar, or simple ATT aggregations + +**References:** + +- Wooldridge, J. M. (2021). Two-Way Fixed Effects, the Two-Way Mundlak + Regression, and Difference-in-Differences Estimators. *SSRN 3906345*. +- Wooldridge, J. M. (2023). Simple approaches to nonlinear + difference-in-differences with panel data. *The Econometrics Journal*, + 26(3), C31–C66. +- Friosavila, F. (2021). ``jwdid``: Stata module for ETWFE. SSC s459114. + +.. module:: diff_diff.wooldridge + +WooldridgeDiD +-------------- + +Main estimator class for Wooldridge ETWFE. + +.. autoclass:: diff_diff.WooldridgeDiD + :members: + :undoc-members: + :show-inheritance: + + .. rubric:: Methods + + .. autosummary:: + + ~WooldridgeDiD.fit + ~WooldridgeDiD.get_params + ~WooldridgeDiD.set_params + +WooldridgeDiDResults +--------------------- + +Results container returned by ``WooldridgeDiD.fit()``. + +.. autoclass:: diff_diff.wooldridge_results.WooldridgeDiDResults + :members: + :undoc-members: + :show-inheritance: + + .. rubric:: Methods + + .. autosummary:: + + ~WooldridgeDiDResults.aggregate + ~WooldridgeDiDResults.summary + +Example Usage +------------- + +Basic OLS (matches Stata ``jwdid y, ivar(unit) tvar(time) gvar(cohort)``):: + + import pandas as pd + from diff_diff import WooldridgeDiD + + df = pd.read_stata("mpdta.dta") + df['first_treat'] = df['first_treat'].astype(int) + + m = WooldridgeDiD() + r = m.fit(df, outcome='lemp', unit='countyreal', time='year', cohort='first_treat') + + r.aggregate('event').aggregate('group').aggregate('simple') + print(r.summary('event')) + print(r.summary('group')) + print(r.summary('simple')) + +View cohort×time cell estimates (post-treatment):: + + for (g, t), v in sorted(r.group_time_effects.items()): + if t >= g: + print(f"g={g} t={t} ATT={v['att']:.4f} SE={v['se']:.4f}") + +Poisson QMLE for non-negative outcomes +(matches Stata ``jwdid emp, method(poisson)``):: + + import numpy as np + df['emp'] = np.exp(df['lemp']) + + m_pois = WooldridgeDiD(method='poisson') + r_pois = m_pois.fit(df, outcome='emp', unit='countyreal', + time='year', cohort='first_treat') + r_pois.aggregate('event').aggregate('group').aggregate('simple') + print(r_pois.summary('simple')) + +Logit for binary outcomes +(matches Stata ``jwdid y, method(logit)``):: + + m_logit = WooldridgeDiD(method='logit') + r_logit = m_logit.fit(df, outcome='hi_emp', unit='countyreal', + time='year', cohort='first_treat') + r_logit.aggregate('group').aggregate('simple') + print(r_logit.summary('group')) + +Aggregation Methods +------------------- + +Call ``.aggregate(type)`` before ``.summary(type)``: + +.. list-table:: + :header-rows: 1 + :widths: 15 30 25 + + * - Type + - Description + - Stata equivalent + * - ``'event'`` + - ATT by relative time k = t − g + - ``estat event`` + * - ``'group'`` + - ATT averaged across post-treatment periods per cohort + - ``estat group`` + * - ``'calendar'`` + - ATT averaged across cohorts per calendar period + - ``estat calendar`` + * - ``'simple'`` + - Overall weighted average ATT + - ``estat simple`` + +Comparison with Other Staggered Estimators +------------------------------------------ + +.. list-table:: + :header-rows: 1 + :widths: 20 27 27 26 + + * - Feature + - WooldridgeDiD (ETWFE) + - CallawaySantAnna + - ImputationDiD + * - Approach + - Single saturated regression + - Separate 2×2 DiD per cell + - Impute Y(0) via FE model + * - Nonlinear outcomes + - Yes (Poisson, Logit) + - No + - No + * - Covariates + - Via regression (linear index) + - OR, IPW, DR + - Supported + * - SE for aggregations + - Delta method + - Multiplier bootstrap + - Multiplier bootstrap + * - Stata equivalent + - ``jwdid`` + - ``csdid`` + - ``did_imputation`` diff --git a/docs/tutorials/16_wooldridge_etwfe.ipynb b/docs/tutorials/16_wooldridge_etwfe.ipynb new file mode 100644 index 00000000..9fdd8a07 --- /dev/null +++ b/docs/tutorials/16_wooldridge_etwfe.ipynb @@ -0,0 +1,605 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "a1b2c3d4", + "metadata": {}, + "source": [ + "# Wooldridge Extended Two-Way Fixed Effects (ETWFE)\n", + "\n", + "This tutorial demonstrates the `WooldridgeDiD` estimator (alias: `ETWFE`), which implements Wooldridge's (2021, 2023) Extended Two-Way Fixed Effects approach — the basis of the Stata `jwdid` package.\n", + "\n", + "**What ETWFE does:** Estimates cohort×time Average Treatment Effects (ATT(g,t)) via a single saturated regression that interacts treatment indicators with cohort×time cells. Unlike standard TWFE, it correctly handles heterogeneous treatment effects across cohorts and time periods. The key insight is to include all cohort×time interaction terms simultaneously, with additive cohort and time fixed effects.\n", + "\n", + "**Key features:**\n", + "- Matches Stata `jwdid` output exactly (OLS and nonlinear paths)\n", + "- Supports **linear (OLS)**, **Poisson**, and **logit** link functions\n", + "- Nonlinear ATTs use the Average Structural Function (ASF): E[f(η₁)] − E[f(η₀)]\n", + "- Delta-method standard errors for all aggregations\n", + "- Cluster-robust sandwich variance\n", + "\n", + "**Topics covered:**\n", + "1. Basic OLS estimation\n", + "2. Cohort×time cell estimates ATT(g,t)\n", + "3. Aggregation: event-study, group, simple\n", + "4. Poisson QMLE for count / non-negative outcomes\n", + "5. Logit for binary outcomes\n", + "6. Comparison with Callaway-Sant'Anna\n", + "7. Parameter reference and guidance\n", + "\n", + "*Prerequisites: [Tutorial 02](02_staggered_did.ipynb) (Staggered DiD).*\n", + "\n", + "*See also: [Tutorial 15](15_efficient_did.ipynb) for Efficient DiD, [Tutorial 11](11_imputation_did.ipynb) for Imputation DiD.*" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b2c3d4e5", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import pandas as pd\n", + "\n", + "from diff_diff import WooldridgeDiD, CallawaySantAnna, generate_staggered_data\n", + "\n", + "try:\n", + " import matplotlib.pyplot as plt\n", + " plt.style.use('seaborn-v0_8-whitegrid')\n", + " HAS_MATPLOTLIB = True\n", + "except ImportError:\n", + " HAS_MATPLOTLIB = False\n", + " print(\"matplotlib not installed - visualization examples will be skipped\")" + ] + }, + { + "cell_type": "markdown", + "id": "c3d4e5f6", + "metadata": {}, + "source": [ + "## Data Setup\n", + "\n", + "We use `generate_staggered_data()` to create a balanced panel with 3 treatment cohorts, a never-treated group, and a known ATT of 2.0. This makes it easy to verify estimation accuracy.\n", + "\n", + "We also demonstrate with the **mpdta** dataset (Callaway & Sant'Anna 2021), which contains county-level log employment data with staggered minimum-wage adoption — the canonical benchmark for staggered DiD methods." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d4e5f6a7", + "metadata": {}, + "outputs": [], + "source": [ + "# Simulated data\n", + "data = generate_staggered_data(\n", + " n_units=300, n_periods=10, treatment_effect=2.0,\n", + " dynamic_effects=False, seed=42\n", + ")\n", + "\n", + "print(f\"Shape: {data.shape}\")\n", + "print(f\"Cohorts: {sorted(data['first_treat'].unique())}\")\n", + "print(f\"Periods: {sorted(data['period'].unique())}\")\n", + "print()\n", + "data.head()" + ] + }, + { + "cell_type": "markdown", + "id": "e5f6a7b8", + "metadata": {}, + "source": [ + "## Basic OLS Estimation\n", + "\n", + "The default `method='ols'` fits a single regression with:\n", + "- Treatment interaction dummies (one per treatment cohort × post-treatment period cell)\n", + "- Additive cohort fixed effects (`i.gvar` in Stata)\n", + "- Additive time fixed effects (`i.tvar` in Stata)\n", + "\n", + "This matches Stata's `jwdid y, ivar(unit) tvar(time) gvar(cohort)`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f6a7b8c9", + "metadata": {}, + "outputs": [], + "source": [ + "m = WooldridgeDiD() # default: method='ols'\n", + "r = m.fit(data, outcome='outcome', unit='unit', time='period', cohort='first_treat')\n", + "\n", + "# Compute aggregations\n", + "r.aggregate('event').aggregate('group').aggregate('simple')\n", + "\n", + "print(r.summary('simple'))" + ] + }, + { + "cell_type": "markdown", + "id": "a7b8c9d0", + "metadata": {}, + "source": [ + "## Cohort×Time Cell Estimates ATT(g,t)\n", + "\n", + "The raw building blocks are ATT(g,t) — the treatment effect for cohort `g` at calendar time `t`. These are stored in `r.group_time_effects` and correspond to Stata's regression output table (`first_treat#year#c.__tr__`).\n", + "\n", + "Post-treatment cells have `t >= g`; pre-treatment cells (`t < g`) serve as placebo checks." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b8c9d0e1", + "metadata": {}, + "outputs": [], + "source": [ + "print(\"Post-treatment ATT(g,t) cells\")\n", + "print(\"{:>8} {:>8} | {:>10} {:>10} {:>7} {:>7}\".format(\n", + " \"cohort\", \"year\", \"Coef.\", \"Std.Err.\", \"t\", \"P>|t|\"))\n", + "print(\"-\" * 60)\n", + "\n", + "for (g, t), v in sorted(r.group_time_effects.items()):\n", + " if t < g:\n", + " continue\n", + " row = \"{:>8} {:>8} | {:>10.4f} {:>10.4f} {:>7.2f} {:>7.3f}\".format(\n", + " int(g), int(t), v['att'], v['se'], v['t_stat'], v['p_value']\n", + " )\n", + " print(row)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c9d0e1f2", + "metadata": {}, + "outputs": [], + "source": [ + "# Also show pre-treatment placebo cells\n", + "print(\"Pre-treatment placebo ATT(g,t) cells (should be ~0 under parallel trends)\")\n", + "print(\"{:>8} {:>8} | {:>10} {:>10} {:>7} {:>7}\".format(\n", + " \"cohort\", \"year\", \"Coef.\", \"Std.Err.\", \"t\", \"P>|t|\"))\n", + "print(\"-\" * 60)\n", + "\n", + "for (g, t), v in sorted(r.group_time_effects.items()):\n", + " if t >= g:\n", + " continue\n", + " row = \"{:>8} {:>8} | {:>10.4f} {:>10.4f} {:>7.2f} {:>7.3f}\".format(\n", + " int(g), int(t), v['att'], v['se'], v['t_stat'], v['p_value']\n", + " )\n", + " print(row)" + ] + }, + { + "cell_type": "markdown", + "id": "d0e1f2a3", + "metadata": {}, + "source": [ + "## Aggregation Methods\n", + "\n", + "ETWFE supports four aggregation types, matching Stata's `estat` post-estimation commands:\n", + "\n", + "| Python | Stata | Description |\n", + "|--------|-------|-------------|\n", + "| `aggregate('event')` | `estat event` | By relative time k = t − g |\n", + "| `aggregate('group')` | `estat group` | By treatment cohort g |\n", + "| `aggregate('calendar')` | `estat calendar` | By calendar time t |\n", + "| `aggregate('simple')` | `estat simple` | Overall weighted average ATT |\n", + "\n", + "Standard errors use the delta method, propagating uncertainty from the cell-level ATT covariance matrix." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e1f2a3b4", + "metadata": {}, + "outputs": [], + "source": [ + "# Event-study aggregation: ATT by relative time k = t - g\n", + "print(r.summary('event'))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f2a3b4c5", + "metadata": {}, + "outputs": [], + "source": [ + "# Group aggregation: ATT averaged across post-treatment periods for each cohort\n", + "print(r.summary('group'))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a3b4c5d6", + "metadata": {}, + "outputs": [], + "source": [ + "# Simple ATT: overall weighted average\n", + "print(r.summary('simple'))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b4c5d6e7", + "metadata": {}, + "outputs": [], + "source": [ + "# Event study plot\n", + "if HAS_MATPLOTLIB:\n", + " es = r.event_study_effects\n", + " ks = sorted(es.keys())\n", + " atts = [es[k]['att'] for k in ks]\n", + " lo = [es[k]['conf_int'][0] for k in ks]\n", + " hi = [es[k]['conf_int'][1] for k in ks]\n", + "\n", + " fig, ax = plt.subplots(figsize=(9, 5))\n", + " ax.errorbar(ks, atts, yerr=[np.array(atts) - np.array(lo), np.array(hi) - np.array(atts)],\n", + " fmt='o-', capsize=4, color='steelblue', label='ETWFE (OLS)')\n", + " ax.axhline(0, color='black', linestyle='--', linewidth=0.8)\n", + " ax.axvline(-0.5, color='red', linestyle=':', linewidth=0.8, label='Treatment onset')\n", + " ax.set_xlabel('Relative period (k = t − g)')\n", + " ax.set_ylabel('ATT')\n", + " ax.set_title('ETWFE Event Study')\n", + " ax.legend()\n", + " plt.tight_layout()\n", + " plt.show()\n", + "else:\n", + " print(\"Install matplotlib to see the event study plot: pip install matplotlib\")" + ] + }, + { + "cell_type": "markdown", + "id": "c5d6e7f8", + "metadata": {}, + "source": [ + "## Poisson QMLE for Count / Non-Negative Outcomes\n", + "\n", + "`method='poisson'` fits a Poisson QMLE regression. This is valid for any non-negative continuous outcome, not just count data — the Poisson log-likelihood produces consistent estimates whenever the conditional mean is correctly specified as exp(Xβ).\n", + "\n", + "The ATT is computed as the **Average Structural Function (ASF) difference**:\n", + "\n", + "$$\\text{ATT}(g,t) = \\frac{1}{N_{g,t}} \\sum_{i \\in g,t} \\left[\\exp(\\eta_{i,1}) - \\exp(\\eta_{i,0})\\right]$$\n", + "\n", + "where η₁ = Xβ (with treatment) and η₀ = Xβ − δ (counterfactual without treatment).\n", + "\n", + "This matches Stata's `jwdid y, method(poisson)`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d6e7f8a9", + "metadata": {}, + "outputs": [], + "source": [ + "# Simulate a non-negative outcome (e.g., employment level)\n", + "data_pois = data.copy()\n", + "data_pois['emp'] = np.exp(data_pois['outcome'] / 4 + 3) # positive outcome\n", + "\n", + "m_pois = WooldridgeDiD(method='poisson')\n", + "r_pois = m_pois.fit(data_pois, outcome='emp', unit='unit', time='period', cohort='first_treat')\n", + "r_pois.aggregate('event').aggregate('group').aggregate('simple')\n", + "\n", + "print(r_pois.summary('simple'))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e7f8a9b0", + "metadata": {}, + "outputs": [], + "source": [ + "# Cohort×time cells (post-treatment, Poisson)\n", + "print(\"Poisson ATT(g,t) — post-treatment cells\")\n", + "print(\"{:>8} {:>8} | {:>10} {:>10} {:>7} {:>7}\".format(\n", + " \"cohort\", \"year\", \"ATT\", \"Std.Err.\", \"t\", \"P>|t|\"))\n", + "print(\"-\" * 60)\n", + "\n", + "for (g, t), v in sorted(r_pois.group_time_effects.items()):\n", + " if t < g:\n", + " continue\n", + " print(\"{:>8} {:>8} | {:>10.4f} {:>10.4f} {:>7.2f} {:>7.3f}\".format(\n", + " int(g), int(t), v['att'], v['se'], v['t_stat'], v['p_value']\n", + " ))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f8a9b0c1", + "metadata": {}, + "outputs": [], + "source": [ + "print(r_pois.summary('event'))\n", + "print(r_pois.summary('group'))" + ] + }, + { + "cell_type": "markdown", + "id": "a9b0c1d2", + "metadata": {}, + "source": [ + "## Logit for Binary Outcomes\n", + "\n", + "`method='logit'` fits a logit model and computes ATT as the ASF probability difference:\n", + "\n", + "$$\\text{ATT}(g,t) = \\frac{1}{N_{g,t}} \\sum_{i \\in g,t} \\left[\\Lambda(\\eta_{i,1}) - \\Lambda(\\eta_{i,0})\\right]$$\n", + "\n", + "where Λ(·) is the logistic function. Standard errors use the delta method.\n", + "\n", + "This matches Stata's `jwdid y, method(logit)`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b0c1d2e3", + "metadata": {}, + "outputs": [], + "source": [ + "# Create a binary outcome\n", + "data_logit = data.copy()\n", + "median_val = data_logit.loc[data_logit['period'] == data_logit['period'].min(), 'outcome'].median()\n", + "data_logit['hi_outcome'] = (data_logit['outcome'] > median_val).astype(int)\n", + "\n", + "print(f\"Binary outcome mean: {data_logit['hi_outcome'].mean():.3f}\")\n", + "\n", + "m_logit = WooldridgeDiD(method='logit')\n", + "r_logit = m_logit.fit(data_logit, outcome='hi_outcome', unit='unit', time='period', cohort='first_treat')\n", + "r_logit.aggregate('event').aggregate('group').aggregate('simple')\n", + "\n", + "print(r_logit.summary('simple'))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c1d2e3f4", + "metadata": {}, + "outputs": [], + "source": [ + "print(r_logit.summary('group'))" + ] + }, + { + "cell_type": "markdown", + "id": "d2e3f4a5", + "metadata": {}, + "source": [ + "## mpdta: Real-World Example\n", + "\n", + "The **mpdta** dataset (Callaway & Sant'Anna 2021) contains county-level log employment (`lemp`) data with staggered minimum-wage adoption (`first_treat` = year of treatment, 0 = never treated). It is the canonical benchmark for staggered DiD methods.\n", + "\n", + "This replicates Stata's `jwdid lemp, ivar(countyreal) tvar(year) gvar(first_treat)` and `jwdid lemp, ivar(countyreal) tvar(year) gvar(first_treat) method(poisson)` exactly." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e3f4a5b6", + "metadata": {}, + "outputs": [], + "source": [ + "# Load mpdta (adjust path as needed)\n", + "try:\n", + " mpdta = pd.read_stata(\"mpdta.dta\")\n", + " mpdta['first_treat'] = mpdta['first_treat'].astype(int)\n", + " mpdta['countyreal'] = mpdta['countyreal'].astype(int)\n", + " mpdta['year'] = mpdta['year'].astype(int)\n", + " HAS_MPDTA = True\n", + " print(f\"mpdta loaded: {mpdta.shape}\")\n", + " print(f\"Cohorts: {sorted(mpdta['first_treat'].unique())}\")\n", + "except FileNotFoundError:\n", + " HAS_MPDTA = False\n", + " print(\"mpdta.dta not found — skipping real-data cells.\")\n", + " print(\"Download from: https://github.com/bcallaway11/did (R package data)\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f4a5b6c7", + "metadata": {}, + "outputs": [], + "source": [ + "if HAS_MPDTA:\n", + " # OLS — matches: jwdid lemp, ivar(countyreal) tvar(year) gvar(first_treat)\n", + " m_ols = WooldridgeDiD(method='ols')\n", + " r_ols = m_ols.fit(mpdta, outcome='lemp', unit='countyreal', time='year', cohort='first_treat')\n", + " r_ols.aggregate('event').aggregate('group').aggregate('simple')\n", + " print(r_ols.summary('event'))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a5b6c7d8", + "metadata": {}, + "outputs": [], + "source": [ + "if HAS_MPDTA:\n", + " # cohort x time ATT cells (post-treatment)\n", + " # Matches Stata: first_treat#year#c.__tr__ output table\n", + " print(\"ATT(g,t) — post-treatment cells (matches Stata jwdid output)\")\n", + " print(\"{:>6} {:>6} | {:>9} {:>9} {:>7} {:>7}\".format(\n", + " \"cohort\", \"year\", \"Coef.\", \"Std.Err.\", \"t\", \"P>|t|\"))\n", + " print(\"-\" * 55)\n", + " for (g, t), v in sorted(r_ols.group_time_effects.items()):\n", + " if t < g:\n", + " continue\n", + " print(\"{:>6} {:>6} | {:>9.4f} {:>9.4f} {:>7.2f} {:>7.3f}\".format(\n", + " g, t, v['att'], v['se'], v['t_stat'], v['p_value']))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b6c7d8e9", + "metadata": {}, + "outputs": [], + "source": [ + "if HAS_MPDTA:\n", + " # Poisson — matches: gen emp=exp(lemp) / jwdid emp, method(poisson)\n", + " mpdta['emp'] = np.exp(mpdta['lemp'])\n", + "\n", + " m_pois2 = WooldridgeDiD(method='poisson')\n", + " r_pois2 = m_pois2.fit(mpdta, outcome='emp', unit='countyreal', time='year', cohort='first_treat')\n", + " r_pois2.aggregate('event').aggregate('group').aggregate('simple')\n", + "\n", + " print(r_pois2.summary('event'))\n", + " print(r_pois2.summary('group'))\n", + " print(r_pois2.summary('simple'))" + ] + }, + { + "cell_type": "markdown", + "id": "c7d8e9f0", + "metadata": {}, + "source": [ + "## Comparison with Callaway-Sant'Anna\n", + "\n", + "ETWFE and Callaway-Sant'Anna are both valid for staggered designs. Under homogeneous treatment effects and additive parallel trends, they should produce similar ATT(g,t) point estimates. Key differences:\n", + "\n", + "| Aspect | WooldridgeDiD (ETWFE) | CallawaySantAnna |\n", + "|--------|----------------------|------------------|\n", + "| Approach | Single saturated regression | Separate 2×2 DiD per cell |\n", + "| Nonlinear outcomes | Yes (Poisson, Logit) | No |\n", + "| Covariates | Via regression (linear index) | OR, IPW, DR |\n", + "| SE for aggregations | Delta method | Multiplier bootstrap |\n", + "| Stata equivalent | `jwdid` | `csdid` |" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d8e9f0a1", + "metadata": {}, + "outputs": [], + "source": [ + "# Compare overall ATT: ETWFE vs Callaway-Sant'Anna\n", + "cs = CallawaySantAnna()\n", + "r_cs = cs.fit(data, outcome='outcome', unit='unit', time='period', first_treat='first_treat')\n", + "\n", + "m_etwfe = WooldridgeDiD(method='ols')\n", + "r_etwfe = m_etwfe.fit(data, outcome='outcome', unit='unit', time='period', cohort='first_treat')\n", + "r_etwfe.aggregate('simple')\n", + "\n", + "print(\"Overall ATT Comparison (true effect = 2.0)\")\n", + "print(\"=\" * 60)\n", + "print(\"{:<25} {:>10} {:>10} {:>12}\".format(\"Estimator\", \"ATT\", \"SE\", \"95% CI\"))\n", + "print(\"-\" * 60)\n", + "\n", + "for name, est_r in [(\"WooldridgeDiD (ETWFE)\", r_etwfe), (\"CallawaySantAnna\", r_cs)]:\n", + " ci = est_r.overall_conf_int\n", + " print(\"{:<25} {:>10.4f} {:>10.4f} [{:.3f}, {:.3f}]\".format(\n", + " name, est_r.overall_att, est_r.overall_se, ci[0], ci[1]\n", + " ))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e9f0a1b2", + "metadata": {}, + "outputs": [], + "source": [ + "# Event-study comparison\n", + "r_cs_es = CallawaySantAnna().fit(\n", + " data, outcome='outcome', unit='unit', time='period',\n", + " first_treat='first_treat', aggregate='event_study'\n", + ")\n", + "\n", + "if HAS_MATPLOTLIB:\n", + " es_etwfe = r_etwfe.event_study_effects\n", + " es_cs = {int(row['relative_period']): row\n", + " for _, row in r_cs_es.to_dataframe(level='event_study').iterrows()}\n", + "\n", + " ks = sorted(es_etwfe.keys())\n", + "\n", + " fig, ax = plt.subplots(figsize=(10, 5))\n", + " offset = 0.1\n", + "\n", + " atts_e = [es_etwfe[k]['att'] for k in ks]\n", + " lo_e = [es_etwfe[k]['conf_int'][0] for k in ks]\n", + " hi_e = [es_etwfe[k]['conf_int'][1] for k in ks]\n", + " ax.errorbar([k - offset for k in ks], atts_e,\n", + " yerr=[np.array(atts_e) - np.array(lo_e), np.array(hi_e) - np.array(atts_e)],\n", + " fmt='o-', capsize=4, color='steelblue', label='ETWFE')\n", + "\n", + " ks_cs = sorted(es_cs.keys())\n", + " atts_cs = [es_cs[k]['effect'] for k in ks_cs]\n", + " lo_cs = [es_cs[k]['conf_int_lower'] for k in ks_cs]\n", + " hi_cs = [es_cs[k]['conf_int_upper'] for k in ks_cs]\n", + " ax.errorbar([k + offset for k in ks_cs], atts_cs,\n", + " yerr=[np.array(atts_cs) - np.array(lo_cs), np.array(hi_cs) - np.array(atts_cs)],\n", + " fmt='s--', capsize=4, color='darkorange', label='Callaway-Sant\\'Anna')\n", + "\n", + " ax.axhline(0, color='black', linestyle='--', linewidth=0.8)\n", + " ax.axvline(-0.5, color='red', linestyle=':', linewidth=0.8)\n", + " ax.set_xlabel('Relative period (k = t − g)')\n", + " ax.set_ylabel('ATT')\n", + " ax.set_title('Event Study: ETWFE vs Callaway-Sant\\'Anna')\n", + " ax.legend()\n", + " plt.tight_layout()\n", + " plt.show()\n", + "else:\n", + " print(\"Install matplotlib to see the comparison plot: pip install matplotlib\")" + ] + }, + { + "cell_type": "markdown", + "id": "f0a1b2c3", + "metadata": {}, + "source": [ + "## Summary\n", + "\n", + "**Key takeaways:**\n", + "\n", + "1. **ETWFE via a single regression**: all ATT(g,t) cells estimated jointly, not separately — computationally efficient and internally consistent\n", + "2. **OLS path** matches Stata `jwdid` exactly: additive cohort + time FEs, treatment interaction dummies\n", + "3. **Nonlinear paths** (Poisson, Logit) use the ASF formula: E[f(η₁)] − E[f(η₀)] — the only valid ATT definition for nonlinear models\n", + "4. **Four aggregations** mirror Stata's `estat` commands: event, group, calendar, simple\n", + "5. **Delta-method SEs** for all aggregations, including nonlinear paths\n", + "6. **When to prefer ETWFE**: nonlinear outcomes, or when a single-regression framework is preferred\n", + "7. **When to prefer CS/ImputationDiD**: covariate adjustment via IPW/DR, or multiplier bootstrap inference\n", + "\n", + "**Parameter reference:**\n", + "\n", + "| Parameter | Default | Description |\n", + "|-----------|---------|-------------|\n", + "| `method` | `'ols'` | `'ols'`, `'poisson'`, or `'logit'` |\n", + "| `control_group` | `'not_yet_treated'` | `'not_yet_treated'` or `'never_treated'` |\n", + "| `anticipation` | `0` | Anticipation periods before treatment |\n", + "| `alpha` | `0.05` | Significance level |\n", + "| `cluster` | `None` | Column for clustering (default: unit variable) |\n", + "\n", + "**References:**\n", + "- Wooldridge, J. M. (2021). Two-Way Fixed Effects, the Two-Way Mundlak Regression, and Difference-in-Differences Estimators. *SSRN 3906345*.\n", + "- Wooldridge, J. M. (2023). Simple approaches to nonlinear difference-in-differences with panel data. *The Econometrics Journal*, 26(3), C31–C66.\n", + "- Friosavila, F. (2021). `jwdid`: Stata module for ETWFE. SSC s459114.\n", + "\n", + "*See also: [Tutorial 02](02_staggered_did.ipynb) for Callaway-Sant'Anna, [Tutorial 15](15_efficient_did.ipynb) for Efficient DiD.*" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.10.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/tutorials/README.md b/docs/tutorials/README.md index 201da9b4..80ea5008 100644 --- a/docs/tutorials/README.md +++ b/docs/tutorials/README.md @@ -51,6 +51,16 @@ Efficient Difference-in-Differences (Chen, Sant'Anna & Xie 2025): - Event study and group-level aggregation - Bootstrap inference and diagnostics +### 16. Wooldridge ETWFE (`16_wooldridge_etwfe.ipynb`) +Extended Two-Way Fixed Effects (Wooldridge 2021/2023), equivalent to Stata `jwdid`: +- Basic OLS estimation matching Stata `jwdid` output exactly +- Cohort×time ATT(g,t) cell estimates +- Four aggregation methods: event-study, group, calendar, simple +- Poisson QMLE for count / non-negative outcomes +- Logit for binary outcomes +- Real-world mpdta example (staggered minimum-wage adoption) +- Comparison with Callaway-Sant'Anna + ## Running the Notebooks 1. Install diff-diff with dependencies: diff --git a/uv.lock b/uv.lock new file mode 100644 index 00000000..75bc50ef --- /dev/null +++ b/uv.lock @@ -0,0 +1,1903 @@ +version = 1 +revision = 2 +requires-python = ">=3.9, <3.14" +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version >= '3.12' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.10.*'", + "python_full_version < '3.10'", +] + +[[package]] +name = "alabaster" +version = "0.7.16" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/3e/13dd8e5ed9094e734ac430b5d0eb4f2bb001708a8b7856cbf8e084e001ba/alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65", size = 23776, upload-time = "2024-01-10T00:56:10.189Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/34/d4e1c02d3bee589efb5dfa17f88ea08bdb3e3eac12bc475462aec52ed223/alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92", size = 13511, upload-time = "2024-01-10T00:56:08.388Z" }, +] + +[[package]] +name = "alabaster" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version >= '3.12' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210, upload-time = "2024-07-26T18:15:03.762Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" }, +] + +[[package]] +name = "babel" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, +] + +[[package]] +name = "black" +version = "25.11.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "mypy-extensions", marker = "python_full_version < '3.10'" }, + { name = "packaging", marker = "python_full_version < '3.10'" }, + { name = "pathspec", marker = "python_full_version < '3.10'" }, + { name = "platformdirs", version = "4.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pytokens", marker = "python_full_version < '3.10'" }, + { name = "tomli", marker = "python_full_version < '3.10'" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8c/ad/33adf4708633d047950ff2dfdea2e215d84ac50ef95aff14a614e4b6e9b2/black-25.11.0.tar.gz", hash = "sha256:9a323ac32f5dc75ce7470501b887250be5005a01602e931a15e45593f70f6e08", size = 655669, upload-time = "2025-11-10T01:53:50.558Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/d2/6caccbc96f9311e8ec3378c296d4f4809429c43a6cd2394e3c390e86816d/black-25.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ec311e22458eec32a807f029b2646f661e6859c3f61bc6d9ffb67958779f392e", size = 1743501, upload-time = "2025-11-10T01:59:06.202Z" }, + { url = "https://files.pythonhosted.org/packages/69/35/b986d57828b3f3dccbf922e2864223197ba32e74c5004264b1c62bc9f04d/black-25.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1032639c90208c15711334d681de2e24821af0575573db2810b0763bcd62e0f0", size = 1597308, upload-time = "2025-11-10T01:57:58.633Z" }, + { url = "https://files.pythonhosted.org/packages/39/8e/8b58ef4b37073f52b64a7b2dd8c9a96c84f45d6f47d878d0aa557e9a2d35/black-25.11.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c0f7c461df55cf32929b002335883946a4893d759f2df343389c4396f3b6b37", size = 1656194, upload-time = "2025-11-10T01:57:10.909Z" }, + { url = "https://files.pythonhosted.org/packages/8d/30/9c2267a7955ecc545306534ab88923769a979ac20a27cf618d370091e5dd/black-25.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:f9786c24d8e9bd5f20dc7a7f0cdd742644656987f6ea6947629306f937726c03", size = 1347996, upload-time = "2025-11-10T01:57:22.391Z" }, + { url = "https://files.pythonhosted.org/packages/c4/62/d304786b75ab0c530b833a89ce7d997924579fb7484ecd9266394903e394/black-25.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:895571922a35434a9d8ca67ef926da6bc9ad464522a5fe0db99b394ef1c0675a", size = 1727891, upload-time = "2025-11-10T02:01:40.507Z" }, + { url = "https://files.pythonhosted.org/packages/82/5d/ffe8a006aa522c9e3f430e7b93568a7b2163f4b3f16e8feb6d8c3552761a/black-25.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cb4f4b65d717062191bdec8e4a442539a8ea065e6af1c4f4d36f0cdb5f71e170", size = 1581875, upload-time = "2025-11-10T01:57:51.192Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c8/7c8bda3108d0bb57387ac41b4abb5c08782b26da9f9c4421ef6694dac01a/black-25.11.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d81a44cbc7e4f73a9d6ae449ec2317ad81512d1e7dce7d57f6333fd6259737bc", size = 1642716, upload-time = "2025-11-10T01:56:51.589Z" }, + { url = "https://files.pythonhosted.org/packages/34/b9/f17dea34eecb7cc2609a89627d480fb6caea7b86190708eaa7eb15ed25e7/black-25.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:7eebd4744dfe92ef1ee349dc532defbf012a88b087bb7ddd688ff59a447b080e", size = 1352904, upload-time = "2025-11-10T01:59:26.252Z" }, + { url = "https://files.pythonhosted.org/packages/7f/12/5c35e600b515f35ffd737da7febdb2ab66bb8c24d88560d5e3ef3d28c3fd/black-25.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:80e7486ad3535636657aa180ad32a7d67d7c273a80e12f1b4bfa0823d54e8fac", size = 1772831, upload-time = "2025-11-10T02:03:47Z" }, + { url = "https://files.pythonhosted.org/packages/1a/75/b3896bec5a2bb9ed2f989a970ea40e7062f8936f95425879bbe162746fe5/black-25.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6cced12b747c4c76bc09b4db057c319d8545307266f41aaee665540bc0e04e96", size = 1608520, upload-time = "2025-11-10T01:58:46.895Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b5/2bfc18330eddbcfb5aab8d2d720663cd410f51b2ed01375f5be3751595b0/black-25.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cb2d54a39e0ef021d6c5eef442e10fd71fcb491be6413d083a320ee768329dd", size = 1682719, upload-time = "2025-11-10T01:56:55.24Z" }, + { url = "https://files.pythonhosted.org/packages/96/fb/f7dc2793a22cdf74a72114b5ed77fe3349a2e09ef34565857a2f917abdf2/black-25.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:ae263af2f496940438e5be1a0c1020e13b09154f3af4df0835ea7f9fe7bfa409", size = 1362684, upload-time = "2025-11-10T01:57:07.639Z" }, + { url = "https://files.pythonhosted.org/packages/ad/47/3378d6a2ddefe18553d1115e36aea98f4a90de53b6a3017ed861ba1bd3bc/black-25.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0a1d40348b6621cc20d3d7530a5b8d67e9714906dfd7346338249ad9c6cedf2b", size = 1772446, upload-time = "2025-11-10T02:02:16.181Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4b/0f00bfb3d1f7e05e25bfc7c363f54dc523bb6ba502f98f4ad3acf01ab2e4/black-25.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:51c65d7d60bb25429ea2bf0731c32b2a2442eb4bd3b2afcb47830f0b13e58bfd", size = 1607983, upload-time = "2025-11-10T02:02:52.502Z" }, + { url = "https://files.pythonhosted.org/packages/99/fe/49b0768f8c9ae57eb74cc10a1f87b4c70453551d8ad498959721cc345cb7/black-25.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:936c4dd07669269f40b497440159a221ee435e3fddcf668e0c05244a9be71993", size = 1682481, upload-time = "2025-11-10T01:57:12.35Z" }, + { url = "https://files.pythonhosted.org/packages/55/17/7e10ff1267bfa950cc16f0a411d457cdff79678fbb77a6c73b73a5317904/black-25.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:f42c0ea7f59994490f4dccd64e6b2dd49ac57c7c84f38b8faab50f8759db245c", size = 1363869, upload-time = "2025-11-10T01:58:24.608Z" }, + { url = "https://files.pythonhosted.org/packages/d5/9a/5b2c0e3215fe748fcf515c2dd34658973a1210bf610e24de5ba887e4f1c8/black-25.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a3bb5ce32daa9ff0605d73b6f19da0b0e6c1f8f2d75594db539fdfed722f2b06", size = 1743063, upload-time = "2025-11-10T02:02:43.175Z" }, + { url = "https://files.pythonhosted.org/packages/a1/20/245164c6efc27333409c62ba54dcbfbe866c6d1957c9a6c0647786e950da/black-25.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9815ccee1e55717fe9a4b924cae1646ef7f54e0f990da39a34fc7b264fcf80a2", size = 1596867, upload-time = "2025-11-10T02:00:17.157Z" }, + { url = "https://files.pythonhosted.org/packages/ca/6f/1a3859a7da205f3d50cf3a8bec6bdc551a91c33ae77a045bb24c1f46ab54/black-25.11.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92285c37b93a1698dcbc34581867b480f1ba3a7b92acf1fe0467b04d7a4da0dc", size = 1655678, upload-time = "2025-11-10T01:57:09.028Z" }, + { url = "https://files.pythonhosted.org/packages/56/1a/6dec1aeb7be90753d4fcc273e69bc18bfd34b353223ed191da33f7519410/black-25.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:43945853a31099c7c0ff8dface53b4de56c41294fa6783c0441a8b1d9bf668bc", size = 1347452, upload-time = "2025-11-10T01:57:01.871Z" }, + { url = "https://files.pythonhosted.org/packages/00/5d/aed32636ed30a6e7f9efd6ad14e2a0b0d687ae7c8c7ec4e4a557174b895c/black-25.11.0-py3-none-any.whl", hash = "sha256:e3f562da087791e96cefcd9dda058380a442ab322a02e222add53736451f604b", size = 204918, upload-time = "2025-11-10T01:53:48.917Z" }, +] + +[[package]] +name = "black" +version = "26.3.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version >= '3.12' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "mypy-extensions", marker = "python_full_version >= '3.10'" }, + { name = "packaging", marker = "python_full_version >= '3.10'" }, + { name = "pathspec", marker = "python_full_version >= '3.10'" }, + { name = "platformdirs", version = "4.9.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytokens", marker = "python_full_version >= '3.10'" }, + { name = "tomli", marker = "python_full_version == '3.10.*'" }, + { name = "typing-extensions", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/c5/61175d618685d42b005847464b8fb4743a67b1b8fdb75e50e5a96c31a27a/black-26.3.1.tar.gz", hash = "sha256:2c50f5063a9641c7eed7795014ba37b0f5fa227f3d408b968936e24bc0566b07", size = 666155, upload-time = "2026-03-12T03:36:03.593Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/a8/11170031095655d36ebc6664fe0897866f6023892396900eec0e8fdc4299/black-26.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:86a8b5035fce64f5dcd1b794cf8ec4d31fe458cf6ce3986a30deb434df82a1d2", size = 1866562, upload-time = "2026-03-12T03:39:58.639Z" }, + { url = "https://files.pythonhosted.org/packages/69/ce/9e7548d719c3248c6c2abfd555d11169457cbd584d98d179111338423790/black-26.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5602bdb96d52d2d0672f24f6ffe5218795736dd34807fd0fd55ccd6bf206168b", size = 1703623, upload-time = "2026-03-12T03:40:00.347Z" }, + { url = "https://files.pythonhosted.org/packages/7f/0a/8d17d1a9c06f88d3d030d0b1d4373c1551146e252afe4547ed601c0e697f/black-26.3.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c54a4a82e291a1fee5137371ab488866b7c86a3305af4026bdd4dc78642e1ac", size = 1768388, upload-time = "2026-03-12T03:40:01.765Z" }, + { url = "https://files.pythonhosted.org/packages/52/79/c1ee726e221c863cde5164f925bacf183dfdf0397d4e3f94889439b947b4/black-26.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:6e131579c243c98f35bce64a7e08e87fb2d610544754675d4a0e73a070a5aa3a", size = 1412969, upload-time = "2026-03-12T03:40:03.252Z" }, + { url = "https://files.pythonhosted.org/packages/73/a5/15c01d613f5756f68ed8f6d4ec0a1e24b82b18889fa71affd3d1f7fad058/black-26.3.1-cp310-cp310-win_arm64.whl", hash = "sha256:5ed0ca58586c8d9a487352a96b15272b7fa55d139fc8496b519e78023a8dab0a", size = 1220345, upload-time = "2026-03-12T03:40:04.892Z" }, + { url = "https://files.pythonhosted.org/packages/17/57/5f11c92861f9c92eb9dddf515530bc2d06db843e44bdcf1c83c1427824bc/black-26.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:28ef38aee69e4b12fda8dba75e21f9b4f979b490c8ac0baa7cb505369ac9e1ff", size = 1851987, upload-time = "2026-03-12T03:40:06.248Z" }, + { url = "https://files.pythonhosted.org/packages/54/aa/340a1463660bf6831f9e39646bf774086dbd8ca7fc3cded9d59bbdf4ad0a/black-26.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf9bf162ed91a26f1adba8efda0b573bc6924ec1408a52cc6f82cb73ec2b142c", size = 1689499, upload-time = "2026-03-12T03:40:07.642Z" }, + { url = "https://files.pythonhosted.org/packages/f3/01/b726c93d717d72733da031d2de10b92c9fa4c8d0c67e8a8a372076579279/black-26.3.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:474c27574d6d7037c1bc875a81d9be0a9a4f9ee95e62800dab3cfaadbf75acd5", size = 1754369, upload-time = "2026-03-12T03:40:09.279Z" }, + { url = "https://files.pythonhosted.org/packages/e3/09/61e91881ca291f150cfc9eb7ba19473c2e59df28859a11a88248b5cbbc4d/black-26.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:5e9d0d86df21f2e1677cc4bd090cd0e446278bcbbe49bf3659c308c3e402843e", size = 1413613, upload-time = "2026-03-12T03:40:10.943Z" }, + { url = "https://files.pythonhosted.org/packages/16/73/544f23891b22e7efe4d8f812371ab85b57f6a01b2fc45e3ba2e52ba985b8/black-26.3.1-cp311-cp311-win_arm64.whl", hash = "sha256:9a5e9f45e5d5e1c5b5c29b3bd4265dcc90e8b92cf4534520896ed77f791f4da5", size = 1219719, upload-time = "2026-03-12T03:40:12.597Z" }, + { url = "https://files.pythonhosted.org/packages/dc/f8/da5eae4fc75e78e6dceb60624e1b9662ab00d6b452996046dfa9b8a6025b/black-26.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e6f89631eb88a7302d416594a32faeee9fb8fb848290da9d0a5f2903519fc1", size = 1895920, upload-time = "2026-03-12T03:40:13.921Z" }, + { url = "https://files.pythonhosted.org/packages/2c/9f/04e6f26534da2e1629b2b48255c264cabf5eedc5141d04516d9d68a24111/black-26.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41cd2012d35b47d589cb8a16faf8a32ef7a336f56356babd9fcf70939ad1897f", size = 1718499, upload-time = "2026-03-12T03:40:15.239Z" }, + { url = "https://files.pythonhosted.org/packages/04/91/a5935b2a63e31b331060c4a9fdb5a6c725840858c599032a6f3aac94055f/black-26.3.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f76ff19ec5297dd8e66eb64deda23631e642c9393ab592826fd4bdc97a4bce7", size = 1794994, upload-time = "2026-03-12T03:40:17.124Z" }, + { url = "https://files.pythonhosted.org/packages/e7/0a/86e462cdd311a3c2a8ece708d22aba17d0b2a0d5348ca34b40cdcbea512e/black-26.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:ddb113db38838eb9f043623ba274cfaf7d51d5b0c22ecb30afe58b1bb8322983", size = 1420867, upload-time = "2026-03-12T03:40:18.83Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e5/22515a19cb7eaee3440325a6b0d95d2c0e88dd180cb011b12ae488e031d1/black-26.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:dfdd51fc3e64ea4f35873d1b3fb25326773d55d2329ff8449139ebaad7357efb", size = 1230124, upload-time = "2026-03-12T03:40:20.425Z" }, + { url = "https://files.pythonhosted.org/packages/f5/77/5728052a3c0450c53d9bb3945c4c46b91baa62b2cafab6801411b6271e45/black-26.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:855822d90f884905362f602880ed8b5df1b7e3ee7d0db2502d4388a954cc8c54", size = 1895034, upload-time = "2026-03-12T03:40:21.813Z" }, + { url = "https://files.pythonhosted.org/packages/52/73/7cae55fdfdfbe9d19e9a8d25d145018965fe2079fa908101c3733b0c55a0/black-26.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8a33d657f3276328ce00e4d37fe70361e1ec7614da5d7b6e78de5426cb56332f", size = 1718503, upload-time = "2026-03-12T03:40:23.666Z" }, + { url = "https://files.pythonhosted.org/packages/e1/87/af89ad449e8254fdbc74654e6467e3c9381b61472cc532ee350d28cfdafb/black-26.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f1cd08e99d2f9317292a311dfe578fd2a24b15dbce97792f9c4d752275c1fa56", size = 1793557, upload-time = "2026-03-12T03:40:25.497Z" }, + { url = "https://files.pythonhosted.org/packages/43/10/d6c06a791d8124b843bf325ab4ac7d2f5b98731dff84d6064eafd687ded1/black-26.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:c7e72339f841b5a237ff14f7d3880ddd0fc7f98a1199e8c4327f9a4f478c1839", size = 1422766, upload-time = "2026-03-12T03:40:27.14Z" }, + { url = "https://files.pythonhosted.org/packages/59/4f/40a582c015f2d841ac24fed6390bd68f0fc896069ff3a886317959c9daf8/black-26.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:afc622538b430aa4c8c853f7f63bc582b3b8030fd8c80b70fb5fa5b834e575c2", size = 1232140, upload-time = "2026-03-12T03:40:28.882Z" }, + { url = "https://files.pythonhosted.org/packages/8e/0d/52d98722666d6fc6c3dd4c76df339501d6efd40e0ff95e6186a7b7f0befd/black-26.3.1-py3-none-any.whl", hash = "sha256:2bd5aa94fc267d38bb21a70d7410a89f1a1d318841855f698746f8e7f51acd1b", size = 207542, upload-time = "2026-03-12T03:36:01.668Z" }, +] + +[[package]] +name = "certifi" +version = "2026.2.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/60/e3bec1881450851b087e301bedc3daa9377a4d45f1c26aa90b0b235e38aa/charset_normalizer-3.4.6.tar.gz", hash = "sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6", size = 143363, upload-time = "2026-03-15T18:53:25.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/8c/2c56124c6dc53a774d435f985b5973bc592f42d437be58c0c92d65ae7296/charset_normalizer-3.4.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2e1d8ca8611099001949d1cdfaefc510cf0f212484fe7c565f735b68c78c3c95", size = 298751, upload-time = "2026-03-15T18:50:00.003Z" }, + { url = "https://files.pythonhosted.org/packages/86/2a/2a7db6b314b966a3bcad8c731c0719c60b931b931de7ae9f34b2839289ee/charset_normalizer-3.4.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e25369dc110d58ddf29b949377a93e0716d72a24f62bad72b2b39f155949c1fd", size = 200027, upload-time = "2026-03-15T18:50:01.702Z" }, + { url = "https://files.pythonhosted.org/packages/68/f2/0fe775c74ae25e2a3b07b01538fc162737b3e3f795bada3bc26f4d4d495c/charset_normalizer-3.4.6-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:259695e2ccc253feb2a016303543d691825e920917e31f894ca1a687982b1de4", size = 220741, upload-time = "2026-03-15T18:50:03.194Z" }, + { url = "https://files.pythonhosted.org/packages/10/98/8085596e41f00b27dd6aa1e68413d1ddda7e605f34dd546833c61fddd709/charset_normalizer-3.4.6-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dda86aba335c902b6149a02a55b38e96287157e609200811837678214ba2b1db", size = 215802, upload-time = "2026-03-15T18:50:05.859Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ce/865e4e09b041bad659d682bbd98b47fb490b8e124f9398c9448065f64fee/charset_normalizer-3.4.6-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51fb3c322c81d20567019778cb5a4a6f2dc1c200b886bc0d636238e364848c89", size = 207908, upload-time = "2026-03-15T18:50:07.676Z" }, + { url = "https://files.pythonhosted.org/packages/a8/54/8c757f1f7349262898c2f169e0d562b39dcb977503f18fdf0814e923db78/charset_normalizer-3.4.6-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:4482481cb0572180b6fd976a4d5c72a30263e98564da68b86ec91f0fe35e8565", size = 194357, upload-time = "2026-03-15T18:50:09.327Z" }, + { url = "https://files.pythonhosted.org/packages/6f/29/e88f2fac9218907fc7a70722b393d1bbe8334c61fe9c46640dba349b6e66/charset_normalizer-3.4.6-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:39f5068d35621da2881271e5c3205125cc456f54e9030d3f723288c873a71bf9", size = 205610, upload-time = "2026-03-15T18:50:10.732Z" }, + { url = "https://files.pythonhosted.org/packages/4c/c5/21d7bb0cb415287178450171d130bed9d664211fdd59731ed2c34267b07d/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8bea55c4eef25b0b19a0337dc4e3f9a15b00d569c77211fa8cde38684f234fb7", size = 203512, upload-time = "2026-03-15T18:50:12.535Z" }, + { url = "https://files.pythonhosted.org/packages/a4/be/ce52f3c7fdb35cc987ad38a53ebcef52eec498f4fb6c66ecfe62cfe57ba2/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f0cdaecd4c953bfae0b6bb64910aaaca5a424ad9c72d85cb88417bb9814f7550", size = 195398, upload-time = "2026-03-15T18:50:14.236Z" }, + { url = "https://files.pythonhosted.org/packages/81/a0/3ab5dd39d4859a3555e5dadfc8a9fa7f8352f8c183d1a65c90264517da0e/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:150b8ce8e830eb7ccb029ec9ca36022f756986aaaa7956aad6d9ec90089338c0", size = 221772, upload-time = "2026-03-15T18:50:15.581Z" }, + { url = "https://files.pythonhosted.org/packages/04/6e/6a4e41a97ba6b2fa87f849c41e4d229449a586be85053c4d90135fe82d26/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e68c14b04827dd76dcbd1aeea9e604e3e4b78322d8faf2f8132c7138efa340a8", size = 205759, upload-time = "2026-03-15T18:50:17.047Z" }, + { url = "https://files.pythonhosted.org/packages/db/3b/34a712a5ee64a6957bf355b01dc17b12de457638d436fdb05d01e463cd1c/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:3778fd7d7cd04ae8f54651f4a7a0bd6e39a0cf20f801720a4c21d80e9b7ad6b0", size = 216938, upload-time = "2026-03-15T18:50:18.44Z" }, + { url = "https://files.pythonhosted.org/packages/cb/05/5bd1e12da9ab18790af05c61aafd01a60f489778179b621ac2a305243c62/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:dad6e0f2e481fffdcf776d10ebee25e0ef89f16d691f1e5dee4b586375fdc64b", size = 210138, upload-time = "2026-03-15T18:50:19.852Z" }, + { url = "https://files.pythonhosted.org/packages/bd/8e/3cb9e2d998ff6b21c0a1860343cb7b83eba9cdb66b91410e18fc4969d6ab/charset_normalizer-3.4.6-cp310-cp310-win32.whl", hash = "sha256:74a2e659c7ecbc73562e2a15e05039f1e22c75b7c7618b4b574a3ea9118d1557", size = 144137, upload-time = "2026-03-15T18:50:21.505Z" }, + { url = "https://files.pythonhosted.org/packages/d8/8f/78f5489ffadb0db3eb7aff53d31c24531d33eb545f0c6f6567c25f49a5ff/charset_normalizer-3.4.6-cp310-cp310-win_amd64.whl", hash = "sha256:aa9cccf4a44b9b62d8ba8b4dd06c649ba683e4bf04eea606d2e94cfc2d6ff4d6", size = 154244, upload-time = "2026-03-15T18:50:22.81Z" }, + { url = "https://files.pythonhosted.org/packages/e4/74/e472659dffb0cadb2f411282d2d76c60da1fc94076d7fffed4ae8a93ec01/charset_normalizer-3.4.6-cp310-cp310-win_arm64.whl", hash = "sha256:e985a16ff513596f217cee86c21371b8cd011c0f6f056d0920aa2d926c544058", size = 143312, upload-time = "2026-03-15T18:50:24.074Z" }, + { url = "https://files.pythonhosted.org/packages/62/28/ff6f234e628a2de61c458be2779cb182bc03f6eec12200d4a525bbfc9741/charset_normalizer-3.4.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:82060f995ab5003a2d6e0f4ad29065b7672b6593c8c63559beefe5b443242c3e", size = 293582, upload-time = "2026-03-15T18:50:25.454Z" }, + { url = "https://files.pythonhosted.org/packages/1c/b7/b1a117e5385cbdb3205f6055403c2a2a220c5ea80b8716c324eaf75c5c95/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60c74963d8350241a79cb8feea80e54d518f72c26db618862a8f53e5023deaf9", size = 197240, upload-time = "2026-03-15T18:50:27.196Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5f/2574f0f09f3c3bc1b2f992e20bce6546cb1f17e111c5be07308dc5427956/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6e4333fb15c83f7d1482a76d45a0818897b3d33f00efd215528ff7c51b8e35d", size = 217363, upload-time = "2026-03-15T18:50:28.601Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d1/0ae20ad77bc949ddd39b51bf383b6ca932f2916074c95cad34ae465ab71f/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bc72863f4d9aba2e8fd9085e63548a324ba706d2ea2c83b260da08a59b9482de", size = 212994, upload-time = "2026-03-15T18:50:30.102Z" }, + { url = "https://files.pythonhosted.org/packages/60/ac/3233d262a310c1b12633536a07cde5ddd16985e6e7e238e9f3f9423d8eb9/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9cc4fc6c196d6a8b76629a70ddfcd4635a6898756e2d9cac5565cf0654605d73", size = 204697, upload-time = "2026-03-15T18:50:31.654Z" }, + { url = "https://files.pythonhosted.org/packages/25/3c/8a18fc411f085b82303cfb7154eed5bd49c77035eb7608d049468b53f87c/charset_normalizer-3.4.6-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:0c173ce3a681f309f31b87125fecec7a5d1347261ea11ebbb856fa6006b23c8c", size = 191673, upload-time = "2026-03-15T18:50:33.433Z" }, + { url = "https://files.pythonhosted.org/packages/ff/a7/11cfe61d6c5c5c7438d6ba40919d0306ed83c9ab957f3d4da2277ff67836/charset_normalizer-3.4.6-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c907cdc8109f6c619e6254212e794d6548373cc40e1ec75e6e3823d9135d29cc", size = 201120, upload-time = "2026-03-15T18:50:35.105Z" }, + { url = "https://files.pythonhosted.org/packages/b5/10/cf491fa1abd47c02f69687046b896c950b92b6cd7337a27e6548adbec8e4/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:404a1e552cf5b675a87f0651f8b79f5f1e6fd100ee88dc612f89aa16abd4486f", size = 200911, upload-time = "2026-03-15T18:50:36.819Z" }, + { url = "https://files.pythonhosted.org/packages/28/70/039796160b48b18ed466fde0af84c1b090c4e288fae26cd674ad04a2d703/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e3c701e954abf6fc03a49f7c579cc80c2c6cc52525340ca3186c41d3f33482ef", size = 192516, upload-time = "2026-03-15T18:50:38.228Z" }, + { url = "https://files.pythonhosted.org/packages/ff/34/c56f3223393d6ff3124b9e78f7de738047c2d6bc40a4f16ac0c9d7a1cb3c/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7a6967aaf043bceabab5412ed6bd6bd26603dae84d5cb75bf8d9a74a4959d398", size = 218795, upload-time = "2026-03-15T18:50:39.664Z" }, + { url = "https://files.pythonhosted.org/packages/e8/3b/ce2d4f86c5282191a041fdc5a4ce18f1c6bd40a5bd1f74cf8625f08d51c1/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5feb91325bbceade6afab43eb3b508c63ee53579fe896c77137ded51c6b6958e", size = 201833, upload-time = "2026-03-15T18:50:41.552Z" }, + { url = "https://files.pythonhosted.org/packages/3b/9b/b6a9f76b0fd7c5b5ec58b228ff7e85095370282150f0bd50b3126f5506d6/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f820f24b09e3e779fe84c3c456cb4108a7aa639b0d1f02c28046e11bfcd088ed", size = 213920, upload-time = "2026-03-15T18:50:43.33Z" }, + { url = "https://files.pythonhosted.org/packages/ae/98/7bc23513a33d8172365ed30ee3a3b3fe1ece14a395e5fc94129541fc6003/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b35b200d6a71b9839a46b9b7fff66b6638bb52fc9658aa58796b0326595d3021", size = 206951, upload-time = "2026-03-15T18:50:44.789Z" }, + { url = "https://files.pythonhosted.org/packages/32/73/c0b86f3d1458468e11aec870e6b3feac931facbe105a894b552b0e518e79/charset_normalizer-3.4.6-cp311-cp311-win32.whl", hash = "sha256:9ca4c0b502ab399ef89248a2c84c54954f77a070f28e546a85e91da627d1301e", size = 143703, upload-time = "2026-03-15T18:50:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/c6/e3/76f2facfe8eddee0bbd38d2594e709033338eae44ebf1738bcefe0a06185/charset_normalizer-3.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:a9e68c9d88823b274cf1e72f28cb5dc89c990edf430b0bfd3e2fb0785bfeabf4", size = 153857, upload-time = "2026-03-15T18:50:47.563Z" }, + { url = "https://files.pythonhosted.org/packages/e2/dc/9abe19c9b27e6cd3636036b9d1b387b78c40dedbf0b47f9366737684b4b0/charset_normalizer-3.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:97d0235baafca5f2b09cf332cc275f021e694e8362c6bb9c96fc9a0eb74fc316", size = 142751, upload-time = "2026-03-15T18:50:49.234Z" }, + { url = "https://files.pythonhosted.org/packages/e5/62/c0815c992c9545347aeea7859b50dc9044d147e2e7278329c6e02ac9a616/charset_normalizer-3.4.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2ef7fedc7a6ecbe99969cd09632516738a97eeb8bd7258bf8a0f23114c057dab", size = 295154, upload-time = "2026-03-15T18:50:50.88Z" }, + { url = "https://files.pythonhosted.org/packages/a8/37/bdca6613c2e3c58c7421891d80cc3efa1d32e882f7c4a7ee6039c3fc951a/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4ea868bc28109052790eb2b52a9ab33f3aa7adc02f96673526ff47419490e21", size = 199191, upload-time = "2026-03-15T18:50:52.658Z" }, + { url = "https://files.pythonhosted.org/packages/6c/92/9934d1bbd69f7f398b38c5dae1cbf9cc672e7c34a4adf7b17c0a9c17d15d/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:836ab36280f21fc1a03c99cd05c6b7af70d2697e374c7af0b61ed271401a72a2", size = 218674, upload-time = "2026-03-15T18:50:54.102Z" }, + { url = "https://files.pythonhosted.org/packages/af/90/25f6ab406659286be929fd89ab0e78e38aa183fc374e03aa3c12d730af8a/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f1ce721c8a7dfec21fcbdfe04e8f68174183cf4e8188e0645e92aa23985c57ff", size = 215259, upload-time = "2026-03-15T18:50:55.616Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ef/79a463eb0fff7f96afa04c1d4c51f8fc85426f918db467854bfb6a569ce3/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e28d62a8fc7a1fa411c43bd65e346f3bce9716dc51b897fbe930c5987b402d5", size = 207276, upload-time = "2026-03-15T18:50:57.054Z" }, + { url = "https://files.pythonhosted.org/packages/f7/72/d0426afec4b71dc159fa6b4e68f868cd5a3ecd918fec5813a15d292a7d10/charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:530d548084c4a9f7a16ed4a294d459b4f229db50df689bfe92027452452943a0", size = 195161, upload-time = "2026-03-15T18:50:58.686Z" }, + { url = "https://files.pythonhosted.org/packages/bf/18/c82b06a68bfcb6ce55e508225d210c7e6a4ea122bfc0748892f3dc4e8e11/charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:30f445ae60aad5e1f8bdbb3108e39f6fbc09f4ea16c815c66578878325f8f15a", size = 203452, upload-time = "2026-03-15T18:51:00.196Z" }, + { url = "https://files.pythonhosted.org/packages/44/d6/0c25979b92f8adafdbb946160348d8d44aa60ce99afdc27df524379875cb/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ac2393c73378fea4e52aa56285a3d64be50f1a12395afef9cce47772f60334c2", size = 202272, upload-time = "2026-03-15T18:51:01.703Z" }, + { url = "https://files.pythonhosted.org/packages/2e/3d/7fea3e8fe84136bebbac715dd1221cc25c173c57a699c030ab9b8900cbb7/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:90ca27cd8da8118b18a52d5f547859cc1f8354a00cd1e8e5120df3e30d6279e5", size = 195622, upload-time = "2026-03-15T18:51:03.526Z" }, + { url = "https://files.pythonhosted.org/packages/57/8a/d6f7fd5cb96c58ef2f681424fbca01264461336d2a7fc875e4446b1f1346/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e5a94886bedca0f9b78fecd6afb6629142fd2605aa70a125d49f4edc6037ee6", size = 220056, upload-time = "2026-03-15T18:51:05.269Z" }, + { url = "https://files.pythonhosted.org/packages/16/50/478cdda782c8c9c3fb5da3cc72dd7f331f031e7f1363a893cdd6ca0f8de0/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:695f5c2823691a25f17bc5d5ffe79fa90972cc34b002ac6c843bb8a1720e950d", size = 203751, upload-time = "2026-03-15T18:51:06.858Z" }, + { url = "https://files.pythonhosted.org/packages/75/fc/cc2fcac943939c8e4d8791abfa139f685e5150cae9f94b60f12520feaa9b/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:231d4da14bcd9301310faf492051bee27df11f2bc7549bc0bb41fef11b82daa2", size = 216563, upload-time = "2026-03-15T18:51:08.564Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b7/a4add1d9a5f68f3d037261aecca83abdb0ab15960a3591d340e829b37298/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a056d1ad2633548ca18ffa2f85c202cfb48b68615129143915b8dc72a806a923", size = 209265, upload-time = "2026-03-15T18:51:10.312Z" }, + { url = "https://files.pythonhosted.org/packages/6c/18/c094561b5d64a24277707698e54b7f67bd17a4f857bbfbb1072bba07c8bf/charset_normalizer-3.4.6-cp312-cp312-win32.whl", hash = "sha256:c2274ca724536f173122f36c98ce188fd24ce3dad886ec2b7af859518ce008a4", size = 144229, upload-time = "2026-03-15T18:51:11.694Z" }, + { url = "https://files.pythonhosted.org/packages/ab/20/0567efb3a8fd481b8f34f739ebddc098ed062a59fed41a8d193a61939e8f/charset_normalizer-3.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:c8ae56368f8cc97c7e40a7ee18e1cedaf8e780cd8bc5ed5ac8b81f238614facb", size = 154277, upload-time = "2026-03-15T18:51:13.004Z" }, + { url = "https://files.pythonhosted.org/packages/15/57/28d79b44b51933119e21f65479d0864a8d5893e494cf5daab15df0247c17/charset_normalizer-3.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:899d28f422116b08be5118ef350c292b36fc15ec2daeb9ea987c89281c7bb5c4", size = 142817, upload-time = "2026-03-15T18:51:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/1e/1d/4fdabeef4e231153b6ed7567602f3b68265ec4e5b76d6024cf647d43d981/charset_normalizer-3.4.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f", size = 294823, upload-time = "2026-03-15T18:51:15.755Z" }, + { url = "https://files.pythonhosted.org/packages/47/7b/20e809b89c69d37be748d98e84dce6820bf663cf19cf6b942c951a3e8f41/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843", size = 198527, upload-time = "2026-03-15T18:51:17.177Z" }, + { url = "https://files.pythonhosted.org/packages/37/a6/4f8d27527d59c039dce6f7622593cdcd3d70a8504d87d09eb11e9fdc6062/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d73beaac5e90173ac3deb9928a74763a6d230f494e4bfb422c217a0ad8e629bf", size = 218388, upload-time = "2026-03-15T18:51:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/f6/9b/4770ccb3e491a9bacf1c46cc8b812214fe367c86a96353ccc6daf87b01ec/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d60377dce4511655582e300dc1e5a5f24ba0cb229005a1d5c8d0cb72bb758ab8", size = 214563, upload-time = "2026-03-15T18:51:20.374Z" }, + { url = "https://files.pythonhosted.org/packages/2b/58/a199d245894b12db0b957d627516c78e055adc3a0d978bc7f65ddaf7c399/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9", size = 206587, upload-time = "2026-03-15T18:51:21.807Z" }, + { url = "https://files.pythonhosted.org/packages/7e/70/3def227f1ec56f5c69dfc8392b8bd63b11a18ca8178d9211d7cc5e5e4f27/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:a26611d9987b230566f24a0a125f17fe0de6a6aff9f25c9f564aaa2721a5fb88", size = 194724, upload-time = "2026-03-15T18:51:23.508Z" }, + { url = "https://files.pythonhosted.org/packages/58/ab/9318352e220c05efd31c2779a23b50969dc94b985a2efa643ed9077bfca5/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34315ff4fc374b285ad7f4a0bf7dcbfe769e1b104230d40f49f700d4ab6bbd84", size = 202956, upload-time = "2026-03-15T18:51:25.239Z" }, + { url = "https://files.pythonhosted.org/packages/75/13/f3550a3ac25b70f87ac98c40d3199a8503676c2f1620efbf8d42095cfc40/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ddd609f9e1af8c7bd6e2aca279c931aefecd148a14402d4e368f3171769fd", size = 201923, upload-time = "2026-03-15T18:51:26.682Z" }, + { url = "https://files.pythonhosted.org/packages/1b/db/c5c643b912740b45e8eec21de1bbab8e7fc085944d37e1e709d3dcd9d72f/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:80d0a5615143c0b3225e5e3ef22c8d5d51f3f72ce0ea6fb84c943546c7b25b6c", size = 195366, upload-time = "2026-03-15T18:51:28.129Z" }, + { url = "https://files.pythonhosted.org/packages/5a/67/3b1c62744f9b2448443e0eb160d8b001c849ec3fef591e012eda6484787c/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:92734d4d8d187a354a556626c221cd1a892a4e0802ccb2af432a1d85ec012194", size = 219752, upload-time = "2026-03-15T18:51:29.556Z" }, + { url = "https://files.pythonhosted.org/packages/f6/98/32ffbaf7f0366ffb0445930b87d103f6b406bc2c271563644bde8a2b1093/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:613f19aa6e082cf96e17e3ffd89383343d0d589abda756b7764cf78361fd41dc", size = 203296, upload-time = "2026-03-15T18:51:30.921Z" }, + { url = "https://files.pythonhosted.org/packages/41/12/5d308c1bbe60cabb0c5ef511574a647067e2a1f631bc8634fcafaccd8293/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2b1a63e8224e401cafe7739f77efd3f9e7f5f2026bda4aead8e59afab537784f", size = 215956, upload-time = "2026-03-15T18:51:32.399Z" }, + { url = "https://files.pythonhosted.org/packages/53/e9/5f85f6c5e20669dbe56b165c67b0260547dea97dba7e187938833d791687/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6cceb5473417d28edd20c6c984ab6fee6c6267d38d906823ebfe20b03d607dc2", size = 208652, upload-time = "2026-03-15T18:51:34.214Z" }, + { url = "https://files.pythonhosted.org/packages/f1/11/897052ea6af56df3eef3ca94edafee410ca699ca0c7b87960ad19932c55e/charset_normalizer-3.4.6-cp313-cp313-win32.whl", hash = "sha256:d7de2637729c67d67cf87614b566626057e95c303bc0a55ffe391f5205e7003d", size = 143940, upload-time = "2026-03-15T18:51:36.15Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5c/724b6b363603e419829f561c854b87ed7c7e31231a7908708ac086cdf3e2/charset_normalizer-3.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:572d7c822caf521f0525ba1bce1a622a0b85cf47ffbdae6c9c19e3b5ac3c4389", size = 154101, upload-time = "2026-03-15T18:51:37.876Z" }, + { url = "https://files.pythonhosted.org/packages/01/a5/7abf15b4c0968e47020f9ca0935fb3274deb87cb288cd187cad92e8cdffd/charset_normalizer-3.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a4474d924a47185a06411e0064b803c68be044be2d60e50e8bddcc2649957c1f", size = 143109, upload-time = "2026-03-15T18:51:39.565Z" }, + { url = "https://files.pythonhosted.org/packages/41/85/580dbaa12ab31041ed7df59f0bebc8893514fc21da6c05c3a1c1707d118f/charset_normalizer-3.4.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:31215157227939b4fb3d740cd23fe27be0439afef67b785a1eb78a3ae69cba9e", size = 298620, upload-time = "2026-03-15T18:52:57.332Z" }, + { url = "https://files.pythonhosted.org/packages/67/2c/1e55af3a5e2f52e44396d5c5b731e0ae4f3bb92915ff09a610fb2f4497eb/charset_normalizer-3.4.6-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecbbd45615a6885fe3240eb9db73b9e62518b611850fdf8ab08bd56de7ad2b17", size = 200106, upload-time = "2026-03-15T18:52:59.2Z" }, + { url = "https://files.pythonhosted.org/packages/10/42/0f2f51a1d16caa45fbf384fd337d4242df1a5b313babee211381d2d39a96/charset_normalizer-3.4.6-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c45a03a4c69820a399f1dda9e1d8fbf3562eda46e7720458180302021b08f778", size = 220539, upload-time = "2026-03-15T18:53:01.019Z" }, + { url = "https://files.pythonhosted.org/packages/1c/0c/4e10996c740eec0f4ae8afbbbfa25f66e8479c4b6ee9cff1ca366a4f6c04/charset_normalizer-3.4.6-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e8aeb10fcbe92767f0fa69ad5a72deca50d0dca07fbde97848997d778a50c9fe", size = 215821, upload-time = "2026-03-15T18:53:02.621Z" }, + { url = "https://files.pythonhosted.org/packages/46/73/205ae7644ebb581a7c6fa9c3751e283606e145f0e6f066003c66aafc9973/charset_normalizer-3.4.6-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:54fae94be3d75f3e573c9a1b5402dc593de19377013c9a0e4285e3d402dd3a2a", size = 207917, upload-time = "2026-03-15T18:53:04.413Z" }, + { url = "https://files.pythonhosted.org/packages/b3/ca/18f7dcf19afdab8097aeb2feb8b3809bb4b6ee356cb720abf5263d79406a/charset_normalizer-3.4.6-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:2f7fdd9b6e6c529d6a2501a2d36b240109e78a8ceaef5687cfcfa2bbe671d297", size = 194513, upload-time = "2026-03-15T18:53:06.025Z" }, + { url = "https://files.pythonhosted.org/packages/e4/6a/e7e3e204c8d79832a091e00b24595af1d5d9800d37dc1f67a6b264cc99a6/charset_normalizer-3.4.6-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1d02209e06550bdaef34af58e041ad71b88e624f5d825519da3a3308e22687", size = 205612, upload-time = "2026-03-15T18:53:07.494Z" }, + { url = "https://files.pythonhosted.org/packages/ff/ae/2169ebcea2851c5460c7a21993a0f87028be3c3e60899cb36251e1135cf5/charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8bc5f0687d796c05b1e28ab0d38a50e6309906ee09375dd3aff6a9c09dd6e8f4", size = 203519, upload-time = "2026-03-15T18:53:09.048Z" }, + { url = "https://files.pythonhosted.org/packages/43/a0/6a49a925b9c225fe35dffeac5c76f68996b814c637e9d7213718f96be109/charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:ee4ec14bc1680d6b0afab9aea2ef27e26d2024f18b24a2d7155a52b60da7e833", size = 195411, upload-time = "2026-03-15T18:53:10.542Z" }, + { url = "https://files.pythonhosted.org/packages/47/f7/a26b0a18e52b1a0f11f53c2c400ed062f386ac227a64ae4be4c5a64699be/charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:d1a2ee9c1499fc8f86f4521f27a973c914b211ffa87322f4ee33bb35392da2c5", size = 221653, upload-time = "2026-03-15T18:53:12.394Z" }, + { url = "https://files.pythonhosted.org/packages/a7/3a/ed1d3b5bb55e3634bd5c31cedbe4fff79d0e5b8d9a062f663a757a07760d/charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:48696db7f18afb80a068821504296eb0787d9ce239b91ca15059d1d3eaacf13b", size = 205650, upload-time = "2026-03-15T18:53:13.934Z" }, + { url = "https://files.pythonhosted.org/packages/b1/27/c75819eea5ceeefc49bae329327bb91e81adc346e2a9873d9fdb9e77cde6/charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4f41da960b196ea355357285ad1316a00099f22d0929fe168343b99b254729c9", size = 216919, upload-time = "2026-03-15T18:53:15.44Z" }, + { url = "https://files.pythonhosted.org/packages/0f/42/6e91bf8b15f67b7c957091138a36057a083e60703cc27848d5e36ca1eb03/charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:802168e03fba8bbc5ce0d866d589e4b1ca751d06edee69f7f3a19c5a9fe6b597", size = 210101, upload-time = "2026-03-15T18:53:17.045Z" }, + { url = "https://files.pythonhosted.org/packages/99/ff/101af2605e66a7ee59961d7f9e1060df7c92e8ea54208a02ab881422c24e/charset_normalizer-3.4.6-cp39-cp39-win32.whl", hash = "sha256:8761ac29b6c81574724322a554605608a9960769ea83d2c73e396f3df896ad54", size = 144136, upload-time = "2026-03-15T18:53:19.152Z" }, + { url = "https://files.pythonhosted.org/packages/1d/da/de5942dfbf21f28c19e9202267dabf7bc73f195465d020a3a60054520cc5/charset_normalizer-3.4.6-cp39-cp39-win_amd64.whl", hash = "sha256:1cf0a70018692f85172348fe06d3a4b63f94ecb055e13a00c644d368eb82e5b8", size = 154210, upload-time = "2026-03-15T18:53:20.576Z" }, + { url = "https://files.pythonhosted.org/packages/06/df/1b780a25b86d22b1d736f6ac883afd38ffdf30ddc18e5dc0e82211f493f1/charset_normalizer-3.4.6-cp39-cp39-win_arm64.whl", hash = "sha256:3516bbb8d42169de9e61b8520cbeeeb716f12f4ecfe3fd30a9919aa16c806ca8", size = 143225, upload-time = "2026-03-15T18:53:22.072Z" }, + { url = "https://files.pythonhosted.org/packages/2a/68/687187c7e26cb24ccbd88e5069f5ef00eba804d36dde11d99aad0838ab45/charset_normalizer-3.4.6-py3-none-any.whl", hash = "sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69", size = 61455, upload-time = "2026-03-15T18:53:23.833Z" }, +] + +[[package]] +name = "click" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version >= '3.12' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, +] +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, 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, 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, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.10.7" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/51/26/d22c300112504f5f9a9fd2297ce33c35f3d353e4aeb987c8419453b2a7c2/coverage-7.10.7.tar.gz", hash = "sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239", size = 827704, upload-time = "2025-09-21T20:03:56.815Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/6c/3a3f7a46888e69d18abe3ccc6fe4cb16cccb1e6a2f99698931dafca489e6/coverage-7.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a", size = 217987, upload-time = "2025-09-21T20:00:57.218Z" }, + { url = "https://files.pythonhosted.org/packages/03/94/952d30f180b1a916c11a56f5c22d3535e943aa22430e9e3322447e520e1c/coverage-7.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5", size = 218388, upload-time = "2025-09-21T20:01:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/50/2b/9e0cf8ded1e114bcd8b2fd42792b57f1c4e9e4ea1824cde2af93a67305be/coverage-7.10.7-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:240af60539987ced2c399809bd34f7c78e8abe0736af91c3d7d0e795df633d17", size = 245148, upload-time = "2025-09-21T20:01:01.768Z" }, + { url = "https://files.pythonhosted.org/packages/19/20/d0384ac06a6f908783d9b6aa6135e41b093971499ec488e47279f5b846e6/coverage-7.10.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b", size = 246958, upload-time = "2025-09-21T20:01:03.355Z" }, + { url = "https://files.pythonhosted.org/packages/60/83/5c283cff3d41285f8eab897651585db908a909c572bdc014bcfaf8a8b6ae/coverage-7.10.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87", size = 248819, upload-time = "2025-09-21T20:01:04.968Z" }, + { url = "https://files.pythonhosted.org/packages/60/22/02eb98fdc5ff79f423e990d877693e5310ae1eab6cb20ae0b0b9ac45b23b/coverage-7.10.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e28299d9f2e889e6d51b1f043f58d5f997c373cc12e6403b90df95b8b047c13e", size = 245754, upload-time = "2025-09-21T20:01:06.321Z" }, + { url = "https://files.pythonhosted.org/packages/b4/bc/25c83bcf3ad141b32cd7dc45485ef3c01a776ca3aa8ef0a93e77e8b5bc43/coverage-7.10.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c4e16bd7761c5e454f4efd36f345286d6f7c5fa111623c355691e2755cae3b9e", size = 246860, upload-time = "2025-09-21T20:01:07.605Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b7/95574702888b58c0928a6e982038c596f9c34d52c5e5107f1eef729399b5/coverage-7.10.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b1c81d0e5e160651879755c9c675b974276f135558cf4ba79fee7b8413a515df", size = 244877, upload-time = "2025-09-21T20:01:08.829Z" }, + { url = "https://files.pythonhosted.org/packages/47/b6/40095c185f235e085df0e0b158f6bd68cc6e1d80ba6c7721dc81d97ec318/coverage-7.10.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:606cc265adc9aaedcc84f1f064f0e8736bc45814f15a357e30fca7ecc01504e0", size = 245108, upload-time = "2025-09-21T20:01:10.527Z" }, + { url = "https://files.pythonhosted.org/packages/c8/50/4aea0556da7a4b93ec9168420d170b55e2eb50ae21b25062513d020c6861/coverage-7.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:10b24412692df990dbc34f8fb1b6b13d236ace9dfdd68df5b28c2e39cafbba13", size = 245752, upload-time = "2025-09-21T20:01:11.857Z" }, + { url = "https://files.pythonhosted.org/packages/6a/28/ea1a84a60828177ae3b100cb6723838523369a44ec5742313ed7db3da160/coverage-7.10.7-cp310-cp310-win32.whl", hash = "sha256:b51dcd060f18c19290d9b8a9dd1e0181538df2ce0717f562fff6cf74d9fc0b5b", size = 220497, upload-time = "2025-09-21T20:01:13.459Z" }, + { url = "https://files.pythonhosted.org/packages/fc/1a/a81d46bbeb3c3fd97b9602ebaa411e076219a150489bcc2c025f151bd52d/coverage-7.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:3a622ac801b17198020f09af3eaf45666b344a0d69fc2a6ffe2ea83aeef1d807", size = 221392, upload-time = "2025-09-21T20:01:14.722Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5d/c1a17867b0456f2e9ce2d8d4708a4c3a089947d0bec9c66cdf60c9e7739f/coverage-7.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59", size = 218102, upload-time = "2025-09-21T20:01:16.089Z" }, + { url = "https://files.pythonhosted.org/packages/54/f0/514dcf4b4e3698b9a9077f084429681bf3aad2b4a72578f89d7f643eb506/coverage-7.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a", size = 218505, upload-time = "2025-09-21T20:01:17.788Z" }, + { url = "https://files.pythonhosted.org/packages/20/f6/9626b81d17e2a4b25c63ac1b425ff307ecdeef03d67c9a147673ae40dc36/coverage-7.10.7-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699", size = 248898, upload-time = "2025-09-21T20:01:19.488Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ef/bd8e719c2f7417ba03239052e099b76ea1130ac0cbb183ee1fcaa58aaff3/coverage-7.10.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d", size = 250831, upload-time = "2025-09-21T20:01:20.817Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b6/bf054de41ec948b151ae2b79a55c107f5760979538f5fb80c195f2517718/coverage-7.10.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e", size = 252937, upload-time = "2025-09-21T20:01:22.171Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e5/3860756aa6f9318227443c6ce4ed7bf9e70bb7f1447a0353f45ac5c7974b/coverage-7.10.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6b8b09c1fad947c84bbbc95eca841350fad9cbfa5a2d7ca88ac9f8d836c92e23", size = 249021, upload-time = "2025-09-21T20:01:23.907Z" }, + { url = "https://files.pythonhosted.org/packages/26/0f/bd08bd042854f7fd07b45808927ebcce99a7ed0f2f412d11629883517ac2/coverage-7.10.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4376538f36b533b46f8971d3a3e63464f2c7905c9800db97361c43a2b14792ab", size = 250626, upload-time = "2025-09-21T20:01:25.721Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a7/4777b14de4abcc2e80c6b1d430f5d51eb18ed1d75fca56cbce5f2db9b36e/coverage-7.10.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:121da30abb574f6ce6ae09840dae322bef734480ceafe410117627aa54f76d82", size = 248682, upload-time = "2025-09-21T20:01:27.105Z" }, + { url = "https://files.pythonhosted.org/packages/34/72/17d082b00b53cd45679bad682fac058b87f011fd8b9fe31d77f5f8d3a4e4/coverage-7.10.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:88127d40df529336a9836870436fc2751c339fbaed3a836d42c93f3e4bd1d0a2", size = 248402, upload-time = "2025-09-21T20:01:28.629Z" }, + { url = "https://files.pythonhosted.org/packages/81/7a/92367572eb5bdd6a84bfa278cc7e97db192f9f45b28c94a9ca1a921c3577/coverage-7.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ba58bbcd1b72f136080c0bccc2400d66cc6115f3f906c499013d065ac33a4b61", size = 249320, upload-time = "2025-09-21T20:01:30.004Z" }, + { url = "https://files.pythonhosted.org/packages/2f/88/a23cc185f6a805dfc4fdf14a94016835eeb85e22ac3a0e66d5e89acd6462/coverage-7.10.7-cp311-cp311-win32.whl", hash = "sha256:972b9e3a4094b053a4e46832b4bc829fc8a8d347160eb39d03f1690316a99c14", size = 220536, upload-time = "2025-09-21T20:01:32.184Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ef/0b510a399dfca17cec7bc2f05ad8bd78cf55f15c8bc9a73ab20c5c913c2e/coverage-7.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:a7b55a944a7f43892e28ad4bc0561dfd5f0d73e605d1aa5c3c976b52aea121d2", size = 221425, upload-time = "2025-09-21T20:01:33.557Z" }, + { url = "https://files.pythonhosted.org/packages/51/7f/023657f301a276e4ba1850f82749bc136f5a7e8768060c2e5d9744a22951/coverage-7.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:736f227fb490f03c6488f9b6d45855f8e0fd749c007f9303ad30efab0e73c05a", size = 220103, upload-time = "2025-09-21T20:01:34.929Z" }, + { url = "https://files.pythonhosted.org/packages/13/e4/eb12450f71b542a53972d19117ea5a5cea1cab3ac9e31b0b5d498df1bd5a/coverage-7.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417", size = 218290, upload-time = "2025-09-21T20:01:36.455Z" }, + { url = "https://files.pythonhosted.org/packages/37/66/593f9be12fc19fb36711f19a5371af79a718537204d16ea1d36f16bd78d2/coverage-7.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973", size = 218515, upload-time = "2025-09-21T20:01:37.982Z" }, + { url = "https://files.pythonhosted.org/packages/66/80/4c49f7ae09cafdacc73fbc30949ffe77359635c168f4e9ff33c9ebb07838/coverage-7.10.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c", size = 250020, upload-time = "2025-09-21T20:01:39.617Z" }, + { url = "https://files.pythonhosted.org/packages/a6/90/a64aaacab3b37a17aaedd83e8000142561a29eb262cede42d94a67f7556b/coverage-7.10.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7", size = 252769, upload-time = "2025-09-21T20:01:41.341Z" }, + { url = "https://files.pythonhosted.org/packages/98/2e/2dda59afd6103b342e096f246ebc5f87a3363b5412609946c120f4e7750d/coverage-7.10.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6", size = 253901, upload-time = "2025-09-21T20:01:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/53/dc/8d8119c9051d50f3119bb4a75f29f1e4a6ab9415cd1fa8bf22fcc3fb3b5f/coverage-7.10.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59", size = 250413, upload-time = "2025-09-21T20:01:44.469Z" }, + { url = "https://files.pythonhosted.org/packages/98/b3/edaff9c5d79ee4d4b6d3fe046f2b1d799850425695b789d491a64225d493/coverage-7.10.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b", size = 251820, upload-time = "2025-09-21T20:01:45.915Z" }, + { url = "https://files.pythonhosted.org/packages/11/25/9a0728564bb05863f7e513e5a594fe5ffef091b325437f5430e8cfb0d530/coverage-7.10.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a", size = 249941, upload-time = "2025-09-21T20:01:47.296Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fd/ca2650443bfbef5b0e74373aac4df67b08180d2f184b482c41499668e258/coverage-7.10.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb", size = 249519, upload-time = "2025-09-21T20:01:48.73Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/f692f125fb4299b6f963b0745124998ebb8e73ecdfce4ceceb06a8c6bec5/coverage-7.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1", size = 251375, upload-time = "2025-09-21T20:01:50.529Z" }, + { url = "https://files.pythonhosted.org/packages/5e/75/61b9bbd6c7d24d896bfeec57acba78e0f8deac68e6baf2d4804f7aae1f88/coverage-7.10.7-cp312-cp312-win32.whl", hash = "sha256:77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256", size = 220699, upload-time = "2025-09-21T20:01:51.941Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f3/3bf7905288b45b075918d372498f1cf845b5b579b723c8fd17168018d5f5/coverage-7.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba", size = 221512, upload-time = "2025-09-21T20:01:53.481Z" }, + { url = "https://files.pythonhosted.org/packages/5c/44/3e32dbe933979d05cf2dac5e697c8599cfe038aaf51223ab901e208d5a62/coverage-7.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf", size = 220147, upload-time = "2025-09-21T20:01:55.2Z" }, + { url = "https://files.pythonhosted.org/packages/9a/94/b765c1abcb613d103b64fcf10395f54d69b0ef8be6a0dd9c524384892cc7/coverage-7.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d", size = 218320, upload-time = "2025-09-21T20:01:56.629Z" }, + { url = "https://files.pythonhosted.org/packages/72/4f/732fff31c119bb73b35236dd333030f32c4bfe909f445b423e6c7594f9a2/coverage-7.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b", size = 218575, upload-time = "2025-09-21T20:01:58.203Z" }, + { url = "https://files.pythonhosted.org/packages/87/02/ae7e0af4b674be47566707777db1aa375474f02a1d64b9323e5813a6cdd5/coverage-7.10.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e", size = 249568, upload-time = "2025-09-21T20:01:59.748Z" }, + { url = "https://files.pythonhosted.org/packages/a2/77/8c6d22bf61921a59bce5471c2f1f7ac30cd4ac50aadde72b8c48d5727902/coverage-7.10.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b", size = 252174, upload-time = "2025-09-21T20:02:01.192Z" }, + { url = "https://files.pythonhosted.org/packages/b1/20/b6ea4f69bbb52dac0aebd62157ba6a9dddbfe664f5af8122dac296c3ee15/coverage-7.10.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49", size = 253447, upload-time = "2025-09-21T20:02:02.701Z" }, + { url = "https://files.pythonhosted.org/packages/f9/28/4831523ba483a7f90f7b259d2018fef02cb4d5b90bc7c1505d6e5a84883c/coverage-7.10.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911", size = 249779, upload-time = "2025-09-21T20:02:04.185Z" }, + { url = "https://files.pythonhosted.org/packages/a7/9f/4331142bc98c10ca6436d2d620c3e165f31e6c58d43479985afce6f3191c/coverage-7.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0", size = 251604, upload-time = "2025-09-21T20:02:06.034Z" }, + { url = "https://files.pythonhosted.org/packages/ce/60/bda83b96602036b77ecf34e6393a3836365481b69f7ed7079ab85048202b/coverage-7.10.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f", size = 249497, upload-time = "2025-09-21T20:02:07.619Z" }, + { url = "https://files.pythonhosted.org/packages/5f/af/152633ff35b2af63977edd835d8e6430f0caef27d171edf2fc76c270ef31/coverage-7.10.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c", size = 249350, upload-time = "2025-09-21T20:02:10.34Z" }, + { url = "https://files.pythonhosted.org/packages/9d/71/d92105d122bd21cebba877228990e1646d862e34a98bb3374d3fece5a794/coverage-7.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f", size = 251111, upload-time = "2025-09-21T20:02:12.122Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9e/9fdb08f4bf476c912f0c3ca292e019aab6712c93c9344a1653986c3fd305/coverage-7.10.7-cp313-cp313-win32.whl", hash = "sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698", size = 220746, upload-time = "2025-09-21T20:02:13.919Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b1/a75fd25df44eab52d1931e89980d1ada46824c7a3210be0d3c88a44aaa99/coverage-7.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843", size = 221541, upload-time = "2025-09-21T20:02:15.57Z" }, + { url = "https://files.pythonhosted.org/packages/14/3a/d720d7c989562a6e9a14b2c9f5f2876bdb38e9367126d118495b89c99c37/coverage-7.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546", size = 220170, upload-time = "2025-09-21T20:02:17.395Z" }, + { url = "https://files.pythonhosted.org/packages/bb/22/e04514bf2a735d8b0add31d2b4ab636fc02370730787c576bb995390d2d5/coverage-7.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c", size = 219029, upload-time = "2025-09-21T20:02:18.936Z" }, + { url = "https://files.pythonhosted.org/packages/11/0b/91128e099035ece15da3445d9015e4b4153a6059403452d324cbb0a575fa/coverage-7.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15", size = 219259, upload-time = "2025-09-21T20:02:20.44Z" }, + { url = "https://files.pythonhosted.org/packages/8b/51/66420081e72801536a091a0c8f8c1f88a5c4bf7b9b1bdc6222c7afe6dc9b/coverage-7.10.7-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4", size = 260592, upload-time = "2025-09-21T20:02:22.313Z" }, + { url = "https://files.pythonhosted.org/packages/5d/22/9b8d458c2881b22df3db5bb3e7369e63d527d986decb6c11a591ba2364f7/coverage-7.10.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0", size = 262768, upload-time = "2025-09-21T20:02:24.287Z" }, + { url = "https://files.pythonhosted.org/packages/f7/08/16bee2c433e60913c610ea200b276e8eeef084b0d200bdcff69920bd5828/coverage-7.10.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0", size = 264995, upload-time = "2025-09-21T20:02:26.133Z" }, + { url = "https://files.pythonhosted.org/packages/20/9d/e53eb9771d154859b084b90201e5221bca7674ba449a17c101a5031d4054/coverage-7.10.7-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65", size = 259546, upload-time = "2025-09-21T20:02:27.716Z" }, + { url = "https://files.pythonhosted.org/packages/ad/b0/69bc7050f8d4e56a89fb550a1577d5d0d1db2278106f6f626464067b3817/coverage-7.10.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541", size = 262544, upload-time = "2025-09-21T20:02:29.216Z" }, + { url = "https://files.pythonhosted.org/packages/ef/4b/2514b060dbd1bc0aaf23b852c14bb5818f244c664cb16517feff6bb3a5ab/coverage-7.10.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6", size = 260308, upload-time = "2025-09-21T20:02:31.226Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/7ba2175007c246d75e496f64c06e94122bdb914790a1285d627a918bd271/coverage-7.10.7-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999", size = 258920, upload-time = "2025-09-21T20:02:32.823Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/fac9f7abbc841409b9a410309d73bfa6cfb2e51c3fada738cb607ce174f8/coverage-7.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2", size = 261434, upload-time = "2025-09-21T20:02:34.86Z" }, + { url = "https://files.pythonhosted.org/packages/ee/51/a03bec00d37faaa891b3ff7387192cef20f01604e5283a5fabc95346befa/coverage-7.10.7-cp313-cp313t-win32.whl", hash = "sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a", size = 221403, upload-time = "2025-09-21T20:02:37.034Z" }, + { url = "https://files.pythonhosted.org/packages/53/22/3cf25d614e64bf6d8e59c7c669b20d6d940bb337bdee5900b9ca41c820bb/coverage-7.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb", size = 222469, upload-time = "2025-09-21T20:02:39.011Z" }, + { url = "https://files.pythonhosted.org/packages/49/a1/00164f6d30d8a01c3c9c48418a7a5be394de5349b421b9ee019f380df2a0/coverage-7.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb", size = 220731, upload-time = "2025-09-21T20:02:40.939Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ad/d1c25053764b4c42eb294aae92ab617d2e4f803397f9c7c8295caa77a260/coverage-7.10.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fff7b9c3f19957020cac546c70025331113d2e61537f6e2441bc7657913de7d3", size = 217978, upload-time = "2025-09-21T20:03:30.362Z" }, + { url = "https://files.pythonhosted.org/packages/52/2f/b9f9daa39b80ece0b9548bbb723381e29bc664822d9a12c2135f8922c22b/coverage-7.10.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bc91b314cef27742da486d6839b677b3f2793dfe52b51bbbb7cf736d5c29281c", size = 218370, upload-time = "2025-09-21T20:03:32.147Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6e/30d006c3b469e58449650642383dddf1c8fb63d44fdf92994bfd46570695/coverage-7.10.7-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:567f5c155eda8df1d3d439d40a45a6a5f029b429b06648235f1e7e51b522b396", size = 244802, upload-time = "2025-09-21T20:03:33.919Z" }, + { url = "https://files.pythonhosted.org/packages/b0/49/8a070782ce7e6b94ff6a0b6d7c65ba6bc3091d92a92cef4cd4eb0767965c/coverage-7.10.7-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2af88deffcc8a4d5974cf2d502251bc3b2db8461f0b66d80a449c33757aa9f40", size = 246625, upload-time = "2025-09-21T20:03:36.09Z" }, + { url = "https://files.pythonhosted.org/packages/6a/92/1c1c5a9e8677ce56d42b97bdaca337b2d4d9ebe703d8c174ede52dbabd5f/coverage-7.10.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7315339eae3b24c2d2fa1ed7d7a38654cba34a13ef19fbcb9425da46d3dc594", size = 248399, upload-time = "2025-09-21T20:03:38.342Z" }, + { url = "https://files.pythonhosted.org/packages/c0/54/b140edee7257e815de7426d5d9846b58505dffc29795fff2dfb7f8a1c5a0/coverage-7.10.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:912e6ebc7a6e4adfdbb1aec371ad04c68854cd3bf3608b3514e7ff9062931d8a", size = 245142, upload-time = "2025-09-21T20:03:40.591Z" }, + { url = "https://files.pythonhosted.org/packages/e4/9e/6d6b8295940b118e8b7083b29226c71f6154f7ff41e9ca431f03de2eac0d/coverage-7.10.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f49a05acd3dfe1ce9715b657e28d138578bc40126760efb962322c56e9ca344b", size = 246284, upload-time = "2025-09-21T20:03:42.355Z" }, + { url = "https://files.pythonhosted.org/packages/db/e5/5e957ca747d43dbe4d9714358375c7546cb3cb533007b6813fc20fce37ad/coverage-7.10.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:cce2109b6219f22ece99db7644b9622f54a4e915dad65660ec435e89a3ea7cc3", size = 244353, upload-time = "2025-09-21T20:03:44.218Z" }, + { url = "https://files.pythonhosted.org/packages/9a/45/540fc5cc92536a1b783b7ef99450bd55a4b3af234aae35a18a339973ce30/coverage-7.10.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:f3c887f96407cea3916294046fc7dab611c2552beadbed4ea901cbc6a40cc7a0", size = 244430, upload-time = "2025-09-21T20:03:46.065Z" }, + { url = "https://files.pythonhosted.org/packages/75/0b/8287b2e5b38c8fe15d7e3398849bb58d382aedc0864ea0fa1820e8630491/coverage-7.10.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:635adb9a4507c9fd2ed65f39693fa31c9a3ee3a8e6dc64df033e8fdf52a7003f", size = 245311, upload-time = "2025-09-21T20:03:48.19Z" }, + { url = "https://files.pythonhosted.org/packages/0c/1d/29724999984740f0c86d03e6420b942439bf5bd7f54d4382cae386a9d1e9/coverage-7.10.7-cp39-cp39-win32.whl", hash = "sha256:5a02d5a850e2979b0a014c412573953995174743a3f7fa4ea5a6e9a3c5617431", size = 220500, upload-time = "2025-09-21T20:03:50.024Z" }, + { url = "https://files.pythonhosted.org/packages/43/11/4b1e6b129943f905ca54c339f343877b55b365ae2558806c1be4f7476ed5/coverage-7.10.7-cp39-cp39-win_amd64.whl", hash = "sha256:c134869d5ffe34547d14e174c866fd8fe2254918cc0a95e99052903bc1543e07", size = 221408, upload-time = "2025-09-21T20:03:51.803Z" }, + { url = "https://files.pythonhosted.org/packages/ec/16/114df1c291c22cac3b0c127a73e0af5c12ed7bbb6558d310429a0ae24023/coverage-7.10.7-py3-none-any.whl", hash = "sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260", size = 209952, upload-time = "2025-09-21T20:03:53.918Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version < '3.10'" }, +] + +[[package]] +name = "coverage" +version = "7.13.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version >= '3.12' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/33/e8c48488c29a73fd089f9d71f9653c1be7478f2ad6b5bc870db11a55d23d/coverage-7.13.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0723d2c96324561b9aa76fb982406e11d93cdb388a7a7da2b16e04719cf7ca5", size = 219255, upload-time = "2026-03-17T10:29:51.081Z" }, + { url = "https://files.pythonhosted.org/packages/da/bd/b0ebe9f677d7f4b74a3e115eec7ddd4bcf892074963a00d91e8b164a6386/coverage-7.13.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52f444e86475992506b32d4e5ca55c24fc88d73bcbda0e9745095b28ef4dc0cf", size = 219772, upload-time = "2026-03-17T10:29:52.867Z" }, + { url = "https://files.pythonhosted.org/packages/48/cc/5cb9502f4e01972f54eedd48218bb203fe81e294be606a2bc93970208013/coverage-7.13.5-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:704de6328e3d612a8f6c07000a878ff38181ec3263d5a11da1db294fa6a9bdf8", size = 246532, upload-time = "2026-03-17T10:29:54.688Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d8/3217636d86c7e7b12e126e4f30ef1581047da73140614523af7495ed5f2d/coverage-7.13.5-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a1a6d79a14e1ec1832cabc833898636ad5f3754a678ef8bb4908515208bf84f4", size = 248333, upload-time = "2026-03-17T10:29:56.221Z" }, + { url = "https://files.pythonhosted.org/packages/2b/30/2002ac6729ba2d4357438e2ed3c447ad8562866c8c63fc16f6dfc33afe56/coverage-7.13.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79060214983769c7ba3f0cee10b54c97609dca4d478fa1aa32b914480fd5738d", size = 250211, upload-time = "2026-03-17T10:29:57.938Z" }, + { url = "https://files.pythonhosted.org/packages/6c/85/552496626d6b9359eb0e2f86f920037c9cbfba09b24d914c6e1528155f7d/coverage-7.13.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:356e76b46783a98c2a2fe81ec79df4883a1e62895ea952968fb253c114e7f930", size = 252125, upload-time = "2026-03-17T10:29:59.388Z" }, + { url = "https://files.pythonhosted.org/packages/44/21/40256eabdcbccdb6acf6b381b3016a154399a75fe39d406f790ae84d1f3c/coverage-7.13.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0cef0cdec915d11254a7f549c1170afecce708d30610c6abdded1f74e581666d", size = 247219, upload-time = "2026-03-17T10:30:01.199Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e8/96e2a6c3f21a0ea77d7830b254a1542d0328acc8d7bdf6a284ba7e529f77/coverage-7.13.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dc022073d063b25a402454e5712ef9e007113e3a676b96c5f29b2bda29352f40", size = 248248, upload-time = "2026-03-17T10:30:03.317Z" }, + { url = "https://files.pythonhosted.org/packages/da/ba/8477f549e554827da390ec659f3c38e4b6d95470f4daafc2d8ff94eaa9c2/coverage-7.13.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9b74db26dfea4f4e50d48a4602207cd1e78be33182bc9cbf22da94f332f99878", size = 246254, upload-time = "2026-03-17T10:30:04.832Z" }, + { url = "https://files.pythonhosted.org/packages/55/59/bc22aef0e6aa179d5b1b001e8b3654785e9adf27ef24c93dc4228ebd5d68/coverage-7.13.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ad146744ca4fd09b50c482650e3c1b1f4dfa1d4792e0a04a369c7f23336f0400", size = 250067, upload-time = "2026-03-17T10:30:06.535Z" }, + { url = "https://files.pythonhosted.org/packages/de/1b/c6a023a160806a5137dca53468fd97530d6acad24a22003b1578a9c2e429/coverage-7.13.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:c555b48be1853fe3997c11c4bd521cdd9a9612352de01fa4508f16ec341e6fe0", size = 246521, upload-time = "2026-03-17T10:30:08.486Z" }, + { url = "https://files.pythonhosted.org/packages/2d/3f/3532c85a55aa2f899fa17c186f831cfa1aa434d88ff792a709636f64130e/coverage-7.13.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7034b5c56a58ae5e85f23949d52c14aca2cfc6848a31764995b7de88f13a1ea0", size = 247126, upload-time = "2026-03-17T10:30:09.966Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2e/b9d56af4a24ef45dfbcda88e06870cb7d57b2b0bfa3a888d79b4c8debd76/coverage-7.13.5-cp310-cp310-win32.whl", hash = "sha256:eb7fdf1ef130660e7415e0253a01a7d5a88c9c4d158bcf75cbbd922fd65a5b58", size = 221860, upload-time = "2026-03-17T10:30:11.393Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cc/d938417e7a4d7f0433ad4edee8bb2acdc60dc7ac5af19e2a07a048ecbee3/coverage-7.13.5-cp310-cp310-win_amd64.whl", hash = "sha256:3e1bb5f6c78feeb1be3475789b14a0f0a5b47d505bfc7267126ccbd50289999e", size = 222788, upload-time = "2026-03-17T10:30:12.886Z" }, + { url = "https://files.pythonhosted.org/packages/4b/37/d24c8f8220ff07b839b2c043ea4903a33b0f455abe673ae3c03bbdb7f212/coverage-7.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66a80c616f80181f4d643b0f9e709d97bcea413ecd9631e1dedc7401c8e6695d", size = 219381, upload-time = "2026-03-17T10:30:14.68Z" }, + { url = "https://files.pythonhosted.org/packages/35/8b/cd129b0ca4afe886a6ce9d183c44d8301acbd4ef248622e7c49a23145605/coverage-7.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:145ede53ccbafb297c1c9287f788d1bc3efd6c900da23bf6931b09eafc931587", size = 219880, upload-time = "2026-03-17T10:30:16.231Z" }, + { url = "https://files.pythonhosted.org/packages/55/2f/e0e5b237bffdb5d6c530ce87cc1d413a5b7d7dfd60fb067ad6d254c35c76/coverage-7.13.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0672854dc733c342fa3e957e0605256d2bf5934feeac328da9e0b5449634a642", size = 250303, upload-time = "2026-03-17T10:30:17.748Z" }, + { url = "https://files.pythonhosted.org/packages/92/be/b1afb692be85b947f3401375851484496134c5554e67e822c35f28bf2fbc/coverage-7.13.5-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ec10e2a42b41c923c2209b846126c6582db5e43a33157e9870ba9fb70dc7854b", size = 252218, upload-time = "2026-03-17T10:30:19.804Z" }, + { url = "https://files.pythonhosted.org/packages/da/69/2f47bb6fa1b8d1e3e5d0c4be8ccb4313c63d742476a619418f85740d597b/coverage-7.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be3d4bbad9d4b037791794ddeedd7d64a56f5933a2c1373e18e9e568b9141686", size = 254326, upload-time = "2026-03-17T10:30:21.321Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d0/79db81da58965bd29dabc8f4ad2a2af70611a57cba9d1ec006f072f30a54/coverage-7.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d2afbc5cc54d286bfb54541aa50b64cdb07a718227168c87b9e2fb8f25e1743", size = 256267, upload-time = "2026-03-17T10:30:23.094Z" }, + { url = "https://files.pythonhosted.org/packages/e5/32/d0d7cc8168f91ddab44c0ce4806b969df5f5fdfdbb568eaca2dbc2a04936/coverage-7.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ad050321264c49c2fa67bb599100456fc51d004b82534f379d16445da40fb75", size = 250430, upload-time = "2026-03-17T10:30:25.311Z" }, + { url = "https://files.pythonhosted.org/packages/4d/06/a055311d891ddbe231cd69fdd20ea4be6e3603ffebddf8704b8ca8e10a3c/coverage-7.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7300c8a6d13335b29bb76d7651c66af6bd8658517c43499f110ddc6717bfc209", size = 252017, upload-time = "2026-03-17T10:30:27.284Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f6/d0fd2d21e29a657b5f77a2fe7082e1568158340dceb941954f776dce1b7b/coverage-7.13.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:eb07647a5738b89baab047f14edd18ded523de60f3b30e75c2acc826f79c839a", size = 250080, upload-time = "2026-03-17T10:30:29.481Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ab/0d7fb2efc2e9a5eb7ddcc6e722f834a69b454b7e6e5888c3a8567ecffb31/coverage-7.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9adb6688e3b53adffefd4a52d72cbd8b02602bfb8f74dcd862337182fd4d1a4e", size = 253843, upload-time = "2026-03-17T10:30:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/ba/6f/7467b917bbf5408610178f62a49c0ed4377bb16c1657f689cc61470da8ce/coverage-7.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7c8d4bc913dd70b93488d6c496c77f3aff5ea99a07e36a18f865bca55adef8bd", size = 249802, upload-time = "2026-03-17T10:30:33.358Z" }, + { url = "https://files.pythonhosted.org/packages/75/2c/1172fb689df92135f5bfbbd69fc83017a76d24ea2e2f3a1154007e2fb9f8/coverage-7.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0e3c426ffc4cd952f54ee9ffbdd10345709ecc78a3ecfd796a57236bfad0b9b8", size = 250707, upload-time = "2026-03-17T10:30:35.2Z" }, + { url = "https://files.pythonhosted.org/packages/67/21/9ac389377380a07884e3b48ba7a620fcd9dbfaf1d40565facdc6b36ec9ef/coverage-7.13.5-cp311-cp311-win32.whl", hash = "sha256:259b69bb83ad9894c4b25be2528139eecba9a82646ebdda2d9db1ba28424a6bf", size = 221880, upload-time = "2026-03-17T10:30:36.775Z" }, + { url = "https://files.pythonhosted.org/packages/af/7f/4cd8a92531253f9d7c1bbecd9fa1b472907fb54446ca768c59b531248dc5/coverage-7.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:258354455f4e86e3e9d0d17571d522e13b4e1e19bf0f8596bcf9476d61e7d8a9", size = 222816, upload-time = "2026-03-17T10:30:38.891Z" }, + { url = "https://files.pythonhosted.org/packages/12/a6/1d3f6155fb0010ca68eba7fe48ca6c9da7385058b77a95848710ecf189b1/coverage-7.13.5-cp311-cp311-win_arm64.whl", hash = "sha256:bff95879c33ec8da99fc9b6fe345ddb5be6414b41d6d1ad1c8f188d26f36e028", size = 221483, upload-time = "2026-03-17T10:30:40.463Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c3/a396306ba7db865bf96fc1fb3b7fd29bcbf3d829df642e77b13555163cd6/coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01", size = 219554, upload-time = "2026-03-17T10:30:42.208Z" }, + { url = "https://files.pythonhosted.org/packages/a6/16/a68a19e5384e93f811dccc51034b1fd0b865841c390e3c931dcc4699e035/coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422", size = 219908, upload-time = "2026-03-17T10:30:43.906Z" }, + { url = "https://files.pythonhosted.org/packages/29/72/20b917c6793af3a5ceb7fb9c50033f3ec7865f2911a1416b34a7cfa0813b/coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f", size = 251419, upload-time = "2026-03-17T10:30:45.545Z" }, + { url = "https://files.pythonhosted.org/packages/8c/49/cd14b789536ac6a4778c453c6a2338bc0a2fb60c5a5a41b4008328b9acc1/coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5", size = 254159, upload-time = "2026-03-17T10:30:47.204Z" }, + { url = "https://files.pythonhosted.org/packages/9d/00/7b0edcfe64e2ed4c0340dac14a52ad0f4c9bd0b8b5e531af7d55b703db7c/coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376", size = 255270, upload-time = "2026-03-17T10:30:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/93/89/7ffc4ba0f5d0a55c1e84ea7cee39c9fc06af7b170513d83fbf3bbefce280/coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256", size = 257538, upload-time = "2026-03-17T10:30:50.77Z" }, + { url = "https://files.pythonhosted.org/packages/81/bd/73ddf85f93f7e6fa83e77ccecb6162d9415c79007b4bc124008a4995e4a7/coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c", size = 251821, upload-time = "2026-03-17T10:30:52.5Z" }, + { url = "https://files.pythonhosted.org/packages/a0/81/278aff4e8dec4926a0bcb9486320752811f543a3ce5b602cc7a29978d073/coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5", size = 253191, upload-time = "2026-03-17T10:30:54.543Z" }, + { url = "https://files.pythonhosted.org/packages/70/ee/fe1621488e2e0a58d7e94c4800f0d96f79671553488d401a612bebae324b/coverage-7.13.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09", size = 251337, upload-time = "2026-03-17T10:30:56.663Z" }, + { url = "https://files.pythonhosted.org/packages/37/a6/f79fb37aa104b562207cc23cb5711ab6793608e246cae1e93f26b2236ed9/coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9", size = 255404, upload-time = "2026-03-17T10:30:58.427Z" }, + { url = "https://files.pythonhosted.org/packages/75/f0/ed15262a58ec81ce457ceb717b7f78752a1713556b19081b76e90896e8d4/coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf", size = 250903, upload-time = "2026-03-17T10:31:00.093Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e9/9129958f20e7e9d4d56d51d42ccf708d15cac355ff4ac6e736e97a9393d2/coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c", size = 252780, upload-time = "2026-03-17T10:31:01.916Z" }, + { url = "https://files.pythonhosted.org/packages/a4/d7/0ad9b15812d81272db94379fe4c6df8fd17781cc7671fdfa30c76ba5ff7b/coverage-7.13.5-cp312-cp312-win32.whl", hash = "sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf", size = 222093, upload-time = "2026-03-17T10:31:03.642Z" }, + { url = "https://files.pythonhosted.org/packages/29/3d/821a9a5799fac2556bcf0bd37a70d1d11fa9e49784b6d22e92e8b2f85f18/coverage-7.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810", size = 222900, upload-time = "2026-03-17T10:31:05.651Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fa/2238c2ad08e35cf4f020ea721f717e09ec3152aea75d191a7faf3ef009a8/coverage-7.13.5-cp312-cp312-win_arm64.whl", hash = "sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de", size = 221515, upload-time = "2026-03-17T10:31:07.293Z" }, + { url = "https://files.pythonhosted.org/packages/74/8c/74fedc9663dcf168b0a059d4ea756ecae4da77a489048f94b5f512a8d0b3/coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1", size = 219576, upload-time = "2026-03-17T10:31:09.045Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3", size = 219942, upload-time = "2026-03-17T10:31:10.708Z" }, + { url = "https://files.pythonhosted.org/packages/5f/13/93419671cee82b780bab7ea96b67c8ef448f5f295f36bf5031154ec9a790/coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26", size = 250935, upload-time = "2026-03-17T10:31:12.392Z" }, + { url = "https://files.pythonhosted.org/packages/ac/68/1666e3a4462f8202d836920114fa7a5ee9275d1fa45366d336c551a162dd/coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3", size = 253541, upload-time = "2026-03-17T10:31:14.247Z" }, + { url = "https://files.pythonhosted.org/packages/4e/5e/3ee3b835647be646dcf3c65a7c6c18f87c27326a858f72ab22c12730773d/coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b", size = 254780, upload-time = "2026-03-17T10:31:16.193Z" }, + { url = "https://files.pythonhosted.org/packages/44/b3/cb5bd1a04cfcc49ede6cd8409d80bee17661167686741e041abc7ee1b9a9/coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a", size = 256912, upload-time = "2026-03-17T10:31:17.89Z" }, + { url = "https://files.pythonhosted.org/packages/1b/66/c1dceb7b9714473800b075f5c8a84f4588f887a90eb8645282031676e242/coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969", size = 251165, upload-time = "2026-03-17T10:31:19.605Z" }, + { url = "https://files.pythonhosted.org/packages/b7/62/5502b73b97aa2e53ea22a39cf8649ff44827bef76d90bf638777daa27a9d/coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161", size = 252908, upload-time = "2026-03-17T10:31:21.312Z" }, + { url = "https://files.pythonhosted.org/packages/7d/37/7792c2d69854397ca77a55c4646e5897c467928b0e27f2d235d83b5d08c6/coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15", size = 250873, upload-time = "2026-03-17T10:31:23.565Z" }, + { url = "https://files.pythonhosted.org/packages/a3/23/bc866fb6163be52a8a9e5d708ba0d3b1283c12158cefca0a8bbb6e247a43/coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1", size = 255030, upload-time = "2026-03-17T10:31:25.58Z" }, + { url = "https://files.pythonhosted.org/packages/7d/8b/ef67e1c222ef49860701d346b8bbb70881bef283bd5f6cbba68a39a086c7/coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6", size = 250694, upload-time = "2026-03-17T10:31:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/46/0d/866d1f74f0acddbb906db212e096dee77a8e2158ca5e6bb44729f9d93298/coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17", size = 252469, upload-time = "2026-03-17T10:31:29.472Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f5/be742fec31118f02ce42b21c6af187ad6a344fed546b56ca60caacc6a9a0/coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85", size = 222112, upload-time = "2026-03-17T10:31:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/66/40/7732d648ab9d069a46e686043241f01206348e2bbf128daea85be4d6414b/coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b", size = 222923, upload-time = "2026-03-17T10:31:33.633Z" }, + { url = "https://files.pythonhosted.org/packages/48/af/fea819c12a095781f6ccd504890aaddaf88b8fab263c4940e82c7b770124/coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664", size = 221540, upload-time = "2026-03-17T10:31:35.445Z" }, + { url = "https://files.pythonhosted.org/packages/23/d2/17879af479df7fbbd44bd528a31692a48f6b25055d16482fdf5cdb633805/coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d", size = 220262, upload-time = "2026-03-17T10:31:37.184Z" }, + { url = "https://files.pythonhosted.org/packages/5b/4c/d20e554f988c8f91d6a02c5118f9abbbf73a8768a3048cb4962230d5743f/coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0", size = 220617, upload-time = "2026-03-17T10:31:39.245Z" }, + { url = "https://files.pythonhosted.org/packages/29/9c/f9f5277b95184f764b24e7231e166dfdb5780a46d408a2ac665969416d61/coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806", size = 261912, upload-time = "2026-03-17T10:31:41.324Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f6/7f1ab39393eeb50cfe4747ae8ef0e4fc564b989225aa1152e13a180d74f8/coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3", size = 263987, upload-time = "2026-03-17T10:31:43.724Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d7/62c084fb489ed9c6fbdf57e006752e7c516ea46fd690e5ed8b8617c7d52e/coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9", size = 266416, upload-time = "2026-03-17T10:31:45.769Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f6/df63d8660e1a0bff6125947afda112a0502736f470d62ca68b288ea762d8/coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd", size = 267558, upload-time = "2026-03-17T10:31:48.293Z" }, + { url = "https://files.pythonhosted.org/packages/5b/02/353ca81d36779bd108f6d384425f7139ac3c58c750dcfaafe5d0bee6436b/coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606", size = 261163, upload-time = "2026-03-17T10:31:50.125Z" }, + { url = "https://files.pythonhosted.org/packages/2c/16/2e79106d5749bcaf3aee6d309123548e3276517cd7851faa8da213bc61bf/coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e", size = 263981, upload-time = "2026-03-17T10:31:51.961Z" }, + { url = "https://files.pythonhosted.org/packages/29/c7/c29e0c59ffa6942030ae6f50b88ae49988e7e8da06de7ecdbf49c6d4feae/coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0", size = 261604, upload-time = "2026-03-17T10:31:53.872Z" }, + { url = "https://files.pythonhosted.org/packages/40/48/097cdc3db342f34006a308ab41c3a7c11c3f0d84750d340f45d88a782e00/coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87", size = 265321, upload-time = "2026-03-17T10:31:55.997Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1f/4994af354689e14fd03a75f8ec85a9a68d94e0188bbdab3fc1516b55e512/coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479", size = 260502, upload-time = "2026-03-17T10:31:58.308Z" }, + { url = "https://files.pythonhosted.org/packages/22/c6/9bb9ef55903e628033560885f5c31aa227e46878118b63ab15dc7ba87797/coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2", size = 262688, upload-time = "2026-03-17T10:32:00.141Z" }, + { url = "https://files.pythonhosted.org/packages/14/4f/f5df9007e50b15e53e01edea486814783a7f019893733d9e4d6caad75557/coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a", size = 222788, upload-time = "2026-03-17T10:32:02.246Z" }, + { url = "https://files.pythonhosted.org/packages/e1/98/aa7fccaa97d0f3192bec013c4e6fd6d294a6ed44b640e6bb61f479e00ed5/coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819", size = 223851, upload-time = "2026-03-17T10:32:04.416Z" }, + { url = "https://files.pythonhosted.org/packages/3d/8b/e5c469f7352651e5f013198e9e21f97510b23de957dd06a84071683b4b60/coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911", size = 222104, upload-time = "2026-03-17T10:32:06.65Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version >= '3.10' and python_full_version <= '3.11'" }, +] + +[[package]] +name = "diff-diff" +version = "2.7.1" +source = { editable = "." } +dependencies = [ + { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "pandas", version = "3.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "scipy", version = "1.13.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] + +[package.optional-dependencies] +dev = [ + { name = "black", version = "25.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "black", version = "26.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "maturin" }, + { name = "mypy" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytest-cov" }, + { name = "pytest-xdist" }, + { name = "ruff" }, +] +docs = [ + { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "sphinx-rtd-theme" }, +] + +[package.metadata] +requires-dist = [ + { name = "black", marker = "extra == 'dev'", specifier = ">=23.0" }, + { name = "maturin", marker = "extra == 'dev'", specifier = ">=1.4,<2.0" }, + { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.0" }, + { name = "numpy", specifier = ">=1.20.0" }, + { name = "pandas", specifier = ">=1.3.0" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0" }, + { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.0" }, + { name = "pytest-xdist", marker = "extra == 'dev'", specifier = ">=3.0" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.1.0" }, + { name = "scipy", specifier = ">=1.7.0" }, + { name = "sphinx", marker = "extra == 'docs'", specifier = ">=6.0" }, + { name = "sphinx-rtd-theme", marker = "extra == 'docs'", specifier = ">=1.0" }, +] +provides-extras = ["dev", "docs"] + +[[package]] +name = "docutils" +version = "0.21.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.10.*'", + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444, upload-time = "2024-04-23T18:57:18.24Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408, upload-time = "2024-04-23T18:57:14.835Z" }, +] + +[[package]] +name = "docutils" +version = "0.22.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version >= '3.12' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "execnet" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, +] + +[[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, 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, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "imagesize" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/cf/59/4b0dd64676aa6fb4986a755790cb6fc558559cf0084effad516820208ec3/imagesize-1.5.0.tar.gz", hash = "sha256:8bfc5363a7f2133a89f0098451e0bcb1cd71aba4dc02bbcecb39d99d40e1b94f", size = 1281127, upload-time = "2026-03-03T01:59:54.651Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/b1/a0662b03103c66cf77101a187f396ea91167cd9b7d5d3a2e465ad2c7ee9b/imagesize-1.5.0-py2.py3-none-any.whl", hash = "sha256:32677681b3f434c2cb496f00e89c5a291247b35b1f527589909e008057da5899", size = 5763, upload-time = "2026-03-03T01:59:52.343Z" }, +] + +[[package]] +name = "imagesize" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version >= '3.12' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/6c/e6/7bf14eeb8f8b7251141944835abd42eb20a658d89084b7e1f3e5fe394090/imagesize-2.0.0.tar.gz", hash = "sha256:8e8358c4a05c304f1fccf7ff96f036e7243a189e9e42e90851993c558cfe9ee3", size = 1773045, upload-time = "2026-03-03T14:18:29.941Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/53/fb7122b71361a0d121b669dcf3d31244ef75badbbb724af388948de543e2/imagesize-2.0.0-py2.py3-none-any.whl", hash = "sha256:5667c5bbb57ab3f1fa4bc366f4fbc971db3d5ed011fd2715fd8001f782718d96", size = 9441, upload-time = "2026-03-03T14:18:27.892Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version >= '3.12' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.10.*'", +] +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, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +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, 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, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "librt" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/56/9c/b4b0c54d84da4a94b37bd44151e46d5e583c9534c7e02250b961b1b6d8a8/librt-0.8.1.tar.gz", hash = "sha256:be46a14693955b3bd96014ccbdb8339ee8c9346fbe11c1b78901b55125f14c73", size = 177471, upload-time = "2026-02-17T16:13:06.101Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/5f/63f5fa395c7a8a93558c0904ba8f1c8d1b997ca6a3de61bc7659970d66bf/librt-0.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:81fd938344fecb9373ba1b155968c8a329491d2ce38e7ddb76f30ffb938f12dc", size = 65697, upload-time = "2026-02-17T16:11:06.903Z" }, + { url = "https://files.pythonhosted.org/packages/ff/e0/0472cf37267b5920eff2f292ccfaede1886288ce35b7f3203d8de00abfe6/librt-0.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5db05697c82b3a2ec53f6e72b2ed373132b0c2e05135f0696784e97d7f5d48e7", size = 68376, upload-time = "2026-02-17T16:11:08.395Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8bd1359fdcd27ab897cd5963294fa4a7c83b20a8564678e4fd12157e56a5/librt-0.8.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d56bc4011975f7460bea7b33e1ff425d2f1adf419935ff6707273c77f8a4ada6", size = 197084, upload-time = "2026-02-17T16:11:09.774Z" }, + { url = "https://files.pythonhosted.org/packages/e2/fe/163e33fdd091d0c2b102f8a60cc0a61fd730ad44e32617cd161e7cd67a01/librt-0.8.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdc0f588ff4b663ea96c26d2a230c525c6fc62b28314edaaaca8ed5af931ad0", size = 207337, upload-time = "2026-02-17T16:11:11.311Z" }, + { url = "https://files.pythonhosted.org/packages/01/99/f85130582f05dcf0c8902f3d629270231d2f4afdfc567f8305a952ac7f14/librt-0.8.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:97c2b54ff6717a7a563b72627990bec60d8029df17df423f0ed37d56a17a176b", size = 219980, upload-time = "2026-02-17T16:11:12.499Z" }, + { url = "https://files.pythonhosted.org/packages/6f/54/cb5e4d03659e043a26c74e08206412ac9a3742f0477d96f9761a55313b5f/librt-0.8.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8f1125e6bbf2f1657d9a2f3ccc4a2c9b0c8b176965bb565dd4d86be67eddb4b6", size = 212921, upload-time = "2026-02-17T16:11:14.484Z" }, + { url = "https://files.pythonhosted.org/packages/b1/81/a3a01e4240579c30f3487f6fed01eb4bc8ef0616da5b4ebac27ca19775f3/librt-0.8.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8f4bb453f408137d7581be309b2fbc6868a80e7ef60c88e689078ee3a296ae71", size = 221381, upload-time = "2026-02-17T16:11:17.459Z" }, + { url = "https://files.pythonhosted.org/packages/08/b0/fc2d54b4b1c6fb81e77288ff31ff25a2c1e62eaef4424a984f228839717b/librt-0.8.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c336d61d2fe74a3195edc1646d53ff1cddd3a9600b09fa6ab75e5514ba4862a7", size = 216714, upload-time = "2026-02-17T16:11:19.197Z" }, + { url = "https://files.pythonhosted.org/packages/96/96/85daa73ffbd87e1fb287d7af6553ada66bf25a2a6b0de4764344a05469f6/librt-0.8.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:eb5656019db7c4deacf0c1a55a898c5bb8f989be904597fcb5232a2f4828fa05", size = 214777, upload-time = "2026-02-17T16:11:20.443Z" }, + { url = "https://files.pythonhosted.org/packages/12/9c/c3aa7a2360383f4bf4f04d98195f2739a579128720c603f4807f006a4225/librt-0.8.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c25d9e338d5bed46c1632f851babf3d13c78f49a225462017cf5e11e845c5891", size = 237398, upload-time = "2026-02-17T16:11:22.083Z" }, + { url = "https://files.pythonhosted.org/packages/61/19/d350ea89e5274665185dabc4bbb9c3536c3411f862881d316c8b8e00eb66/librt-0.8.1-cp310-cp310-win32.whl", hash = "sha256:aaab0e307e344cb28d800957ef3ec16605146ef0e59e059a60a176d19543d1b7", size = 54285, upload-time = "2026-02-17T16:11:23.27Z" }, + { url = "https://files.pythonhosted.org/packages/4f/d6/45d587d3d41c112e9543a0093d883eb57a24a03e41561c127818aa2a6bcc/librt-0.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:56e04c14b696300d47b3bc5f1d10a00e86ae978886d0cee14e5714fafb5df5d2", size = 61352, upload-time = "2026-02-17T16:11:24.207Z" }, + { url = "https://files.pythonhosted.org/packages/1d/01/0e748af5e4fee180cf7cd12bd12b0513ad23b045dccb2a83191bde82d168/librt-0.8.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:681dc2451d6d846794a828c16c22dc452d924e9f700a485b7ecb887a30aad1fd", size = 65315, upload-time = "2026-02-17T16:11:25.152Z" }, + { url = "https://files.pythonhosted.org/packages/9d/4d/7184806efda571887c798d573ca4134c80ac8642dcdd32f12c31b939c595/librt-0.8.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3b4350b13cc0e6f5bec8fa7caf29a8fb8cdc051a3bae45cfbfd7ce64f009965", size = 68021, upload-time = "2026-02-17T16:11:26.129Z" }, + { url = "https://files.pythonhosted.org/packages/ae/88/c3c52d2a5d5101f28d3dc89298444626e7874aa904eed498464c2af17627/librt-0.8.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ac1e7817fd0ed3d14fd7c5df91daed84c48e4c2a11ee99c0547f9f62fdae13da", size = 194500, upload-time = "2026-02-17T16:11:27.177Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5d/6fb0a25b6a8906e85b2c3b87bee1d6ed31510be7605b06772f9374ca5cb3/librt-0.8.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:747328be0c5b7075cde86a0e09d7a9196029800ba75a1689332348e998fb85c0", size = 205622, upload-time = "2026-02-17T16:11:28.242Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a6/8006ae81227105476a45691f5831499e4d936b1c049b0c1feb17c11b02d1/librt-0.8.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0af2bd2bc204fa27f3d6711d0f360e6b8c684a035206257a81673ab924aa11e", size = 218304, upload-time = "2026-02-17T16:11:29.344Z" }, + { url = "https://files.pythonhosted.org/packages/ee/19/60e07886ad16670aae57ef44dada41912c90906a6fe9f2b9abac21374748/librt-0.8.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d480de377f5b687b6b1bc0c0407426da556e2a757633cc7e4d2e1a057aa688f3", size = 211493, upload-time = "2026-02-17T16:11:30.445Z" }, + { url = "https://files.pythonhosted.org/packages/9c/cf/f666c89d0e861d05600438213feeb818c7514d3315bae3648b1fc145d2b6/librt-0.8.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d0ee06b5b5291f609ddb37b9750985b27bc567791bc87c76a569b3feed8481ac", size = 219129, upload-time = "2026-02-17T16:11:32.021Z" }, + { url = "https://files.pythonhosted.org/packages/8f/ef/f1bea01e40b4a879364c031476c82a0dc69ce068daad67ab96302fed2d45/librt-0.8.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9e2c6f77b9ad48ce5603b83b7da9ee3e36b3ab425353f695cba13200c5d96596", size = 213113, upload-time = "2026-02-17T16:11:33.192Z" }, + { url = "https://files.pythonhosted.org/packages/9b/80/cdab544370cc6bc1b72ea369525f547a59e6938ef6863a11ab3cd24759af/librt-0.8.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:439352ba9373f11cb8e1933da194dcc6206daf779ff8df0ed69c5e39113e6a99", size = 212269, upload-time = "2026-02-17T16:11:34.373Z" }, + { url = "https://files.pythonhosted.org/packages/9d/9c/48d6ed8dac595654f15eceab2035131c136d1ae9a1e3548e777bb6dbb95d/librt-0.8.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:82210adabbc331dbb65d7868b105185464ef13f56f7f76688565ad79f648b0fe", size = 234673, upload-time = "2026-02-17T16:11:36.063Z" }, + { url = "https://files.pythonhosted.org/packages/16/01/35b68b1db517f27a01be4467593292eb5315def8900afad29fabf56304ba/librt-0.8.1-cp311-cp311-win32.whl", hash = "sha256:52c224e14614b750c0a6d97368e16804a98c684657c7518752c356834fff83bb", size = 54597, upload-time = "2026-02-17T16:11:37.544Z" }, + { url = "https://files.pythonhosted.org/packages/71/02/796fe8f02822235966693f257bf2c79f40e11337337a657a8cfebba5febc/librt-0.8.1-cp311-cp311-win_amd64.whl", hash = "sha256:c00e5c884f528c9932d278d5c9cbbea38a6b81eb62c02e06ae53751a83a4d52b", size = 61733, upload-time = "2026-02-17T16:11:38.691Z" }, + { url = "https://files.pythonhosted.org/packages/28/ad/232e13d61f879a42a4e7117d65e4984bb28371a34bb6fb9ca54ec2c8f54e/librt-0.8.1-cp311-cp311-win_arm64.whl", hash = "sha256:f7cdf7f26c2286ffb02e46d7bac56c94655540b26347673bea15fa52a6af17e9", size = 52273, upload-time = "2026-02-17T16:11:40.308Z" }, + { url = "https://files.pythonhosted.org/packages/95/21/d39b0a87ac52fc98f621fb6f8060efb017a767ebbbac2f99fbcbc9ddc0d7/librt-0.8.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a28f2612ab566b17f3698b0da021ff9960610301607c9a5e8eaca62f5e1c350a", size = 66516, upload-time = "2026-02-17T16:11:41.604Z" }, + { url = "https://files.pythonhosted.org/packages/69/f1/46375e71441c43e8ae335905e069f1c54febee63a146278bcee8782c84fd/librt-0.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:60a78b694c9aee2a0f1aaeaa7d101cf713e92e8423a941d2897f4fa37908dab9", size = 68634, upload-time = "2026-02-17T16:11:43.268Z" }, + { url = "https://files.pythonhosted.org/packages/0a/33/c510de7f93bf1fa19e13423a606d8189a02624a800710f6e6a0a0f0784b3/librt-0.8.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:758509ea3f1eba2a57558e7e98f4659d0ea7670bff49673b0dde18a3c7e6c0eb", size = 198941, upload-time = "2026-02-17T16:11:44.28Z" }, + { url = "https://files.pythonhosted.org/packages/dd/36/e725903416409a533d92398e88ce665476f275081d0d7d42f9c4951999e5/librt-0.8.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:039b9f2c506bd0ab0f8725aa5ba339c6f0cd19d3b514b50d134789809c24285d", size = 209991, upload-time = "2026-02-17T16:11:45.462Z" }, + { url = "https://files.pythonhosted.org/packages/30/7a/8d908a152e1875c9f8eac96c97a480df425e657cdb47854b9efaa4998889/librt-0.8.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bb54f1205a3a6ab41a6fd71dfcdcbd278670d3a90ca502a30d9da583105b6f7", size = 224476, upload-time = "2026-02-17T16:11:46.542Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b8/a22c34f2c485b8903a06f3fe3315341fe6876ef3599792344669db98fcff/librt-0.8.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:05bd41cdee35b0c59c259f870f6da532a2c5ca57db95b5f23689fcb5c9e42440", size = 217518, upload-time = "2026-02-17T16:11:47.746Z" }, + { url = "https://files.pythonhosted.org/packages/79/6f/5c6fea00357e4f82ba44f81dbfb027921f1ab10e320d4a64e1c408d035d9/librt-0.8.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adfab487facf03f0d0857b8710cf82d0704a309d8ffc33b03d9302b4c64e91a9", size = 225116, upload-time = "2026-02-17T16:11:49.298Z" }, + { url = "https://files.pythonhosted.org/packages/f2/a0/95ced4e7b1267fe1e2720a111685bcddf0e781f7e9e0ce59d751c44dcfe5/librt-0.8.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:153188fe98a72f206042be10a2c6026139852805215ed9539186312d50a8e972", size = 217751, upload-time = "2026-02-17T16:11:50.49Z" }, + { url = "https://files.pythonhosted.org/packages/93/c2/0517281cb4d4101c27ab59472924e67f55e375bc46bedae94ac6dc6e1902/librt-0.8.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dd3c41254ee98604b08bd5b3af5bf0a89740d4ee0711de95b65166bf44091921", size = 218378, upload-time = "2026-02-17T16:11:51.783Z" }, + { url = "https://files.pythonhosted.org/packages/43/e8/37b3ac108e8976888e559a7b227d0ceac03c384cfd3e7a1c2ee248dbae79/librt-0.8.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e0d138c7ae532908cbb342162b2611dbd4d90c941cd25ab82084aaf71d2c0bd0", size = 241199, upload-time = "2026-02-17T16:11:53.561Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5b/35812d041c53967fedf551a39399271bbe4257e681236a2cf1a69c8e7fa1/librt-0.8.1-cp312-cp312-win32.whl", hash = "sha256:43353b943613c5d9c49a25aaffdba46f888ec354e71e3529a00cca3f04d66a7a", size = 54917, upload-time = "2026-02-17T16:11:54.758Z" }, + { url = "https://files.pythonhosted.org/packages/de/d1/fa5d5331b862b9775aaf2a100f5ef86854e5d4407f71bddf102f4421e034/librt-0.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:ff8baf1f8d3f4b6b7257fcb75a501f2a5499d0dda57645baa09d4d0d34b19444", size = 62017, upload-time = "2026-02-17T16:11:55.748Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7c/c614252f9acda59b01a66e2ddfd243ed1c7e1deab0293332dfbccf862808/librt-0.8.1-cp312-cp312-win_arm64.whl", hash = "sha256:0f2ae3725904f7377e11cc37722d5d401e8b3d5851fb9273d7f4fe04f6b3d37d", size = 52441, upload-time = "2026-02-17T16:11:56.801Z" }, + { url = "https://files.pythonhosted.org/packages/c5/3c/f614c8e4eaac7cbf2bbdf9528790b21d89e277ee20d57dc6e559c626105f/librt-0.8.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7e6bad1cd94f6764e1e21950542f818a09316645337fd5ab9a7acc45d99a8f35", size = 66529, upload-time = "2026-02-17T16:11:57.809Z" }, + { url = "https://files.pythonhosted.org/packages/ab/96/5836544a45100ae411eda07d29e3d99448e5258b6e9c8059deb92945f5c2/librt-0.8.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cf450f498c30af55551ba4f66b9123b7185362ec8b625a773b3d39aa1a717583", size = 68669, upload-time = "2026-02-17T16:11:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/06/53/f0b992b57af6d5531bf4677d75c44f095f2366a1741fb695ee462ae04b05/librt-0.8.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:eca45e982fa074090057132e30585a7e8674e9e885d402eae85633e9f449ce6c", size = 199279, upload-time = "2026-02-17T16:11:59.862Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ad/4848cc16e268d14280d8168aee4f31cea92bbd2b79ce33d3e166f2b4e4fc/librt-0.8.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c3811485fccfda840861905b8c70bba5ec094e02825598bb9d4ca3936857a04", size = 210288, upload-time = "2026-02-17T16:12:00.954Z" }, + { url = "https://files.pythonhosted.org/packages/52/05/27fdc2e95de26273d83b96742d8d3b7345f2ea2bdbd2405cc504644f2096/librt-0.8.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e4af413908f77294605e28cfd98063f54b2c790561383971d2f52d113d9c363", size = 224809, upload-time = "2026-02-17T16:12:02.108Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d0/78200a45ba3240cb042bc597d6f2accba9193a2c57d0356268cbbe2d0925/librt-0.8.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5212a5bd7fae98dae95710032902edcd2ec4dc994e883294f75c857b83f9aba0", size = 218075, upload-time = "2026-02-17T16:12:03.631Z" }, + { url = "https://files.pythonhosted.org/packages/af/72/a210839fa74c90474897124c064ffca07f8d4b347b6574d309686aae7ca6/librt-0.8.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e692aa2d1d604e6ca12d35e51fdc36f4cda6345e28e36374579f7ef3611b3012", size = 225486, upload-time = "2026-02-17T16:12:04.725Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c1/a03cc63722339ddbf087485f253493e2b013039f5b707e8e6016141130fa/librt-0.8.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4be2a5c926b9770c9e08e717f05737a269b9d0ebc5d2f0060f0fe3fe9ce47acb", size = 218219, upload-time = "2026-02-17T16:12:05.828Z" }, + { url = "https://files.pythonhosted.org/packages/58/f5/fff6108af0acf941c6f274a946aea0e484bd10cd2dc37610287ce49388c5/librt-0.8.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fd1a720332ea335ceb544cf0a03f81df92abd4bb887679fd1e460976b0e6214b", size = 218750, upload-time = "2026-02-17T16:12:07.09Z" }, + { url = "https://files.pythonhosted.org/packages/71/67/5a387bfef30ec1e4b4f30562c8586566faf87e47d696768c19feb49e3646/librt-0.8.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2af9e01e0ef80d95ae3c720be101227edae5f2fe7e3dc63d8857fadfc5a1d", size = 241624, upload-time = "2026-02-17T16:12:08.43Z" }, + { url = "https://files.pythonhosted.org/packages/d4/be/24f8502db11d405232ac1162eb98069ca49c3306c1d75c6ccc61d9af8789/librt-0.8.1-cp313-cp313-win32.whl", hash = "sha256:086a32dbb71336627e78cc1d6ee305a68d038ef7d4c39aaff41ae8c9aa46e91a", size = 54969, upload-time = "2026-02-17T16:12:09.633Z" }, + { url = "https://files.pythonhosted.org/packages/5c/73/c9fdf6cb2a529c1a092ce769a12d88c8cca991194dfe641b6af12fa964d2/librt-0.8.1-cp313-cp313-win_amd64.whl", hash = "sha256:e11769a1dbda4da7b00a76cfffa67aa47cfa66921d2724539eee4b9ede780b79", size = 62000, upload-time = "2026-02-17T16:12:10.632Z" }, + { url = "https://files.pythonhosted.org/packages/d3/97/68f80ca3ac4924f250cdfa6e20142a803e5e50fca96ef5148c52ee8c10ea/librt-0.8.1-cp313-cp313-win_arm64.whl", hash = "sha256:924817ab3141aca17893386ee13261f1d100d1ef410d70afe4389f2359fea4f0", size = 52495, upload-time = "2026-02-17T16:12:11.633Z" }, + { url = "https://files.pythonhosted.org/packages/01/1f/c7d8b66a3ca3ca3ed8ded4b32c96ee58a45920ebbbaa934355c74adcc33e/librt-0.8.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3dff3d3ca8db20e783b1bc7de49c0a2ab0b8387f31236d6a026597d07fcd68ac", size = 65990, upload-time = "2026-02-17T16:12:48.972Z" }, + { url = "https://files.pythonhosted.org/packages/56/be/ee9ba1730052313d08457f19beaa1b878619978863fba09b40aed5b5c123/librt-0.8.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:08eec3a1fc435f0d09c87b6bf1ec798986a3544f446b864e4099633a56fcd9ed", size = 68640, upload-time = "2026-02-17T16:12:50.24Z" }, + { url = "https://files.pythonhosted.org/packages/81/27/b7309298b96f7690cec3ceee38004c1a7f60fcd96d952d3ac344a1e3e8b3/librt-0.8.1-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e3f0a41487fd5fad7e760b9e8a90e251e27c2816fbc2cff36a22a0e6bcbbd9dd", size = 196099, upload-time = "2026-02-17T16:12:52.788Z" }, + { url = "https://files.pythonhosted.org/packages/10/48/160a5aacdcb21824b10a52378c39e88c46a29bb31efdaf3910dd1f9b670e/librt-0.8.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bacdb58d9939d95cc557b4dbaa86527c9db2ac1ed76a18bc8d26f6dc8647d851", size = 206663, upload-time = "2026-02-17T16:12:55.017Z" }, + { url = "https://files.pythonhosted.org/packages/ee/65/33dd1d8caabb7c6805d87d095b143417dc96b0277c06ffa0508361422c82/librt-0.8.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6d7ab1f01aa753188605b09a51faa44a3327400b00b8cce424c71910fc0a128", size = 219318, upload-time = "2026-02-17T16:12:56.145Z" }, + { url = "https://files.pythonhosted.org/packages/09/d4/353805aa6181c7950a2462bd6e855366eeca21a501f375228d72a51547df/librt-0.8.1-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4998009e7cb9e896569f4be7004f09d0ed70d386fa99d42b6d363f6d200501ac", size = 212191, upload-time = "2026-02-17T16:12:57.326Z" }, + { url = "https://files.pythonhosted.org/packages/06/08/725b3f304d61eba56c713c251fb833a06d84bf93381caad5152366f5d2bb/librt-0.8.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2cc68eeeef5e906839c7bb0815748b5b0a974ec27125beefc0f942715785b551", size = 220672, upload-time = "2026-02-17T16:12:58.497Z" }, + { url = "https://files.pythonhosted.org/packages/0e/55/e8cdf04145872b3b97cb9b68287b22d1c08348227063f305aec11a3e6ce7/librt-0.8.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:0bf69d79a23f4f40b8673a947a234baeeb133b5078b483b7297c5916539cf5d5", size = 216172, upload-time = "2026-02-17T16:12:59.751Z" }, + { url = "https://files.pythonhosted.org/packages/8f/d8/23b1c6592d2422dd6829c672f45b1f1c257f219926b0d216fedb572d0184/librt-0.8.1-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:22b46eabd76c1986ee7d231b0765ad387d7673bbd996aa0d0d054b38ac65d8f6", size = 214116, upload-time = "2026-02-17T16:13:01.056Z" }, + { url = "https://files.pythonhosted.org/packages/c9/92/2b44fd3cc3313f44e43bdbb41343735b568fa675fa351642b408ee48d418/librt-0.8.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:237796479f4d0637d6b9cbcb926ff424a97735e68ade6facf402df4ec93375ed", size = 236664, upload-time = "2026-02-17T16:13:02.314Z" }, + { url = "https://files.pythonhosted.org/packages/00/23/92313ecdab80e142d8ea10e8dfa6297694359dbaacc9e81679bdc8cbceb6/librt-0.8.1-cp39-cp39-win32.whl", hash = "sha256:4beb04b8c66c6ae62f8c1e0b2f097c1ebad9295c929a8d5286c05eae7c2fc7dc", size = 54368, upload-time = "2026-02-17T16:13:03.549Z" }, + { url = "https://files.pythonhosted.org/packages/68/36/18f6e768afad6b55a690d38427c53251b69b7ba8795512730fd2508b31a9/librt-0.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:64548cde61b692dc0dc379f4b5f59a2f582c2ebe7890d09c1ae3b9e66fa015b7", size = 61507, upload-time = "2026-02-17T16:13:04.556Z" }, +] + +[[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, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, + { 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/56/23/0d8c13a44bde9154821586520840643467aee574d8ce79a17da539ee7fed/markupsafe-3.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26", size = 11623, upload-time = "2025-09-27T18:37:29.296Z" }, + { url = "https://files.pythonhosted.org/packages/fd/23/07a2cb9a8045d5f3f0890a8c3bc0859d7a47bfd9a560b563899bec7b72ed/markupsafe-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc", size = 12049, upload-time = "2025-09-27T18:37:30.234Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e4/6be85eb81503f8e11b61c0b6369b6e077dcf0a74adbd9ebf6b349937b4e9/markupsafe-3.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c", size = 21923, upload-time = "2025-09-27T18:37:31.177Z" }, + { url = "https://files.pythonhosted.org/packages/6f/bc/4dc914ead3fe6ddaef035341fee0fc956949bbd27335b611829292b89ee2/markupsafe-3.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42", size = 20543, upload-time = "2025-09-27T18:37:32.168Z" }, + { url = "https://files.pythonhosted.org/packages/89/6e/5fe81fbcfba4aef4093d5f856e5c774ec2057946052d18d168219b7bd9f9/markupsafe-3.0.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b", size = 20585, upload-time = "2025-09-27T18:37:33.166Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f6/e0e5a3d3ae9c4020f696cd055f940ef86b64fe88de26f3a0308b9d3d048c/markupsafe-3.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758", size = 21387, upload-time = "2025-09-27T18:37:34.185Z" }, + { url = "https://files.pythonhosted.org/packages/c8/25/651753ef4dea08ea790f4fbb65146a9a44a014986996ca40102e237aa49a/markupsafe-3.0.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2", size = 20133, upload-time = "2025-09-27T18:37:35.138Z" }, + { url = "https://files.pythonhosted.org/packages/dc/0a/c3cf2b4fef5f0426e8a6d7fce3cb966a17817c568ce59d76b92a233fdbec/markupsafe-3.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d", size = 20588, upload-time = "2025-09-27T18:37:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/cd/1b/a7782984844bd519ad4ffdbebbba2671ec5d0ebbeac34736c15fb86399e8/markupsafe-3.0.3-cp39-cp39-win32.whl", hash = "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7", size = 14566, upload-time = "2025-09-27T18:37:37.09Z" }, + { url = "https://files.pythonhosted.org/packages/18/1f/8d9c20e1c9440e215a44be5ab64359e207fcb4f675543f1cf9a2a7f648d0/markupsafe-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e", size = 15053, upload-time = "2025-09-27T18:37:38.054Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d3/fe08482b5cd995033556d45041a4f4e76e7f0521112a9c9991d40d39825f/markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8", size = 13928, upload-time = "2025-09-27T18:37:39.037Z" }, +] + +[[package]] +name = "maturin" +version = "1.12.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0c/18/8b2eebd3ea086a5ec73d7081f95ec64918ceda1900075902fc296ea3ad55/maturin-1.12.6.tar.gz", hash = "sha256:d37be3a811a7f2ee28a0fa0964187efa50e90f21da0c6135c27787fa0b6a89db", size = 269165, upload-time = "2026-03-01T14:54:04.21Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/8b/9ddfde8a485489e3ebdc50ee3042ef1c854f00dfea776b951068f6ffe451/maturin-1.12.6-py3-none-linux_armv6l.whl", hash = "sha256:6892b4176992fcc143f9d1c1c874a816e9a041248eef46433db87b0f0aff4278", size = 9789847, upload-time = "2026-03-01T14:54:09.172Z" }, + { url = "https://files.pythonhosted.org/packages/ef/e8/5f7fd3763f214a77ac0388dbcc71cc30aec5490016bd0c8e6bd729fc7b0a/maturin-1.12.6-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:c0c742beeeef7fb93b6a81bd53e75507887e396fd1003c45117658d063812dad", size = 19023833, upload-time = "2026-03-01T14:53:46.743Z" }, + { url = "https://files.pythonhosted.org/packages/e0/7f/706ff3839c8b2046436d4c2bc97596c558728264d18abc298a1ad862a4be/maturin-1.12.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2cb41139295eed6411d3cdafc7430738094c2721f34b7eeb44f33cac516115dc", size = 9821620, upload-time = "2026-03-01T14:54:12.04Z" }, + { url = "https://files.pythonhosted.org/packages/0e/9c/70917fb123c8dd6b595e913616c9c72d730cbf4a2b6cac8077dc02a12586/maturin-1.12.6-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:351f3af1488a7cbdcff3b6d8482c17164273ac981378a13a4a9937a49aec7d71", size = 9849107, upload-time = "2026-03-01T14:53:48.971Z" }, + { url = "https://files.pythonhosted.org/packages/59/ea/f1d6ad95c0a12fbe761a7c28a57540341f188564dbe8ad730a4d1788cd32/maturin-1.12.6-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:6dbddfe4dc7ddee60bbac854870bd7cfec660acb54d015d24597d59a1c828f61", size = 10242855, upload-time = "2026-03-01T14:53:44.605Z" }, + { url = "https://files.pythonhosted.org/packages/93/1b/2419843a4f1d2fb4747f3dc3d9c4a2881cd97a3274dd94738fcdf0835e79/maturin-1.12.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:8fdb0f63e77ee3df0f027a120e9af78dbc31edf0eb0f263d55783c250c33b728", size = 9674972, upload-time = "2026-03-01T14:53:52.763Z" }, + { url = "https://files.pythonhosted.org/packages/71/46/b60ab2fc996d904b40e55bd475599dcdccd8f7ad3e649bf95e87970df466/maturin-1.12.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:fa84b7493a2e80759cacc2e668fa5b444d55b9994e90707c42904f55d6322c1e", size = 9645755, upload-time = "2026-03-01T14:53:58.497Z" }, + { url = "https://files.pythonhosted.org/packages/a4/96/03f2b55a8c226805115232fc23c4a4f33f0c9d39e11efab8166dc440f80d/maturin-1.12.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:e90dc12bc6a38e9495692a36c9e231c4d7e0c9bfde60719468ab7d8673db3c45", size = 12737612, upload-time = "2026-03-01T14:54:05.393Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c2/648667022c5b53cdccefa67c245e8a984970f3045820f00c2e23bdb2aff4/maturin-1.12.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:06fc8d089f98623ce924c669b70911dfed30f9a29956c362945f727f9abc546b", size = 10455028, upload-time = "2026-03-01T14:54:07.349Z" }, + { url = "https://files.pythonhosted.org/packages/63/d6/5b5efe3ca0c043357ed3f8d2b2d556169fdbf1ff75e50e8e597708a359d2/maturin-1.12.6-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:75133e56274d43b9227fd49dca9a86e32f1fd56a7b55544910c4ce978c2bb5aa", size = 10014531, upload-time = "2026-03-01T14:53:54.548Z" }, + { url = "https://files.pythonhosted.org/packages/68/d5/39c594c27b1a8b32a0cb95fff9ad60b888c4352d1d1c389ac1bd20dc1e16/maturin-1.12.6-py3-none-win32.whl", hash = "sha256:3f32e0a3720b81423c9d35c14e728cb1f954678124749776dc72d533ea1115e8", size = 8553012, upload-time = "2026-03-01T14:53:50.706Z" }, + { url = "https://files.pythonhosted.org/packages/94/66/b262832a91747e04051e21f986bd01a8af81fbffafacc7d66a11e79aab5f/maturin-1.12.6-py3-none-win_amd64.whl", hash = "sha256:977290159d252db946054a0555263c59b3d0c7957135c69e690f4b1558ee9983", size = 9890470, upload-time = "2026-03-01T14:53:56.659Z" }, + { url = "https://files.pythonhosted.org/packages/e3/47/76b8ca470ddc8d7d36aa8c15f5a6aed1841806bb93a0f4ead8ee61e9a088/maturin-1.12.6-py3-none-win_arm64.whl", hash = "sha256:bae91976cdc8148038e13c881e1e844e5c63e58e026e8b9945aa2d19b3b4ae89", size = 8606158, upload-time = "2026-03-01T14:54:02.423Z" }, +] + +[[package]] +name = "mypy" +version = "1.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/63/e499890d8e39b1ff2df4c0c6ce5d371b6844ee22b8250687a99fd2f657a8/mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec", size = 13101333, upload-time = "2025-12-15T05:03:03.28Z" }, + { url = "https://files.pythonhosted.org/packages/72/4b/095626fc136fba96effc4fd4a82b41d688ab92124f8c4f7564bffe5cf1b0/mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b", size = 12164102, upload-time = "2025-12-15T05:02:33.611Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/952928dd081bf88a83a5ccd49aaecfcd18fd0d2710c7ff07b8fb6f7032b9/mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6", size = 12765799, upload-time = "2025-12-15T05:03:28.44Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0d/93c2e4a287f74ef11a66fb6d49c7a9f05e47b0a4399040e6719b57f500d2/mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74", size = 13522149, upload-time = "2025-12-15T05:02:36.011Z" }, + { url = "https://files.pythonhosted.org/packages/7b/0e/33a294b56aaad2b338d203e3a1d8b453637ac36cb278b45005e0901cf148/mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1", size = 13810105, upload-time = "2025-12-15T05:02:40.327Z" }, + { url = "https://files.pythonhosted.org/packages/0e/fd/3e82603a0cb66b67c5e7abababce6bf1a929ddf67bf445e652684af5c5a0/mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac", size = 10057200, upload-time = "2025-12-15T05:02:51.012Z" }, + { url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" }, + { url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933, upload-time = "2025-12-15T05:03:15.606Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754, upload-time = "2025-12-15T05:02:26.731Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", size = 10055772, upload-time = "2025-12-15T05:03:26.179Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, + { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" }, + { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, + { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, + { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" }, + { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" }, + { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f7/88436084550ca9af5e610fa45286be04c3b63374df3e021c762fe8c4369f/mypy-1.19.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7bcfc336a03a1aaa26dfce9fff3e287a3ba99872a157561cbfcebe67c13308e3", size = 13102606, upload-time = "2025-12-15T05:02:46.833Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a5/43dfad311a734b48a752790571fd9e12d61893849a01bff346a54011957f/mypy-1.19.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b7951a701c07ea584c4fe327834b92a30825514c868b1f69c30445093fdd9d5a", size = 12164496, upload-time = "2025-12-15T05:03:41.947Z" }, + { url = "https://files.pythonhosted.org/packages/88/f0/efbfa391395cce2f2771f937e0620cfd185ec88f2b9cd88711028a768e96/mypy-1.19.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b13cfdd6c87fc3efb69ea4ec18ef79c74c3f98b4e5498ca9b85ab3b2c2329a67", size = 12772068, upload-time = "2025-12-15T05:02:53.689Z" }, + { url = "https://files.pythonhosted.org/packages/25/05/58b3ba28f5aed10479e899a12d2120d582ba9fa6288851b20bf1c32cbb4f/mypy-1.19.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f28f99c824ecebcdaa2e55d82953e38ff60ee5ec938476796636b86afa3956e", size = 13520385, upload-time = "2025-12-15T05:02:38.328Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a0/c006ccaff50b31e542ae69b92fe7e2f55d99fba3a55e01067dd564325f85/mypy-1.19.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c608937067d2fc5a4dd1a5ce92fd9e1398691b8c5d012d66e1ddd430e9244376", size = 13796221, upload-time = "2025-12-15T05:03:22.147Z" }, + { url = "https://files.pythonhosted.org/packages/b2/ff/8bdb051cd710f01b880472241bd36b3f817a8e1c5d5540d0b761675b6de2/mypy-1.19.1-cp39-cp39-win_amd64.whl", hash = "sha256:409088884802d511ee52ca067707b90c883426bd95514e8cfda8281dc2effe24", size = 10055456, upload-time = "2025-12-15T05:03:35.169Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "numpy" +version = "2.0.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/75/10dd1f8116a8b796cb2c737b674e02d02e80454bda953fa7e65d8c12b016/numpy-2.0.2.tar.gz", hash = "sha256:883c987dee1880e2a864ab0dc9892292582510604156762362d9326444636e78", size = 18902015, upload-time = "2024-08-26T20:19:40.945Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/21/91/3495b3237510f79f5d81f2508f9f13fea78ebfdf07538fc7444badda173d/numpy-2.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:51129a29dbe56f9ca83438b706e2e69a39892b5eda6cedcb6b0c9fdc9b0d3ece", size = 21165245, upload-time = "2024-08-26T20:04:14.625Z" }, + { url = "https://files.pythonhosted.org/packages/05/33/26178c7d437a87082d11019292dce6d3fe6f0e9026b7b2309cbf3e489b1d/numpy-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f15975dfec0cf2239224d80e32c3170b1d168335eaedee69da84fbe9f1f9cd04", size = 13738540, upload-time = "2024-08-26T20:04:36.784Z" }, + { url = "https://files.pythonhosted.org/packages/ec/31/cc46e13bf07644efc7a4bf68df2df5fb2a1a88d0cd0da9ddc84dc0033e51/numpy-2.0.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:8c5713284ce4e282544c68d1c3b2c7161d38c256d2eefc93c1d683cf47683e66", size = 5300623, upload-time = "2024-08-26T20:04:46.491Z" }, + { url = "https://files.pythonhosted.org/packages/6e/16/7bfcebf27bb4f9d7ec67332ffebee4d1bf085c84246552d52dbb548600e7/numpy-2.0.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:becfae3ddd30736fe1889a37f1f580e245ba79a5855bff5f2a29cb3ccc22dd7b", size = 6901774, upload-time = "2024-08-26T20:04:58.173Z" }, + { url = "https://files.pythonhosted.org/packages/f9/a3/561c531c0e8bf082c5bef509d00d56f82e0ea7e1e3e3a7fc8fa78742a6e5/numpy-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2da5960c3cf0df7eafefd806d4e612c5e19358de82cb3c343631188991566ccd", size = 13907081, upload-time = "2024-08-26T20:05:19.098Z" }, + { url = "https://files.pythonhosted.org/packages/fa/66/f7177ab331876200ac7563a580140643d1179c8b4b6a6b0fc9838de2a9b8/numpy-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:496f71341824ed9f3d2fd36cf3ac57ae2e0165c143b55c3a035ee219413f3318", size = 19523451, upload-time = "2024-08-26T20:05:47.479Z" }, + { url = "https://files.pythonhosted.org/packages/25/7f/0b209498009ad6453e4efc2c65bcdf0ae08a182b2b7877d7ab38a92dc542/numpy-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a61ec659f68ae254e4d237816e33171497e978140353c0c2038d46e63282d0c8", size = 19927572, upload-time = "2024-08-26T20:06:17.137Z" }, + { url = "https://files.pythonhosted.org/packages/3e/df/2619393b1e1b565cd2d4c4403bdd979621e2c4dea1f8532754b2598ed63b/numpy-2.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d731a1c6116ba289c1e9ee714b08a8ff882944d4ad631fd411106a30f083c326", size = 14400722, upload-time = "2024-08-26T20:06:39.16Z" }, + { url = "https://files.pythonhosted.org/packages/22/ad/77e921b9f256d5da36424ffb711ae79ca3f451ff8489eeca544d0701d74a/numpy-2.0.2-cp310-cp310-win32.whl", hash = "sha256:984d96121c9f9616cd33fbd0618b7f08e0cfc9600a7ee1d6fd9b239186d19d97", size = 6472170, upload-time = "2024-08-26T20:06:50.361Z" }, + { url = "https://files.pythonhosted.org/packages/10/05/3442317535028bc29cf0c0dd4c191a4481e8376e9f0db6bcf29703cadae6/numpy-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:c7b0be4ef08607dd04da4092faee0b86607f111d5ae68036f16cc787e250a131", size = 15905558, upload-time = "2024-08-26T20:07:13.881Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cf/034500fb83041aa0286e0fb16e7c76e5c8b67c0711bb6e9e9737a717d5fe/numpy-2.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:49ca4decb342d66018b01932139c0961a8f9ddc7589611158cb3c27cbcf76448", size = 21169137, upload-time = "2024-08-26T20:07:45.345Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d9/32de45561811a4b87fbdee23b5797394e3d1504b4a7cf40c10199848893e/numpy-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:11a76c372d1d37437857280aa142086476136a8c0f373b2e648ab2c8f18fb195", size = 13703552, upload-time = "2024-08-26T20:08:06.666Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ca/2f384720020c7b244d22508cb7ab23d95f179fcfff33c31a6eeba8d6c512/numpy-2.0.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:807ec44583fd708a21d4a11d94aedf2f4f3c3719035c76a2bbe1fe8e217bdc57", size = 5298957, upload-time = "2024-08-26T20:08:15.83Z" }, + { url = "https://files.pythonhosted.org/packages/0e/78/a3e4f9fb6aa4e6fdca0c5428e8ba039408514388cf62d89651aade838269/numpy-2.0.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8cafab480740e22f8d833acefed5cc87ce276f4ece12fdaa2e8903db2f82897a", size = 6905573, upload-time = "2024-08-26T20:08:27.185Z" }, + { url = "https://files.pythonhosted.org/packages/a0/72/cfc3a1beb2caf4efc9d0b38a15fe34025230da27e1c08cc2eb9bfb1c7231/numpy-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a15f476a45e6e5a3a79d8a14e62161d27ad897381fecfa4a09ed5322f2085669", size = 13914330, upload-time = "2024-08-26T20:08:48.058Z" }, + { url = "https://files.pythonhosted.org/packages/ba/a8/c17acf65a931ce551fee11b72e8de63bf7e8a6f0e21add4c937c83563538/numpy-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13e689d772146140a252c3a28501da66dfecd77490b498b168b501835041f951", size = 19534895, upload-time = "2024-08-26T20:09:16.536Z" }, + { url = "https://files.pythonhosted.org/packages/ba/86/8767f3d54f6ae0165749f84648da9dcc8cd78ab65d415494962c86fac80f/numpy-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9ea91dfb7c3d1c56a0e55657c0afb38cf1eeae4544c208dc465c3c9f3a7c09f9", size = 19937253, upload-time = "2024-08-26T20:09:46.263Z" }, + { url = "https://files.pythonhosted.org/packages/df/87/f76450e6e1c14e5bb1eae6836478b1028e096fd02e85c1c37674606ab752/numpy-2.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c1c9307701fec8f3f7a1e6711f9089c06e6284b3afbbcd259f7791282d660a15", size = 14414074, upload-time = "2024-08-26T20:10:08.483Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ca/0f0f328e1e59f73754f06e1adfb909de43726d4f24c6a3f8805f34f2b0fa/numpy-2.0.2-cp311-cp311-win32.whl", hash = "sha256:a392a68bd329eafac5817e5aefeb39038c48b671afd242710b451e76090e81f4", size = 6470640, upload-time = "2024-08-26T20:10:19.732Z" }, + { url = "https://files.pythonhosted.org/packages/eb/57/3a3f14d3a759dcf9bf6e9eda905794726b758819df4663f217d658a58695/numpy-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:286cd40ce2b7d652a6f22efdfc6d1edf879440e53e76a75955bc0c826c7e64dc", size = 15910230, upload-time = "2024-08-26T20:10:43.413Z" }, + { url = "https://files.pythonhosted.org/packages/45/40/2e117be60ec50d98fa08c2f8c48e09b3edea93cfcabd5a9ff6925d54b1c2/numpy-2.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:df55d490dea7934f330006d0f81e8551ba6010a5bf035a249ef61a94f21c500b", size = 20895803, upload-time = "2024-08-26T20:11:13.916Z" }, + { url = "https://files.pythonhosted.org/packages/46/92/1b8b8dee833f53cef3e0a3f69b2374467789e0bb7399689582314df02651/numpy-2.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8df823f570d9adf0978347d1f926b2a867d5608f434a7cff7f7908c6570dcf5e", size = 13471835, upload-time = "2024-08-26T20:11:34.779Z" }, + { url = "https://files.pythonhosted.org/packages/7f/19/e2793bde475f1edaea6945be141aef6c8b4c669b90c90a300a8954d08f0a/numpy-2.0.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9a92ae5c14811e390f3767053ff54eaee3bf84576d99a2456391401323f4ec2c", size = 5038499, upload-time = "2024-08-26T20:11:43.902Z" }, + { url = "https://files.pythonhosted.org/packages/e3/ff/ddf6dac2ff0dd50a7327bcdba45cb0264d0e96bb44d33324853f781a8f3c/numpy-2.0.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:a842d573724391493a97a62ebbb8e731f8a5dcc5d285dfc99141ca15a3302d0c", size = 6633497, upload-time = "2024-08-26T20:11:55.09Z" }, + { url = "https://files.pythonhosted.org/packages/72/21/67f36eac8e2d2cd652a2e69595a54128297cdcb1ff3931cfc87838874bd4/numpy-2.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05e238064fc0610c840d1cf6a13bf63d7e391717d247f1bf0318172e759e692", size = 13621158, upload-time = "2024-08-26T20:12:14.95Z" }, + { url = "https://files.pythonhosted.org/packages/39/68/e9f1126d757653496dbc096cb429014347a36b228f5a991dae2c6b6cfd40/numpy-2.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0123ffdaa88fa4ab64835dcbde75dcdf89c453c922f18dced6e27c90d1d0ec5a", size = 19236173, upload-time = "2024-08-26T20:12:44.049Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e9/1f5333281e4ebf483ba1c888b1d61ba7e78d7e910fdd8e6499667041cc35/numpy-2.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:96a55f64139912d61de9137f11bf39a55ec8faec288c75a54f93dfd39f7eb40c", size = 19634174, upload-time = "2024-08-26T20:13:13.634Z" }, + { url = "https://files.pythonhosted.org/packages/71/af/a469674070c8d8408384e3012e064299f7a2de540738a8e414dcfd639996/numpy-2.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec9852fb39354b5a45a80bdab5ac02dd02b15f44b3804e9f00c556bf24b4bded", size = 14099701, upload-time = "2024-08-26T20:13:34.851Z" }, + { url = "https://files.pythonhosted.org/packages/d0/3d/08ea9f239d0e0e939b6ca52ad403c84a2bce1bde301a8eb4888c1c1543f1/numpy-2.0.2-cp312-cp312-win32.whl", hash = "sha256:671bec6496f83202ed2d3c8fdc486a8fc86942f2e69ff0e986140339a63bcbe5", size = 6174313, upload-time = "2024-08-26T20:13:45.653Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b5/4ac39baebf1fdb2e72585c8352c56d063b6126be9fc95bd2bb5ef5770c20/numpy-2.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:cfd41e13fdc257aa5778496b8caa5e856dc4896d4ccf01841daee1d96465467a", size = 15606179, upload-time = "2024-08-26T20:14:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/43/c1/41c8f6df3162b0c6ffd4437d729115704bd43363de0090c7f913cfbc2d89/numpy-2.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9059e10581ce4093f735ed23f3b9d283b9d517ff46009ddd485f1747eb22653c", size = 21169942, upload-time = "2024-08-26T20:14:40.108Z" }, + { url = "https://files.pythonhosted.org/packages/39/bc/fd298f308dcd232b56a4031fd6ddf11c43f9917fbc937e53762f7b5a3bb1/numpy-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:423e89b23490805d2a5a96fe40ec507407b8ee786d66f7328be214f9679df6dd", size = 13711512, upload-time = "2024-08-26T20:15:00.985Z" }, + { url = "https://files.pythonhosted.org/packages/96/ff/06d1aa3eeb1c614eda245c1ba4fb88c483bee6520d361641331872ac4b82/numpy-2.0.2-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:2b2955fa6f11907cf7a70dab0d0755159bca87755e831e47932367fc8f2f2d0b", size = 5306976, upload-time = "2024-08-26T20:15:10.876Z" }, + { url = "https://files.pythonhosted.org/packages/2d/98/121996dcfb10a6087a05e54453e28e58694a7db62c5a5a29cee14c6e047b/numpy-2.0.2-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:97032a27bd9d8988b9a97a8c4d2c9f2c15a81f61e2f21404d7e8ef00cb5be729", size = 6906494, upload-time = "2024-08-26T20:15:22.055Z" }, + { url = "https://files.pythonhosted.org/packages/15/31/9dffc70da6b9bbf7968f6551967fc21156207366272c2a40b4ed6008dc9b/numpy-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e795a8be3ddbac43274f18588329c72939870a16cae810c2b73461c40718ab1", size = 13912596, upload-time = "2024-08-26T20:15:42.452Z" }, + { url = "https://files.pythonhosted.org/packages/b9/14/78635daab4b07c0930c919d451b8bf8c164774e6a3413aed04a6d95758ce/numpy-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b258c385842546006213344c50655ff1555a9338e2e5e02a0756dc3e803dd", size = 19526099, upload-time = "2024-08-26T20:16:11.048Z" }, + { url = "https://files.pythonhosted.org/packages/26/4c/0eeca4614003077f68bfe7aac8b7496f04221865b3a5e7cb230c9d055afd/numpy-2.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fec9451a7789926bcf7c2b8d187292c9f93ea30284802a0ab3f5be8ab36865d", size = 19932823, upload-time = "2024-08-26T20:16:40.171Z" }, + { url = "https://files.pythonhosted.org/packages/f1/46/ea25b98b13dccaebddf1a803f8c748680d972e00507cd9bc6dcdb5aa2ac1/numpy-2.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9189427407d88ff25ecf8f12469d4d39d35bee1db5d39fc5c168c6f088a6956d", size = 14404424, upload-time = "2024-08-26T20:17:02.604Z" }, + { url = "https://files.pythonhosted.org/packages/c8/a6/177dd88d95ecf07e722d21008b1b40e681a929eb9e329684d449c36586b2/numpy-2.0.2-cp39-cp39-win32.whl", hash = "sha256:905d16e0c60200656500c95b6b8dca5d109e23cb24abc701d41c02d74c6b3afa", size = 6476809, upload-time = "2024-08-26T20:17:13.553Z" }, + { url = "https://files.pythonhosted.org/packages/ea/2b/7fc9f4e7ae5b507c1a3a21f0f15ed03e794c1242ea8a242ac158beb56034/numpy-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:a3f4ab0caa7f053f6797fcd4e1e25caee367db3112ef2b6ef82d749530768c73", size = 15911314, upload-time = "2024-08-26T20:17:36.72Z" }, + { url = "https://files.pythonhosted.org/packages/8f/3b/df5a870ac6a3be3a86856ce195ef42eec7ae50d2a202be1f5a4b3b340e14/numpy-2.0.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7f0a0c6f12e07fa94133c8a67404322845220c06a9e80e85999afe727f7438b8", size = 21025288, upload-time = "2024-08-26T20:18:07.732Z" }, + { url = "https://files.pythonhosted.org/packages/2c/97/51af92f18d6f6f2d9ad8b482a99fb74e142d71372da5d834b3a2747a446e/numpy-2.0.2-pp39-pypy39_pp73-macosx_14_0_x86_64.whl", hash = "sha256:312950fdd060354350ed123c0e25a71327d3711584beaef30cdaa93320c392d4", size = 6762793, upload-time = "2024-08-26T20:18:19.125Z" }, + { url = "https://files.pythonhosted.org/packages/12/46/de1fbd0c1b5ccaa7f9a005b66761533e2f6a3e560096682683a223631fe9/numpy-2.0.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26df23238872200f63518dd2aa984cfca675d82469535dc7162dc2ee52d9dd5c", size = 19334885, upload-time = "2024-08-26T20:18:47.237Z" }, + { url = "https://files.pythonhosted.org/packages/cc/dc/d330a6faefd92b446ec0f0dfea4c3207bb1fef3c4771d19cf4543efd2c78/numpy-2.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a46288ec55ebbd58947d31d72be2c63cbf839f0a63b49cb755022310792a3385", size = 15828784, upload-time = "2024-08-26T20:19:11.19Z" }, +] + +[[package]] +name = "numpy" +version = "2.2.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" }, + { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" }, + { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" }, + { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" }, + { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" }, + { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" }, + { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" }, + { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" }, + { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" }, + { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" }, + { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" }, + { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" }, + { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" }, + { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" }, + { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" }, + { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" }, + { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" }, + { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" }, + { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" }, + { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" }, + { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" }, + { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" }, + { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" }, + { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" }, + { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" }, + { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" }, + { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" }, + { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" }, + { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" }, + { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" }, + { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" }, + { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" }, + { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" }, + { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" }, + { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" }, + { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" }, + { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" }, + { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" }, + { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" }, + { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" }, +] + +[[package]] +name = "numpy" +version = "2.4.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version >= '3.12' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +sdist = { url = "https://files.pythonhosted.org/packages/10/8b/c265f4823726ab832de836cdd184d0986dcf94480f81e8739692a7ac7af2/numpy-2.4.3.tar.gz", hash = "sha256:483a201202b73495f00dbc83796c6ae63137a9bdade074f7648b3e32613412dd", size = 20727743, upload-time = "2026-03-09T07:58:53.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/51/5093a2df15c4dc19da3f79d1021e891f5dcf1d9d1db6ba38891d5590f3fe/numpy-2.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:33b3bf58ee84b172c067f56aeadc7ee9ab6de69c5e800ab5b10295d54c581adb", size = 16957183, upload-time = "2026-03-09T07:55:57.774Z" }, + { url = "https://files.pythonhosted.org/packages/b5/7c/c061f3de0630941073d2598dc271ac2f6cbcf5c83c74a5870fea07488333/numpy-2.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8ba7b51e71c05aa1f9bc3641463cd82308eab40ce0d5c7e1fd4038cbf9938147", size = 14968734, upload-time = "2026-03-09T07:56:00.494Z" }, + { url = "https://files.pythonhosted.org/packages/ef/27/d26c85cbcd86b26e4f125b0668e7a7c0542d19dd7d23ee12e87b550e95b5/numpy-2.4.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a1988292870c7cb9d0ebb4cc96b4d447513a9644801de54606dc7aabf2b7d920", size = 5475288, upload-time = "2026-03-09T07:56:02.857Z" }, + { url = "https://files.pythonhosted.org/packages/2b/09/3c4abbc1dcd8010bf1a611d174c7aa689fc505585ec806111b4406f6f1b1/numpy-2.4.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:23b46bb6d8ecb68b58c09944483c135ae5f0e9b8d8858ece5e4ead783771d2a9", size = 6805253, upload-time = "2026-03-09T07:56:04.53Z" }, + { url = "https://files.pythonhosted.org/packages/21/bc/e7aa3f6817e40c3f517d407742337cbb8e6fc4b83ce0b55ab780c829243b/numpy-2.4.3-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a016db5c5dba78fa8fe9f5d80d6708f9c42ab087a739803c0ac83a43d686a470", size = 15969479, upload-time = "2026-03-09T07:56:06.638Z" }, + { url = "https://files.pythonhosted.org/packages/78/51/9f5d7a41f0b51649ddf2f2320595e15e122a40610b233d51928dd6c92353/numpy-2.4.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:715de7f82e192e8cae5a507a347d97ad17598f8e026152ca97233e3666daaa71", size = 16901035, upload-time = "2026-03-09T07:56:09.405Z" }, + { url = "https://files.pythonhosted.org/packages/64/6e/b221dd847d7181bc5ee4857bfb026182ef69499f9305eb1371cbb1aea626/numpy-2.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2ddb7919366ee468342b91dea2352824c25b55814a987847b6c52003a7c97f15", size = 17325657, upload-time = "2026-03-09T07:56:12.067Z" }, + { url = "https://files.pythonhosted.org/packages/eb/b8/8f3fd2da596e1063964b758b5e3c970aed1949a05200d7e3d46a9d46d643/numpy-2.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a315e5234d88067f2d97e1f2ef670a7569df445d55400f1e33d117418d008d52", size = 18635512, upload-time = "2026-03-09T07:56:14.629Z" }, + { url = "https://files.pythonhosted.org/packages/5c/24/2993b775c37e39d2f8ab4125b44337ab0b2ba106c100980b7c274a22bee7/numpy-2.4.3-cp311-cp311-win32.whl", hash = "sha256:2b3f8d2c4589b1a2028d2a770b0fc4d1f332fb5e01521f4de3199a896d158ddd", size = 6238100, upload-time = "2026-03-09T07:56:17.243Z" }, + { url = "https://files.pythonhosted.org/packages/76/1d/edccf27adedb754db7c4511d5eac8b83f004ae948fe2d3509e8b78097d4c/numpy-2.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:77e76d932c49a75617c6d13464e41203cd410956614d0a0e999b25e9e8d27eec", size = 12609816, upload-time = "2026-03-09T07:56:19.089Z" }, + { url = "https://files.pythonhosted.org/packages/92/82/190b99153480076c8dce85f4cfe7d53ea84444145ffa54cb58dcd460d66b/numpy-2.4.3-cp311-cp311-win_arm64.whl", hash = "sha256:eb610595dd91560905c132c709412b512135a60f1851ccbd2c959e136431ff67", size = 10485757, upload-time = "2026-03-09T07:56:21.753Z" }, + { url = "https://files.pythonhosted.org/packages/a9/ed/6388632536f9788cea23a3a1b629f25b43eaacd7d7377e5d6bc7b9deb69b/numpy-2.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:61b0cbabbb6126c8df63b9a3a0c4b1f44ebca5e12ff6997b80fcf267fb3150ef", size = 16669628, upload-time = "2026-03-09T07:56:24.252Z" }, + { url = "https://files.pythonhosted.org/packages/74/1b/ee2abfc68e1ce728b2958b6ba831d65c62e1b13ce3017c13943f8f9b5b2e/numpy-2.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7395e69ff32526710748f92cd8c9849b361830968ea3e24a676f272653e8983e", size = 14696872, upload-time = "2026-03-09T07:56:26.991Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d1/780400e915ff5638166f11ca9dc2c5815189f3d7cf6f8759a1685e586413/numpy-2.4.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:abdce0f71dcb4a00e4e77f3faf05e4616ceccfe72ccaa07f47ee79cda3b7b0f4", size = 5203489, upload-time = "2026-03-09T07:56:29.414Z" }, + { url = "https://files.pythonhosted.org/packages/0b/bb/baffa907e9da4cc34a6e556d6d90e032f6d7a75ea47968ea92b4858826c4/numpy-2.4.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:48da3a4ee1336454b07497ff7ec83903efa5505792c4e6d9bf83d99dc07a1e18", size = 6550814, upload-time = "2026-03-09T07:56:32.225Z" }, + { url = "https://files.pythonhosted.org/packages/7b/12/8c9f0c6c95f76aeb20fc4a699c33e9f827fa0d0f857747c73bb7b17af945/numpy-2.4.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32e3bef222ad6b052280311d1d60db8e259e4947052c3ae7dd6817451fc8a4c5", size = 15666601, upload-time = "2026-03-09T07:56:34.461Z" }, + { url = "https://files.pythonhosted.org/packages/bd/79/cc665495e4d57d0aa6fbcc0aa57aa82671dfc78fbf95fe733ed86d98f52a/numpy-2.4.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7dd01a46700b1967487141a66ac1a3cf0dd8ebf1f08db37d46389401512ca97", size = 16621358, upload-time = "2026-03-09T07:56:36.852Z" }, + { url = "https://files.pythonhosted.org/packages/a8/40/b4ecb7224af1065c3539f5ecfff879d090de09608ad1008f02c05c770cb3/numpy-2.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:76f0f283506c28b12bba319c0fab98217e9f9b54e6160e9c79e9f7348ba32e9c", size = 17016135, upload-time = "2026-03-09T07:56:39.337Z" }, + { url = "https://files.pythonhosted.org/packages/f7/b1/6a88e888052eed951afed7a142dcdf3b149a030ca59b4c71eef085858e43/numpy-2.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:737f630a337364665aba3b5a77e56a68cc42d350edd010c345d65a3efa3addcc", size = 18345816, upload-time = "2026-03-09T07:56:42.31Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8f/103a60c5f8c3d7fc678c19cd7b2476110da689ccb80bc18050efbaeae183/numpy-2.4.3-cp312-cp312-win32.whl", hash = "sha256:26952e18d82a1dbbc2f008d402021baa8d6fc8e84347a2072a25e08b46d698b9", size = 5960132, upload-time = "2026-03-09T07:56:44.851Z" }, + { url = "https://files.pythonhosted.org/packages/d7/7c/f5ee1bf6ed888494978046a809df2882aad35d414b622893322df7286879/numpy-2.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:65f3c2455188f09678355f5cae1f959a06b778bc66d535da07bf2ef20cd319d5", size = 12316144, upload-time = "2026-03-09T07:56:47.057Z" }, + { url = "https://files.pythonhosted.org/packages/71/46/8d1cb3f7a00f2fb6394140e7e6623696e54c6318a9d9691bb4904672cf42/numpy-2.4.3-cp312-cp312-win_arm64.whl", hash = "sha256:2abad5c7fef172b3377502bde47892439bae394a71bc329f31df0fd829b41a9e", size = 10220364, upload-time = "2026-03-09T07:56:49.849Z" }, + { url = "https://files.pythonhosted.org/packages/b6/d0/1fe47a98ce0df229238b77611340aff92d52691bcbc10583303181abf7fc/numpy-2.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b346845443716c8e542d54112966383b448f4a3ba5c66409771b8c0889485dd3", size = 16665297, upload-time = "2026-03-09T07:56:52.296Z" }, + { url = "https://files.pythonhosted.org/packages/27/d9/4e7c3f0e68dfa91f21c6fb6cf839bc829ec920688b1ce7ec722b1a6202fb/numpy-2.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2629289168f4897a3c4e23dc98d6f1731f0fc0fe52fb9db19f974041e4cc12b9", size = 14691853, upload-time = "2026-03-09T07:56:54.992Z" }, + { url = "https://files.pythonhosted.org/packages/3a/66/bd096b13a87549683812b53ab211e6d413497f84e794fb3c39191948da97/numpy-2.4.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:bb2e3cf95854233799013779216c57e153c1ee67a0bf92138acca0e429aefaee", size = 5198435, upload-time = "2026-03-09T07:56:57.184Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2f/687722910b5a5601de2135c891108f51dfc873d8e43c8ed9f4ebb440b4a2/numpy-2.4.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:7f3408ff897f8ab07a07fbe2823d7aee6ff644c097cc1f90382511fe982f647f", size = 6546347, upload-time = "2026-03-09T07:56:59.531Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ec/7971c4e98d86c564750393fab8d7d83d0a9432a9d78bb8a163a6dc59967a/numpy-2.4.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:decb0eb8a53c3b009b0962378065589685d66b23467ef5dac16cbe818afde27f", size = 15664626, upload-time = "2026-03-09T07:57:01.385Z" }, + { url = "https://files.pythonhosted.org/packages/7e/eb/7daecbea84ec935b7fc732e18f532073064a3816f0932a40a17f3349185f/numpy-2.4.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5f51900414fc9204a0e0da158ba2ac52b75656e7dce7e77fb9f84bfa343b4cc", size = 16608916, upload-time = "2026-03-09T07:57:04.008Z" }, + { url = "https://files.pythonhosted.org/packages/df/58/2a2b4a817ffd7472dca4421d9f0776898b364154e30c95f42195041dc03b/numpy-2.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6bd06731541f89cdc01b261ba2c9e037f1543df7472517836b78dfb15bd6e476", size = 17015824, upload-time = "2026-03-09T07:57:06.347Z" }, + { url = "https://files.pythonhosted.org/packages/4a/ca/627a828d44e78a418c55f82dd4caea8ea4a8ef24e5144d9e71016e52fb40/numpy-2.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22654fe6be0e5206f553a9250762c653d3698e46686eee53b399ab90da59bd92", size = 18334581, upload-time = "2026-03-09T07:57:09.114Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c0/76f93962fc79955fcba30a429b62304332345f22d4daec1cb33653425643/numpy-2.4.3-cp313-cp313-win32.whl", hash = "sha256:d71e379452a2f670ccb689ec801b1218cd3983e253105d6e83780967e899d687", size = 5958618, upload-time = "2026-03-09T07:57:11.432Z" }, + { url = "https://files.pythonhosted.org/packages/b1/3c/88af0040119209b9b5cb59485fa48b76f372c73068dbf9254784b975ac53/numpy-2.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:0a60e17a14d640f49146cb38e3f105f571318db7826d9b6fef7e4dce758faecd", size = 12312824, upload-time = "2026-03-09T07:57:13.586Z" }, + { url = "https://files.pythonhosted.org/packages/58/ce/3d07743aced3d173f877c3ef6a454c2174ba42b584ab0b7e6d99374f51ed/numpy-2.4.3-cp313-cp313-win_arm64.whl", hash = "sha256:c9619741e9da2059cd9c3f206110b97583c7152c1dc9f8aafd4beb450ac1c89d", size = 10221218, upload-time = "2026-03-09T07:57:16.183Z" }, + { url = "https://files.pythonhosted.org/packages/62/09/d96b02a91d09e9d97862f4fc8bfebf5400f567d8eb1fe4b0cc4795679c15/numpy-2.4.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7aa4e54f6469300ebca1d9eb80acd5253cdfa36f2c03d79a35883687da430875", size = 14819570, upload-time = "2026-03-09T07:57:18.564Z" }, + { url = "https://files.pythonhosted.org/packages/b5/ca/0b1aba3905fdfa3373d523b2b15b19029f4f3031c87f4066bd9d20ef6c6b/numpy-2.4.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d1b90d840b25874cf5cd20c219af10bac3667db3876d9a495609273ebe679070", size = 5326113, upload-time = "2026-03-09T07:57:21.052Z" }, + { url = "https://files.pythonhosted.org/packages/c0/63/406e0fd32fcaeb94180fd6a4c41e55736d676c54346b7efbce548b94a914/numpy-2.4.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a749547700de0a20a6718293396ec237bb38218049cfce788e08fcb716e8cf73", size = 6646370, upload-time = "2026-03-09T07:57:22.804Z" }, + { url = "https://files.pythonhosted.org/packages/b6/d0/10f7dc157d4b37af92720a196be6f54f889e90dcd30dce9dc657ed92c257/numpy-2.4.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94f3c4a151a2e529adf49c1d54f0f57ff8f9b233ee4d44af623a81553ab86368", size = 15723499, upload-time = "2026-03-09T07:57:24.693Z" }, + { url = "https://files.pythonhosted.org/packages/66/f1/d1c2bf1161396629701bc284d958dc1efa3a5a542aab83cf11ee6eb4cba5/numpy-2.4.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22c31dc07025123aedf7f2db9e91783df13f1776dc52c6b22c620870dc0fab22", size = 16657164, upload-time = "2026-03-09T07:57:27.676Z" }, + { url = "https://files.pythonhosted.org/packages/1a/be/cca19230b740af199ac47331a21c71e7a3d0ba59661350483c1600d28c37/numpy-2.4.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:148d59127ac95979d6f07e4d460f934ebdd6eed641db9c0db6c73026f2b2101a", size = 17081544, upload-time = "2026-03-09T07:57:30.664Z" }, + { url = "https://files.pythonhosted.org/packages/b9/c5/9602b0cbb703a0936fb40f8a95407e8171935b15846de2f0776e08af04c7/numpy-2.4.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a97cbf7e905c435865c2d939af3d93f99d18eaaa3cabe4256f4304fb51604349", size = 18380290, upload-time = "2026-03-09T07:57:33.763Z" }, + { url = "https://files.pythonhosted.org/packages/ed/81/9f24708953cd30be9ee36ec4778f4b112b45165812f2ada4cc5ea1c1f254/numpy-2.4.3-cp313-cp313t-win32.whl", hash = "sha256:be3b8487d725a77acccc9924f65fd8bce9af7fac8c9820df1049424a2115af6c", size = 6082814, upload-time = "2026-03-09T07:57:36.491Z" }, + { url = "https://files.pythonhosted.org/packages/e2/9e/52f6eaa13e1a799f0ab79066c17f7016a4a8ae0c1aefa58c82b4dab690b4/numpy-2.4.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1ec84fd7c8e652b0f4aaaf2e6e9cc8eaa9b1b80a537e06b2e3a2fb176eedcb26", size = 12452673, upload-time = "2026-03-09T07:57:38.281Z" }, + { url = "https://files.pythonhosted.org/packages/c4/04/b8cece6ead0b30c9fbd99bb835ad7ea0112ac5f39f069788c5558e3b1ab2/numpy-2.4.3-cp313-cp313t-win_arm64.whl", hash = "sha256:120df8c0a81ebbf5b9020c91439fccd85f5e018a927a39f624845be194a2be02", size = 10290907, upload-time = "2026-03-09T07:57:40.747Z" }, + { url = "https://files.pythonhosted.org/packages/64/e4/4dab9fb43c83719c29241c535d9e07be73bea4bc0c6686c5816d8e1b6689/numpy-2.4.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c6b124bfcafb9e8d3ed09130dbee44848c20b3e758b6bbf006e641778927c028", size = 16834892, upload-time = "2026-03-09T07:58:35.334Z" }, + { url = "https://files.pythonhosted.org/packages/c9/29/f8b6d4af90fed3dfda84ebc0df06c9833d38880c79ce954e5b661758aa31/numpy-2.4.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:76dbb9d4e43c16cf9aa711fcd8de1e2eeb27539dcefb60a1d5e9f12fae1d1ed8", size = 14893070, upload-time = "2026-03-09T07:58:37.7Z" }, + { url = "https://files.pythonhosted.org/packages/9a/04/a19b3c91dbec0a49269407f15d5753673a09832daed40c45e8150e6fa558/numpy-2.4.3-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:29363fbfa6f8ee855d7569c96ce524845e3d726d6c19b29eceec7dd555dab152", size = 5399609, upload-time = "2026-03-09T07:58:39.853Z" }, + { url = "https://files.pythonhosted.org/packages/79/34/4d73603f5420eab89ea8a67097b31364bf7c30f811d4dd84b1659c7476d9/numpy-2.4.3-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:bc71942c789ef415a37f0d4eab90341425a00d538cd0642445d30b41023d3395", size = 6714355, upload-time = "2026-03-09T07:58:42.365Z" }, + { url = "https://files.pythonhosted.org/packages/58/ad/1100d7229bb248394939a12a8074d485b655e8ed44207d328fdd7fcebc7b/numpy-2.4.3-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e58765ad74dcebd3ef0208a5078fba32dc8ec3578fe84a604432950cd043d79", size = 15800434, upload-time = "2026-03-09T07:58:44.837Z" }, + { url = "https://files.pythonhosted.org/packages/0c/fd/16d710c085d28ba4feaf29ac60c936c9d662e390344f94a6beaa2ac9899b/numpy-2.4.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e236dbda4e1d319d681afcbb136c0c4a8e0f1a5c58ceec2adebb547357fe857", size = 16729409, upload-time = "2026-03-09T07:58:47.972Z" }, + { url = "https://files.pythonhosted.org/packages/57/a7/b35835e278c18b85206834b3aa3abe68e77a98769c59233d1f6300284781/numpy-2.4.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:4b42639cdde6d24e732ff823a3fa5b701d8acad89c4142bc1d0bd6dc85200ba5", size = 12504685, upload-time = "2026-03-09T07:58:50.525Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "pandas" +version = "2.3.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.10.*'", + "python_full_version < '3.10'", +] +dependencies = [ + { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "python-dateutil", marker = "python_full_version < '3.11'" }, + { name = "pytz", marker = "python_full_version < '3.11'" }, + { name = "tzdata", marker = "python_full_version < '3.11'" }, +] +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/3d/f7/f425a00df4fcc22b292c6895c6831c0c8ae1d9fac1e024d16f98a9ce8749/pandas-2.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:376c6446ae31770764215a6c937f72d917f214b43560603cd60da6408f183b6c", size = 11555763, upload-time = "2025-09-29T23:16:53.287Z" }, + { url = "https://files.pythonhosted.org/packages/13/4f/66d99628ff8ce7857aca52fed8f0066ce209f96be2fede6cef9f84e8d04f/pandas-2.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e19d192383eab2f4ceb30b412b22ea30690c9e618f78870357ae1d682912015a", size = 10801217, upload-time = "2025-09-29T23:17:04.522Z" }, + { url = "https://files.pythonhosted.org/packages/1d/03/3fc4a529a7710f890a239cc496fc6d50ad4a0995657dccc1d64695adb9f4/pandas-2.3.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5caf26f64126b6c7aec964f74266f435afef1c1b13da3b0636c7518a1fa3e2b1", size = 12148791, upload-time = "2025-09-29T23:17:18.444Z" }, + { url = "https://files.pythonhosted.org/packages/40/a8/4dac1f8f8235e5d25b9955d02ff6f29396191d4e665d71122c3722ca83c5/pandas-2.3.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd7478f1463441ae4ca7308a70e90b33470fa593429f9d4c578dd00d1fa78838", size = 12769373, upload-time = "2025-09-29T23:17:35.846Z" }, + { url = "https://files.pythonhosted.org/packages/df/91/82cc5169b6b25440a7fc0ef3a694582418d875c8e3ebf796a6d6470aa578/pandas-2.3.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4793891684806ae50d1288c9bae9330293ab4e083ccd1c5e383c34549c6e4250", size = 13200444, upload-time = "2025-09-29T23:17:49.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/ae/89b3283800ab58f7af2952704078555fa60c807fff764395bb57ea0b0dbd/pandas-2.3.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:28083c648d9a99a5dd035ec125d42439c6c1c525098c58af0fc38dd1a7a1b3d4", size = 13858459, upload-time = "2025-09-29T23:18:03.722Z" }, + { url = "https://files.pythonhosted.org/packages/85/72/530900610650f54a35a19476eca5104f38555afccda1aa11a92ee14cb21d/pandas-2.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:503cf027cf9940d2ceaa1a93cfb5f8c8c7e6e90720a2850378f0b3f3b1e06826", size = 11346086, upload-time = "2025-09-29T23:18:18.505Z" }, + { 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/56/b4/52eeb530a99e2a4c55ffcd352772b599ed4473a0f892d127f4147cf0f88e/pandas-2.3.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c503ba5216814e295f40711470446bc3fd00f0faea8a086cbc688808e26f92a2", size = 11567720, upload-time = "2025-09-29T23:33:06.209Z" }, + { url = "https://files.pythonhosted.org/packages/48/4a/2d8b67632a021bced649ba940455ed441ca854e57d6e7658a6024587b083/pandas-2.3.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a637c5cdfa04b6d6e2ecedcb81fc52ffb0fd78ce2ebccc9ea964df9f658de8c8", size = 10810302, upload-time = "2025-09-29T23:33:35.846Z" }, + { url = "https://files.pythonhosted.org/packages/13/e6/d2465010ee0569a245c975dc6967b801887068bc893e908239b1f4b6c1ac/pandas-2.3.3-cp39-cp39-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:854d00d556406bffe66a4c0802f334c9ad5a96b4f1f868adf036a21b11ef13ff", size = 12154874, upload-time = "2025-09-29T23:33:49.939Z" }, + { url = "https://files.pythonhosted.org/packages/1f/18/aae8c0aa69a386a3255940e9317f793808ea79d0a525a97a903366bb2569/pandas-2.3.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf1f8a81d04ca90e32a0aceb819d34dbd378a98bf923b6398b9a3ec0bf44de29", size = 12790141, upload-time = "2025-09-29T23:34:05.655Z" }, + { url = "https://files.pythonhosted.org/packages/f7/26/617f98de789de00c2a444fbe6301bb19e66556ac78cff933d2c98f62f2b4/pandas-2.3.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:23ebd657a4d38268c7dfbdf089fbc31ea709d82e4923c5ffd4fbd5747133ce73", size = 13208697, upload-time = "2025-09-29T23:34:21.835Z" }, + { url = "https://files.pythonhosted.org/packages/b9/fb/25709afa4552042bd0e15717c75e9b4a2294c3dc4f7e6ea50f03c5136600/pandas-2.3.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5554c929ccc317d41a5e3d1234f3be588248e61f08a74dd17c9eabb535777dc9", size = 13879233, upload-time = "2025-09-29T23:34:35.079Z" }, + { url = "https://files.pythonhosted.org/packages/98/af/7be05277859a7bc399da8ba68b88c96b27b48740b6cf49688899c6eb4176/pandas-2.3.3-cp39-cp39-win_amd64.whl", hash = "sha256:d3e28b3e83862ccf4d85ff19cf8c20b2ae7e503881711ff2d534dc8f761131aa", size = 11359119, upload-time = "2025-09-29T23:34:46.339Z" }, +] + +[[package]] +name = "pandas" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version >= '3.12' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "python-dateutil", marker = "python_full_version >= '3.11'" }, + { name = "tzdata", marker = "(python_full_version >= '3.11' and sys_platform == 'emscripten') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/0c/b28ed414f080ee0ad153f848586d61d1878f91689950f037f976ce15f6c8/pandas-3.0.1.tar.gz", hash = "sha256:4186a699674af418f655dbd420ed87f50d56b4cd6603784279d9eef6627823c8", size = 4641901, upload-time = "2026-02-17T22:20:16.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/07/c7087e003ceee9b9a82539b40414ec557aa795b584a1a346e89180853d79/pandas-3.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:de09668c1bf3b925c07e5762291602f0d789eca1b3a781f99c1c78f6cac0e7ea", size = 10323380, upload-time = "2026-02-17T22:18:16.133Z" }, + { url = "https://files.pythonhosted.org/packages/c1/27/90683c7122febeefe84a56f2cde86a9f05f68d53885cebcc473298dfc33e/pandas-3.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:24ba315ba3d6e5806063ac6eb717504e499ce30bd8c236d8693a5fd3f084c796", size = 9923455, upload-time = "2026-02-17T22:18:19.13Z" }, + { url = "https://files.pythonhosted.org/packages/0e/f1/ed17d927f9950643bc7631aa4c99ff0cc83a37864470bc419345b656a41f/pandas-3.0.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:406ce835c55bac912f2a0dcfaf27c06d73c6b04a5dde45f1fd3169ce31337389", size = 10753464, upload-time = "2026-02-17T22:18:21.134Z" }, + { url = "https://files.pythonhosted.org/packages/2e/7c/870c7e7daec2a6c7ff2ac9e33b23317230d4e4e954b35112759ea4a924a7/pandas-3.0.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:830994d7e1f31dd7e790045235605ab61cff6c94defc774547e8b7fdfbff3dc7", size = 11255234, upload-time = "2026-02-17T22:18:24.175Z" }, + { url = "https://files.pythonhosted.org/packages/5c/39/3653fe59af68606282b989c23d1a543ceba6e8099cbcc5f1d506a7bae2aa/pandas-3.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a64ce8b0f2de1d2efd2ae40b0abe7f8ae6b29fbfb3812098ed5a6f8e235ad9bf", size = 11767299, upload-time = "2026-02-17T22:18:26.824Z" }, + { url = "https://files.pythonhosted.org/packages/9b/31/1daf3c0c94a849c7a8dab8a69697b36d313b229918002ba3e409265c7888/pandas-3.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9832c2c69da24b602c32e0c7b1b508a03949c18ba08d4d9f1c1033426685b447", size = 12333292, upload-time = "2026-02-17T22:18:28.996Z" }, + { url = "https://files.pythonhosted.org/packages/1f/67/af63f83cd6ca603a00fe8530c10a60f0879265b8be00b5930e8e78c5b30b/pandas-3.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:84f0904a69e7365f79a0c77d3cdfccbfb05bf87847e3a51a41e1426b0edb9c79", size = 9892176, upload-time = "2026-02-17T22:18:31.79Z" }, + { url = "https://files.pythonhosted.org/packages/79/ab/9c776b14ac4b7b4140788eca18468ea39894bc7340a408f1d1e379856a6b/pandas-3.0.1-cp311-cp311-win_arm64.whl", hash = "sha256:4a68773d5a778afb31d12e34f7dd4612ab90de8c6fb1d8ffe5d4a03b955082a1", size = 9151328, upload-time = "2026-02-17T22:18:35.721Z" }, + { url = "https://files.pythonhosted.org/packages/37/51/b467209c08dae2c624873d7491ea47d2b47336e5403309d433ea79c38571/pandas-3.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:476f84f8c20c9f5bc47252b66b4bb25e1a9fc2fa98cead96744d8116cb85771d", size = 10344357, upload-time = "2026-02-17T22:18:38.262Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f1/e2567ffc8951ab371db2e40b2fe068e36b81d8cf3260f06ae508700e5504/pandas-3.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0ab749dfba921edf641d4036c4c21c0b3ea70fea478165cb98a998fb2a261955", size = 9884543, upload-time = "2026-02-17T22:18:41.476Z" }, + { url = "https://files.pythonhosted.org/packages/d7/39/327802e0b6d693182403c144edacbc27eb82907b57062f23ef5a4c4a5ea7/pandas-3.0.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8e36891080b87823aff3640c78649b91b8ff6eea3c0d70aeabd72ea43ab069b", size = 10396030, upload-time = "2026-02-17T22:18:43.822Z" }, + { url = "https://files.pythonhosted.org/packages/3d/fe/89d77e424365280b79d99b3e1e7d606f5165af2f2ecfaf0c6d24c799d607/pandas-3.0.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:532527a701281b9dd371e2f582ed9094f4c12dd9ffb82c0c54ee28d8ac9520c4", size = 10876435, upload-time = "2026-02-17T22:18:45.954Z" }, + { url = "https://files.pythonhosted.org/packages/b5/a6/2a75320849dd154a793f69c951db759aedb8d1dd3939eeacda9bdcfa1629/pandas-3.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:356e5c055ed9b0da1580d465657bc7d00635af4fd47f30afb23025352ba764d1", size = 11405133, upload-time = "2026-02-17T22:18:48.533Z" }, + { url = "https://files.pythonhosted.org/packages/58/53/1d68fafb2e02d7881df66aa53be4cd748d25cbe311f3b3c85c93ea5d30ca/pandas-3.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9d810036895f9ad6345b8f2a338dd6998a74e8483847403582cab67745bff821", size = 11932065, upload-time = "2026-02-17T22:18:50.837Z" }, + { url = "https://files.pythonhosted.org/packages/75/08/67cc404b3a966b6df27b38370ddd96b3b023030b572283d035181854aac5/pandas-3.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:536232a5fe26dd989bd633e7a0c450705fdc86a207fec7254a55e9a22950fe43", size = 9741627, upload-time = "2026-02-17T22:18:53.905Z" }, + { url = "https://files.pythonhosted.org/packages/86/4f/caf9952948fb00d23795f09b893d11f1cacb384e666854d87249530f7cbe/pandas-3.0.1-cp312-cp312-win_arm64.whl", hash = "sha256:0f463ebfd8de7f326d38037c7363c6dacb857c5881ab8961fb387804d6daf2f7", size = 9052483, upload-time = "2026-02-17T22:18:57.31Z" }, + { url = "https://files.pythonhosted.org/packages/0b/48/aad6ec4f8d007534c091e9a7172b3ec1b1ee6d99a9cbb936b5eab6c6cf58/pandas-3.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5272627187b5d9c20e55d27caf5f2cd23e286aba25cadf73c8590e432e2b7262", size = 10317509, upload-time = "2026-02-17T22:18:59.498Z" }, + { url = "https://files.pythonhosted.org/packages/a8/14/5990826f779f79148ae9d3a2c39593dc04d61d5d90541e71b5749f35af95/pandas-3.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:661e0f665932af88c7877f31da0dc743fe9c8f2524bdffe23d24fdcb67ef9d56", size = 9860561, upload-time = "2026-02-17T22:19:02.265Z" }, + { url = "https://files.pythonhosted.org/packages/fa/80/f01ff54664b6d70fed71475543d108a9b7c888e923ad210795bef04ffb7d/pandas-3.0.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:75e6e292ff898679e47a2199172593d9f6107fd2dd3617c22c2946e97d5df46e", size = 10365506, upload-time = "2026-02-17T22:19:05.017Z" }, + { url = "https://files.pythonhosted.org/packages/f2/85/ab6d04733a7d6ff32bfc8382bf1b07078228f5d6ebec5266b91bfc5c4ff7/pandas-3.0.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1ff8cf1d2896e34343197685f432450ec99a85ba8d90cce2030c5eee2ef98791", size = 10873196, upload-time = "2026-02-17T22:19:07.204Z" }, + { url = "https://files.pythonhosted.org/packages/48/a9/9301c83d0b47c23ac5deab91c6b39fd98d5b5db4d93b25df8d381451828f/pandas-3.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eca8b4510f6763f3d37359c2105df03a7a221a508f30e396a51d0713d462e68a", size = 11370859, upload-time = "2026-02-17T22:19:09.436Z" }, + { url = "https://files.pythonhosted.org/packages/59/fe/0c1fc5bd2d29c7db2ab372330063ad555fb83e08422829c785f5ec2176ca/pandas-3.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:06aff2ad6f0b94a17822cf8b83bbb563b090ed82ff4fe7712db2ce57cd50d9b8", size = 11924584, upload-time = "2026-02-17T22:19:11.562Z" }, + { url = "https://files.pythonhosted.org/packages/d6/7d/216a1588b65a7aa5f4535570418a599d943c85afb1d95b0876fc00aa1468/pandas-3.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:9fea306c783e28884c29057a1d9baa11a349bbf99538ec1da44c8476563d1b25", size = 9742769, upload-time = "2026-02-17T22:19:13.926Z" }, + { url = "https://files.pythonhosted.org/packages/c4/cb/810a22a6af9a4e97c8ab1c946b47f3489c5bca5adc483ce0ffc84c9cc768/pandas-3.0.1-cp313-cp313-win_arm64.whl", hash = "sha256:a8d37a43c52917427e897cb2e429f67a449327394396a81034a4449b99afda59", size = 9043855, upload-time = "2026-02-17T22:19:16.09Z" }, + { url = "https://files.pythonhosted.org/packages/92/fa/423c89086cca1f039cf1253c3ff5b90f157b5b3757314aa635f6bf3e30aa/pandas-3.0.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d54855f04f8246ed7b6fc96b05d4871591143c46c0b6f4af874764ed0d2d6f06", size = 10752673, upload-time = "2026-02-17T22:19:18.304Z" }, + { url = "https://files.pythonhosted.org/packages/22/23/b5a08ec1f40020397f0faba72f1e2c11f7596a6169c7b3e800abff0e433f/pandas-3.0.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e1b677accee34a09e0dc2ce5624e4a58a1870ffe56fc021e9caf7f23cd7668f", size = 10404967, upload-time = "2026-02-17T22:19:20.726Z" }, + { url = "https://files.pythonhosted.org/packages/5c/81/94841f1bb4afdc2b52a99daa895ac2c61600bb72e26525ecc9543d453ebc/pandas-3.0.1-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a9cabbdcd03f1b6cd254d6dda8ae09b0252524be1592594c00b7895916cb1324", size = 10320575, upload-time = "2026-02-17T22:19:24.919Z" }, + { url = "https://files.pythonhosted.org/packages/0a/8b/2ae37d66a5342a83adadfd0cb0b4bf9c3c7925424dd5f40d15d6cfaa35ee/pandas-3.0.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ae2ab1f166668b41e770650101e7090824fd34d17915dd9cd479f5c5e0065e9", size = 10710921, upload-time = "2026-02-17T22:19:27.181Z" }, + { url = "https://files.pythonhosted.org/packages/a2/61/772b2e2757855e232b7ccf7cb8079a5711becb3a97f291c953def15a833f/pandas-3.0.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6bf0603c2e30e2cafac32807b06435f28741135cb8697eae8b28c7d492fc7d76", size = 11334191, upload-time = "2026-02-17T22:19:29.411Z" }, + { url = "https://files.pythonhosted.org/packages/1b/08/b16c6df3ef555d8495d1d265a7963b65be166785d28f06a350913a4fac78/pandas-3.0.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6c426422973973cae1f4a23e51d4ae85974f44871b24844e4f7de752dd877098", size = 11782256, upload-time = "2026-02-17T22:19:32.34Z" }, + { url = "https://files.pythonhosted.org/packages/55/80/178af0594890dee17e239fca96d3d8670ba0f5ff59b7d0439850924a9c09/pandas-3.0.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b03f91ae8c10a85c1613102c7bef5229b5379f343030a3ccefeca8a33414cf35", size = 10485047, upload-time = "2026-02-17T22:19:34.605Z" }, +] + +[[package]] +name = "pathspec" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.4.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634, upload-time = "2025-08-26T14:32:04.268Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.9.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version >= '3.12' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/19/56/8d4c30c8a1d07013911a8fdbd8f89440ef9f08d07a1b50ab8ca8be5a20f9/platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934", size = 28737, upload-time = "2026-03-05T18:34:13.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868", size = 21216, upload-time = "2026-03-05T18:34:12.172Z" }, +] + +[[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, 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, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +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, 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, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.10'" }, + { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "packaging", marker = "python_full_version < '3.10'" }, + { name = "pluggy", marker = "python_full_version < '3.10'" }, + { name = "pygments", marker = "python_full_version < '3.10'" }, + { name = "tomli", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version >= '3.12' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, + { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "packaging", marker = "python_full_version >= '3.10'" }, + { name = "pluggy", marker = "python_full_version >= '3.10'" }, + { name = "pygments", marker = "python_full_version >= '3.10'" }, + { name = "tomli", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", version = "7.10.7", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version < '3.10'" }, + { name = "coverage", version = "7.13.5", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version >= '3.10'" }, + { name = "pluggy" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +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, upload-time = "2025-09-09T10:57:00.695Z" }, +] + +[[package]] +name = "pytest-xdist" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "execnet" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +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, 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, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "pytokens" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/34/b4e015b99031667a7b960f888889c5bd34ef585c85e1cb56a594b92836ac/pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a", size = 23015, upload-time = "2026-01-30T01:03:45.924Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/24/f206113e05cb8ef51b3850e7ef88f20da6f4bf932190ceb48bd3da103e10/pytokens-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a44ed93ea23415c54f3face3b65ef2b844d96aeb3455b8a69b3df6beab6acc5", size = 161522, upload-time = "2026-01-30T01:02:50.393Z" }, + { url = "https://files.pythonhosted.org/packages/d4/e9/06a6bf1b90c2ed81a9c7d2544232fe5d2891d1cd480e8a1809ca354a8eb2/pytokens-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:add8bf86b71a5d9fb5b89f023a80b791e04fba57960aa790cc6125f7f1d39dfe", size = 246945, upload-time = "2026-01-30T01:02:52.399Z" }, + { url = "https://files.pythonhosted.org/packages/69/66/f6fb1007a4c3d8b682d5d65b7c1fb33257587a5f782647091e3408abe0b8/pytokens-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:670d286910b531c7b7e3c0b453fd8156f250adb140146d234a82219459b9640c", size = 259525, upload-time = "2026-01-30T01:02:53.737Z" }, + { url = "https://files.pythonhosted.org/packages/04/92/086f89b4d622a18418bac74ab5db7f68cf0c21cf7cc92de6c7b919d76c88/pytokens-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4e691d7f5186bd2842c14813f79f8884bb03f5995f0575272009982c5ac6c0f7", size = 262693, upload-time = "2026-01-30T01:02:54.871Z" }, + { url = "https://files.pythonhosted.org/packages/b4/7b/8b31c347cf94a3f900bdde750b2e9131575a61fdb620d3d3c75832262137/pytokens-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:27b83ad28825978742beef057bfe406ad6ed524b2d28c252c5de7b4a6dd48fa2", size = 103567, upload-time = "2026-01-30T01:02:56.414Z" }, + { url = "https://files.pythonhosted.org/packages/3d/92/790ebe03f07b57e53b10884c329b9a1a308648fc083a6d4a39a10a28c8fc/pytokens-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d70e77c55ae8380c91c0c18dea05951482e263982911fc7410b1ffd1dadd3440", size = 160864, upload-time = "2026-01-30T01:02:57.882Z" }, + { url = "https://files.pythonhosted.org/packages/13/25/a4f555281d975bfdd1eba731450e2fe3a95870274da73fb12c40aeae7625/pytokens-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a58d057208cb9075c144950d789511220b07636dd2e4708d5645d24de666bdc", size = 248565, upload-time = "2026-01-30T01:02:59.912Z" }, + { url = "https://files.pythonhosted.org/packages/17/50/bc0394b4ad5b1601be22fa43652173d47e4c9efbf0044c62e9a59b747c56/pytokens-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b49750419d300e2b5a3813cf229d4e5a4c728dae470bcc89867a9ad6f25a722d", size = 260824, upload-time = "2026-01-30T01:03:01.471Z" }, + { url = "https://files.pythonhosted.org/packages/4e/54/3e04f9d92a4be4fc6c80016bc396b923d2a6933ae94b5f557c939c460ee0/pytokens-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9907d61f15bf7261d7e775bd5d7ee4d2930e04424bab1972591918497623a16", size = 264075, upload-time = "2026-01-30T01:03:04.143Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1b/44b0326cb5470a4375f37988aea5d61b5cc52407143303015ebee94abfd6/pytokens-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:ee44d0f85b803321710f9239f335aafe16553b39106384cef8e6de40cb4ef2f6", size = 103323, upload-time = "2026-01-30T01:03:05.412Z" }, + { url = "https://files.pythonhosted.org/packages/41/5d/e44573011401fb82e9d51e97f1290ceb377800fb4eed650b96f4753b499c/pytokens-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:140709331e846b728475786df8aeb27d24f48cbcf7bcd449f8de75cae7a45083", size = 160663, upload-time = "2026-01-30T01:03:06.473Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e6/5bbc3019f8e6f21d09c41f8b8654536117e5e211a85d89212d59cbdab381/pytokens-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d6c4268598f762bc8e91f5dbf2ab2f61f7b95bdc07953b602db879b3c8c18e1", size = 255626, upload-time = "2026-01-30T01:03:08.177Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3c/2d5297d82286f6f3d92770289fd439956b201c0a4fc7e72efb9b2293758e/pytokens-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24afde1f53d95348b5a0eb19488661147285ca4dd7ed752bbc3e1c6242a304d1", size = 269779, upload-time = "2026-01-30T01:03:09.756Z" }, + { url = "https://files.pythonhosted.org/packages/20/01/7436e9ad693cebda0551203e0bf28f7669976c60ad07d6402098208476de/pytokens-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ad948d085ed6c16413eb5fec6b3e02fa00dc29a2534f088d3302c47eb59adf9", size = 268076, upload-time = "2026-01-30T01:03:10.957Z" }, + { url = "https://files.pythonhosted.org/packages/2e/df/533c82a3c752ba13ae7ef238b7f8cdd272cf1475f03c63ac6cf3fcfb00b6/pytokens-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:3f901fe783e06e48e8cbdc82d631fca8f118333798193e026a50ce1b3757ea68", size = 103552, upload-time = "2026-01-30T01:03:12.066Z" }, + { url = "https://files.pythonhosted.org/packages/cb/dc/08b1a080372afda3cceb4f3c0a7ba2bde9d6a5241f1edb02a22a019ee147/pytokens-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b", size = 160720, upload-time = "2026-01-30T01:03:13.843Z" }, + { url = "https://files.pythonhosted.org/packages/64/0c/41ea22205da480837a700e395507e6a24425151dfb7ead73343d6e2d7ffe/pytokens-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f", size = 254204, upload-time = "2026-01-30T01:03:14.886Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d2/afe5c7f8607018beb99971489dbb846508f1b8f351fcefc225fcf4b2adc0/pytokens-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1", size = 268423, upload-time = "2026-01-30T01:03:15.936Z" }, + { url = "https://files.pythonhosted.org/packages/68/d4/00ffdbd370410c04e9591da9220a68dc1693ef7499173eb3e30d06e05ed1/pytokens-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4", size = 266859, upload-time = "2026-01-30T01:03:17.458Z" }, + { url = "https://files.pythonhosted.org/packages/a7/c9/c3161313b4ca0c601eeefabd3d3b576edaa9afdefd32da97210700e47652/pytokens-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78", size = 103520, upload-time = "2026-01-30T01:03:18.652Z" }, + { url = "https://files.pythonhosted.org/packages/51/2a/f125667ce48105bf1f4e50e03cfa7b24b8c4f47684d7f1cf4dcb6f6b1c15/pytokens-0.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:34bcc734bd2f2d5fe3b34e7b3c0116bfb2397f2d9666139988e7a3eb5f7400e3", size = 161464, upload-time = "2026-01-30T01:03:39.11Z" }, + { url = "https://files.pythonhosted.org/packages/40/df/065a30790a7ca6bb48ad9018dd44668ed9135610ebf56a2a4cb8e513fd5c/pytokens-0.4.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:941d4343bf27b605e9213b26bfa1c4bf197c9c599a9627eb7305b0defcfe40c1", size = 246159, upload-time = "2026-01-30T01:03:40.131Z" }, + { url = "https://files.pythonhosted.org/packages/a5/1c/fd09976a7e04960dabc07ab0e0072c7813d566ec67d5490a4c600683c158/pytokens-0.4.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3ad72b851e781478366288743198101e5eb34a414f1d5627cdd585ca3b25f1db", size = 259120, upload-time = "2026-01-30T01:03:41.233Z" }, + { url = "https://files.pythonhosted.org/packages/52/49/59fdc6fc5a390ae9f308eadeb97dfc70fc2d804ffc49dd39fc97604622ec/pytokens-0.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:682fa37ff4d8e95f7df6fe6fe6a431e8ed8e788023c6bcc0f0880a12eab80ad1", size = 262196, upload-time = "2026-01-30T01:03:42.696Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e7/d6734dccf0080e3dc00a55b0827ab5af30c886f8bc127bbc04bc3445daec/pytokens-0.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:30f51edd9bb7f85c748979384165601d028b84f7bd13fe14d3e065304093916a", size = 103510, upload-time = "2026-01-30T01:03:43.915Z" }, + { url = "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", size = 13729, upload-time = "2026-01-30T01:03:45.029Z" }, +] + +[[package]] +name = "pytz" +version = "2026.1.post1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/56/db/b8721d71d945e6a8ac63c0fc900b2067181dbb50805958d4d4661cf7d277/pytz-2026.1.post1.tar.gz", hash = "sha256:3378dde6a0c3d26719182142c56e60c7f9af7e968076f31aae569d72a0358ee1", size = 321088, upload-time = "2026-03-03T07:47:50.683Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/99/781fe0c827be2742bcc775efefccb3b048a3a9c6ce9aec0cbf4a101677e5/pytz-2026.1.post1-py2.py3-none-any.whl", hash = "sha256:f2fd16142fda348286a75e1a524be810bb05d444e5a081f37f7affc635035f7a", size = 510489, upload-time = "2026-03-03T07:47:49.167Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { 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, 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, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "roman-numerals" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/f9/41dc953bbeb056c17d5f7a519f50fdf010bd0553be2d630bc69d1e022703/roman_numerals-4.1.0.tar.gz", hash = "sha256:1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2", size = 9077, upload-time = "2025-12-17T18:25:34.381Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl", hash = "sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7", size = 7676, upload-time = "2025-12-17T18:25:33.098Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/df/f8629c19c5318601d3121e230f74cbee7a3732339c52b21daa2b82ef9c7d/ruff-0.15.6.tar.gz", hash = "sha256:8394c7bb153a4e3811a4ecdacd4a8e6a4fa8097028119160dffecdcdf9b56ae4", size = 4597916, upload-time = "2026-03-12T23:05:47.51Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/2f/4e03a7e5ce99b517e98d3b4951f411de2b0fa8348d39cf446671adcce9a2/ruff-0.15.6-py3-none-linux_armv6l.whl", hash = "sha256:7c98c3b16407b2cf3d0f2b80c80187384bc92c6774d85fefa913ecd941256fff", size = 10508953, upload-time = "2026-03-12T23:05:17.246Z" }, + { url = "https://files.pythonhosted.org/packages/70/60/55bcdc3e9f80bcf39edf0cd272da6fa511a3d94d5a0dd9e0adf76ceebdb4/ruff-0.15.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ee7dcfaad8b282a284df4aa6ddc2741b3f4a18b0555d626805555a820ea181c3", size = 10942257, upload-time = "2026-03-12T23:05:23.076Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f9/005c29bd1726c0f492bfa215e95154cf480574140cb5f867c797c18c790b/ruff-0.15.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3bd9967851a25f038fc8b9ae88a7fbd1b609f30349231dffaa37b6804923c4bb", size = 10322683, upload-time = "2026-03-12T23:05:33.738Z" }, + { url = "https://files.pythonhosted.org/packages/5f/74/2f861f5fd7cbb2146bddb5501450300ce41562da36d21868c69b7a828169/ruff-0.15.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13f4594b04e42cd24a41da653886b04d2ff87adbf57497ed4f728b0e8a4866f8", size = 10660986, upload-time = "2026-03-12T23:05:53.245Z" }, + { url = "https://files.pythonhosted.org/packages/c1/a1/309f2364a424eccb763cdafc49df843c282609f47fe53aa83f38272389e0/ruff-0.15.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e2ed8aea2f3fe57886d3f00ea5b8aae5bf68d5e195f487f037a955ff9fbaac9e", size = 10332177, upload-time = "2026-03-12T23:05:56.145Z" }, + { url = "https://files.pythonhosted.org/packages/30/41/7ebf1d32658b4bab20f8ac80972fb19cd4e2c6b78552be263a680edc55ac/ruff-0.15.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70789d3e7830b848b548aae96766431c0dc01a6c78c13381f423bf7076c66d15", size = 11170783, upload-time = "2026-03-12T23:06:01.742Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/6d488f6adca047df82cd62c304638bcb00821c36bd4881cfca221561fdfc/ruff-0.15.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:542aaf1de3154cea088ced5a819ce872611256ffe2498e750bbae5247a8114e9", size = 12044201, upload-time = "2026-03-12T23:05:28.697Z" }, + { url = "https://files.pythonhosted.org/packages/71/68/e6f125df4af7e6d0b498f8d373274794bc5156b324e8ab4bf5c1b4fc0ec7/ruff-0.15.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c22e6f02c16cfac3888aa636e9eba857254d15bbacc9906c9689fdecb1953ab", size = 11421561, upload-time = "2026-03-12T23:05:31.236Z" }, + { url = "https://files.pythonhosted.org/packages/f1/9f/f85ef5fd01a52e0b472b26dc1b4bd228b8f6f0435975442ffa4741278703/ruff-0.15.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98893c4c0aadc8e448cfa315bd0cc343a5323d740fe5f28ef8a3f9e21b381f7e", size = 11310928, upload-time = "2026-03-12T23:05:45.288Z" }, + { url = "https://files.pythonhosted.org/packages/8c/26/b75f8c421f5654304b89471ed384ae8c7f42b4dff58fa6ce1626d7f2b59a/ruff-0.15.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:70d263770d234912374493e8cc1e7385c5d49376e41dfa51c5c3453169dc581c", size = 11235186, upload-time = "2026-03-12T23:05:50.677Z" }, + { url = "https://files.pythonhosted.org/packages/fc/d4/d5a6d065962ff7a68a86c9b4f5500f7d101a0792078de636526c0edd40da/ruff-0.15.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:55a1ad63c5a6e54b1f21b7514dfadc0c7fb40093fa22e95143cf3f64ebdcd512", size = 10635231, upload-time = "2026-03-12T23:05:37.044Z" }, + { url = "https://files.pythonhosted.org/packages/d6/56/7c3acf3d50910375349016cf33de24be021532042afbed87942858992491/ruff-0.15.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8dc473ba093c5ec238bb1e7429ee676dca24643c471e11fbaa8a857925b061c0", size = 10340357, upload-time = "2026-03-12T23:06:04.748Z" }, + { url = "https://files.pythonhosted.org/packages/06/54/6faa39e9c1033ff6a3b6e76b5df536931cd30caf64988e112bbf91ef5ce5/ruff-0.15.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:85b042377c2a5561131767974617006f99f7e13c63c111b998f29fc1e58a4cfb", size = 10860583, upload-time = "2026-03-12T23:05:58.978Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/509a201b843b4dfb0b32acdedf68d951d3377988cae43949ba4c4133a96a/ruff-0.15.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cef49e30bc5a86a6a92098a7fbf6e467a234d90b63305d6f3ec01225a9d092e0", size = 11410976, upload-time = "2026-03-12T23:05:39.955Z" }, + { url = "https://files.pythonhosted.org/packages/6c/25/3fc9114abf979a41673ce877c08016f8e660ad6cf508c3957f537d2e9fa9/ruff-0.15.6-py3-none-win32.whl", hash = "sha256:bbf67d39832404812a2d23020dda68fee7f18ce15654e96fb1d3ad21a5fe436c", size = 10616872, upload-time = "2026-03-12T23:05:42.451Z" }, + { url = "https://files.pythonhosted.org/packages/89/7a/09ece68445ceac348df06e08bf75db72d0e8427765b96c9c0ffabc1be1d9/ruff-0.15.6-py3-none-win_amd64.whl", hash = "sha256:aee25bc84c2f1007ecb5037dff75cef00414fdf17c23f07dc13e577883dca406", size = 11787271, upload-time = "2026-03-12T23:05:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/7f/d0/578c47dd68152ddddddf31cd7fc67dc30b7cdf639a86275fda821b0d9d98/ruff-0.15.6-py3-none-win_arm64.whl", hash = "sha256:c34de3dd0b0ba203be50ae70f5910b17188556630e2178fd7d79fc030eb0d837", size = 11060497, upload-time = "2026-03-12T23:05:25.968Z" }, +] + +[[package]] +name = "scipy" +version = "1.13.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/00/48c2f661e2816ccf2ecd77982f6605b2950afe60f60a52b4cbbc2504aa8f/scipy-1.13.1.tar.gz", hash = "sha256:095a87a0312b08dfd6a6155cbbd310a8c51800fc931b8c0b84003014b874ed3c", size = 57210720, upload-time = "2024-05-23T03:29:26.079Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/59/41b2529908c002ade869623b87eecff3e11e3ce62e996d0bdcb536984187/scipy-1.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:20335853b85e9a49ff7572ab453794298bcf0354d8068c5f6775a0eabf350aca", size = 39328076, upload-time = "2024-05-23T03:19:01.687Z" }, + { url = "https://files.pythonhosted.org/packages/d5/33/f1307601f492f764062ce7dd471a14750f3360e33cd0f8c614dae208492c/scipy-1.13.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:d605e9c23906d1994f55ace80e0125c587f96c020037ea6aa98d01b4bd2e222f", size = 30306232, upload-time = "2024-05-23T03:19:09.089Z" }, + { url = "https://files.pythonhosted.org/packages/c0/66/9cd4f501dd5ea03e4a4572ecd874936d0da296bd04d1c45ae1a4a75d9c3a/scipy-1.13.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cfa31f1def5c819b19ecc3a8b52d28ffdcc7ed52bb20c9a7589669dd3c250989", size = 33743202, upload-time = "2024-05-23T03:19:15.138Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ba/7255e5dc82a65adbe83771c72f384d99c43063648456796436c9a5585ec3/scipy-1.13.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26264b282b9da0952a024ae34710c2aff7d27480ee91a2e82b7b7073c24722f", size = 38577335, upload-time = "2024-05-23T03:19:21.984Z" }, + { url = "https://files.pythonhosted.org/packages/49/a5/bb9ded8326e9f0cdfdc412eeda1054b914dfea952bda2097d174f8832cc0/scipy-1.13.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:eccfa1906eacc02de42d70ef4aecea45415f5be17e72b61bafcfd329bdc52e94", size = 38820728, upload-time = "2024-05-23T03:19:28.225Z" }, + { url = "https://files.pythonhosted.org/packages/12/30/df7a8fcc08f9b4a83f5f27cfaaa7d43f9a2d2ad0b6562cced433e5b04e31/scipy-1.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:2831f0dc9c5ea9edd6e51e6e769b655f08ec6db6e2e10f86ef39bd32eb11da54", size = 46210588, upload-time = "2024-05-23T03:19:35.661Z" }, + { url = "https://files.pythonhosted.org/packages/b4/15/4a4bb1b15bbd2cd2786c4f46e76b871b28799b67891f23f455323a0cdcfb/scipy-1.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:27e52b09c0d3a1d5b63e1105f24177e544a222b43611aaf5bc44d4a0979e32f9", size = 39333805, upload-time = "2024-05-23T03:19:43.081Z" }, + { url = "https://files.pythonhosted.org/packages/ba/92/42476de1af309c27710004f5cdebc27bec62c204db42e05b23a302cb0c9a/scipy-1.13.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:54f430b00f0133e2224c3ba42b805bfd0086fe488835effa33fa291561932326", size = 30317687, upload-time = "2024-05-23T03:19:48.799Z" }, + { url = "https://files.pythonhosted.org/packages/80/ba/8be64fe225360a4beb6840f3cbee494c107c0887f33350d0a47d55400b01/scipy-1.13.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e89369d27f9e7b0884ae559a3a956e77c02114cc60a6058b4e5011572eea9299", size = 33694638, upload-time = "2024-05-23T03:19:55.104Z" }, + { url = "https://files.pythonhosted.org/packages/36/07/035d22ff9795129c5a847c64cb43c1fa9188826b59344fee28a3ab02e283/scipy-1.13.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a78b4b3345f1b6f68a763c6e25c0c9a23a9fd0f39f5f3d200efe8feda560a5fa", size = 38569931, upload-time = "2024-05-23T03:20:01.82Z" }, + { url = "https://files.pythonhosted.org/packages/d9/10/f9b43de37e5ed91facc0cfff31d45ed0104f359e4f9a68416cbf4e790241/scipy-1.13.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:45484bee6d65633752c490404513b9ef02475b4284c4cfab0ef946def50b3f59", size = 38838145, upload-time = "2024-05-23T03:20:09.173Z" }, + { url = "https://files.pythonhosted.org/packages/4a/48/4513a1a5623a23e95f94abd675ed91cfb19989c58e9f6f7d03990f6caf3d/scipy-1.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:5713f62f781eebd8d597eb3f88b8bf9274e79eeabf63afb4a737abc6c84ad37b", size = 46196227, upload-time = "2024-05-23T03:20:16.433Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7b/fb6b46fbee30fc7051913068758414f2721003a89dd9a707ad49174e3843/scipy-1.13.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5d72782f39716b2b3509cd7c33cdc08c96f2f4d2b06d51e52fb45a19ca0c86a1", size = 39357301, upload-time = "2024-05-23T03:20:23.538Z" }, + { url = "https://files.pythonhosted.org/packages/dc/5a/2043a3bde1443d94014aaa41e0b50c39d046dda8360abd3b2a1d3f79907d/scipy-1.13.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:017367484ce5498445aade74b1d5ab377acdc65e27095155e448c88497755a5d", size = 30363348, upload-time = "2024-05-23T03:20:29.885Z" }, + { url = "https://files.pythonhosted.org/packages/e7/cb/26e4a47364bbfdb3b7fb3363be6d8a1c543bcd70a7753ab397350f5f189a/scipy-1.13.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:949ae67db5fa78a86e8fa644b9a6b07252f449dcf74247108c50e1d20d2b4627", size = 33406062, upload-time = "2024-05-23T03:20:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/88/ab/6ecdc526d509d33814835447bbbeedbebdec7cca46ef495a61b00a35b4bf/scipy-1.13.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de3ade0e53bc1f21358aa74ff4830235d716211d7d077e340c7349bc3542e884", size = 38218311, upload-time = "2024-05-23T03:20:42.086Z" }, + { url = "https://files.pythonhosted.org/packages/0b/00/9f54554f0f8318100a71515122d8f4f503b1a2c4b4cfab3b4b68c0eb08fa/scipy-1.13.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2ac65fb503dad64218c228e2dc2d0a0193f7904747db43014645ae139c8fad16", size = 38442493, upload-time = "2024-05-23T03:20:48.292Z" }, + { url = "https://files.pythonhosted.org/packages/3e/df/963384e90733e08eac978cd103c34df181d1fec424de383cdc443f418dd4/scipy-1.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:cdd7dacfb95fea358916410ec61bbc20440f7860333aee6d882bb8046264e949", size = 45910955, upload-time = "2024-05-23T03:20:55.091Z" }, + { url = "https://files.pythonhosted.org/packages/7f/29/c2ea58c9731b9ecb30b6738113a95d147e83922986b34c685b8f6eefde21/scipy-1.13.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:436bbb42a94a8aeef855d755ce5a465479c721e9d684de76bf61a62e7c2b81d5", size = 39352927, upload-time = "2024-05-23T03:21:01.95Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c0/e71b94b20ccf9effb38d7147c0064c08c622309fd487b1b677771a97d18c/scipy-1.13.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:8335549ebbca860c52bf3d02f80784e91a004b71b059e3eea9678ba994796a24", size = 30324538, upload-time = "2024-05-23T03:21:07.634Z" }, + { url = "https://files.pythonhosted.org/packages/6d/0f/aaa55b06d474817cea311e7b10aab2ea1fd5d43bc6a2861ccc9caec9f418/scipy-1.13.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d533654b7d221a6a97304ab63c41c96473ff04459e404b83275b60aa8f4b7004", size = 33732190, upload-time = "2024-05-23T03:21:14.41Z" }, + { url = "https://files.pythonhosted.org/packages/35/f5/d0ad1a96f80962ba65e2ce1de6a1e59edecd1f0a7b55990ed208848012e0/scipy-1.13.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:637e98dcf185ba7f8e663e122ebf908c4702420477ae52a04f9908707456ba4d", size = 38612244, upload-time = "2024-05-23T03:21:21.827Z" }, + { url = "https://files.pythonhosted.org/packages/8d/02/1165905f14962174e6569076bcc3315809ae1291ed14de6448cc151eedfd/scipy-1.13.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a014c2b3697bde71724244f63de2476925596c24285c7a637364761f8710891c", size = 38845637, upload-time = "2024-05-23T03:21:28.729Z" }, + { url = "https://files.pythonhosted.org/packages/3e/77/dab54fe647a08ee4253963bcd8f9cf17509c8ca64d6335141422fe2e2114/scipy-1.13.1-cp39-cp39-win_amd64.whl", hash = "sha256:392e4ec766654852c25ebad4f64e4e584cf19820b980bc04960bca0b0cd6eaa2", size = 46227440, upload-time = "2024-05-23T03:21:35.888Z" }, +] + +[[package]] +name = "scipy" +version = "1.15.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/2f/4966032c5f8cc7e6a60f1b2e0ad686293b9474b65246b0c642e3ef3badd0/scipy-1.15.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c", size = 38702770, upload-time = "2025-05-08T16:04:20.849Z" }, + { url = "https://files.pythonhosted.org/packages/a0/6e/0c3bf90fae0e910c274db43304ebe25a6b391327f3f10b5dcc638c090795/scipy-1.15.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253", size = 30094511, upload-time = "2025-05-08T16:04:27.103Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b1/4deb37252311c1acff7f101f6453f0440794f51b6eacb1aad4459a134081/scipy-1.15.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:aef683a9ae6eb00728a542b796f52a5477b78252edede72b8327a886ab63293f", size = 22368151, upload-time = "2025-05-08T16:04:31.731Z" }, + { url = "https://files.pythonhosted.org/packages/38/7d/f457626e3cd3c29b3a49ca115a304cebb8cc6f31b04678f03b216899d3c6/scipy-1.15.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:1c832e1bd78dea67d5c16f786681b28dd695a8cb1fb90af2e27580d3d0967e92", size = 25121732, upload-time = "2025-05-08T16:04:36.596Z" }, + { url = "https://files.pythonhosted.org/packages/db/0a/92b1de4a7adc7a15dcf5bddc6e191f6f29ee663b30511ce20467ef9b82e4/scipy-1.15.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:263961f658ce2165bbd7b99fa5135195c3a12d9bef045345016b8b50c315cb82", size = 35547617, upload-time = "2025-05-08T16:04:43.546Z" }, + { url = "https://files.pythonhosted.org/packages/8e/6d/41991e503e51fc1134502694c5fa7a1671501a17ffa12716a4a9151af3df/scipy-1.15.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2abc762b0811e09a0d3258abee2d98e0c703eee49464ce0069590846f31d40", size = 37662964, upload-time = "2025-05-08T16:04:49.431Z" }, + { url = "https://files.pythonhosted.org/packages/25/e1/3df8f83cb15f3500478c889be8fb18700813b95e9e087328230b98d547ff/scipy-1.15.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ed7284b21a7a0c8f1b6e5977ac05396c0d008b89e05498c8b7e8f4a1423bba0e", size = 37238749, upload-time = "2025-05-08T16:04:55.215Z" }, + { url = "https://files.pythonhosted.org/packages/93/3e/b3257cf446f2a3533ed7809757039016b74cd6f38271de91682aa844cfc5/scipy-1.15.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5380741e53df2c566f4d234b100a484b420af85deb39ea35a1cc1be84ff53a5c", size = 40022383, upload-time = "2025-05-08T16:05:01.914Z" }, + { url = "https://files.pythonhosted.org/packages/d1/84/55bc4881973d3f79b479a5a2e2df61c8c9a04fcb986a213ac9c02cfb659b/scipy-1.15.3-cp310-cp310-win_amd64.whl", hash = "sha256:9d61e97b186a57350f6d6fd72640f9e99d5a4a2b8fbf4b9ee9a841eab327dc13", size = 41259201, upload-time = "2025-05-08T16:05:08.166Z" }, + { url = "https://files.pythonhosted.org/packages/96/ab/5cc9f80f28f6a7dff646c5756e559823614a42b1939d86dd0ed550470210/scipy-1.15.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:993439ce220d25e3696d1b23b233dd010169b62f6456488567e830654ee37a6b", size = 38714255, upload-time = "2025-05-08T16:05:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4a/66ba30abe5ad1a3ad15bfb0b59d22174012e8056ff448cb1644deccbfed2/scipy-1.15.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:34716e281f181a02341ddeaad584205bd2fd3c242063bd3423d61ac259ca7eba", size = 30111035, upload-time = "2025-05-08T16:05:20.152Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fa/a7e5b95afd80d24313307f03624acc65801846fa75599034f8ceb9e2cbf6/scipy-1.15.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3b0334816afb8b91dab859281b1b9786934392aa3d527cd847e41bb6f45bee65", size = 22384499, upload-time = "2025-05-08T16:05:24.494Z" }, + { url = "https://files.pythonhosted.org/packages/17/99/f3aaddccf3588bb4aea70ba35328c204cadd89517a1612ecfda5b2dd9d7a/scipy-1.15.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:6db907c7368e3092e24919b5e31c76998b0ce1684d51a90943cb0ed1b4ffd6c1", size = 25152602, upload-time = "2025-05-08T16:05:29.313Z" }, + { url = "https://files.pythonhosted.org/packages/56/c5/1032cdb565f146109212153339f9cb8b993701e9fe56b1c97699eee12586/scipy-1.15.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:721d6b4ef5dc82ca8968c25b111e307083d7ca9091bc38163fb89243e85e3889", size = 35503415, upload-time = "2025-05-08T16:05:34.699Z" }, + { url = "https://files.pythonhosted.org/packages/bd/37/89f19c8c05505d0601ed5650156e50eb881ae3918786c8fd7262b4ee66d3/scipy-1.15.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39cb9c62e471b1bb3750066ecc3a3f3052b37751c7c3dfd0fd7e48900ed52982", size = 37652622, upload-time = "2025-05-08T16:05:40.762Z" }, + { url = "https://files.pythonhosted.org/packages/7e/31/be59513aa9695519b18e1851bb9e487de66f2d31f835201f1b42f5d4d475/scipy-1.15.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:795c46999bae845966368a3c013e0e00947932d68e235702b5c3f6ea799aa8c9", size = 37244796, upload-time = "2025-05-08T16:05:48.119Z" }, + { url = "https://files.pythonhosted.org/packages/10/c0/4f5f3eeccc235632aab79b27a74a9130c6c35df358129f7ac8b29f562ac7/scipy-1.15.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:18aaacb735ab38b38db42cb01f6b92a2d0d4b6aabefeb07f02849e47f8fb3594", size = 40047684, upload-time = "2025-05-08T16:05:54.22Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a7/0ddaf514ce8a8714f6ed243a2b391b41dbb65251affe21ee3077ec45ea9a/scipy-1.15.3-cp311-cp311-win_amd64.whl", hash = "sha256:ae48a786a28412d744c62fd7816a4118ef97e5be0bee968ce8f0a2fba7acf3bb", size = 41246504, upload-time = "2025-05-08T16:06:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/37/4b/683aa044c4162e10ed7a7ea30527f2cbd92e6999c10a8ed8edb253836e9c/scipy-1.15.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ac6310fdbfb7aa6612408bd2f07295bcbd3fda00d2d702178434751fe48e019", size = 38766735, upload-time = "2025-05-08T16:06:06.471Z" }, + { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:185cd3d6d05ca4b44a8f1595af87f9c372bb6acf9c808e99aa3e9aa03bd98cf6", size = 30173284, upload-time = "2025-05-08T16:06:11.686Z" }, + { url = "https://files.pythonhosted.org/packages/07/9c/0ddb0d0abdabe0d181c1793db51f02cd59e4901da6f9f7848e1f96759f0d/scipy-1.15.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477", size = 22446958, upload-time = "2025-05-08T16:06:15.97Z" }, + { url = "https://files.pythonhosted.org/packages/af/43/0bce905a965f36c58ff80d8bea33f1f9351b05fad4beaad4eae34699b7a1/scipy-1.15.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c", size = 25242454, upload-time = "2025-05-08T16:06:20.394Z" }, + { url = "https://files.pythonhosted.org/packages/56/30/a6f08f84ee5b7b28b4c597aca4cbe545535c39fe911845a96414700b64ba/scipy-1.15.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05045d8b9bfd807ee1b9f38761993297b10b245f012b11b13b91ba8945f7e45", size = 35210199, upload-time = "2025-05-08T16:06:26.159Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1f/03f52c282437a168ee2c7c14a1a0d0781a9a4a8962d84ac05c06b4c5b555/scipy-1.15.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271e3713e645149ea5ea3e97b57fdab61ce61333f97cfae392c28ba786f9bb49", size = 37309455, upload-time = "2025-05-08T16:06:32.778Z" }, + { url = "https://files.pythonhosted.org/packages/89/b1/fbb53137f42c4bf630b1ffdfc2151a62d1d1b903b249f030d2b1c0280af8/scipy-1.15.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cfd56fc1a8e53f6e89ba3a7a7251f7396412d655bca2aa5611c8ec9a6784a1e", size = 36885140, upload-time = "2025-05-08T16:06:39.249Z" }, + { url = "https://files.pythonhosted.org/packages/2e/2e/025e39e339f5090df1ff266d021892694dbb7e63568edcfe43f892fa381d/scipy-1.15.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ff17c0bb1cb32952c09217d8d1eed9b53d1463e5f1dd6052c7857f83127d539", size = 39710549, upload-time = "2025-05-08T16:06:45.729Z" }, + { url = "https://files.pythonhosted.org/packages/e6/eb/3bf6ea8ab7f1503dca3a10df2e4b9c3f6b3316df07f6c0ded94b281c7101/scipy-1.15.3-cp312-cp312-win_amd64.whl", hash = "sha256:52092bc0472cfd17df49ff17e70624345efece4e1a12b23783a1ac59a1b728ed", size = 40966184, upload-time = "2025-05-08T16:06:52.623Z" }, + { url = "https://files.pythonhosted.org/packages/73/18/ec27848c9baae6e0d6573eda6e01a602e5649ee72c27c3a8aad673ebecfd/scipy-1.15.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c620736bcc334782e24d173c0fdbb7590a0a436d2fdf39310a8902505008759", size = 38728256, upload-time = "2025-05-08T16:06:58.696Z" }, + { url = "https://files.pythonhosted.org/packages/74/cd/1aef2184948728b4b6e21267d53b3339762c285a46a274ebb7863c9e4742/scipy-1.15.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:7e11270a000969409d37ed399585ee530b9ef6aa99d50c019de4cb01e8e54e62", size = 30109540, upload-time = "2025-05-08T16:07:04.209Z" }, + { url = "https://files.pythonhosted.org/packages/5b/d8/59e452c0a255ec352bd0a833537a3bc1bfb679944c4938ab375b0a6b3a3e/scipy-1.15.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8c9ed3ba2c8a2ce098163a9bdb26f891746d02136995df25227a20e71c396ebb", size = 22383115, upload-time = "2025-05-08T16:07:08.998Z" }, + { url = "https://files.pythonhosted.org/packages/08/f5/456f56bbbfccf696263b47095291040655e3cbaf05d063bdc7c7517f32ac/scipy-1.15.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0bdd905264c0c9cfa74a4772cdb2070171790381a5c4d312c973382fc6eaf730", size = 25163884, upload-time = "2025-05-08T16:07:14.091Z" }, + { url = "https://files.pythonhosted.org/packages/a2/66/a9618b6a435a0f0c0b8a6d0a2efb32d4ec5a85f023c2b79d39512040355b/scipy-1.15.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79167bba085c31f38603e11a267d862957cbb3ce018d8b38f79ac043bc92d825", size = 35174018, upload-time = "2025-05-08T16:07:19.427Z" }, + { url = "https://files.pythonhosted.org/packages/b5/09/c5b6734a50ad4882432b6bb7c02baf757f5b2f256041da5df242e2d7e6b6/scipy-1.15.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9deabd6d547aee2c9a81dee6cc96c6d7e9a9b1953f74850c179f91fdc729cb7", size = 37269716, upload-time = "2025-05-08T16:07:25.712Z" }, + { url = "https://files.pythonhosted.org/packages/77/0a/eac00ff741f23bcabd352731ed9b8995a0a60ef57f5fd788d611d43d69a1/scipy-1.15.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dde4fc32993071ac0c7dd2d82569e544f0bdaff66269cb475e0f369adad13f11", size = 36872342, upload-time = "2025-05-08T16:07:31.468Z" }, + { url = "https://files.pythonhosted.org/packages/fe/54/4379be86dd74b6ad81551689107360d9a3e18f24d20767a2d5b9253a3f0a/scipy-1.15.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f77f853d584e72e874d87357ad70f44b437331507d1c311457bed8ed2b956126", size = 39670869, upload-time = "2025-05-08T16:07:38.002Z" }, + { url = "https://files.pythonhosted.org/packages/87/2e/892ad2862ba54f084ffe8cc4a22667eaf9c2bcec6d2bff1d15713c6c0703/scipy-1.15.3-cp313-cp313-win_amd64.whl", hash = "sha256:b90ab29d0c37ec9bf55424c064312930ca5f4bde15ee8619ee44e69319aab163", size = 40988851, upload-time = "2025-05-08T16:08:33.671Z" }, + { url = "https://files.pythonhosted.org/packages/1b/e9/7a879c137f7e55b30d75d90ce3eb468197646bc7b443ac036ae3fe109055/scipy-1.15.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3ac07623267feb3ae308487c260ac684b32ea35fd81e12845039952f558047b8", size = 38863011, upload-time = "2025-05-08T16:07:44.039Z" }, + { url = "https://files.pythonhosted.org/packages/51/d1/226a806bbd69f62ce5ef5f3ffadc35286e9fbc802f606a07eb83bf2359de/scipy-1.15.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6487aa99c2a3d509a5227d9a5e889ff05830a06b2ce08ec30df6d79db5fcd5c5", size = 30266407, upload-time = "2025-05-08T16:07:49.891Z" }, + { url = "https://files.pythonhosted.org/packages/e5/9b/f32d1d6093ab9eeabbd839b0f7619c62e46cc4b7b6dbf05b6e615bbd4400/scipy-1.15.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:50f9e62461c95d933d5c5ef4a1f2ebf9a2b4e83b0db374cb3f1de104d935922e", size = 22540030, upload-time = "2025-05-08T16:07:54.121Z" }, + { url = "https://files.pythonhosted.org/packages/e7/29/c278f699b095c1a884f29fda126340fcc201461ee8bfea5c8bdb1c7c958b/scipy-1.15.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:14ed70039d182f411ffc74789a16df3835e05dc469b898233a245cdfd7f162cb", size = 25218709, upload-time = "2025-05-08T16:07:58.506Z" }, + { url = "https://files.pythonhosted.org/packages/24/18/9e5374b617aba742a990581373cd6b68a2945d65cc588482749ef2e64467/scipy-1.15.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a769105537aa07a69468a0eefcd121be52006db61cdd8cac8a0e68980bbb723", size = 34809045, upload-time = "2025-05-08T16:08:03.929Z" }, + { url = "https://files.pythonhosted.org/packages/e1/fe/9c4361e7ba2927074360856db6135ef4904d505e9b3afbbcb073c4008328/scipy-1.15.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db984639887e3dffb3928d118145ffe40eff2fa40cb241a306ec57c219ebbbb", size = 36703062, upload-time = "2025-05-08T16:08:09.558Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8e/038ccfe29d272b30086b25a4960f757f97122cb2ec42e62b460d02fe98e9/scipy-1.15.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:40e54d5c7e7ebf1aa596c374c49fa3135f04648a0caabcb66c52884b943f02b4", size = 36393132, upload-time = "2025-05-08T16:08:15.34Z" }, + { url = "https://files.pythonhosted.org/packages/10/7e/5c12285452970be5bdbe8352c619250b97ebf7917d7a9a9e96b8a8140f17/scipy-1.15.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5e721fed53187e71d0ccf382b6bf977644c533e506c4d33c3fb24de89f5c3ed5", size = 38979503, upload-time = "2025-05-08T16:08:21.513Z" }, + { url = "https://files.pythonhosted.org/packages/81/06/0a5e5349474e1cbc5757975b21bd4fad0e72ebf138c5592f191646154e06/scipy-1.15.3-cp313-cp313t-win_amd64.whl", hash = "sha256:76ad1fb5f8752eabf0fa02e4cc0336b4e8f021e2d5f061ed37d6d264db35e3ca", size = 40308097, upload-time = "2025-05-08T16:08:27.627Z" }, +] + +[[package]] +name = "scipy" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version >= '3.12' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/75/b4ce781849931fef6fd529afa6b63711d5a733065722d0c3e2724af9e40a/scipy-1.17.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:1f95b894f13729334fb990162e911c9e5dc1ab390c58aa6cbecb389c5b5e28ec", size = 31613675, upload-time = "2026-02-23T00:16:00.13Z" }, + { url = "https://files.pythonhosted.org/packages/f7/58/bccc2861b305abdd1b8663d6130c0b3d7cc22e8d86663edbc8401bfd40d4/scipy-1.17.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:e18f12c6b0bc5a592ed23d3f7b891f68fd7f8241d69b7883769eb5d5dfb52696", size = 28162057, upload-time = "2026-02-23T00:16:09.456Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ee/18146b7757ed4976276b9c9819108adbc73c5aad636e5353e20746b73069/scipy-1.17.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a3472cfbca0a54177d0faa68f697d8ba4c80bbdc19908c3465556d9f7efce9ee", size = 20334032, upload-time = "2026-02-23T00:16:17.358Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e6/cef1cf3557f0c54954198554a10016b6a03b2ec9e22a4e1df734936bd99c/scipy-1.17.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:766e0dc5a616d026a3a1cffa379af959671729083882f50307e18175797b3dfd", size = 22709533, upload-time = "2026-02-23T00:16:25.791Z" }, + { url = "https://files.pythonhosted.org/packages/4d/60/8804678875fc59362b0fb759ab3ecce1f09c10a735680318ac30da8cd76b/scipy-1.17.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:744b2bf3640d907b79f3fd7874efe432d1cf171ee721243e350f55234b4cec4c", size = 33062057, upload-time = "2026-02-23T00:16:36.931Z" }, + { url = "https://files.pythonhosted.org/packages/09/7d/af933f0f6e0767995b4e2d705a0665e454d1c19402aa7e895de3951ebb04/scipy-1.17.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43af8d1f3bea642559019edfe64e9b11192a8978efbd1539d7bc2aaa23d92de4", size = 35349300, upload-time = "2026-02-23T00:16:49.108Z" }, + { url = "https://files.pythonhosted.org/packages/b4/3d/7ccbbdcbb54c8fdc20d3b6930137c782a163fa626f0aef920349873421ba/scipy-1.17.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd96a1898c0a47be4520327e01f874acfd61fb48a9420f8aa9f6483412ffa444", size = 35127333, upload-time = "2026-02-23T00:17:01.293Z" }, + { url = "https://files.pythonhosted.org/packages/e8/19/f926cb11c42b15ba08e3a71e376d816ac08614f769b4f47e06c3580c836a/scipy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4eb6c25dd62ee8d5edf68a8e1c171dd71c292fdae95d8aeb3dd7d7de4c364082", size = 37741314, upload-time = "2026-02-23T00:17:12.576Z" }, + { url = "https://files.pythonhosted.org/packages/95/da/0d1df507cf574b3f224ccc3d45244c9a1d732c81dcb26b1e8a766ae271a8/scipy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:d30e57c72013c2a4fe441c2fcb8e77b14e152ad48b5464858e07e2ad9fbfceff", size = 36607512, upload-time = "2026-02-23T00:17:23.424Z" }, + { url = "https://files.pythonhosted.org/packages/68/7f/bdd79ceaad24b671543ffe0ef61ed8e659440eb683b66f033454dcee90eb/scipy-1.17.1-cp311-cp311-win_arm64.whl", hash = "sha256:9ecb4efb1cd6e8c4afea0daa91a87fbddbce1b99d2895d151596716c0b2e859d", size = 24599248, upload-time = "2026-02-23T00:17:34.561Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/b992b488d6f299dbe3f11a20b24d3dda3d46f1a635ede1c46b5b17a7b163/scipy-1.17.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:35c3a56d2ef83efc372eaec584314bd0ef2e2f0d2adb21c55e6ad5b344c0dcb8", size = 31610954, upload-time = "2026-02-23T00:17:49.855Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/cf107b01494c19dc100f1d0b7ac3cc08666e96ba2d64db7626066cee895e/scipy-1.17.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:fcb310ddb270a06114bb64bbe53c94926b943f5b7f0842194d585c65eb4edd76", size = 28172662, upload-time = "2026-02-23T00:18:01.64Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a9/599c28631bad314d219cf9ffd40e985b24d603fc8a2f4ccc5ae8419a535b/scipy-1.17.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cc90d2e9c7e5c7f1a482c9875007c095c3194b1cfedca3c2f3291cdc2bc7c086", size = 20344366, upload-time = "2026-02-23T00:18:12.015Z" }, + { url = "https://files.pythonhosted.org/packages/35/f5/906eda513271c8deb5af284e5ef0206d17a96239af79f9fa0aebfe0e36b4/scipy-1.17.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:c80be5ede8f3f8eded4eff73cc99a25c388ce98e555b17d31da05287015ffa5b", size = 22704017, upload-time = "2026-02-23T00:18:21.502Z" }, + { url = "https://files.pythonhosted.org/packages/da/34/16f10e3042d2f1d6b66e0428308ab52224b6a23049cb2f5c1756f713815f/scipy-1.17.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e19ebea31758fac5893a2ac360fedd00116cbb7628e650842a6691ba7ca28a21", size = 32927842, upload-time = "2026-02-23T00:18:35.367Z" }, + { url = "https://files.pythonhosted.org/packages/01/8e/1e35281b8ab6d5d72ebe9911edcdffa3f36b04ed9d51dec6dd140396e220/scipy-1.17.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02ae3b274fde71c5e92ac4d54bc06c42d80e399fec704383dcd99b301df37458", size = 35235890, upload-time = "2026-02-23T00:18:49.188Z" }, + { url = "https://files.pythonhosted.org/packages/c5/5c/9d7f4c88bea6e0d5a4f1bc0506a53a00e9fcb198de372bfe4d3652cef482/scipy-1.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a604bae87c6195d8b1045eddece0514d041604b14f2727bbc2b3020172045eb", size = 35003557, upload-time = "2026-02-23T00:18:54.74Z" }, + { url = "https://files.pythonhosted.org/packages/65/94/7698add8f276dbab7a9de9fb6b0e02fc13ee61d51c7c3f85ac28b65e1239/scipy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f590cd684941912d10becc07325a3eeb77886fe981415660d9265c4c418d0bea", size = 37625856, upload-time = "2026-02-23T00:19:00.307Z" }, + { url = "https://files.pythonhosted.org/packages/a2/84/dc08d77fbf3d87d3ee27f6a0c6dcce1de5829a64f2eae85a0ecc1f0daa73/scipy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:41b71f4a3a4cab9d366cd9065b288efc4d4f3c0b37a91a8e0947fb5bd7f31d87", size = 36549682, upload-time = "2026-02-23T00:19:07.67Z" }, + { url = "https://files.pythonhosted.org/packages/bc/98/fe9ae9ffb3b54b62559f52dedaebe204b408db8109a8c66fdd04869e6424/scipy-1.17.1-cp312-cp312-win_arm64.whl", hash = "sha256:f4115102802df98b2b0db3cce5cb9b92572633a1197c77b7553e5203f284a5b3", size = 24547340, upload-time = "2026-02-23T00:19:12.024Z" }, + { url = "https://files.pythonhosted.org/packages/76/27/07ee1b57b65e92645f219b37148a7e7928b82e2b5dbeccecb4dff7c64f0b/scipy-1.17.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c", size = 31590199, upload-time = "2026-02-23T00:19:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ae/db19f8ab842e9b724bf5dbb7db29302a91f1e55bc4d04b1025d6d605a2c5/scipy-1.17.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f", size = 28154001, upload-time = "2026-02-23T00:19:22.241Z" }, + { url = "https://files.pythonhosted.org/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d", size = 20325719, upload-time = "2026-02-23T00:19:26.329Z" }, + { url = "https://files.pythonhosted.org/packages/b2/83/15087d945e0e4d48ce2377498abf5ad171ae013232ae31d06f336e64c999/scipy-1.17.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b", size = 22683595, upload-time = "2026-02-23T00:19:30.304Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e0/e58fbde4a1a594c8be8114eb4aac1a55bcd6587047efc18a61eb1f5c0d30/scipy-1.17.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6", size = 32896429, upload-time = "2026-02-23T00:19:35.536Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464", size = 35203952, upload-time = "2026-02-23T00:19:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a5/9afd17de24f657fdfe4df9a3f1ea049b39aef7c06000c13db1530d81ccca/scipy-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950", size = 34979063, upload-time = "2026-02-23T00:19:47.547Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/88b1d2384b424bf7c924f2038c1c409f8d88bb2a8d49d097861dd64a57b2/scipy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369", size = 37598449, upload-time = "2026-02-23T00:19:53.238Z" }, + { url = "https://files.pythonhosted.org/packages/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448", size = 36510943, upload-time = "2026-02-23T00:20:50.89Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fd/3be73c564e2a01e690e19cc618811540ba5354c67c8680dce3281123fb79/scipy-1.17.1-cp313-cp313-win_arm64.whl", hash = "sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87", size = 24545621, upload-time = "2026-02-23T00:20:55.871Z" }, + { url = "https://files.pythonhosted.org/packages/6f/6b/17787db8b8114933a66f9dcc479a8272e4b4da75fe03b0c282f7b0ade8cd/scipy-1.17.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a", size = 31936708, upload-time = "2026-02-23T00:19:58.694Z" }, + { url = "https://files.pythonhosted.org/packages/38/2e/524405c2b6392765ab1e2b722a41d5da33dc5c7b7278184a8ad29b6cb206/scipy-1.17.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0", size = 28570135, upload-time = "2026-02-23T00:20:03.934Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c3/5bd7199f4ea8556c0c8e39f04ccb014ac37d1468e6cfa6a95c6b3562b76e/scipy-1.17.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce", size = 20741977, upload-time = "2026-02-23T00:20:07.935Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b8/8ccd9b766ad14c78386599708eb745f6b44f08400a5fd0ade7cf89b6fc93/scipy-1.17.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6", size = 23029601, upload-time = "2026-02-23T00:20:12.161Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a0/3cb6f4d2fb3e17428ad2880333cac878909ad1a89f678527b5328b93c1d4/scipy-1.17.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e", size = 33019667, upload-time = "2026-02-23T00:20:17.208Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c3/2d834a5ac7bf3a0c806ad1508efc02dda3c8c61472a56132d7894c312dea/scipy-1.17.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475", size = 35264159, upload-time = "2026-02-23T00:20:23.087Z" }, + { url = "https://files.pythonhosted.org/packages/4d/77/d3ed4becfdbd217c52062fafe35a72388d1bd82c2d0ba5ca19d6fcc93e11/scipy-1.17.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50", size = 35102771, upload-time = "2026-02-23T00:20:28.636Z" }, + { url = "https://files.pythonhosted.org/packages/bd/12/d19da97efde68ca1ee5538bb261d5d2c062f0c055575128f11a2730e3ac1/scipy-1.17.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca", size = 37665910, upload-time = "2026-02-23T00:20:34.743Z" }, + { url = "https://files.pythonhosted.org/packages/06/1c/1172a88d507a4baaf72c5a09bb6c018fe2ae0ab622e5830b703a46cc9e44/scipy-1.17.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c", size = 36562980, upload-time = "2026-02-23T00:20:40.575Z" }, + { url = "https://files.pythonhosted.org/packages/70/b0/eb757336e5a76dfa7911f63252e3b7d1de00935d7705cf772db5b45ec238/scipy-1.17.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49", size = 24856543, upload-time = "2026-02-23T00:20:45.313Z" }, +] + +[[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, 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, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "snowballstemmer" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/75/a7/9810d872919697c9d01295633f5d574fb416d47e535f258272ca1f01f447/snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895", size = 105575, upload-time = "2025-05-09T16:34:51.843Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274, upload-time = "2025-05-09T16:34:50.371Z" }, +] + +[[package]] +name = "sphinx" +version = "7.4.7" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "alabaster", version = "0.7.16", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "babel", marker = "python_full_version < '3.10'" }, + { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, + { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "imagesize", version = "1.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "jinja2", marker = "python_full_version < '3.10'" }, + { name = "packaging", marker = "python_full_version < '3.10'" }, + { name = "pygments", marker = "python_full_version < '3.10'" }, + { name = "requests", marker = "python_full_version < '3.10'" }, + { name = "snowballstemmer", marker = "python_full_version < '3.10'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version < '3.10'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version < '3.10'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version < '3.10'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version < '3.10'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version < '3.10'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version < '3.10'" }, + { name = "tomli", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/be/50e50cb4f2eff47df05673d361095cafd95521d2a22521b920c67a372dcb/sphinx-7.4.7.tar.gz", hash = "sha256:242f92a7ea7e6c5b406fdc2615413890ba9f699114a9c09192d7dfead2ee9cfe", size = 8067911, upload-time = "2024-07-20T14:46:56.059Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/ef/153f6803c5d5f8917dbb7f7fcf6d34a871ede3296fa89c2c703f5f8a6c8e/sphinx-7.4.7-py3-none-any.whl", hash = "sha256:c2419e2135d11f1951cd994d6eb18a1835bd8fdd8429f9ca375dc1f3281bd239", size = 3401624, upload-time = "2024-07-20T14:46:52.142Z" }, +] + +[[package]] +name = "sphinx" +version = "8.1.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "alabaster", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "babel", marker = "python_full_version == '3.10.*'" }, + { name = "colorama", marker = "python_full_version == '3.10.*' and sys_platform == 'win32'" }, + { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "imagesize", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "jinja2", marker = "python_full_version == '3.10.*'" }, + { name = "packaging", marker = "python_full_version == '3.10.*'" }, + { name = "pygments", marker = "python_full_version == '3.10.*'" }, + { name = "requests", marker = "python_full_version == '3.10.*'" }, + { name = "snowballstemmer", marker = "python_full_version == '3.10.*'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version == '3.10.*'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version == '3.10.*'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version == '3.10.*'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version == '3.10.*'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version == '3.10.*'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version == '3.10.*'" }, + { name = "tomli", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/be0b61178fe2cdcb67e2a92fc9ebb488e3c51c4f74a36a7824c0adf23425/sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927", size = 8184611, upload-time = "2024-10-13T20:27:13.93Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/60/1ddff83a56d33aaf6f10ec8ce84b4c007d9368b21008876fceda7e7381ef/sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2", size = 3487125, upload-time = "2024-10-13T20:27:10.448Z" }, +] + +[[package]] +name = "sphinx" +version = "9.0.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "alabaster", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "babel", marker = "python_full_version == '3.11.*'" }, + { name = "colorama", marker = "python_full_version == '3.11.*' and sys_platform == 'win32'" }, + { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "imagesize", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "jinja2", marker = "python_full_version == '3.11.*'" }, + { name = "packaging", marker = "python_full_version == '3.11.*'" }, + { name = "pygments", marker = "python_full_version == '3.11.*'" }, + { name = "requests", marker = "python_full_version == '3.11.*'" }, + { name = "roman-numerals", marker = "python_full_version == '3.11.*'" }, + { name = "snowballstemmer", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version == '3.11.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/50/a8c6ccc36d5eacdfd7913ddccd15a9cee03ecafc5ee2bc40e1f168d85022/sphinx-9.0.4.tar.gz", hash = "sha256:594ef59d042972abbc581d8baa577404abe4e6c3b04ef61bd7fc2acbd51f3fa3", size = 8710502, upload-time = "2025-12-04T07:45:27.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/3f/4bbd76424c393caead2e1eb89777f575dee5c8653e2d4b6afd7a564f5974/sphinx-9.0.4-py3-none-any.whl", hash = "sha256:5bebc595a5e943ea248b99c13814c1c5e10b3ece718976824ffa7959ff95fffb", size = 3917713, upload-time = "2025-12-04T07:45:24.944Z" }, +] + +[[package]] +name = "sphinx" +version = "9.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version >= '3.12' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "alabaster", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "babel", marker = "python_full_version >= '3.12'" }, + { name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, + { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "imagesize", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "jinja2", marker = "python_full_version >= '3.12'" }, + { name = "packaging", marker = "python_full_version >= '3.12'" }, + { name = "pygments", marker = "python_full_version >= '3.12'" }, + { name = "requests", marker = "python_full_version >= '3.12'" }, + { name = "roman-numerals", marker = "python_full_version >= '3.12'" }, + { name = "snowballstemmer", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/bd/f08eb0f4eed5c83f1ba2a3bd18f7745a2b1525fad70660a1c00224ec468a/sphinx-9.1.0.tar.gz", hash = "sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb", size = 8718324, upload-time = "2025-12-31T15:09:27.646Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/f7/b1884cb3188ab181fc81fa00c266699dab600f927a964df02ec3d5d1916a/sphinx-9.1.0-py3-none-any.whl", hash = "sha256:c84fdd4e782504495fe4f2c0b3413d6c2bf388589bb352d439b2a3bb99991978", size = 3921742, upload-time = "2025-12-31T15:09:25.561Z" }, +] + +[[package]] +name = "sphinx-rtd-theme" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-jquery" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/84/68/a1bfbf38c0f7bccc9b10bbf76b94606f64acb1552ae394f0b8285bfaea25/sphinx_rtd_theme-3.1.0.tar.gz", hash = "sha256:b44276f2c276e909239a4f6c955aa667aaafeb78597923b1c60babc76db78e4c", size = 7620915, upload-time = "2026-01-12T16:03:31.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/c7/b5c8015d823bfda1a346adb2c634a2101d50bb75d421eb6dcb31acd25ebc/sphinx_rtd_theme-3.1.0-py2.py3-none-any.whl", hash = "sha256:1785824ae8e6632060490f67cf3a72d404a85d2d9fc26bce3619944de5682b89", size = 7655617, upload-time = "2026-01-12T16:03:28.101Z" }, +] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053, upload-time = "2024-07-29T01:09:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" }, +] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967, upload-time = "2024-07-29T01:09:23.417Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" }, +] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617, upload-time = "2024-07-29T01:09:37.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" }, +] + +[[package]] +name = "sphinxcontrib-jquery" +version = "4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/f3/aa67467e051df70a6330fe7770894b3e4f09436dea6881ae0b4f3d87cad8/sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a", size = 122331, upload-time = "2023-03-14T15:01:01.944Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/85/749bd22d1a68db7291c89e2ebca53f4306c3f205853cf31e9de279034c3c/sphinxcontrib_jquery-4.1-py2.py3-none-any.whl", hash = "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae", size = 121104, upload-time = "2023-03-14T15:01:00.356Z" }, +] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787, upload-time = "2019-01-21T16:10:16.347Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" }, +] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165, upload-time = "2024-07-29T01:09:56.435Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" }, +] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080, upload-time = "2024-07-29T01:10:09.332Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, + { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, + { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, + { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, + { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, + { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, + { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, + { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, + { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, + { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, + { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, + { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, + { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, + { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, + { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, +] + +[[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, 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, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "tzdata" +version = "2025.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, +]