diff --git a/.env.example b/.env.example index 328fcd3..179fd68 100644 --- a/.env.example +++ b/.env.example @@ -2,3 +2,7 @@ # 註冊:https://stackapps.com/apps/oauth/register # 複製此檔為 .env 並填入 key(.env 已 gitignored) SE_API_KEY= + +# OpenAI API key — Module D LLM roles (gate / extraction / aggregator / narrative) +# Provider 走 provider-neutral seam,預設 OpenAI(ADR 0002 / PLAN ▼Q2) +OPENAI_API_KEY= diff --git a/.gitignore b/.gitignore index 3db4967..e070552 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,6 @@ data/* # OS .DS_Store + +# Brainstorming visual companion (local mockups) +.superpowers/ diff --git a/CONTEXT.md b/CONTEXT.md index 3b0ccc7..2285862 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -5,7 +5,7 @@ ## Language **Query**: -一段 developer-facing 自然語言問題,研究的測量入口。Demo 預備一組 query 作為 finding 載體。 +一段 developer-facing 自然語言問題,研究的測量入口。**是開放的實作問題(「這問題有哪些實作解法?」),不預設已命名的 X-vs-Y 選擇;「選擇」從 practice breakdown emergent(ADR 0006)。** Demo 預備一組 query 作為 finding 載體。 _Avoid_: question(保留給 SO post 那種),prompt(保留給 LLM input) **Question**: @@ -17,16 +17,16 @@ _Avoid_: query,post(太泛) _Avoid_: topic(已棄用 BERTopic 框架),cluster(保留給 practice 側) **Answer**: -SO 上一篇 answer post(body)。Raw embedding clustering 在 answer body 上 probe 已證為失敗路線。 +SO 上一篇 answer post(body)。Raw embedding clustering 在 answer body 上 probe 已證為失敗路線。Dashboard 上標為 **"original answer"**,與我們抽出的 **practice** 做明確區分(answer = SO 原文,practice = 我們抽的單位)。 _Avoid_: response, comment **Practice**: -從一篇 answer 用 LLM 抽出的結構化 normalization:`{practice: 1 sentence, conditions: short list, evidence_type: prose|code|both}`。**Answer 側的 normalization 單位**,分群在 practice 上做、不在 raw answer 上做。Step 1 of hierarchical map-reduce (ADR 0002)。 +從一篇 answer 用 LLM 抽出的結構化 normalization:`{practice: 1 sentence, conditions: short list, evidence_type: prose|code|both}`。**Answer 側的 normalization 單位**,分群在 practice 上做、不在 raw answer 上做。Step 1 of hierarchical map-reduce (ADR 0002)。**一篇 answer 可抽出多個 practice(每個是一票;ADR 0007)。practice 專指推薦的實作 action(prescription);純解釋成因(diagnosis)的答案不產 practice(ADR 0006)。** _Avoid_: stance(opinion-mining 借詞、不擬合 SO Q&A 的「推薦 action」結構,且和 project README 「community-consensus practice」標題不一致),opinion,view,claim **Practice cluster**: -在一個 query-equivalent canonical group 內,多個 answer 的 practice 句聚成的群。**Consensus shape 的呈現單位**。Discovery 用 hierarchical map-reduce (ADR 0002):Step 2 LLM aggregator + companion deterministic SBERT+HDBSCAN run,兩者 agreement 報為 method-defensibility 指標。 -_Avoid_: position cluster, answer cluster, stance cluster +在一個 query-equivalent canonical group 內,多個 answer 的 practice 句聚成的群。**Consensus shape 的呈現單位**。Discovery 用 hierarchical map-reduce (ADR 0002):Step 2 LLM aggregator + companion deterministic SBERT+HDBSCAN run,兩者 agreement 報為 method-defensibility 指標。Dashboard 上以 **"community"** 呈現(一群提供相近 practice 的 answers)—— 注意這是「做法社群」,**勿與整個 reactjs SO「社群/community」(專案標題的那個 community) 混淆**。 +_Avoid_: position cluster, answer cluster, stance cluster;UI 的 "community" 不指 SO 全體群眾 **Breakdown / Shape**: 一個 canonical group 內各 practice cluster 的 size + authority share 分布。**取代 Convergent/Divergent hard label**;finding 是「shape 長這樣」,不是「label 是 X」。 @@ -44,8 +44,9 @@ _Avoid_: reputation alone(單純 SO native reputation 不是 authority signal - A **Query** retrieves one **Query-equivalent canonical group** - A **Query-equivalent canonical group** contains many **Questions** - A **Question** has one or more **Answers** -- An **Answer** is normalized to one **Practice**(`{practice, conditions, evidence_type}`) +- An **Answer** is normalized to **one or more Practices**(每個 `{practice, conditions, evidence_type}`;ADR 0007) - **Practices** within a canonical group cluster into multiple **Practice clusters** +- 歸屬單位是 **Practice**,不是 Answer:一篇 Answer 的多個 Practice 可落在**不同 Practice cluster(community)**,故 Answer 沒有單一「所屬 cluster」 - Each **Practice cluster** has size + **Authority** share → forms the **Breakdown** ## Example dialogue @@ -61,3 +62,4 @@ _Avoid_: reputation alone(單純 SO native reputation 不是 authority signal - 早期討論「question 的聚合」混用了「BERTopic topic」與「canonical group」兩個概念 — 已釐清:BERTopic 路線棄用(ADR 0001),測量單位是 query-equivalent canonical group。 - 「Consensus」的判定方式:曾在「Convergent/Divergent hard label with 0.6 threshold」與「shape-as-finding」之間 — 已釐清:取後者(ADR 0001)。 - Answer 側的 normalization 單位曾叫 `stance` — 已改名為 `practice`(2026-05-29)。理由:`practice` 是 project 原生詞(README 標題就是 "community-consensus practice");`stance` 是 opinion-mining 借詞、預設「對固定 target claim 支持/反對」的二分結構,不擬合 SO Q&A 的「每個 answer 自行提議一個 action / approach」結構。 +- **Dashboard 目前的 authority 與 grouping 是 SO proxy,非真 B/C**:authority = SO vote score(pre-differentiator、age-confounded),grouping = SO 原生 duplicate links(非真 canonical RAG+gate)。兩者藏在 `providers/so_proxy.py` 的 provider seam 後、可無痛替換(ADR 0008);UI 帶 provenance/PRE-DIFFERENTIATOR 標記,**勿把 proxy 數字當 finding**。 diff --git a/README.md b/README.md index 4d148a2..cabf40e 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Mapping community-consensus practice landscape in framework-level Stack Overflow Stack Overflow 上「一個 question 多個 answer」的結構,本身就是 collective expert opinion 的 implicit aggregator。不同 answer 間的 practice 分布加上作者社群權威,能定量描述某社群對某類技術議題的共識狀態。 -**2026-05-29 pivot 後**(見 `docs/adr/0001`–`0005`):對 developer query 採 LLM-canonicalized retrieval + hierarchical map-reduce practice extraction + community-network-derived authority overlay,揭示 React 社群在 implementation choice 場景下的 **practice breakdown shape**(emergent convergent / divergent / shifting / authority-disputed);不再有 hard typology label,shape 從 breakdown 自然 emergent。Demo = 6–8 stratified-selected developer queries。在資料覆蓋充足的 query 上做 per-query trajectory case study。 +**2026-05-29 pivot 後**(見 `docs/adr/0001`–`0007`):對 developer query 採 LLM-canonicalized retrieval + hierarchical map-reduce practice extraction + community-network-derived authority overlay,揭示 React 社群在 implementation choice 場景下的 **practice breakdown shape**(emergent convergent / divergent / shifting / authority-disputed);不再有 hard typology label,shape 從 breakdown 自然 emergent。Demo = 6–8 stratified-selected developer queries。在資料覆蓋充足的 query 上做 per-query trajectory case study。 完整研究設計見: - **`docs/spec.md`** — team-internal implementation reference(流程、演算法、DB schema、實作待辦)— **post-pivot 已對齊** ADR 0001–0005 @@ -52,7 +52,9 @@ cp .env.example .env │ │ ├── 0002-practice-clustering-hierarchical-map-reduce.md │ │ ├── 0003-authority-as-deterministic-weight-plus-narrative-overlay.md │ │ ├── 0004-default-view-beeswarm-with-overlay.md -│ │ └── 0005-demo-query-selection-stratified-hybrid.md +│ │ ├── 0005-demo-query-selection-stratified-hybrid.md +│ │ ├── 0006-query-framing-open-implementation-problem.md +│ │ └── 0007-extraction-multipractice-clustering-method.md │ └── reference/ │ └── se-api-overview.html # SE API reference ├── probe/ # 探索性檢驗 diff --git a/docs/adr/0002-practice-clustering-hierarchical-map-reduce.md b/docs/adr/0002-practice-clustering-hierarchical-map-reduce.md index 381d20d..7c46ddd 100644 --- a/docs/adr/0002-practice-clustering-hierarchical-map-reduce.md +++ b/docs/adr/0002-practice-clustering-hierarchical-map-reduce.md @@ -1,6 +1,6 @@ # 0002 — Practice clustering via hierarchical map-reduce (LLM + companion deterministic run) -**Status:** accepted (2026-05-29, terminology updated practice/stance 2026-05-29) +**Status:** accepted (2026-05-29, terminology updated practice/stance 2026-05-29). **Refined by ADR 0007 (2026-05-29):** Step-1 emits a *list* of practices per answer; companion = matched-k agglomerative (not HDBSCAN-from-scratch); `majority_vote_on_assignments` = k=3 co-association consensus. Model/provider now OpenAI GPT-5.4 family (PLAN ▼Q2), not Haiku. ## Context @@ -20,7 +20,7 @@ Independent WebSearch synthesis (2024–2026 literature) ratified the hierarchic **Primary method:** Hierarchical map-reduce on practice statements. -- **Step 1 — Practice extraction (parallel, per answer)**: LLM extracts structured normalization from each answer. Output schema: `{practice: 1 sentence, conditions: short list, evidence_type: prose|code|both}`. The 1-sentence practice is the unit; the conditions field exists to prevent minority-position erasure (e.g., "recommends X *only if* state is local"). Few-shot React-specific anchoring; explicit instruction to interpret code blocks as practice evidence; temp=0. +- **Step 1 — Practice extraction (parallel, per answer)**: LLM extracts structured normalization from each answer. Output schema: `{practice: 1 sentence, conditions: short list, evidence_type: prose|code|both}`. The 1-sentence practice is the unit; the conditions field records scope caveats the answer **explicitly states** (e.g., "recommends X *only if* state is local"). Few-shot React-specific anchoring; explicit instruction to interpret code blocks as practice evidence; temp=0. **[Refined by ADR 0007: Step-1 emits a *list* of practices per answer (one answer can vote for several approaches); `conditions` is a displayed annotation that does NOT affect cluster boundaries.]** - **Step 2 — Practice aggregator (one-shot)**: LLM aggregator over N short practice statements (≤2K tokens for N≤100; far below lost-in-the-middle danger zone). Output: cluster assignment per answer + cluster names + brief breakdown description, JSON-schema-enforced. Temp=0 + **k=3 multi-sample voting** on cluster assignments. @@ -38,7 +38,7 @@ Independent WebSearch synthesis (2024–2026 literature) ratified the hierarchic ## Consequences -- Per-query cost ≈ $0.05 (Haiku 4.5 + parallel Step 1 + single Step 2); demo 8 queries total ≈ $0.40 / ~1 min wall clock. Not a constraint. +- Per-query cost (OpenAI GPT-5.4 family, measured in Module-D probe): extraction k=1 over ~163 pooled answers ≈ $0.21 (gpt-5.4-mini); aggregator k=3 one-shot ≈ $0.04 (gpt-5.4); narrative negligible. Demo 8 queries ≈ a few $. Not a constraint. (Old ~$0.05 estimate was bound to Haiku 4.5 + N=50.) - LLM non-determinism is contained in two well-defined steps with explicit reproducibility controls (temp=0, structured output, k=3 voting). Step-1 schema (practice + conditions + evidence_type) prevents minority erasure. - Companion deterministic run buys: (a) a reproducibility floor that does not depend on LLM availability, (b) a quantitative agreement number for the methodology section, (c) protection against silent LLM drift across model versions. - Practice extraction has its own failure modes (information loss, code interpretation, domain jargon); these go in limitations with prompt-engineering mitigations recorded in implementation. diff --git a/docs/adr/0004-default-view-beeswarm-with-overlay.md b/docs/adr/0004-default-view-beeswarm-with-overlay.md index 0084ff1..d8950c9 100644 --- a/docs/adr/0004-default-view-beeswarm-with-overlay.md +++ b/docs/adr/0004-default-view-beeswarm-with-overlay.md @@ -12,7 +12,7 @@ ADR 0003 鬆綁了 cluster authority 的單一公式,把 dashboard 重新定 Default view = **beeswarm**: -- **個別 answer = 1 點**(first-class visual unit) +- **個別 answer = 1 點**(first-class visual unit)—— 注:ADR 0007 後一答可含多 practice;default view 仍以該答案的 **primary** practice 一答一點,「一答多點」viz 延後(D-5 再定) - **點水平位置** = practice cluster 分欄(cluster boundary 由 ADR 0002 決定) - **點大小** = author authority(default yearly PageRank;可切 reputation / full-period PageRank / vote score / answer length 等) - **點顏色** = year band(default;可切 accept_status / evidence_type / cluster_id 等) @@ -30,3 +30,38 @@ Default view = **beeswarm**: - **失去 of**:原 spec § 3.1 的 "tag-level landscape heatmap"(30 topic × 7 year 整面 heatmap)—— 那是 corpus-wide framing 的產物,ADR 0001 已棄 - **實作風險**:beeswarm with size / color 多 axis switching 在 Streamlit 上要 Plotly + 自訂 layout,不是 one-liner。Demo 8 query 可接受 - **可訪問性**:點大小編碼對視障使用者不友善,需要 alt text / sortable table fallback(保留 LineUp-style table 為 secondary tab,per 候選 D 的精神) + +## Amendment (2026-05-30) — D-5 interactive viz exploration + +Probe `src/module_d/probe_d/viz/` explored the default-view idiom space on real data (q54069253 +slice, 46 practices). The tentative beeswarm default is refined into a small composed system: + +- **Overview / default = clustered circle-pack**, realized as a **d3-force floating layout** with + **organic gooey/metaball cluster blobs** — the cluster boundary is an irregular shape that grows + along its member nodes, not a circle. Nodes draggable (fling); whole bubble draggable; collide + keeps nodes non-overlapping. (`viz/make_force.py` → `force.html`) +- **Single-practice temporal = click a practice → 2D continuous-time trajectory**: that cluster's + answers pin onto a real **date axis (old→new)**, beeswarm-stacked, others fade. This is the + PRIMARY analytical temporal tool. A single-practice trajectory is just a 1-D slice through the + space-time cube, so the cube is **not** needed for the core finding. (`viz/make_traj.py`; also + folded into `force.html`) +- **Time = expandable dimension, demoted from colour.** Ordered variable → ordered channel + (position / facet), never colour. Default is time-collapsed (pooled); time is revealed on demand + (per-practice trajectory, or per-year small-multiples). **This drops the original "point colour = + year band" default (§ 2.9)**; colour is freed (→ evidence_type / neutral). +- **3D Space-Time Cube = optional demo showpiece ONLY** (engagement, not a quantitative figure): + orthographic keeps cross-year sizes fair; empty gap-years shown honestly; gated on the 2019–2022 + backfill. (`viz/make_stc_cube.py`; static concept = `data/d5_stc_concept.png`) +- **Static finding figure** stays beeswarm / circle-pack PNG (paper-printable). + +Shared encoding (all idioms): **disk size ∝ log1p(vote)** (raw vote is power-law and can be +negative; size carries only ordinal "clearly bigger" — the exact authority/concentration number +goes in narrative/tooltip text); **accepted = ring**; **long-tail singletons collapse into one +labelled group** (not erased — long-tail mass is itself the divergent-shape signal). Authority here +is the **SO vote proxy — PRE-DIFFERENTIATOR and age-confounded** (older answers accrue votes); real +network authority (yearly PageRank) enters only with Module B (+ backfill for this fixture). + +Tech: prototypes use matplotlib (static PNG) + D3/Three.js (interactive HTML). The production +dashboard leans **D3 / SVG** (force + trajectory) over the originally-assumed Streamlit + Plotly, +and the architecture stays open to a JS/WebGL stack (D3 / Three.js / deck.gl) for the cube +showpiece. Layout / animation polish is deferred (tune later). diff --git a/docs/adr/0005-demo-query-selection-stratified-hybrid.md b/docs/adr/0005-demo-query-selection-stratified-hybrid.md index bdc78ed..9fb44ff 100644 --- a/docs/adr/0005-demo-query-selection-stratified-hybrid.md +++ b/docs/adr/0005-demo-query-selection-stratified-hybrid.md @@ -16,7 +16,7 @@ Candidate strategies: Adopt **stratified hybrid (a)+(c)**:pre-state criteria → 跑 candidate pool → data-driven 篩入 final N。 -1. **Persona definition (pre-stated)**: "Stuck developer facing implementation choice — has a real implementation problem, doesn't know how to implement or which of several approaches to use." 這個 persona 自然 implies 多種 approach 存在,是 community consensus measurement 的有效場景。 +1. **Persona definition (pre-stated)**: "Stuck developer facing implementation choice — has a real implementation problem, doesn't know how to implement or which of several approaches to use." 這個 persona 自然 implies 多種 approach 存在,是 community consensus measurement 的有效場景。**(Broadened by ADR 0006:query 是開放實作問題「這問題有哪些實作解法」、不預設已命名的 X-vs-Y choice;此 implementation-choice persona 為其特例。Stratification machinery 不變。)** 2. **Stratification dimensions (pre-stated)**: - **Shape stratum** (predicted breakdown shape): convergent / divergent / shifting trajectory / authority-disputed diff --git a/docs/adr/0006-query-framing-open-implementation-problem.md b/docs/adr/0006-query-framing-open-implementation-problem.md new file mode 100644 index 0000000..af2661a --- /dev/null +++ b/docs/adr/0006-query-framing-open-implementation-problem.md @@ -0,0 +1,24 @@ +# 0006 — Query framing: open implementation problem, surface community practices + +**Status:** accepted (2026-05-29) — broadens the persona in ADR 0005 §Decision.1; spec § 1.1 / § 2.10 updated to match + +## Context + +ADR 0005 fixed the demo-query persona as "stuck developer facing implementation **choice** — doesn't know which of several approaches to use." Module-D probe grilling (on the `q54069253` "useState not updating" canonical chain) showed this is too narrow: + +- The answer-rich, recurring React questions in the corpus — and the persona-real ones — are **open problems** ("why doesn't this work / how do I do X"), not pre-named A-vs-B choices. The "Redux vs Context"-style explicit choice debates are scarce in the 2023–2026 window (they sit in the pre-backfill 2019–2021 era). +- Yet such open problems DO surface multiple community approaches in their answers — the "choice" is **emergent in the breakdown**, not stated in the query. Forcing a choice framing onto the query would exclude most of the usable corpus. + +## Decision + +A **query** is an open developer-facing implementation problem — *"what are the implementation approaches for this problem?"* — NOT a pre-named choice between alternatives. The research output is the **emergent community-practice breakdown** (authority-weighted) for that problem; any "choice" is read off the breakdown, not presupposed by the query. + +- This **broadens** ADR 0005's persona (it subsumes the implementation-choice case) and keeps the rest of ADR 0005 intact: the shape / topic / temporal strata and the data-driven within-stratum selection still apply. +- Query selection still favours problems with **practice diversity** (multiple legitimate approaches); single-fix bug questions (one obvious answer) are filtered out — their breakdown is trivial. +- Downstream (ADR 0007): the consensus object is the **prescription** (recommended action). Pure-diagnosis answers (explaining *why* without proposing an action) are excluded — diagnosis convergence is correctness, not a preference-consensus signal. + +## Consequences + +- More of the 2023–2026 corpus becomes valid demo material (how-to / debugging problems with diverse fixes), reducing dependence on the 2019–2022 backfill for demonstrating *mechanics* (cross-year *trajectory* still needs backfill). +- CONTEXT.md `Query` term updated with this framing. +- The ADR 0005 selection-bias limitation persists: chosen problems are still researcher-selected, not universal. diff --git a/docs/adr/0007-extraction-multipractice-clustering-method.md b/docs/adr/0007-extraction-multipractice-clustering-method.md new file mode 100644 index 0000000..5956d0c --- /dev/null +++ b/docs/adr/0007-extraction-multipractice-clustering-method.md @@ -0,0 +1,26 @@ +# 0007 — Multi-practice extraction; clustering = LLM aggregator + matched-k companion + k=3 co-association consensus + +**Status:** accepted (2026-05-29) — refines ADR 0002 (Step-1 cardinality, companion method, voting definition). Validated by the Module-D probe (`src/module_d/probe_d/`). + +## Context + +ADR 0002 fixed hierarchical map-reduce: Step-1 one practice per answer; companion SBERT + HDBSCAN; "k=3 voting on cluster assignment, majority decides" — but left `majority_vote_on_assignments` undefined. The Module-D probe on the `q54069253` canonical chain (46 practices from 35 answers) surfaced three concrete gaps. + +## Decision + +1. **Step-1 yields a LIST of practices per answer (0..N), not one.** Rich answers genuinely propose several approaches; keeping only one undercounts community support for the others and distorts the breakdown — multi-position visibility is the point (RQ2). An **empty list** means the answer proposes no implementation practice (pure diagnosis / "me too" / thanks → excluded, per ADR 0006). The default beeswarm plots each answer at its **primary** practice; the one-answer-multiple-points visual is deferred (ADR 0004 stays tentative on this). + +2. **Companion = matched-k partitioning, not HDBSCAN-from-scratch.** Probe: `HDBSCAN(min_cluster_size=2)` on normalized, short, topically-homogeneous practice sentences **degenerates** (44/46 in one blob; ARI 0.009) — there are no density valleys. Given **k = the LLM aggregator's cluster count**, agglomerative / k-means on the *same* embeddings recovers moderate agreement (ARI ~0.38; head-only, n≥2 clusters, ~0.45). The embedding is not the bottleneck (`text-embedding-3-small` MTEB ~62 > spec's SBERT `mpnet` ~58 / `MiniLM` 56); density-from-scratch was the wrong tool. The companion borrows only `k` from the LLM (shared granularity) — the grouping decision stays embedding-only. The **production** companion should still use **local SBERT** for LLM-provider independence (ADR 0002's reproducibility-floor rationale), accepting equal-or-slightly-lower quality vs the OpenAI embedder. + +3. **`majority_vote_on_assignments` = co-association consensus.** Run the aggregator k=3; for each practice pair, count how often the runs co-cluster them; pairs co-occurring in a **majority (≥ 2/3)** are linked; **connected components** form the consensus clustering. This is label-alignment-free — immune to differing cluster counts / IDs across runs. Probe: per-run counts 17/16/16 (unstable granularity) but consensus pair-level agreement **0.99** (97% of within-cluster pairs unanimous across the 3 runs); head clusters all 1.00. + +4. **Report two agreement axes separately** (spec § 4 conflated them): + - **(i) LLM self-stability** across the k=3 runs — the co-association agreement (~0.99). A *reproducibility* metric. + - **(ii) cross-method agreement** vs the independent embedding companion — matched-k ARI (~0.38). A *method-defensibility* metric. + They answer different questions. ARI ~0.38 falls in spec § 4's pre-existing "< 0.5 → report & discuss" branch. + +## Consequences + +- ADR 0002 deltas: Step-1 "1 sentence practice" → list; HDBSCAN companion → matched-k agglomerative; undefined majority vote → co-association consensus. +- DB schema (spec § 3.2): `practice_extractions` becomes one row per (answer, practice) with a primary/rank flag, not per-answer; `practice_clusters.voting_agreement` = co-association agreement. +- Numbers above are from a **single-chain probe** (`q54069253`, 46 practices); full-scale (163 pooled answers) and other queries are pending. The qualitative findings (HDBSCAN degenerates; co-association stable; two distinct axes) are expected to generalise; exact ARI will vary. diff --git a/docs/adr/0008-module-d-bc-data-contract-provider-seams.md b/docs/adr/0008-module-d-bc-data-contract-provider-seams.md new file mode 100644 index 0000000..5619c63 --- /dev/null +++ b/docs/adr/0008-module-d-bc-data-contract-provider-seams.md @@ -0,0 +1,43 @@ +# 0008 — Module D ↔ B/C data contract: provider seams + normalized Breakdown + +**Status:** accepted (2026-06-01) — realizes the PLAN "對外接口" for Module D. Validated by the D-5 dashboard on the `q54069253` slice (46 practices / 8 communities / 28 answers) with zero API/DB calls at runtime. + +## Context + +Module D (answer-side practice breakdown + authority overlay + dashboard) depends on two other teams' outputs that do not exist yet (PLAN § 對外接口): + +- **Module B** — network authority (yearly PageRank + multi-source author authority lookup). +- **Module C** — `query → canonical_group` (which SO questions/answers form one measurement unit). + +D still needed real data to validate the D-5 dashboard idioms now. Two bad options were on the table: (a) block D until B/C ship, or (b) wire the dashboard directly to stand-in signals — SO vote score as authority, SO native duplicate links as the grouping — and let those proxy assumptions soak into the dashboard code. Both are wrong, because the proxies are **explicitly throwaway**: vote-as-authority is pre-differentiator and age-confounded (older answers accrue votes; the differentiator only exists once *network* authority enters the weighting — ADR 0003), and SO duplicate links are not the real canonical RAG+gate grouping. They must be swappable for the real providers without touching the dashboard. + +## Decision + +Decouple D from B/C behind two **provider seams**, mirroring the provider-neutral `llm_call` seam already adopted for the LLM tier (PLAN ▼Q2). + +1. **Ports** (`ports.py`) — two `Protocol`s are the only contact surface with B/C: + - `AuthorityProvider.score(answer) -> float` (Module B) + - `CanonicalGroupProvider.fetch(group_id) -> CanonicalGroup` (Module C) + +2. **Normalized contract** (`contract.py`) — the dashboard reads *only* a `Breakdown`: + - `authority` is a **first-class per-point field**. Both the circle-pack geometry (`r`) and the dashboard's node size read this one field — there is no second, divergent size formula. (The earlier probe re-derived `log(vote)` in JS *and* in the packer; that split is gone.) + - **provenance** (`authority_source` / `group_source`) travels with the data, so the UI can honestly badge "proxy" and a swap is visible. + - per-answer detail cards (body + author) for the click-to-detail panel. + +3. **Proxies are quarantined** (`providers/so_proxy.py`) — `VoteAuthorityProxy` and `DuplicateChainGroups` are the *only* place the proxy assumptions live; nothing else in Module D imports them. `breakdown.py` is the single integration point that joins D's own offline outputs (extraction + clustering) with B (authority) and C (group), then lays it out via `layout.py`. + +**Swap = replace one provider + re-run `dashboard/build`.** `contract` / `layout` / `render` are untouched. + +## Consequences + +- **D is unblocked on B/C.** The full D-5 dashboard idiom (force-floating organic bubbles · click-to-detail · community-swimlane timeline) was validated on real practice data with no runtime API/DB dependency — the committed `viz_data.json` lets the generators run standalone. +- **Swap blast radius is confined** to `providers/so_proxy.py` (+ one build re-run). The dashboard never learns where authority/grouping came from beyond the provenance string — the same blast-radius discipline as the LLM seam. +- **Honesty burden.** The proxy numbers are *wrong on purpose* (vote ≠ network authority; SO dup links ≠ canonical grouping). Guarded by the provenance fields + PRE-DIFFERENTIATOR labels in the UI, but a reader must not mistake proxy values for findings. +- **Known proxy artifact:** because one answer yields multiple practices that can land in different communities (ADR 0007), a community's `Σ votes` double-counts an answer across communities. Acceptable for the proxy; revisit when real per-author network authority replaces the per-answer vote sum. +- **Not done here:** Module B real authority (yearly PageRank + the 2019–2022 backfill needed to cover this fixture's early authors) and Module C real grouping. This ADR fixes only the *seam*; the providers behind it stay proxies until those land. + +## Relationships to other ADRs + +- **0003** (authority as deterministic post-weight, not boundary) — the proxy occupies the same slot the real network authority will; swapping it does not move cluster boundaries. +- **0004** (default-view idiom + amendment) — the contract is what that dashboard consumes. +- **0007** (multi-practice extraction) — the reason `Σ votes` double-counts; the contract carries one point per practice, deduping answers only for the detail cards. diff --git a/docs/spec.md b/docs/spec.md index ccc2284..51552d4 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -3,7 +3,7 @@ > **用途:** team-internal implementation reference — 給所有開發者看的設計依據,包含所有設計決策及其理由。 > **對應:** `../deliverables/proposal.md`(對外 proposal、給老師看的精簡版;2026-05-29 pivot 後 proposal 也待改) > **與 Framing A 的關係:** 並列存在,最終擇一。Framing A 的 spec 在 `../sentiment-divergence/spec.md`。 -> **Binding decisions:** `docs/adr/0001` – `docs/adr/0005`(pivot 後的鎖定設計,本 spec 是它們的整本化展開) +> **Binding decisions:** `docs/adr/0001` – `docs/adr/0007`(pivot 後的鎖定設計,本 spec 是它們的整本化展開;0006/0007 為 Module-D probe 後的 refinement) > **Last updated:** 2026-05-29(post-pivot rewrite) --- @@ -12,7 +12,7 @@ ### 1.1 一句話描述 -從 Stack Overflow 的 `[reactjs]` 討論中,**對 developer query 採 LLM-canonicalized retrieval + hierarchical map-reduce practice extraction + community-network-derived authority overlay,揭示 React 社群在 implementation choice 場景下的 community-consensus practice breakdown shape(emergent convergent / divergent / shifting / authority-disputed),並在資料覆蓋充足的 query 上做 per-query trajectory case study**。Demo = 6–8 stratified-selected developer queries,持 "stuck developer facing implementation choice" persona。 +從 Stack Overflow 的 `[reactjs]` 討論中,**對 developer query 採 LLM-canonicalized retrieval + hierarchical map-reduce practice extraction + community-network-derived authority overlay,揭示 React 社群在 implementation choice 場景下的 community-consensus practice breakdown shape(emergent convergent / divergent / shifting / authority-disputed),並在資料覆蓋充足的 query 上做 per-query trajectory case study**。Demo = 6–8 stratified-selected developer queries。Persona 為「開放實作問題 → 揭露社群有哪些 practice」(broadened,見 ADR 0006;原 "implementation choice" 為其特例)。 ### 1.2 研究問題 @@ -121,7 +121,7 @@ Binding ADR: **0002** #### Step 1 — Practice extraction(per answer, parallel) -- LLM 對每篇 answer 抽出 `{practice: 1 sentence, conditions: short list, evidence_type: prose|code|both}` +- LLM 對每篇 answer 抽出 **一個 practice list**(0..N,空 = 無實作解法;ADR 0007),每個 `{practice: 1 sentence, conditions: short list, evidence_type: prose|code|both}` - Few-shot React-specific anchoring;prompt 明說 code block 也是 practice evidence - temp=0、parallel calls(stateless per answer) - 預期 wall clock:N=50 ~5s @@ -131,13 +131,13 @@ Binding ADR: **0002** - 把 N 個 practice statement 一次塞 LLM(≤ 2K token,遠在 lost-in-the-middle danger zone 之下) - LLM 做 cluster discovery + naming - JSON schema enforced output:cluster assignment per answer + cluster name + brief description -- temp=0、**k=3 voting on cluster assignment**;majority decides,記錄 voting agreement +- temp=0、**k=3 voting on cluster assignment**;majority 以 **co-association consensus** 定(pair 共現 ≥2/3 → 連通分量,label-alignment-free;ADR 0007);voting agreement = 共識群內平均共現率(probe 實測 0.99) #### Companion deterministic run(每個 canonical group) -- 同 Step-1 practice statement → SBERT embed → HDBSCAN -- Report cluster assignment agreement(Adjusted Rand Index)vs Step-2 LLM aggregator -- 作為 method-defensibility 量化指標進論文 method 段 +- 同 Step-1 practice statement → SBERT embed → **matched-k agglomerative**(k = LLM aggregator 群數;HDBSCAN-from-scratch 對同質短句退化、已棄,ADR 0007) +- Report cluster assignment agreement(Adjusted Rand Index)vs Step-2 LLM aggregator(probe 實測 ~0.38) +- 作為 **cross-method** defensibility 指標進論文(與 k=3 self-stability 0.99 是不同軸,分開報;ADR 0007 §4) #### 為何不選別的路線 @@ -384,6 +384,8 @@ CREATE TABLE canonical_group ( PRIMARY KEY (query_id, question_id) ); +-- ADR 0007: 一答可多 practice → 實際改為每 (answer, practice) 一列(加 practice_rank,PK 改 autoincrement id)。 +-- 下方 DDL 為 pre-0007 single-practice 版,待 implementer 依 ADR 0007 調整。 CREATE TABLE practice_extractions ( answer_id INTEGER PRIMARY KEY REFERENCES answers(id), query_id INTEGER REFERENCES queries(id), @@ -517,6 +519,8 @@ Prompt 設計細節(few-shot examples、邊緣案例判則)見 § 10。「 ### 3.6 Hierarchical map-reduce practice clustering +> **ADR 0007 refinement(post-probe):** 下方 code 的 `companion_deterministic`(HDBSCAN) 對同質短 practice 句退化(44/46 一坨),已改為 **matched-k agglomerative**(k = LLM 群數);`majority_vote_on_assignments` = **k=3 co-association consensus**(pair 共現 ≥2/3 → 連通分量,label-alignment-free)。Extraction 也改吐 **practice list**(一答多解)。參考實作見 `src/module_d/probe_d/d2_cluster.py` / `d2_consensus.py`。 + ```python from sklearn.cluster import HDBSCAN from sklearn.metrics import adjusted_rand_score @@ -626,8 +630,8 @@ Dashboard (Streamlit + Plotly) 依 § 2.9 default view 將上述資料 render | **RAG retrieval recall** | Top-K vs SO confirmed-dup subset 對齊度 | local | recall@100 ≥ 65%、recall@500 ≥ 85%(per probe 09) | | **LLM equivalence gate 對齊度** | 抽 30 (query, candidate) pair 人工 vs LLM | 人工 + LLM | agreement > 70% → 信任 LLM 全 corpus;否則退回純人工或加 voting | | **Practice extraction 品質** | 30 answer 抽樣,人工驗 practice/conditions/evidence_type | 人工 | 一致率 > 75% | -| **Step-2 aggregator 內部 reproducibility** | k=3 voting agreement metric | 內建 | majority agreement > 0.7 per cluster | -| **Companion deterministic agreement** | LLM aggregator vs HDBSCAN,Adjusted Rand Index | sklearn | ARI > 0.5 → method-defensible;若低,paper 報並討論 emergent vs deterministic 各自價值 | +| **Step-2 LLM self-stability(k=3)** | co-association agreement(共識群內平均共現率;ADR 0007)| 內建 | > 0.7 per cluster;probe 實測 **0.99**(head 全 1.00)| +| **Companion cross-method agreement** | LLM aggregator vs **matched-k agglomerative** embedding companion,Adjusted Rand Index(ADR 0007;非 HDBSCAN)| sklearn | ARI > 0.5 → strong;probe 實測 **~0.38** → 落「< 0.5 → report & discuss」。與上列 self-stability 是**不同軸** | | **RQ3 baseline comparison** | 並列 5 條件 demo per query | 人工 + 5 條件各自的 API | **不評分**、並列表 + raw output 進 appendix | | Practitioner contrast study | per query 5 條件下 final-pick answer / consensus surface 並列討論 | 人工 + LLM | 6–8 query × 5 condition × 質性比較段 | | 整體意義 | 6–8 demo query case study + cross-query patterns | 人工撰寫 | 報告含具體 case + 跨 case 觀察 | @@ -652,13 +656,13 @@ Dashboard (Streamlit + Plotly) 依 § 2.9 default view 將上述資料 render | 儲存 | SQLite | 內建 | | Sentence embedding | `sentence-transformers` (`all-mpnet-base-v2` 或更新) | >= 2.5 | | Question canonicalization | 自寫 RAG + LLM gate | LLM provider § 10 | -| Practice clustering | LLM (Step 1 + 2) + sklearn HDBSCAN (companion) | sklearn >= 1.3, HDBSCAN >= 0.8.33 | +| Practice clustering | LLM aggregator (Step 1 + 2) + sklearn matched-k agglomerative (companion; ADR 0007,HDBSCAN-from-scratch 退化已棄) | sklearn >= 1.3 | | 圖分析 | NetworkX + python-louvain | networkx >= 3.0, python-louvain >= 0.16 | -| Reproducibility(k=3 voting)| 自寫 majority vote utility | — | +| Reproducibility(k=3 voting)| co-association consensus(ADR 0007)| — | | 視覺化 | Streamlit + Plotly | streamlit >= 1.30 | | GPU | Google Colab / Kaggle | 免費版 | | Python 環境 | uv(per 使用者習慣) | latest | -| LLM API(gate / extraction / aggregator / narrative)| Claude Haiku 4.5 / Gemini 2.5 Flash / Groq Llama;provider 實作期決定(§ 10) | latest | +| LLM API(gate / extraction / aggregator / narrative)| **OpenAI GPT-5.4 family**(gate nano / extraction mini / aggregator+narrative standard);strict structured outputs;provider-neutral seam(ADR 0002 / PLAN ▼Q2) | latest | --- @@ -673,7 +677,7 @@ Dashboard (Streamlit + Plotly) 依 § 2.9 default view 將上述資料 render | Unique users | ~80k–130k | scale from tag-level + window-span probes | | SBERT inference 時間(全 corpus)| ~6–9 hr Colab GPU | 4M+ embeddings | | SQLite DB 大小 | ~6–10 GB | 含 raw + embeddings + post-pivot tables | -| **Per query pipeline LLM cost** | ~$0.05 | Step-1 N=50 parallel + Step-2 k=3 + narrative | +| **Per query pipeline LLM cost** | ~$0.25 | OpenAI GPT-5.4:extraction k=1(~163 pooled A,mini)≈$0.21 + aggregator k=3(standard)≈$0.04 + narrative;Module-D probe 實測 | | **Demo 全跑 LLM cost** | ~$5–10 | 15–20 candidate × 5 baseline 條件 | | 完整 pipeline 一次跑時間 | ~15–24 hr | 含 backfill fetch + NLP + per-query 全 pool | @@ -733,7 +737,7 @@ Dashboard (Streamlit + Plotly) 依 § 2.9 default view 將上述資料 render |----------|------|------| | **高** | **2019–2022 SO 資料 backfill** | 跑 SE API paginated fetch;30–60 min connectivity;不影響 Module B / C 設計 | | **高** | **RAG retrieval top-K + embedding model 校準** | 實作 mini-probe:K ∈ {100, 200, 500} × MiniLM vs mpnet;驗 recall vs cost | -| **高** | **LLM provider / model 選擇** | 涵蓋四角色:equivalence gate / practice extraction / Step-2 aggregator / narrative。候選 Claude Haiku 4.5、Gemini 2.5 Flash、Groq Llama;prompt 固定版本、temp=0 + k=3 voting reproducibility 保證 | +| ✅ 定 | **LLM provider / model(resolved)** | **OpenAI GPT-5.4 family**(gate nano / extract mini / aggregator+narrative standard),strict structured outputs,call site 走 provider-neutral seam(ADR 0002 / PLAN ▼Q2)。temp=0 + k=3 voting reproducibility。prompt 版本 pinning 仍待(G12)| | 中 | PageRank vs SO native multi-source overlap mini-probe | § 2.6 校準步驟,~30 min | | 中 | **Practice extraction prompt design** | Few-shot React-specific anchor 選哪 5–10 個範例;code-as-evidence 規則;conditions field 取捨;測 30 answer 對齊度 | | 中 | **Step-2 aggregator prompt design** | JSON schema 鎖法;cluster naming style 規則;k=3 disagreement resolution(majority vote 失敗時 fallback) | diff --git a/docs/superpowers/specs/2026-06-01-dashboard-editorial-ink-design.md b/docs/superpowers/specs/2026-06-01-dashboard-editorial-ink-design.md new file mode 100644 index 0000000..d02ca10 --- /dev/null +++ b/docs/superpowers/specs/2026-06-01-dashboard-editorial-ink-design.md @@ -0,0 +1,99 @@ +# Dashboard visual design — "The Editorial · Ink" + +**Date:** 2026-06-01 **Status:** approved (brainstorming) **Scope:** Module D dashboard look-and-feel (color + type + texture) **and** the answer-detail panel (practice-led redesign + SO-markdown content rendering). Adds `conditions`/`evidence_type` to the data contract; does **not** change the bubble-field idiom, encodings, or the B/C provider seams. + +## Decision + +Adopt a single, intentional visual identity for the practice-breakdown dashboard: + +> **The Editorial · Ink** — a warm-paper, journal-typeset surface where saturated, colorblind-safe data sits on a quiet ink-and-paper chrome. The memorable idea: *rigorous scientific data that feels hand-set / printed.* + +This replaces the inherited "generic dark data-viz" defaults (which were never a deliberate choice). + +### How we got here (narrowing path) + +1. Broad sweep — 4 directions: Refined Dark, Light Research-Paper, Colorblind-safe Scientific, Warm Brand. +2. User kept **B (Research-Paper)** + **C (Scientific)** → the "credibility" half of the spectrum. +3. Within B/C, 3 high-fidelity mocks (Journal / Instrument / Editorial) → user chose **Editorial**. +4. Within Editorial, 3 chrome-accent options (Ink / Plum / Pine) → user chose **Ink**. + +## Design tokens + +Implement as CSS custom properties in `:root` so a future dark variant is a one-block override. + +### Foundation +| token | value | role | +|---|---|---| +| `--paper` | `#FBF8F2` | canvas / app background (warm paper) | +| `--panel` | `#FFFFFF` | cards, panels, toggle | +| `--panel2` | `#F5F0E7` | inset stat cells, answer-body block | +| `--ink` | `#20242B` | primary text | +| `--muted` | `#6B7280` | secondary text | +| `--muted2` | `#9AA0A8` | faint mono micro-labels | +| `--line` | `#E7E0D4` | borders / separators | +| paper grain | `radial-gradient(rgba(120,100,70,.06) 1px,transparent 1px)` @ 5px | subtle texture (atmosphere, not flat) | + +### Type (three roles) +| role | family | usage | +|---|---|---| +| display | **Fraunces** upright 500–600 | community names (community card + practice cards) | +| body | **Newsreader** serif 400–500 | answer content, descriptions | +| data/label | **IBM Plex Mono** 400–600 | numbers, `COMMUNITY`/`ORIGINAL ANSWER` micro-labels (uppercase, letter-spacing .1em), axis ticks, toggle, small map labels | + +Rationale: a display serif carries the "editorial" character; mono for all data keeps figures aligned and nods to the developer/StackOverflow origin; small map labels use mono (not display) for legibility at ≤11px. Community names are **upright, not italic** — italic hurt readability on the long, sentence-style cluster names. + +### Community data palette — Paul-Tol *muted* (CVD-safe) +Applied in cluster-size order; `long-tail` is always neutral grey. +``` +#332288 #88CCEE #44AA99 #117733 #DDCC77 #CC6677 #882255 #AA4499 +long-tail → #9AA0A6 +``` +- Bubbles use `mix-blend-mode:multiply` so overlaps darken organically (the gooey idiom on a light ground). +- The two light fills (`#88CCEE`, `#DDCC77`) are low-contrast on paper; the existing paper-colored node stroke (`#FBF8F2`, 2.4px) already cuts each node out cleanly, which covers it. + +### Accent & semantic +| token | value | role | +|---|---|---| +| `--accent` | `#241A12` (warm ink-black) | toggle-active, title rule, emphasis numbers, (future) selection ring | +| `--accepted` | `#C8A24A` (gold) | accepted marker (gold ★ / ✓) in hover + detail surfaces — semantic "endorsed" | + +**Accepted is not marked in the bubble field.** It is a secondary, age-confounded signal (like vote) and must not compete with authority (= bubble size); a per-practice ring would also multiply across a multi-practice answer. It surfaces only on hover (tooltip) and in the community card / answer panel, as a gold ★ / ✓. Decided 2026-06-01 after comparing on-field options (gold ring · corner ✓ badge · center pip · fill-weight · ink etch · double keyline · letterpress emboss · deepen-shade · gold seal) — every on-field marker cluttered the organic field or over-emphasized a weak signal. The ink **selection/focus** ring stays deferred to the formal frontend. + +### Encodings (unchanged — data contract / ADR 0004) +Bubble radius = `authority` (first-class contract field, shared by layout `r` and the front-end). Bubble fill = community color. The **bubble-field** idiom (hover tooltip · click node → answer panel · click blob → community card · ⏱ timeline swimlanes · inline/push reflow) is untouched; the answer panel's **internal** layout was redesigned — see *Answer detail panel* below. + +### Honesty markup (ADR 0008) +`authority = SO vote · proxy` and `PRE-DIFFERENTIATOR` stay rendered in muted mono so the proxy caveat survives the prettier skin — proxy values must not read as findings. + +## Answer detail panel — practice-led (added 2026-06-01) + +Clicking a bubble selects a **practice**, but the panel was answer-led, burying the unit of analysis (CONTEXT.md: "歸屬單位是 Practice"). Since **hover already gives the practice quick-look**, the panel's distinct job is the **source answer**. Division: *hover = practice · click = its original answer*. + +- **Contract plumbing:** `conditions` + `evidence_type` (per practice, from extraction) now flow through `contract.py` → `breakdown.py` → `build` (previously dropped). Re-run build to populate. +- **Uniform practice cards** (no per-click highlight): community = a small colored **dot + name**; a left accent bar read as generic "AI-dashboard" and was dropped. `evidence_type` renders as **chips** `⟨⟩ code` / `¶ prose` — two chips when both kinds back it (the literal word "both" was unclear). `conditions` render as a `WHEN ·` line only when present. +- **No persistent click-highlight:** darkening the clicked card over-emphasised a redundant cue and looked heavy; orientation is a **transient flash + auto-scroll** to the clicked practice on open. +- **Answer content = SO markdown, not HTML.** `mdToHtml` renders an escape-first subset (fenced → `
`, inline code, bold, links, lists, blockquotes, paragraphs): code reads as code, prose as paragraphs. Escape-first → any raw HTML in the body is shown literally, never executed.
+
+## Accessibility
+- Paul-Tol muted is colorblind-safe across common CVD types.
+- `--ink` on `--paper` ≈ 13:1 contrast; mono micro-labels use `--muted`/`--muted2` kept ≥ ~4.5:1 on their backgrounds.
+
+## Scope & non-goals
+- **This round: light theme only.** Tokens are authored as `:root` variables so a dark variant is a later override, not a rewrite.
+- The current `render.py` restyle is a **preview** to validate the identity on real data; a more formal frontend will be built later. Keep the restyle a focused token swap — no architectural rewrite.
+
+## Mapping to `render.py`
+Single-file change: rewrite the `
+
+
+
+
+
+
+
+
+
+
+ +
+
+ + + + + +
+
+
+
+ + diff --git a/src/module_d/dashboard/render.py b/src/module_d/dashboard/render.py new file mode 100644 index 0000000..0569ba5 --- /dev/null +++ b/src/module_d/dashboard/render.py @@ -0,0 +1,529 @@ +"""Render the breakdown dashboard (force-floating organic bubbles) from the contract's +viz_data.json. The idiom is fixed (ADR 0004 amendment); this only binds the data. + +On load, the D-4 narrative plays as an overture (dimmed field, staggered reveal) and recedes to a +standfirst ribbon — rendered from meta.narrative; the whole block no-ops when that key is null. + +Two decoupled detail surfaces + a map-level timeline: + - hover a node → concise tooltip (cluster · practice · vote · year · accepted) + - click a blob / legend → COMMUNITY card (top-left): cluster stats + clickable answer list + - click a node / answer row → ANSWER panel (right): that answer's content + data; the community + card stays in place, so community and answer never share a surface + - double-click a blob → expand that cluster's TRAJECTORY (answers pin onto a real date axis, + old→new). Exit via the top-center button. + +Node size reads `authority` (the Module B seam's output), NOT raw vote, so swapping the authority +provider reshapes the dashboard with zero change here. + +Run: PYTHONPATH=src uv run python -m module_d.dashboard.render +""" +from __future__ import annotations + +import json +from pathlib import Path + +DATA = Path(__file__).resolve().parent / "data" / "viz_data.json" +OUT_HTML = Path(__file__).resolve().parent / "force.html" + +TEMPLATE = r""" + + +Practice breakdown — organic floating bubbles + trajectory + + +
+
+
+
+
+
+
+
+
+ +
+
+ + + + + +
+
+
+
+ + +""" + + +def main() -> None: + data = json.loads(DATA.read_text()) + html = TEMPLATE.replace("__DATA__", json.dumps(data)) + OUT_HTML.write_text(html) + print(f"wrote {OUT_HTML} · {len(html)} bytes · {data['meta']['n_points']} points / " + f"{len(data['answers'])} answer cards") + + +if __name__ == "__main__": + main() diff --git a/src/module_d/data/canonical_q54069253.json b/src/module_d/data/canonical_q54069253.json new file mode 100644 index 0000000..a22fa42 --- /dev/null +++ b/src/module_d/data/canonical_q54069253.json @@ -0,0 +1,389 @@ +{ + "_meta": { + "canonical_id": 54069253, + "note": "out-of-window (2019) canonical pulled probe-local; dups live in main DB", + "source": "StackExchange API 2.3, CC-BY-SA" + }, + "canonical_question": { + "question_id": 54069253, + "title": "The useState set method is not reflecting a change immediately", + "score": 908, + "answer_count": 21, + "creation_date": 1546841132, + "tags": [ + "javascript", + "reactjs", + "react-hooks" + ], + "link": "https://stackoverflow.com/questions/54069253/the-usestate-set-method-is-not-reflecting-a-change-immediately", + "body_html": "

I am trying to learn hooks and the useState method has made me confused. I am assigning an initial value to a state in the form of an array. The set method in useState is not working for me, both with and without the spread syntax.

\n

I have made an API on another PC that I am calling and fetching the data which I want to set into the state.

\n

Here is my code:

\n

\r\n
\r\n
<div id=\"root\"></div>\n\n<script type=\"text/babel\" defer>\n// import React, { useState, useEffect } from \"react\";\n// import ReactDOM from \"react-dom\";\nconst { useState, useEffect } = React; // web-browser variant\n\nconst StateSelector = () => {\n  const initialValue = [\n    {\n      category: \"\",\n      photo: \"\",\n      description: \"\",\n      id: 0,\n      name: \"\",\n      rating: 0\n    }\n  ];\n\n  const [movies, setMovies] = useState(initialValue);\n\n  useEffect(() => {\n    (async function() {\n      try {\n        // const response = await fetch(\"http://192.168.1.164:5000/movies/display\");\n        // const json = await response.json();\n        // const result = json.data.result;\n        const result = [\n          {\n            category: \"cat1\",\n            description: \"desc1\",\n            id: \"1546514491119\",\n            name: \"randomname2\",\n            photo: null,\n            rating: \"3\"\n          },\n          {\n            category: \"cat2\",\n            description: \"desc1\",\n            id: \"1546837819818\",\n            name: \"randomname1\",\n            rating: \"5\"\n          }\n        ];\n        console.log(\"result =\", result);\n        setMovies(result);\n        console.log(\"movies =\", movies);\n      } catch (e) {\n        console.error(e);\n      }\n    })();\n  }, []);\n\n  return <p>hello</p>;\n};\n\nconst rootElement = document.getElementById(\"root\");\nReactDOM.render(<StateSelector />, rootElement);\n</script>\n\n<script src=\"https://unpkg.com/@babel/standalone@7/babel.min.js\"></script>\n<script src=\"https://unpkg.com/react@17/umd/react.production.min.js\"></script>\n<script src=\"https://unpkg.com/react-dom@17/umd/react-dom.production.min.js\"></script>
\r\n
\r\n
\r\n

\n

Neither setMovies(result) nor setMovies(...result) works.

\n

I expect the result variable to be pushed into the movies array.

\n", + "body_text": "I am trying to learn hooks and the `useState` method has made me confused. I am assigning an initial value to a state in the form of an array. The set method in `useState` is not working for me, both with and without the spread syntax. \n I have made an API on another PC that I am calling and fetching the data which I want to set into the state. \n Here is my code: \n\n```\n\n// import React, { useState, useEffect } from \"react\";\n// import ReactDOM from \"react-dom\";\nconst { useState, useEffect } = React; // web-browser variant\n\nconst StateSelector = () => {\n const initialValue = [\n {\n category: \"\",\n photo: \"\",\n description: \"\",\n id: 0,\n name: \"\",\n rating: 0\n }\n ];\n\n const [movies, setMovies] = useState(initialValue);\n\n useEffect(() => {\n (async function() {\n try {\n // const response = await fetch(\"http://192.168.1.164:5000/movies/display\");\n // const json = await response.json();\n // const result = json.data.result;\n const result = [\n {\n category: \"cat1\",\n description: \"desc1\",\n id: \"1546514491119\",\n name: \"randomname2\",\n photo: null,\n rating: \"3\"\n },\n {\n category: \"cat2\",\n description: \"desc1\",\n id: \"1546837819818\",\n name: \"randomname1\",\n rating: \"5\"\n }\n ];\n console.log(\"result =\", result);\n setMovies(result);\n console.log(\"movies =\", movies);\n } catch (e) {\n console.error(e);\n }\n })();\n }, []);\n\n return hello ;\n};\n\nconst rootElement = document.getElementById(\"root\");\nReactDOM.render( , rootElement);\n\n```\n\n Neither `setMovies(result)` nor `setMovies(...result)` works. \n I expect the `result` variable to be pushed into the `movies` array." + }, + "canonical_answers": [ + { + "answer_id": 54069332, + "question_id": 54069253, + "score": 985, + "is_accepted": 1, + "creation_date": 1546841728, + "owner_user_id": 5928186, + "owner_display_name": "Shubham Khatri", + "body_html": "

Much like .setState() in class components created by extending React.Component or React.PureComponent, the state update using the updater provided by useState hook is also asynchronous, and will not be reflected immediately.

\n

Also, the main issue here is not just the asynchronous nature but the fact that state values are used by functions based on their current closures, and state updates will reflect in the next re-render by which the existing closures are not affected, but new ones are created. Now in the current state, the values within hooks are obtained by existing closures, and when a re-render happens, the closures are updated based on whether the function is recreated again or not.

\n

Even if you add a setTimeout the function, though the timeout will run after some time by which the re-render would have happened, the setTimeout will still use the value from its previous closure and not the updated one.

\n
setMovies(result);\nconsole.log(movies) // movies here will not be updated\n
\n

If you want to perform an action on state update, you need to use the useEffect hook, much like using componentDidUpdate in class components since the setter returned by useState doesn't have a callback pattern

\n
useEffect(() => {\n    // action on update of movies\n}, [movies]);\n
\n

As far as the syntax to update state is concerned, setMovies(result) will replace the previous movies value in the state with those available from the async request.

\n

However, if you want to merge the response with the previously existing values, you must use the callback syntax of state updation along with the correct use of spread syntax like

\n
setMovies(prevMovies => ([...prevMovies, ...result]));\n
\n", + "body_text": "Much like `.setState()` in class components created by extending `React.Component` or `React.PureComponent`, the state update using the updater provided by `useState` hook is also asynchronous, and will not be reflected immediately. \n Also, the main issue here is not just the asynchronous nature but the fact that state values are used by functions based on their current closures, and state updates will reflect in the next re-render by which the existing closures are not affected, but new ones are created . Now in the current state, the values within hooks are obtained by existing closures, and when a re-render happens, the closures are updated based on whether the function is recreated again or not. \n Even if you add a `setTimeout` the function, though the timeout will run after some time by which the re-render would have happened, the `setTimeout` will still use the value from its previous closure and not the updated one. \n\n```\nsetMovies(result);\nconsole.log(movies) // movies here will not be updated\n\n```\n\n If you want to perform an action on state update, you need to use the `useEffect` hook, much like using `componentDidUpdate` in class components since the setter returned by `useState` doesn't have a callback pattern \n\n```\nuseEffect(() => {\n // action on update of movies\n}, [movies]);\n\n```\n\n As far as the syntax to update state is concerned, `setMovies(result)` will replace the previous `movies` value in the state with those available from the async request. \n However, if you want to merge the response with the previously existing values, you must use the callback syntax of state updation along with the correct use of spread syntax like \n\n```\nsetMovies(prevMovies => ([...prevMovies, ...result]));\n\n```" + }, + { + "answer_id": 58877875, + "question_id": 54069253, + "score": 544, + "is_accepted": 0, + "creation_date": 1573823797, + "owner_user_id": 1176601, + "owner_display_name": "Aprillion", + "body_html": "

Additional details to the previous answer:

\n

While React's setState is asynchronous (both classes and hooks), and it's tempting to use that fact to explain the observed behavior, it is not the reason why it happens.

\n

TLDR: The reason is a closure scope around an immutable const value.

\n
\n

Solutions:

\n\n
\n

Explanation why it happens:

\n

If async was the only reason, it would be possible to await setState().

\n

However, both props and state are assumed to be unchanging during 1 render.

\n
\n

Treat this.state as if it were immutable.

\n
\n

With hooks, this assumption is enhanced by using constant values with the const keyword:

\n
const [state, setState] = useState('initial')\n
\n

The value might be different between 2 renders, but remains a constant inside the render itself and inside any closures (functions that live longer even after render is finished, e.g. useEffect, event handlers, inside any Promise or setTimeout).

\n

Consider following fake, but synchronous, React-like implementation:

\n

\r\n
\r\n
// sync implementation:\n\nlet internalState\nlet renderAgain\n\nconst setState = (updateFn) => {\n  internalState = updateFn(internalState)\n  renderAgain()\n}\n\nconst useState = (defaultState) => {\n  if (!internalState) {\n    internalState = defaultState\n  }\n  return [internalState, setState]\n}\n\nconst render = (component, node) => {\n  const {html, handleClick} = component()\n  node.innerHTML = html\n  renderAgain = () => render(component, node)\n  return handleClick\n}\n\n// test:\n\nconst MyComponent = () => {\n  const [x, setX] = useState(1)\n  console.log('in render:', x) // ✅\n  \n  const handleClick = () => {\n    setX(current => current + 1)\n    console.log('in handler/effect/Promise/setTimeout:', x) // ❌ NOT updated\n  }\n  \n  return {\n    html: `<button>${x}</button>`,\n    handleClick\n  }\n}\n\nconst triggerClick = render(MyComponent, document.getElementById('root'))\ntriggerClick()\ntriggerClick()\ntriggerClick()
\r\n
<div id=\"root\"></div>
\r\n
\r\n
\r\n

\n", + "body_text": "Additional details to the previous answer : \n While React's `setState` is asynchronous (both classes and hooks), and it's tempting to use that fact to explain the observed behavior, it is not the reason why it happens. \n TLDR: The reason is a closure scope around an immutable `const` value. \n \n Solutions: \n \n read the value in render function (not inside nested functions): \n\n```\n useEffect(() => { setMovies(result) }, [])\n console.log(movies)\n\n```\n\n add the variable into dependencies (and use the react-hooks/exhaustive-deps eslint rule): \n\n```\n useEffect(() => { setMovies(result) }, [])\n useEffect(() => { console.log(movies) }, [movies])\n\n```\n\n use a temporary variable: \n\n```\n useEffect(() => {\n const newMovies = result\n console.log(newMovies)\n setMovies(newMovies)\n }, [])\n\n```\n\n use a mutable reference (if we don't need a state and only want to remember the value - updating a ref doesn't trigger re-render): \n\n```\n const moviesRef = useRef(initialValue)\n useEffect(() => {\n moviesRef.current = result\n console.log(moviesRef.current)\n }, [])\n\n```\n\n Explanation why it happens: \n If async was the only reason, it would be possible to `await setState()`. \n However, both `props` and `state` are assumed to be unchanging during 1 render . \n \n Treat `this.state` as if it were immutable. \n \n With hooks, this assumption is enhanced by using constant values with the `const` keyword: \n\n```\nconst [state, setState] = useState('initial')\n\n```\n\n The value might be different between 2 renders, but remains a constant inside the render itself and inside any closures (functions that live longer even after render is finished, e.g. `useEffect`, event handlers, inside any Promise or setTimeout). \n Consider following fake, but synchronous , React-like implementation: \n\n```\n// sync implementation:\n\nlet internalState\nlet renderAgain\n\nconst setState = (updateFn) => {\n internalState = updateFn(internalState)\n renderAgain()\n}\n\nconst useState = (defaultState) => {\n if (!internalState) {\n internalState = defaultState\n }\n return [internalState, setState]\n}\n\nconst render = (component, node) => {\n const {html, handleClick} = component()\n node.innerHTML = html\n renderAgain = () => render(component, node)\n return handleClick\n}\n\n// test:\n\nconst MyComponent = () => {\n const [x, setX] = useState(1)\n console.log('in render:', x) // ✅\n \n const handleClick = () => {\n setX(current => current + 1)\n console.log('in handler/effect/Promise/setTimeout:', x) // ❌ NOT updated\n }\n \n return {\n html: ` ${x} `,\n handleClick\n }\n}\n\nconst triggerClick = render(MyComponent, document.getElementById('root'))\ntriggerClick()\ntriggerClick()\ntriggerClick()\n```\n\n```\n \n```" + }, + { + "answer_id": 65436417, + "question_id": 54069253, + "score": 32, + "is_accepted": 0, + "creation_date": 1608802095, + "owner_user_id": 1229624, + "owner_display_name": "Aminadav Glickshtein", + "body_html": "

I know that there are already very good answers. But I want to give another idea how to solve the same issue, and access the latest 'movie' state, using my module react-useStateRef it has 11,000+ weekly downloads.

\n

As you understand by using React state you can render the page every time the state change. But by using React ref, you can always get the latest values.

\n

So the module react-useStateRef let you use state's and ref's together. It's backward compatible with React.useState, so you can just replace the import statement

\n
const { useEffect } = React\nimport { useState } from 'react-usestateref'\n\n  const [movies, setMovies] = useState(initialValue);\n\n  useEffect(() => {\n    (async function() {\n      try {\n\n        const result = [\n          {\n            id: "1546514491119",\n          },\n        ];\n        console.log("result =", result);\n        setMovies(result);\n        console.log("movies =", movies.current); // will give you the latest results\n      } catch (e) {\n        console.error(e);\n      }\n    })();\n  }, []);\n
\n

More information:

\n\n", + "body_text": "I know that there are already very good answers. But I want to give another idea how to solve the same issue, and access the latest 'movie' state, using my module react-useStateRef it has 11,000+ weekly downloads. \n As you understand by using React state you can render the page every time the state change. But by using React ref, you can always get the latest values. \n So the module `react-useStateRef` let you use state's and ref's together. It's backward compatible with `React.useState`, so you can just replace the `import` statement \n\n```\nconst { useEffect } = React\nimport { useState } from 'react-usestateref'\n\n const [movies, setMovies] = useState(initialValue);\n\n useEffect(() => {\n (async function() {\n try {\n\n const result = [\n {\n id: \"1546514491119\",\n },\n ];\n console.log(\"result =\", result);\n setMovies(result);\n console.log(\"movies =\", movies.current); // will give you the latest results\n } catch (e) {\n console.error(e);\n }\n })();\n }, []);\n\n```\n\n More information: \n \n react-usestsateref" + }, + { + "answer_id": 75844926, + "question_id": 54069253, + "score": 20, + "is_accepted": 0, + "creation_date": 1679784058, + "owner_user_id": 13488990, + "owner_display_name": "Ahmed Sbai", + "body_html": "

Most of the answers here are about how to update a state based on its previous value, but I don't understand how that relates to the question

\n
\n

The useState set method is not reflecting a change immediately

\n
\n
\n

React 18

\n

useState is asynchronous:

\n

When an event that triggers a certain code, occurs, the code starts running, and when it finshes, react will check if there was a state update and if it is the case, only then the value of the useState hook is updated and this leads to a new render in which the new value is availabe.

\n
const [example,setExemple] = useState("")\n//...\n<button\n  onClick={() => {\n    const newValue = "new";\n    setExample(newValue);\n    console.log(example); // output "" and this is normal, because the component didn't rerenderd yet so the new value is not availabe yet\n  }}\n>\n  Update state\n</button>\n
\n

Supposing we have a scenario where we have a state which depends on another state, for example we want to make an API call based on the new value of example every time it is updated and then store the data from response in another state anotherExample.
\nto achieve so we have two ways:

\n

1. use the value of newValue:

\n
<button\n  onClick={async () => {\n    const newValue = "new";\n    const response = await axios.get(`http://127.0.0.1:5000/${newValue}`);\n    setExample(newValue);\n    setAnotherExample(response.data);\n  }}\n>\n  test\n</button>\n
\n

since you know that example will receive this value, you can create your logic based on it directly.

\n

2. trigger a useEffect to run each time example is updated by including example in its dependency array:

\n
<button\n  onClick={() => {\n    const newValue = "new";\n    setExample(newValue);\n  }}\n>\n  test\n</button>\n
\n
useEffect(() => {\n async function test(){\n  const response = await axios.get(`http://127.0.0.1:5000/${example}`);\n  setAnotherExample(response.data);\n } \n test();\n}, [example])\n
\n

so when example is updated with the event function the component rerenders, we are now in a new different render that once finished, useEffect will run because the value of example is different from what is was during the last render, and since it is a new different render, the new value of example useState hook is available here.

\n

Note: the useEffect hook will run anyway during the first mount.

\n

Which approach better?

\n\n

Conclusion:

\n

As it is mentioned above, each one has its advantage, so it depends on the use case.

\n

the second method is more recommended, however the first can be more performant in some cases, for example when you are sure the code will only run when newValue gets a new value using onChange, or maybe when you want to use some other local variables that you will no longer have access to from inside useEffect

\n", + "body_text": "Most of the answers here are about how to update a state based on its previous value, but I don't understand how that relates to the question \n \n The useState set method is not reflecting a change immediately \n\n React 18 \n useState is asynchronous: \n When an event that triggers a certain code, occurs, the code starts running, and when it finshes, react will check if there was a state update and if it is the case, only then the value of the `useState` hook is updated and this leads to a new render in which the new value is availabe. \n\n```\nconst [example,setExemple] = useState(\"\")\n//...\n {\n const newValue = \"new\";\n setExample(newValue);\n console.log(example); // output \"\" and this is normal, because the component didn't rerenderd yet so the new value is not availabe yet\n }}\n>\n Update state\n\n```\n\n Supposing we have a scenario where we have a state which depends on another state, for example we want to make an API call based on the new value of `example` every time it is updated and then store the data from response in another state `anotherExample`. \nto achieve so we have two ways: \n 1. use the value of `newValue`: \n\n```\n {\n const newValue = \"new\";\n const response = await axios.get(`http://127.0.0.1:5000/${newValue}`);\n setExample(newValue);\n setAnotherExample(response.data);\n }}\n>\n test\n\n```\n\n since you know that `example` will receive this value, you can create your logic based on it directly. \n 2. trigger a useEffect to run each time `example` is updated by including `example` in its dependency array: \n\n```\n {\n const newValue = \"new\";\n setExample(newValue);\n }}\n>\n test\n\n```\n\n```\nuseEffect(() => {\n async function test(){\n const response = await axios.get(`http://127.0.0.1:5000/${example}`);\n setAnotherExample(response.data);\n } \n test();\n}, [example])\n\n```\n\n so when `example` is updated with the event function the component rerenders, we are now in a new different render that once finished, `useEffect` will run because the value of `example` is different from what is was during the last render , and since it is a new different render, the new value of `example` useState hook is available here. \n Note: the `useEffect` hook will run anyway during the first mount. \n Which approach better? \n \n while the first method will make all the work in one render 🙂 (a better approach) \"React groups multiple state updates into a single re-render for better performance\" the second method will do it in two renders, the first when `example` is updated and the second when `anotherExample` is updated from inside `useEffect` 😕 \n \n since the component only rerenders when the new value of a `useState` hook is different from the old one, so when `newValue` is equal to `example` the component will not rerender so the `useEffect` will not run and `anotherExample` will not be updated 🙂 (a better approach) , however in the first method the API is called anyway and we don't want to do that if there is no need also if this happens `anotherExample` will be updated (`anotherExample` will receive the same data it already contains because it is the same REQUEST since `newValue` is equal to `example`) but if the response in an object or an array then, `Object.is` method (that the `useState` hook utilizezs), cannot detect if the new value is equal to the previous one, therefore, the component will rerender 😕 \n\n Conclusion: \n As it is mentioned above, each one has its advantage, so it depends on the use case. \n the second method is more recommended , however the first can be more performant in some cases, for example when you are sure the code will only run when `newValue` gets a new value using `onChange`, or maybe when you want to use some other local variables that you will no longer have access to from inside useEffect" + }, + { + "answer_id": 72697737, + "question_id": 54069253, + "score": 16, + "is_accepted": 0, + "creation_date": 1655801015, + "owner_user_id": 15408466, + "owner_display_name": "Nirjal Mahat", + "body_html": "

As other answers above have clarified the error here, which is that useState is asynchronous and you are trying to use the value just after setState. It is not updating on the console.log() part because of the asynchronous nature of setState, it lets your further code to execute, while the value updating happens on the background. Thus you are getting the previous value. When the setState is completed on the background it will update the value and you will have access to that value on the next render.

\n

If anyone is interested to understand this in detail. Here is a really good Conference talk on the topic.

\n

https://www.youtube.com/watch?v=8aGhZQkoFbQ

\n", + "body_text": "As other answers above have clarified the error here, which is that `useState` is asynchronous and you are trying to use the value just after `setState`. It is not updating on the `console.log()` part because of the asynchronous nature of `setState`, it lets your further code to execute, while the value updating happens on the background. Thus you are getting the previous value. When the `setState` is completed on the background it will update the value and you will have access to that value on the next render. \n If anyone is interested to understand this in detail. Here is a really good Conference talk on the topic. \n https://www.youtube.com/watch?v=8aGhZQkoFbQ" + }, + { + "answer_id": 63504457, + "question_id": 54069253, + "score": 15, + "is_accepted": 0, + "creation_date": 1597924226, + "owner_user_id": 9483504, + "owner_display_name": "Muhammad Imran Siddique", + "body_html": "

React's useEffect has its own state/lifecycle. It's related to mutation of state, and it will not update the state until the effect is destroyed.

\n

Just pass a single argument in parameters in useEffect params array or leave it a blank, it will work perfectly.

\n
const [data, setData] = useState();\nuseEffect(() => {\n  setData({\n    countries: ["Aus", "Bad"],\n    projects: "projects",\n    regions: "regions",\n  });\n}, []);\n
\n

Alternatively, you can try React.useRef() for instant change in the React hook.

\n
const movies = React.useRef(null);\nuseEffect(() => {\n  movies.current = "values";\n  console.log(movies.current);\n}, []);\n
\n", + "body_text": "React's useEffect has its own state/lifecycle. It's related to mutation of state, and it will not update the state until the effect is destroyed. \n Just pass a single argument in parameters in useEffect params array or leave it a blank, it will work perfectly. \n\n```\nconst [data, setData] = useState();\nuseEffect(() => {\n setData({\n countries: [\"Aus\", \"Bad\"],\n projects: \"projects\",\n regions: \"regions\",\n });\n}, []);\n\n```\n\n Alternatively, you can try React.useRef() for instant change in the React hook. \n\n```\nconst movies = React.useRef(null);\nuseEffect(() => {\n movies.current = \"values\";\n console.log(movies.current);\n}, []);\n\n```" + }, + { + "answer_id": 62413849, + "question_id": 54069253, + "score": 13, + "is_accepted": 0, + "creation_date": 1592326972, + "owner_user_id": 2912739, + "owner_display_name": "Al Joslin", + "body_html": "

I just finished a rewrite with useReducer, following the Kent C. Dodds article (reference below), which really gave me a solid result that suffers not one bit from these closure problems.

\n

See: How to use React Context effectively

\n

I condensed his readable boilerplate to my preferred level of DRYness. Reading his sandbox implementation will show you how it actually works.

\n
import React from 'react'\n\n// Reference: https://kentcdodds.com/blog/how-to-use-react-context-effectively\n\nconst ApplicationDispatch = React.createContext()\nconst ApplicationContext = React.createContext()\n\nfunction stateReducer(state, action) {\n  if (state.hasOwnProperty(action.type)) {\n    return { ...state, [action.type]: state[action.type] = action.newValue };\n  }\n  throw new Error(`Unhandled action type: ${action.type}`);\n}\n\nconst initialState = {\n  keyCode: '',\n  testCode: '',\n  testMode: false,\n  phoneNumber: '',\n  resultCode: null,\n  mobileInfo: '',\n  configName: '',\n  appConfig: {},\n};\n\nfunction DispatchProvider({ children }) {\n  const [state, dispatch] = React.useReducer(stateReducer, initialState);\n  return (\n    <ApplicationDispatch.Provider value={dispatch}>\n      <ApplicationContext.Provider value={state}>\n        {children}\n      </ApplicationContext.Provider>\n    </ApplicationDispatch.Provider>\n  )\n}\n\nfunction useDispatchable(stateName) {\n  const context = React.useContext(ApplicationContext);\n  const dispatch = React.useContext(ApplicationDispatch);\n  return [context[stateName], newValue => dispatch({ type: stateName, newValue })];\n}\n\nfunction useKeyCode() { return useDispatchable('keyCode'); }\nfunction useTestCode() { return useDispatchable('testCode'); }\nfunction useTestMode() { return useDispatchable('testMode'); }\nfunction usePhoneNumber() { return useDispatchable('phoneNumber'); }\nfunction useResultCode() { return useDispatchable('resultCode'); }\nfunction useMobileInfo() { return useDispatchable('mobileInfo'); }\nfunction useConfigName() { return useDispatchable('configName'); }\nfunction useAppConfig() { return useDispatchable('appConfig'); }\n\nexport {\n  DispatchProvider,\n  useKeyCode,\n  useTestCode,\n  useTestMode,\n  usePhoneNumber,\n  useResultCode,\n  useMobileInfo,\n  useConfigName,\n  useAppConfig,\n}\n
\n

With a usage similar to this:

\n
import { useHistory } from "react-router-dom";\n\n// https://react-bootstrap.github.io/components/alerts\nimport { Container, Row } from 'react-bootstrap';\n\nimport { useAppConfig, useKeyCode, usePhoneNumber } from '../../ApplicationDispatchProvider';\n\nimport { ControlSet } from '../../components/control-set';\nimport { keypadClass } from '../../utils/style-utils';\nimport { MaskedEntry } from '../../components/masked-entry';\nimport { Messaging } from '../../components/messaging';\nimport { SimpleKeypad, HandleKeyPress, ALT_ID } from '../../components/simple-keypad';\n\nexport const AltIdPage = () => {\n  const history = useHistory();\n  const [keyCode, setKeyCode] = useKeyCode();\n  const [phoneNumber, setPhoneNumber] = usePhoneNumber();\n  const [appConfig, setAppConfig] = useAppConfig();\n\n  const keyPressed = btn => {\n    const maxLen = appConfig.phoneNumberEntry.entryLen;\n    const newValue = HandleKeyPress(btn, phoneNumber).slice(0, maxLen);\n    setPhoneNumber(newValue);\n  }\n\n  const doSubmit = () => {\n    history.push('s');\n  }\n\n  const disableBtns = phoneNumber.length < appConfig.phoneNumberEntry.entryLen;\n\n  return (\n    <Container fluid className="text-center">\n      <Row>\n        <Messaging {...{ msgColors: appConfig.pageColors, msgLines: appConfig.entryMsgs.altIdMsgs }} />\n      </Row>\n      <Row>\n        <MaskedEntry {...{ ...appConfig.phoneNumberEntry, entryColors: appConfig.pageColors, entryLine: phoneNumber }} />\n      </Row>\n      <Row>\n        <SimpleKeypad {...{ keyboardName: ALT_ID, themeName: appConfig.keyTheme, keyPressed, styleClass: keypadClass }} />\n      </Row>\n      <Row>\n        <ControlSet {...{ btnColors: appConfig.buttonColors, disabled: disableBtns, btns: [{ text: 'Submit', click: doSubmit }] }} />\n      </Row>\n    </Container>\n  );\n};\n\nAltIdPage.propTypes = {};\n
\n

Now everything persists smoothly everywhere across all my pages.

\n", + "body_text": "I just finished a rewrite with useReducer, following the Kent C. Dodds article (reference below), which really gave me a solid result that suffers not one bit from these closure problems. \n See: How to use React Context effectively \n I condensed his readable boilerplate to my preferred level of DRYness . Reading his sandbox implementation will show you how it actually works. \n\n```\nimport React from 'react'\n\n// Reference: https://kentcdodds.com/blog/how-to-use-react-context-effectively\n\nconst ApplicationDispatch = React.createContext()\nconst ApplicationContext = React.createContext()\n\nfunction stateReducer(state, action) {\n if (state.hasOwnProperty(action.type)) {\n return { ...state, [action.type]: state[action.type] = action.newValue };\n }\n throw new Error(`Unhandled action type: ${action.type}`);\n}\n\nconst initialState = {\n keyCode: '',\n testCode: '',\n testMode: false,\n phoneNumber: '',\n resultCode: null,\n mobileInfo: '',\n configName: '',\n appConfig: {},\n};\n\nfunction DispatchProvider({ children }) {\n const [state, dispatch] = React.useReducer(stateReducer, initialState);\n return (\n\n {children}\n\n )\n}\n\nfunction useDispatchable(stateName) {\n const context = React.useContext(ApplicationContext);\n const dispatch = React.useContext(ApplicationDispatch);\n return [context[stateName], newValue => dispatch({ type: stateName, newValue })];\n}\n\nfunction useKeyCode() { return useDispatchable('keyCode'); }\nfunction useTestCode() { return useDispatchable('testCode'); }\nfunction useTestMode() { return useDispatchable('testMode'); }\nfunction usePhoneNumber() { return useDispatchable('phoneNumber'); }\nfunction useResultCode() { return useDispatchable('resultCode'); }\nfunction useMobileInfo() { return useDispatchable('mobileInfo'); }\nfunction useConfigName() { return useDispatchable('configName'); }\nfunction useAppConfig() { return useDispatchable('appConfig'); }\n\nexport {\n DispatchProvider,\n useKeyCode,\n useTestCode,\n useTestMode,\n usePhoneNumber,\n useResultCode,\n useMobileInfo,\n useConfigName,\n useAppConfig,\n}\n\n```\n\n With a usage similar to this: \n\n```\nimport { useHistory } from \"react-router-dom\";\n\n// https://react-bootstrap.github.io/components/alerts\nimport { Container, Row } from 'react-bootstrap';\n\nimport { useAppConfig, useKeyCode, usePhoneNumber } from '../../ApplicationDispatchProvider';\n\nimport { ControlSet } from '../../components/control-set';\nimport { keypadClass } from '../../utils/style-utils';\nimport { MaskedEntry } from '../../components/masked-entry';\nimport { Messaging } from '../../components/messaging';\nimport { SimpleKeypad, HandleKeyPress, ALT_ID } from '../../components/simple-keypad';\n\nexport const AltIdPage = () => {\n const history = useHistory();\n const [keyCode, setKeyCode] = useKeyCode();\n const [phoneNumber, setPhoneNumber] = usePhoneNumber();\n const [appConfig, setAppConfig] = useAppConfig();\n\n const keyPressed = btn => {\n const maxLen = appConfig.phoneNumberEntry.entryLen;\n const newValue = HandleKeyPress(btn, phoneNumber).slice(0, maxLen);\n setPhoneNumber(newValue);\n }\n\n const doSubmit = () => {\n history.push('s');\n }\n\n const disableBtns = phoneNumber.length \n\n );\n};\n\nAltIdPage.propTypes = {};\n\n```\n\n Now everything persists smoothly everywhere across all my pages." + }, + { + "answer_id": 68311173, + "question_id": 54069253, + "score": 12, + "is_accepted": 0, + "creation_date": 1625803795, + "owner_user_id": 288096, + "owner_display_name": "windmaomao", + "body_html": "

The closure is not the only reason.

\n

Based on the source code of useState (simplified below), it seems to me the value is never assigned right away.

\n

An update action is queued when you invoke setValue. And after the schedule kicks in and only when you get to the next render, this update action then is applied to that state.

\n

Which means even if we don't have a closure issue, the React version of useState is not going to give you the new value right away. The new value doesn't even exist until next render.

\n
  function useState(initialState) {\n    let hook;\n    ...\n\n    let baseState = hook.memoizedState;\n    if (hook.queue.pending) {\n      let firstUpdate = hook.queue.pending.next;\n\n      do {\n        const action = firstUpdate.action;\n        baseState = action(baseState);            // setValue HERE\n        firstUpdate = firstUpdate.next;\n      } while (firstUpdate !== hook.queue.pending);\n\n      hook.queue.pending = null;\n    }\n    hook.memoizedState = baseState;\n\n    return [baseState, dispatchAction.bind(null, hook.queue)];\n  }\n\nfunction dispatchAction(queue, action) {\n  const update = {\n    action,\n    next: null\n  };\n  if (queue.pending === null) {\n    update.next = update;\n  } else {\n    update.next = queue.pending.next;\n    queue.pending.next = update;\n  }\n  queue.pending = update;\n\n  isMount = false;\n  workInProgressHook = fiber.memoizedState;\n  schedule();\n}\n
\n

There's also an article explaining the above in the similar way, We don't know how React state hook works

\n", + "body_text": "The closure is not the only reason. \n Based on the source code of `useState` (simplified below), it seems to me the value is never assigned right away. \n An update action is queued when you invoke `setValue`. And after the schedule kicks in and only when you get to the next render, this update action then is applied to that state. \n Which means even if we don't have a closure issue, the React version of `useState` is not going to give you the new value right away. The new value doesn't even exist until next render. \n\n```\n function useState(initialState) {\n let hook;\n ...\n\n let baseState = hook.memoizedState;\n if (hook.queue.pending) {\n let firstUpdate = hook.queue.pending.next;\n\n do {\n const action = firstUpdate.action;\n baseState = action(baseState); // setValue HERE\n firstUpdate = firstUpdate.next;\n } while (firstUpdate !== hook.queue.pending);\n\n hook.queue.pending = null;\n }\n hook.memoizedState = baseState;\n\n return [baseState, dispatchAction.bind(null, hook.queue)];\n }\n\nfunction dispatchAction(queue, action) {\n const update = {\n action,\n next: null\n };\n if (queue.pending === null) {\n update.next = update;\n } else {\n update.next = queue.pending.next;\n queue.pending.next = update;\n }\n queue.pending = update;\n\n isMount = false;\n workInProgressHook = fiber.memoizedState;\n schedule();\n}\n\n```\n\n There's also an article explaining the above in the similar way, We don't know how React state hook works" + }, + { + "answer_id": 66797722, + "question_id": 54069253, + "score": 5, + "is_accepted": 0, + "creation_date": 1616668691, + "owner_user_id": 11618485, + "owner_display_name": "Abhishek Sharma", + "body_html": "

I found this to be good. Instead of defining state (approach 1) as, example,

\n
const initialValue = 1;\n
\n
const [state,setState] = useState(initialValue)\n\n
\n

Try this approach (approach 2),

\n
const [state = initialValue,setState] = useState()\n\n
\n

This resolved the rerender issue without using useEffect since we are not concerned with its internal closure approach with this case.

\n

P.S.: If you are concerned with using old state for any use case then useState with useEffect needs to be used since it will need to have that state, so approach 1 shall be used in this situation.

\n", + "body_text": "I found this to be good. Instead of defining state (approach 1) as, example, \n\n```\nconst initialValue = 1;\n\n```\n\n```\nconst [state,setState] = useState(initialValue)\n\n```\n\n Try this approach (approach 2), \n\n```\nconst [state = initialValue,setState] = useState()\n\n```\n\n This resolved the rerender issue without using useEffect since we are not concerned with its internal closure approach with this case. \n P.S.: If you are concerned with using old state for any use case then useState with useEffect needs to be used since it will need to have that state, so approach 1 shall be used in this situation." + }, + { + "answer_id": 68960240, + "question_id": 54069253, + "score": 3, + "is_accepted": 0, + "creation_date": 1630107068, + "owner_user_id": 16770666, + "owner_display_name": "amritanshu singh", + "body_html": "

If we have to update state only, then a better way can be if we use the push method to do so.

\n

Here is my code. I want to store URLs from Firebase in state.

\n
const [imageUrl, setImageUrl] = useState([]);\nconst [reload, setReload] = useState(0);\n\nuseEffect(() => {\n    if (reload === 4) {\n        downloadUrl1();\n    }\n}, [reload]);\n\n\nconst downloadUrl = async () => {\n    setImages([]);\n    try {\n        for (let i = 0; i < images.length; i++) {\n            let url = await storage().ref(urls[i].path).getDownloadURL();\n            imageUrl.push(url);\n            setImageUrl([...imageUrl]);\n\n            console.log(url, 'check', urls.length, 'length', imageUrl.length);\n        }\n    }\n    catch (e) {\n        console.log(e);\n    }\n};\n\nconst handleSubmit = async () => {\n    setReload(4);\n    await downloadUrl();\n    console.log(imageUrl);\n    console.log('post submitted');\n};\n
\n

This code works to put URLs in state as an array. This might also work for you.

\n", + "body_text": "If we have to update state only, then a better way can be if we use the push method to do so. \n Here is my code. I want to store URLs from Firebase in state. \n\n```\nconst [imageUrl, setImageUrl] = useState([]);\nconst [reload, setReload] = useState(0);\n\nuseEffect(() => {\n if (reload === 4) {\n downloadUrl1();\n }\n}, [reload]);\n\nconst downloadUrl = async () => {\n setImages([]);\n try {\n for (let i = 0; i {\n setReload(4);\n await downloadUrl();\n console.log(imageUrl);\n console.log('post submitted');\n};\n\n```\n\n This code works to put URLs in state as an array. This might also work for you." + }, + { + "answer_id": 78093905, + "question_id": 54069253, + "score": 2, + "is_accepted": 0, + "creation_date": 1709404498, + "owner_user_id": 865220, + "owner_display_name": "ishandutta2007", + "body_html": "

I would like to add to what @aprillion and @shubham-khatri suggested.

\n

@shubham-khatri suggested this:

\n
useEffect(() => {\n    // action on update of movies\n}, [movies]);\n
\n

right ?

\n

But my code was like this (myfunction being triggered on a click event)

\n
const myfunction = useEffect(async () => {\n    // action on update of movies\n}, [movies]);\n
\n

This above code was still not working. Note the difference, the difference is I have async. In case you have async the code will not wait for updated movies irrespective of [movies] , so you will have to do like this:

\n
const myfunction = useEffect(async (mymovies) => {\n    // action on passed value of mymovies\n}, []);\n
\n

and the calling code changed from onClick={myfunction} to onClick={() => myfunction(movies)}

\n", + "body_text": "I would like to add to what @aprillion and @shubham-khatri suggested. \n @shubham-khatri suggested this: \n\n```\nuseEffect(() => {\n // action on update of movies\n}, [movies]);\n\n```\n\n right ? \n But my code was like this (`myfunction` being triggered on a click event) \n\n```\nconst myfunction = useEffect(async () => {\n // action on update of movies\n}, [movies]);\n\n```\n\n This above code was still not working. Note the difference, the difference is I have `async`. In case you have `async` the code will not wait for updated `movies` irrespective of `[movies]` , so you will have to do like this: \n\n```\nconst myfunction = useEffect(async (mymovies) => {\n // action on passed value of mymovies\n}, []);\n\n```\n\n and the calling code changed from `onClick={myfunction}` to `onClick={() => myfunction(movies)}`" + }, + { + "answer_id": 78123008, + "question_id": 54069253, + "score": 2, + "is_accepted": 0, + "creation_date": 1709829450, + "owner_user_id": 4511312, + "owner_display_name": "sharath shankar", + "body_html": "

Since useState is async direct replacement of state is not a good idea. so it is better to access the previous state and set new updated value.

\n

for example:

\n
setState((prev) => [...prev, ...result]);\n//result is array of objects \n
\n", + "body_text": "Since `useState` is async direct replacement of state is not a good idea. so it is better to access the previous state and set new updated value. \n for example: \n\n```\nsetState((prev) => [...prev, ...result]);\n//result is array of objects \n\n```" + }, + { + "answer_id": 67658803, + "question_id": 54069253, + "score": 1, + "is_accepted": 0, + "creation_date": 1621766591, + "owner_user_id": 11046238, + "owner_display_name": "Dmitriy Mozgovoy", + "body_html": "

With custom hooks from my library, you can wait for the state values to update:

\n
    \n
  1. useAsyncWatcher(...values):watcherFn(peekPrevValue: boolean)=>Promise - is a promise wrapper around useEffect that can wait for updates and return a new value and possibly a previous one if the optional peekPrevValue argument is set to true.
  2. \n
\n

(Live Demo)

\n
    import React, { useState, useEffect, useCallback } from "react";\n    import { useAsyncWatcher } from "use-async-effect2";\n    \n    function TestComponent(props) {\n      const [counter, setCounter] = useState(0);\n      const [text, setText] = useState("");\n    \n      const textWatcher = useAsyncWatcher(text);\n    \n      useEffect(() => {\n        setText(`Counter: ${counter}`);\n      }, [counter]);\n    \n      const inc = useCallback(() => {\n        (async () => {\n          await new Promise((resolve) => setTimeout(resolve, 1000));\n          setCounter((counter) => counter + 1);\n          const updatedText = await textWatcher();\n          console.log(updatedText);\n        })();\n      }, []);\n    \n      return (\n        <div className="component">\n          <div className="caption">useAsyncEffect demo</div>\n          <div>{counter}</div>\n          <button onClick={inc}>Inc counter</button>\n        </div>\n      );\n    }\n    \n    export default TestComponent;\n
\n
    \n
  1. useAsyncDeepState is a deep state implementation (similar to this.setState (patchObject)) whose setter can return a promise synchronized with the internal effect. If the setter is called with no arguments, it does not change the state values, but simply subscribes to state updates. In this case, you can get the state value from anywhere inside your component, since function closures are no longer a hindrance.
  2. \n
\n

(Live Demo)

\n
import React, { useCallback, useEffect } from "react";\nimport { useAsyncDeepState } from "use-async-effect2";\n\nfunction TestComponent(props) {\n  const [state, setState] = useAsyncDeepState({\n    counter: 0,\n    computedCounter: 0\n  });\n\n  useEffect(() => {\n    setState(({ counter }) => ({\n      computedCounter: counter * 2\n    }));\n  }, [state.counter]);\n\n  const inc = useCallback(() => {\n    (async () => {\n      await new Promise((resolve) => setTimeout(resolve, 1000));\n      await setState(({ counter }) => ({ counter: counter + 1 }));\n      console.log("computedCounter=", state.computedCounter);\n    })();\n  });\n\n  return (\n    <div className="component">\n      <div className="caption">useAsyncDeepState demo</div>\n      <div>state.counter : {state.counter}</div>\n      <div>state.computedCounter : {state.computedCounter}</div>\n      <button onClick={() => inc()}>Inc counter</button>\n    </div>\n  );\n}\n
\n", + "body_text": "With custom hooks from my library, you can wait for the state values to update: \n \n `useAsyncWatcher(...values):watcherFn(peekPrevValue: boolean)=>Promise` - is a promise wrapper around useEffect that can wait for updates and return a new value and possibly a previous one if the optional `peekPrevValue` argument is set to true. \n \n ( Live Demo ) \n\n```\n import React, { useState, useEffect, useCallback } from \"react\";\n import { useAsyncWatcher } from \"use-async-effect2\";\n \n function TestComponent(props) {\n const [counter, setCounter] = useState(0);\n const [text, setText] = useState(\"\");\n \n const textWatcher = useAsyncWatcher(text);\n \n useEffect(() => {\n setText(`Counter: ${counter}`);\n }, [counter]);\n \n const inc = useCallback(() => {\n (async () => {\n await new Promise((resolve) => setTimeout(resolve, 1000));\n setCounter((counter) => counter + 1);\n const updatedText = await textWatcher();\n console.log(updatedText);\n })();\n }, []);\n \n return (\n \n useAsyncEffect demo \n {counter} \n Inc counter \n \n );\n }\n \n export default TestComponent;\n\n```\n\n `useAsyncDeepState` is a deep state implementation (similar to this.setState (patchObject)) whose setter can return a promise synchronized with the internal effect. If the setter is called with no arguments, it does not change the state values, but simply subscribes to state updates. In this case, you can get the state value from anywhere inside your component, since function closures are no longer a hindrance. \n \n ( Live Demo ) \n\n```\nimport React, { useCallback, useEffect } from \"react\";\nimport { useAsyncDeepState } from \"use-async-effect2\";\n\nfunction TestComponent(props) {\n const [state, setState] = useAsyncDeepState({\n counter: 0,\n computedCounter: 0\n });\n\n useEffect(() => {\n setState(({ counter }) => ({\n computedCounter: counter * 2\n }));\n }, [state.counter]);\n\n const inc = useCallback(() => {\n (async () => {\n await new Promise((resolve) => setTimeout(resolve, 1000));\n await setState(({ counter }) => ({ counter: counter + 1 }));\n console.log(\"computedCounter=\", state.computedCounter);\n })();\n });\n\n return (\n \n useAsyncDeepState demo \n state.counter : {state.counter} \n state.computedCounter : {state.computedCounter} \n inc()}>Inc counter \n \n );\n}\n\n```" + }, + { + "answer_id": 68216455, + "question_id": 54069253, + "score": 1, + "is_accepted": 0, + "creation_date": 1625170765, + "owner_user_id": 12754117, + "owner_display_name": "rjhcnf", + "body_html": "
var [state, setState] = useState(defaultValue)\n\nuseEffect(() => {\n   var updatedState\n   setState(currentState => {    // Do not change the state by get the updated state\n      updateState = currentState\n      return currentState\n   })\n   alert(updateState) // The current state.\n})\n
\n", + "body_text": "```\nvar [state, setState] = useState(defaultValue)\n\nuseEffect(() => {\n var updatedState\n setState(currentState => { // Do not change the state by get the updated state\n updateState = currentState\n return currentState\n })\n alert(updateState) // The current state.\n})\n\n```" + }, + { + "answer_id": 70850040, + "question_id": 54069253, + "score": 1, + "is_accepted": 0, + "creation_date": 1643119584, + "owner_user_id": 4689036, + "owner_display_name": "GMKHussain", + "body_html": "

Without any additional NPM packages

\n
//...\nconst BackendPageListing = () => {\n\n    const [ myData, setMyData] = useState( {\n        id: 1,\n        content: "abc"\n    })\n\n    const myFunction = ( x ) => {\n\n        setPagenateInfo({\n        ...myData,\n        content: x\n        })\n\n        console.log(myData) // not reflecting change immediately\n\n        let myDataNew = {...myData, content: x };\n\n        console.log(myDataNew) // Reflecting change immediately\n\n    }\n\n    return (\n        <>\n            <button onClick={()=>{ myFunction("New Content")} }>Update MyData</button>\n        </>\n    )\n
\n", + "body_text": "Without any additional NPM packages \n\n```\n//...\nconst BackendPageListing = () => {\n\n const [ myData, setMyData] = useState( {\n id: 1,\n content: \"abc\"\n })\n\n const myFunction = ( x ) => {\n\n setPagenateInfo({\n ...myData,\n content: x\n })\n\n console.log(myData) // not reflecting change immediately\n\n let myDataNew = {...myData, content: x };\n\n console.log(myDataNew) // Reflecting change immediately\n\n }\n\n return (\n <>\n { myFunction(\"New Content\")} }>Update MyData \n \n )\n\n```" + }, + { + "answer_id": 77007163, + "question_id": 54069253, + "score": 1, + "is_accepted": 0, + "creation_date": 1693391324, + "owner_user_id": 6877799, + "owner_display_name": "AmerllicA", + "body_html": "

⚠️ Functional Components

\n

I believe a super clean way would be to create a custom hook that provides the ability to pass a callback to the setter function, then it would be a 100% guarantee to do some actions exactly after the update of the state.

\n

By taking a look at this post you can understand how to make the useStateCallback hook. Defining a state by using the useStateCallback would be like the following:

\n
const [count, setCount] = useStateCallback(0);\n\nconst handleFooBar = () => {\n  setCount(c => c + 1, () => { // The callback function\n    // All actions here will be run exactly AFTER the update of the count state\n  })\n};\n
\n", + "body_text": "⚠️ Functional Components \n I believe a super clean way would be to create a custom hook that provides the ability to pass a callback to the setter function, then it would be a 100% guarantee to do some actions exactly after the update of the state. \n By taking a look at this post you can understand how to make the `useStateCallback` hook. Defining a state by using the `useStateCallback` would be like the following: \n\n```\nconst [count, setCount] = useStateCallback(0);\n\nconst handleFooBar = () => {\n setCount(c => c + 1, () => { // The callback function\n // All actions here will be run exactly AFTER the update of the count state\n })\n};\n\n```" + }, + { + "answer_id": 77355623, + "question_id": 54069253, + "score": 1, + "is_accepted": 0, + "creation_date": 1698184807, + "owner_user_id": 8285811, + "owner_display_name": "Akaisteph7", + "body_html": "

Issue

\n

My issue wasn't really trying to access the state right away after calling the set method. I was attempting to do it in a completely different function after the rerender had happened but the update still wasn't being reflected. The target function was defined in a functional component but it was being called from a class component.

\n

In my case, I ended up realizing that it was an issue caused by a stale closure. This was likely happening because class components do not use useState functionality and so the class component in my code took the function passed to it and created a copy of it or something and that copy was not using the most up-to-date reference to my variable. Actual variables passed directly to the class component still got reflected properly though.

\n

Solution

\n

Replacing the class component with a functional component solved the issue for me.

\n", + "body_text": "Issue \n My issue wasn't really trying to access the state right away after calling the set method. I was attempting to do it in a completely different function after the rerender had happened but the update still wasn't being reflected. The target function was defined in a functional component but it was being called from a class component. \n In my case, I ended up realizing that it was an issue caused by a stale closure. This was likely happening because class components do not use `useState` functionality and so the class component in my code took the function passed to it and created a copy of it or something and that copy was not using the most up-to-date reference to my variable. Actual variables passed directly to the class component still got reflected properly though. \n Solution \n Replacing the class component with a functional component solved the issue for me." + }, + { + "answer_id": 78054146, + "question_id": 54069253, + "score": 1, + "is_accepted": 0, + "creation_date": 1708811903, + "owner_user_id": 23395968, + "owner_display_name": "Edward Thomas", + "body_html": "

You can try to use the useEffect hook with a dependency array containing isOpen, the callback function will be executed every time the isOpen state changes, ensuring that console.log(isOpen) logs the updated state value. This way, you can correctly handle state updates and ensure that your UI reflects the current state of your component.

\n", + "body_text": "You can try to use the useEffect hook with a dependency array containing isOpen, the callback function will be executed every time the isOpen state changes, ensuring that console.log(isOpen) logs the updated state value. This way, you can correctly handle state updates and ensure that your UI reflects the current state of your component." + }, + { + "answer_id": 70405577, + "question_id": 54069253, + "score": -1, + "is_accepted": 0, + "creation_date": 1639848698, + "owner_user_id": 5823517, + "owner_display_name": "Tunn", + "body_html": "

I am not saying to do this, but it isn't hard to do what the OP asked without useEffect.

\n

Use a promise to resolve the new state in the body of the setter function:

\n
const getState = <T>(\n  setState: React.Dispatch<React.SetStateAction<T>>\n): Promise<T> => {\n  return new Promise((resolve) => {\n    setState((currentState: T) => {\n      resolve(currentState);\n      return currentState;\n    });\n  });\n};\n
\n

And this is how you use it (example shows the comparison between count and outOfSyncCount/syncCount in the UI rendering):

\n
const App: React.FC = () => {\n  const [count, setCount] = useState(0);\n  const [outOfSyncCount, setOutOfSyncCount] = useState(0);\n  const [syncCount, setSyncCount] = useState(0);\n\n  const handleOnClick = async () => {\n    setCount(count + 1);\n\n    // Doesn't work\n    setOutOfSyncCount(count);\n\n    // Works\n    const newCount = await getState(setCount);\n    setSyncCount(newCount);\n  };\n\n  return (\n    <>\n      <h2>Count = {count}</h2>\n      <h2>Synced count = {syncCount}</h2>\n      <h2>Out of sync count = {outOfSyncCount}</h2>\n      <button onClick={handleOnClick}>Increment</button>\n    </>\n  );\n};\n
\n", + "body_text": "I am not saying to do this, but it isn't hard to do what the OP asked without useEffect. \n Use a promise to resolve the new state in the body of the setter function: \n\n```\nconst getState = (\n setState: React.Dispatch >\n): Promise => {\n return new Promise((resolve) => {\n setState((currentState: T) => {\n resolve(currentState);\n return currentState;\n });\n });\n};\n\n```\n\n And this is how you use it (example shows the comparison between `count` and `outOfSyncCount`/`syncCount` in the UI rendering): \n\n```\nconst App: React.FC = () => {\n const [count, setCount] = useState(0);\n const [outOfSyncCount, setOutOfSyncCount] = useState(0);\n const [syncCount, setSyncCount] = useState(0);\n\n const handleOnClick = async () => {\n setCount(count + 1);\n\n // Doesn't work\n setOutOfSyncCount(count);\n\n // Works\n const newCount = await getState(setCount);\n setSyncCount(newCount);\n };\n\n return (\n <>\n Count = {count} \n Synced count = {syncCount} \n Out of sync count = {outOfSyncCount} \n Increment \n \n );\n};\n\n```" + }, + { + "answer_id": 79729529, + "question_id": 54069253, + "score": -1, + "is_accepted": 0, + "creation_date": 1754642617, + "owner_user_id": 5352831, + "owner_display_name": "Joee", + "body_html": "

Alternate solution is to use "useRef" instead.

\n
import { useRef } from "react";\nconst movies = useRef();\nconsole.log("movies =", movies.current);\nmovies.current = "Avatar"\nconsole.log("movies =", movies.current);\n
\n", + "body_text": "Alternate solution is to use \"useRef\" instead. \n\n```\nimport { useRef } from \"react\";\nconst movies = useRef();\nconsole.log(\"movies =\", movies.current);\nmovies.current = \"Avatar\"\nconsole.log(\"movies =\", movies.current);\n\n```" + }, + { + "answer_id": 61863883, + "question_id": 54069253, + "score": -2, + "is_accepted": 0, + "creation_date": 1589784562, + "owner_user_id": 6081749, + "owner_display_name": "Nikita Malyschkin", + "body_html": "
// replace\nreturn <p>hello</p>;\n// with\nreturn <p>{JSON.stringify(movies)}</p>;\n
\n\n

Now you should see, that your code actually does work. What does not work is the console.log(movies). This is because movies points to the old state. If you move your console.log(movies) outside of useEffect, right above the return, you will see the updated movies object.

\n", + "body_text": "```\n// replace\nreturn hello ;\n// with\nreturn {JSON.stringify(movies)} ;\n\n```\n\n Now you should see, that your code actually does work. What does not work is the `console.log(movies)`. This is because `movies` points to the old state . If you move your `console.log(movies)` outside of `useEffect`, right above the return, you will see the updated movies object." + } + ], + "dup_question_ids": [ + 74989069, + 74996640, + 75030746, + 75044239, + 75045264, + 75072831, + 75088729, + 75131644, + 75136452, + 75147592, + 75155855, + 75222677, + 75244564, + 75256605, + 75291612, + 75338697, + 75342404, + 75416213, + 75444757, + 75512170, + 75531897, + 75547433, + 75657586, + 75728425, + 75776917, + 75848696, + 75869689, + 75915334, + 75958379, + 76029592, + 76040963, + 76087585, + 76119269, + 76152644, + 76222312, + 76237132, + 76253959, + 76365686, + 76390964, + 76462800, + 76543820, + 76596774, + 76642498, + 76665849, + 76690624, + 76726065, + 76734757, + 76734763, + 76767716, + 76828155, + 76828704, + 76842801, + 76950232, + 76973961, + 76998888, + 77024992, + 77107721, + 77127947, + 77165106, + 77255926, + 77260874, + 77261702, + 77342087, + 77362261, + 77424024, + 77493194, + 77503064, + 77504568, + 77519965, + 77532245, + 77537893, + 77571454, + 77630358, + 77643370, + 77654388, + 77692564, + 77704899, + 77731170, + 77762753, + 77789999, + 77812854, + 77814993, + 77828292, + 77831092, + 77831101, + 77976612, + 78015251, + 78054110, + 78072677, + 78095702, + 78110624, + 78174664, + 78206817, + 78235632, + 78263221, + 78270769, + 78283173, + 78301874, + 78320396, + 78401748, + 78508511, + 78570270, + 78585527, + 78592601, + 78620146, + 78643437, + 78651409, + 78683400, + 78699694, + 78711891, + 78719638, + 78768496, + 78784444, + 78790840, + 78805394, + 78833250, + 78873089, + 78897777, + 78937544, + 78941267, + 78962438, + 78964726, + 79003771, + 79073981, + 79150399, + 79175658, + 79234402, + 79292073, + 79435733, + 79569312, + 79584479, + 79616934 + ] +} \ No newline at end of file diff --git a/src/module_d/data/clusters.json b/src/module_d/data/clusters.json new file mode 100644 index 0000000..c0dea9e --- /dev/null +++ b/src/module_d/data/clusters.json @@ -0,0 +1,230 @@ +{ + "n": 163, + "clusters": [ + { + "name": "useEffect after state change", + "members": [ + 0, + 4, + 11, + 19, + 21, + 22, + 28, + 31, + 39, + 41, + 42, + 43, + 44, + 45, + 53, + 54, + 57, + 60, + 61, + 63, + 67, + 77, + 85, + 86, + 88, + 91, + 94, + 98, + 101, + 102, + 105, + 110, + 111, + 115, + 117, + 119, + 123, + 125, + 132, + 133, + 135, + 136, + 139, + 141, + 143, + 145, + 146, + 154 + ], + "agreement": 1.0 + }, + { + "name": "Use local/event/response value immediately", + "members": [ + 5, + 10, + 23, + 29, + 40, + 46, + 52, + 62, + 82, + 83, + 84, + 92, + 93, + 95, + 100, + 106, + 108, + 109, + 112, + 118, + 120, + 121, + 127, + 149, + 151, + 160, + 161 + ], + "agreement": 0.9753 + }, + { + "name": "Functional updater from previous state", + "members": [ + 2, + 8, + 24, + 27, + 47, + 49, + 73, + 74, + 75, + 76, + 99, + 103, + 104, + 131, + 134, + 137, + 138, + 159, + 162 + ], + "agreement": 1.0 + }, + { + "name": "Read updated state on next render", + "members": [ + 3, + 12, + 35, + 36, + 38, + 48, + 51, + 55, + 72, + 107, + 140, + 142, + 144, + 148, + 158 + ], + "agreement": 0.9556 + }, + { + "name": "Avoid redundant derived state", + "members": [ + 64, + 68, + 69, + 70, + 71, + 97, + 113, + 116, + 152, + 153 + ], + "agreement": 0.9333 + }, + { + "name": "useRef for mutable latest value", + "members": [ + 6, + 14, + 34, + 59, + 81, + 90, + 126, + 128 + ], + "agreement": 1.0 + }, + { + "name": "Custom async/callback state helpers", + "members": [ + 9, + 25, + 26, + 30, + 32, + 33, + 79, + 157 + ], + "agreement": 0.8333 + }, + { + "name": "Direct set vs merge/immutability", + "members": [ + 1, + 50, + 56, + 89, + 122, + 124 + ], + "agreement": 0.8889 + }, + { + "name": "Alternative UI/data-flow specifics", + "members": [ + 37, + 65, + 66, + 87 + ], + "agreement": 0.7778 + }, + { + "name": "Reducer/context/custom state architecture", + "members": [ + 15, + 16, + 17 + ], + "agreement": 1.0 + }, + { + "name": "Initialize state properly", + "members": [ + 18, + 58 + ], + "agreement": 1.0 + }, + { + "name": "Direct set vs merge/immutability", + "members": [ + 147, + 155 + ], + "agreement": 0.6667 + } + ], + "_note": "k=3 co-association consensus (majority 2/3 of 163 practices); 12 multi-member head clusters; singletons omitted → long-tail" +} \ No newline at end of file diff --git a/src/module_d/data/extractions.json b/src/module_d/data/extractions.json new file mode 100644 index 0000000..d3899ae --- /dev/null +++ b/src/module_d/data/extractions.json @@ -0,0 +1,2446 @@ +[ + { + "answer_id": 54069332, + "question_id": 54069253, + "source": "canonical", + "score": 985, + "is_accepted": 1, + "body_len": 1734, + "practices": [ + { + "practice": "Use a useEffect with the state in its dependency array to perform an action when that state updates.", + "conditions": [], + "evidence_type": "both" + }, + { + "practice": "Set state directly with the new value, e.g. setMovies(result), to replace the previous state value.", + "conditions": [], + "evidence_type": "both" + }, + { + "practice": "Use the functional updater form with spread syntax to merge new results into the previous array state, e.g. setMovies(prevMovies => [...prevMovies, ...result]).", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 58877875, + "question_id": 54069253, + "source": "canonical", + "score": 544, + "is_accepted": 0, + "body_len": 2652, + "practices": [ + { + "practice": "Read the value in the render function instead of inside nested functions when you need the updated state.", + "conditions": [], + "evidence_type": "both" + }, + { + "practice": "Put the state variable in a useEffect dependency list and log or react to it in a separate effect.", + "conditions": [ + "use the react-hooks/exhaustive-deps eslint rule" + ], + "evidence_type": "both" + }, + { + "practice": "Compute a temporary local variable and use that value immediately instead of reading state back.", + "conditions": [], + "evidence_type": "both" + }, + { + "practice": "Use a mutable ref to remember the latest value when you do not need a re-render.", + "conditions": [ + "only if you do not need state", + "only if you only want to remember the value" + ], + "evidence_type": "both" + }, + { + "practice": "Treat state and props as immutable within a render and expect closures to keep the render's constant value.", + "conditions": [], + "evidence_type": "both" + }, + { + "practice": "Use the functional updater form inside the handler or async callback when updating from the current value.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 65436417, + "question_id": 54069253, + "source": "canonical", + "score": 32, + "is_accepted": 0, + "body_len": 1006, + "practices": [ + { + "practice": "Use the react-useStateRef / react-usestateref hook instead of React.useState so you can access the latest state through a ref while still rendering from state.", + "conditions": [ + "backward compatible with React.useState" + ], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 75844926, + "question_id": 54069253, + "source": "canonical", + "score": 20, + "is_accepted": 0, + "body_len": 3753, + "practices": [ + { + "practice": "Use the locally computed new value directly in your logic instead of reading the state back immediately after calling the setter.", + "conditions": [], + "evidence_type": "both" + }, + { + "practice": "Trigger a useEffect from the updated state by listing that state in the dependency array, and run the follow-up logic there.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 72697737, + "question_id": 54069253, + "source": "canonical", + "score": 16, + "is_accepted": 0, + "body_len": 670, + "practices": [ + { + "practice": "Read the updated state on the next render instead of immediately after calling the setter.", + "conditions": [], + "evidence_type": "prose" + } + ] + }, + { + "answer_id": 63504457, + "question_id": 54069253, + "source": "canonical", + "score": 15, + "is_accepted": 0, + "body_len": 633, + "practices": [ + { + "practice": "Use useEffect with an empty dependency array to set state after the component mounts.", + "conditions": [ + "leave the dependency array blank" + ], + "evidence_type": "both" + }, + { + "practice": "Use React.useRef() for an instant mutable value instead of waiting for state to update.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 62413849, + "question_id": 54069253, + "source": "canonical", + "score": 13, + "is_accepted": 0, + "body_len": 3236, + "practices": [ + { + "practice": "Rewrite the state management around useReducer instead of relying on useState setters for this shared application state.", + "conditions": [], + "evidence_type": "both" + }, + { + "practice": "Expose the reducer state and dispatch through React context so pages can read and update the shared state everywhere.", + "conditions": [], + "evidence_type": "both" + }, + { + "practice": "Create custom hooks that return a state slice together with a setter-like dispatch wrapper for that slice.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 68311173, + "question_id": 54069253, + "source": "canonical", + "score": 12, + "is_accepted": 0, + "body_len": 1419, + "practices": [] + }, + { + "answer_id": 66797722, + "question_id": 54069253, + "source": "canonical", + "score": 5, + "is_accepted": 0, + "body_len": 601, + "practices": [ + { + "practice": "Initialize the state variable with a default value in the destructuring pattern, using `const [state = initialValue, setState] = useState()` instead of passing the initial value to `useState(initialValue)`.", + "conditions": [], + "evidence_type": "both" + }, + { + "practice": "Use `useState` with `useEffect` when you are concerned about using old state for a use case that needs that state.", + "conditions": [ + "when concerned with using old state for any use case" + ], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 68960240, + "question_id": 54069253, + "source": "canonical", + "score": 3, + "is_accepted": 0, + "body_len": 586, + "practices": [ + { + "practice": "Use the push method to update state when you only need to update state.", + "conditions": [], + "evidence_type": "both" + }, + { + "practice": "Use a reload state value together with a useEffect that triggers downloadUrl1 when reload equals 4.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 78093905, + "question_id": 54069253, + "source": "canonical", + "score": 2, + "is_accepted": 0, + "body_len": 798, + "practices": [ + { + "practice": "Run action on update of movies inside a useEffect that depends on [movies].", + "conditions": [], + "evidence_type": "both" + }, + { + "practice": "Make the effect callback async and pass the current movies value into a separate function called from onClick as onClick={() => myfunction(movies)}.", + "conditions": [ + "in the answer's async wrapper variant" + ], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 78123008, + "question_id": 54069253, + "source": "canonical", + "score": 2, + "is_accepted": 0, + "body_len": 243, + "practices": [ + { + "practice": "Access the previous state and set a new updated value by using the functional updater form, such as appending `result` to the prior array with `setState(prev => [...prev, ...result])`.", + "conditions": [ + "when the state is an array" + ], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 67658803, + "question_id": 54069253, + "source": "canonical", + "score": 1, + "is_accepted": 0, + "body_len": 2225, + "practices": [ + { + "practice": "Use a custom hook like useAsyncWatcher to wait for state values to update and return the new value, optionally including the previous value.", + "conditions": [ + "promise wrapper around useEffect" + ], + "evidence_type": "both" + }, + { + "practice": "Use a deep state hook like useAsyncDeepState whose setter can return a promise synchronized with the internal effect.", + "conditions": [ + "setter can be called with no arguments to subscribe to state updates" + ], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 68216455, + "question_id": 54069253, + "source": "canonical", + "score": 1, + "is_accepted": 0, + "body_len": 270, + "practices": [ + { + "practice": "Capture the current state inside the functional updater callback instead of trying to read the setter result.", + "conditions": [], + "evidence_type": "code" + }, + { + "practice": "Run the alert inside a useEffect after the state update logic.", + "conditions": [], + "evidence_type": "code" + } + ] + }, + { + "answer_id": 70850040, + "question_id": 54069253, + "source": "canonical", + "score": 1, + "is_accepted": 0, + "body_len": 470, + "practices": [ + { + "practice": "Compute a new object locally with the updated field (for example, let myDataNew = {...myData, content: x}) instead of reading state back immediately.", + "conditions": [], + "evidence_type": "code" + } + ] + }, + { + "answer_id": 77007163, + "question_id": 54069253, + "source": "canonical", + "score": 1, + "is_accepted": 0, + "body_len": 645, + "practices": [ + { + "practice": "Create a custom hook that lets you pass a callback to the state setter so actions run after the state update completes.", + "conditions": [ + "functional components" + ], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 77355623, + "question_id": 54069253, + "source": "canonical", + "score": 1, + "is_accepted": 0, + "body_len": 882, + "practices": [] + }, + { + "answer_id": 78054146, + "question_id": 54069253, + "source": "canonical", + "score": 1, + "is_accepted": 0, + "body_len": 339, + "practices": [ + { + "practice": "Use a useEffect hook with the state variable in its dependency array so the callback runs after the state changes.", + "conditions": [], + "evidence_type": "prose" + } + ] + }, + { + "answer_id": 70405577, + "question_id": 54069253, + "source": "canonical", + "score": -1, + "is_accepted": 0, + "body_len": 994, + "practices": [ + { + "practice": "Wrap the state setter in a Promise that resolves from the setter callback so you can await the new state value.", + "conditions": [], + "evidence_type": "both" + }, + { + "practice": "Use the awaited value returned by that wrapper to update another piece of state after the setter runs.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 79729529, + "question_id": 54069253, + "source": "canonical", + "score": -1, + "is_accepted": 0, + "body_len": 222, + "practices": [ + { + "practice": "Use useRef instead of state when you want to read and write a value immediately through .current.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 61863883, + "question_id": 54069253, + "source": "canonical", + "score": -2, + "is_accepted": 0, + "body_len": 360, + "practices": [ + { + "practice": "Render the state value with JSON.stringify(movies) instead of returning the previous placeholder value.", + "conditions": [], + "evidence_type": "both" + }, + { + "practice": "Move console.log(movies) outside useEffect and place it right above the return so it runs in the render body and sees the updated state on the next render.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 75222714, + "question_id": 75222677, + "source": "dup", + "score": 4, + "is_accepted": 0, + "body_len": 127, + "practices": [ + { + "practice": "Create the entire array first and then set the state at the end of the loop.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 74996705, + "question_id": 74996640, + "source": "dup", + "score": 3, + "is_accepted": 0, + "body_len": 254, + "practices": [ + { + "practice": "See the new value on the next render instead of inside the event handler.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 75532130, + "question_id": 75531897, + "source": "dup", + "score": 3, + "is_accepted": 1, + "body_len": 373, + "practices": [ + { + "practice": "Add a useEffect hook so you can observe the value once it has been updated even though the setter is asynchronous.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 77520031, + "question_id": 77519965, + "source": "dup", + "score": 3, + "is_accepted": 0, + "body_len": 537, + "practices": [ + { + "practice": "Do not read the just-set state value and use the value you already have from your JSON instead.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 79074049, + "question_id": 79073981, + "source": "dup", + "score": 3, + "is_accepted": 1, + "body_len": 96, + "practices": [] + }, + { + "answer_id": 75444831, + "question_id": 75444757, + "source": "dup", + "score": 2, + "is_accepted": 0, + "body_len": 124, + "practices": [ + { + "practice": "Subscribe to the state variable using useEffect if you want to get new values of it.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 76222362, + "question_id": 76222312, + "source": "dup", + "score": 2, + "is_accepted": 1, + "body_len": 649, + "practices": [] + }, + { + "answer_id": 76462884, + "question_id": 76462800, + "source": "dup", + "score": 2, + "is_accepted": 1, + "body_len": 78, + "practices": [] + }, + { + "answer_id": 76690669, + "question_id": 76690624, + "source": "dup", + "score": 2, + "is_accepted": 1, + "body_len": 699, + "practices": [ + { + "practice": "Use a useEffect hook with squares in its dependency list so checkWinner runs after squares changes.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 78110717, + "question_id": 78110624, + "source": "dup", + "score": 2, + "is_accepted": 1, + "body_len": 248, + "practices": [ + { + "practice": "Use useEffect to respond to changes in state.", + "conditions": [], + "evidence_type": "both" + }, + { + "practice": "Use another effect to respond to those changes.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 78111410, + "question_id": 78110624, + "source": "dup", + "score": 2, + "is_accepted": 0, + "body_len": 546, + "practices": [ + { + "practice": "Use useEffect to handle the updated state instead of relying on the setter to update immediately.", + "conditions": [], + "evidence_type": "prose" + } + ] + }, + { + "answer_id": 78712264, + "question_id": 78711891, + "source": "dup", + "score": 2, + "is_accepted": 0, + "body_len": 233, + "practices": [ + { + "practice": "Remove the useEffect and fetch the new value from the API on the rerender after setting inputsData to false on the button click.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 74996715, + "question_id": 74996640, + "source": "dup", + "score": 1, + "is_accepted": 0, + "body_len": 211, + "practices": [ + { + "practice": "Use a callback function as the state updater so the value updates accordingly.", + "conditions": [], + "evidence_type": "both" + }, + { + "practice": "Use a hook in a functional component so the component renders whenever the state changes.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 75031044, + "question_id": 75030746, + "source": "dup", + "score": 1, + "is_accepted": 0, + "body_len": 558, + "practices": [ + { + "practice": "Pass a callback function to the setter when you need to use the old value.", + "conditions": [ + "only when the new value depends on the old value" + ], + "evidence_type": "prose" + }, + { + "practice": "Set a definite value directly when it does not depend on the old value.", + "conditions": [ + "only when the new value does not depend on the old value" + ], + "evidence_type": "prose" + }, + { + "practice": "Read the value on the next render, such as in the body of the component before the JSX return.", + "conditions": [], + "evidence_type": "prose" + } + ] + }, + { + "answer_id": 75088901, + "question_id": 75088729, + "source": "dup", + "score": 1, + "is_accepted": 1, + "body_len": 750, + "practices": [ + { + "practice": "Run the checks on the locally computed value when you write it to state instead of checking the stale instance afterward.", + "conditions": [], + "evidence_type": "both" + }, + { + "practice": "Move the checks into a useEffect that depends on the state constant so they run after the state changes.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 75131695, + "question_id": 75131644, + "source": "dup", + "score": 1, + "is_accepted": 1, + "body_len": 591, + "practices": [] + }, + { + "answer_id": 75342427, + "question_id": 75342404, + "source": "dup", + "score": 1, + "is_accepted": 0, + "body_len": 311, + "practices": [ + { + "practice": "Add another useEffect that listens for changes in the product state and sets isLoading to false after product has finished setting.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 75512277, + "question_id": 75512170, + "source": "dup", + "score": 1, + "is_accepted": 0, + "body_len": 751, + "practices": [ + { + "practice": "Use the updated value from the newly created function after React re-renders instead of relying on the old value captured in the current function.", + "conditions": [], + "evidence_type": "both" + }, + { + "practice": "Set the array state from the newly created array rather than from the old value captured in the current function.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 75848775, + "question_id": 75848696, + "source": "dup", + "score": 1, + "is_accepted": 0, + "body_len": 338, + "practices": [ + { + "practice": "Use useEffect to log or otherwise act on the value after it has changed.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 76391015, + "question_id": 76390964, + "source": "dup", + "score": 1, + "is_accepted": 1, + "body_len": 6, + "practices": [] + }, + { + "answer_id": 76391518, + "question_id": 76390964, + "source": "dup", + "score": 1, + "is_accepted": 0, + "body_len": 192, + "practices": [ + { + "practice": "Initialize the relevant state values to an empty string or empty array instead of leaving them undefined.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 76598617, + "question_id": 76596774, + "source": "dup", + "score": 1, + "is_accepted": 1, + "body_len": 245, + "practices": [ + { + "practice": "Use useRef instead of state if the quotes array is not shown in the UI and only needs to persist between renderings.", + "conditions": [ + "if the quotes array is not presented in the UI" + ], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 76726082, + "question_id": 76726065, + "source": "dup", + "score": 1, + "is_accepted": 1, + "body_len": 339, + "practices": [ + { + "practice": "Get the new value after the next render inside an effect.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 76828243, + "question_id": 76828155, + "source": "dup", + "score": 1, + "is_accepted": 1, + "body_len": 350, + "practices": [ + { + "practice": "Use useEffect to trigger the follow-up logic after the state has been updated.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 76950268, + "question_id": 76950232, + "source": "dup", + "score": 1, + "is_accepted": 0, + "body_len": 187, + "practices": [] + }, + { + "answer_id": 76950271, + "question_id": 76950232, + "source": "dup", + "score": 1, + "is_accepted": 0, + "body_len": 487, + "practices": [ + { + "practice": "Store the returned value from the prompt in a local variable and use that instead of trying to read the state immediately after calling the setter.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 76998916, + "question_id": 76998888, + "source": "dup", + "score": 1, + "is_accepted": 0, + "body_len": 127, + "practices": [] + }, + { + "answer_id": 77493673, + "question_id": 77493194, + "source": "dup", + "score": 1, + "is_accepted": 0, + "body_len": 800, + "practices": [ + { + "practice": "Use a useEffect hook to verify or observe the state after it has changed.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 77520036, + "question_id": 77519965, + "source": "dup", + "score": 1, + "is_accepted": 1, + "body_len": 42, + "practices": [] + }, + { + "answer_id": 77630847, + "question_id": 77630358, + "source": "dup", + "score": 1, + "is_accepted": 0, + "body_len": 473, + "practices": [ + { + "practice": "Remove the locally defined state variable and use the prop state directly instead of keeping two separate state variables.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 77654476, + "question_id": 77654388, + "source": "dup", + "score": 1, + "is_accepted": 1, + "body_len": 142, + "practices": [] + }, + { + "answer_id": 77732138, + "question_id": 77731170, + "source": "dup", + "score": 1, + "is_accepted": 0, + "body_len": 312, + "practices": [ + { + "practice": "Filter the array before setting it, instead of cloning it and then filtering and setting it.", + "conditions": [], + "evidence_type": "both" + }, + { + "practice": "Extract the filtering logic to a function and do the entire filtering in a single step.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 78072731, + "question_id": 78072677, + "source": "dup", + "score": 1, + "is_accepted": 0, + "body_len": 135, + "practices": [] + }, + { + "answer_id": 78110732, + "question_id": 78110624, + "source": "dup", + "score": 1, + "is_accepted": 0, + "body_len": 330, + "practices": [ + { + "practice": "Use a useEffect hook to listen to the state change instead of validating the value immediately after setting it.", + "conditions": [], + "evidence_type": "both" + }, + { + "practice": "Do not use the found state if it is unnecessary; use the description state directly instead.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 78110909, + "question_id": 78110624, + "source": "dup", + "score": 1, + "is_accepted": 0, + "body_len": 1008, + "practices": [ + { + "practice": "Compute the derived values on each render instead of keeping them in their own state.", + "conditions": [], + "evidence_type": "both" + }, + { + "practice": "Use useMemo to avoid unnecessary recalculations when the computation is resource heavy or the component may render for unrelated changes.", + "conditions": [ + "if the computation is resource heavy", + "if the component might render because of unrelated changes" + ], + "evidence_type": "both" + }, + { + "practice": "Memoize the component to avoid unnecessary calculations.", + "conditions": [ + "if the computation is resource heavy", + "if the component might render because of unrelated changes" + ], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 78235672, + "question_id": 78235632, + "source": "dup", + "score": 1, + "is_accepted": 0, + "body_len": 274, + "practices": [] + }, + { + "answer_id": 78263264, + "question_id": 78263221, + "source": "dup", + "score": 1, + "is_accepted": 1, + "body_len": 220, + "practices": [] + }, + { + "answer_id": 78586081, + "question_id": 78585527, + "source": "dup", + "score": 1, + "is_accepted": 0, + "body_len": 1504, + "practices": [ + { + "practice": "Use the state in handlers and effects rather than expecting the setter to make it available immediately in the same render.", + "conditions": [], + "evidence_type": "both" + }, + { + "practice": "Use updater functions when you need to create new state from previous state or when accessing the state variable itself is inconvenient.", + "conditions": [ + "for multiple updates within the same event", + "when accessing the state variable itself is inconvenient" + ], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 78592854, + "question_id": 78592601, + "source": "dup", + "score": 1, + "is_accepted": 1, + "body_len": 2657, + "practices": [] + }, + { + "answer_id": 78620196, + "question_id": 78620146, + "source": "dup", + "score": 1, + "is_accepted": 0, + "body_len": 211, + "practices": [] + }, + { + "answer_id": 78651629, + "question_id": 78651409, + "source": "dup", + "score": 1, + "is_accepted": 1, + "body_len": 576, + "practices": [ + { + "practice": "Use the functional update form of the useState setter when setting the same state variable multiple times so later updates do not overwrite earlier ones with stale values.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 78784567, + "question_id": 78784444, + "source": "dup", + "score": 1, + "is_accepted": 1, + "body_len": 515, + "practices": [] + }, + { + "answer_id": 78873102, + "question_id": 78873089, + "source": "dup", + "score": 1, + "is_accepted": 1, + "body_len": 1207, + "practices": [ + { + "practice": "Use the functional updater form when pushing to a useState array so each update receives the previous array state.", + "conditions": [], + "evidence_type": "prose" + } + ] + }, + { + "answer_id": 78873104, + "question_id": 78873089, + "source": "dup", + "score": 1, + "is_accepted": 0, + "body_len": 413, + "practices": [ + { + "practice": "Use the functional form of setState so each update to the array considers the current state at the time of the update.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 74989092, + "question_id": 74989069, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 112, + "practices": [] + }, + { + "answer_id": 74989184, + "question_id": 74989069, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 366, + "practices": [] + }, + { + "answer_id": 74989427, + "question_id": 74989069, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 177, + "practices": [] + }, + { + "answer_id": 75044305, + "question_id": 75044239, + "source": "dup", + "score": 0, + "is_accepted": 1, + "body_len": 255, + "practices": [] + }, + { + "answer_id": 75045296, + "question_id": 75045264, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 269, + "practices": [] + }, + { + "answer_id": 75045313, + "question_id": 75045264, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 143, + "practices": [ + { + "practice": "Use the useEffect hook to run code after the state update has been applied.", + "conditions": [], + "evidence_type": "prose" + } + ] + }, + { + "answer_id": 75072889, + "question_id": 75072831, + "source": "dup", + "score": 0, + "is_accepted": 1, + "body_len": 491, + "practices": [ + { + "practice": "Store the input value in state as the user changes it so the value is already available when submitting.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 75073014, + "question_id": 75072831, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 477, + "practices": [ + { + "practice": "Add a callback to the state change and execute the API in that callback after the state has been changed.", + "conditions": [], + "evidence_type": "prose" + }, + { + "practice": "Use the useCallback() hook with the state in its dependency array so the function executes on every state change.", + "conditions": [], + "evidence_type": "prose" + } + ] + }, + { + "answer_id": 75088957, + "question_id": 75088729, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 114, + "practices": [ + { + "practice": "Store the chess.js instance in a ref instead of creating a new one every time you want to make a move.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 75088967, + "question_id": 75088729, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 308, + "practices": [ + { + "practice": "Inline the makeAMove logic and use the updated game value for the rest of the event handler instead of reading state back immediately.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 75147830, + "question_id": 75147592, + "source": "dup", + "score": 0, + "is_accepted": 1, + "body_len": 151, + "practices": [ + { + "practice": "Store the urls in a local array instead of relying on the useState value in the same render cycle.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 75155917, + "question_id": 75155855, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 202, + "practices": [ + { + "practice": "Store the value in a temporary variable instead of reading state back immediately after calling the setter.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 75155926, + "question_id": 75155855, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 294, + "practices": [ + { + "practice": "Put the logging or check in a useEffect that re-runs when the state changes so you can see the updated value.", + "conditions": [ + "without a dependency array" + ], + "evidence_type": "both" + }, + { + "practice": "Use the change in randomNoteId to console.log the value and verify whether it really changed.", + "conditions": [ + "for testing purpose" + ], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 75222723, + "question_id": 75222677, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 224, + "practices": [ + { + "practice": "Cache a new output in the function and call the hook at the end instead of calling setOptions inside the loop.", + "conditions": [], + "evidence_type": "prose" + } + ] + }, + { + "answer_id": 75256763, + "question_id": 75256605, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 370, + "practices": [] + }, + { + "answer_id": 75342624, + "question_id": 75342404, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 363, + "practices": [] + }, + { + "answer_id": 75547512, + "question_id": 75547433, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 413, + "practices": [ + { + "practice": "Create a useEffect that depends on the state so you can log or run code after the state updates.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 75728557, + "question_id": 75728425, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 204, + "practices": [ + { + "practice": "Replace the state value directly instead of spreading the old prevData object.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 75777164, + "question_id": 75776917, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 418, + "practices": [ + { + "practice": "Use useRef when you need unbound state to be reflected without waiting for a re-render.", + "conditions": [ + "for unbound state", + "without re-render" + ], + "evidence_type": "prose" + } + ] + }, + { + "answer_id": 76032929, + "question_id": 76029592, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 191, + "practices": [] + }, + { + "answer_id": 76041000, + "question_id": 76040963, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 92, + "practices": [] + }, + { + "answer_id": 76237264, + "question_id": 76237132, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 195, + "practices": [ + { + "practice": "Add a useEffect to run logic after the state has been updated.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 76365779, + "question_id": 76365686, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 96, + "practices": [] + }, + { + "answer_id": 76462832, + "question_id": 76462800, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 279, + "practices": [ + { + "practice": "Use the selected value from the event directly instead of reading state back immediately after calling the setter.", + "conditions": [], + "evidence_type": "prose" + }, + { + "practice": "Log event.target.value to verify the current selection instead of logging the state variable right after setState.", + "conditions": [], + "evidence_type": "prose" + } + ] + }, + { + "answer_id": 76544358, + "question_id": 76543820, + "source": "dup", + "score": 0, + "is_accepted": 1, + "body_len": 402, + "practices": [ + { + "practice": "Use useEffect to access data that was recently updated after the state change has taken effect.", + "conditions": [], + "evidence_type": "prose" + }, + { + "practice": "If you need the value immediately after calling the setter, use the same value you are passing to setCompanies or setServices instead of reading state back.", + "conditions": [], + "evidence_type": "prose" + }, + { + "practice": "Do not await setState because setState does not return a promise.", + "conditions": [], + "evidence_type": "prose" + } + ] + }, + { + "answer_id": 76596800, + "question_id": 76596774, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 726, + "practices": [ + { + "practice": "Avoid keeping a separate \"quotes\" state variable if you can set the randomQuote value directly inside fetchData and display that value on the frontend.", + "conditions": [], + "evidence_type": "both" + }, + { + "practice": "Put the state variable in the useEffect dependency array if you need to do something with it after updating it.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 76665918, + "question_id": 76665849, + "source": "dup", + "score": 0, + "is_accepted": 1, + "body_len": 128, + "practices": [] + }, + { + "answer_id": 76726076, + "question_id": 76726065, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 308, + "practices": [ + { + "practice": "Use the previous state in the setTodos function.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 76734827, + "question_id": 76734763, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 403, + "practices": [ + { + "practice": "Pass the data directly from the React Hook Form values into the handleLogin request instead of relying on the user state.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 76767772, + "question_id": 76767716, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 589, + "practices": [ + { + "practice": "Use a useEffect to log or otherwise access the latest state value after it has updated.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 77107999, + "question_id": 77107721, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 612, + "practices": [] + }, + { + "answer_id": 77128034, + "question_id": 77127947, + "source": "dup", + "score": 0, + "is_accepted": 1, + "body_len": 985, + "practices": [ + { + "practice": "Use useEffect to perform actions after the state variable has been updated, with that state in the dependency array.", + "conditions": [], + "evidence_type": "both" + }, + { + "practice": "Pass a callback function to setState so React provides the previous state as an argument to that function.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 77260886, + "question_id": 77260874, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 275, + "practices": [] + }, + { + "answer_id": 77503102, + "question_id": 77503064, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 173, + "practices": [ + { + "practice": "Use a callback in the setSelectedData function so the update happens through that callback instead of reading the state immediately.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 77503134, + "question_id": 77503064, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 382, + "practices": [] + }, + { + "answer_id": 77504121, + "question_id": 77503064, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 243, + "practices": [] + }, + { + "answer_id": 77504698, + "question_id": 77504568, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 225, + "practices": [ + { + "practice": "Log the state with useEffect or directly inside the JSX to confirm the updated value.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 77504979, + "question_id": 77504568, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 405, + "practices": [] + }, + { + "answer_id": 77538377, + "question_id": 77537893, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 506, + "practices": [ + { + "practice": "Use the fetched data directly in your JSX instead of reading it back from state inside the axios callback.", + "conditions": [], + "evidence_type": "prose" + }, + { + "practice": "Read the updated state on the next render rather than expecting it immediately after dispatching or setting it.", + "conditions": [], + "evidence_type": "prose" + } + ] + }, + { + "answer_id": 77571486, + "question_id": 77571454, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 270, + "practices": [ + { + "practice": "Use the response object directly instead of reading the newly set state value immediately after calling the setter.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 77630824, + "question_id": 77630358, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 364, + "practices": [ + { + "practice": "Create the new state separately in a local value if you need the data immediately instead of waiting for the next render.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 77693336, + "question_id": 77692564, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 126, + "practices": [] + }, + { + "answer_id": 77704920, + "question_id": 77704899, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 488, + "practices": [ + { + "practice": "Use a useEffect to check for the state change and call the validate function when that state changes.", + "conditions": [], + "evidence_type": "both" + }, + { + "practice": "Make the effect look for both the relevant state and the relevant handler/function so it can call the validate function.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 77812965, + "question_id": 77812854, + "source": "dup", + "score": 0, + "is_accepted": 1, + "body_len": 347, + "practices": [ + { + "practice": "Create a temporary source of truth from which both state and localStorage obtain their arguments.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 77976736, + "question_id": 77976612, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 324, + "practices": [ + { + "practice": "Do not store feedback as state when it is derived from another state.", + "conditions": [], + "evidence_type": "prose" + } + ] + }, + { + "answer_id": 78015797, + "question_id": 78015251, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 277, + "practices": [ + { + "practice": "Use a state index to select the right question to display, similar to carousel functionality.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 78073022, + "question_id": 78072677, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 6, + "practices": [] + }, + { + "answer_id": 78095736, + "question_id": 78095702, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 271, + "practices": [] + }, + { + "answer_id": 78096304, + "question_id": 78095702, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 200, + "practices": [ + { + "practice": "Watch the state value for changes using useEffect.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 78111454, + "question_id": 78110624, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 401, + "practices": [ + { + "practice": "Do not create state for values that can be computed or derived from the input value; compute them instead.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 78206918, + "question_id": 78206817, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 157, + "practices": [] + }, + { + "answer_id": 78235707, + "question_id": 78235632, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 109, + "practices": [ + { + "practice": "Move the checkTypaheadMatches function into a useEffect hook.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 78270789, + "question_id": 78270769, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 227, + "practices": [] + }, + { + "answer_id": 78283223, + "question_id": 78283173, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 571, + "practices": [ + { + "practice": "Move the logic into a separate function and call that function after the state update path instead of expecting the setter to update synchronously.", + "conditions": [], + "evidence_type": "both" + }, + { + "practice": "Validate on any date change so the error message appears as soon as a selection is invalid.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 78508656, + "question_id": 78508511, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 877, + "practices": [ + { + "practice": "Use the new json.nextPageToken directly in the URL construction instead of reading nextToken immediately after calling setNextToken.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 78592770, + "question_id": 78592601, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 186, + "practices": [] + }, + { + "answer_id": 78592840, + "question_id": 78592601, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 622, + "practices": [] + }, + { + "answer_id": 78643612, + "question_id": 78643437, + "source": "dup", + "score": 0, + "is_accepted": 1, + "body_len": 517, + "practices": [ + { + "practice": "Use the value from the event instead of the state when you need the actual value immediately.", + "conditions": [], + "evidence_type": "both" + }, + { + "practice": "Use the direct state setter rather than the alternative setter mentioned in the question.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 78651590, + "question_id": 78651409, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 241, + "practices": [ + { + "practice": "Check the updated image state in a useEffect after calling setImage.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 78651690, + "question_id": 78651409, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 260, + "practices": [ + { + "practice": "Preserve the existing state when setting it so a field like name does not overwrite the rest of the state.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 78683781, + "question_id": 78683400, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 487, + "practices": [ + { + "practice": "Add the relevant piece of state to the dependency array if you insist on using the hook.", + "conditions": [ + "if you insist on using the hook" + ], + "evidence_type": "both" + }, + { + "practice": "Use a ref hook so you can access the value anywhere.", + "conditions": [ + "if you insist on using the hook" + ], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 78699983, + "question_id": 78699694, + "source": "dup", + "score": 0, + "is_accepted": 1, + "body_len": 1178, + "practices": [ + { + "practice": "Keep a separate simple JavaScript object for other computation while leaving React state alone for GUI rendering.", + "conditions": [], + "evidence_type": "both" + }, + { + "practice": "Use refs to hold values that need to persist across GUI re-renders.", + "conditions": [], + "evidence_type": "both" + }, + { + "practice": "Use state for values that need to be displayed in the GUI so React can track the changes and re-render when they change.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 78712183, + "question_id": 78711891, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 658, + "practices": [ + { + "practice": "Use a boolean flag to guard against an async race condition so the later state update wins.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 78712259, + "question_id": 78711891, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 187, + "practices": [ + { + "practice": "Use the functional updater form to merge the response data into the previous state object: setInputsData(oldState => ({...oldState, ...response.data})).", + "conditions": [], + "evidence_type": "prose" + } + ] + }, + { + "answer_id": 78721091, + "question_id": 78719638, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 240, + "practices": [] + }, + { + "answer_id": 78769319, + "question_id": 78768496, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 106, + "practices": [] + }, + { + "answer_id": 78790908, + "question_id": 78790840, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 189, + "practices": [] + }, + { + "answer_id": 78790964, + "question_id": 78790840, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 262, + "practices": [ + { + "practice": "Have another useEffect with game_cards as the dependency so the alert runs every time game_cards changes.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 78805409, + "question_id": 78805394, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 93, + "practices": [ + { + "practice": "Use a hook to watch for changes to the value after the callback updates it.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 78897849, + "question_id": 78897777, + "source": "dup", + "score": 0, + "is_accepted": 1, + "body_len": 353, + "practices": [ + { + "practice": "Use the functional updater form to serialize multiple state updates in the handler.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 78941300, + "question_id": 78941267, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 618, + "practices": [ + { + "practice": "Add logging in a useEffect that listens for changes to authUser to verify the state update.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 78962570, + "question_id": 78962438, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 278, + "practices": [] + }, + { + "answer_id": 78964761, + "question_id": 78964726, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 125, + "practices": [ + { + "practice": "Use a useEffect with the relevant state in its dependency array to run code after the state update has completed.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 78965750, + "question_id": 78964726, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 38, + "practices": [] + }, + { + "answer_id": 79176129, + "question_id": 79175658, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 502, + "practices": [ + { + "practice": "Use a functional update so the callback always works with the latest snackbar state.", + "conditions": [], + "evidence_type": "both" + }, + { + "practice": "Remove the state value from the useCallback dependency array when the callback uses the functional updater.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 79436357, + "question_id": 79435733, + "source": "dup", + "score": 0, + "is_accepted": 0, + "body_len": 438, + "practices": [] + }, + { + "answer_id": 75032390, + "question_id": 75030746, + "source": "dup", + "score": -1, + "is_accepted": 0, + "body_len": 463, + "practices": [ + { + "practice": "Use a side effect with useEffect to run the log at the right moment after the state change.", + "conditions": [], + "evidence_type": "both" + }, + { + "practice": "Render the value in the component so you can see the updated state on a later render.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 75244600, + "question_id": 75244564, + "source": "dup", + "score": -1, + "is_accepted": 0, + "body_len": 197, + "practices": [] + }, + { + "answer_id": 76596848, + "question_id": 76596774, + "source": "dup", + "score": -1, + "is_accepted": 0, + "body_len": 77, + "practices": [] + }, + { + "answer_id": 76950258, + "question_id": 76950232, + "source": "dup", + "score": -1, + "is_accepted": 0, + "body_len": 66, + "practices": [ + { + "practice": "Use useEffect to do something whenever state changes.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 76950284, + "question_id": 76950232, + "source": "dup", + "score": -1, + "is_accepted": 1, + "body_len": 680, + "practices": [ + { + "practice": "Read the updated state during the next render, when React has re-rendered the component.", + "conditions": [], + "evidence_type": "both" + }, + { + "practice": "Use a useEffect hook if you only need the state when it changes rather than on every render.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 76974033, + "question_id": 76973961, + "source": "dup", + "score": -1, + "is_accepted": 0, + "body_len": 953, + "practices": [ + { + "practice": "Read the updated state on the next render, since the value is assigned in the next render after the fetch finishes.", + "conditions": [ + "during the fetch / after the communication with the server finishes" + ], + "evidence_type": "both" + }, + { + "practice": "Put the logging or other follow-up logic in a useEffect that depends on the state so it runs whenever that state changes.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 77025579, + "question_id": 77024992, + "source": "dup", + "score": -1, + "is_accepted": 0, + "body_len": 599, + "practices": [ + { + "practice": "Use a useEffect to do something after the state has been updated.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 77165168, + "question_id": 77165106, + "source": "dup", + "score": -1, + "is_accepted": 0, + "body_len": 95, + "practices": [ + { + "practice": "Make a deep copy of the updated value before setting state so React gets a new reference.", + "conditions": [], + "evidence_type": "prose" + } + ] + }, + { + "answer_id": 77261732, + "question_id": 77261702, + "source": "dup", + "score": -1, + "is_accepted": 0, + "body_len": 234, + "practices": [ + { + "practice": "Use the updated state on the next render rather than expecting it to change immediately.", + "conditions": [], + "evidence_type": "both" + }, + { + "practice": "Save the new state into a variable and use that variable in the callback instead of reading state back immediately.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 77362386, + "question_id": 77362261, + "source": "dup", + "score": -1, + "is_accepted": 1, + "body_len": 43, + "practices": [] + }, + { + "answer_id": 77816335, + "question_id": 77814993, + "source": "dup", + "score": -1, + "is_accepted": 0, + "body_len": 304, + "practices": [ + { + "practice": "Add some delay before calling the getData method.", + "conditions": [], + "evidence_type": "prose" + }, + { + "practice": "Pass the needed values as parameters to the getData() method instead of reading state back later.", + "conditions": [], + "evidence_type": "prose" + } + ] + }, + { + "answer_id": 78175229, + "question_id": 78174664, + "source": "dup", + "score": -1, + "is_accepted": 0, + "body_len": 25, + "practices": [] + }, + { + "answer_id": 78175319, + "question_id": 78174664, + "source": "dup", + "score": -1, + "is_accepted": 0, + "body_len": 49, + "practices": [] + }, + { + "answer_id": 78301907, + "question_id": 78301874, + "source": "dup", + "score": -1, + "is_accepted": 1, + "body_len": 470, + "practices": [ + { + "practice": "Do not keep totalPrice as its own state when it can be directly calculated from the cart; calculate it during rendering instead.", + "conditions": [], + "evidence_type": "both" + }, + { + "practice": "If the calculation is expensive, wrap it in useMemo so it only recalculates when cartItems change.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 78401972, + "question_id": 78401748, + "source": "dup", + "score": -1, + "is_accepted": 1, + "body_len": 305, + "practices": [ + { + "practice": "Use another useEffect if you want to run an operation right after the state change.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 78570306, + "question_id": 78570270, + "source": "dup", + "score": -1, + "is_accepted": 0, + "body_len": 840, + "practices": [ + { + "practice": "Use the spread operator to set an array state from the returned array data instead of dumping it directly into state.", + "conditions": [ + "when the backend returns an array" + ], + "evidence_type": "both" + }, + { + "practice": "Console.log the state after the data is set rather than immediately after calling the setter.", + "conditions": [], + "evidence_type": "both" + }, + { + "practice": "Use callbacks to see the data after it is set.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 78833418, + "question_id": 78833250, + "source": "dup", + "score": -1, + "is_accepted": 1, + "body_len": 248, + "practices": [] + }, + { + "answer_id": 79234459, + "question_id": 79234402, + "source": "dup", + "score": -1, + "is_accepted": 0, + "body_len": 570, + "practices": [ + { + "practice": "Rely on the component's next render cycle to access the updated state instead of expecting it immediately after calling setState.", + "conditions": [], + "evidence_type": "prose" + } + ] + }, + { + "answer_id": 79292211, + "question_id": 79292073, + "source": "dup", + "score": -1, + "is_accepted": 0, + "body_len": 87, + "practices": [ + { + "practice": "Use the updater function form of the state setter.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 79584523, + "question_id": 79584479, + "source": "dup", + "score": -1, + "is_accepted": 0, + "body_len": 189, + "practices": [ + { + "practice": "Log the value you are about to set instead of accessing the state variable immediately after calling the setter.", + "conditions": [], + "evidence_type": "prose" + } + ] + }, + { + "answer_id": 79617200, + "question_id": 79616934, + "source": "dup", + "score": -1, + "is_accepted": 0, + "body_len": 323, + "practices": [ + { + "practice": "Calculate the new index manually and use it directly for both the state update and any other logic that depends on it.", + "conditions": [], + "evidence_type": "both" + } + ] + }, + { + "answer_id": 75030835, + "question_id": 75030746, + "source": "dup", + "score": -2, + "is_accepted": 0, + "body_len": 353, + "practices": [ + { + "practice": "Use a function with the previous value as its first argument when updating boolean or number state.", + "conditions": [], + "evidence_type": "prose" + } + ] + } +] \ No newline at end of file diff --git a/src/module_d/data/query_narrative.json b/src/module_d/data/query_narrative.json new file mode 100644 index 0000000..46ef3fc --- /dev/null +++ b/src/module_d/data/query_narrative.json @@ -0,0 +1,11 @@ +{ + "query": "The useState set method is not reflecting a change immediately", + "group_size": 70, + "shape": "mixed", + "dominant_approach": "useEffect after state change", + "authority_alignment": "dispersed", + "temporal": "insufficient_coverage", + "headline": "Many fixes coexist; useEffect leads but doesn’t dominate", + "body": "The biggest camp is useEffect after state change at 48 of 163 practices (29%), with perfect within-cluster agreement, but that is far from a majority. The breakdown is broad rather than unanimous: using the local/event/response value immediately (27) and functional updater from previous state (19) are substantial rivals, followed by several smaller clusters and a long tail. Authority is spread across multiple approaches, and the answers are concentrated in 2023-2024, so there is no solid temporal trajectory to read.", + "_note": "query/group_size = Module C placeholders; shape/authority/temporal/headline/body = D-4 LLM output" +} \ No newline at end of file diff --git a/src/module_d/layout.py b/src/module_d/layout.py new file mode 100644 index 0000000..f1528b8 --- /dev/null +++ b/src/module_d/layout.py @@ -0,0 +1,37 @@ +"""Circle-pack geometry for the breakdown: non-overlapping circles whose AREA encodes authority +at both levels — outer (cluster) = authority mass, inner (answer) = each point's authority. Pure +geometry, no domain logic, no I/O. Cluster order is decided by the caller and preserved here. +""" +from __future__ import annotations + +import circlify + +from module_d.contract import Breakdown, PracticePoint + + +def pack(breakdown: Breakdown) -> Breakdown: + """Fill x/y/r on every point and cluster shell via a 2-level circle pack (cluster → its + points), sized by authority. Mutates and returns the breakdown.""" + pts_by_cluster: dict[str, list[PracticePoint]] = {} + for p in breakdown.points: + pts_by_cluster.setdefault(p.cluster, []).append(p) + + data = [] + for shell in breakdown.clusters: + kids = [{"id": id(p), "datum": max(p.authority, 1e-6)} for p in pts_by_cluster[shell.id]] + data.append({"id": shell.id, "datum": sum(k["datum"] for k in kids), "children": kids}) + + circles = circlify.circlify( + data, show_enclosure=False, target_enclosure=circlify.Circle(x=0, y=0, r=1) + ) + + point_by_key = {id(p): p for ps in pts_by_cluster.values() for p in ps} + shell_by_id = {s.id: s for s in breakdown.clusters} + for c in circles: + if c.level == 1: + s = shell_by_id[c.ex["id"]] + s.x, s.y, s.r = c.x, c.y, c.r + else: + p = point_by_key[c.ex["id"]] + p.x, p.y, p.r = c.x, c.y, c.r + return breakdown diff --git a/src/module_d/ports.py b/src/module_d/ports.py new file mode 100644 index 0000000..1765ee6 --- /dev/null +++ b/src/module_d/ports.py @@ -0,0 +1,51 @@ +"""The two seams to other teams' work. Module D depends on these Protocols, not on concrete +sources, so a proxy today and the real provider later are drop-in interchangeable. + + AuthorityProvider ← Module B (yearly PageRank). Proxy: SO vote/accept/reputation. + CanonicalGroupProvider ← Module C (query → canonical_group). Proxy: SO duplicate chain. + +This mirrors the existing provider-neutral `llm_call` seam (PLAN ▼Q2): the call sites are stable, +the blast radius of a real swap is confined to one adapter. +""" +from __future__ import annotations + +from dataclasses import dataclass +from typing import Protocol, runtime_checkable + + +@dataclass +class Answer: + """One answer's identity + the signals a real authority provider would also key on.""" + + answer_id: int + vote: int + is_accepted: bool + source: str # "canonical" | "dup" + date: int | None # epoch seconds + reputation: int | None = None + body: str | None = None # answer content (for the detail panel) + author: str | None = None + + +@dataclass +class CanonicalGroup: + group_id: str + answers: list[Answer] + + +@runtime_checkable +class CanonicalGroupProvider(Protocol): + """Module C seam: resolve a group to its member answers.""" + + source_name: str + + def fetch(self, group_id: str) -> CanonicalGroup: ... + + +@runtime_checkable +class AuthorityProvider(Protocol): + """Module B seam: map an answer to a scalar authority.""" + + source_name: str + + def score(self, answer: Answer) -> float: ... diff --git a/src/module_d/probe_d/build_narrative.py b/src/module_d/probe_d/build_narrative.py new file mode 100644 index 0000000..32f0b59 --- /dev/null +++ b/src/module_d/probe_d/build_narrative.py @@ -0,0 +1,160 @@ +"""Probe D-4 — per-query narrative of the practice-breakdown shape (spec §1.2 RQ2/RQ3/RQ4). + +Assembles a precomputed SIGNAL TABLE from the existing artifacts (no DB, no re-clustering) and +feeds it to the narrative LLM. The narrative is a pure function (signals -> prose): it reports +only what the table shows. Authority is attributed at the ANSWER level and DEDUPED WITHIN each +cluster (one authoritative answer that contributes several practices to a cluster counts once), +so `answers` measures how many INDEPENDENT answers back a cluster; cross-cluster spanning is kept +(a high-authority author may genuinely back several approaches). + +Inputs (all already on disk): + data/extractions.json merged gate-kept 163 practices, per-answer (answer_id, score) [flat order] + data/clusters.json 12 head clusters: name, members (flat indices), agreement + dashboard/data/viz_data.json per-answer year (query title falls back to a constant for now) + +DRY RUN by default — prints the signal table, the full message payload, and a token/cost estimate, +and does NOT call the LLM. Pass --run to actually invoke the narrative model (costs a few cents): + uv run python src/module_d/probe_d/build_narrative.py # dry run, $0 + uv run python src/module_d/probe_d/build_narrative.py --run # real call +""" +import json +import statistics +import sys +from collections import defaultdict +from pathlib import Path + +from llm import PRICE, USAGE, llm_call +from prompts import build_narrative_messages +from schemas import QueryNarrative + +HERE = Path(__file__).resolve().parent +MOD = HERE.parent # src/module_d +EXT = MOD / "data" / "extractions.json" +CLUST = MOD / "data" / "clusters.json" +VIZ = MOD / "dashboard" / "data" / "viz_data.json" +OUT_JSON = MOD / "data" / "query_narrative.json" # persisted on --run; build.py folds into meta.narrative +ANCHOR_TITLE = "The useState set method is not reflecting a change immediately" +# Module C placeholders: the canonical question title + count of questions that contributed answers. +# Real canonical title comes from data/canonical_q54069253.json; Module C will own both fields. +QUERY_TITLE = ANCHOR_TITLE +GROUP_SIZE = 70 + + +def build_signal_table() -> str: + ext = json.loads(EXT.read_text()) + flat = [(r["answer_id"], r.get("score") or 0) for r in ext for _ in r["practices"]] + total = len(flat) + cl = json.loads(CLUST.read_text())["clusters"] + viz = json.loads(VIZ.read_text()) + a2y = {p["answer_id"]: p.get("year") for p in viz["points"]} + title = viz.get("meta", {}).get("title") or viz.get("meta", {}).get("query") or ANCHOR_TITLE + + # query-level answers-by-year (distinct answers) + by_year = defaultdict(set) + for aid, _ in flat: + y = a2y.get(aid) + if y: + by_year[y].add(aid) + total_ans = len({a for a, _ in flat}) + year_line = " ".join(f"{y}:{len(by_year[y])}" for y in sorted(by_year)) + + # cross-year coverage: how concentrated are answers? (the ONLY input the model gates RQ4 on — + # per-cluster median years are outlier-prone and must NOT drive the temporal call) + yc = sorted(((y, len(s)) for y, s in by_year.items()), key=lambda kv: -kv[1]) + if len(yc) >= 2: + (y1, c1), (y2, c2) = yc[0], yc[1] + busy = ", ".join(str(y) for y in sorted((y1, y2))) + cov_line = (f"CROSS-YEAR COVERAGE: {(c1 + c2) / total_ans:.0%} of answers in the two " + f"busiest years ({busy}); {total_ans - c1 - c2} of {total_ans} elsewhere.") + else: + cov_line = f"CROSS-YEAR COVERAGE: all {total_ans} answers in a single year." + + # per-cluster signals, authority DEDUPED to distinct answer within the cluster + rows, head_size, wsum = [], 0, 0.0 + for c in cl: + members = c["members"] + head_size += len(members) + wsum += c["agreement"] * len(members) + ans_score = {} + for m in members: + aid, sc = flat[m] + ans_score[aid] = max(ans_score.get(aid, sc), sc) # one row per distinct answer + peak = max(ans_score.values()) if ans_score else 0 + years = [a2y[a] for a in ans_score if a2y.get(a)] # median over distinct answers + ymed = int(statistics.median(years)) if years else 0 + rows.append((len(members), c["agreement"], len(ans_score), peak, ymed, c["name"])) + + rows.sort(key=lambda r: -r[0]) + longtail = total - head_size + mean_agree = wsum / head_size if head_size else 0.0 + + lines = [ + f"QUERY: {title}", + f"TOTALS: {total} practices · {total_ans} answers · " + f"{len(cl)} head clusters + {longtail} long-tail · " + f"mean within-cluster agreement {mean_agree:.2f}", + f"ANSWERS BY YEAR: {year_line}", + cov_line, + "", + "APPROACH CLUSTERS (largest first):", + f" {'size':>4} {'share':>5} {'agree':>5} {'answers':>7} {'peak_vote':>9} " + f"{'median_yr':>9} approach", + ] + for size, agree, nans, peak, ymed, name in rows: + share = f"{round(100 * size / total)}%" + lines.append(f" {size:>4} {share:>5} {agree:>5.2f} {nans:>7} {peak:>9} " + f"{ymed:>9} {name}") + lines.append(f"LONG TAIL: {longtail} idiosyncratic single-practice approaches") + return "\n".join(lines) + + +def main() -> None: + table = build_signal_table() + messages = build_narrative_messages(table) + + print("=" * 78) + print("SIGNAL TABLE (fed to the narrative model)") + print("=" * 78) + print(table) + + chars = sum(len(m["content"]) for m in messages) + in_tok = chars // 4 # rough char/4 heuristic (no tiktoken dep) + out_tok = 350 # reasoning + 5 short fields, generous + pin, pout = PRICE["narrative"] + est = in_tok / 1e6 * pin + out_tok / 1e6 * pout + print() + print("=" * 78) + print(f"PAYLOAD: {len(messages)} messages (2 few-shot pairs + system + this query)") + print(f"COST ESTIMATE: ~{in_tok:,} in + ~{out_tok} out tok @ narrative price " + f"(${pin}/${pout} per 1M) ≈ ${est:.4f}") + print("=" * 78) + + if "--run" not in sys.argv: + print("\nDRY RUN — no LLM call made. Re-run with --run to invoke the narrative model.") + return + + out: QueryNarrative = llm_call(messages, QueryNarrative, tier="narrative") + print("\n--- NARRATIVE ---") + print(f"shape={out.shape} | dominant={out.dominant_approach!r}") + print(f"authority_alignment={out.authority_alignment} | temporal={out.temporal}") + print(f"\nHEADLINE: {out.headline}") + print(f"BODY: {out.body}") + print(f"\n(reasoning: {out.reasoning})") + + # persist for the dashboard (build.py folds this into viz_data.meta.narrative). query/group_size + # are Module C placeholders; the verdict enums + prose are this run's D-4 output. + record = { + "query": QUERY_TITLE, "group_size": GROUP_SIZE, + "shape": out.shape, "dominant_approach": out.dominant_approach, + "authority_alignment": out.authority_alignment, "temporal": out.temporal, + "headline": out.headline, "body": out.body, + "_note": "query/group_size = Module C placeholders; shape/authority/temporal/headline/body = D-4 LLM output", + } + OUT_JSON.write_text(json.dumps(record, ensure_ascii=False, indent=2)) + print(f"\nwrote {OUT_JSON.relative_to(MOD.parent.parent)}") + cost = USAGE["input"] / 1e6 * pin + USAGE["output"] / 1e6 * pout + print(f"\n{USAGE['calls']} call | {USAGE['input']:,}+{USAGE['output']:,} tok | ~${cost:.4f}") + + +if __name__ == "__main__": + main() diff --git a/src/module_d/probe_d/d1_extract.py b/src/module_d/probe_d/d1_extract.py new file mode 100644 index 0000000..0571082 --- /dev/null +++ b/src/module_d/probe_d/d1_extract.py @@ -0,0 +1,111 @@ +"""Probe D-1 — Step-1 practice extraction over the q54069253 canonical chain. + +Input unit (PLAN ▼Q5): the 2019 canonical's 21 answers (probe-local fixture) UNION the +132 in-window duplicates' answers (main DB) = ~163 pooled answers. All canonical answers +are processed first; duplicates are sampled ACROSS the score range (not just top) so thin +low-score answers are exercised (verifies empty-practice + evidence_type variance). + +Usage: + uv run python src/module_d/probe_d/d1_extract.py [N] # default 30 + +Writes probe_d/data/d1_extractions.json. Does NOT touch the main DB (probe-local). +""" +import json +import sqlite3 +import sys +from collections import Counter +from pathlib import Path + +from llm import TIER_MODEL, llm_call, usage_report +from prompts import build_extraction_messages +from schemas import AnswerExtraction + +HERE = Path(__file__).resolve().parent +ROOT = HERE.parents[2] +FIXTURE = HERE / "data" / "canonical_54069253.json" +DB = ROOT / "data" / "so_data_reactjs.db" +OUT = HERE / "data" / "d1_extractions.json" +MAX_CONDITIONS = 3 # ▼Q2 cardinality enforced in Python, not in the schema + + +def load_answers() -> list[dict]: + fx = json.loads(FIXTURE.read_text()) + title = fx["canonical_question"]["title"] + recs = [ + {"answer_id": a["answer_id"], "question_id": a["question_id"], "parent_title": title, + "source": "canonical", "body_text": a["body_text"], "score": a["score"], + "is_accepted": a["is_accepted"], "body_len": len(a["body_text"])} + for a in fx["canonical_answers"] + ] + con = sqlite3.connect(DB) + qmarks = ",".join("?" * len(fx["dup_question_ids"])) + rows = con.execute( + f"""SELECT a.answer_id, a.question_id, q.title, a.body_text, a.score, a.is_accepted + FROM answers a JOIN questions q ON q.question_id = a.question_id + WHERE a.question_id IN ({qmarks}) + ORDER BY a.score DESC""", + fx["dup_question_ids"], + ).fetchall() + con.close() + for aid, qid, qtitle, body, score, acc in rows: + body = body or "" + recs.append({"answer_id": aid, "question_id": qid, "parent_title": qtitle, + "source": "dup", "body_text": body, "score": score, + "is_accepted": acc, "body_len": len(body)}) + return recs + + +def build_slice(answers: list[dict], n: int) -> list[dict]: + canonical = [a for a in answers if a["source"] == "canonical"] + dups = [a for a in answers if a["source"] == "dup"] # score desc + k = max(0, n - len(canonical)) + step = max(1, len(dups) // k) if k else 1 + dup_sample = dups[::step][:k] if k else [] + return (canonical + dup_sample)[:n] + + +def main() -> None: + n = int(sys.argv[1]) if len(sys.argv) > 1 else 30 + answers = load_answers() + sl = build_slice(answers, n) + print(f"pooled: {len(answers)} | running {len(sl)} " + f"(canonical {sum(a['source']=='canonical' for a in sl)}, " + f"dup-spread {sum(a['source']=='dup' for a in sl)}) | tier=extract ({TIER_MODEL['extract']})\n") + + results, overflow = [], 0 + for i, a in enumerate(sl, 1): + ex: AnswerExtraction = llm_call( + build_extraction_messages(a["parent_title"], a["body_text"]), + AnswerExtraction, tier="extract") + practices = [] + for p in ex.practices: + if len(p.conditions) > MAX_CONDITIONS: + overflow += 1 + practices.append({"practice": p.practice, "conditions": p.conditions[:MAX_CONDITIONS], + "evidence_type": p.evidence_type}) + results.append({k: a[k] for k in ("answer_id", "question_id", "source", "score", + "is_accepted", "body_len")} | {"practices": practices}) + head = f"[{i:>2}/{len(sl)}] a{a['answer_id']} ({a['source']}, score={a['score']}, len={a['body_len']})" + if not practices: + print(f"{head} -> · no practice") + else: + print(f"{head} -> {len(practices)} practice(s)") + for p in practices: + cond = f" (cond: {p['conditions']})" if p["conditions"] else "" + print(f" [{p['evidence_type']}] {p['practice'][:88]}{cond}") + + OUT.write_text(json.dumps(results, ensure_ascii=False, indent=2)) + with_p = sum(bool(r["practices"]) for r in results) + total_p = sum(len(r["practices"]) for r in results) + multi = sum(len(r["practices"]) >= 2 for r in results) + et = Counter(p["evidence_type"] for r in results for p in r["practices"]) + cond_n = sum(bool(p["conditions"]) for r in results for p in r["practices"]) + print(f"\nsummary: {with_p}/{len(results)} answers have >=1 practice " + f"({len(results)-with_p} empty) | {total_p} practices total | {multi} multi-practice answers") + print(f" evidence_type {dict(et)} | conditions non-empty {cond_n}/{total_p} | overflow(>3) {overflow}") + print(f"written: {OUT.relative_to(ROOT)}") + print(usage_report()) + + +if __name__ == "__main__": + main() diff --git a/src/module_d/probe_d/d1_extract_gated.py b/src/module_d/probe_d/d1_extract_gated.py new file mode 100644 index 0000000..9bbf386 --- /dev/null +++ b/src/module_d/probe_d/d1_extract_gated.py @@ -0,0 +1,117 @@ +"""Probe D-1g — MERGED Step-1 extraction + Step-1b relevance/substance gate in ONE call. + +Collapses d1_extract.py (extract) + d1b_gate.py (gate) into a single per-answer LLM call: +the model extracts each practice and immediately tags it relevant/substantive against the +canonical problem. Kept = relevant AND substantive, applied as a DETERMINISTIC Python filter +(the drop stays auditable). Halves answer-side calls (N vs 2N) and gives the gate the full +answer body as context — which the standalone gate (practice strings only) never had. + +This is the CANDIDATE main-axis step; the standalone d1_extract + d1b_gate are kept as the +A/B baseline. Promote only after d1_ab_compare.py shows no leniency bias + ~0 GOOD casualties. + +Writes: + - d1_gated_full.json : every extracted practice WITH its verdict (audit / A/B input) + - d1_gated.json : kept practices only, SAME shape as d1_filtered.json (downstream drop-in, + e.g. `uv run python d2_consensus.py <...>/d1_gated.json`) + +Usage: + uv run python src/module_d/probe_d/d1_extract_gated.py [N] # default 30 +""" +import json +import re +from collections import Counter +from pathlib import Path + +from d1_extract import build_slice, load_answers +from llm import TIER_MODEL, llm_call, usage_report +from prompts import CANONICAL_PROBLEM, build_extraction_gated_messages +from schemas import AnswerExtractionGated + +HERE = Path(__file__).resolve().parent +ROOT = HERE.parents[2] +OUT_FULL = HERE / "data" / "d1_gated_full.json" +OUT_KEPT = HERE / "data" / "d1_gated.json" +MAX_CONDITIONS = 3 # ▼Q2 cardinality enforced in Python, not in the schema +META_KEYS = ("answer_id", "question_id", "source", "score", "is_accepted", "body_len") + +# evidence_type is a STRUCTURAL property of the source answer, not an LLM judgment (user call +# 2026-06-02): does the answer contain a code BLOCK? Markers differ by source — fixture answers +# use markdown ``` fences; DB answers had code replaced by a standalone [CODE] placeholder line. +# Answer-level (every practice of an answer inherits it), since nothing downstream reads it per +# practice and the dashboard unit is the answer. Validated vs the separate LLM run before adopting. +_FENCE = re.compile(r"```.*?```", re.S) # markdown fenced block (fixture) +_DBBLOCK = re.compile(r"(^|\n)[ \t]*\[CODE\][ \t]*(\n|$)") # [CODE] standalone line (DB block) + + +def evidence_type(body: str) -> str: + """'prose' (no code block) | 'code' (essentially only code) | 'both' (code + prose).""" + body = body or "" + if not (_FENCE.search(body) or _DBBLOCK.search(body)): + return "prose" + prose = _DBBLOCK.sub(" ", _FENCE.sub(" ", body)).replace("[CODE]", " ") + return "code" if len(re.sub(r"\s+", " ", prose).strip()) < 40 else "both" + + +def main() -> None: + import sys + + n = int(sys.argv[1]) if len(sys.argv) > 1 else 30 + answers = load_answers() + sl = build_slice(answers, n) + print(f"pooled: {len(answers)} | running {len(sl)} " + f"(canonical {sum(a['source']=='canonical' for a in sl)}, " + f"dup-spread {sum(a['source']=='dup' for a in sl)}) | tier=extract ({TIER_MODEL['extract']})\n") + + full_records, kept_records, dropped, overflow = [], [], [], 0 + for i, a in enumerate(sl, 1): + ex: AnswerExtractionGated = llm_call( + build_extraction_gated_messages(CANONICAL_PROBLEM, a["parent_title"], a["body_text"]), + AnswerExtractionGated, tier="extract") + et = evidence_type(a["body_text"]) # deterministic, answer-level + all_ps, kept_ps = [], [] + for p in ex.practices: + if len(p.conditions) > MAX_CONDITIONS: + overflow += 1 + conds = p.conditions[:MAX_CONDITIONS] + all_ps.append({"practice": p.practice, "conditions": conds, + "evidence_type": et, "relevant": p.relevant, + "substantive": p.substantive, "gate_reason": p.gate_reason}) + if p.relevant and p.substantive: + kept_ps.append({"practice": p.practice, "conditions": conds, + "evidence_type": et}) + else: + why = ("off-topic" if not p.relevant else "") + \ + (" non-substantive" if not p.substantive else "") + dropped.append((a["answer_id"], why.strip(), p.practice)) + meta = {k: a[k] for k in META_KEYS} + full_records.append(meta | {"practices": all_ps}) + kept_records.append(meta | {"practices": kept_ps}) + + head = f"[{i:>2}/{len(sl)}] a{a['answer_id']} ({a['source']}, score={a['score']})" + if not all_ps: + print(f"{head} -> · no practice") + else: + print(f"{head} -> {len(all_ps)} practice(s), {len(kept_ps)} kept") + for p in all_ps: + tag = "keep" if (p["relevant"] and p["substantive"]) else "DROP" + print(f" [{tag}] [{p['evidence_type']}] {p['practice'][:80]}") + + OUT_FULL.write_text(json.dumps(full_records, ensure_ascii=False, indent=2)) + OUT_KEPT.write_text(json.dumps(kept_records, ensure_ascii=False, indent=2)) + + n_all = sum(len(r["practices"]) for r in full_records) + n_kept = sum(len(r["practices"]) for r in kept_records) + with_kept = sum(bool(r["practices"]) for r in kept_records) + et = Counter(p["evidence_type"] for r in full_records for p in r["practices"]) + print(f"\nsummary: {n_all} practices extracted -> {n_kept} kept ({len(dropped)} dropped) " + f"| {with_kept}/{len(kept_records)} answers keep >=1") + print(f" evidence_type {dict(et)} | overflow(>3 cond) {overflow}") + print(f" dropped reasons: {dict(Counter(d[1] for d in dropped))}") + for aid, why, txt in dropped: + print(f" a{aid} [{why}] {txt[:74]}") + print(f"\nwritten: {OUT_FULL.relative_to(ROOT)} + {OUT_KEPT.relative_to(ROOT)}") + print(usage_report()) + + +if __name__ == "__main__": + main() diff --git a/src/module_d/probe_d/d2_consensus.py b/src/module_d/probe_d/d2_consensus.py new file mode 100644 index 0000000..8c53e3a --- /dev/null +++ b/src/module_d/probe_d/d2_consensus.py @@ -0,0 +1,118 @@ +"""Probe D-2b — k=3 aggregator + co-association consensus clustering. + +This is the concrete definition of spec § 3.6's black-box `majority_vote_on_assignments`: +run the Step-2 aggregator K times, then for each PAIR of practices count how often the K +runs put them in the same cluster. Pairs that co-occur in a majority (>= ceil(K/2)) of runs +are linked; connected components of those links form the consensus clustering. This is +label-alignment-free (immune to differing cluster counts / IDs across runs). + +The per-consensus-cluster mean co-association is the spec § 4 "voting agreement" metric +(target > 0.7). Reads probe_d/data/d1_extractions.json. Usage: + uv run python src/module_d/probe_d/d2_consensus.py +""" +import json +import sys +from collections import defaultdict +from pathlib import Path + +import numpy as np +from scipy.sparse import csr_matrix +from scipy.sparse.csgraph import connected_components +from sklearn.cluster import AgglomerativeClustering +from sklearn.metrics import adjusted_rand_score +from sklearn.preprocessing import normalize + +from llm import USAGE, embed, llm_call +from prompts import build_aggregator_messages +from schemas import AggregatorOutput + +HERE = Path(__file__).resolve().parent +# Canonical D1+D1b input = merged extract+gate (d1_gated.json). Override with argv[1] to cluster a +# different set (e.g. d1_extractions.json raw, or the legacy separate d1_filtered.json). +D1 = HERE / "data" / "d1_gated.json" +OUT = HERE / "data" / "d2_assignment.json" # build-shape {n, clusters:[{name, members}], _note} +K = 3 + + +def to_labels(agg: AggregatorOutput, n: int) -> list[int]: + lab = [-1] * n + for ci, c in enumerate(agg.clusters): + for idx in c.member_indices: + if 0 <= idx < n: + lab[idx] = ci + return lab + + +def main() -> None: + src = Path(sys.argv[1]) if len(sys.argv) > 1 else D1 # default = merged d1_gated.json + data = json.loads(src.read_text()) + sents = [p["practice"] for r in data for p in r["practices"]] + n = len(sents) + print(f"k={K} aggregator voting on {n} practices\n") + + runs = llm_call(build_aggregator_messages(sents), AggregatorOutput, tier="aggregate", k_voting=K) + labelsets = [to_labels(r, n) for r in runs] + print("per-run cluster counts:", [len(r.clusters) for r in runs]) + + # co-association counts (0..K) + co = np.zeros((n, n), dtype=int) + for lab in labelsets: + for i in range(n): + if lab[i] == -1: + continue + for j in range(i + 1, n): + if lab[j] == lab[i]: + co[i, j] += 1 + co[j, i] += 1 + + thresh = K // 2 + 1 # majority = 2 of 3 + _, comp = connected_components(csr_matrix((co >= thresh).astype(int)), directed=False) + groups = defaultdict(list) + for i, c in enumerate(comp): + groups[c].append(i) + ordered = sorted(groups.values(), key=len, reverse=True) + + def agreement(members: list[int]) -> float: + if len(members) < 2: + return 1.0 + vals = [co[i, j] / K for a, i in enumerate(members) for j in members[a + 1:]] + return sum(vals) / len(vals) + + head_clusters = [] + print(f"\n=== consensus: {len(ordered)} clusters ({sum(len(g) >= 2 for g in ordered)} multi-member) ===") + for members in ordered: + if len(members) < 2: + continue + names = [runs[0].clusters[labelsets[0][i]].name for i in members if labelsets[0][i] != -1] + name = max(set(names), key=names.count) if names else "(unnamed)" + head_clusters.append({"name": name, "members": sorted(members), + "agreement": round(agreement(members), 4)}) # D-4 narrative reads this + print(f" n={len(members)} agreement={agreement(members):.2f} ~ {name}") + for i in members: + print(f" - {sents[i][:72]}") + print(f" (+ {sum(len(g) == 1 for g in ordered)} singleton consensus clusters → long-tail)") + + OUT.write_text(json.dumps( + {"n": n, "clusters": head_clusters, + "_note": f"k={K} co-association consensus (majority {thresh}/{K} of {n} practices); " + f"{len(head_clusters)} multi-member head clusters; singletons omitted → long-tail"}, + ensure_ascii=False, indent=2)) + print(f"wrote {OUT.relative_to(HERE.parents[2])} · {len(head_clusters)} head clusters / n={n}") + + within = [(i, j) for g in ordered if len(g) >= 2 for a, i in enumerate(g) for j in g[a + 1:]] + if within: + print(f"\noverall within-cluster agreement = {np.mean([co[i, j] / K for i, j in within]):.2f} " + f"| unanimous 3/3 pairs = {np.mean([co[i, j] == K for i, j in within]):.0%}") + + # does the STABLE consensus agree better with the embedding companion than k=1 did? + X = normalize(np.array(embed(sents))) + agglo = AgglomerativeClustering(n_clusters=len(ordered)).fit_predict(X) + print(f"ARI (consensus vs agglomerative matched-k={len(ordered)}) = " + f"{adjusted_rand_score(comp, agglo):.3f} (k=1 single run was ~0.39)") + + cost = USAGE["input"] / 1e6 * 2.50 + USAGE["output"] / 1e6 * 12.00 + print(f"\n{USAGE['calls']} aggregator calls | ~${cost:.4f}") + + +if __name__ == "__main__": + main() diff --git a/src/module_d/probe_d/fetch_canonical.py b/src/module_d/probe_d/fetch_canonical.py new file mode 100644 index 0000000..9f53feb --- /dev/null +++ b/src/module_d/probe_d/fetch_canonical.py @@ -0,0 +1,105 @@ +"""Fetch the out-of-window canonical question q54069253 + its answers from the SE API. + +Module D probe fixture builder. The 132 in-window duplicates (+ their answers) already +live in the main DB (data/so_data_reactjs.db); only this 2019 canonical is outside the +2023-2026 window, so we pull it once and store it probe-local. We deliberately do NOT +insert into the main DB — Module A owns that window invariant (see PLAN ▼Q5b). + +Re-runnable. Reads SE_API_KEY from .env. Public CC-BY-SA data. +""" +import json +import re +import html +import time +import urllib.request +import urllib.parse +from pathlib import Path + +CANONICAL_ID = 54069253 +SE_BASE = "https://api.stackexchange.com/2.3" +ROOT = Path(__file__).resolve().parents[3] +DUP_PAIRS = ROOT / "probe/2026-05-25-canonical-grouping-density/data/so_dup_pairs.json" +OUT = Path(__file__).resolve().parent / "data" / f"canonical_{CANONICAL_ID}.json" + + +def load_key() -> str: + for line in (ROOT / ".env").read_text().splitlines(): + if line.startswith("SE_API_KEY"): + return line.split("=", 1)[1].strip() + raise SystemExit("SE_API_KEY not found in .env") + + +def clean(body_html: str) -> str: + """HTML -> text, preserving code as fenced/inline markdown so code-as-evidence survives.""" + s = re.sub(r"]*>\s*]*>([\s\S]*?)\s*
", + lambda m: "\n```\n" + html.unescape(m.group(1)) + "\n```\n", body_html) + s = re.sub(r"]*>([\s\S]*?)", lambda m: "`" + html.unescape(m.group(1)) + "`", s) + s = re.sub(r"<[^>]+>", " ", s) + s = html.unescape(s) + s = re.sub(r"[ \t]+", " ", s) + s = re.sub(r"\n\s*\n\s*\n+", "\n\n", s) + return s.strip() + + +def get(path: str, params: dict, key: str) -> dict: + params = {**params, "site": "stackoverflow", "key": key} + url = f"{SE_BASE}{path}?{urllib.parse.urlencode(params)}" + data = json.load(urllib.request.urlopen(url)) + if data.get("backoff"): + time.sleep(int(data["backoff"]) + 1) + return data + + +def main() -> None: + key = load_key() + q = get(f"/questions/{CANONICAL_ID}", {"filter": "withbody"}, key)["items"][0] + ans = get(f"/questions/{CANONICAL_ID}/answers", + {"filter": "withbody", "sort": "votes", "order": "desc", "pagesize": 100}, key) + + pairs = json.loads(DUP_PAIRS.read_text()) + dup_ids = sorted({d for d, t in pairs if t == CANONICAL_ID}) + + def norm(item: dict) -> dict: + owner = item.get("owner", {}) or {} + return { + "answer_id": item["answer_id"], + "question_id": CANONICAL_ID, + "score": item.get("score"), + "is_accepted": int(bool(item.get("is_accepted"))), + "creation_date": item.get("creation_date"), + "owner_user_id": owner.get("user_id"), + "owner_display_name": owner.get("display_name"), + "body_html": item.get("body", ""), + "body_text": clean(item.get("body", "")), + } + + fixture = { + "_meta": { + "canonical_id": CANONICAL_ID, + "note": "out-of-window (2019) canonical pulled probe-local; dups live in main DB", + "source": "StackExchange API 2.3, CC-BY-SA", + }, + "canonical_question": { + "question_id": q["question_id"], + "title": q["title"], + "score": q.get("score"), + "answer_count": q.get("answer_count"), + "creation_date": q.get("creation_date"), + "tags": q.get("tags", []), + "link": q.get("link"), + "body_html": q.get("body", ""), + "body_text": clean(q.get("body", "")), + }, + "canonical_answers": [norm(a) for a in ans["items"]], + "dup_question_ids": dup_ids, + } + OUT.parent.mkdir(parents=True, exist_ok=True) + OUT.write_text(json.dumps(fixture, ensure_ascii=False, indent=2)) + print(f"quota_remaining={ans.get('quota_remaining')}") + print(f"canonical answers fetched: {len(fixture['canonical_answers'])}") + print(f"dup_question_ids stored: {len(dup_ids)}") + print(f"written: {OUT.relative_to(ROOT)}") + + +if __name__ == "__main__": + main() diff --git a/src/module_d/probe_d/llm.py b/src/module_d/probe_d/llm.py new file mode 100644 index 0000000..1cff614 --- /dev/null +++ b/src/module_d/probe_d/llm.py @@ -0,0 +1,110 @@ +"""The Q2 provider-neutral seam — currently with an OpenAI-strict adapter only. + +Call sites use `llm_call(messages, schema, tier, ...)` and never touch OpenAI directly. +Swapping provider = rewrite this file's adapter, not the call sites (PLAN ▼Q2). +We embrace OpenAI strict Structured Outputs: the parsed object is schema-guaranteed, +so there is no parse/retry loop here. +""" +import os +from pathlib import Path +from typing import Type, TypeVar + +from openai import BadRequestError, OpenAI +from pydantic import BaseModel + +ROOT = Path(__file__).resolve().parents[3] + +# Semantic tier -> concrete model id. tier is portable; this mapping is provider-specific. +# G12 TODO: pin dated snapshots (e.g. gpt-5.4-mini-2026-xx) + record resolved comp.model. +TIER_MODEL = { + "gate": "gpt-5.4-nano", + "extract": "gpt-5.4-mini", + "aggregate": "gpt-5.4", + "narrative": "gpt-5.4", +} + +_NO_TEMP: set[str] = set() # tiers empirically found to reject a custom temperature +T = TypeVar("T", bound=BaseModel) + +# Running token tally (G12/cost visibility). Prices are USD per 1M tokens. +USAGE = {"calls": 0, "input": 0, "output": 0} +PRICE = { # tier -> (input, output) + "gate": (0.20, 1.20), # gpt-5.4-nano (approx) + "extract": (0.75, 4.50), # gpt-5.4-mini + "aggregate": (2.50, 12.00), # gpt-5.4 (approx) + "narrative": (2.50, 12.00), +} + + +def usage_report() -> str: + pin, pout = PRICE["extract"] # probe is extract-only; refine when mixing tiers + cost = USAGE["input"] / 1e6 * pin + USAGE["output"] / 1e6 * pout + return (f"LLM usage: {USAGE['calls']} calls | {USAGE['input']:,} in + " + f"{USAGE['output']:,} out tok | ~${cost:.4f} @ extract price") + + +def _load_key() -> str: + key = os.environ.get("OPENAI_API_KEY") + if key: + return key + for line in (ROOT / ".env").read_text().splitlines(): + if line.startswith("OPENAI_API_KEY"): + return line.split("=", 1)[1].strip() + raise SystemExit("OPENAI_API_KEY not found (env or .env)") + + +_client = OpenAI(api_key=_load_key()) + + +def _parse(**kwargs): + # parse() graduated out of .beta in recent SDKs; fall back just in case. + try: + return _client.chat.completions.parse(**kwargs) + except AttributeError: + return _client.beta.chat.completions.parse(**kwargs) + + +def llm_call( + messages: list[dict], + schema: Type[T], + tier: str, + k_voting: int = 1, + temperature: float = 0.0, +) -> T | list[T]: + """Return a schema-validated instance (k_voting=1) or list of them (k_voting>1).""" + model = TIER_MODEL[tier] + + def one() -> T: + kwargs = dict(model=model, messages=messages, response_format=schema) + if tier not in _NO_TEMP: + kwargs["temperature"] = temperature + try: + comp = _parse(**kwargs) + except BadRequestError as e: + if "temperature" in str(e).lower(): + _NO_TEMP.add(tier) # this model only allows default temp; drop and retry + kwargs.pop("temperature", None) + comp = _parse(**kwargs) + else: + raise + if comp.usage: + USAGE["calls"] += 1 + USAGE["input"] += comp.usage.prompt_tokens + USAGE["output"] += comp.usage.completion_tokens + msg = comp.choices[0].message + if getattr(msg, "refusal", None): + raise RuntimeError(f"model refused: {msg.refusal}") + return msg.parsed + + return one() if k_voting == 1 else [one() for _ in range(k_voting)] + + +def embed(texts: list[str], model: str = "text-embedding-3-small") -> list[list[float]]: + """Probe stand-in for the spec's local SBERT companion embedder. + + NOTE: the real companion (ADR 0002) must use local SBERT so it is independent of the + LLM provider (reproducibility floor / drift check). This OpenAI embedder is a probe + shortcut to avoid a heavy torch install; swap is one function. + """ + resp = _client.embeddings.create(model=model, input=texts) + return [d.embedding for d in resp.data] diff --git a/src/module_d/probe_d/prompts.py b/src/module_d/probe_d/prompts.py new file mode 100644 index 0000000..da8dcbe --- /dev/null +++ b/src/module_d/probe_d/prompts.py @@ -0,0 +1,460 @@ +"""Extraction prompt (Step-1) for Probe D-1. + +Framing: the query is an open implementation problem; we surface the community's practices. +Extract EVERY distinct implementation practice an answer proposes (0..N) — an answer that +recommends two approaches is two votes. Pure-explanation / "me too" answers yield []. + +conditions is tightened (slice v0 over-generated): capture ONLY scope caveats the answer +literally states; never infer "when this is useful". +""" +import json + +_SYSTEM = """\ +You read ONE Stack Overflow answer to a React implementation problem and extract the +implementation practices it proposes. + +A *practice* is a concrete action/approach the answer recommends the developer take to +solve the problem — NOT a sentiment or an explanation of root cause. Stack Overflow answers +propose actions; capture each distinct action. + +Output: +- reasoning: 1-2 sentences of your own thinking before committing. Not stored. +- practices: a list with ONE entry per DISTINCT implementation practice the answer proposes. + - Return [] (empty) if the answer proposes no implementation practice — e.g. it only + explains WHY the problem happens, says "I have the same issue", says thanks, or asks a + clarifying question. A pure root-cause explanation with no recommended action is []. + - If the answer recommends two genuinely different approaches (e.g. "use the functional + updater" AND "use useEffect"), return BOTH as separate entries, primary first. + Each entry: + - practice: ONE declarative sentence stating the recommended action/approach. Phrase + similar recommendations similarly and genuinely different ones differently. + - conditions: 0-3 SHORT caveats that the answer EXPLICITLY states about when this + practice applies (e.g. "only for local state", "only in React 18+", "only if the + mismatch is intentional"). Use [] unless the answer literally states such a caveat. + NEVER infer or invent a condition; do not restate "when this is useful". + - evidence_type: "code" if this practice is shown essentially as a code snippet with + little prose, "prose" if explained with NO code block, "both" if it explains AND shows + code. A code block IS valid practice evidence. + +Be faithful to the answer; do not add your own React knowledge.""" + +# FS1: the accepted answer proposes TWO practices -> list of 2, no stated conditions. +_FS1_INPUT = """\ +Question: The useState set method is not reflecting a change immediately +Answer: setState is asynchronous, so the variable still holds the old value right after you \ +call the setter. Use the functional update form so you build off the latest state: +``` +setCount(prev => prev + 1) +``` +And if you need to run something once the value has actually changed, do it in a useEffect \ +that lists that state as a dependency.""" +_FS1_OUTPUT = { + "reasoning": "Two distinct actions: functional updater, and useEffect for post-update logic. No stated scope caveats.", + "practices": [ + { + "practice": "Use the functional updater form of the state setter (setState(prev => ...)) to build off the latest state instead of the stale value.", + "conditions": [], + "evidence_type": "both", + }, + { + "practice": "Run logic that depends on the updated value inside a useEffect that lists that state as a dependency.", + "conditions": [], + "evidence_type": "prose", + }, + ], +} + +# FS2: a "me too" non-answer -> empty list. +_FS2_INPUT = """\ +Question: The useState set method is not reflecting a change immediately +Answer: I'm hitting exactly the same thing on React 18. Did anyone find a clean fix? \ +Following this thread.""" +_FS2_OUTPUT = { + "reasoning": "No recommendation — a 'me too' follow request.", + "practices": [], +} + +# FS3: a single practice WITH a genuinely stated condition (teaches the conditions field). +_FS3_INPUT = """\ +Question: React hydration warning: Extra attributes from the server +Answer: You can add suppressHydrationWarning to silence it, but only do this when the \ +mismatch is intentional (e.g. a server-rendered timestamp). If it's an unexpected mismatch, \ +fix the underlying difference instead.""" +_FS3_OUTPUT = { + "reasoning": "One action (suppressHydrationWarning) with an explicit caveat stated by the answer.", + "practices": [ + { + "practice": "Add suppressHydrationWarning to silence the hydration mismatch warning.", + "conditions": ["only when the mismatch is intentional"], + "evidence_type": "prose", + } + ], +} + + +_AGG_SYSTEM = """\ +You are given a numbered list of React implementation practices, each extracted from a +different Stack Overflow answer to the SAME underlying problem. Group them into clusters +where each cluster represents the SAME underlying approach/recommendation, regardless of +wording. Genuinely different approaches go in different clusters. A practice with no sibling +may be its own singleton cluster. + +Output: +- reasoning: brief thinking, not stored. +- clusters: list of {name: short label for the approach, description: <=2 sentences, + member_indices: 0-based indices into the input list}. Assign EVERY index to exactly one + cluster — do not skip or duplicate an index.""" + + +_GATE_CANON = """\ +A React developer is confused that a useState setter does not update the state immediately \ +— reading or using the state right after calling the setter shows the old value. The \ +breakdown collects the FULL LANDSCAPE of approaches the community offers for working with \ +state under this confusion. That landscape legitimately INCLUDES general state-handling \ +patterns, e.g.: reacting to the change in a useEffect; the functional updater form \ +setX(prev => ...); using a locally-computed value instead of reading state back; reading \ +the value on the next render / in the render body; useRef for an instant mutable value; \ +useReducer or context; not storing derived state (compute it / useMemo); custom state hooks \ +or callback/promise wrappers; setting state directly. ALL of these count as relevant.""" + +_GATE_SYSTEM = """\ +You curate a community "practice breakdown" for ONE canonical problem (below). For each +candidate practice decide two INDEPENDENT axes: + +- relevant: TRUE if it is any approach a React developer would use to WORK WITH STATE given + this confusion — INCLUDING the general state-handling patterns listed in the problem + statement. Mark FALSE ONLY when the practice addresses a DIFFERENT React subsystem the + asker merely also touched: controlled form inputs, event bubbling / stopPropagation, form + or date validation, routing, CSS / styling, parsing one specific API response shape, + environment config, or dev tooling. When unsure about a state-handling practice, mark TRUE. + +- substantive: TRUE for a standalone, generalizable recommendation. FALSE ONLY for a + content-free placeholder that just points back at the answer ("use the approach shown", + "use the provided version", "pick either of the two approaches", "change the function to + the provided version") or a fix that names specific local identifiers with no general + lesson. + +STRONG bias to KEEP (both TRUE). Niche / advanced / unpopular but on-topic approaches MUST +be kept. Output one verdict per input practice, echoing its practice_index.""" + +_GATE_FS_INPUT = ( + "Canonical problem:\n" + _GATE_CANON + "\n\nPractices:\n" + "0: Run logic that depends on the updated value inside a useEffect that lists that state as a dependency.\n" + "1: Use the functional updater form setX(prev => ...) so each update builds on the latest state.\n" + "2: Rewrite the state management to use useReducer with a shared context provider.\n" + "3: Keep the input controlled by setting its value from state.\n" + "4: Use the first code-based approach shown in the answer.\n" + "5: Use a ref to read the latest value without waiting for a re-render.\n" + "6: Call stopPropagation on the click event to prevent it from bubbling.\n" + "7: Remove the unnecessary squares[a] !== null check from the match function." +) +_GATE_FS_OUTPUT = { + "verdicts": [ + {"reason": "Core approach: react to the updated value in an effect.", + "practice_index": 0, "relevant": True, "substantive": True}, + {"reason": "Functional updater — a central state-handling pattern, relevant.", + "practice_index": 1, "relevant": True, "substantive": True}, + {"reason": "useReducer/context is a legitimate state-restructuring approach, relevant.", + "practice_index": 2, "relevant": True, "substantive": True}, + {"reason": "About controlled form inputs — a different subsystem that co-occurred.", + "practice_index": 3, "relevant": False, "substantive": True}, + {"reason": "Content-free placeholder pointing at the answer's code.", + "practice_index": 4, "relevant": True, "substantive": False}, + {"reason": "useRef for the latest value — on-topic, keep even if niche.", + "practice_index": 5, "relevant": True, "substantive": True}, + {"reason": "Event bubbling / stopPropagation — different subsystem.", + "practice_index": 6, "relevant": False, "substantive": True}, + {"reason": "One-off fix naming a specific match function; no general lesson.", + "practice_index": 7, "relevant": False, "substantive": False}, + ] +} + + +def build_gate_messages(practices: list[str]) -> list[dict]: + listing = "\n".join(f"{i}: {p}" for i, p in enumerate(practices)) + return [ + {"role": "system", "content": _GATE_SYSTEM}, + {"role": "user", "content": _GATE_FS_INPUT}, + {"role": "assistant", "content": json.dumps(_GATE_FS_OUTPUT, ensure_ascii=False)}, + {"role": "user", "content": "Canonical problem:\n" + _GATE_CANON + "\n\nPractices:\n" + listing}, + ] + + +def build_aggregator_messages(practices: list[str]) -> list[dict]: + listing = "\n".join(f"{i}: {p}" for i, p in enumerate(practices)) + return [ + {"role": "system", "content": _AGG_SYSTEM}, + {"role": "user", "content": "Practices:\n" + listing}, + ] + + +def build_extraction_messages(question_title: str, answer_text: str) -> list[dict]: + return [ + {"role": "system", "content": _SYSTEM}, + {"role": "user", "content": _FS1_INPUT}, + {"role": "assistant", "content": json.dumps(_FS1_OUTPUT, ensure_ascii=False)}, + {"role": "user", "content": _FS2_INPUT}, + {"role": "assistant", "content": json.dumps(_FS2_OUTPUT, ensure_ascii=False)}, + {"role": "user", "content": _FS3_INPUT}, + {"role": "assistant", "content": json.dumps(_FS3_OUTPUT, ensure_ascii=False)}, + {"role": "user", "content": f"Question: {question_title}\nAnswer: {answer_text}"}, + ] + + +# ── Step-1 + Step-1b merged: extract AND gate in one call ────────────────────────────────── +# Reuses the exact validated language of _SYSTEM (extraction) + _GATE_SYSTEM (the two gate +# axes). The order instruction ("extract first, then judge each") + the schema field order +# (practice before the gate fields) jointly enforce extract-generously-then-gate. +CANONICAL_PROBLEM = _GATE_CANON # public alias for callers (the probe's fixed canonical problem) + +_EXTRACT_GATE_SYSTEM = """\ +You read ONE Stack Overflow answer to a React implementation problem and do TWO things in a +SINGLE pass: (1) extract the implementation practices the answer proposes, then (2) for each +extracted practice, judge whether it belongs in the breakdown for THE CANONICAL PROBLEM +stated below. + +Do (1) FIRST and generously, exactly as a faithful extractor would; only AFTER writing a +practice do you judge it. NEVER silently skip a practice because you suspect it is off-topic — +always extract it, then mark it. This keeps niche / minority approaches visible. + +(1) EXTRACTION +A *practice* is a concrete action/approach the answer recommends the developer take to solve +the problem — NOT a sentiment or an explanation of root cause. For each DISTINCT practice: + - practice: ONE declarative sentence stating the recommended action/approach. PRESERVE the + answer's specificity — keep the scope the answer actually names (e.g. "when updating a Map", + "the referenced data") instead of flattening it into a bland generic sentence. Phrase similar + recommendations similarly and genuinely different ones differently. + - conditions: 0-3 SHORT caveats the answer EXPLICITLY states about when this practice applies + (e.g. "only for local state", "only in React 18+"). Use [] unless the answer literally + states such a caveat. NEVER infer or invent a condition; do not restate "when this is useful". + (Do NOT classify evidence_type — whether the answer shows code is detected deterministically + in Python, not by you.) +Return practices=[] if the answer proposes no implementation practice — e.g. it only explains +WHY the problem happens, says "me too", says thanks, or asks a clarifying question. If it +recommends two genuinely different approaches, return BOTH (primary first). Be faithful to the +answer; do not add your own React knowledge. + +(2) PER-PRACTICE GATE — judge each practice against the CANONICAL PROBLEM below (NOT the +answer's own question), on two INDEPENDENT axes. Judge the PRACTICE AS STATED — never let the +source answer's terseness or missing code lower your verdict: + - gate_reason: 1 short sentence justifying the two booleans. Write it BEFORE them. + - relevant: TRUE if it is any approach a React developer would use to address the canonical + problem — INCLUDING the patterns in the canonical problem statement AND immutable array/ + object updates (creating a NEW array/object / deep copy instead of mutating, so React sees a + new reference). Mark FALSE ONLY when the practice addresses a DIFFERENT React subsystem the + asker merely also touched: controlled form inputs, event bubbling / stopPropagation, form or + date validation, routing, CSS / styling, parsing one specific API response shape, environment + config, or dev tooling. When unsure about an on-topic practice, mark TRUE. + - substantive: judge the PRACTICE itself, NOT how much the answer explained it. A recognizable, + reusable React state technique — e.g. the functional updater form, reacting in a useEffect, + using a ref, immutable updates, useReducer, reading on the next render — is substantive EVEN + IF the source answer is terse or shows no code. Mark FALSE ONLY for: (a) a content-free + placeholder that just points back at the answer ("use the approach shown", "use the provided + version", "try this", "change the function to the provided version"), or (b) a fix that names + specific local identifiers with no general lesson. +STRONG bias to KEEP (both TRUE). Niche / advanced / unpopular but on-topic approaches MUST be +kept; a well-known majority pattern is ALWAYS substantive.""" + + +def _eg_user(canon: str, title: str, answer: str) -> str: + return f"Canonical problem:\n{canon}\n\nQuestion: {title}\nAnswer: {answer}" + + +# FS_A: two on-topic state practices -> both extracted, both kept (relevant+substantive). +_EG_FSA_OUTPUT = { + "reasoning": "Two distinct on-topic state-handling actions: functional updater and a useEffect; no stated caveats.", + "practices": [ + {"practice": "Use the functional updater form of the state setter (setState(prev => ...)) to build off the latest state instead of the stale value.", + "conditions": [], + "gate_reason": "Functional updater is a central state-handling pattern for this confusion.", + "relevant": True, "substantive": True}, + {"practice": "Run logic that depends on the updated value inside a useEffect that lists that state as a dependency.", + "conditions": [], + "gate_reason": "Reacting to the updated value in an effect is a core approach to the problem.", + "relevant": True, "substantive": True}, + ], +} + +# FS_B: a "me too" non-answer -> empty list (nothing to gate). +_EG_FSB_OUTPUT = {"reasoning": "No recommendation — a 'me too' follow request.", "practices": []} + +# FS_C: mixed — on-topic (keep) + different-subsystem (relevant=False) + placeholder (substantive=False). +_EG_FSC_ANSWER = """\ +You can read the latest value through a ref instead of waiting for the next render: +``` +const latest = useRef(count); +latest.current = count; +``` +Also make sure to call stopPropagation on the button's onClick so the outer handler does not \ +fire too. If neither helps, just use the version shown above.""" +_EG_FSC_OUTPUT = { + "reasoning": "Three actions: a ref to read latest state (on-topic), stopPropagation (different subsystem), and a placeholder pointer.", + "practices": [ + {"practice": "Use a ref to hold and read the latest value without waiting for a re-render.", + "conditions": [], + "gate_reason": "useRef for the latest value is an on-topic state-handling approach; keep even if niche.", + "relevant": True, "substantive": True}, + {"practice": "Call stopPropagation on the click event so the outer handler does not also fire.", + "conditions": [], + "gate_reason": "Event bubbling / stopPropagation is a different subsystem the asker merely also touched.", + "relevant": False, "substantive": True}, + {"practice": "Use the version shown above.", + "conditions": [], + "gate_reason": "Content-free placeholder that just points back at the answer's code.", + "relevant": True, "substantive": False}, + ], +} + +# FS_D: a TERSE answer naming real techniques -> both KEPT. Teaches that substantive judges the +# PRACTICE, not the answer's brevity, and that immutable (new-reference) updates are on-topic. +_EG_FSD_ANSWER = """\ +Just use the functional update form: setCount(c => c + 1). You can also make a deep copy of the \ +array before setting it so React sees a new reference.""" +_EG_FSD_OUTPUT = { + "reasoning": "Two real techniques named tersely: the functional updater and an immutable (new-reference) update.", + "practices": [ + {"practice": "Use the functional updater form of the state setter (setCount(prev => ...)) so each update builds on the latest value.", + "conditions": [], + "gate_reason": "Functional updater is a recognizable general state pattern — substantive even though the answer is terse.", + "relevant": True, "substantive": True}, + {"practice": "Make a deep copy / new array before setting state so React sees a new reference.", + "conditions": [], + "gate_reason": "Creating a new reference (immutable update) is an on-topic state-handling approach; substantive.", + "relevant": True, "substantive": True}, + ], +} + + +# ── Step-4: per-query narrative (D-4) ────────────────────────────────────────────────────── +# The narrative is a pure function (signal table -> prose). It narrates ONE query's RQ2/RQ3/RQ4 +# (spec §1.2) from precomputed numbers only — never invents a pattern, never adds React lore. +# Two enums carry honesty guards: temporal.insufficient_coverage (no trajectory on thin years) +# and authority_alignment.inconclusive (no authority claim on flat votes). +_NARR_SYSTEM = """\ +You are given a precomputed SIGNAL TABLE describing how a developer community's answers to ONE +React implementation problem break down into approach clusters. Write a short narrative of the +SHAPE of that breakdown. Report ONLY what the numbers show — never invent a pattern that is not +in the table, and never add outside React knowledge. + +Decide three labels about THIS query (spec research questions), each strictly from the table: + +- shape (RQ2) — compare the largest cluster's share and its size against the runner-up (the + boundary is a spectrum, so there are three bands; always STATE the leader's share in the body): + "convergent" = one approach clearly leads — it holds a near-majority of practices OR is at + least ~2x the size of the runner-up; + "divergent" = no clear leader — the top two approaches are comparable (largest under ~1.5x + the runner-up); + "mixed" = the largest leads but not decisively (between those two bands) — say so plainly. +- authority_alignment (RQ3) — compare peak_vote across clusters, and note `answers` (how many + INDEPENDENT answers back a cluster; the same high-vote answer can span several clusters, so a big + peak from few distinct answers is one loud voice, not broad endorsement): + "aligned" = the highest community votes sit on the leading approach; + "contrarian" = the top-voted answer(s) back a NON-leading approach; + "dispersed" = high authority is spread across several approaches; + "inconclusive"= votes are too flat or low to say anything. +- temporal (RQ4) — judge coverage ONLY from ANSWERS BY YEAR and the CROSS-YEAR COVERAGE line, and + NEVER from any per-cluster median_yr. If most answers fall in one or two years (CROSS-YEAR + COVERAGE high), you MUST output "insufficient_coverage" and make NO trajectory claim — even if a + handful of older answers exist in earlier years (they do not constitute coverage). Only when + answers are genuinely spread across several well-populated years may you claim a trajectory: + "settled" (one approach steady across years) / "contested" (rival approaches persist across years) + / "converging" / "diverging". Do not manufacture a trend from a lopsided year distribution. + +Then write: +- dominant_approach: copy the EXACT name of the leading cluster. +- headline: ONE punchy line, <= 14 words, capturing the breakdown's shape. +- body: 2-3 sentences. (1) the mainstream — dominant_approach, how dominant by share, how unanimous + via agreement. (2) the shape of divergence — clean winner vs genuine split, the main rival + approach(es), how long the tail is. (3) OPTIONAL — the authority or temporal finding, but ONLY if + it is real per the rules above; if neither is, stop at two sentences. Stating that coverage is too + thin to read a trend is allowed (it is honesty, not a trajectory claim). +- reasoning: think through the table first (not stored). Be concrete; name approaches. Never assert + an authority or temporal pattern the table does not support.""" + +# FS_N1: convergent + authority aligned + thin years -> insufficient_coverage (teaches the guard). +_NARR_FS1_INPUT = """\ +QUERY: How do I debounce a value/input in React? +TOTALS: 60 practices · 50 answers · 4 head clusters + 5 long-tail · mean within-cluster agreement 0.95 +ANSWERS BY YEAR: 2022:1 2023:40 2024:9 +CROSS-YEAR COVERAGE: 98% of answers in the two busiest years (2023, 2024); 1 of 50 elsewhere. + +APPROACH CLUSTERS (largest first): + size share agree answers peak_vote median_yr approach + 35 58% 0.97 30 420 2023 useEffect + setTimeout debounce + 15 25% 0.93 13 180 2023 custom useDebounce hook + 8 13% 1.00 6 95 2023 lodash.debounce + 2 3% 1.00 2 12 2023 external debounce library +LONG TAIL: 5 idiosyncratic single-practice approaches""" +_NARR_FS1_OUTPUT = { + "reasoning": "useEffect+setTimeout owns 35/60 (58%) and is >2x the runner-up (15) -> convergent; the top " + "vote (420) sits on it -> aligned. CROSS-YEAR COVERAGE is 98% in 2023-2024 -> too thin for a trajectory.", + "shape": "convergent", + "dominant_approach": "useEffect + setTimeout debounce", + "authority_alignment": "aligned", + "temporal": "insufficient_coverage", + "headline": "One timer pattern dominates; a custom hook trails", + "body": "The community converges on a useEffect-plus-setTimeout debounce — 35 of 60 practices, " + "near-unanimous within the group (0.97) — and the highest-voted answers sit on it. A custom " + "useDebounce hook is the main alternative (15 practices), with smaller lodash.debounce and " + "external-library camps and a thin idiosyncratic tail. The answers fall almost entirely in " + "2023, so no temporal trajectory can be read.", +} + +# FS_N2: genuine split + top vote on the NON-largest camp -> divergent + contrarian; even years -> contested. +_NARR_FS2_INPUT = """\ +QUERY: Should I manage shared app state with Context or Redux? +TOTALS: 40 practices · 38 answers · 3 head clusters + 4 long-tail · mean within-cluster agreement 0.91 +ANSWERS BY YEAR: 2019:8 2020:9 2021:9 2022:7 2023:5 +CROSS-YEAR COVERAGE: 47% of answers in the two busiest years (2020, 2021); 20 of 38 elsewhere. + +APPROACH CLUSTERS (largest first): + size share agree answers peak_vote median_yr approach + 16 40% 0.92 16 300 2021 Context + useReducer for app state + 14 35% 0.95 14 650 2020 Redux Toolkit for large/shared state + 6 15% 0.88 6 90 2022 lightweight stores (Zustand/Jotai) +LONG TAIL: 4 idiosyncratic single-practice approaches""" +_NARR_FS2_OUTPUT = { + "reasoning": "16 vs 14 is near-even (largest only ~1.1x the runner-up) -> divergent, no clear leader. The " + "single biggest vote (650) is on Redux, the second-largest camp, not the largest -> contrarian. " + "CROSS-YEAR COVERAGE is only 47% in the busiest two years, answers spread across 2019-2023 and " + "both camps persist -> contested, a real call since coverage is broad.", + "shape": "divergent", + "dominant_approach": "Context + useReducer for app state", + "authority_alignment": "contrarian", + "temporal": "contested", + "headline": "A genuine split: Context vs Redux, no clear winner", + "body": "The field is genuinely divided — Context-plus-useReducer (16 practices) and Redux Toolkit (14) " + "carry near-equal weight, with a smaller modern-lightweight-store camp. Authority leans against " + "headcount: the single most up-voted answer (650) backs Redux rather than the marginally larger " + "Context camp. Both approaches appear steadily across 2019-2023 — a long-standing contested choice, " + "not one converging on a winner.", +} + + +def build_narrative_messages(signal_table: str) -> list[dict]: + return [ + {"role": "system", "content": _NARR_SYSTEM}, + {"role": "user", "content": _NARR_FS1_INPUT}, + {"role": "assistant", "content": json.dumps(_NARR_FS1_OUTPUT, ensure_ascii=False)}, + {"role": "user", "content": _NARR_FS2_INPUT}, + {"role": "assistant", "content": json.dumps(_NARR_FS2_OUTPUT, ensure_ascii=False)}, + {"role": "user", "content": signal_table}, + ] + + +def build_extraction_gated_messages(canonical_problem: str, question_title: str, + answer_text: str) -> list[dict]: + q = "The useState set method is not reflecting a change immediately" + qd = "State does not update right after setState" + return [ + {"role": "system", "content": _EXTRACT_GATE_SYSTEM}, + {"role": "user", "content": _eg_user(_GATE_CANON, q, _FS1_INPUT.split("Answer: ", 1)[1])}, + {"role": "assistant", "content": json.dumps(_EG_FSA_OUTPUT, ensure_ascii=False)}, + {"role": "user", "content": _eg_user(_GATE_CANON, q, _FS2_INPUT.split("Answer: ", 1)[1])}, + {"role": "assistant", "content": json.dumps(_EG_FSB_OUTPUT, ensure_ascii=False)}, + {"role": "user", "content": _eg_user(_GATE_CANON, q, _EG_FSC_ANSWER)}, + {"role": "assistant", "content": json.dumps(_EG_FSC_OUTPUT, ensure_ascii=False)}, + {"role": "user", "content": _eg_user(_GATE_CANON, qd, _EG_FSD_ANSWER)}, + {"role": "assistant", "content": json.dumps(_EG_FSD_OUTPUT, ensure_ascii=False)}, + {"role": "user", "content": _eg_user(canonical_problem, question_title, answer_text)}, + ] diff --git a/src/module_d/probe_d/schemas.py b/src/module_d/probe_d/schemas.py new file mode 100644 index 0000000..243abc3 --- /dev/null +++ b/src/module_d/probe_d/schemas.py @@ -0,0 +1,110 @@ +"""Pydantic schemas for Module D LLM roles (OpenAI strict structured outputs). + +Strict-mode rules (PLAN ▼Q2): all fields required, additionalProperties:false (SDK sets +it), no defaults. `reasoning` is first so CoT is generated before the answer fields. +Cardinality (conditions ≤ 3) is enforced in Python post-hoc, NOT in the schema — it is the +least portable strict keyword (PLAN ▼Q2). + +Framing (grill 2026-05-29): a query is an open implementation problem; the output is the +set of community practices for it. So one answer may propose several practices (votes for +several approaches) — hence `practices` is a list (M3). Empty list = the answer proposes no +implementation practice (pure "why" explanation / "me too" / thanks). Pure-diagnosis +answers are intentionally excluded: diagnosis convergence is correctness, not the +consensus-practice signal. +""" +from typing import Literal + +from pydantic import BaseModel + + +class Practice(BaseModel): + """One implementation practice (recommended action/approach) proposed by an answer.""" + + practice: str # 1 declarative sentence: the recommended action/approach + conditions: list[str] # ONLY scope caveats the answer explicitly states; [] otherwise. + evidence_type: Literal["prose", "code", "both"] # how THIS practice is evidenced + + +class AnswerExtraction(BaseModel): + """Step-1 output for one SO answer (ADR 0002, CONTEXT.md).""" + + reasoning: str # brief CoT; generated first, not persisted + practices: list[Practice] # 0..N; empty = answer proposes no implementation practice + + +class PracticeVerdict(BaseModel): + """Gate verdict for ONE candidate practice (reason first so CoT precedes the booleans).""" + + reason: str # brief justification; not persisted + practice_index: int # echoes the 0-based input index + relevant: bool # is it an approach to THE canonical problem (vs a different React issue)? + substantive: bool # standalone & generalizable (not a placeholder / one-off per-case fix)? + + +class RelevanceGate(BaseModel): + """Step-1b: per-practice relevance + substance gate for one answer (D-owned, complements C).""" + + verdicts: list[PracticeVerdict] # one per input practice, any order (mapped by practice_index) + + +class ExtractedPractice(BaseModel): + """Step-1+1b merged: one extracted practice carrying its inline gate verdict. + + Field order is deliberate: the extraction fields (practice/conditions) come BEFORE the gate + fields so the model commits the practice first, THEN judges it — extract generously, gate + second (protects minority-approach recall). gate_reason precedes the two booleans so its CoT + is generated before the verdict. NOTE: evidence_type is NOT judged here — it is a structural + property of the source answer (does it contain a code block?), computed deterministically in + Python at answer level (see d1_extract_gated.evidence_type), not by the LLM. + """ + + practice: str # 1 declarative sentence: the recommended action/approach + conditions: list[str] # ONLY scope caveats the answer explicitly states; [] otherwise. + gate_reason: str # brief justification for the two booleans; written before them, not persisted + relevant: bool # approach to THE canonical problem (vs a different React subsystem)? + substantive: bool # standalone & generalizable (not a placeholder / one-off per-case fix)? + + +class AnswerExtractionGated(BaseModel): + """Step-1+1b merged output for one SO answer: extract practices AND gate them in one call. + + Kept practice = relevant AND substantive (deterministic post-filter in Python, so the drop + stays auditable). Empty list = answer proposes no implementation practice. + """ + + reasoning: str # brief extraction CoT; generated first, not persisted + practices: list[ExtractedPractice] # 0..N, each with its inline gate verdict + + +class Cluster(BaseModel): + """A group of practices representing the same underlying approach (Step-2).""" + + name: str # short label for the approach + description: str # <= 2 sentences + member_indices: list[int] # 0-based indices into the input practice list + + +class AggregatorOutput(BaseModel): + """Step-2: LLM aggregator clusters the practices of one canonical group (ADR 0002).""" + + reasoning: str # brief CoT; not persisted + clusters: list[Cluster] + + +class QueryNarrative(BaseModel): + """D-4: per-query narrative of the practice-breakdown shape — one query's RQ2/RQ3/RQ4 in prose. + + A pure function of the precomputed signal table: report what the numbers show, never invent. + The enum fields are machine-readable RQ verdicts the prose must stay consistent with; two of + them carry built-in honesty guards — `temporal.insufficient_coverage` (RQ4 only claims a + trajectory when cross-year coverage supports it) and `authority_alignment.inconclusive` (no + authority claim when votes are flat). reasoning first (CoT before commitments); strict. + """ + + reasoning: str # CoT over the signal table; weigh size vs authority vs years before labeling. Not persisted. + shape: Literal["convergent", "mixed", "divergent"] # RQ2: leader dominates / leads-but-not-decisively / no clear leader + dominant_approach: str # RQ2: name of the leading (largest) cluster, even when shape is mixed/divergent + authority_alignment: Literal["aligned", "contrarian", "dispersed", "inconclusive"] # RQ3 + temporal: Literal["settled", "contested", "converging", "diverging", "insufficient_coverage"] # RQ4 (coverage-gated) + headline: str # one-line punchline (dashboard hero text) + body: str # 2-3 sentence prose synthesis, consistent with the labels above diff --git a/src/module_d/providers/__init__.py b/src/module_d/providers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/module_d/providers/so_proxy.py b/src/module_d/providers/so_proxy.py new file mode 100644 index 0000000..0289208 --- /dev/null +++ b/src/module_d/providers/so_proxy.py @@ -0,0 +1,75 @@ +"""SO-data proxy implementations of the Module B and Module C seams. + +Everything here is the PRE-DIFFERENTIATOR stand-in: authority is a vote proxy (age-confounded — +older answers accrue votes), grouping is SO's own duplicate links (not the real canonical +RAG+gate). Both are meant to be deleted wholesale once the real providers land — nothing else in +Module D imports this file, so the swap is local. +""" +from __future__ import annotations + +import json +import math +import sqlite3 +from pathlib import Path + +from module_d.ports import Answer, CanonicalGroup + + +class VoteAuthorityProxy: + """Module B seam, proxied. authority ≈ log1p(vote) + accepted bonus, with a floor so every + answer stays visible. Vote-primary on purpose (raw vote is power-law and age-confounded); the + exact blend is irrelevant because it lives entirely behind this one method. Swap for the + yearly-PageRank lookup and the dashboard never notices.""" + + source_name = "so_proxy" + + def __init__(self, accepted_bonus: float = 0.5, floor: float = 0.5): + self.accepted_bonus = accepted_bonus + self.floor = floor + + def score(self, a: Answer) -> float: + base = math.log1p(max(a.vote, 0)) + if a.is_accepted: + base += self.accepted_bonus + return base + self.floor + + +class DuplicateChainGroups: + """Module C seam, proxied: one canonical group == one SO duplicate chain. + + Canonical (out-of-window) answers come from the probe snapshot JSON; in-window dup answers and + author reputation come from the main DB. Swap for Module C's `query → canonical_group`. + """ + + source_name = "so_duplicate_links" + + def __init__(self, snapshot: Path, db_path: Path): + self.snapshot = Path(snapshot) + self.db_path = Path(db_path) + + def fetch(self, group_id: str) -> CanonicalGroup: + snap = json.loads(self.snapshot.read_text()) + con = sqlite3.connect(self.db_path) + try: + reputation = dict(con.execute("SELECT user_id, reputation FROM users")) + answers = [ + Answer(a["answer_id"], a["score"], bool(a["is_accepted"]), "canonical", + a["creation_date"], reputation.get(a["owner_user_id"]), + a.get("body_text"), a.get("owner_display_name")) + for a in snap["canonical_answers"] + ] + dup_qids = snap["dup_question_ids"] + placeholders = ",".join("?" * len(dup_qids)) + rows = con.execute( + f"SELECT answer_id, score, is_accepted, creation_date, owner_user_id, " + f"body_text, owner_display_name " + f"FROM answers WHERE question_id IN ({placeholders})", + dup_qids, + ) + answers += [ + Answer(aid, score, bool(acc), "dup", cdate, reputation.get(owner), body, author) + for aid, score, acc, cdate, owner, body, author in rows + ] + finally: + con.close() + return CanonicalGroup(group_id, answers) diff --git a/uv.lock b/uv.lock index df6e33f..9a62cd6 100644 --- a/uv.lock +++ b/uv.lock @@ -2,7 +2,1049 @@ version = 1 revision = 3 requires-python = ">=3.11" +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, +] + +[[package]] +name = "certifi" +version = "2026.5.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/ce/ee2ecad540810a79593028e88299baeae54d346cc7a0d94b6199988b89b1/certifi-2026.5.20.tar.gz", hash = "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d", size = 135422, upload-time = "2026-05-20T11:46:50.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/8c/57e832b7af6d7c5abe66eb3fbe3a3a32f4d11ea23a1aa7131371035be991/certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897", size = 134134, upload-time = "2026-05-20T11:46:48.578Z" }, +] + +[[package]] +name = "circlify" +version = "0.15.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/8f/ff3d9a37dec872ea83681627239c29c852675f6514380a50ed55982a0371/circlify-0.15.1.tar.gz", hash = "sha256:54103d82ab679128edb4cb77a0a1d8501935ca866a2dd8ae5605650508621bc9", size = 11808, upload-time = "2025-09-16T01:26:47.77Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/d8/0abcfb8629cc529e12df60e8bd73717c2588588f0744415158bde9d94ebf/circlify-0.15.1-py3-none-any.whl", hash = "sha256:5905236d60fa8e8adc6e10812392df7da274ab7454916d4a98f9a913eb474a1f", size = 11500, upload-time = "2025-09-16T01:26:46.556Z" }, +] + +[[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 = "community-consensus-practice" version = "0.1.0" source = { editable = "." } +dependencies = [ + { name = "circlify" }, + { name = "matplotlib" }, + { name = "openai" }, + { name = "pydantic" }, + { name = "scikit-learn" }, +] + +[package.metadata] +requires-dist = [ + { name = "circlify", specifier = ">=0.15.1" }, + { name = "matplotlib", specifier = ">=3.10.9" }, + { name = "openai", specifier = ">=2.38.0" }, + { name = "pydantic", specifier = ">=2.13.4" }, + { name = "scikit-learn", specifier = ">=1.8.0" }, +] + +[[package]] +name = "contourpy" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773, upload-time = "2025-07-26T12:01:02.277Z" }, + { url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149, upload-time = "2025-07-26T12:01:04.072Z" }, + { url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222, upload-time = "2025-07-26T12:01:05.688Z" }, + { url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234, upload-time = "2025-07-26T12:01:07.054Z" }, + { url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555, upload-time = "2025-07-26T12:01:08.801Z" }, + { url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238, upload-time = "2025-07-26T12:01:10.319Z" }, + { url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218, upload-time = "2025-07-26T12:01:12.659Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867, upload-time = "2025-07-26T12:01:15.533Z" }, + { url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677, upload-time = "2025-07-26T12:01:17.088Z" }, + { url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234, upload-time = "2025-07-26T12:01:18.256Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123, upload-time = "2025-07-26T12:01:19.848Z" }, + { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" }, + { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" }, + { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" }, + { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" }, + { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" }, + { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" }, + { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" }, + { url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" }, + { url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" }, + { url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672, upload-time = "2025-07-26T12:01:41.942Z" }, + { url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234, upload-time = "2025-07-26T12:01:43.499Z" }, + { url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169, upload-time = "2025-07-26T12:01:45.219Z" }, + { url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859, upload-time = "2025-07-26T12:01:46.519Z" }, + { url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062, upload-time = "2025-07-26T12:01:48.964Z" }, + { url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932, upload-time = "2025-07-26T12:01:51.979Z" }, + { url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" }, + { url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730, upload-time = "2025-07-26T12:01:57.051Z" }, + { url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897, upload-time = "2025-07-26T12:01:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751, upload-time = "2025-07-26T12:02:00.343Z" }, + { url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486, upload-time = "2025-07-26T12:02:02.128Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106, upload-time = "2025-07-26T12:02:03.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548, upload-time = "2025-07-26T12:02:05.165Z" }, + { url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297, upload-time = "2025-07-26T12:02:07.379Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023, upload-time = "2025-07-26T12:02:10.171Z" }, + { url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" }, + { url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" }, + { url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810, upload-time = "2025-07-26T12:02:18.9Z" }, + { url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871, upload-time = "2025-07-26T12:02:20.418Z" }, + { url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264, upload-time = "2025-07-26T12:02:21.916Z" }, + { url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819, upload-time = "2025-07-26T12:02:23.759Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650, upload-time = "2025-07-26T12:02:26.181Z" }, + { url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833, upload-time = "2025-07-26T12:02:28.782Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" }, + { url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" }, + { url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" }, + { url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" }, + { url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748, upload-time = "2025-07-26T12:02:37.193Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554, upload-time = "2025-07-26T12:02:38.894Z" }, + { url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118, upload-time = "2025-07-26T12:02:40.642Z" }, + { url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" }, + { url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" }, + { url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" }, + { url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" }, + { url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809, upload-time = "2025-07-26T12:02:52.74Z" }, + { url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593, upload-time = "2025-07-26T12:02:54.037Z" }, + { url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202, upload-time = "2025-07-26T12:02:55.947Z" }, + { url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207, upload-time = "2025-07-26T12:02:57.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" }, +] + +[[package]] +name = "cycler" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + +[[package]] +name = "fonttools" +version = "4.63.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/84/69/c97f2c18e0db87d2c7b15da1974dace76ae938f1cfa22e2727a648b7ed43/fonttools-4.63.0.tar.gz", hash = "sha256:caeb583deeb5168e694b65cda8b4ee62abedfa66cf88488734466f2366b9c4e0", size = 3597189, upload-time = "2026-05-14T12:04:30.958Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/2b/a7f1545bdf5da69c4bda0cea2a5781f0ad2a6623e0277267672db43c5fe6/fonttools-4.63.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2b8ae05d9eacf6081414d759c0a352769ac28ce31280d6bb8e77b03f9e3c449f", size = 2881793, upload-time = "2026-05-14T12:02:56.645Z" }, + { url = "https://files.pythonhosted.org/packages/49/50/965308c703f085f225db2886813b27e015b8b3438c350b22dd65b52c2a2c/fonttools-4.63.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:79cdc9f567aec74a72918fd060283911406750cbc9fd28c1316023deb6ce31a9", size = 2428130, upload-time = "2026-05-14T12:02:58.891Z" }, + { url = "https://files.pythonhosted.org/packages/d8/38/6937fbd7f2dc3a6b48725851bc2c15ec949b9af14d9bbcb5fe83cdf9bdf9/fonttools-4.63.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c14b4fd138c4bafcca294765c547914e1aa431ae1ca94ab99d8db08c958bd3b", size = 5111952, upload-time = "2026-05-14T12:03:01.263Z" }, + { url = "https://files.pythonhosted.org/packages/0b/43/a81f20050a3115b57d62c8e781446949512eac36690dc384ccea65ff4cc1/fonttools-4.63.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76ac49f929aecaf82d83250b8347e099d7aecba0f4726c1d9b6df3b8bb5fe18", size = 5082308, upload-time = "2026-05-14T12:03:03.211Z" }, + { url = "https://files.pythonhosted.org/packages/67/00/cdd9d4944ca6ae280d01e69cc37bde3bf663630b837a6fc6d2cd65d80e0e/fonttools-4.63.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dcf076a4474fe0d7367e5bbf5b052c7284fa1feca729c04176ce513521afd8a0", size = 5087932, upload-time = "2026-05-14T12:03:05.147Z" }, + { url = "https://files.pythonhosted.org/packages/f5/f1/0aa0dbea778c75adbef223c42019fd47d22262b905974d62d829545d485f/fonttools-4.63.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7dd683fef0663e9f0f45cf541d788d24caa3ec9db50796b588e1757d8b3bc007", size = 5213271, upload-time = "2026-05-14T12:03:07.238Z" }, + { url = "https://files.pythonhosted.org/packages/a8/99/253e4056e1f0e67b9390125a154b73b5eb73ad521bece95c004858fdeec2/fonttools-4.63.0-cp311-cp311-win32.whl", hash = "sha256:afefc1ed0a59785a7fb06ea7e1678e849c193e1e387db783579bc7b3056fcfcb", size = 2304473, upload-time = "2026-05-14T12:03:09.271Z" }, + { url = "https://files.pythonhosted.org/packages/08/60/defa5e69641db890a63be281f41345f4c33b157824eaf0b9fad3e08b0dcb/fonttools-4.63.0-cp311-cp311-win_amd64.whl", hash = "sha256:063e08bd17bd5a90127a14123de0d6a952dbc847695fd98b63c043d58057f90c", size = 2356389, upload-time = "2026-05-14T12:03:11.53Z" }, + { url = "https://files.pythonhosted.org/packages/08/ef/b3c6b9b5be2f82416d73fe2ed2e96e2793cd80e7510bd6a17ca79cdd88ec/fonttools-4.63.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:37dd23e621e3b0aef1baa70a303b80aaf38449632cfc8fd2a55fb285bbccfc02", size = 2881131, upload-time = "2026-05-14T12:03:13.386Z" }, + { url = "https://files.pythonhosted.org/packages/44/a0/c815bea63117fa63e4e1c01f8a1110d2112fa003f838e6467094ec2432ce/fonttools-4.63.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a9faff9e0c1f76f9fd55899d2ce785832efebab37eb8ae13995853aef178bef0", size = 2426704, upload-time = "2026-05-14T12:03:15.801Z" }, + { url = "https://files.pythonhosted.org/packages/44/04/0b91d8e916e92ad1fac9e4624760baf0fd5ff2ead614c2f68fb21373f03f/fonttools-4.63.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef3048ef05dbb552b89817713d9cac912e00d0fde4a3105c00d29e52e10c89af", size = 5044298, upload-time = "2026-05-14T12:03:18.085Z" }, + { url = "https://files.pythonhosted.org/packages/77/c7/2342da9830e3e9d4870305ca5d2091d2a83284f2953079b7bdd3b5e029d8/fonttools-4.63.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:58dc6bb86a78d782f00f9190ca02c119cf5bbe2807536e361e18d42019f877d8", size = 4999800, upload-time = "2026-05-14T12:03:20.161Z" }, + { url = "https://files.pythonhosted.org/packages/e6/6d/67fe16c48d7ce050979b33f47e0d28a318f02da030602e944c34f7a16ef3/fonttools-4.63.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee08ebfa58f6e1aeff5697ab9582105bb620008c1caafb681e4c557e7483027b", size = 4982666, upload-time = "2026-05-14T12:03:22.87Z" }, + { url = "https://files.pythonhosted.org/packages/f2/00/3bbab338c07c71fa56269953845e92c951a61457bbbb0f1022551ea266d9/fonttools-4.63.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:27fdc65af8da6f88b9c6121c47a464cbe359fcfff7ff6fc2d37a1f395d755b78", size = 5133598, upload-time = "2026-05-14T12:03:25.168Z" }, + { url = "https://files.pythonhosted.org/packages/62/f2/aa27c7f98db5b064883dadcc5283947e81e034de42e22a33675878d98b54/fonttools-4.63.0-cp312-cp312-win32.whl", hash = "sha256:af2fd1664d00a397d75f806985ddb36282091c2131a73a6485c23b4a34722263", size = 2292575, upload-time = "2026-05-14T12:03:27.496Z" }, + { url = "https://files.pythonhosted.org/packages/87/36/cccb9bc2a6ab63d1b2980374f0dca72ce95ae267c9b4cfe77455bb70d0d4/fonttools-4.63.0-cp312-cp312-win_amd64.whl", hash = "sha256:59ac449f8cca9b4ffa08d2e7bbadad87ce710d69d1eda5c3c1ce579baa987272", size = 2343211, upload-time = "2026-05-14T12:03:30.057Z" }, + { url = "https://files.pythonhosted.org/packages/0f/8d/d8fec3dcde2963f8c908fb315e5ff2cd0ac34f82394bbbf73a2aa5145ce3/fonttools-4.63.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:cd7e9857e5e63738b9d9fd707bc1f59c8b09e5177726d23664db393c59bb08bd", size = 2876062, upload-time = "2026-05-14T12:03:32.554Z" }, + { url = "https://files.pythonhosted.org/packages/ef/71/d935dc54e4ff121bfdd11e08702db63a7e6f25af21d8a3d7b7212df53641/fonttools-4.63.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c2a2a42198b696a6f48fad91709afb55176e66a5e566131219dba372fb7f8c59", size = 2424594, upload-time = "2026-05-14T12:03:34.86Z" }, + { url = "https://files.pythonhosted.org/packages/8e/40/e76320afa1df918e146155ef239b1719ee266092e96f5423bfd075affba1/fonttools-4.63.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e874792a8212b44583ea02189d9e693906b2f78b261f372f95d6c563210ac1d", size = 5024840, upload-time = "2026-05-14T12:03:36.745Z" }, + { url = "https://files.pythonhosted.org/packages/ce/36/0b805d8c485f872f65a509cbe3b58a5d0d17bee855333b54a150c79d3061/fonttools-4.63.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:22135da48a348785c5e2d5d2d9d6bec5ed44adacbaeb9db12d9493bf6c6bfa68", size = 4975801, upload-time = "2026-05-14T12:03:38.833Z" }, + { url = "https://files.pythonhosted.org/packages/c8/26/2cee03d0aa083ab022da5c07aff9ed3f689da1defb81ad6917c9627896da/fonttools-4.63.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ccf41f2efdf56994d22d73bef4ced1052161958169428d06ba9724ea9e9a64be", size = 4965009, upload-time = "2026-05-14T12:03:41.494Z" }, + { url = "https://files.pythonhosted.org/packages/7e/48/cc4b66d9058c0d0982c833fad10127c4b0e9324606aafa41382295ca4102/fonttools-4.63.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9ced0bd02ac751dd6319b0da88aaef24414e3b0dbc32bb4f24944821a3741a27", size = 5105892, upload-time = "2026-05-14T12:03:43.525Z" }, + { url = "https://files.pythonhosted.org/packages/d8/1f/a98a30a814b9ddef3a2e706025f90b9e0bc94890e6cb15254bc86547d11a/fonttools-4.63.0-cp313-cp313-win32.whl", hash = "sha256:85be818f5506e8a7753153def2c9550178f0ecae6a47b5e0e8dbb23f7cc90380", size = 2291313, upload-time = "2026-05-14T12:03:45.594Z" }, + { url = "https://files.pythonhosted.org/packages/92/46/5177b01f3b4abfdd4409f31cca4ab279c9343a26efbe9ec78c97fc612e02/fonttools-4.63.0-cp313-cp313-win_amd64.whl", hash = "sha256:ba04cb5891d4c0c21b6da95eda8d7b090021508a294fff33464fc7d241e0856b", size = 2342299, upload-time = "2026-05-14T12:03:47.414Z" }, + { url = "https://files.pythonhosted.org/packages/27/d2/23d25e3f247b328be58d04a4c9f894178a0d1eda7d42867cfb388adaf416/fonttools-4.63.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fd1e3094f42d806d3d7c79162fc59e5910fcbe3a7360c385b8da969bc4493745", size = 2875338, upload-time = "2026-05-14T12:03:50.052Z" }, + { url = "https://files.pythonhosted.org/packages/cd/58/7dfa0c761cb3b2964e2a84c4dc986c926a87de0cb9fb60d5b28ded3f2914/fonttools-4.63.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6e528da43bc3791085f8cb6141b1d13e459226790240340fcbb4625649238b03", size = 2422661, upload-time = "2026-05-14T12:03:52.154Z" }, + { url = "https://files.pythonhosted.org/packages/dd/87/64cfa18a7a1621d17b7f4502b2b0ed8a135a90c3db51ea590ee99043e76b/fonttools-4.63.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b2248c5decb223562f7902ff6325077a073f608ee8e33e88ad88db734eb9f49", size = 5010526, upload-time = "2026-05-14T12:03:54.647Z" }, + { url = "https://files.pythonhosted.org/packages/36/e1/a8933a72c45a87177fbde2696e0d0755c8c9062f8c077a961c6215fa27b1/fonttools-4.63.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:308f957cdeaf8abe4e5f2f124902ef405448af92c90f80e302a3b771c2e6116b", size = 4923946, upload-time = "2026-05-14T12:03:56.984Z" }, + { url = "https://files.pythonhosted.org/packages/27/60/872e6e233b8c5e8b41413796ff18b7fe479661bd40147e071b450dfad7a1/fonttools-4.63.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bf00f21eb5fb721dbaf73d1e9da6d02a1af7768f2ebcf9798be98beab8ba90f6", size = 4962489, upload-time = "2026-05-14T12:03:59.443Z" }, + { url = "https://files.pythonhosted.org/packages/30/c4/83c24f2ec38b90cfda84bf4b1a1f49df80e84a1db4e7ac6e0d41bf23bc39/fonttools-4.63.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c1aaa4b9c75798400ac043ce04d74e7830376c85095a5a6ed7cba2f17a266bf4", size = 5071870, upload-time = "2026-05-14T12:04:02.122Z" }, + { url = "https://files.pythonhosted.org/packages/de/40/3ae22b60ff1d41ce0bd044b31238cdc72cef99f28b976f1e128ebd618c9b/fonttools-4.63.0-cp314-cp314-win32.whl", hash = "sha256:22693918177bd9ceabec4736d338045f357769416fc6b0b2508eefef75b08616", size = 2295026, upload-time = "2026-05-14T12:04:04.47Z" }, + { url = "https://files.pythonhosted.org/packages/c3/d4/98078064ccc76b45cb0f6c002452011e93c4bd26f6850344f0951cc1fe89/fonttools-4.63.0-cp314-cp314-win_amd64.whl", hash = "sha256:7d782fac32985914c351556f68ac0855391572bcd87de50e05970d3cd4c96fc5", size = 2347454, upload-time = "2026-05-14T12:04:06.752Z" }, + { url = "https://files.pythonhosted.org/packages/49/4e/652d1580c5f4e39f7d103b0c793e4773129ad633dce4addd0cf4dfebde02/fonttools-4.63.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:6db5140a60a5d731d21ec076745b40a310607731b0a565b50776393188649001", size = 2958152, upload-time = "2026-05-14T12:04:08.706Z" }, + { url = "https://files.pythonhosted.org/packages/0e/55/ad864c9a9b219f552eb46b32cd7906c466e5a578ba0c3abfcc0fe7413eb6/fonttools-4.63.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7d76edbff9014094dbf03bd2d074709dfa6ec7aba13d838c937a2b33d2d6a86e", size = 2460809, upload-time = "2026-05-14T12:04:10.783Z" }, + { url = "https://files.pythonhosted.org/packages/ea/2b/0aa8db70f18cf52e49b4ed5ecec68547f981160bf5ded3b5aed6faa0a6f9/fonttools-4.63.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0eac00b9118c3c2f87d272e45341871c5b3066baa3c86897fa634a7c3fb59096", size = 5148649, upload-time = "2026-05-14T12:04:12.747Z" }, + { url = "https://files.pythonhosted.org/packages/7f/63/18e4369c25043096f1048e0c9915951adc4f842bd81c6b18155824d6fa99/fonttools-4.63.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51394295f1a51de8b5f30bdb1e1b9a4231536c7064ef5c6e211eec19fa36036f", size = 4932147, upload-time = "2026-05-14T12:04:14.806Z" }, + { url = "https://files.pythonhosted.org/packages/a1/3f/67f3eac2ffd8a98446c5022f8ed3864eac878a5ff7af8df4c8286dba16cc/fonttools-4.63.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9e12f105d2b6342c559c298afb674006bb2893afc7102dcf8a1b55b0486b4e40", size = 5027237, upload-time = "2026-05-14T12:04:17.675Z" }, + { url = "https://files.pythonhosted.org/packages/1a/ba/4e6214cb38a7b04779e97bb7636de9a5c7f20af7018d03dee0b64c08510a/fonttools-4.63.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:796f27556dbe094c4824f75ca85267e4df776c79036c8441469a4df37038c196", size = 5053933, upload-time = "2026-05-14T12:04:20.818Z" }, + { url = "https://files.pythonhosted.org/packages/34/3b/214dcc19ee31d3d38fb5ad2755c11ef0514e5dc300bbaf41c0b69f393799/fonttools-4.63.0-cp314-cp314t-win32.whl", hash = "sha256:948428a275741f0b64b113c955425a953314f4b9ab9997f73a72c83e68e569c8", size = 2359326, upload-time = "2026-05-14T12:04:24.22Z" }, + { url = "https://files.pythonhosted.org/packages/dd/1e/3ff1a9b523058c2eeb6a9d50f5574e2a738200d0d94107d5bc4105e8da3f/fonttools-4.63.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6d4741eb179121cab9eea4cb2393d24492373a260d7945006358c08cfbf45419", size = 2425829, upload-time = "2026-05-14T12:04:26.829Z" }, + { url = "https://files.pythonhosted.org/packages/2c/47/c99d5268f354002ce80f8d029cd9d7d872969da1de8b93d32de4dc56d6f4/fonttools-4.63.0-py3-none-any.whl", hash = "sha256:445af2eab030a16b9171ea8bdda7ebf7d96bda2df88ee182a464252f6e05e20d", size = 1164562, upload-time = "2026-05-14T12:04:29.092Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.17" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/28/99c51f664567218d824af024c0251650fb27e4ca066df188dab0769c5b91/idna-3.17.tar.gz", hash = "sha256:5eb0cb53bc467c12eadcf6de83163ad8527cec9416f44b9b61b19caedad2b87f", size = 196048, upload-time = "2026-05-28T14:32:38.55Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/a7/f76514cc40ad6234098ecdebda08732d75964776c51a42845b7da10649e2/idna-3.17-py3-none-any.whl", hash = "sha256:466e48829084efe2548012b855df21540b96f2e20e51bd124c851536556a592c", size = 65316, upload-time = "2026-05-28T14:32:37.035Z" }, +] + +[[package]] +name = "jiter" +version = "0.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/b5/55f06bb281d92fb3cc86d14e1def2bd908bb77693183e7cb1f5a3c388b0c/jiter-0.15.0.tar.gz", hash = "sha256:4251acc80e2b7c9b7b8823456ea0fceeb0734dac2df7636d3c711b38476b5a76", size = 166640, upload-time = "2026-05-19T10:09:48.361Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/13/daa722f5765c393576f466378f9dfd29d77c9bed939e0688f96afa3601ea/jiter-0.15.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0f862193b8696249d22ec433e85fd2ab0ad9596bc3e45e6c0bc55e8aeba97be2", size = 310899, upload-time = "2026-05-19T10:07:12.89Z" }, + { url = "https://files.pythonhosted.org/packages/7f/82/2d2551829b082f4b6d82b9f939b031fb808a10aab1ec0664f82e150bb9a2/jiter-0.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1303d4d68a9b051ea90502402063ecf3807da00ad2affa19ca1ae3b90b3c5f67", size = 314963, upload-time = "2026-05-19T10:07:14.539Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0a/8b1a51466f7fe9f31dbe4bc7e0ca848674f9825e0f737b929b97e8c60aa7/jiter-0.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:392b8ab019e5502d08aff85c6272209c24bc2cbe706ea82a56368f524236614a", size = 341730, upload-time = "2026-05-19T10:07:15.869Z" }, + { url = "https://files.pythonhosted.org/packages/f6/2a/e71dea19822e2e404e83992a08c1d6b9b617bb944f28c9c2fbd85d02c91e/jiter-0.15.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:773b6eb282ce11ee19f05f6b2d4404fa308e5bbd353b0b80a0262caad6db2cd7", size = 366214, upload-time = "2026-05-19T10:07:17.259Z" }, + { url = "https://files.pythonhosted.org/packages/c4/59/97e1fa539d124a509a00ab7f669289d1c1d236ecabf12948a18f16c91082/jiter-0.15.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2c0c44d569ce0f2850f5c926f8caeb5f245fbc84475aeb36efccc2103e6dbd", size = 459527, upload-time = "2026-05-19T10:07:18.741Z" }, + { url = "https://files.pythonhosted.org/packages/d1/7a/4a68d331aef8cf2e2393c14a3aacb635c62aa86071b0229899fb5baaa907/jiter-0.15.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:032396229564bca02440396bd327710719f724f5e7b7e9f7a8eb3faa4a2c2281", size = 375451, upload-time = "2026-05-19T10:07:20.208Z" }, + { url = "https://files.pythonhosted.org/packages/7b/7e/1c445c2b6f0e30a274dc8082e0c3c7825411cce80d726bccd697c98cc8d3/jiter-0.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3d37768fce7f88dd2a8c6091f2325dea27d30d30d5c6e7a1c0f0af77723b708", size = 349428, upload-time = "2026-05-19T10:07:22.372Z" }, + { url = "https://files.pythonhosted.org/packages/00/94/e20d38984fc17a636371bffd2ae0f698124fdc8e75ef969cd2da6ba7cea7/jiter-0.15.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:2c9cb907439d20bd0c7d7565ca01ee52234203208433749bae5b516907526928", size = 355405, upload-time = "2026-05-19T10:07:23.916Z" }, + { url = "https://files.pythonhosted.org/packages/94/fa/4d09f814779d0ea80a28ed8e4c6662ec9a4a8ecef0ac52190ebac6262d14/jiter-0.15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9100ddbec09741cc66feb0fc6773f8bdbd0e3c345689368f260082ff85dcc0cd", size = 393688, upload-time = "2026-05-19T10:07:25.854Z" }, + { url = "https://files.pythonhosted.org/packages/54/9d/8eb5d4fb8bf7e93a75964a5da71a75c67c864baf7fa3f98598187b3c7e57/jiter-0.15.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ae1b0d82ac2d987f9ea512b1c9adfcc71a28de3dea3a6039b54d76cffda9901e", size = 520853, upload-time = "2026-05-19T10:07:27.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/2c/5e07874e59e623a943a0acf1552a80d05b70f31b402287a8fc6d7ec634c7/jiter-0.15.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8020c99ec13a7db2b6f96cbe82ef4721c88b426a4892f27478044af0284615ef", size = 551016, upload-time = "2026-05-19T10:07:28.846Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/d2d34422143474cadc15b60d482b1c35683dbc5c63c24346ddd0df09bcaf/jiter-0.15.0-cp311-cp311-win32.whl", hash = "sha256:42bfb257930800cf43e7c62c832402c704ab60797c992faf88d20e903eac8f32", size = 209518, upload-time = "2026-05-19T10:07:30.431Z" }, + { url = "https://files.pythonhosted.org/packages/1d/7d/52778b930e5cc3e52a37d950b1c10494244308b4329b25a0ff0d88303a81/jiter-0.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:860a74063284a2ae9bfedd694f299cc2c68e2696c5f3d440cc9d18bb81b9dd04", size = 200565, upload-time = "2026-05-19T10:07:32.125Z" }, + { url = "https://files.pythonhosted.org/packages/3b/4f/d9b4067feb69b3fa6eb0488e1b59e2ad5b463fe39f59e527eab2aca00bb0/jiter-0.15.0-cp311-cp311-win_arm64.whl", hash = "sha256:37a10c377ce3a4a85f4a67f28b7afe093154cde77eaf248a72e856aa08b4d865", size = 195488, upload-time = "2026-05-19T10:07:33.846Z" }, + { url = "https://files.pythonhosted.org/packages/44/53/4f6bddbcde3c71e56d0aa1337ec95950f3d27dd4153e25aadf0feac71751/jiter-0.15.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0e90a1c315a0226ec822d973817967f9223b7701546c8c2a7913e7ab0926294d", size = 308793, upload-time = "2026-05-19T10:07:35.25Z" }, + { url = "https://files.pythonhosted.org/packages/01/84/c01099b59a285a1ebba64ae93f62bfa036675340fd1b0045ae65890a0442/jiter-0.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8c9004af7c8d67cce7f1aae1026fb55607f4aa600710d08ede3a3ce4aeefe7e0", size = 309570, upload-time = "2026-05-19T10:07:36.919Z" }, + { url = "https://files.pythonhosted.org/packages/58/64/8fb7f9d45bb98190355454cd04dad8d8f27223d6bd52f83af07f637168a6/jiter-0.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c210f8b35dc6f30aafd4b4365ca89b9d1189f21ab49b8e68fa6322a847aef138", size = 336783, upload-time = "2026-05-19T10:07:38.694Z" }, + { url = "https://files.pythonhosted.org/packages/c3/b6/f5739011d009b3a30f6a53c5240979030ba29ae46a8c67e3a15759f7c37d/jiter-0.15.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f30bae8bc1c2d613e28e5af3e8cceb09b742f1c8a8a5f839fb67afaffc03b61", size = 363555, upload-time = "2026-05-19T10:07:40.832Z" }, + { url = "https://files.pythonhosted.org/packages/e5/12/98a9d9f766665e8a3b6252454e17cb0c464606a28cf2fa09399b003345fa/jiter-0.15.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c60e71b6d10cfc284c9bf36bd885e8d44c46f688ce50aa91b5edd90181dea687", size = 452255, upload-time = "2026-05-19T10:07:42.62Z" }, + { url = "https://files.pythonhosted.org/packages/e8/d5/60f972840f79c5e7544fce567c56f1e4e50468f996baba3e78d823dd62a6/jiter-0.15.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ab068bce62a45aa3e7367eceaffb5dde60b7eb853be8dece45132e3d0ff4879", size = 373559, upload-time = "2026-05-19T10:07:44.201Z" }, + { url = "https://files.pythonhosted.org/packages/ee/cf/d46ef1234ba335aabc2f013210db8e0821a22f5e644a2e9449df199ecc23/jiter-0.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa248c9eb220197d363f688818dac2fd4b2f0cd7d843ca7105d652034823427d", size = 346055, upload-time = "2026-05-19T10:07:46.005Z" }, + { url = "https://files.pythonhosted.org/packages/f0/63/4d2749d8d54d230bad9b3a6b0d00cc28c6ff6b2fdffc26a8ccf76cc5a974/jiter-0.15.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2a77aadd57cac1682e4401a72724d2796d89a4ba129b1a5812aa94ee480826eb", size = 351406, upload-time = "2026-05-19T10:07:47.855Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b9/9965b990035d8773328e0a8c8b457a87bf2b19f6c4126d9d99296be5d16a/jiter-0.15.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2ae901f3a55bfafdde31d289590fa25e3245735a2b1e8c7cc15871710a002871", size = 389357, upload-time = "2026-05-19T10:07:49.665Z" }, + { url = "https://files.pythonhosted.org/packages/2d/55/9ddf903deda1413e87fed792f416b7123daee5b8efbad6a202a7421c36a5/jiter-0.15.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f0b271b462769543716f92d3a4f90527df6ef5ed05ee95ec4137f513e21e1b77", size = 517263, upload-time = "2026-05-19T10:07:51.537Z" }, + { url = "https://files.pythonhosted.org/packages/e8/76/a0c40ad064d3a20a4fde231e35d56e9a01ce82164278180e82d5daf85469/jiter-0.15.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2fb6a5d26af81fc0f00f9360a891e05cf755e149bba391c4d563adc54812973d", size = 548646, upload-time = "2026-05-19T10:07:53.196Z" }, + { url = "https://files.pythonhosted.org/packages/23/4f/eca9b954942916ba2f453891b8593ab444cd872396fe66a3936616f236f3/jiter-0.15.0-cp312-cp312-win32.whl", hash = "sha256:c2f6bb8b5216ab9e7873bc08b5d7bef2b8abbb578a3069bf1cd14a45d71d771d", size = 206427, upload-time = "2026-05-19T10:07:55.307Z" }, + { url = "https://files.pythonhosted.org/packages/95/bf/8ead82a87495149542748e828d153fd232a512a22c83b02c4815c1a9c7d8/jiter-0.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:40b2c7e92c44a84d748d21706c68dc6ff8161d80b59c99d774721a0d2317d7c7", size = 197300, upload-time = "2026-05-19T10:07:56.651Z" }, + { url = "https://files.pythonhosted.org/packages/f4/e4/9b8a78fb2d894471bc344e37f1949bdd784bd914d031dba0ba3a40c71dd7/jiter-0.15.0-cp312-cp312-win_arm64.whl", hash = "sha256:cc0bc345cf2df9d1c00ac443f50d543c1ccfa8b0422cb85b1ab70d681c0b255b", size = 192702, upload-time = "2026-05-19T10:07:58.307Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f4/f708c900ecee41b2025ef8413d5351e5649eb2125c506f6720cc69b06f5c/jiter-0.15.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1c11465f97e2abf45a014b83b730222f8f1c5335e802c7055a67d50de6f1f4e3", size = 307829, upload-time = "2026-05-19T10:07:59.704Z" }, + { url = "https://files.pythonhosted.org/packages/86/59/db537c0949e83668c38481d426b9f2fd5ab758c4ee53a811dd0a510626a0/jiter-0.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d1e7b1776f0797956c509e123d0952d10d293a9492dea9f288ab9570ec01d1a5", size = 308445, upload-time = "2026-05-19T10:08:01.184Z" }, + { url = "https://files.pythonhosted.org/packages/37/38/ea0e13b18c30ef951da0d47d39e7fa9edb82a93a62990ffbd7cea9b622d4/jiter-0.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:351a341c2105aa430b7047e30f1bf7975f6313b00165d3fc07be2edaf741f279", size = 336181, upload-time = "2026-05-19T10:08:02.688Z" }, + { url = "https://files.pythonhosted.org/packages/58/fc/2303901b16c4ba05865588990a420c0b4156270b44379c20931544a1d962/jiter-0.15.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4ab395feec8d249ec4044e228e98a7033f043426a265df439dc3698823f0a4e4", size = 362985, upload-time = "2026-05-19T10:08:04.394Z" }, + { url = "https://files.pythonhosted.org/packages/5b/6f/11bace093c52e7d4d26c8e606ccd7ae8c972189622469ec0d9e28161e28b/jiter-0.15.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2a438005b6f22d0273413484d6094d7c2c5d10ec1b3a3bf128e0d1d3ba53258", size = 453292, upload-time = "2026-05-19T10:08:05.967Z" }, + { url = "https://files.pythonhosted.org/packages/22/db/987f2f086ca4d7a6582eb4ccd513f9b26b42d9e4243a087609a3137a8fc7/jiter-0.15.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f18f85e4218d1b40f000f42a92239a7a61a902cd42c65e6c360dbd17dcb20894", size = 373501, upload-time = "2026-05-19T10:08:07.857Z" }, + { url = "https://files.pythonhosted.org/packages/8f/7c/89fbcabb2739b7a5b8dc959a1b6c5761f6484f5fed3486854b3c789bb1de/jiter-0.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1aa62e277fc1cbd80e6deacae6f4d983b41b3d7728e0645c5d741a6149bba45", size = 344683, upload-time = "2026-05-19T10:08:09.431Z" }, + { url = "https://files.pythonhosted.org/packages/30/6f/6cca7692e7dddfec6d8d76c54dc97f2af2a41df4ac0674b999df1f09a5f3/jiter-0.15.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:6550fa135c7deb8ead6af49ed7ff648532ea8334a1447fe34a36315ef79c5c29", size = 350892, upload-time = "2026-05-19T10:08:11.352Z" }, + { url = "https://files.pythonhosted.org/packages/39/14/0338d6190cb8e6d22e677ab1d4eabd4117f67cca70c54cd04b82ff64e068/jiter-0.15.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:066f8f33f18b2419cd8213b2436fa7fbc9c499f315971cfa3ce1f9820c001b1b", size = 388723, upload-time = "2026-05-19T10:08:12.912Z" }, + { url = "https://files.pythonhosted.org/packages/90/31/cc19f4a1bdb6afb09ce6a2f2615aa8d44d994eba0d8e6105ed1af920e736/jiter-0.15.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:75e8a04e91432dde9f1838373cf93d23726c79d3e908d319acf0e796f85592e7", size = 516648, upload-time = "2026-05-19T10:08:14.808Z" }, + { url = "https://files.pythonhosted.org/packages/49/9f/833c541512cd091b63c10c0381973dfe11bc7a503a818c16384417e0c81e/jiter-0.15.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:a97261f1fccb8e50ecd2890a96e46efdc3f57c80a197324c6777827231eca712", size = 547382, upload-time = "2026-05-19T10:08:16.927Z" }, + { url = "https://files.pythonhosted.org/packages/d2/11/e7b70e91f90bc4477e8eee9e8a5f7cf3cb41b4525d6394dc98a714eb8f7f/jiter-0.15.0-cp313-cp313-win32.whl", hash = "sha256:c77496cb10bd7549690fbbab3e5ec05857b83e49276f4a9423a766ddd2afcd4c", size = 205845, upload-time = "2026-05-19T10:08:18.401Z" }, + { url = "https://files.pythonhosted.org/packages/4b/23/5c20d9ad6f02c493e4023e5d2d09e1c1f15fe2753c9102c544aff068a88e/jiter-0.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b15741f501469009ae0ae90b7147958a664a7dede40aa7ff174a8a4645f546d0", size = 196842, upload-time = "2026-05-19T10:08:20.131Z" }, + { url = "https://files.pythonhosted.org/packages/6b/11/1eb400ef248e8c925fd883fbe325daf5e42cd1b0d308539dd332bd4f7ffc/jiter-0.15.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d6a60072b44c3c2b797a7ddcbcbbf2b34ea3cfd4721580fbfd2a09d9d9b84ba", size = 192212, upload-time = "2026-05-19T10:08:21.807Z" }, + { url = "https://files.pythonhosted.org/packages/8a/60/2fd8d7c79da8acf9b7b277c7616847773779356b92acfc9bb158452174da/jiter-0.15.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ef1fd24d9413f6209e00d3d5a453e67acfe004a25cc6c8e8484faed4311ab9e8", size = 315065, upload-time = "2026-05-19T10:08:23.218Z" }, + { url = "https://files.pythonhosted.org/packages/46/f4/008fb7d65e8ac2abf00811651a661e025c4ba80bbc6f378450384ddd3aed/jiter-0.15.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:144f8e72cb53dab146347b91cceac01f5481237f2b93b4a339a1ee8f8878b67c", size = 339444, upload-time = "2026-05-19T10:08:24.701Z" }, + { url = "https://files.pythonhosted.org/packages/00/55/90b0c7b9c6896c0f2a591dd36d36b71d22e09674bfef178fa03ba3f81499/jiter-0.15.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:553fcac2ef2cb990877f9fc0833b8b629a3e6a5670b6b5fd58219b41a653ddc4", size = 347779, upload-time = "2026-05-19T10:08:26.408Z" }, + { url = "https://files.pythonhosted.org/packages/51/6b/69666cec5000fd57734c118437394516c749ae8dbeea9fb66d6fef9c4775/jiter-0.15.0-cp313-cp313t-win_amd64.whl", hash = "sha256:774f93f65031856bf14ad9f59bdcab8b8cad501e5ceabd51ba3525f76937a25b", size = 200395, upload-time = "2026-05-19T10:08:28.055Z" }, + { url = "https://files.pythonhosted.org/packages/39/04/a6aa62cd27e8149b0d28df5561f10f6cceaf7935a9ccf3f1c5a05f9a0cd8/jiter-0.15.0-cp313-cp313t-win_arm64.whl", hash = "sha256:f1e1754960f38ec40613a07e5e372df67acb3b890fb383b6fb3de3e49ddbf3c7", size = 190516, upload-time = "2026-05-19T10:08:29.35Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d2/079f350ebf7859d081de30aa890f9e3be68516f754f3ba32366ffff4dcee/jiter-0.15.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:ac0d9ddea4350974be7a221fc25895f251a8fee748c889bdced2141c0fec1a49", size = 308884, upload-time = "2026-05-19T10:08:31.667Z" }, + { url = "https://files.pythonhosted.org/packages/04/4e/a2c30a7f69b48c03b20935d647479106fe932f6e63f75faf53937197e05d/jiter-0.15.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:01a8222cf05ab1128e239421156c207949808acaaea2bdfd33130ae666786e86", size = 310028, upload-time = "2026-05-19T10:08:33.304Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/2e7cdfd3cf8ca967be38c48f5cf474d79f089efaf559a40f15984a77ae69/jiter-0.15.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:182226cbc930c9fab81bc2e41a4da672f89539906dadb05e75670ac07b94f71f", size = 337485, upload-time = "2026-05-19T10:08:35.259Z" }, + { url = "https://files.pythonhosted.org/packages/9b/11/15a1aa28b120b8ee5b4f1fb894c125046225f09847738bd64233d3b84883/jiter-0.15.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:71683c38c825452999b5717fcae07ea708e8c93003e808be4319c1b02e3d176e", size = 364223, upload-time = "2026-05-19T10:08:36.694Z" }, + { url = "https://files.pythonhosted.org/packages/b7/25/f442e8af5f3d0dcf47b39e83a0efd9ee45ea946aa6d04625dc3181eae3b6/jiter-0.15.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30f2218e6a9e5c18bc10fe6d41ac189c442c88eacf11bad9f28ef95a9bef00e6", size = 456387, upload-time = "2026-05-19T10:08:38.143Z" }, + { url = "https://files.pythonhosted.org/packages/da/f4/37f2d2c9f64f49af7da652ed7532bb5a2372e588e6927c3fdd76f911db65/jiter-0.15.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5157de9f76eb4bc5ea74a1219366a25f945ad305641d74e04f59c54087091aa9", size = 374461, upload-time = "2026-05-19T10:08:39.869Z" }, + { url = "https://files.pythonhosted.org/packages/60/28/edcfbbbf0cb15436f36664a8908a0df47ab9006298d4cd937dc08ea932d6/jiter-0.15.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90c5db5527c221249a876160663ab891ace358c17f7b9c93ec1478b7f0550e5c", size = 345924, upload-time = "2026-05-19T10:08:41.668Z" }, + { url = "https://files.pythonhosted.org/packages/47/13/89fba6398dab7f202b7278c4b4aac122399d2c0183971c4a57a3b7088df5/jiter-0.15.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:3e4540b8e74e4268811ac05db226a6a128ff572e7e0ce3f1163b693cadb184cd", size = 352283, upload-time = "2026-05-19T10:08:43.091Z" }, + { url = "https://files.pythonhosted.org/packages/1b/da/0f6af8cef2c565a1ab44d970f268c43ccaa72707386ea6388e6fe2b6cd26/jiter-0.15.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:62ebd14e47e9aed9df4472afcb2663668ce4d74891cd54f86bf6e44029d6dc89", size = 389985, upload-time = "2026-05-19T10:08:44.915Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ec/b9cb7d6d29e24ee14910266157d2a279d7a8f60ee0df7fa840882976ba64/jiter-0.15.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0be6f5ad41a809f303f416d17cec92a7a725902fb9b4f3de3d19362ac0ef8554", size = 517695, upload-time = "2026-05-19T10:08:46.486Z" }, + { url = "https://files.pythonhosted.org/packages/64/5e/6d1bda880723aae0ad86b4b763f044362448efe31e3e819635d41cb03451/jiter-0.15.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:813dfbb17d65328bf86e5f0905dd277ba2265d3ca20556e86c0c7035b7182e5a", size = 548868, upload-time = "2026-05-19T10:08:48.026Z" }, + { url = "https://files.pythonhosted.org/packages/0c/72/7de501cf38dcacaf35098796f3a50e0f2e338baba18a58946c618544b809/jiter-0.15.0-cp314-cp314-win32.whl", hash = "sha256:50e51156192722a9c58db112837d3f8ef96fb3c5ecc14e95f409134b08b158ec", size = 206380, upload-time = "2026-05-19T10:08:49.738Z" }, + { url = "https://files.pythonhosted.org/packages/1e/a9/e19addf4b0c1bdce52c6da12351e6bc42c340c45e7c09e2158e46d293ccc/jiter-0.15.0-cp314-cp314-win_amd64.whl", hash = "sha256:30ce1a5d16b5641dc935d50ef775af6a0871e3d14ab05d6fc54dff371b78e558", size = 197687, upload-time = "2026-05-19T10:08:51.088Z" }, + { url = "https://files.pythonhosted.org/packages/f2/c9/776b1db01db25fc6c1d58d1979a37b0a9fe787e5f5b1d062d2eaacb77923/jiter-0.15.0-cp314-cp314-win_arm64.whl", hash = "sha256:510c8b3c17a0ed9ac69850c0438dada3c9b82d9c4d589fcb62002a5a9cf3a866", size = 192571, upload-time = "2026-05-19T10:08:52.451Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f6/45bb4670bacf300fd2c7abadbfb3af376e5f1b6ae75fd9bc069891d15870/jiter-0.15.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7553333dd0930c104a5a0db8df72bf7219fe663d731383b576bb6ed6351c984d", size = 317151, upload-time = "2026-05-19T10:08:53.867Z" }, + { url = "https://files.pythonhosted.org/packages/d7/68/ed635ad5acd7b73e454283083bbb7c8205ad10e88b0d9d7d793b09fe8226/jiter-0.15.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2143ab06181d2b029eedcb6af3cebe95f11bbac62441781860f98ee9330a6a6", size = 341243, upload-time = "2026-05-19T10:08:55.383Z" }, + { url = "https://files.pythonhosted.org/packages/5d/db/3ff4176b817b8ea33879e71e13d8bc2b0d481a7ed3fe9e080f333d415c16/jiter-0.15.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6eac374c5c975709b69c10f09afd199df74150172156ad10c8d4fd785b7da995", size = 363629, upload-time = "2026-05-19T10:08:56.928Z" }, + { url = "https://files.pythonhosted.org/packages/ab/24/5f8270e0ba9c883582f96f722f8a0b58015c7ce1f8c6d4571cf394e99b6b/jiter-0.15.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b3b3b775e33d3bfaec9899edc526ae97b0da0bf9d071a46124ba419149a414f8", size = 456198, upload-time = "2026-05-19T10:08:58.618Z" }, + { url = "https://files.pythonhosted.org/packages/45/5b/76fc02b0b5c54c3d18c60653156e2f76fde1816f9b4722db68d6ee2c897e/jiter-0.15.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3071db3346334beae1360b46da4606da57bf3528c167b3c38533afaf9f2c5", size = 373710, upload-time = "2026-05-19T10:09:00.151Z" }, + { url = "https://files.pythonhosted.org/packages/c4/52/4310821b0ea9277994d3e1f49fc6a4b34e4800caebacb2c0af81da59a454/jiter-0.15.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6694a173ecabc12eb60efbc0b474464ead1951ff65cd8b1e72100715c64512b", size = 349901, upload-time = "2026-05-19T10:09:01.621Z" }, + { url = "https://files.pythonhosted.org/packages/93/fe/67648c35b3594fba8854ac64cc8a826d8bcd18324bbdb53d77697c60b6ef/jiter-0.15.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:a254e10b593624d230c365b6d616b22ca0ad65e63a16e6631c2b3466022e6ba8", size = 352438, upload-time = "2026-05-19T10:09:03.216Z" }, + { url = "https://files.pythonhosted.org/packages/cb/28/0a1879d07ad6b3e025a2750027363452ced93c2d16d1c9d4b153ffd51c91/jiter-0.15.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d8d2955167274e15d79a7a020afdd9b39c990eb80b2d89fca695d92dcfdd38ec", size = 388152, upload-time = "2026-05-19T10:09:04.741Z" }, + { url = "https://files.pythonhosted.org/packages/c1/78/46c6f6b56ba85c90021f4afd72ed42f691f8f84daacb5fe27277070e3858/jiter-0.15.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:acf4ee4d1fc55917239fe72972fb292dd773055d05eb040d36f4326e02cc2c0e", size = 517707, upload-time = "2026-05-19T10:09:06.231Z" }, + { url = "https://files.pythonhosted.org/packages/ca/cb/720662d4c88fcad606e826fef5424365527ba43ce4868a479aed8f8c507e/jiter-0.15.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:e7196e56f1cd69af1dbb07dff02dcfb260a50b45a82d409d92a06fedb32473b5", size = 548241, upload-time = "2026-05-19T10:09:08.093Z" }, + { url = "https://files.pythonhosted.org/packages/60/e3/935b8034fd143f21125c87d51404a9e0e1449186a494405721ff5d1d695e/jiter-0.15.0-cp314-cp314t-win32.whl", hash = "sha256:7f6163c0f10b055245f814dcc59f4818da60dfe72f3e72ab89fc24b6bd5e9c52", size = 207950, upload-time = "2026-05-19T10:09:09.616Z" }, + { url = "https://files.pythonhosted.org/packages/93/59/984fd9ece895953dad3e0880a650e766f5a2da2c5514f0eafdaaabbeb5f9/jiter-0.15.0-cp314-cp314t-win_amd64.whl", hash = "sha256:980c256edb05b78a111b99c4de3b1d32e31634b867fd1fc2cf726e7b7bba9854", size = 200055, upload-time = "2026-05-19T10:09:11.367Z" }, + { url = "https://files.pythonhosted.org/packages/0e/a4/cf8d779feb133a27a2e3bc833bccb9e13aa332cdf820497ebf72c10ce8c3/jiter-0.15.0-cp314-cp314t-win_arm64.whl", hash = "sha256:66b1880df2d01e206e8339769d1c7c1753bcb653efd6289e203f6f24ebada0c0", size = 191244, upload-time = "2026-05-19T10:09:12.74Z" }, + { url = "https://files.pythonhosted.org/packages/65/43/1fc62172aa98b50a7de9a25554060db510f85c89cfbed0dfe13e1907a139/jiter-0.15.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:411fa4dfa5a7ae3d11491027ffb9beadec3996010a986862db70d91abba1c750", size = 305585, upload-time = "2026-05-19T10:09:35.995Z" }, + { url = "https://files.pythonhosted.org/packages/e8/c4/dd58fcd9e2df83666e5c1c1347bef58ce919cd8efc3ffa38aeea62ce493b/jiter-0.15.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:2b0074e2f56eb2dacca1689760fd2852a068f85a0547a157b82cb4cafeb6768b", size = 306936, upload-time = "2026-05-19T10:09:37.435Z" }, + { url = "https://files.pythonhosted.org/packages/39/86/b695e16f1180c07f43ea98e73ecd21cf63fa2e1b0c1103739013784d11ae/jiter-0.15.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:913d02d29c9606643418d9ccfc3b72492ab25a6bf7889934e09a3490f8d3438b", size = 342453, upload-time = "2026-05-19T10:09:39.294Z" }, + { url = "https://files.pythonhosted.org/packages/34/56/55d76614af37fe3f22a3347d1e410d2a15da581997cb2da499a625000bb5/jiter-0.15.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b15d3ec9b0449c40e85319bdb4caa8b77ab526e74f5532ed94bec15e2f66822c", size = 345606, upload-time = "2026-05-19T10:09:40.727Z" }, + { url = "https://files.pythonhosted.org/packages/73/38/505941b2b092fd5bbbd60a52a880db1173f1690ae6751bed3af1c9ddcb4e/jiter-0.15.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:631f13a3d04e97d4e083993b10f4b99530e3a10d953e2eb5e196b7dc7f812ce0", size = 303769, upload-time = "2026-05-19T10:09:42.203Z" }, + { url = "https://files.pythonhosted.org/packages/e7/95/a06692b29e77473f286e1ec1f426d3ca44d7b5843be8ad21d7a5f3fcdcc0/jiter-0.15.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:b6c0ffae686c39bf3737be60793783267628783ea42545632c10b291105aee45", size = 305128, upload-time = "2026-05-19T10:09:43.657Z" }, + { url = "https://files.pythonhosted.org/packages/23/85/7270d7ad41d6061a25b950c6bf91d638bd9aacb113200a8c8d57a055fd67/jiter-0.15.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d54fb5b31dea401a41af3f8a7d2512e9b6a6a005491e6166c7e4ffab9639a9c", size = 340459, upload-time = "2026-05-19T10:09:45.452Z" }, + { url = "https://files.pythonhosted.org/packages/c8/8d/302cb2057b7513327b4d575cff6b1d066ee6431a5357fc3f8867cd684406/jiter-0.15.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d5d6090cdc1b7c9e780dfb04949a990adb1e301a2fc0bbcee7de4638d33f9a", size = 344469, upload-time = "2026-05-19T10:09:46.864Z" }, +] + +[[package]] +name = "joblib" +version = "1.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" }, +] + +[[package]] +name = "kiwisolver" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/67/9c61eccb13f0bdca9307614e782fec49ffdde0f7a2314935d489fa93cd9c/kiwisolver-1.5.0.tar.gz", hash = "sha256:d4193f3d9dc3f6f79aaed0e5637f45d98850ebf01f7ca20e69457f3e8946b66a", size = 103482, upload-time = "2026-03-09T13:15:53.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/dd/a495a9c104be1c476f0386e714252caf2b7eca883915422a64c50b88c6f5/kiwisolver-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9eed0f7edbb274413b6ee781cca50541c8c0facd3d6fd289779e494340a2b85c", size = 122798, upload-time = "2026-03-09T13:12:58.963Z" }, + { url = "https://files.pythonhosted.org/packages/11/60/37b4047a2af0cf5ef6d8b4b26e91829ae6fc6a2d1f74524bcb0e7cd28a32/kiwisolver-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c4923e404d6bcd91b6779c009542e5647fef32e4a5d75e115e3bbac6f2335eb", size = 66216, upload-time = "2026-03-09T13:13:00.155Z" }, + { url = "https://files.pythonhosted.org/packages/0a/aa/510dc933d87767584abfe03efa445889996c70c2990f6f87c3ebaa0a18c5/kiwisolver-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0df54df7e686afa55e6f21fb86195224a6d9beb71d637e8d7920c95cf0f89aac", size = 63911, upload-time = "2026-03-09T13:13:01.671Z" }, + { url = "https://files.pythonhosted.org/packages/80/46/bddc13df6c2a40741e0cc7865bb1c9ed4796b6760bd04ce5fae3928ef917/kiwisolver-1.5.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2517e24d7315eb51c10664cdb865195df38ab74456c677df67bb47f12d088a27", size = 1438209, upload-time = "2026-03-09T13:13:03.385Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d6/76621246f5165e5372f02f5e6f3f48ea336a8f9e96e43997d45b240ed8cd/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff710414307fefa903e0d9bdf300972f892c23477829f49504e59834f4195398", size = 1248888, upload-time = "2026-03-09T13:13:05.231Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c1/31559ec6fb39a5b48035ce29bb63ade628f321785f38c384dee3e2c08bc1/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6176c1811d9d5a04fa391c490cc44f451e240697a16977f11c6f722efb9041db", size = 1266304, upload-time = "2026-03-09T13:13:06.743Z" }, + { url = "https://files.pythonhosted.org/packages/5e/ef/1cb8276f2d29cc6a41e0a042f27946ca347d3a4a75acf85d0a16aa6dcc82/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50847dca5d197fcbd389c805aa1a1cf32f25d2e7273dc47ab181a517666b68cc", size = 1319650, upload-time = "2026-03-09T13:13:08.607Z" }, + { url = "https://files.pythonhosted.org/packages/4c/e4/5ba3cecd7ce6236ae4a80f67e5d5531287337d0e1f076ca87a5abe4cd5d0/kiwisolver-1.5.0-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:01808c6d15f4c3e8559595d6d1fe6411c68e4a3822b4b9972b44473b24f4e679", size = 970949, upload-time = "2026-03-09T13:13:10.299Z" }, + { url = "https://files.pythonhosted.org/packages/5a/69/dc61f7ae9a2f071f26004ced87f078235b5507ab6e5acd78f40365655034/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f1f9f4121ec58628c96baa3de1a55a4e3a333c5102c8e94b64e23bf7b2083309", size = 2199125, upload-time = "2026-03-09T13:13:11.841Z" }, + { url = "https://files.pythonhosted.org/packages/e5/7b/abbe0f1b5afa85f8d084b73e90e5f801c0939eba16ac2e49af7c61a6c28d/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b7d335370ae48a780c6e6a6bbfa97342f563744c39c35562f3f367665f5c1de2", size = 2293783, upload-time = "2026-03-09T13:13:14.399Z" }, + { url = "https://files.pythonhosted.org/packages/8a/80/5908ae149d96d81580d604c7f8aefd0e98f4fd728cf172f477e9f2a81744/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:800ee55980c18545af444d93fdd60c56b580db5cc54867d8cbf8a1dc0829938c", size = 1960726, upload-time = "2026-03-09T13:13:16.047Z" }, + { url = "https://files.pythonhosted.org/packages/84/08/a78cb776f8c085b7143142ce479859cfec086bd09ee638a317040b6ef420/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:c438f6ca858697c9ab67eb28246c92508af972e114cac34e57a6d4ba17a3ac08", size = 2464738, upload-time = "2026-03-09T13:13:17.897Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e1/65584da5356ed6cb12c63791a10b208860ac40a83de165cb6a6751a686e3/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8c63c91f95173f9c2a67c7c526b2cea976828a0e7fced9cdcead2802dc10f8a4", size = 2270718, upload-time = "2026-03-09T13:13:19.421Z" }, + { url = "https://files.pythonhosted.org/packages/be/6c/28f17390b62b8f2f520e2915095b3c94d88681ecf0041e75389d9667f202/kiwisolver-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:beb7f344487cdcb9e1efe4b7a29681b74d34c08f0043a327a74da852a6749e7b", size = 73480, upload-time = "2026-03-09T13:13:20.818Z" }, + { url = "https://files.pythonhosted.org/packages/d8/0e/2ee5debc4f77a625778fec5501ff3e8036fe361b7ee28ae402a485bb9694/kiwisolver-1.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:ad4ae4ffd1ee9cd11357b4c66b612da9888f4f4daf2f36995eda64bd45370cac", size = 64930, upload-time = "2026-03-09T13:13:21.997Z" }, + { url = "https://files.pythonhosted.org/packages/4d/b2/818b74ebea34dabe6d0c51cb1c572e046730e64844da6ed646d5298c40ce/kiwisolver-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4e9750bc21b886308024f8a54ccb9a2cc38ac9fa813bf4348434e3d54f337ff9", size = 123158, upload-time = "2026-03-09T13:13:23.127Z" }, + { url = "https://files.pythonhosted.org/packages/bf/d9/405320f8077e8e1c5c4bd6adc45e1e6edf6d727b6da7f2e2533cf58bff71/kiwisolver-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:72ec46b7eba5b395e0a7b63025490d3214c11013f4aacb4f5e8d6c3041829588", size = 66388, upload-time = "2026-03-09T13:13:24.765Z" }, + { url = "https://files.pythonhosted.org/packages/99/9f/795fedf35634f746151ca8839d05681ceb6287fbed6cc1c9bf235f7887c2/kiwisolver-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ed3a984b31da7481b103f68776f7128a89ef26ed40f4dc41a2223cda7fb24819", size = 64068, upload-time = "2026-03-09T13:13:25.878Z" }, + { url = "https://files.pythonhosted.org/packages/c4/13/680c54afe3e65767bed7ec1a15571e1a2f1257128733851ade24abcefbcc/kiwisolver-1.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb5136fb5352d3f422df33f0c879a1b0c204004324150cc3b5e3c4f310c9049f", size = 1477934, upload-time = "2026-03-09T13:13:27.166Z" }, + { url = "https://files.pythonhosted.org/packages/c8/2f/cebfcdb60fd6a9b0f6b47a9337198bcbad6fbe15e68189b7011fd914911f/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2af221f268f5af85e776a73d62b0845fc8baf8ef0abfae79d29c77d0e776aaf", size = 1278537, upload-time = "2026-03-09T13:13:28.707Z" }, + { url = "https://files.pythonhosted.org/packages/f2/0d/9b782923aada3fafb1d6b84e13121954515c669b18af0c26e7d21f579855/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b0f172dc8ffaccb8522d7c5d899de00133f2f1ca7b0a49b7da98e901de87bf2d", size = 1296685, upload-time = "2026-03-09T13:13:30.528Z" }, + { url = "https://files.pythonhosted.org/packages/27/70/83241b6634b04fe44e892688d5208332bde130f38e610c0418f9ede47ded/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6ab8ba9152203feec73758dad83af9a0bbe05001eb4639e547207c40cfb52083", size = 1346024, upload-time = "2026-03-09T13:13:32.818Z" }, + { url = "https://files.pythonhosted.org/packages/e4/db/30ed226fb271ae1a6431fc0fe0edffb2efe23cadb01e798caeb9f2ceae8f/kiwisolver-1.5.0-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:cdee07c4d7f6d72008d3f73b9bf027f4e11550224c7c50d8df1ae4a37c1402a6", size = 987241, upload-time = "2026-03-09T13:13:34.435Z" }, + { url = "https://files.pythonhosted.org/packages/ec/bd/c314595208e4c9587652d50959ead9e461995389664e490f4dce7ff0f782/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7c60d3c9b06fb23bd9c6139281ccbdc384297579ae037f08ae90c69f6845c0b1", size = 2227742, upload-time = "2026-03-09T13:13:36.4Z" }, + { url = "https://files.pythonhosted.org/packages/c1/43/0499cec932d935229b5543d073c2b87c9c22846aab48881e9d8d6e742a2d/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e315e5ec90d88e140f57696ff85b484ff68bb311e36f2c414aa4286293e6dee0", size = 2323966, upload-time = "2026-03-09T13:13:38.204Z" }, + { url = "https://files.pythonhosted.org/packages/3d/6f/79b0d760907965acfd9d61826a3d41f8f093c538f55cd2633d3f0db269f6/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:1465387ac63576c3e125e5337a6892b9e99e0627d52317f3ca79e6930d889d15", size = 1977417, upload-time = "2026-03-09T13:13:39.966Z" }, + { url = "https://files.pythonhosted.org/packages/ab/31/01d0537c41cb75a551a438c3c7a80d0c60d60b81f694dac83dd436aec0d0/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:530a3fd64c87cffa844d4b6b9768774763d9caa299e9b75d8eca6a4423b31314", size = 2491238, upload-time = "2026-03-09T13:13:41.698Z" }, + { url = "https://files.pythonhosted.org/packages/e4/34/8aefdd0be9cfd00a44509251ba864f5caf2991e36772e61c408007e7f417/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1d9daea4ea6b9be74fe2f01f7fbade8d6ffab263e781274cffca0dba9be9eec9", size = 2294947, upload-time = "2026-03-09T13:13:43.343Z" }, + { url = "https://files.pythonhosted.org/packages/ad/cf/0348374369ca588f8fe9c338fae49fa4e16eeb10ffb3d012f23a54578a9e/kiwisolver-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:f18c2d9782259a6dc132fdc7a63c168cbc74b35284b6d75c673958982a378384", size = 73569, upload-time = "2026-03-09T13:13:45.792Z" }, + { url = "https://files.pythonhosted.org/packages/28/26/192b26196e2316e2bd29deef67e37cdf9870d9af8e085e521afff0fed526/kiwisolver-1.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:f7c7553b13f69c1b29a5bde08ddc6d9d0c8bfb84f9ed01c30db25944aeb852a7", size = 64997, upload-time = "2026-03-09T13:13:46.878Z" }, + { url = "https://files.pythonhosted.org/packages/9d/69/024d6711d5ba575aa65d5538042e99964104e97fa153a9f10bc369182bc2/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:fd40bb9cd0891c4c3cb1ddf83f8bbfa15731a248fdc8162669405451e2724b09", size = 123166, upload-time = "2026-03-09T13:13:48.032Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/adbb40df306f587054a348831220812b9b1d787aff714cfbc8556e38fccd/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c0e1403fd7c26d77c1f03e096dc58a5c726503fa0db0456678b8668f76f521e3", size = 66395, upload-time = "2026-03-09T13:13:49.365Z" }, + { url = "https://files.pythonhosted.org/packages/a8/3a/d0a972b34e1c63e2409413104216cd1caa02c5a37cb668d1687d466c1c45/kiwisolver-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dda366d548e89a90d88a86c692377d18d8bd64b39c1fb2b92cb31370e2896bbd", size = 64065, upload-time = "2026-03-09T13:13:50.562Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0a/7b98e1e119878a27ba8618ca1e18b14f992ff1eda40f47bccccf4de44121/kiwisolver-1.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:332b4f0145c30b5f5ad9374881133e5aa64320428a57c2c2b61e9d891a51c2f3", size = 1477903, upload-time = "2026-03-09T13:13:52.084Z" }, + { url = "https://files.pythonhosted.org/packages/18/d8/55638d89ffd27799d5cc3d8aa28e12f4ce7a64d67b285114dbedc8ea4136/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c50b89ffd3e1a911c69a1dd3de7173c0cd10b130f56222e57898683841e4f96", size = 1278751, upload-time = "2026-03-09T13:13:54.673Z" }, + { url = "https://files.pythonhosted.org/packages/b8/97/b4c8d0d18421ecceba20ad8701358453b88e32414e6f6950b5a4bad54e65/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4db576bb8c3ef9365f8b40fe0f671644de6736ae2c27a2c62d7d8a1b4329f099", size = 1296793, upload-time = "2026-03-09T13:13:56.287Z" }, + { url = "https://files.pythonhosted.org/packages/c4/10/f862f94b6389d8957448ec9df59450b81bec4abb318805375c401a1e6892/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0b85aad90cea8ac6797a53b5d5f2e967334fa4d1149f031c4537569972596cb8", size = 1346041, upload-time = "2026-03-09T13:13:58.269Z" }, + { url = "https://files.pythonhosted.org/packages/a3/6a/f1650af35821eaf09de398ec0bc2aefc8f211f0cda50204c9f1673741ba9/kiwisolver-1.5.0-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:d36ca54cb4c6c4686f7cbb7b817f66f5911c12ddb519450bbe86707155028f87", size = 987292, upload-time = "2026-03-09T13:13:59.871Z" }, + { url = "https://files.pythonhosted.org/packages/de/19/d7fb82984b9238115fe629c915007be608ebd23dc8629703d917dbfaffd4/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:38f4a703656f493b0ad185211ccfca7f0386120f022066b018eb5296d8613e23", size = 2227865, upload-time = "2026-03-09T13:14:01.401Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b9/46b7f386589fd222dac9e9de9c956ce5bcefe2ee73b4e79891381dda8654/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3ac2360e93cb41be81121755c6462cff3beaa9967188c866e5fce5cf13170859", size = 2324369, upload-time = "2026-03-09T13:14:02.972Z" }, + { url = "https://files.pythonhosted.org/packages/92/8b/95e237cf3d9c642960153c769ddcbe278f182c8affb20cecc1cc983e7cc5/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c95cab08d1965db3d84a121f1c7ce7479bdd4072c9b3dafd8fecce48a2e6b902", size = 1977989, upload-time = "2026-03-09T13:14:04.503Z" }, + { url = "https://files.pythonhosted.org/packages/1b/95/980c9df53501892784997820136c01f62bc1865e31b82b9560f980c0e649/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fc20894c3d21194d8041a28b65622d5b86db786da6e3cfe73f0c762951a61167", size = 2491645, upload-time = "2026-03-09T13:14:06.106Z" }, + { url = "https://files.pythonhosted.org/packages/cb/32/900647fd0840abebe1561792c6b31e6a7c0e278fc3973d30572a965ca14c/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7a32f72973f0f950c1920475d5c5ea3d971b81b6f0ec53b8d0a956cc965f22e0", size = 2295237, upload-time = "2026-03-09T13:14:08.891Z" }, + { url = "https://files.pythonhosted.org/packages/be/8a/be60e3bbcf513cc5a50f4a3e88e1dcecebb79c1ad607a7222877becaa101/kiwisolver-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bf3acf1419fa93064a4c2189ac0b58e3be7872bf6ee6177b0d4c63dc4cea276", size = 73573, upload-time = "2026-03-09T13:14:12.327Z" }, + { url = "https://files.pythonhosted.org/packages/4d/d2/64be2e429eb4fca7f7e1c52a91b12663aeaf25de3895e5cca0f47ef2a8d0/kiwisolver-1.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:fa8eb9ecdb7efb0b226acec134e0d709e87a909fa4971a54c0c4f6e88635484c", size = 64998, upload-time = "2026-03-09T13:14:13.469Z" }, + { url = "https://files.pythonhosted.org/packages/b0/69/ce68dd0c85755ae2de490bf015b62f2cea5f6b14ff00a463f9d0774449ff/kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:db485b3847d182b908b483b2ed133c66d88d49cacf98fd278fadafe11b4478d1", size = 125700, upload-time = "2026-03-09T13:14:14.636Z" }, + { url = "https://files.pythonhosted.org/packages/74/aa/937aac021cf9d4349990d47eb319309a51355ed1dbdc9c077cdc9224cb11/kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:be12f931839a3bdfe28b584db0e640a65a8bcbc24560ae3fdb025a449b3d754e", size = 67537, upload-time = "2026-03-09T13:14:15.808Z" }, + { url = "https://files.pythonhosted.org/packages/ee/20/3a87fbece2c40ad0f6f0aefa93542559159c5f99831d596050e8afae7a9f/kiwisolver-1.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:16b85d37c2cbb3253226d26e64663f755d88a03439a9c47df6246b35defbdfb7", size = 65514, upload-time = "2026-03-09T13:14:18.035Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7f/f943879cda9007c45e1f7dba216d705c3a18d6b35830e488b6c6a4e7cdf0/kiwisolver-1.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4432b835675f0ea7414aab3d37d119f7226d24869b7a829caeab49ebda407b0c", size = 1584848, upload-time = "2026-03-09T13:14:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/37/f8/4d4f85cc1870c127c88d950913370dd76138482161cd07eabbc450deff01/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b0feb50971481a2cc44d94e88bdb02cdd497618252ae226b8eb1201b957e368", size = 1391542, upload-time = "2026-03-09T13:14:21.54Z" }, + { url = "https://files.pythonhosted.org/packages/04/0b/65dd2916c84d252b244bd405303220f729e7c17c9d7d33dca6feeff9ffc4/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56fa888f10d0f367155e76ce849fa1166fc9730d13bd2d65a2aa13b6f5424489", size = 1404447, upload-time = "2026-03-09T13:14:23.205Z" }, + { url = "https://files.pythonhosted.org/packages/39/5c/2606a373247babce9b1d056c03a04b65f3cf5290a8eac5d7bdead0a17e21/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:940dda65d5e764406b9fb92761cbf462e4e63f712ab60ed98f70552e496f3bf1", size = 1455918, upload-time = "2026-03-09T13:14:24.74Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d1/c6078b5756670658e9192a2ef11e939c92918833d2745f85cd14a6004bdf/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_39_riscv64.whl", hash = "sha256:89fc958c702ee9a745e4700378f5d23fddbc46ff89e8fdbf5395c24d5c1452a3", size = 1072856, upload-time = "2026-03-09T13:14:26.597Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c8/7def6ddf16eb2b3741d8b172bdaa9af882b03c78e9b0772975408801fa63/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9027d773c4ff81487181a925945743413f6069634d0b122d0b37684ccf4f1e18", size = 2333580, upload-time = "2026-03-09T13:14:28.237Z" }, + { url = "https://files.pythonhosted.org/packages/9e/87/2ac1fce0eb1e616fcd3c35caa23e665e9b1948bb984f4764790924594128/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:5b233ea3e165e43e35dba1d2b8ecc21cf070b45b65ae17dd2747d2713d942021", size = 2423018, upload-time = "2026-03-09T13:14:30.018Z" }, + { url = "https://files.pythonhosted.org/packages/67/13/c6700ccc6cc218716bfcda4935e4b2997039869b4ad8a94f364c5a3b8e63/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ce9bf03dad3b46408c08649c6fbd6ca28a9fce0eb32fdfffa6775a13103b5310", size = 2062804, upload-time = "2026-03-09T13:14:32.888Z" }, + { url = "https://files.pythonhosted.org/packages/1b/bd/877056304626943ff0f1f44c08f584300c199b887cb3176cd7e34f1515f1/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:fc4d3f1fb9ca0ae9f97b095963bc6326f1dbfd3779d6679a1e016b9baaa153d3", size = 2597482, upload-time = "2026-03-09T13:14:34.971Z" }, + { url = "https://files.pythonhosted.org/packages/75/19/c60626c47bf0f8ac5dcf72c6c98e266d714f2fbbfd50cf6dab5ede3aaa50/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f443b4825c50a51ee68585522ab4a1d1257fac65896f282b4c6763337ac9f5d2", size = 2394328, upload-time = "2026-03-09T13:14:36.816Z" }, + { url = "https://files.pythonhosted.org/packages/47/84/6a6d5e5bb8273756c27b7d810d47f7ef2f1f9b9fd23c9ee9a3f8c75c9cef/kiwisolver-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:893ff3a711d1b515ba9da14ee090519bad4610ed1962fbe298a434e8c5f8db53", size = 68410, upload-time = "2026-03-09T13:14:38.695Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/060f45052f2a01ad5762c8fdecd6d7a752b43400dc29ff75cd47225a40fd/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8df31fe574b8b3993cc61764f40941111b25c2d9fea13d3ce24a49907cd2d615", size = 123231, upload-time = "2026-03-09T13:14:41.323Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a7/78da680eadd06ff35edef6ef68a1ad273bad3e2a0936c9a885103230aece/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1d49a49ac4cbfb7c1375301cd1ec90169dfeae55ff84710d782260ce77a75a02", size = 66489, upload-time = "2026-03-09T13:14:42.534Z" }, + { url = "https://files.pythonhosted.org/packages/49/b2/97980f3ad4fae37dd7fe31626e2bf75fbf8bdf5d303950ec1fab39a12da8/kiwisolver-1.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0cbe94b69b819209a62cb27bdfa5dc2a8977d8de2f89dfd97ba4f53ed3af754e", size = 64063, upload-time = "2026-03-09T13:14:44.759Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f9/b06c934a6aa8bc91f566bd2a214fd04c30506c2d9e2b6b171953216a65b6/kiwisolver-1.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80aa065ffd378ff784822a6d7c3212f2d5f5e9c3589614b5c228b311fd3063ac", size = 1475913, upload-time = "2026-03-09T13:14:46.247Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f0/f768ae564a710135630672981231320bc403cf9152b5596ec5289de0f106/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e7f886f47ab881692f278ae901039a234e4025a68e6dfab514263a0b1c4ae05", size = 1282782, upload-time = "2026-03-09T13:14:48.458Z" }, + { url = "https://files.pythonhosted.org/packages/e2/9f/1de7aad00697325f05238a5f2eafbd487fb637cc27a558b5367a5f37fb7f/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5060731cc3ed12ca3a8b57acd4aeca5bbc2f49216dd0bec1650a1acd89486bcd", size = 1300815, upload-time = "2026-03-09T13:14:50.721Z" }, + { url = "https://files.pythonhosted.org/packages/5a/c2/297f25141d2e468e0ce7f7a7b92e0cf8918143a0cbd3422c1ad627e85a06/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a4aa69609f40fce3cbc3f87b2061f042eee32f94b8f11db707b66a26461591a", size = 1347925, upload-time = "2026-03-09T13:14:52.304Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d3/f4c73a02eb41520c47610207b21afa8cdd18fdbf64ffd94674ae21c4812d/kiwisolver-1.5.0-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:d168fda2dbff7b9b5f38e693182d792a938c31db4dac3a80a4888de603c99554", size = 991322, upload-time = "2026-03-09T13:14:54.637Z" }, + { url = "https://files.pythonhosted.org/packages/7b/46/d3f2efef7732fcda98d22bf4ad5d3d71d545167a852ca710a494f4c15343/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:413b820229730d358efd838ecbab79902fe97094565fdc80ddb6b0a18c18a581", size = 2232857, upload-time = "2026-03-09T13:14:56.471Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ec/2d9756bf2b6d26ae4349b8d3662fb3993f16d80c1f971c179ce862b9dbae/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5124d1ea754509b09e53738ec185584cc609aae4a3b510aaf4ed6aa047ef9303", size = 2329376, upload-time = "2026-03-09T13:14:58.072Z" }, + { url = "https://files.pythonhosted.org/packages/8f/9f/876a0a0f2260f1bde92e002b3019a5fabc35e0939c7d945e0fa66185eb20/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e4415a8db000bf49a6dd1c478bf70062eaacff0f462b92b0ba68791a905861f9", size = 1982549, upload-time = "2026-03-09T13:14:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4f/ba3624dfac23a64d54ac4179832860cb537c1b0af06024936e82ca4154a0/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d618fd27420381a4f6044faa71f46d8bfd911bd077c555f7138ed88729bfbe79", size = 2494680, upload-time = "2026-03-09T13:15:01.364Z" }, + { url = "https://files.pythonhosted.org/packages/39/b7/97716b190ab98911b20d10bf92eca469121ec483b8ce0edd314f51bc85af/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5092eb5b1172947f57d6ea7d89b2f29650414e4293c47707eb499ec07a0ac796", size = 2297905, upload-time = "2026-03-09T13:15:03.925Z" }, + { url = "https://files.pythonhosted.org/packages/a3/36/4e551e8aa55c9188bca9abb5096805edbf7431072b76e2298e34fd3a3008/kiwisolver-1.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:d76e2d8c75051d58177e762164d2e9ab92886534e3a12e795f103524f221dd8e", size = 75086, upload-time = "2026-03-09T13:15:07.775Z" }, + { url = "https://files.pythonhosted.org/packages/70/15/9b90f7df0e31a003c71649cf66ef61c3c1b862f48c81007fa2383c8bd8d7/kiwisolver-1.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:fa6248cd194edff41d7ea9425ced8ca3a6f838bfb295f6f1d6e6bb694a8518df", size = 66577, upload-time = "2026-03-09T13:15:09.139Z" }, + { url = "https://files.pythonhosted.org/packages/17/01/7dc8c5443ff42b38e72731643ed7cf1ed9bf01691ae5cdca98501999ed83/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d1ffeb80b5676463d7a7d56acbe8e37a20ce725570e09549fe738e02ca6b7e1e", size = 125794, upload-time = "2026-03-09T13:15:10.525Z" }, + { url = "https://files.pythonhosted.org/packages/46/8a/b4ebe46ebaac6a303417fab10c2e165c557ddaff558f9699d302b256bc53/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc4d8e252f532ab46a1de9349e2d27b91fce46736a9eedaa37beaca66f574ed4", size = 67646, upload-time = "2026-03-09T13:15:12.016Z" }, + { url = "https://files.pythonhosted.org/packages/60/35/10a844afc5f19d6f567359bf4789e26661755a2f36200d5d1ed8ad0126e5/kiwisolver-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6783e069732715ad0c3ce96dbf21dbc2235ab0593f2baf6338101f70371f4028", size = 65511, upload-time = "2026-03-09T13:15:13.311Z" }, + { url = "https://files.pythonhosted.org/packages/f8/8a/685b297052dd041dcebce8e8787b58923b6e78acc6115a0dc9189011c44b/kiwisolver-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e7c4c09a490dc4d4a7f8cbee56c606a320f9dc28cf92a7157a39d1ce7676a657", size = 1584858, upload-time = "2026-03-09T13:15:15.103Z" }, + { url = "https://files.pythonhosted.org/packages/9e/80/04865e3d4638ac5bddec28908916df4a3075b8c6cc101786a96803188b96/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a075bd7bd19c70cf67c8badfa36cf7c5d8de3c9ddb8420c51e10d9c50e94920", size = 1392539, upload-time = "2026-03-09T13:15:16.661Z" }, + { url = "https://files.pythonhosted.org/packages/ba/01/77a19cacc0893fa13fafa46d1bba06fb4dc2360b3292baf4b56d8e067b24/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bdd3e53429ff02aa319ba59dfe4ceeec345bf46cf180ec2cf6fd5b942e7975e9", size = 1405310, upload-time = "2026-03-09T13:15:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/53/39/bcaf5d0cca50e604cfa9b4e3ae1d64b50ca1ae5b754122396084599ef903/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cdcb35dc9d807259c981a85531048ede628eabcffb3239adf3d17463518992d", size = 1456244, upload-time = "2026-03-09T13:15:20.444Z" }, + { url = "https://files.pythonhosted.org/packages/d0/7a/72c187abc6975f6978c3e39b7cf67aeb8b3c0a8f9790aa7fd412855e9e1f/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:70d593af6a6ca332d1df73d519fddb5148edb15cd90d5f0155e3746a6d4fcc65", size = 1073154, upload-time = "2026-03-09T13:15:22.039Z" }, + { url = "https://files.pythonhosted.org/packages/c7/ca/cf5b25783ebbd59143b4371ed0c8428a278abe68d6d0104b01865b1bbd0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:377815a8616074cabbf3f53354e1d040c35815a134e01d7614b7692e4bf8acfa", size = 2334377, upload-time = "2026-03-09T13:15:23.741Z" }, + { url = "https://files.pythonhosted.org/packages/4a/e5/b1f492adc516796e88751282276745340e2a72dcd0d36cf7173e0daf3210/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0255a027391d52944eae1dbb5d4cc5903f57092f3674e8e544cdd2622826b3f0", size = 2425288, upload-time = "2026-03-09T13:15:25.789Z" }, + { url = "https://files.pythonhosted.org/packages/e6/e5/9b21fbe91a61b8f409d74a26498706e97a48008bfcd1864373d32a6ba31c/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:012b1eb16e28718fa782b5e61dc6f2da1f0792ca73bd05d54de6cb9561665fc9", size = 2063158, upload-time = "2026-03-09T13:15:27.63Z" }, + { url = "https://files.pythonhosted.org/packages/b1/02/83f47986138310f95ea95531f851b2a62227c11cbc3e690ae1374fe49f0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e3aafb33aed7479377e5e9a82e9d4bf87063741fc99fc7ae48b0f16e32bdd6f", size = 2597260, upload-time = "2026-03-09T13:15:29.421Z" }, + { url = "https://files.pythonhosted.org/packages/07/18/43a5f24608d8c313dd189cf838c8e68d75b115567c6279de7796197cfb6a/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7a116ae737f0000343218c4edf5bd45893bfeaff0993c0b215d7124c9f77646", size = 2394403, upload-time = "2026-03-09T13:15:31.517Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b5/98222136d839b8afabcaa943b09bd05888c2d36355b7e448550211d1fca4/kiwisolver-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1dd9b0b119a350976a6d781e7278ec7aca0b201e1a9e2d23d9804afecb6ca681", size = 79687, upload-time = "2026-03-09T13:15:33.204Z" }, + { url = "https://files.pythonhosted.org/packages/99/a2/ca7dc962848040befed12732dff6acae7fb3c4f6fc4272b3f6c9a30b8713/kiwisolver-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:58f812017cd2985c21fbffb4864d59174d4903dd66fa23815e74bbc7a0e2dd57", size = 70032, upload-time = "2026-03-09T13:15:34.411Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fa/2910df836372d8761bb6eff7d8bdcb1613b5c2e03f260efe7abe34d388a7/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_10_13_x86_64.whl", hash = "sha256:5ae8e62c147495b01a0f4765c878e9bfdf843412446a247e28df59936e99e797", size = 130262, upload-time = "2026-03-09T13:15:35.629Z" }, + { url = "https://files.pythonhosted.org/packages/0f/41/c5f71f9f00aabcc71fee8b7475e3f64747282580c2fe748961ba29b18385/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f6764a4ccab3078db14a632420930f6186058750df066b8ea2a7106df91d3203", size = 138036, upload-time = "2026-03-09T13:15:36.894Z" }, + { url = "https://files.pythonhosted.org/packages/fa/06/7399a607f434119c6e1fdc8ec89a8d51ccccadf3341dee4ead6bd14caaf5/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c31c13da98624f957b0fb1b5bae5383b2333c2c3f6793d9825dd5ce79b525cb7", size = 194295, upload-time = "2026-03-09T13:15:38.22Z" }, + { url = "https://files.pythonhosted.org/packages/b5/91/53255615acd2a1eaca307ede3c90eb550bae9c94581f8c00081b6b1c8f44/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:1f1489f769582498610e015a8ef2d36f28f505ab3096d0e16b4858a9ec214f57", size = 75987, upload-time = "2026-03-09T13:15:39.65Z" }, + { url = "https://files.pythonhosted.org/packages/e9/eb/5fcbbbf9a0e2c3a35effb88831a483345326bbc3a030a3b5b69aee647f84/kiwisolver-1.5.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ec4c85dc4b687c7f7f15f553ff26a98bfe8c58f5f7f0ac8905f0ba4c7be60232", size = 59532, upload-time = "2026-03-09T13:15:47.047Z" }, + { url = "https://files.pythonhosted.org/packages/c3/9b/e17104555bb4db148fd52327feea1e96be4b88e8e008b029002c281a21ab/kiwisolver-1.5.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:12e91c215a96e39f57989c8912ae761286ac5a9584d04030ceb3368a357f017a", size = 57420, upload-time = "2026-03-09T13:15:48.199Z" }, + { url = "https://files.pythonhosted.org/packages/48/44/2b5b95b7aa39fb2d8d9d956e0f3d5d45aef2ae1d942d4c3ffac2f9cfed1a/kiwisolver-1.5.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be4a51a55833dc29ab5d7503e7bcb3b3af3402d266018137127450005cdfe737", size = 79892, upload-time = "2026-03-09T13:15:49.694Z" }, + { url = "https://files.pythonhosted.org/packages/52/7d/7157f9bba6b455cfb4632ed411e199fc8b8977642c2b12082e1bd9e6d173/kiwisolver-1.5.0-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:daae526907e262de627d8f70058a0f64acc9e2641c164c99c8f594b34a799a16", size = 77603, upload-time = "2026-03-09T13:15:50.945Z" }, + { url = "https://files.pythonhosted.org/packages/0a/dd/8050c947d435c8d4bc94e3252f4d8bb8a76cfb424f043a8680be637a57f1/kiwisolver-1.5.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:59cd8683f575d96df5bb48f6add94afc055012c29e28124fcae2b63661b9efb1", size = 73558, upload-time = "2026-03-09T13:15:52.112Z" }, +] + +[[package]] +name = "matplotlib" +version = "3.10.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "contourpy" }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/1b/4be5be87d43d327a0cf4de1a56e86f7f84c89312452406cf122efe2839e6/matplotlib-3.10.9.tar.gz", hash = "sha256:fd66508e8c6877d98e586654b608a0456db8d7e8a546eb1e2600efd957302358", size = 34811233, upload-time = "2026-04-24T00:14:13.539Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/8c/290f021104741fea63769c31494f5324c0cd249bf536a65a4350767b1f22/matplotlib-3.10.9-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:68cfdcede415f7c8f5577b03303dd94526cdb6d11036cecdc205e08733b2d2bb", size = 8306860, upload-time = "2026-04-24T00:12:01.207Z" }, + { url = "https://files.pythonhosted.org/packages/51/18/325cd32ece1120d1da51cc4e4294c6580190699490183fc2fe8cb6d61ec5/matplotlib-3.10.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfca0129678bd56379db26c52b5d77ed7de314c047492fbdc763aa7501710cfb", size = 8199254, upload-time = "2026-04-24T00:12:04.239Z" }, + { url = "https://files.pythonhosted.org/packages/79/db/e28c1b83e3680740aa78925f5fb2ae4d16207207419ad75ea9fe604f8676/matplotlib-3.10.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e436d155fa8a3399dc62683f8f5d0e2e50d25d0144a73edd73f82eec8f4abfb", size = 8777092, upload-time = "2026-04-24T00:12:06.793Z" }, + { url = "https://files.pythonhosted.org/packages/55/fa/3ce7adfe9ba101748f465211660d9c6374c876b671bdb8c2bb6d347e8b94/matplotlib-3.10.9-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56fc0bd271b00025c6edfdc7c2dcd247372c8e1544971d62e1dc7c17367e8bf9", size = 9595691, upload-time = "2026-04-24T00:12:09.706Z" }, + { url = "https://files.pythonhosted.org/packages/36/c4/6960a76686ed668f2c60f84e9799ba4c0d56abdb36b1577b60c1d061d1ec/matplotlib-3.10.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a5a6104ed666402ba5106d7f36e0e0cdca4e8d7fa4d39708ca88019e2835a2eb", size = 9659771, upload-time = "2026-04-24T00:12:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0d/271aace3342157c64700c9ff4c59c7b392f3dbab393692e8db6fbe7ab96c/matplotlib-3.10.9-cp311-cp311-win_amd64.whl", hash = "sha256:d730e984eddf56974c3e72b6129c7ca462ac38dc624338f4b0b23eb23ecba00f", size = 8205112, upload-time = "2026-04-24T00:12:15.773Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ee/cb57ad4754f3e7b9174ce6ce66d9205fb827067e48a9f58ac09d7e7d6b77/matplotlib-3.10.9-cp311-cp311-win_arm64.whl", hash = "sha256:51bf0ddbdc598e060d46c16b5590708f81a1624cefbaaf62f6a81bf9285b8c80", size = 8132310, upload-time = "2026-04-24T00:12:18.645Z" }, + { url = "https://files.pythonhosted.org/packages/35/c6/5581e26c72233ebb2a2a6fed2d24fb7c66b4700120b813f51b0555acf0b6/matplotlib-3.10.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f0c3c28d9fbcc1fe7a03be236d73430cf6409c41fb2383a7ac52fe932b072cb1", size = 8319908, upload-time = "2026-04-24T00:12:21.323Z" }, + { url = "https://files.pythonhosted.org/packages/b7/18/4880dd762e40cd360c1bf06e890c5a97b997e91cb324602b1a19950ad5ce/matplotlib-3.10.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41cb28c2bd769aa3e98322c6ab09854cbcc52ab69d2759d681bba3e327b2b320", size = 8216016, upload-time = "2026-04-24T00:12:23.4Z" }, + { url = "https://files.pythonhosted.org/packages/32/91/d024616abdba99e83120e07a20658976f6a343646710760c4a51df126029/matplotlib-3.10.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ae20801130378b82d647ff5047c07316295b68dc054ca6b3c13519d0ea624285", size = 8789336, upload-time = "2026-04-24T00:12:26.096Z" }, + { url = "https://files.pythonhosted.org/packages/5c/04/030a2f61ef2158f5e4c259487a92ac877732499fb33d871585d89e03c42d/matplotlib-3.10.9-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c63ebcd8b4b169eb2f5c200552ae6b8be8999a005b6b507ed76fb8d7d674fe2", size = 9604602, upload-time = "2026-04-24T00:12:29.052Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c2/541e4d09d87bb6b5830fc28b4c887a9a8cf4e1c6cee698a8c05552ae2003/matplotlib-3.10.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d75d11c949914165976c621b2324f9ef162af7ebf4b057ddf95dd1dba7e5edcf", size = 9670966, upload-time = "2026-04-24T00:12:32.131Z" }, + { url = "https://files.pythonhosted.org/packages/04/a1/4571fc46e7702de8d0c2dc54ad1b2f8e29328dea3ee90831181f7353d93c/matplotlib-3.10.9-cp312-cp312-win_amd64.whl", hash = "sha256:d091f9d758b34aaaaa6331d13574bf01891d903b3dec59bfff458ef7551de5d6", size = 8217462, upload-time = "2026-04-24T00:12:35.226Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d0/2269edb12aa30c13c8bcc9382892e39943ce1d28aab4ec296e0381798e81/matplotlib-3.10.9-cp312-cp312-win_arm64.whl", hash = "sha256:10cc5ce06d10231c36f40e875f3c7e8050362a4ee8f0ee5d29a6b3277d57bb42", size = 8136688, upload-time = "2026-04-24T00:12:37.442Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d3/8d4f6afbecb49fc04e060a57c0fce39ea51cc163a6bd87303ccd698e4fa6/matplotlib-3.10.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b580440f1ff81a0e34122051a3dfabb7e4b7f9e380629929bde0eff9af72165f", size = 8320331, upload-time = "2026-04-24T00:12:39.688Z" }, + { url = "https://files.pythonhosted.org/packages/63/d9/9e14bc7564bf92d5ffa801ae5fac819ce74b925dfb55e3ebde61a3bbad3e/matplotlib-3.10.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b1b745c489cd1a77a0dc1120a05dc87af9798faebc913601feb8c73d89bf2d1e", size = 8216461, upload-time = "2026-04-24T00:12:42.494Z" }, + { url = "https://files.pythonhosted.org/packages/8a/17/4402d0d14ccf1dfc70932600b68097fbbf9c898a4871d2cbbe79c7801a32/matplotlib-3.10.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8f3bcac1ca5ed000a6f4337d47ba67dfddf37ed6a46c15fd7f014997f7bf865f", size = 8790091, upload-time = "2026-04-24T00:12:44.789Z" }, + { url = "https://files.pythonhosted.org/packages/3e/0b/322aeec06dd9b91411f92028b37d447342770a24392aa4813e317064dad5/matplotlib-3.10.9-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a8d66a55def891c33147ba3ba9bfcabf0b526a43764c818acbb4525e5ed0838", size = 9605027, upload-time = "2026-04-24T00:12:47.583Z" }, + { url = "https://files.pythonhosted.org/packages/74/88/5f13482f55e7b00bcfc09838b093c2456e1379978d2a146844aae05350ad/matplotlib-3.10.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d843374407c4017a6403b59c6c81606773d136f3259d5b6da3131bc814542cc2", size = 9671269, upload-time = "2026-04-24T00:12:50.878Z" }, + { url = "https://files.pythonhosted.org/packages/c5/e0/0840fd2f93da988ec660b8ad1984abe9f25d2aed22a5e394ff1c68c88307/matplotlib-3.10.9-cp313-cp313-win_amd64.whl", hash = "sha256:f4399f64b3e94cd500195490972ae1ee81170df1636fa15364d157d5bdd7b921", size = 8217588, upload-time = "2026-04-24T00:12:53.784Z" }, + { url = "https://files.pythonhosted.org/packages/47/b9/d706d06dd605c49b9f83a2aed8c13e3e5db70697d7a80b7e3d7915de6b17/matplotlib-3.10.9-cp313-cp313-win_arm64.whl", hash = "sha256:ba7b3b8ef09eab7df0e86e9ae086faa433efbfbdb46afcb3aa16aabf779469a8", size = 8136913, upload-time = "2026-04-24T00:12:56.501Z" }, + { url = "https://files.pythonhosted.org/packages/9b/45/6e32d96978264c8ca8c4b1010adb955a1a49cfaf314e212bbc8908f04a61/matplotlib-3.10.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:09218df8a93712bd6ea133e83a153c755448cf7868316c531cffcc43f69d1cc9", size = 8368019, upload-time = "2026-04-24T00:12:58.896Z" }, + { url = "https://files.pythonhosted.org/packages/86/0a/c8e3d3bba245f0f7fc424937f8ff7ef77291a36af3edb97ccd78aa93d84f/matplotlib-3.10.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:82368699727bfb7b0182e1aa13082e3c08e092fa1a25d3e1fd92405bff96f6d4", size = 8264645, upload-time = "2026-04-24T00:13:01.406Z" }, + { url = "https://files.pythonhosted.org/packages/3d/aa/5bf5a14fe4fed73a4209a155606f8096ff797aad89c6c35179026571133e/matplotlib-3.10.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3225f4e1edcb8c86c884ddf79ebe20ecd0a67d30188f279897554ccd8fded4dc", size = 8802194, upload-time = "2026-04-24T00:13:03.702Z" }, + { url = "https://files.pythonhosted.org/packages/dd/5e/b4be852d6bba6fd15893fadf91ff26ae49cb91aac789e95dde9d342e664f/matplotlib-3.10.9-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de2445a0c6690d21b7eb6ce071cebad6d40a2e9bdf10d039074a96ba19797b99", size = 9622684, upload-time = "2026-04-24T00:13:06.647Z" }, + { url = "https://files.pythonhosted.org/packages/4c/3d/ed428c971139112ef730f62770654d609467346d09d4b62617e1afd68a5a/matplotlib-3.10.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:b2b9516251cb89ff618d757daec0e2ed1bf21248013844a853d87ef85ab3081d", size = 9680790, upload-time = "2026-04-24T00:13:10.009Z" }, + { url = "https://files.pythonhosted.org/packages/e7/09/052e884aaf2b985c63cb79f715f1d5b6a3eaa7de78f6a52b9dbc077d5b53/matplotlib-3.10.9-cp313-cp313t-win_amd64.whl", hash = "sha256:e9fae004b941b23ff2edcf1567a857ed77bafc8086ffa258190462328434faf8", size = 8287571, upload-time = "2026-04-24T00:13:13.087Z" }, + { url = "https://files.pythonhosted.org/packages/f4/38/ae27288e788c35a4250491422f3db7750366fc8c97d6f36fbdecfc1f5518/matplotlib-3.10.9-cp313-cp313t-win_arm64.whl", hash = "sha256:6b63d9c7c769b88ab81e10dc86e4e0607cf56817b9f9e6cf24b2a5f1693b8e38", size = 8188292, upload-time = "2026-04-24T00:13:15.546Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e6/3bd8afd04949f02eabc1c17115ea5255e19cacd4d06fc5abdde4eeb0052c/matplotlib-3.10.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:172db52c9e683f5d12eaf57f0f54834190e12581fe1cc2a19595a8f5acb4e77d", size = 8321276, upload-time = "2026-04-24T00:13:18.318Z" }, + { url = "https://files.pythonhosted.org/packages/41/86/86231232fff41c9f8e4a1a7d7a597d349a02527109c3af7d618366122139/matplotlib-3.10.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97e35e8d39ccc85859095e01a53847432ba9a53ddf7986f7a54a11b73d0e143f", size = 8218218, upload-time = "2026-04-24T00:13:20.974Z" }, + { url = "https://files.pythonhosted.org/packages/85/8f/becc9722cafc64f5d2eb0b7c1bf5f585271c618a45dbd8fabeb021f898b6/matplotlib-3.10.9-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aba1615dabe83188e19d4f75a253c6a08423e04c1425e64039f800050a69de6b", size = 9608145, upload-time = "2026-04-24T00:13:23.228Z" }, + { url = "https://files.pythonhosted.org/packages/32/5d/f7e914f7d9325abff4057cee62c0fa70263683189f774473cbfb534cd13b/matplotlib-3.10.9-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34cf8167e023ad956c15f36302911d5406bd99a9862c1a8499ea6f7c0e015dc2", size = 9885085, upload-time = "2026-04-24T00:13:25.849Z" }, + { url = "https://files.pythonhosted.org/packages/a5/fd/fa69f2221534e80cc5772ac2b7d222011a2acafc2ec7216d5dd174c864ae/matplotlib-3.10.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:59476c6d29d612b8e9bb6ce8c5b631be6ba8f9e3a2421f22a02b192c7dd28716", size = 9672358, upload-time = "2026-04-24T00:13:28.906Z" }, + { url = "https://files.pythonhosted.org/packages/ab/1a/5a4f747a8b271cbb024946d2dd3c913ab5032ba430626f8c3528ada96b4b/matplotlib-3.10.9-cp314-cp314-win_amd64.whl", hash = "sha256:336b9acc64d309063126edcdaca00db9373af3c476bb94388fe9c5a53ad13e6f", size = 8349970, upload-time = "2026-04-24T00:13:31.904Z" }, + { url = "https://files.pythonhosted.org/packages/64/dc/95d60ecaefe30680a154b52ea96ab4b0dab547f1fd6aa12f5fb655e89cae/matplotlib-3.10.9-cp314-cp314-win_arm64.whl", hash = "sha256:2dc9477819ffd78ad12a20df1d9d6a6bd4fec6aaa9072681465fddca052f1456", size = 8272785, upload-time = "2026-04-24T00:13:34.511Z" }, + { url = "https://files.pythonhosted.org/packages/70/a0/005d68bc8b8418300ce6591f18586910a8526806e2ab663933d9f20a41e9/matplotlib-3.10.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:da4e09638420548f31c354032a6250e473c68e5a4e96899b4844cf39ddea23fe", size = 8367999, upload-time = "2026-04-24T00:13:36.962Z" }, + { url = "https://files.pythonhosted.org/packages/22/05/1236cc9290be70b2498af20ca348add76e3fffe7f67b477db5133a84f3ea/matplotlib-3.10.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:345f6f68ecc8da0ca56fad2ea08fde1a115eda530079eca185d50a7bc3e146c6", size = 8264543, upload-time = "2026-04-24T00:13:39.851Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c2/071f5a5ff6c5bd63aaaf2f45c811d9bf2ced94bde188d9e1a519e21d0cba/matplotlib-3.10.9-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4edcfbd8565339aa62f1cd4012f7180926fdbe71850f7b0d3c379c175cd6b66c", size = 9622800, upload-time = "2026-04-24T00:13:42.296Z" }, + { url = "https://files.pythonhosted.org/packages/95/57/da7d1f10a85624b9e7db68e069dd94e58dc41dbf9463c5921632ecbe3661/matplotlib-3.10.9-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6be157fe17fc37cb95ac1d7374cf717ce9259616edec911a78d9d26dae8522d4", size = 9888561, upload-time = "2026-04-24T00:13:45.026Z" }, + { url = "https://files.pythonhosted.org/packages/67/b2/ef8d6bb59b0edb6c16c968b70f548aa13b54348972def5aa6ac85df67145/matplotlib-3.10.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4e42042d54db34fda4e95a7bd3e5789c2a995d2dad3eb8850232ee534092fbbf", size = 9680884, upload-time = "2026-04-24T00:13:48.066Z" }, + { url = "https://files.pythonhosted.org/packages/61/1c/d21bfeb9931881ebe96bcfcff27c7ae4b160ae0ec291a714c42641a56d75/matplotlib-3.10.9-cp314-cp314t-win_amd64.whl", hash = "sha256:c27df8b3848f32a83d1767566595e43cfaa4460380974da06f4279a7ec143c39", size = 8432333, upload-time = "2026-04-24T00:13:51.008Z" }, + { url = "https://files.pythonhosted.org/packages/78/23/92493c3e6e1b635ccfff146f7b99e674808787915420373ac399283764c2/matplotlib-3.10.9-cp314-cp314t-win_arm64.whl", hash = "sha256:a49f1eadc84ca85fd72fa4e89e70e61bf86452df6f971af04b12c60761a0772c", size = 8324785, upload-time = "2026-04-24T00:13:53.633Z" }, + { url = "https://files.pythonhosted.org/packages/63/e2/9f66ca6a651a52abfe0d4964ce01439ed34f3f1e119de10ff3a07f403043/matplotlib-3.10.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:42fb814efabe95c06c1994d8ab5a8385f43a249e23badd3ba931d4308e5bca20", size = 8304420, upload-time = "2026-04-24T00:14:04.57Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e8/467c03568218792906aa87b5e7bb379b605e056ed0c74fe00c051786d925/matplotlib-3.10.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f76e640a5268850bfda54b5131b1b1941cc685e42c5fa98ed9f2d64038308cba", size = 8197981, upload-time = "2026-04-24T00:14:07.233Z" }, + { url = "https://files.pythonhosted.org/packages/6f/87/afead29192170917537934c6aff4b008c805fff7b1ccea0c79120d96beda/matplotlib-3.10.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3fc0364dfbe1d07f6d15c5ebd0c5bf89e126916e5a8667dd4a7a6e84c36653d4", size = 8774002, upload-time = "2026-04-24T00:14:09.816Z" }, +] + +[[package]] +name = "numpy" +version = "2.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/49/ec46835a70be8fa6446c495126ac84fdb28cb2558e1620ffb87a10c8b64c/numpy-2.4.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4", size = 16969194, upload-time = "2026-05-18T23:33:13.503Z" }, + { url = "https://files.pythonhosted.org/packages/0e/0d/f5957185c0ee2f3e12f78715aa9e3b353fd83633316c8532b38faa37e3f6/numpy-2.4.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d", size = 14964111, upload-time = "2026-05-18T23:33:17.795Z" }, + { url = "https://files.pythonhosted.org/packages/ad/40/40a40ee0ddf7ceb782c49af278894b686e586d65d8c1889c8b5da01a3d7d/numpy-2.4.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8", size = 5469159, upload-time = "2026-05-18T23:33:20.654Z" }, + { url = "https://files.pythonhosted.org/packages/63/13/f9a8046535cb21deae82f8d03de9617e08882d274fad2539630761888228/numpy-2.4.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8155154c7c691289fe18f510b5d4657c68c67989f293f0535a91360392ff6538", size = 6798936, upload-time = "2026-05-18T23:33:22.987Z" }, + { url = "https://files.pythonhosted.org/packages/33/a8/6fa8c1a345a8c85dbb21932c447bee07c30a2c2a3f31e369c0a84b300147/numpy-2.4.6-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47", size = 15966692, upload-time = "2026-05-18T23:33:26.62Z" }, + { url = "https://files.pythonhosted.org/packages/02/03/74fe2a4cb3817d94d86402f2506554130a2f01414e299b5a843e5a8a957f/numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93", size = 16918164, upload-time = "2026-05-18T23:33:29.955Z" }, + { url = "https://files.pythonhosted.org/packages/c5/80/3615be3313f7e7696609bc194b9f0101da809df79e859bdb84e0cd043f46/numpy-2.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8", size = 17322877, upload-time = "2026-05-18T23:33:34.724Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ac/a691e0fe2675e370d0e08ff905adc49a1c8830e8cae03efe4477e92cd55d/numpy-2.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6", size = 18651487, upload-time = "2026-05-18T23:33:38.217Z" }, + { url = "https://files.pythonhosted.org/packages/15/a7/9bc1cd626d7bf6869bfedf27b91b6ab5dd607758bf8e959d6fa80c6a59cb/numpy-2.4.6-cp311-cp311-win32.whl", hash = "sha256:ddea102b48f9e339f3948bf22040944184627a30fdf7f858667673b9c5f033c8", size = 6233945, upload-time = "2026-05-18T23:33:41.331Z" }, + { url = "https://files.pythonhosted.org/packages/c5/31/7fc6239c12bce7e931463251cca4426c465e1876ba3cc785402ef4dd8f4e/numpy-2.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:1e254a00cdf42b1e4d5b3d68d33af63268d41340d8885df2ab6470f2e1500147", size = 12608406, upload-time = "2026-05-18T23:33:44.131Z" }, + { url = "https://files.pythonhosted.org/packages/27/83/140f85a466595a16382996a1bf06b2b54bcd597488921b0c9daaeeda72af/numpy-2.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:ed9749eef4cbd126da3dc1d6bcb3a57f5eb7ac6a6484146bdbf743f552dfc577", size = 10479528, upload-time = "2026-05-18T23:33:50.725Z" }, + { url = "https://files.pythonhosted.org/packages/95/2a/3d7b5ac8aac24feaf9ad7ed58f45b0bbc06d37e4338ae84c9f2298b570f9/numpy-2.4.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1", size = 16689119, upload-time = "2026-05-18T23:33:54.065Z" }, + { url = "https://files.pythonhosted.org/packages/ea/12/92c4c131527599e8288d6918e888d88726f84d805d784b771f32408aeaef/numpy-2.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb", size = 14699246, upload-time = "2026-05-18T23:33:57.621Z" }, + { url = "https://files.pythonhosted.org/packages/ad/fe/c0a6b7b2ca128a8fb228575147073b660656734b8ebe4d76c8fd748dcc79/numpy-2.4.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41", size = 5204410, upload-time = "2026-05-18T23:34:00.302Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d4/9770d14ba719432bb90a421bfd443872ed0f70f7264b64bec12ea363d5fd/numpy-2.4.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698", size = 6551240, upload-time = "2026-05-18T23:34:02.852Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c6/50a46a6205feba2343f1d6d17438107c5dc491ed1c736e6ea68689fd906b/numpy-2.4.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f", size = 15671012, upload-time = "2026-05-18T23:34:05.485Z" }, + { url = "https://files.pythonhosted.org/packages/99/60/14115e6364fa676c5397c2ad3004e527e9aa487abf5d0706ec81bbd08529/numpy-2.4.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853", size = 16645538, upload-time = "2026-05-18T23:34:09.265Z" }, + { url = "https://files.pythonhosted.org/packages/ae/c5/693cbe59e57db94d2231fa519ca3978dc9e19da5a8f088588f5c6e947ff2/numpy-2.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a", size = 17020706, upload-time = "2026-05-18T23:34:13.053Z" }, + { url = "https://files.pythonhosted.org/packages/ef/fc/85b7c4eff9b4966ade25c2273cf7e7012e92366c032058653934b37de044/numpy-2.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2", size = 18368541, upload-time = "2026-05-18T23:34:17.024Z" }, + { url = "https://files.pythonhosted.org/packages/f6/81/e1b27545deedce7f4a0b348618c6b62d74e36a4dc9ccd42f3eb2f85eee32/numpy-2.4.6-cp312-cp312-win32.whl", hash = "sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45", size = 5962825, upload-time = "2026-05-18T23:34:20.3Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ca/feab00bd44aa5fe1ad2c18f08b4d3bb92e26484b0b1d1443897809ed528c/numpy-2.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751", size = 12321687, upload-time = "2026-05-18T23:34:23.095Z" }, + { url = "https://files.pythonhosted.org/packages/63/cf/5a6d34850a39d1093558564f77ee8e8e0bee5061151b8f05a55711001ec7/numpy-2.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8", size = 10221482, upload-time = "2026-05-18T23:34:25.876Z" }, + { url = "https://files.pythonhosted.org/packages/fb/82/bdab26d7438c6791ca31b7c024ca37c1eab8b726ba236129005cd4a06e45/numpy-2.4.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0", size = 16684648, upload-time = "2026-05-18T23:34:29.41Z" }, + { url = "https://files.pythonhosted.org/packages/1b/30/a80189bcc7f5e4258b3fbc3968d909d1756f54d023299ecc39ad6fdb9ef8/numpy-2.4.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb", size = 14693902, upload-time = "2026-05-18T23:34:33.013Z" }, + { url = "https://files.pythonhosted.org/packages/97/12/70b5d0d7c15e1ebb8a6a84a8caa1d19e181d84fb58bb6d70aca29099dec1/numpy-2.4.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f", size = 5198992, upload-time = "2026-05-18T23:34:36.132Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/ebd2a8f8a83541f8d38cc5667e8c2b69cecfd30da6e45693e8158857d44b/numpy-2.4.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3", size = 6546944, upload-time = "2026-05-18T23:34:38.484Z" }, + { url = "https://files.pythonhosted.org/packages/bb/c5/7b863a97a91671a0338f4253bd3b5a3d3852f0692dae91711c9f4a10e787/numpy-2.4.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b", size = 15669392, upload-time = "2026-05-18T23:34:41.257Z" }, + { url = "https://files.pythonhosted.org/packages/a5/9d/3584b9984ca4c047aea75214ce1a4c4c73d849bd71b604264b7f5653f8a8/numpy-2.4.6-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089", size = 16633220, upload-time = "2026-05-18T23:34:45.075Z" }, + { url = "https://files.pythonhosted.org/packages/05/ae/7c67fba23bd98caec7c99261f3a16072ade14813486b0282cb29846de832/numpy-2.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a", size = 17020800, upload-time = "2026-05-18T23:34:49.065Z" }, + { url = "https://files.pythonhosted.org/packages/d9/5d/3b6725cb31d983c5e66916f5d36f6d7e5521129e4c4404d64f918292a5b6/numpy-2.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605", size = 18357600, upload-time = "2026-05-18T23:34:52.709Z" }, + { url = "https://files.pythonhosted.org/packages/f7/da/2ccc6c2fe8898dee01d90c75c5f5f914a23daf99e3e0f59516a08760c8b5/numpy-2.4.6-cp313-cp313-win32.whl", hash = "sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91", size = 5961134, upload-time = "2026-05-18T23:34:55.618Z" }, + { url = "https://files.pythonhosted.org/packages/b5/cd/9cc4dc876fb065d5c220aae4d5e14826b2715331bb7618ce1fb07a679d99/numpy-2.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359", size = 12318598, upload-time = "2026-05-18T23:34:58.928Z" }, + { url = "https://files.pythonhosted.org/packages/39/1e/c0bcba1f8694116485fe28fd1be698c278fcda4141c5b0e53a2aed8b12a8/numpy-2.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778", size = 10222272, upload-time = "2026-05-18T23:35:02.167Z" }, + { url = "https://files.pythonhosted.org/packages/63/6d/cc5619247c8f4204e507f5883528372e4ac4bb189e579fb859a12e480b1f/numpy-2.4.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1", size = 14821197, upload-time = "2026-05-18T23:35:05.468Z" }, + { url = "https://files.pythonhosted.org/packages/00/58/f1c39161c87d9e9bed660f1ed4bafc0e403d5ec9650b6dd77aead07d489b/numpy-2.4.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe", size = 5326287, upload-time = "2026-05-18T23:35:08.693Z" }, + { url = "https://files.pythonhosted.org/packages/af/57/3917ab0fd97f271a8694513581b8a36c655f111c446852c302f04ccdb6fc/numpy-2.4.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997", size = 6646763, upload-time = "2026-05-18T23:35:11.459Z" }, + { url = "https://files.pythonhosted.org/packages/eb/0f/037e64c494b67581ae18193d770adef354c41f3f2c8ebf865602d949bf8f/numpy-2.4.6-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20", size = 15728070, upload-time = "2026-05-18T23:35:14.79Z" }, + { url = "https://files.pythonhosted.org/packages/21/a6/5d2bae9c9542eb4df16dc9c46dc79c186e9bad53805dfa5399a6023c6db0/numpy-2.4.6-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d", size = 16681752, upload-time = "2026-05-18T23:35:18.836Z" }, + { url = "https://files.pythonhosted.org/packages/92/14/23d1dfb410ae362cd59ce53e936b1513d545eb40db3949ced632e19a459e/numpy-2.4.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67", size = 17086024, upload-time = "2026-05-18T23:35:22.52Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6e/23595a2c642cdf3bc567877064bdd7f91c8b0038a4453cf2daf7248eafe9/numpy-2.4.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd", size = 18403398, upload-time = "2026-05-18T23:35:26.398Z" }, + { url = "https://files.pythonhosted.org/packages/8a/90/0ac3bc947217e66dec77e7cbc6a1979d1af70b6461b82f620d3bccd5e4c8/numpy-2.4.6-cp313-cp313t-win32.whl", hash = "sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab", size = 6084971, upload-time = "2026-05-18T23:35:29.387Z" }, + { url = "https://files.pythonhosted.org/packages/77/71/5673e351671a1d2bd6063b91b44f70c0affea7d1516fa7a6572941ba4aa1/numpy-2.4.6-cp313-cp313t-win_amd64.whl", hash = "sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75", size = 12458532, upload-time = "2026-05-18T23:35:32.175Z" }, + { url = "https://files.pythonhosted.org/packages/3f/88/19d3503c5046e688f049274b27a3ef3d771152fa80d3ba3d01a3dff61abe/numpy-2.4.6-cp313-cp313t-win_arm64.whl", hash = "sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd", size = 10291881, upload-time = "2026-05-18T23:35:35.465Z" }, + { url = "https://files.pythonhosted.org/packages/f8/91/3ab2044d05fd16d343c5ac2e69b127f1b2854040dd20b193257c78028bd3/numpy-2.4.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079", size = 16683458, upload-time = "2026-05-18T23:35:38.353Z" }, + { url = "https://files.pythonhosted.org/packages/8e/62/764ce66fa4147ae6d73071a3abf804ffe606f174618697c571acdf26a7c9/numpy-2.4.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7", size = 14704559, upload-time = "2026-05-18T23:35:42.14Z" }, + { url = "https://files.pythonhosted.org/packages/60/61/23f27c172f022e04025b7dc2367f4d63c1a398120607ec896228649a6f48/numpy-2.4.6-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5", size = 5209716, upload-time = "2026-05-18T23:35:45.377Z" }, + { url = "https://files.pythonhosted.org/packages/03/71/21cf70dc6ea3e3acb95fc53a265b2fc248b981f0194ceb5b475271b8809d/numpy-2.4.6-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096", size = 6543947, upload-time = "2026-05-18T23:35:47.926Z" }, + { url = "https://files.pythonhosted.org/packages/d5/91/64288395ee1799bd2e0b04a305dce9666da90c961e1f3fe982a05ee1c036/numpy-2.4.6-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b", size = 15685197, upload-time = "2026-05-18T23:35:50.863Z" }, + { url = "https://files.pythonhosted.org/packages/f3/eb/ebffaa97dc55502df69584a8f0dcf07f69a3e0b3e2323670a2722db9aa39/numpy-2.4.6-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8", size = 16638245, upload-time = "2026-05-18T23:35:54.752Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0b/54f9da33128d7e350fab89c7455902eeae70349ee52bddb448dc4a576f45/numpy-2.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402", size = 17036587, upload-time = "2026-05-18T23:35:58.355Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f0/fdebc1052db1cc37c64beb22072d67cd6d1c71adca1299f53dec2b5e20d3/numpy-2.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb", size = 18363226, upload-time = "2026-05-18T23:36:02.845Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b4/298628d98c72b57e57f7165ae6a481a1deaf6f3c28262a6e4c739c275930/numpy-2.4.6-cp314-cp314-win32.whl", hash = "sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1", size = 6010196, upload-time = "2026-05-18T23:36:05.92Z" }, + { url = "https://files.pythonhosted.org/packages/df/ac/46de6dda46478f7942f839e094970be2d4a861e005c4b3bf07c92e291a09/numpy-2.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261", size = 12450334, upload-time = "2026-05-18T23:36:09.107Z" }, + { url = "https://files.pythonhosted.org/packages/78/92/b8b798ac784102c0da830d2257d59358e3d3d90d1e2b3f2575dad976c5cf/numpy-2.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6", size = 10495678, upload-time = "2026-05-18T23:36:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/30/34/ec28d1aa8115971537c01469ab2011ee96827930f0a124de1000cc2a7ed7/numpy-2.4.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a", size = 14823672, upload-time = "2026-05-18T23:36:16.473Z" }, + { url = "https://files.pythonhosted.org/packages/16/bd/f6d1fede4e54e8042a7ff97bb495510f3c220f94bcd9e8b228e87c92cc0d/numpy-2.4.6-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e", size = 5328731, upload-time = "2026-05-18T23:36:19.767Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f0/e105b9e2fd728a9910103884decd6951d9dd73896b914a98d9a231de02ee/numpy-2.4.6-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e", size = 6649805, upload-time = "2026-05-18T23:36:22.266Z" }, + { url = "https://files.pythonhosted.org/packages/82/dd/1206a7ca6ab15e3f02069707ca96222e202af681bb73756da7527f3cb837/numpy-2.4.6-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43", size = 15730496, upload-time = "2026-05-18T23:36:25.713Z" }, + { url = "https://files.pythonhosted.org/packages/51/e7/38d3ea825dcab85a591734decb2f6c67caa7c8367d374df1a1c3842f9b07/numpy-2.4.6-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e", size = 16679616, upload-time = "2026-05-18T23:36:29.652Z" }, + { url = "https://files.pythonhosted.org/packages/93/b7/caabfdf53edf663e0b4eb74d7d405d83baef09eb5e83bcd32d601d72b93e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895", size = 17085145, upload-time = "2026-05-18T23:36:33.449Z" }, + { url = "https://files.pythonhosted.org/packages/f9/45/68d7c33a6bcf3e5aa3bdbd57a367e6f615286dfd6482f97e8ffeb734306e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4", size = 18403813, upload-time = "2026-05-18T23:36:37.369Z" }, + { url = "https://files.pythonhosted.org/packages/9c/50/0753655aa844c99cd9e018aacf76f130f1bd81d881bb74bc0aef5d73a8ba/numpy-2.4.6-cp314-cp314t-win32.whl", hash = "sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063", size = 6156982, upload-time = "2026-05-18T23:36:40.817Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d4/7c67becf668f973cb490cec3e98dfd799d866f9c989a54d355672cfa0db6/numpy-2.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627", size = 12638908, upload-time = "2026-05-18T23:36:43.996Z" }, + { url = "https://files.pythonhosted.org/packages/43/bb/e1c71a4295b1b1d1393d50dbb4f2a36283c6859d9d3892e84f00ec5a91d5/numpy-2.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66", size = 10565867, upload-time = "2026-05-18T23:36:47.114Z" }, + { url = "https://files.pythonhosted.org/packages/de/12/b422cc84439adc0d00de605bf4a308890ae5c26f2c71fbd73e5d08fbb0dd/numpy-2.4.6-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:55cced7c52e981362f708ad635198e97a752dfba412cc03c23bbf3bd8d5cd662", size = 16847511, upload-time = "2026-05-18T23:36:50.673Z" }, + { url = "https://files.pythonhosted.org/packages/44/53/f481bef68011740f8849418d82db07230e825013f31f4eef5ba5b805316a/numpy-2.4.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7", size = 14889064, upload-time = "2026-05-18T23:36:53.879Z" }, + { url = "https://files.pythonhosted.org/packages/7f/57/42ed575c10ced8af951d426bc4e1f8aff16fd851db33f067036215a7f860/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f", size = 5394157, upload-time = "2026-05-18T23:36:57.194Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ef/f66cc724fcc36c1e364c67f51ae9146090b8b584f27d58b97fdae3edd737/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:948424b06129ce883307e8cff868c31396d8dc7630a59c61d70d98dbe70f222c", size = 6708728, upload-time = "2026-05-18T23:36:59.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/9c/c531f2293b91265d8b48e9b329f54fdd7ffae73cb4134ea10cca4237e9cc/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0", size = 15798374, upload-time = "2026-05-18T23:37:02.674Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b0/413077f6b1153ed3cba361401c6783bbad6114804a000cc22eb71c13e190/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02", size = 16747286, upload-time = "2026-05-18T23:37:06.327Z" }, + { url = "https://files.pythonhosted.org/packages/15/ce/e5ec180bc41812edcd8daeb8639d205622c0e8c02259d8ab25a0201b3c2a/numpy-2.4.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73", size = 12504263, upload-time = "2026-05-18T23:37:09.715Z" }, +] + +[[package]] +name = "openai" +version = "2.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8f/12/cfa322c5f5dd8fa21aab9a7a8e979e7a11123800f86ca8d82eb68a83d213/openai-2.38.0.tar.gz", hash = "sha256:798694c6cf74145541fda94325b6f8f72d8e1fd0262cc137c8d728177a6a4ce3", size = 772764, upload-time = "2026-05-21T21:23:42.105Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/bf/ccff9be562e24207716d04ef9dc931c76aff0c89a7265da43e2104d7fe06/openai-2.38.0-py3-none-any.whl", hash = "sha256:ec6661c57b2dcc47414a767e6e3335c7ed3d19c9696999283a3c82e95c756a3c", size = 1344910, upload-time = "2026-05-21T21:23:39.636Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pillow" +version = "12.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/e1/748f5663efe6edcfc4e74b2b93edfb9b8b99b67f21a854c3ae416500a2d9/pillow-12.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:8be29e59487a79f173507c30ddf57e733a357f67881430449bb32614075a40ab", size = 5354347, upload-time = "2026-04-01T14:42:44.255Z" }, + { url = "https://files.pythonhosted.org/packages/47/a1/d5ff69e747374c33a3b53b9f98cca7889fce1fd03d79cdc4e1bccc6c5a87/pillow-12.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71cde9a1e1551df7d34a25462fc60325e8a11a82cc2e2f54578e5e9a1e153d65", size = 4695873, upload-time = "2026-04-01T14:42:46.452Z" }, + { url = "https://files.pythonhosted.org/packages/df/21/e3fbdf54408a973c7f7f89a23b2cb97a7ef30c61ab4142af31eee6aebc88/pillow-12.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f490f9368b6fc026f021db16d7ec2fbf7d89e2edb42e8ec09d2c60505f5729c7", size = 6280168, upload-time = "2026-04-01T14:42:49.228Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f1/00b7278c7dd52b17ad4329153748f87b6756ec195ff786c2bdf12518337d/pillow-12.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8bd7903a5f2a4545f6fd5935c90058b89d30045568985a71c79f5fd6edf9b91e", size = 8088188, upload-time = "2026-04-01T14:42:51.735Z" }, + { url = "https://files.pythonhosted.org/packages/ad/cf/220a5994ef1b10e70e85748b75649d77d506499352be135a4989c957b701/pillow-12.2.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3997232e10d2920a68d25191392e3a4487d8183039e1c74c2297f00ed1c50705", size = 6394401, upload-time = "2026-04-01T14:42:54.343Z" }, + { url = "https://files.pythonhosted.org/packages/e9/bd/e51a61b1054f09437acfbc2ff9106c30d1eb76bc1453d428399946781253/pillow-12.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e74473c875d78b8e9d5da2a70f7099549f9eb37ded4e2f6a463e60125bccd176", size = 7079655, upload-time = "2026-04-01T14:42:56.954Z" }, + { url = "https://files.pythonhosted.org/packages/6b/3d/45132c57d5fb4b5744567c3817026480ac7fc3ce5d4c47902bc0e7f6f853/pillow-12.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:56a3f9c60a13133a98ecff6197af34d7824de9b7b38c3654861a725c970c197b", size = 6503105, upload-time = "2026-04-01T14:42:59.847Z" }, + { url = "https://files.pythonhosted.org/packages/7d/2e/9df2fc1e82097b1df3dce58dc43286aa01068e918c07574711fcc53e6fb4/pillow-12.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90e6f81de50ad6b534cab6e5aef77ff6e37722b2f5d908686f4a5c9eba17a909", size = 7203402, upload-time = "2026-04-01T14:43:02.664Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2e/2941e42858ebb67e50ae741473de81c2984e6eff7b397017623c676e2e8d/pillow-12.2.0-cp311-cp311-win32.whl", hash = "sha256:8c984051042858021a54926eb597d6ee3012393ce9c181814115df4c60b9a808", size = 6378149, upload-time = "2026-04-01T14:43:05.274Z" }, + { url = "https://files.pythonhosted.org/packages/69/42/836b6f3cd7f3e5fa10a1f1a5420447c17966044c8fbf589cc0452d5502db/pillow-12.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e6b2a0c538fc200b38ff9eb6628228b77908c319a005815f2dde585a0664b60", size = 7082626, upload-time = "2026-04-01T14:43:08.557Z" }, + { url = "https://files.pythonhosted.org/packages/c2/88/549194b5d6f1f494b485e493edc6693c0a16f4ada488e5bd974ed1f42fad/pillow-12.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:9a8a34cc89c67a65ea7437ce257cea81a9dad65b29805f3ecee8c8fe8ff25ffe", size = 2463531, upload-time = "2026-04-01T14:43:10.743Z" }, + { url = "https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5", size = 5308279, upload-time = "2026-04-01T14:43:13.246Z" }, + { url = "https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421", size = 4695490, upload-time = "2026-04-01T14:43:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462, upload-time = "2026-04-01T14:43:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76", size = 8094744, upload-time = "2026-04-01T14:43:20.716Z" }, + { url = "https://files.pythonhosted.org/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005", size = 6398371, upload-time = "2026-04-01T14:43:23.443Z" }, + { url = "https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780", size = 7087215, upload-time = "2026-04-01T14:43:26.758Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5", size = 6509783, upload-time = "2026-04-01T14:43:29.56Z" }, + { url = "https://files.pythonhosted.org/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5", size = 7212112, upload-time = "2026-04-01T14:43:32.091Z" }, + { url = "https://files.pythonhosted.org/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940", size = 6378489, upload-time = "2026-04-01T14:43:34.601Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5", size = 7084129, upload-time = "2026-04-01T14:43:37.213Z" }, + { url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" }, + { url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" }, + { url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" }, + { url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" }, + { url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" }, + { url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" }, + { url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" }, + { url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" }, + { url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" }, + { url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" }, + { url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" }, + { url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" }, + { url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" }, + { url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" }, + { url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" }, + { url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" }, + { url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" }, + { url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" }, + { url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" }, + { url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" }, + { url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" }, + { url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" }, + { url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" }, + { url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" }, + { url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" }, + { url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" }, + { url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" }, + { url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" }, + { url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" }, + { url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" }, + { url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" }, + { url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" }, + { url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" }, + { url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" }, + { url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" }, + { url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b7/2437044fb910f499610356d1352e3423753c98e34f915252aafecc64889f/pillow-12.2.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0538bd5e05efec03ae613fd89c4ce0368ecd2ba239cc25b9f9be7ed426b0af1f", size = 5273969, upload-time = "2026-04-01T14:45:55.538Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f4/8316e31de11b780f4ac08ef3654a75555e624a98db1056ecb2122d008d5a/pillow-12.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:394167b21da716608eac917c60aa9b969421b5dcbbe02ae7f013e7b85811c69d", size = 4659674, upload-time = "2026-04-01T14:45:58.093Z" }, + { url = "https://files.pythonhosted.org/packages/d4/37/664fca7201f8bb2aa1d20e2c3d5564a62e6ae5111741966c8319ca802361/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5d04bfa02cc2d23b497d1e90a0f927070043f6cbf303e738300532379a4b4e0f", size = 5288479, upload-time = "2026-04-01T14:46:01.141Z" }, + { url = "https://files.pythonhosted.org/packages/49/62/5b0ed78fce87346be7a5cfcfaaad91f6a1f98c26f86bdbafa2066c647ef6/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0c838a5125cee37e68edec915651521191cef1e6aa336b855f495766e77a366e", size = 7032230, upload-time = "2026-04-01T14:46:03.874Z" }, + { url = "https://files.pythonhosted.org/packages/c3/28/ec0fc38107fc32536908034e990c47914c57cd7c5a3ece4d8d8f7ffd7e27/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a6c9fa44005fa37a91ebfc95d081e8079757d2e904b27103f4f5fa6f0bf78c0", size = 5355404, upload-time = "2026-04-01T14:46:06.33Z" }, + { url = "https://files.pythonhosted.org/packages/5e/8b/51b0eddcfa2180d60e41f06bd6d0a62202b20b59c68f5a132e615b75aecf/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25373b66e0dd5905ed63fa3cae13c82fbddf3079f2c8bf15c6fb6a35586324c1", size = 6002215, upload-time = "2026-04-01T14:46:08.83Z" }, + { url = "https://files.pythonhosted.org/packages/bc/60/5382c03e1970de634027cee8e1b7d39776b778b81812aaf45b694dfe9e28/pillow-12.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e", size = 7080946, upload-time = "2026-04-01T14:46:11.734Z" }, +] + +[[package]] +name = "pydantic" +version = "2.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" }, + { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" }, + { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" }, + { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" }, + { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" }, + { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" }, + { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" }, + { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" }, + { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" }, + { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" }, + { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" }, + { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, + { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, + { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, + { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, + { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" }, + { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" }, + { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" }, + { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, + { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" }, + { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" }, + { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" }, + { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" }, +] + +[[package]] +name = "pyparsing" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, +] + +[[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 = "scikit-learn" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "joblib" }, + { name = "numpy" }, + { name = "scipy" }, + { name = "threadpoolctl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/d4/40988bf3b8e34feec1d0e6a051446b1f66225f8529b9309becaeef62b6c4/scikit_learn-1.8.0.tar.gz", hash = "sha256:9bccbb3b40e3de10351f8f5068e105d0f4083b1a65fa07b6634fbc401a6287fd", size = 7335585, upload-time = "2025-12-10T07:08:53.618Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/92/53ea2181da8ac6bf27170191028aee7251f8f841f8d3edbfdcaf2008fde9/scikit_learn-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:146b4d36f800c013d267b29168813f7a03a43ecd2895d04861f1240b564421da", size = 8595835, upload-time = "2025-12-10T07:07:39.385Z" }, + { url = "https://files.pythonhosted.org/packages/01/18/d154dc1638803adf987910cdd07097d9c526663a55666a97c124d09fb96a/scikit_learn-1.8.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:f984ca4b14914e6b4094c5d52a32ea16b49832c03bd17a110f004db3c223e8e1", size = 8080381, upload-time = "2025-12-10T07:07:41.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/44/226142fcb7b7101e64fdee5f49dbe6288d4c7af8abf593237b70fca080a4/scikit_learn-1.8.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e30adb87f0cc81c7690a84f7932dd66be5bac57cfe16b91cb9151683a4a2d3b", size = 8799632, upload-time = "2025-12-10T07:07:43.899Z" }, + { url = "https://files.pythonhosted.org/packages/36/4d/4a67f30778a45d542bbea5db2dbfa1e9e100bf9ba64aefe34215ba9f11f6/scikit_learn-1.8.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ada8121bcb4dac28d930febc791a69f7cb1673c8495e5eee274190b73a4559c1", size = 9103788, upload-time = "2025-12-10T07:07:45.982Z" }, + { url = "https://files.pythonhosted.org/packages/89/3c/45c352094cfa60050bcbb967b1faf246b22e93cb459f2f907b600f2ceda5/scikit_learn-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:c57b1b610bd1f40ba43970e11ce62821c2e6569e4d74023db19c6b26f246cb3b", size = 8081706, upload-time = "2025-12-10T07:07:48.111Z" }, + { url = "https://files.pythonhosted.org/packages/3d/46/5416595bb395757f754feb20c3d776553a386b661658fb21b7c814e89efe/scikit_learn-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:2838551e011a64e3053ad7618dda9310175f7515f1742fa2d756f7c874c05961", size = 7688451, upload-time = "2025-12-10T07:07:49.873Z" }, + { url = "https://files.pythonhosted.org/packages/90/74/e6a7cc4b820e95cc38cf36cd74d5aa2b42e8ffc2d21fe5a9a9c45c1c7630/scikit_learn-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5fb63362b5a7ddab88e52b6dbb47dac3fd7dafeee740dc6c8d8a446ddedade8e", size = 8548242, upload-time = "2025-12-10T07:07:51.568Z" }, + { url = "https://files.pythonhosted.org/packages/49/d8/9be608c6024d021041c7f0b3928d4749a706f4e2c3832bbede4fb4f58c95/scikit_learn-1.8.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:5025ce924beccb28298246e589c691fe1b8c1c96507e6d27d12c5fadd85bfd76", size = 8079075, upload-time = "2025-12-10T07:07:53.697Z" }, + { url = "https://files.pythonhosted.org/packages/dd/47/f187b4636ff80cc63f21cd40b7b2d177134acaa10f6bb73746130ee8c2e5/scikit_learn-1.8.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4496bb2cf7a43ce1a2d7524a79e40bc5da45cf598dbf9545b7e8316ccba47bb4", size = 8660492, upload-time = "2025-12-10T07:07:55.574Z" }, + { url = "https://files.pythonhosted.org/packages/97/74/b7a304feb2b49df9fafa9382d4d09061a96ee9a9449a7cbea7988dda0828/scikit_learn-1.8.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0bcfe4d0d14aec44921545fd2af2338c7471de9cb701f1da4c9d85906ab847a", size = 8931904, upload-time = "2025-12-10T07:07:57.666Z" }, + { url = "https://files.pythonhosted.org/packages/9f/c4/0ab22726a04ede56f689476b760f98f8f46607caecff993017ac1b64aa5d/scikit_learn-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:35c007dedb2ffe38fe3ee7d201ebac4a2deccd2408e8621d53067733e3c74809", size = 8019359, upload-time = "2025-12-10T07:07:59.838Z" }, + { url = "https://files.pythonhosted.org/packages/24/90/344a67811cfd561d7335c1b96ca21455e7e472d281c3c279c4d3f2300236/scikit_learn-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:8c497fff237d7b4e07e9ef1a640887fa4fb765647f86fbe00f969ff6280ce2bb", size = 7641898, upload-time = "2025-12-10T07:08:01.36Z" }, + { url = "https://files.pythonhosted.org/packages/03/aa/e22e0768512ce9255eba34775be2e85c2048da73da1193e841707f8f039c/scikit_learn-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0d6ae97234d5d7079dc0040990a6f7aeb97cb7fa7e8945f1999a429b23569e0a", size = 8513770, upload-time = "2025-12-10T07:08:03.251Z" }, + { url = "https://files.pythonhosted.org/packages/58/37/31b83b2594105f61a381fc74ca19e8780ee923be2d496fcd8d2e1147bd99/scikit_learn-1.8.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:edec98c5e7c128328124a029bceb09eda2d526997780fef8d65e9a69eead963e", size = 8044458, upload-time = "2025-12-10T07:08:05.336Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5a/3f1caed8765f33eabb723596666da4ebbf43d11e96550fb18bdec42b467b/scikit_learn-1.8.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:74b66d8689d52ed04c271e1329f0c61635bcaf5b926db9b12d58914cdc01fe57", size = 8610341, upload-time = "2025-12-10T07:08:07.732Z" }, + { url = "https://files.pythonhosted.org/packages/38/cf/06896db3f71c75902a8e9943b444a56e727418f6b4b4a90c98c934f51ed4/scikit_learn-1.8.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8fdf95767f989b0cfedb85f7ed8ca215d4be728031f56ff5a519ee1e3276dc2e", size = 8900022, upload-time = "2025-12-10T07:08:09.862Z" }, + { url = "https://files.pythonhosted.org/packages/1c/f9/9b7563caf3ec8873e17a31401858efab6b39a882daf6c1bfa88879c0aa11/scikit_learn-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:2de443b9373b3b615aec1bb57f9baa6bb3a9bd093f1269ba95c17d870422b271", size = 7989409, upload-time = "2025-12-10T07:08:12.028Z" }, + { url = "https://files.pythonhosted.org/packages/49/bd/1f4001503650e72c4f6009ac0c4413cb17d2d601cef6f71c0453da2732fc/scikit_learn-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:eddde82a035681427cbedded4e6eff5e57fa59216c2e3e90b10b19ab1d0a65c3", size = 7619760, upload-time = "2025-12-10T07:08:13.688Z" }, + { url = "https://files.pythonhosted.org/packages/d2/7d/a630359fc9dcc95496588c8d8e3245cc8fd81980251079bc09c70d41d951/scikit_learn-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7cc267b6108f0a1499a734167282c00c4ebf61328566b55ef262d48e9849c735", size = 8826045, upload-time = "2025-12-10T07:08:15.215Z" }, + { url = "https://files.pythonhosted.org/packages/cc/56/a0c86f6930cfcd1c7054a2bc417e26960bb88d32444fe7f71d5c2cfae891/scikit_learn-1.8.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:fe1c011a640a9f0791146011dfd3c7d9669785f9fed2b2a5f9e207536cf5c2fd", size = 8420324, upload-time = "2025-12-10T07:08:17.561Z" }, + { url = "https://files.pythonhosted.org/packages/46/1e/05962ea1cebc1cf3876667ecb14c283ef755bf409993c5946ade3b77e303/scikit_learn-1.8.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72358cce49465d140cc4e7792015bb1f0296a9742d5622c67e31399b75468b9e", size = 8680651, upload-time = "2025-12-10T07:08:19.952Z" }, + { url = "https://files.pythonhosted.org/packages/fe/56/a85473cd75f200c9759e3a5f0bcab2d116c92a8a02ee08ccd73b870f8bb4/scikit_learn-1.8.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:80832434a6cc114f5219211eec13dcbc16c2bac0e31ef64c6d346cde3cf054cb", size = 8925045, upload-time = "2025-12-10T07:08:22.11Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b7/64d8cfa896c64435ae57f4917a548d7ac7a44762ff9802f75a79b77cb633/scikit_learn-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ee787491dbfe082d9c3013f01f5991658b0f38aa8177e4cd4bf434c58f551702", size = 8507994, upload-time = "2025-12-10T07:08:23.943Z" }, + { url = "https://files.pythonhosted.org/packages/5e/37/e192ea709551799379958b4c4771ec507347027bb7c942662c7fbeba31cb/scikit_learn-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf97c10a3f5a7543f9b88cbf488d33d175e9146115a451ae34568597ba33dcde", size = 7869518, upload-time = "2025-12-10T07:08:25.71Z" }, + { url = "https://files.pythonhosted.org/packages/24/05/1af2c186174cc92dcab2233f327336058c077d38f6fe2aceb08e6ab4d509/scikit_learn-1.8.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c22a2da7a198c28dd1a6e1136f19c830beab7fdca5b3e5c8bba8394f8a5c45b3", size = 8528667, upload-time = "2025-12-10T07:08:27.541Z" }, + { url = "https://files.pythonhosted.org/packages/a8/25/01c0af38fe969473fb292bba9dc2b8f9b451f3112ff242c647fee3d0dfe7/scikit_learn-1.8.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:6b595b07a03069a2b1740dc08c2299993850ea81cce4fe19b2421e0c970de6b7", size = 8066524, upload-time = "2025-12-10T07:08:29.822Z" }, + { url = "https://files.pythonhosted.org/packages/be/ce/a0623350aa0b68647333940ee46fe45086c6060ec604874e38e9ab7d8e6c/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29ffc74089f3d5e87dfca4c2c8450f88bdc61b0fc6ed5d267f3988f19a1309f6", size = 8657133, upload-time = "2025-12-10T07:08:31.865Z" }, + { url = "https://files.pythonhosted.org/packages/b8/cb/861b41341d6f1245e6ca80b1c1a8c4dfce43255b03df034429089ca2a2c5/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb65db5d7531bccf3a4f6bec3462223bea71384e2cda41da0f10b7c292b9e7c4", size = 8923223, upload-time = "2025-12-10T07:08:34.166Z" }, + { url = "https://files.pythonhosted.org/packages/76/18/a8def8f91b18cd1ba6e05dbe02540168cb24d47e8dcf69e8d00b7da42a08/scikit_learn-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:56079a99c20d230e873ea40753102102734c5953366972a71d5cb39a32bc40c6", size = 8096518, upload-time = "2025-12-10T07:08:36.339Z" }, + { url = "https://files.pythonhosted.org/packages/d1/77/482076a678458307f0deb44e29891d6022617b2a64c840c725495bee343f/scikit_learn-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:3bad7565bc9cf37ce19a7c0d107742b320c1285df7aab1a6e2d28780df167242", size = 7754546, upload-time = "2025-12-10T07:08:38.128Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d1/ef294ca754826daa043b2a104e59960abfab4cf653891037d19dd5b6f3cf/scikit_learn-1.8.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:4511be56637e46c25721e83d1a9cea9614e7badc7040c4d573d75fbe257d6fd7", size = 8848305, upload-time = "2025-12-10T07:08:41.013Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e2/b1f8b05138ee813b8e1a4149f2f0d289547e60851fd1bb268886915adbda/scikit_learn-1.8.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:a69525355a641bf8ef136a7fa447672fb54fe8d60cab5538d9eb7c6438543fb9", size = 8432257, upload-time = "2025-12-10T07:08:42.873Z" }, + { url = "https://files.pythonhosted.org/packages/26/11/c32b2138a85dcb0c99f6afd13a70a951bfdff8a6ab42d8160522542fb647/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c2656924ec73e5939c76ac4c8b026fc203b83d8900362eb2599d8aee80e4880f", size = 8678673, upload-time = "2025-12-10T07:08:45.362Z" }, + { url = "https://files.pythonhosted.org/packages/c7/57/51f2384575bdec454f4fe4e7a919d696c9ebce914590abf3e52d47607ab8/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15fc3b5d19cc2be65404786857f2e13c70c83dd4782676dd6814e3b89dc8f5b9", size = 8922467, upload-time = "2025-12-10T07:08:47.408Z" }, + { url = "https://files.pythonhosted.org/packages/35/4d/748c9e2872637a57981a04adc038dacaa16ba8ca887b23e34953f0b3f742/scikit_learn-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:00d6f1d66fbcf4eba6e356e1420d33cc06c70a45bb1363cd6f6a8e4ebbbdece2", size = 8774395, upload-time = "2025-12-10T07:08:49.337Z" }, + { url = "https://files.pythonhosted.org/packages/60/22/d7b2ebe4704a5e50790ba089d5c2ae308ab6bb852719e6c3bd4f04c3a363/scikit_learn-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f28dd15c6bb0b66ba09728cf09fd8736c304be29409bd8445a080c1280619e8c", size = 8002647, upload-time = "2025-12-10T07:08:51.601Z" }, +] + +[[package]] +name = "scipy" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +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" }, + { url = "https://files.pythonhosted.org/packages/cf/83/333afb452af6f0fd70414dc04f898647ee1423979ce02efa75c3b0f2c28e/scipy-1.17.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717", size = 31584510, upload-time = "2026-02-23T00:21:01.015Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a6/d05a85fd51daeb2e4ea71d102f15b34fedca8e931af02594193ae4fd25f7/scipy-1.17.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9", size = 28170131, upload-time = "2026-02-23T00:21:05.888Z" }, + { url = "https://files.pythonhosted.org/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b", size = 20342032, upload-time = "2026-02-23T00:21:09.904Z" }, + { url = "https://files.pythonhosted.org/packages/c9/35/2c342897c00775d688d8ff3987aced3426858fd89d5a0e26e020b660b301/scipy-1.17.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866", size = 22678766, upload-time = "2026-02-23T00:21:14.313Z" }, + { url = "https://files.pythonhosted.org/packages/ef/f2/7cdb8eb308a1a6ae1e19f945913c82c23c0c442a462a46480ce487fdc0ac/scipy-1.17.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350", size = 32957007, upload-time = "2026-02-23T00:21:19.663Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118", size = 35221333, upload-time = "2026-02-23T00:21:25.278Z" }, + { url = "https://files.pythonhosted.org/packages/d9/77/5b8509d03b77f093a0d52e606d3c4f79e8b06d1d38c441dacb1e26cacf46/scipy-1.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068", size = 35042066, upload-time = "2026-02-23T00:21:31.358Z" }, + { url = "https://files.pythonhosted.org/packages/f9/df/18f80fb99df40b4070328d5ae5c596f2f00fffb50167e31439e932f29e7d/scipy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118", size = 37612763, upload-time = "2026-02-23T00:21:37.247Z" }, + { url = "https://files.pythonhosted.org/packages/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19", size = 37290984, upload-time = "2026-02-23T00:22:35.023Z" }, + { url = "https://files.pythonhosted.org/packages/7c/56/fe201e3b0f93d1a8bcf75d3379affd228a63d7e2d80ab45467a74b494947/scipy-1.17.1-cp314-cp314-win_arm64.whl", hash = "sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293", size = 25192877, upload-time = "2026-02-23T00:22:39.798Z" }, + { url = "https://files.pythonhosted.org/packages/96/ad/f8c414e121f82e02d76f310f16db9899c4fcde36710329502a6b2a3c0392/scipy-1.17.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6", size = 31949750, upload-time = "2026-02-23T00:21:42.289Z" }, + { url = "https://files.pythonhosted.org/packages/7c/b0/c741e8865d61b67c81e255f4f0a832846c064e426636cd7de84e74d209be/scipy-1.17.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1", size = 28585858, upload-time = "2026-02-23T00:21:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1b/3985219c6177866628fa7c2595bfd23f193ceebbe472c98a08824b9466ff/scipy-1.17.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39", size = 20757723, upload-time = "2026-02-23T00:21:52.039Z" }, + { url = "https://files.pythonhosted.org/packages/c0/19/2a04aa25050d656d6f7b9e7b685cc83d6957fb101665bfd9369ca6534563/scipy-1.17.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca", size = 23043098, upload-time = "2026-02-23T00:21:56.185Z" }, + { url = "https://files.pythonhosted.org/packages/86/f1/3383beb9b5d0dbddd030335bf8a8b32d4317185efe495374f134d8be6cce/scipy-1.17.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad", size = 33030397, upload-time = "2026-02-23T00:22:01.404Z" }, + { url = "https://files.pythonhosted.org/packages/41/68/8f21e8a65a5a03f25a79165ec9d2b28c00e66dc80546cf5eb803aeeff35b/scipy-1.17.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a", size = 35281163, upload-time = "2026-02-23T00:22:07.024Z" }, + { url = "https://files.pythonhosted.org/packages/84/8d/c8a5e19479554007a5632ed7529e665c315ae7492b4f946b0deb39870e39/scipy-1.17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4", size = 35116291, upload-time = "2026-02-23T00:22:12.585Z" }, + { url = "https://files.pythonhosted.org/packages/52/52/e57eceff0e342a1f50e274264ed47497b59e6a4e3118808ee58ddda7b74a/scipy-1.17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2", size = 37682317, upload-time = "2026-02-23T00:22:18.513Z" }, + { url = "https://files.pythonhosted.org/packages/11/2f/b29eafe4a3fbc3d6de9662b36e028d5f039e72d345e05c250e121a230dd4/scipy-1.17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484", size = 37345327, upload-time = "2026-02-23T00:22:24.442Z" }, + { url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" }, +] + +[[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 = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "threadpoolctl" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, +] + +[[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 = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +]