Sunstead trust scoring project
0

Configure Feed

Select the types of activity you want to include in your feed.

Initial commit: sunstead trust scoring project

author
Veikka Silvekoski
date (Jun 25, 2026, 11:45 AM +0300) commit c76a8b80
+3819
+20
.envrc.example
··· 1 + # Source before running anything (PRD 4.1). Route all large artifacts to the drive. 2 + export DATA_ROOT="/Volumes/EXT/tangled-trust" # the external drive (Linux: /mnt/ext/tangled-trust) 3 + mkdir -p "$DATA_ROOT"/{venv,pip,hf,torch,pyg,staging,diffs,models,duckdb,logs} 4 + 5 + export PIP_CACHE_DIR="$DATA_ROOT/pip" 6 + export UV_CACHE_DIR="$DATA_ROOT/pip" 7 + export HF_HOME="$DATA_ROOT/hf" 8 + export TORCH_HOME="$DATA_ROOT/torch" 9 + 10 + export DUCKDB_PATH="$DATA_ROOT/duckdb/trust.duckdb" 11 + export STAGING_DIR="$DATA_ROOT/staging" 12 + export MODEL_DIR="$DATA_ROOT/models" 13 + export LOG_DIR="$DATA_ROOT/logs" 14 + 15 + # Claude review (the only secret that stays on the main disk, in .env): 16 + export ANTHROPIC_API_KEY="sk-ant-..." 17 + export CLAUDE_MODEL="claude-sonnet-4-6" 18 + 19 + # Create the venv ON the drive, not in the repo: 20 + # python -m venv "$DATA_ROOT/venv" && source "$DATA_ROOT/venv/bin/activate"
+7
.gitignore
··· 1 + .venv/ 2 + .data/ 3 + __pycache__/ 4 + *.pyc 5 + .envrc 6 + .env 7 + *.duckdb
+161
README.md
··· 1 + # Tangled contributor trust scoring (EigenTrust + Claude) 2 + 3 + Calibrated, explainable, sybil-resistant trust scores that auto-triage Tangled PRs 4 + into **fast-lane / normal-queue / needs-human**. Two independent signals fused by a 5 + gate (not an average): **structural trust** (EigenTrust over the vouch graph) and 6 + **content review** (Claude reading the diff, blind to author identity). 7 + 8 + Built per `prd.md` through **M7**: EigenTrust + Claude end to end; LightGBM learned score 9 + with isotonic calibration; GraphSAGE trained offline and compared (not served — it doesn't 10 + beat M5 on this sparse graph, and the PRD says ship it only if it does); the 11 + attestation-gated sensitive-repo tier (6.13); AT-Proto writeback of assessments as records 12 + (6.11); and the Tangled browser overlay (7.4). Only the ElevenLabs voice briefing is left 13 + as a seam — see "What's skipped". 14 + 15 + ## Layout 16 + 17 + ``` 18 + src/trust/ 19 + config.py env paths (DATA_ROOT fail-fast) + gate/eigen/review tuning 20 + db.py DuckDB schema, feature view, clean_merge label SQL 21 + ingest.py M1 Jetstream -> events -> derive typed tables (--probe confirms NSIDs) 22 + eigentrust.py M3 SciPy power iteration + BFS path explanation (no graph DB) 23 + review.py M4 Claude reviewer, verbatim 6.6 prompt, forced-schema tool use 24 + fusion.py M4 gate decide() + scoring worker (score_pr); loads M5 model if present 25 + learned.py M5 LightGBM + isotonic calibration + TreeSHAP (optional .[learned] extra) 26 + gnn.py M6 GraphSAGE, trained offline + compared vs M5; served only if it wins (.[gnn]) 27 + atproto.py M7 writeback: assessments published as sh.tangled.trust.score records (6.11) 28 + api.py M3/M4 FastAPI: /score /review /leaderboard /metrics /triage + pages 29 + src/trust/static/ triage / dashboard / leaderboard pages 30 + extension/ M7 Tangled browser overlay (7.4) — MV3 content script, UI only 31 + lexicons/ sh.tangled.trust.score lexicon for the writeback (6.11) 32 + seed.py synthetic demo data (trusted core + sybil cluster) 33 + static/ triage / dashboard / leaderboard pages (thin clients of the API) 34 + ``` 35 + 36 + ## Setup 37 + 38 + ```bash 39 + cp .envrc.example .envrc # point DATA_ROOT at the external drive; add ANTHROPIC_API_KEY 40 + source .envrc # in prod: fails fast if the drive is not mounted 41 + uv venv .venv && source .venv/bin/activate && uv pip install -e . 42 + ``` 43 + 44 + `DATA_ROOT` unset → a repo-local `.data/` dev fallback (with a warning). All large 45 + artifacts route under `DATA_ROOT` (PRD 4.1). 46 + 47 + ## Demo (no live data or API key required) 48 + 49 + One command brings up the whole stack (seed → score loop → API) in split panes: 50 + 51 + ```bash 52 + mprocs # reads mprocs.yaml; open http://127.0.0.1:8000 53 + ``` 54 + 55 + Or run the panes by hand: 56 + 57 + ```bash 58 + python -m trust.seed # load the synthetic vouch graph + labelled PRs 59 + python -m trust.score --loop # poll + score PRs, write decisions (--loop for a daemon) 60 + python -m trust.api # serve http://127.0.0.1:8000 (triage / dashboard / leaderboard) 61 + ``` 62 + 63 + > DuckDB is single-writer and a held lock blocks every other open, so each process 64 + > opens the file briefly (open → work → close) with retry — that's what lets the 65 + > mprocs panes share one `trust.duckdb`. Don't run `ingest` and `score` as writers 66 + > at the same time. 67 + 68 + ## Learned score (M5, optional) 69 + 70 + ```bash 71 + uv pip install -e '.[learned]' # lightgbm + scikit-learn (no shap needed) 72 + python -m trust.seed 73 + trust-train # LightGBM on the features, isotonic-calibrated; prints a reliability curve 74 + python -m trust.score # the gate now uses calibrated P(clean), not raw EigenTrust 75 + ``` 76 + 77 + `trust-train` predicts `clean_merge` from the per-DID features (with `eigentrust_score` 78 + **as a feature**, so the model builds on the graph), splits by time, and fits isotonic 79 + regression so the output is a real probability (PRD 6.5/6.8). The model is saved under 80 + `MODEL_DIR`; `fusion.structural_for` loads it automatically and falls back to raw 81 + EigenTrust when it's absent (so the base install still runs). Explanations gain the top 82 + LightGBM **TreeSHAP** contributions (`merged_pr_count (+1.40)`, …) via LightGBM's native 83 + `pred_contrib` — no `shap`/`numba` dependency. 84 + 85 + > On the tiny synthetic data the model is near-degenerate (the reliability curve has two 86 + > bins; one revert sends a contributor to 0). That's expected at N≈22 — real history 87 + > smooths it. To use M5 in a running `mprocs` demo: `trust-train`, then restart the 88 + > `score` and `api` panes so they load the model. 89 + 90 + What it shows (the PRD deliverable): 91 + 92 + - `live/trusted-clean` — authored by **carol**, trust flows maintainer → alice → carol → 93 + **fast-lane** on structural trust alone. 94 + - `live/sybil-buggy` — authored by a throwaway in an isolated mutual-vouch cluster, 95 + starved to **0.000** → **needs_human**. A clean-looking diff could never lift it 96 + (constraint 2). With `ANTHROPIC_API_KEY` set, Claude also attaches a concrete reason 97 + (the diff swaps a constant-time compare for `==`). 98 + - Dashboard: score distribution, fast-lane rate, **0% false-approval** backtest above the 99 + threshold, vouch-graph stats. 100 + 101 + ## Live data 102 + 103 + ```bash 104 + python -m trust.ingest --probe --max-events 300 # confirm real sh.tangled.* NSIDs first 105 + python -m trust.ingest # firehose -> DuckDB, resumable cursor 106 + python -m trust.score # score newly-ingested PRs 107 + ``` 108 + 109 + The collection→record map in `config.COLLECTION_KINDS` is best-guess and marked 110 + `CONFIRM` — verify it against the `--probe` output before trusting derived rows. 111 + 112 + ## Tests 113 + 114 + ```bash 115 + python -m pytest # eigentrust starves sybils; gate never lifts untrusted; schema parses 116 + ``` 117 + 118 + ## GraphSAGE (M6, optional) 119 + 120 + ```bash 121 + uv pip install -e '.[gnn]' # torch + torch-geometric (multi-GB) 122 + trust-seed && trust-train && trust-gnn # trains GraphSAGE offline, compares vs M5 123 + ``` 124 + 125 + `trust-gnn` builds a PyG graph (positive vouches + co-contribution edges; per-DID feature 126 + vectors as node features; denounce-count rides as a feature, no signed-edge GNN), trains an 127 + inductive 2-layer GraphSAGE on a time split, then writes a **verdict** comparing its holdout 128 + accuracy to M5's. `fusion.structural_for` serves the GNN **only if `gnn_wins`** — on the 129 + synthetic graph it loses to M5, so the system keeps the calibrated baseline. That gate is the 130 + PRD's rule ("ship the GNN only if it beats the baseline and is stable"), enforced in code. 131 + 132 + > lightgbm and torch each bundle `libomp`; loading both in one process hangs on macOS. 133 + > `trust/__init__.py` sets `KMP_DUPLICATE_LIB_OK` / `OMP_NUM_THREADS` before either imports. 134 + 135 + ## Native + compliance surfaces (M7) 136 + 137 + - **Attestation-gated sensitive-repo tier (6.13).** A repo in the `sensitive` tier 138 + requires a contributor-issued jurisdiction attestation before fast-lane/merge; a 139 + missing one forces `needs_human` regardless of trust or content risk — the only control 140 + that overrides the score, so it's checked first in `decide()`. The demo seeds a sensitive 141 + repo where an attested DID fast-lanes and an unattested high-trust DID is blocked at 142 + `calibrated_prob 1.00`. Only declared/asserted facts are used; nothing is inferred. 143 + - **AT-Proto writeback (6.11).** `trust-publish` emits each assessment as a public 144 + `sh.tangled.trust.score` record (lexicon in `lexicons/`) on the service's own PDS, so 145 + verdicts are auditable provenance on the network. No creds → dry-run (prints the records); 146 + set `ATPROTO_PDS` / `ATPROTO_IDENTIFIER` / `ATPROTO_PASSWORD` to publish for real. 147 + - **Browser overlay (7.4).** `extension/` is a minimal MV3 content script that injects a 148 + trust hat onto tangled.org from the same `/score` API. Load unpacked; see `extension/README.md`. 149 + Confirm the DID selector against the real DOM (the UI analog of confirming NSIDs). 150 + 151 + ## What's skipped (and when to add it) 152 + 153 + - **ElevenLabs voice briefing (M7).** A thin wrapper: the explanation `summary` is already 154 + "suitable to read aloud" (6.6), so a `/brief/{did}` endpoint that pipes it to TTS is the 155 + whole job — add when you have an ElevenLabs key. 156 + - **Per-PR writeback subject.** `sh.tangled.trust.score` currently keys on the contributor 157 + DID; carry `pr_id` on the `scores` table to reference a specific PR's `at://` URI. 158 + - **SvelteKit frontend.** The three surfaces ship as built-in static pages (the PRD blesses 159 + this for the dashboard); swap to SvelteKit if you need the richer UI kit / native overlay. 160 + - **External signals (6.12): OSV/secret-scan/SAST.** `review_pr` already accepts 161 + `machine_findings` as structured input — wire the scanners into that arg.
+27
extension/README.md
··· 1 + # Tangled Trust Hat (browser overlay, PRD 7.4) 2 + 3 + A minimal MV3 content script that injects a calibrated **trust hat** next to 4 + contributor DIDs on tangled.org, reading the same `/score` API as the dashboard. 5 + UI only — the brain stays in the scoring service. 6 + 7 + ## Load it 8 + 9 + 1. Run the scoring service: `python -m trust.api` (serves `http://127.0.0.1:8000`). 10 + 2. Chrome → `chrome://extensions` → enable Developer mode → **Load unpacked** → pick 11 + this `extension/` folder. 12 + 3. Open a tangled.org PR or contributor page. DIDs get a colored pill 13 + (green fast-lane / amber normal / red needs-review); hover for the reason. 14 + 15 + ## Two things to confirm against the real site 16 + 17 + - **The DID selector.** `content.js` scans text nodes for `did:plc:` / `did:web:` 18 + patterns — a best guess. If tangled.org renders DIDs in attributes or a different 19 + shape, adjust the scan (this is the UI analog of confirming the NSIDs). 20 + - **CORS / host.** The API ships permissive CORS for local use; the manifest grants 21 + `http://127.0.0.1:8000/*`. Point both at your host for a hosted demo. 22 + 23 + ## Not built 24 + 25 + The upstream vision — Tangled's own appview rendering third-party trust records 26 + natively (the 6.11 `sh.tangled.trust.score` records) — is a platform change, not this 27 + extension. Ask Lewis whether the appview can render trust records authored by other DIDs.
+63
extension/content.js
··· 1 + // Tangled-native overlay (PRD 7.4). UI only: it reads the same /score API the 2 + // dashboard does and injects a "trust hat" pill next to contributor DIDs on 3 + // tangled.org. The brain stays in the service; this never touches DuckDB. 4 + // 5 + // ponytail: vanilla content script, no build step (same call as the built-in 6 + // pages) over the Bun/oxlint/zod toolchain. CONFIRM the DID selector against the 7 + // real tangled.org DOM — like the NSIDs, this is a best-guess until verified. 8 + 9 + const API = "http://127.0.0.1:8000"; 10 + const DID_RE = /did:(?:plc:[a-z2-7]+|web:[a-z0-9.\-]+)/gi; 11 + const COLOR = { fast_lane: "#1a7f37", normal_queue: "#9a6700", needs_human: "#cf222e" }; 12 + 13 + const cache = new Map(); 14 + 15 + async function score(did) { 16 + if (cache.has(did)) return cache.get(did); 17 + const p = fetch(`${API}/score/${encodeURIComponent(did)}`) 18 + .then((r) => (r.ok ? r.json() : null)) 19 + .catch(() => null); // service down -> render nothing, never break the page 20 + cache.set(did, p); 21 + return p; 22 + } 23 + 24 + function pill(s) { 25 + const el = document.createElement("span"); 26 + el.className = "tangled-trust-hat"; 27 + el.dataset.did = s.did; 28 + el.textContent = ` ${Math.round((s.calibrated_prob ?? 0) * 100)}% `; 29 + const factors = (s.explanation?.top_factors || []).join("\n"); 30 + el.title = `${s.decision}\n${factors}`; 31 + Object.assign(el.style, { 32 + background: COLOR[s.decision] || "#57606a", 33 + color: "#fff", borderRadius: "999px", padding: "1px 7px", 34 + fontSize: "11px", fontWeight: "600", marginLeft: "6px", verticalAlign: "middle", 35 + }); 36 + return el; 37 + } 38 + 39 + // Find text-node occurrences of a DID and tag their parent once. 40 + async function scan() { 41 + const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT); 42 + const hits = []; 43 + for (let n = walker.nextNode(); n; n = walker.nextNode()) { 44 + const m = n.nodeValue.match(DID_RE); 45 + if (m) hits.push({ node: n, dids: [...new Set(m)] }); 46 + } 47 + for (const { node, dids } of hits) { 48 + const host = node.parentElement; 49 + if (!host || host.querySelector(":scope > .tangled-trust-hat")) continue; 50 + for (const did of dids) { 51 + const s = await score(did); 52 + if (s) host.appendChild(pill(s)); 53 + } 54 + } 55 + } 56 + 57 + scan(); 58 + // tangled.org is an SPA; re-scan on DOM changes (debounced). 59 + let t; 60 + new MutationObserver(() => { 61 + clearTimeout(t); 62 + t = setTimeout(scan, 400); 63 + }).observe(document.body, { childList: true, subtree: true });
+14
extension/manifest.json
··· 1 + { 2 + "manifest_version": 3, 3 + "name": "Tangled Trust Hat", 4 + "version": "0.1.0", 5 + "description": "Overlays calibrated contributor trust (from the local scoring service) onto tangled.org PR and contributor pages. UI only; the brain stays in the service.", 6 + "content_scripts": [ 7 + { 8 + "matches": ["https://tangled.org/*", "https://*.tangled.org/*"], 9 + "js": ["content.js"], 10 + "run_at": "document_idle" 11 + } 12 + ], 13 + "host_permissions": ["http://127.0.0.1:8000/*"] 14 + }
+47
lexicons/sh.tangled.trust.score.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "sh.tangled.trust.score", 4 + "description": "A trust assessment of a Tangled contributor, authored by the trust-scoring service's DID (PRD 6.11). Auditable provenance on the network, not a row in a private file.", 5 + "defs": { 6 + "main": { 7 + "type": "record", 8 + "key": "tid", 9 + "record": { 10 + "type": "object", 11 + "required": ["subject", "calibratedProb", "decision", "structuralTrust", "createdAt"], 12 + "properties": { 13 + "subject": { 14 + "type": "string", 15 + "format": "did", 16 + "description": "The assessed contributor's DID. Reference a specific PR by storing its at:// URI here once pr_id is carried on scores." 17 + }, 18 + "calibratedProb": { 19 + "type": "number", 20 + "description": "Calibrated P(next contribution is a clean merge), 0.0-1.0." 21 + }, 22 + "decision": { 23 + "type": "string", 24 + "enum": ["fast_lane", "normal_queue", "needs_human"] 25 + }, 26 + "structuralTrust": { 27 + "type": "number", 28 + "description": "EigenTrust / learned structural signal, 0.0-1.0." 29 + }, 30 + "contentRisk": { 31 + "type": "number", 32 + "description": "Claude content-review risk when a review ran, else absent." 33 + }, 34 + "summary": { 35 + "type": "string", 36 + "maxGraphemes": 280, 37 + "description": "Human-readable rationale, suitable to read aloud." 38 + }, 39 + "createdAt": { 40 + "type": "string", 41 + "format": "datetime" 42 + } 43 + } 44 + } 45 + } 46 + } 47 + }
+27
mprocs.yaml
··· 1 + # mprocs: brings up the whole stack. Run `mprocs` in the repo root. 2 + # 3 + # DuckDB allows only ONE read-write process at a time and a held lock blocks 4 + # every other open. So each process opens the file briefly (open -> work -> 5 + # close) with retry, letting these panes interleave. Don't run `ingest` and 6 + # `score` at the same time in live mode — they're both writers and will just 7 + # contend; ingest is the single writer, score reads+writes scores in its gaps. 8 + procs: 9 + seed: 10 + shell: uv run trust-seed # one-shot: load demo data, then exits 11 + autostart: true 12 + 13 + score: 14 + shell: uv run python -m trust.score --loop --interval 5 15 + autostart: true 16 + 17 + api: 18 + shell: uv run trust-api # http://127.0.0.1:8000 (triage/dashboard/leaderboard) 19 + autostart: true 20 + 21 + ingest: 22 + shell: uv run python -m trust.ingest # live firehose -> DuckDB 23 + autostart: false # enable after `--probe` confirms NSIDs; pause `seed`/`score` first 24 + 25 + probe: 26 + shell: uv run python -m trust.ingest --probe --max-events 300 27 + autostart: false # confirm real sh.tangled.* collection names
+456
prd.md
··· 1 + # PRD: hybrid contributor trust scoring for Tangled (GNN + Claude) 2 + 3 + You are building a backend service that scores the trustworthiness of contributors on **Tangled**, a code forge built on the **AT Protocol**. The score auto-triages incoming pull requests so maintainers who approve hundreds of PRs a day only review the ones that need a human. The score must be **calibrated** (a real probability), **explainable** (a maintainer can see why), and **adversarially robust** (resistant to throwaway identities pushing machine-generated low-quality code). 4 + 5 + The stack is deliberately lean and self-hosted: a single embedded **DuckDB** store on an external drive, plain Python processes, and no managed cloud data services. Read sections 0, 1, and 2 before writing any code. Build strictly in the order in section 5. 6 + 7 + --- 8 + 9 + ## 0. Mission 10 + 11 + Produce, per contributor DID, a calibrated probability that their next contribution is safe to fast-lane, plus a short human-readable reason, by fusing two independent signals: 12 + 13 + - **Structural trust (the "who"):** the contributor's position in the vouch graph and their historical track record. Sybil-resistant. Built with EigenTrust first, then optionally upgraded with a GNN. 14 + - **Content review (the "what"):** Claude reading the actual diff and discussion of a specific PR to catch problems the graph cannot see. 15 + 16 + These are fused by a **policy/gate**, not a naive average. The output drives a decision: fast-lane, normal queue, or route to a human with a reason attached. 17 + 18 + --- 19 + 20 + ## 1. Threat model and hard constraints (non-negotiable) 21 + 22 + The attacker spins up fresh DIDs and pushes LLM-generated code that looks correct but is subtly wrong, to get it merged with minimal review. Every design choice exists to defeat this: 23 + 24 + 1. **The structural signal must be load-bearing and sybil-resistant.** Trust must flow from a trusted seed set; a cluster of fake DIDs vouching for each other must be starved. This is why EigenTrust (a trust-flow algorithm) is the core, not a vouch count. 25 + 2. **Claude judges content, never identity.** Claude must not see or infer author reputation. A clean-looking diff from an untrusted DID must NOT lift that DID into the fast-lane. Identity is the graph's job; content is Claude's job. 26 + 3. **The score must be calibrated.** 0.9 must mean roughly 90% of such contributors produce clean PRs. 27 + 4. **The score must be explainable.** Emit a structured explanation (top factors plus Claude's rationale), never a bare number. 28 + 5. **Inform, do not enforce.** Tangled's vouching has no punitive consequence; it informs a decision. This system recommends and routes; it does not block users. 29 + 30 + --- 31 + 32 + ## 2. Two decisions that shape the whole build 33 + 34 + **The stack is serviceless and self-hosted.** All state lives in a single embedded DuckDB file on an external drive. The ingester, scoring worker, and API are plain Python processes. There is no message broker, no separate database server, and no managed cloud data service. Models are trained offline; Anthropic serves the Claude inference call. This keeps the moving parts minimal and the footprint small, which suits both the hardware constraint and a hackathon timeline. The resumability a broker would give is already covered by the AT Protocol firehose itself: the Jetstream cursor lets you replay, and the raw event log in DuckDB is the durable record. 35 + 36 + **There is no graph database, and the agent must not add one.** You need graph *computation*, not a graph *engine*, and they are different layers. The vouch graph lives as a plain edge list in a `vouches` table in DuckDB. EigenTrust reads those rows into a SciPy sparse matrix and runs power iteration in memory. GraphSAGE builds a PyTorch Geometric `edge_index` tensor from the same query. At hackathon scale the graph is a few thousand edges and fits in memory many times over, so there is no performance case for Neo4j or any graph DB, and adding one only costs a service to run and a query language to wire up. Path-based explanations ("trust reaches this contributor through maintainers X and Y") are done with a short in-memory breadth-first walk from the seed during the EigenTrust run, not with graph-DB traversal. 37 + 38 + --- 39 + 40 + ## 3. Architecture 41 + 42 + ``` 43 + Jetstream (filtered AT Proto firehose, JSON over WebSocket) 44 + | 45 + v 46 + Ingester (plain Python process) 47 + (confirm NSIDs; persist cursor; batched appends) 48 + | 49 + v 50 + DuckDB file [on external drive: $DUCKDB_PATH] 51 + - events (raw append log) 52 + - contributors 53 + - vouches (edge list) <- the whole graph; no graph DB 54 + - pull_requests (lifecycle) 55 + - features (SQL views / tables) 56 + - scores 57 + - ingest_state (cursor) 58 + | 59 + +-------------------+--------------------+ 60 + | | 61 + v v 62 + STRUCTURAL SIGNAL CONTENT SIGNAL 63 + (reads the vouches edge list) (Claude via Anthropic API) 64 + - EigenTrust (SciPy sparse) - reviews a PR's diff + 65 + - LightGBM on features discussion; returns 66 + - GraphSAGE (PyG; trained offline, structured risk + flags 67 + inference served in-process) + rationale 68 + | | 69 + +-------------------+--------------------+ 70 + | 71 + v 72 + FUSION POLICY / GATE (section 6.7) 73 + | 74 + v 75 + Calibrated score + decision + explanation 76 + | 77 + v 78 + FastAPI (plain process) -> /score /review /leaderboard 79 + + built-in /dashboard (reads DuckDB) 80 + | 81 + v 82 + (stretch) write assessment back as an AT Proto record 83 + ``` 84 + 85 + **Stack roles** 86 + 87 + - **DuckDB (single embedded store).** Holds everything: the raw event log, the curated tables (contributors, the vouch edge list, PR lifecycle, scores), and the feature views. One file on the external drive. Batch-append from the ingester (single writer); the API and the structural step read from it. Excellent at the analytical aggregations the features need. 88 + - **DuckDB VSS extension or sqlite-vec (optional).** Diff-embedding k-NN for the slop-similarity angle: embed diffs and find near-duplicates of known-bad patterns. Keeps vector search serviceless, no separate search engine. 89 + - **Built-in dashboard (recommended, on-theme).** The challenge is about observability and traceability, so the API serves a small static `/dashboard` page that reads DuckDB aggregates. Low effort and your demo centerpiece. A self-hosted Grafana plus Prometheus is an option for richer charts, but it adds services and disk, so default to the built-in page. 90 + - **Plain Python processes.** The ingester, the scoring worker, and the FastAPI service. Run locally during the hackathon. For a hosted demo, one small VM or a single container; no managed platform required. 91 + - **Offline training and Anthropic.** The GNN is trained offline with checkpoints on the drive and served in-process; Anthropic serves the Claude inference call. 92 + 93 + --- 94 + 95 + ## 4. Stack 96 + 97 + - **Language:** Python 3.11+ throughout (the GNN forces PyTorch; keep one language). 98 + - **Ingest:** `websockets` against a public Jetstream instance; batched appends written directly to DuckDB. 99 + - **Store:** DuckDB, embedded, a single file on the external drive (`$DUCKDB_PATH`), for the event log, curated tables, feature views, and scores. Optional DuckDB VSS extension (or sqlite-vec) for diff-embedding similarity. 100 + - **Structural:** NumPy/SciPy sparse for EigenTrust; PyTorch Geometric for the GNN. 101 + - **Learned baseline:** LightGBM; SHAP for explanations. 102 + - **Content:** Anthropic SDK. Default `claude-sonnet-4-6`; cheap pre-pass `claude-haiku-4-5-20251001`; escalate hard cases to `claude-opus-4-8`. Temperature 0. Force the output schema with tool use / structured outputs. 103 + - **Observability:** a built-in FastAPI `/dashboard` reading DuckDB; self-hosted Grafana plus Prometheus optional if you want richer charts. 104 + - **API and runtime:** FastAPI (Python), co-located with the scorer, run as a plain process. The SvelteKit frontend talks to it directly. If you want the API in your TS house style, a thin Hono (Bun) gateway can front the Python scorer, but the direct path avoids a cross-language hop for the hackathon. For a hosted demo, a single small VM or container. 105 + - **Frontend:** SvelteKit + Svelte 5 (runes), shipped via `@sveltejs/adapter-node`. UI kit bits-ui; styling Lightning CSS with the six-layer cascade; icons unplugin-icons + iconify; charts layerchart; tables tanstack/table-core; toasts svelte-sonner; validation zod. Full screen spec in section 7. 106 + - **Tooling (scoring service):** uv for deps, ruff for lint and format, ty for type checking, pytest for tests. 107 + 108 + ### 4.1 Local disk: route every large file to the external drive 109 + 110 + The development machine is short on space, so all large local artifacts live on a mounted external drive, never on the home or system disk. This is cheap to enforce because the heavy local footprint is small and well contained: the data store is a single DuckDB file, and the rest is the Python and ML toolchain (torch plus torch-geometric are multi-GB), the model and embedding caches, and the transient backfill staging. Route all of it through a single `DATA_ROOT` env var. 111 + 112 + Set `DATA_ROOT` to the mounted drive and create the subtree once (macOS shown; on Linux use a path like `/mnt/ext/tangled-trust`): 113 + 114 + ```bash 115 + # .envrc (source this before running anything in the project) 116 + export DATA_ROOT="/Volumes/EXT/tangled-trust" # the external drive 117 + mkdir -p "$DATA_ROOT"/{venv,pip,hf,torch,pyg,staging,diffs,models,duckdb,logs} 118 + 119 + # Python toolchain (the single biggest hog: torch + torch-geometric wheels) 120 + export PIP_CACHE_DIR="$DATA_ROOT/pip" 121 + export UV_CACHE_DIR="$DATA_ROOT/pip" # if using uv 122 + # Create the venv ON the drive, not inside the repo: 123 + # python -m venv "$DATA_ROOT/venv" && source "$DATA_ROOT/venv/bin/activate" 124 + 125 + # Model and embedding caches (GBs if you do local diff embeddings) 126 + export HF_HOME="$DATA_ROOT/hf" 127 + export TRANSFORMERS_CACHE="$DATA_ROOT/hf" 128 + export SENTENCE_TRANSFORMERS_HOME="$DATA_ROOT/hf" 129 + export TORCH_HOME="$DATA_ROOT/torch" 130 + 131 + # App paths, read from env, all defaulting under DATA_ROOT 132 + export DUCKDB_PATH="$DATA_ROOT/duckdb/trust.duckdb" # primary data store 133 + export PYG_ROOT="$DATA_ROOT/pyg" # PyTorch Geometric processed-dataset cache 134 + export STAGING_DIR="$DATA_ROOT/staging" # Jetstream backfill dumps (NDJSON/Parquet) 135 + export DIFF_CORPUS_DIR="$DATA_ROOT/diffs" # cached PR diffs/patches for eval and training 136 + export MODEL_DIR="$DATA_ROOT/models" # GraphSAGE + LightGBM checkpoints, calibrators 137 + export LOG_DIR="$DATA_ROOT/logs" 138 + ``` 139 + 140 + What this covers, by component: 141 + 142 + - **The DuckDB file (`DUCKDB_PATH`):** the entire data store (event log, curated tables, features, scores) is one file on the drive, so the bulk of the data is on the external drive by design. 143 + - **Python venv and pip/uv cache:** the torch and torch-geometric wheels are the largest local cost; both the environment and the download cache live on the drive. 144 + - **PyG dataset cache (`PYG_ROOT`) and checkpoints (`MODEL_DIR`):** the GNN's cached graph tensors and saved weights from offline training. 145 + - **Hugging Face / sentence-transformers / torch-hub caches:** any local embedding model for the diff-similarity path (DuckDB VSS). 146 + - **`STAGING_DIR`:** the raw Jetstream backfill, written as NDJSON or Parquet before it is loaded into DuckDB. Transient but large during a full replay; write it to the drive and delete after load. 147 + - **`DIFF_CORPUS_DIR`:** cached PR patch text for the Claude eval fixture and any training set. 148 + 149 + Rules: 150 + 151 + - Every component reads these from env and must default its large-output paths under `DATA_ROOT`. Do not hardcode repo-relative or home-relative paths for anything that grows, including the DuckDB file. 152 + - At process startup, assert `DATA_ROOT` exists and is writable, and fail fast with a clear message if the drive is not mounted, so a half-run never scatters files (or the DuckDB file) onto the system disk. 153 + - Only the repo and the small `.env` (the Anthropic API key) stay on the main disk. The data store, the venv, and all caches are on the drive. 154 + - A USB external drive is slower than internal SSD, so DuckDB queries, PyG dataset processing, and disk-heavy steps run somewhat slower. At hackathon-scale data this is fine; keep the drive mounted for the whole run. 155 + 156 + --- 157 + 158 + ## 5. Build order (build in this exact order; each milestone must run before the next) 159 + 160 + - **M0 - Set up the local stack.** Mount the external drive, source `.envrc`, create the venv and the DuckDB file under `DATA_ROOT`, install dependencies. Verify `DATA_ROOT` is writable. No services to provision. 161 + - **M1 - Ingest.** Jetstream to DuckDB with a persisted cursor and historical backfill; a step derives typed rows from the raw event log. Confirm the exact Tangled collection names (6.1). Goal: events landing in DuckDB, resumable after a crash. 162 + - **M2 - Dataset.** Reconstruct PR lifecycles, mine the clean-merge label, build per-DID features as DuckDB SQL views or a batch job (6.2, 6.3). 163 + - **M3 - Structural baseline + end-to-end demo.** EigenTrust over the `vouches` table, a `/score/{did}` endpoint, and the triage queue plus leaderboard screens (section 7). After M3 you have a working, sybil-resistant, demoable system with zero ML training. 164 + - **M3.5 - Observability.** The dashboard screen (section 7) reading `/metrics`: trust-score distribution, fast-lane rate, false-approval budget, vouch-graph stats, and ingest lag. Operational telemetry (events/sec, API latency, Claude cost) goes to Prometheus + Grafana, not this screen. Low effort, directly on-theme, and your demo backdrop. 165 + - **M4 - Content layer + decisions.** Claude review component and the fusion gate (6.6, 6.7), optionally enriched with the code-security and supply-chain findings (6.12) as structured input to the reviewer. Now you have the full hybrid: EigenTrust + Claude. 166 + - **M5 - Learned score.** LightGBM on the features (with the EigenTrust score as a feature), calibrated (6.5, 6.8). 167 + - **M6 - GNN upgrade (stretch).** GraphSAGE trained offline; serve inference in-process; compare against M5. Ship only if it beats the baseline and is stable. 168 + - **M7 - Surfaces (stretch).** Write assessments back as AT Proto records (6.11), add the Tangled-native browser-extension overlay (section 7), the attestation-gated sensitive-repo tier (6.13), and/or an ElevenLabs voice briefing on the API. 169 + 170 + The GNN is M6 on purpose: on a new, sparsely vouched network it will likely not beat M5 and is the most likely thing to break mid-demo. Always have M4 working first. 171 + 172 + --- 173 + 174 + ## 6. Component specs 175 + 176 + ### 6.1 Ingestion (Jetstream to DuckDB) 177 + 178 + Connect a websocket to a public Jetstream instance, filtered server-side to only the collections you need: 179 + 180 + ``` 181 + wss://jetstream2.us-east.bsky.network/subscribe?wantedCollections=sh.tangled.* 182 + ``` 183 + 184 + - Also subscribe to `app.bsky.graph.*` if you want the cross-ATmosphere social signal (follower graph, account age). 185 + - **Confirm the exact NSIDs. Do NOT hardcode guesses.** The Tangled lexicons live in the `tangled.org/tangled.org/core` repo; read them, and log a sample of live Jetstream events to see the real `collection` values for pull requests, vouches, CI/pipeline ("spindle") runs, issues, comments, and stars. Known facts: Tangled records live under `sh.tangled.*`; vouch/denounce records are public records on the issuer's PDS and each carries a reason; CI emits pull_request / push / manual pipeline events. Verify everything else against source. 186 + - **Writer:** DuckDB is single-writer and OLAP, so do NOT insert row-by-row from the socket handler. Buffer events in memory and append in batches to the `events` table (or stage them as Parquet under `STAGING_DIR` and load). A single ingester process owns the write path; everything else reads. A derive step turns the raw log into the typed tables (contributors, vouches, pull_requests). 187 + - **Cursor:** persist the `time_us` of the last processed event in an `ingest_state` row (or a small cursor file under `DATA_ROOT`). On reconnect, resume from that cursor minus a few seconds for gapless playback. An absent cursor means live-tail; a past cursor backfills, which is how you build the training history. This cursor plus the durable `events` log is the resumability that a broker would otherwise provide. 188 + - **Account and Identity events** arrive regardless of the collection filter; use Identity events to refresh a DID's handle and document. 189 + - Each event gives `did`, `time_us`, and a `commit` with `operation` (create/update/delete), `collection`, `rkey`, and the JSON `record`. 190 + 191 + ### 6.2 Data model (all in the DuckDB file) 192 + 193 + Every table lives in the single DuckDB file at `$DUCKDB_PATH` on the external drive. 194 + 195 + - `events(did, time_us, operation, collection, rkey, record JSON, ...)` -- the raw append log, written in batches by the ingester 196 + - `contributors(did PK, handle, did_created_at, pds_host, first_seen)` 197 + - `vouches(voucher_did, subject_did, polarity int{+1,-1}, reason text, evidence_uri, created_at, weight)` -- this is the entire graph; no graph DB 198 + - `pull_requests(pr_id PK, author_did, repo, target, opened_at, ci_status, merged bool, merged_at, closed_unmerged bool, additions int, deletions int, files_touched int, diff_text, discussion_len int)` 199 + - `pr_followups(pr_id, reverted bool, patched_same_lines_within_n_days bool)` 200 + - `features` -- per-DID aggregates as a SQL view or a materialized table refreshed by a batch step (merged counts, revert rate, CI pass rate, diff-size stats, discussion length) 201 + - `scores(did, as_of, structural_trust, content_risk, calibrated_prob, decision, explanation_json)` 202 + - `ingest_state(stream, last_time_us)` 203 + 204 + ### 6.3 Label mining (the supervised target) 205 + 206 + For each historical PR, derive a binary `clean_merge` label automatically: 207 + 208 + - **1 (clean):** merged AND CI passed AND not reverted AND the same lines not patched within N days (default N = 14). 209 + - **0 (not clean):** reverted, or closed unmerged, or repeated CI failure, or a quick follow-up fix to the same lines. 210 + - Drop PRs too recent for the N-day window to have elapsed. 211 + 212 + Aggregate to a per-DID signal. **Split by time, not randomly**, so you never train on the future. 213 + 214 + ### 6.4 Structural signal: EigenTrust (required baseline) 215 + 216 + Read the edge list from the DuckDB `vouches` table, build a row-normalized sparse matrix, seed on the trusted maintainer DID(s), and run personalized power iteration: 217 + 218 + ```python 219 + # SELECT voucher_did, subject_did, weight FROM vouches -> build sparse C (n x n) 220 + # C[i][j] = normalized trust i places in j; rows sum to 1. 221 + # Edge weight before normalization: base 1.0, scaled up if the vouch carries 222 + # PR evidence, scaled down by age (time decay). 223 + # p: seed vector, mass on the maintainer DID(s), normalized. 224 + # alpha: restart probability ~0.15. 225 + t = p.copy() 226 + for _ in range(50): 227 + t = (1 - alpha) * (C.T @ t) + alpha * p 228 + t = t / t.sum() 229 + # t[did] is the structural trust; expose it as a signal and as a model feature. 230 + ``` 231 + 232 + - **Denounces:** classic EigenTrust assumes a non-negative stochastic matrix. Keep it simple: a denounce zeroes trust into that node and is recorded as a negative node feature for the learned models. Do NOT make distrust flow transitively. 233 + - Seeding on the maintainer makes scores viewer-relative, matching Tangled's circle philosophy but propagated across the whole graph with decay. 234 + - **Path explanation:** during the run, keep the edge list in memory and do a short BFS from the seed to reconstruct the trust path for the explanation object. No graph DB. 235 + 236 + ### 6.5 Learned signal: LightGBM, then GraphSAGE 237 + 238 + **LightGBM (M5, reliable):** predict `clean_merge` from per-DID features (read from the DuckDB `features` view). Include `eigentrust_score` as a feature so the model builds on the graph signal. Suggested features: 239 + 240 + ``` 241 + eigentrust_score, did_age_days, merged_pr_count, revert_rate, ci_pass_rate, 242 + close_without_merge_ratio, mean_diff_size, mean_files_touched, churn, 243 + mean_discussion_len, bsky_graph_degree, bsky_account_age, denounce_count 244 + ``` 245 + 246 + Trains in seconds, resists overfitting at small N far better than a net, and gives SHAP explanations. Save the model and calibrator under `MODEL_DIR`. Calibrate the output (6.8). 247 + 248 + **GraphSAGE GNN (M6, stretch upgrade):** an inductive node-classification model. 249 + 250 + ```python 251 + # nodes: contributors, with the feature vector above as node features x 252 + # edges: built from the vouches table into a PyG edge_index tensor (positive, 253 + # weighted), plus co-contribution edges; no graph DB involved 254 + # task: node-level binary classification against clean_merge 255 + # model: GraphSAGE, 2 layers, hidden 64, out 1; neighbor sampling (inductive) 256 + # train OFFLINE: BCEWithLogitsLoss on labeled nodes, temporal split; 257 + # checkpoints + PyG cache under MODEL_DIR / PYG_ROOT on the drive 258 + # serve inference in-process: sigmoid(logit) -> structural_trust_gnn 259 + ``` 260 + 261 + - Use the inductive variant so it generalizes to unseen contributors (cold start). 262 + - **Signed edges:** either use a signed GNN (SignedGCN) or, simpler, keep the GNN on positive vouch edges and pass denounce-count as a node feature. 263 + - GNN explanations are weak; the human-facing explanation stays the SHAP factors and/or the EigenTrust path plus Claude's rationale. 264 + 265 + ### 6.6 Content signal: Claude review 266 + 267 + Assesses ONE PR's actual content. **Cost gate:** do not call the expensive model on every PR. 268 + 269 + - `structural_trust >= T_HIGH`: skip the Sonnet review unless the diff touches security-sensitive paths. 270 + - `T_LOW <= structural_trust < T_HIGH` (ambiguous band): run the review. This is where Claude earns its keep. 271 + - `structural_trust < T_LOW`: run the review to attach a concrete reason for the human. 272 + - Optionally run a 1-call Haiku pre-pass everywhere to decide whether a Sonnet review is warranted. 273 + 274 + **Input:** the diff, PR title and description, and discussion text, truncated to a token budget. **No author identity, handle, or history.** 275 + 276 + **Model:** `claude-sonnet-4-6`, temperature 0, output forced to the JSON schema via tool use. 277 + 278 + **System prompt for this component (use verbatim):** 279 + 280 + ``` 281 + You are a code-contribution reviewer for an open-source trust system. You assess ONE 282 + pull request's actual content for quality and safety. You do not decide whether to 283 + merge; you produce a structured risk assessment that a separate policy layer combines 284 + with an identity-trust signal. 285 + 286 + Hard rules: 287 + - Judge only the artifact in front of you: the diff, the PR title and description, and 288 + the discussion. You are given NO information about the author's identity, reputation, 289 + or history, and you must not speculate about it. Identity trust is handled elsewhere. 290 + - Your job is to catch problems a reputation signal cannot see: code that looks correct 291 + but is subtly wrong, plausible-looking machine-generated filler ("slop"), 292 + security-sensitive changes, leaked secrets or credentials, license violations, and 293 + changes whose stated intent does not match what the code does. 294 + - Prefer flagging uncertainty over approving. If the diff is large, unclear, or you 295 + cannot verify correctness, say so and set review_recommended. Never rubber-stamp. 296 + - Be specific. Every flag must point to concrete lines or patterns, not vibes. 297 + - Output ONLY the structured object specified by the tool. No prose outside it. 298 + ``` 299 + 300 + **Output schema (tool use):** 301 + 302 + ```json 303 + { 304 + "content_risk": "float 0.0 (clearly safe/trivial) to 1.0 (clearly broken or dangerous)", 305 + "flags": [ 306 + { 307 + "type": "subtle_bug | slop | security | secret_leak | license | intent_mismatch | untested | oversized | other", 308 + "severity": "low | med | high", 309 + "location": "file and/or line reference", 310 + "explanation": "concrete reason tied to the code" 311 + } 312 + ], 313 + "summary": "1-3 sentence plain-language rationale, suitable to read aloud to a maintainer", 314 + "review_recommended": "boolean" 315 + } 316 + ``` 317 + 318 + ### 6.7 Fusion and decision policy (a gate, not an average) 319 + 320 + ```python 321 + def decide(structural_trust, content, cfg): 322 + # structural_trust: calibrated P(clean) in [0,1] 323 + # content: dict from 6.6, or None if no Claude call was made 324 + risk = 0.0 if content is None else content["content_risk"] 325 + review = False if content is None else content["review_recommended"] 326 + high_flag = bool(content) and any(f["severity"] == "high" for f in content["flags"]) 327 + 328 + if structural_trust < cfg.T_LOW or risk >= cfg.R_HIGH or high_flag: 329 + return "needs_human", build_reason(structural_trust, content) 330 + if structural_trust >= cfg.T_HIGH and risk <= cfg.R_LOW and not review: 331 + return "fast_lane", build_reason(structural_trust, content) 332 + return "normal_queue", build_reason(structural_trust, content) 333 + ``` 334 + 335 + - **Displayed score:** start from the calibrated structural P(clean), then penalize for content flags. A low structural score can never be lifted into fast-lane by clean-looking content (constraint 2). 336 + - Thresholds are config. Set `T_HIGH` from calibration so the historical false-approval rate above it stays under your chosen budget. Write every decision to the DuckDB `scores` table. 337 + 338 + ### 6.8 Calibration 339 + 340 + Hold out a time-based split. Fit isotonic regression (or Platt scaling) mapping the raw model score to an empirical P(clean). Report a reliability curve. The fast-lane threshold then corresponds to a concrete false-approval budget. 341 + 342 + ### 6.9 Explainability 343 + 344 + Emit a structured `explanation` per score: the top SHAP feature contributions (LightGBM) or the dominant EigenTrust path from the in-memory BFS ("vouched by trusted maintainers X, Y; 34 merged PRs; 0 reverts"), plus Claude's `summary` and any flags when a review ran. This is also what a voice layer would read aloud. 345 + 346 + ### 6.10 API and runtime 347 + 348 + Run as plain Python processes. 349 + 350 + - `GET /score/{did}` -> `{ calibrated_prob, structural_trust, content_risk?, decision, explanation, top_factors }` 351 + - `POST /review/pr` -> body `{ diff, title, description, discussion }`, runs 6.6, returns the schema object. 352 + - `GET /leaderboard` -> contributors ranked by calibrated_prob. 353 + - `GET /metrics` -> aggregate JSON for the dashboard: score distribution, fast-lane rate, false-approval rate, vouch-graph stats, ingest lag. The UI (section 7) renders it; the API serves JSON only. 354 + - A scoring worker (a separate process or a loop) picks up new PR records (poll the `events` table for unprocessed PRs, or have the ingester hand them off in-process), runs `decide(...)`, and writes results to `scores`. No message broker. 355 + - Optionally cache hot scores in-process; no separate cache service. 356 + - For a hosted demo, package the processes into a single container or run them on one small VM. 357 + 358 + ### 6.11 AT Proto-native output (stretch, but what the judges reward) 359 + 360 + Give the service its own DID. Write each assessment back as a public record on its PDS (its own lexicon, referencing the PR's `at://` URI), so verdicts are auditable provenance on the network, not rows in a private file. Consume state from the firehose; emit state as records. This is the difference between a native ATProto integration and a tool that happens to read Tangled. 361 + 362 + ### 6.12 External data sources (additional signals) 363 + 364 + All of these are public and either contribution-based or track-record-based, fetched on demand and cached. None requires probing a contributor or correlating identity. Each is a weak, advisory feature, never a determination. 365 + 366 + Code-security and supply-chain (feed the content-risk signal in 6.6 and the gate in 6.7). This targets the malware half of the brief that the trust graph alone does not cover: 367 + 368 + - Vulnerability databases: cross-reference every dependency a PR adds or bumps against OSV.dev, the GitHub Advisory Database, and NVD/CVE, through an index like deps.dev. 369 + - Malicious-package and typosquat signals: flag dependencies that are newly published, low-download, or near-misses of popular names (the classic supply-chain shape), using registry publish age and download stats. 370 + - Secret scanning on the diff (gitleaks or betterleaks) for leaked keys and credentials. 371 + - SAST on the diff (Semgrep rules or CodeQL) for dangerous constructs. 372 + - License data (SPDX) on added files and dependencies, for license violations. 373 + 374 + Hand these machine findings to the Claude reviewer (6.6) as structured input, so it reasons over concrete evidence instead of judging code in a vacuum. 375 + 376 + Verifiable track record (feed the structural features in 6.5). Strong and hard to fake, but use only links the contributor publicly declares; inferring an undeclared one is the deanonymization line in 6.13: 377 + 378 + - Package-registry maintainer history: npm, PyPI, and crates.io tenure, publish history, and download scale for packages they maintain. 379 + - OpenSSF Scorecard and repo-health metrics for repos they own. 380 + - Commit signing: verified SSH/GPG or Sigstore signatures, for cryptographic attribution provenance. 381 + 382 + ATmosphere identity depth (feed the structural and DID-provenance features in 6.5). Your best native sybil signal, because the DID is shared across apps: 383 + 384 + - Participation across other AT Protocol apps under the same DID (blogs, Frontpage, Smoke Signal, and others), with the age and breadth of that footprint. A DID woven through the ATmosphere for years is expensive to fake; a fresh one tied to a single app is the attacker's profile. 385 + - Verified links in the DID document: a domain-verified did:web, a DNS-verified handle, self-declared verified accounts. 386 + 387 + Timezone consistency (a feature, not a location). Derive a coarse activity-timezone band from commit UTC offsets and posting times, which are already in the data, and use it only as a coherence check: a contributor whose declared context, vouch neighborhood, and commit timezone disagree is worth a second look. Never emit it as a location claim. 388 + 389 + ### 6.13 Provenance, jurisdiction, and repo tiering 390 + 391 + The regulatory question is not "where is this contributor" but "is this contribution safe to trust," and jurisdiction, where it genuinely matters, comes from verification, not inference. 392 + 393 + - Verified jurisdiction by assertion: a contributor-issued jurisdiction attestation (a signed record), a verified organizational DID with a known jurisdiction, or a domain-verified did:web on an organization domain. This is the only jurisdiction source a compliance reviewer accepts, and a VPN cannot defeat it. Inference clears neither bar (accuracy against a VPN, lawful use against non-consenting third parties), so the system does not attempt it. 394 + - Repo tiering is the actual control, mirroring how export control works by controlling the artifact and the access rather than surveilling the person: 395 + - Public or civilian tier: open; the trust-graph triage in 6.7 is sufficient. 396 + - Sensitive or dual-use tier: a valid jurisdiction attestation is required before a contribution can be fast-laned or merged. A missing attestation forces `needs_human` regardless of structural trust or content risk. 397 + - The weak hints in 6.12 (PDS host, DID method, handle TLD, locale, timezone) are fed to the model as features; none is treated as a jurisdiction determination. 398 + 399 + This whole layer uses only what a contributor publicly declares or cryptographically asserts. The system never infers or correlates real-world identity or location (see the non-goal in section 8): no IP geolocation, no OSINT location-finding, no cross-platform profile matching, no fingerprinting, no stylometric deanonymization. That is both a legal constraint for an EU operator handling third-party personal data and a fit with the DID and pseudonymity model the platform rests on. 400 + 401 + --- 402 + 403 + ## 7. User-facing surfaces (UI) 404 + 405 + The scoring service is the brain. Every UI is a thin client that reads the API (`/score`, `/leaderboard`, `/metrics`) and never touches the DuckDB file directly. Two surfaces ship as your own SvelteKit app; one is a native overlay. 406 + 407 + Frontend stack: SvelteKit + Svelte 5 with runes, shipped via `@sveltejs/adapter-node`. UI kit bits-ui; styling Lightning CSS with the six-layer cascade (`@layer reset, tokens, base, components, utilities, overrides`); icons unplugin-icons + iconify; charts layerchart; tables tanstack/table-core; toasts svelte-sonner; validation zod. Server state via tanstack query is optional at this scale. 408 + 409 + ### 7.1 Triage queue (the product, route `/`) 410 + 411 + The maintainer's open PRs across their repos, grouped by decision into fast-lane, needs review, and flagged. Each row shows the contributor avatar and handle, the PR title with repo and number, the calibrated score as a pill colored by decision (success / warning / danger), and a one-line reason. Rows expand to the breakdown from the explanation object (6.9): the structural side (the EigenTrust path and top factors) and the content side (Claude's flags and summary). Render the list with tanstack/table-core, sortable and filterable by repo and bucket, with a metric-card strip on top (open, fast-lane, needs review, flagged). Per-row actions: approve a fast-lane row, or pull one into your review anyway. Approving can call Tangled's API to merge, or simply record the action. The decision and the reason come straight from the gate (6.7); the UI renders them, it does not decide. 412 + 413 + ### 7.2 Observability dashboard (route `/dashboard`, milestone M3.5) 414 + 415 + The trust view and your demo backdrop, reading `/metrics`: a score-distribution histogram (layerchart), the fast-lane rate, the false-approval rate from the backtest, vouch-graph stats (contributors, edges, seed), and ingest lag. Keep operational telemetry off this screen. Events per second, API latency, and Claude call cost and latency go to Prometheus + Grafana from your self-hosted stack, and Langfuse can trace the Claude review calls for per-call eval. 416 + 417 + ### 7.3 Leaderboard (route `/leaderboard`) 418 + 419 + Contributors ranked by calibrated trust, the playful nod to the Tangled push-leaderboard tradition. tanstack/table-core, sortable. Cheap to build and good demo candy. 420 + 421 + ### 7.4 Tangled-native overlay (stretch, the native surface) 422 + 423 + A thin browser extension whose content script injects the trust hat and Claude's note onto tangled.org PR and contributor pages, reading the same `/score` API client-side. It is UI only; the brain stays in the service. Build it as a minimal content script with your TS toolchain (Bun build, oxlint and oxfmt, zod to parse the response). This lands inline placement without waiting on Tangled to merge anything. The upstream version, Tangled's own appview rendering third-party trust records natively, is the vision, not the build; ask Lewis whether the appview can render trust records authored by other DIDs. 424 + 425 + Build placement: the triage queue and leaderboard land with M3 once `/score` exists, the dashboard with M3.5, and the extension overlay with M7. 426 + 427 + --- 428 + 429 + ## 8. Guardrails and non-goals 430 + 431 + Do: 432 + - Keep the structural signal sybil-resistant and load-bearing. 433 + - Keep Claude blind to author identity; combine via the gate, not an average. 434 + - Calibrate the score and tie the threshold to a false-approval budget. 435 + - Confirm Tangled NSIDs from source and live stream; never hardcode guesses. 436 + - Keep the stack serviceless and embedded: one DuckDB file on the external drive, plain processes; resumability comes from the Jetstream cursor plus the raw event log, not a message broker. 437 + - Run Claude at temperature 0 with forced schema, and gate calls by cost. 438 + - Keep the brain in the scoring service; the SvelteKit UI and the extension are thin clients that read the API, never the DuckDB file. 439 + - Write every large artifact (the DuckDB file, venv, caches, staging, checkpoints, diffs) under `DATA_ROOT` on the external drive; never on the home or system disk, and fail fast if the drive is not mounted. 440 + - For jurisdiction where it genuinely matters, require a contributor-issued attestation or verified DID, never an inference, and gate sensitive-tier repos on it (6.13). 441 + 442 + Do not: 443 + - Add a graph database. Edges are rows; graph compute is in-memory (SciPy / PyG). 444 + - Add a message broker, a separate database server, or a managed cloud data service; the embedded store is enough at this scale. 445 + - Train the GNN online; train it offline and serve inference in-process. 446 + - Block, ban, or punish users; this system informs and routes only. 447 + - Infer or correlate real-world identity or location: no IP geolocation, no OSINT location-finding, no cross-platform profile matching (LinkedIn and similar), no browser or network fingerprinting, no stylometric deanonymization. Use only what a contributor publicly declares or cryptographically asserts. 448 + - Let clean content fast-lane an untrusted DID. 449 + - Make denounces propagate transitively. 450 + - Ship the GNN unless it beats the calibrated LightGBM baseline and is stable. 451 + 452 + --- 453 + 454 + ## 9. Deliverable 455 + 456 + A running FastAPI scoring service backed by an embedded DuckDB store on the external drive, built from real Tangled data via Jetstream, fronted by a SvelteKit app with a triage queue, an observability dashboard, and a leaderboard, exposing calibrated and explained trust scores and fast-lane / human-review decisions, with EigenTrust + Claude working end to end (M4) before any GNN work. The browser-extension overlay onto Tangled PR pages is the stretch native surface. Include a short demo script that scores a few real contributors and shows one PR routed to a human with Claude's reason and one fast-laned on structural trust.
+41
pyproject.toml
··· 1 + [project] 2 + name = "tangled-trust" 3 + version = "0.1.0" 4 + description = "Hybrid contributor trust scoring for Tangled (EigenTrust + Claude)" 5 + requires-python = ">=3.11" 6 + dependencies = [ 7 + "duckdb>=1.1", 8 + "numpy>=1.26", 9 + "scipy>=1.11", 10 + "fastapi>=0.115", 11 + "uvicorn>=0.30", 12 + "websockets>=12", 13 + "anthropic>=0.40", 14 + "pydantic>=2.7", 15 + ] 16 + 17 + # Stretch milestones (M5 LightGBM, M6 GraphSAGE). Not needed for M0-M4. 18 + # Install with: uv pip install -e '.[learned]' / '.[gnn]' 19 + # shap dropped on purpose: LightGBM's native pred_contrib gives TreeSHAP values 20 + # without the shap/numba dependency (which lags new Python versions). 21 + learned = ["lightgbm>=4.3", "scikit-learn>=1.4"] 22 + gnn = ["torch>=2.2", "torch-geometric>=2.5"] 23 + 24 + [project.scripts] 25 + trust-ingest = "trust.ingest:main" 26 + trust-score = "trust.score:main" 27 + trust-api = "trust.api:main" 28 + trust-seed = "trust.seed:main" 29 + trust-train = "trust.learned:main" 30 + trust-gnn = "trust.gnn:main" 31 + trust-publish = "trust.atproto:main" 32 + 33 + [build-system] 34 + requires = ["hatchling"] 35 + build-backend = "hatchling.build" 36 + 37 + [tool.hatch.build.targets.wheel] 38 + packages = ["src/trust"] 39 + 40 + [tool.ruff] 41 + line-length = 100
+12
src/trust/__init__.py
··· 1 + """Hybrid contributor trust scoring for Tangled (EigenTrust + Claude).""" 2 + 3 + import os as _os 4 + 5 + # lightgbm (M5) and torch (M6) each bundle their own libomp; loading both in one 6 + # process hangs/crashes on macOS. Allow the duplicate and pin to one thread (all 7 + # compute here is hackathon-scale). Package init is the earliest reliable point — 8 + # it runs before any submodule imports lightgbm or torch. 9 + # ponytail: env guard over subprocess isolation; isolate the M5 baseline call in a 10 + # subprocess if this ever misbehaves on another platform. 11 + _os.environ.setdefault("OMP_NUM_THREADS", "1") 12 + _os.environ.setdefault("KMP_DUPLICATE_LIB_OK", "TRUE")
+182
src/trust/api.py
··· 1 + """M3/M4 API + built-in surfaces (PRD 6.10, section 7). 2 + 3 + The scoring service is the brain; the UIs are thin clients that read these JSON 4 + endpoints, never the DuckDB file. Serves the three section-7 surfaces as static 5 + pages (PRD blesses a built-in dashboard; same laziness applies to the others). 6 + ponytail: built-in HTML over a separate SvelteKit/Bun stack; swap if the native 7 + overlay (M7) or richer UI is needed. 8 + """ 9 + 10 + from __future__ import annotations 11 + 12 + import json 13 + from contextlib import asynccontextmanager 14 + from pathlib import Path 15 + 16 + from fastapi import Depends, FastAPI, HTTPException 17 + from fastapi.middleware.cors import CORSMiddleware 18 + from fastapi.responses import FileResponse 19 + from fastapi.staticfiles import StaticFiles 20 + from pydantic import BaseModel 21 + 22 + from .config import CFG 23 + from .db import connection, ensure_schema 24 + from . import eigentrust, fusion, review as review_mod 25 + 26 + 27 + @asynccontextmanager 28 + async def lifespan(app): 29 + ensure_schema() # create tables/view once; readers below open read-only 30 + yield 31 + 32 + 33 + app = FastAPI(title="Tangled trust scoring", lifespan=lifespan) 34 + # ponytail: permissive CORS on a local read-only service so the tangled.org overlay 35 + # (7.4) can read /score client-side. Lock to the extension origin for a hosted demo. 36 + app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["GET"], allow_headers=["*"]) 37 + STATIC = Path(__file__).parent / "static" 38 + 39 + 40 + def get_con(): 41 + # Short-lived read-only connection per request so the score-loop writer can interleave. 42 + with connection(read_only=True) as con: 43 + yield con 44 + 45 + 46 + def _eigen(con): 47 + # ponytail: recompute per request; few-thousand-edge graph -> sub-ms. Cache if it grows. 48 + return eigentrust.compute(con) 49 + 50 + 51 + @app.get("/score/{did}") 52 + def score(did: str, con=Depends(get_con)): 53 + er = _eigen(con) 54 + feats = fusion._features_for(con, did) 55 + structural, model_factors = fusion.structural_for(did, er, feats) # M5 calibrated, or raw EigenTrust 56 + latest = con.execute( 57 + "SELECT content_risk, calibrated_prob, decision, explanation_json FROM scores " 58 + "WHERE did=? ORDER BY as_of DESC LIMIT 1", [did] 59 + ).fetchone() 60 + content_risk = latest[0] if latest else None 61 + decision = latest[2] if latest else fusion.decide(structural, None) 62 + reason = json.loads(latest[3]) if latest else fusion.build_reason( 63 + did, structural, None, er, feats, model_factors) 64 + prob = latest[1] if latest else structural 65 + return {"did": did, "structural_trust": structural, "content_risk": content_risk, 66 + "calibrated_prob": prob, "decision": decision, "explanation": reason, 67 + "top_factors": reason.get("top_factors", [])} 68 + 69 + 70 + class ReviewBody(BaseModel): 71 + diff: str 72 + title: str = "" 73 + description: str = "" 74 + discussion: str = "" 75 + 76 + 77 + @app.post("/review/pr") 78 + def review(body: ReviewBody): 79 + out = review_mod.review_pr(body.diff, body.title, body.description, body.discussion) 80 + if out is None: 81 + raise HTTPException(503, f"set {CFG.review.api_key_env} to enable Claude review") 82 + return out 83 + 84 + 85 + @app.get("/leaderboard") 86 + def leaderboard(limit: int = 50, con=Depends(get_con)): 87 + er = _eigen(con) 88 + handles = dict(con.execute("SELECT did, handle FROM contributors").fetchall()) 89 + ranked = sorted(er.trust.items(), key=lambda kv: kv[1], reverse=True)[:limit] 90 + return [{"did": d, "handle": handles.get(d), "calibrated_prob": round(t, 4), 91 + "decision": fusion.decide(t, None)} for d, t in ranked] 92 + 93 + 94 + @app.get("/triage") 95 + def triage(con=Depends(get_con)): 96 + """Open PRs grouped by decision, with the explanation breakdown (section 7.1).""" 97 + er = _eigen(con) 98 + handles = dict(con.execute("SELECT did, handle FROM contributors").fetchall()) 99 + rows = con.execute( 100 + "SELECT pr_id, author_did, repo FROM pull_requests WHERE NOT merged AND NOT closed_unmerged" 101 + ).fetchall() 102 + out = [] 103 + for pr_id, did, repo in rows: 104 + structural = er.trust.get(did, 0.0) 105 + latest = con.execute( 106 + "SELECT calibrated_prob, decision, explanation_json FROM scores WHERE did=? " 107 + "ORDER BY as_of DESC LIMIT 1", [did] 108 + ).fetchone() 109 + prob = latest[0] if latest else structural 110 + decision = latest[1] if latest else fusion.decide(structural, None) 111 + reason = json.loads(latest[2]) if latest else fusion.build_reason( 112 + did, structural, None, er, fusion._features_for(con, did)) 113 + out.append({"pr_id": pr_id, "repo": repo, "handle": handles.get(did), "did": did, 114 + "calibrated_prob": round(prob, 4), "decision": decision, "explanation": reason}) 115 + return out 116 + 117 + 118 + @app.get("/metrics") 119 + def metrics(con=Depends(get_con)): 120 + """Aggregates for the dashboard (PRD 6.10 / 7.2). JSON only; the UI renders it.""" 121 + er = _eigen(con) 122 + dist = [0] * 10 123 + for t in er.trust.values(): 124 + dist[min(int(t * 10), 9)] += 1 125 + 126 + decisions = {"fast_lane": 0, "normal_queue": 0, "needs_human": 0} 127 + for t in er.trust.values(): 128 + decisions[fusion.decide(t, None)] += 1 129 + total = max(sum(decisions.values()), 1) 130 + 131 + # False-approval backtest: of historical PRs whose author is fast-lane-eligible 132 + # (structural >= T_HIGH), what fraction were NOT clean_merge? (PRD 6.8) 133 + fa_total = fa_bad = 0 134 + for did, clean_rate in con.execute( 135 + "SELECT did, clean_merge_rate FROM features WHERE clean_merge_rate IS NOT NULL" 136 + ).fetchall(): 137 + if er.trust.get(did, 0.0) >= CFG.gate.T_HIGH: 138 + fa_total += 1 139 + if clean_rate < 1.0: 140 + fa_bad += 1 141 + cur = con.execute("SELECT last_time_us FROM ingest_state WHERE stream='jetstream'").fetchone() 142 + return { 143 + "score_distribution": dist, 144 + "decisions": decisions, 145 + "fast_lane_rate": decisions["fast_lane"] / total, 146 + "false_approval_rate": (fa_bad / fa_total) if fa_total else None, 147 + "vouch_graph": { 148 + "contributors": con.execute("SELECT count(*) FROM contributors").fetchone()[0], 149 + "edges": con.execute("SELECT count(*) FROM vouches").fetchone()[0], 150 + "seeds": len(er.seeds), 151 + }, 152 + "ingest_last_time_us": cur[0] if cur else None, 153 + } 154 + 155 + 156 + # --- static surfaces ------------------------------------------------------- 157 + @app.get("/") 158 + def root(): 159 + return FileResponse(STATIC / "triage.html") 160 + 161 + 162 + @app.get("/dashboard") 163 + def dashboard(): 164 + return FileResponse(STATIC / "dashboard.html") 165 + 166 + 167 + @app.get("/leaderboard.html") 168 + def leaderboard_page(): 169 + return FileResponse(STATIC / "leaderboard.html") 170 + 171 + 172 + app.mount("/static", StaticFiles(directory=STATIC), name="static") 173 + 174 + 175 + def main() -> None: 176 + import uvicorn 177 + 178 + uvicorn.run(app, host="127.0.0.1", port=8000) 179 + 180 + 181 + if __name__ == "__main__": 182 + main()
+130
src/trust/atproto.py
··· 1 + """M7 AT-Proto-native output (PRD 6.11): publish each assessment as a public record. 2 + 3 + The service has its own DID/PDS account; it consumes state from the firehose and 4 + emits state as records on the network, so verdicts are auditable provenance rather 5 + than rows in a private file. Records use our own lexicon, `sh.tangled.trust.score`. 6 + 7 + Credentials via env (only these touch the network): 8 + ATPROTO_PDS PDS base URL (e.g. https://pds.example) 9 + ATPROTO_IDENTIFIER the service handle or DID 10 + ATPROTO_PASSWORD an app password 11 + No password -> automatic dry-run (prints the records it would publish), so this is 12 + demoable and testable without an account. 13 + """ 14 + 15 + from __future__ import annotations 16 + 17 + import argparse 18 + import json 19 + import os 20 + 21 + from .config import LOG_DIR # noqa: F401 (keeps DATA_ROOT import side effects consistent) 22 + from .db import connection 23 + 24 + LEXICON = "sh.tangled.trust.score" 25 + 26 + 27 + def build_record(row: dict) -> dict: 28 + """One score row -> an `sh.tangled.trust.score` record (PRD 6.11 lexicon).""" 29 + reason = row.get("explanation") or {} 30 + summary = reason.get("compliance_block") or reason.get("content_summary") \ 31 + or "; ".join(reason.get("top_factors", [])[:2]) or "structural trust assessment" 32 + return { 33 + "$type": LEXICON, 34 + "subject": row["did"], # the assessed contributor (a DID is the native key) 35 + "calibratedProb": round(float(row["calibrated_prob"]), 4), 36 + "decision": row["decision"], 37 + "structuralTrust": round(float(row["structural_trust"]), 4), 38 + "contentRisk": row.get("content_risk"), 39 + "summary": summary[:280], 40 + "createdAt": str(row["as_of"]), 41 + } 42 + 43 + 44 + def _latest_unpublished(con, limit: int) -> list[dict]: 45 + rows = con.execute( 46 + "SELECT s.did, s.as_of, s.structural_trust, s.content_risk, s.calibrated_prob, " 47 + " s.decision, s.explanation_json " 48 + "FROM scores s " 49 + "JOIN (SELECT did, max(as_of) m FROM scores GROUP BY did) l " 50 + " ON s.did = l.did AND s.as_of = l.m " 51 + "WHERE NOT EXISTS (SELECT 1 FROM published_records p WHERE p.did=s.did AND p.as_of=s.as_of) " 52 + "LIMIT ?", [limit] 53 + ).fetchall() 54 + return [{"did": r[0], "as_of": r[1], "structural_trust": r[2], "content_risk": r[3], 55 + "calibrated_prob": r[4], "decision": r[5], "explanation": json.loads(r[6] or "{}")} 56 + for r in rows] 57 + 58 + 59 + def _session(pds: str, identifier: str, password: str) -> tuple[str, str]: 60 + import httpx 61 + 62 + r = httpx.post(f"{pds}/xrpc/com.atproto.server.createSession", 63 + json={"identifier": identifier, "password": password}, timeout=30) 64 + r.raise_for_status() 65 + d = r.json() 66 + return d["accessJwt"], d["did"] 67 + 68 + 69 + def _create_record(pds: str, jwt: str, repo_did: str, record: dict) -> str: 70 + import httpx 71 + 72 + r = httpx.post(f"{pds}/xrpc/com.atproto.repo.createRecord", 73 + headers={"Authorization": f"Bearer {jwt}"}, 74 + json={"repo": repo_did, "collection": LEXICON, "record": record}, timeout=30) 75 + r.raise_for_status() 76 + return r.json()["uri"] 77 + 78 + 79 + def publish(dry_run: bool = False, limit: int = 100) -> list[dict]: 80 + pds = os.environ.get("ATPROTO_PDS") 81 + ident = os.environ.get("ATPROTO_IDENTIFIER") 82 + pw = os.environ.get("ATPROTO_PASSWORD") 83 + live = bool(pds and ident and pw) and not dry_run 84 + 85 + with connection(read_only=not live) as con: 86 + rows = _latest_unpublished(con, limit) 87 + out = [] 88 + jwt = repo_did = None 89 + if live: 90 + jwt, repo_did = _session(pds, ident, pw) 91 + for row in rows: 92 + rec = build_record(row) 93 + if live: 94 + uri = _create_record(pds, jwt, repo_did, rec) 95 + con.execute("INSERT INTO published_records VALUES (?,?,?) " 96 + "ON CONFLICT DO NOTHING", [row["did"], row["as_of"], uri]) 97 + else: 98 + uri = None 99 + out.append({"uri": uri, "record": rec}) 100 + return out 101 + 102 + 103 + def main() -> None: 104 + ap = argparse.ArgumentParser(description="publish trust assessments as AT-Proto records") 105 + ap.add_argument("--dry-run", action="store_true", help="print records without publishing") 106 + ap.add_argument("--limit", type=int, default=100) 107 + args = ap.parse_args() 108 + results = publish(dry_run=args.dry_run, limit=args.limit) 109 + mode = "published" if (results and results[0]["uri"]) else "dry-run (no creds or --dry-run)" 110 + for r in results: 111 + print(f" {r['uri'] or '(dry-run)'} {r['record']['decision']:<12} " 112 + f"{r['record']['calibratedProb']} {r['record']['subject']}") 113 + print(f"[publish] {len(results)} assessments, {mode}") 114 + 115 + 116 + def demo() -> None: 117 + """Self-check: a score row builds a valid record (no network).""" 118 + row = {"did": "did:plc:carol", "as_of": "2026-06-25T00:00:00Z", "structural_trust": 1.0, 119 + "content_risk": None, "calibrated_prob": 1.0, "decision": "fast_lane", 120 + "explanation": {"top_factors": ["trust reaches did:plc:carol via maintainer -> alice"]}} 121 + rec = build_record(row) 122 + assert rec["$type"] == LEXICON and rec["subject"] == "did:plc:carol" 123 + assert rec["decision"] == "fast_lane" and 0 <= rec["calibratedProb"] <= 1 124 + assert rec["summary"] and rec["createdAt"] 125 + print(json.dumps(rec, indent=2)) 126 + print("ok") 127 + 128 + 129 + if __name__ == "__main__": 130 + demo()
+170
src/trust/backfill.py
··· 1 + """Historical backfill: scrape ALL sh.tangled.* records across the network, once. 2 + 3 + `trust.ingest` is the live tail (Jetstream, ~5s replay). This is the history: 4 + enumerate every repo holding a sh.tangled.* collection via the relay's 5 + listReposByCollection, pull each repo's records with listRecords (JSON, no CAR 6 + parsing), archive them raw to the `events` table, and feed them through the SAME 7 + ingest.derive() -> contributors / vouches / pull_requests. Then train as usual 8 + (`python -m trust.learned`, `python -m trust.gnn`). 9 + 10 + Storage: everything goes through connection() -> DUCKDB_PATH under DATA_ROOT, so 11 + point DATA_ROOT at the external drive (`export DATA_ROOT=/Volumes/<drive>`); the 12 + writability assert in ensure_data_root() fails fast if it isn't mounted. 13 + 14 + Resumability: derive() upserts / inserts ON CONFLICT DO NOTHING, so re-running is 15 + idempotent — just run it again to resume. ponytail: idempotent writes instead of a 16 + checkpoint table; add a per-DID cursor table only if a full run gets too slow to repeat. 17 + 18 + Confirm record shapes FIRST: `python -m trust.backfill --sample` prints real 19 + records so you can verify the field names derive() assumes (the config NSID map and 20 + field guesses are flagged unconfirmed). If pulls turn out to be thin pointers 21 + (diff/CI live on the knot), that's a second fetch — don't build it until --sample 22 + proves it's needed (YAGNI). 23 + """ 24 + 25 + from __future__ import annotations 26 + 27 + import argparse 28 + import json 29 + import time 30 + import urllib.error 31 + import urllib.parse 32 + import urllib.request 33 + 34 + from .db import connection, ensure_schema 35 + from .ingest import derive 36 + 37 + # listReposByCollection lives on the relay; listRecords on each repo's PDS. 38 + RELAY = "https://relay1.us-west.bsky.network" 39 + PLC = "https://plc.directory" 40 + 41 + # Collections we know how to derive (must hit a COLLECTION_KINDS substring in config). 42 + COLLECTIONS = ["sh.tangled.repo.pull", "sh.tangled.graph.vouch", "sh.tangled.graph.follow"] 43 + 44 + 45 + def _get(url: str, tries: int = 4) -> dict: 46 + """GET JSON with naive backoff on 429/5xx. ponytail: linear sleep, no token bucket.""" 47 + for i in range(tries): 48 + try: 49 + req = urllib.request.Request(url, headers={"User-Agent": "trust-backfill"}) 50 + with urllib.request.urlopen(req, timeout=30) as r: 51 + return json.load(r) 52 + except urllib.error.HTTPError as e: 53 + if e.code in (429, 502, 503) and i < tries - 1: 54 + time.sleep(2 * (i + 1)) 55 + continue 56 + raise 57 + return {} 58 + 59 + 60 + def _pds(did: str) -> str | None: 61 + """DID -> PDS endpoint. Handles did:plc via PLC directory and did:web inline.""" 62 + if did.startswith("did:web:"): 63 + doc = _get(f"https://{did[len('did:web:'):]}/.well-known/did.json") 64 + else: 65 + doc = _get(f"{PLC}/{urllib.parse.quote(did)}") 66 + for s in doc.get("service", []): 67 + if s.get("type") == "AtprotoPersonalDataServer": 68 + return s["serviceEndpoint"].rstrip("/") 69 + return None 70 + 71 + 72 + def _repos(collection: str): 73 + """All DIDs holding `collection`, paginated via the relay.""" 74 + cursor = None 75 + while True: 76 + q = {"collection": collection, "limit": "500"} 77 + if cursor: 78 + q["cursor"] = cursor 79 + page = _get(f"{RELAY}/xrpc/com.atproto.sync.listReposByCollection?{urllib.parse.urlencode(q)}") 80 + for r in page.get("repos", []): 81 + yield r["did"] 82 + cursor = page.get("cursor") 83 + if not cursor: 84 + return 85 + 86 + 87 + def _records(pds: str, did: str, collection: str): 88 + """All records of one collection in one repo, paginated via listRecords.""" 89 + cursor = None 90 + while True: 91 + q = {"repo": did, "collection": collection, "limit": "100"} 92 + if cursor: 93 + q["cursor"] = cursor 94 + page = _get(f"{pds}/xrpc/com.atproto.repo.listRecords?{urllib.parse.urlencode(q)}") 95 + for rec in page.get("records", []): 96 + yield rec # {uri, cid, value} 97 + cursor = page.get("cursor") 98 + if not cursor: 99 + return 100 + 101 + 102 + def _archive_and_derive(buf: list[tuple]) -> None: 103 + """Durable raw log to `events` (on the external drive) + derive into typed tables. 104 + Does NOT touch ingest_state — that cursor belongs to the live firehose, not backfill.""" 105 + if not buf: 106 + return 107 + with connection(read_only=False) as con: 108 + con.executemany( 109 + "INSERT INTO events (did, time_us, operation, collection, rkey, record) VALUES (?,?,?,?,?,?)", 110 + buf, 111 + ) 112 + derive(con, buf) 113 + 114 + 115 + def backfill(collections=COLLECTIONS, max_repos: int | None = None) -> dict: 116 + ensure_schema() 117 + counts: dict[str, int] = {} 118 + for col in collections: 119 + repos = records = 0 120 + for did in _repos(col): 121 + if max_repos and repos >= max_repos: 122 + break 123 + repos += 1 124 + pds = _pds(did) 125 + if not pds: 126 + print(f"[backfill] {col} {did}: no PDS, skip") 127 + continue 128 + buf = [] 129 + for rec in _records(pds, did, col): 130 + # time_us=0: listRecords has no firehose seq; the archive key is the (did, collection, rkey). 131 + rkey = rec["uri"].rsplit("/", 1)[-1] 132 + buf.append((did, 0, "create", col, rkey, json.dumps(rec["value"]))) 133 + _archive_and_derive(buf) 134 + records += len(buf) 135 + print(f"[backfill] {col} {did} -> {len(buf)} records") 136 + counts[col] = records 137 + print(f"[backfill] {col}: {records} records from {repos} repos") 138 + return counts 139 + 140 + 141 + def sample(collection: str = COLLECTIONS[0], n: int = 3) -> None: 142 + """Print real record values so you can confirm the fields derive() assumes.""" 143 + for did in _repos(collection): 144 + pds = _pds(did) 145 + if not pds: 146 + continue 147 + shown = 0 148 + for rec in _records(pds, did, collection): 149 + print(json.dumps(rec["value"], indent=2)) 150 + shown += 1 151 + if shown >= n: 152 + return 153 + 154 + 155 + def main() -> None: 156 + ap = argparse.ArgumentParser(description="Backfill all sh.tangled.* history into DuckDB (under DATA_ROOT)") 157 + ap.add_argument("--sample", action="store_true", help="print real records to confirm field shapes, write nothing") 158 + ap.add_argument("--collection", default=None, help="restrict to one NSID (default: all known)") 159 + ap.add_argument("--max-repos", type=int, default=None, help="cap repos per collection (smoke test)") 160 + args = ap.parse_args() 161 + if args.sample: 162 + sample(args.collection or COLLECTIONS[0]) 163 + return 164 + cols = [args.collection] if args.collection else COLLECTIONS 165 + c = backfill(cols, max_repos=args.max_repos) 166 + print(f"[backfill] done: {c}") 167 + 168 + 169 + if __name__ == "__main__": 170 + main()
+109
src/trust/config.py
··· 1 + """Config: env-driven paths (all large artifacts under DATA_ROOT) + tuning knobs. 2 + 3 + PRD section 4.1: every large file lives on the external drive, routed through 4 + DATA_ROOT. We fail fast at startup if DATA_ROOT is set-but-unwritable, so a 5 + half-run never scatters the DuckDB file onto the system disk. 6 + """ 7 + 8 + from __future__ import annotations 9 + 10 + import os 11 + from dataclasses import dataclass, field 12 + from pathlib import Path 13 + 14 + # ponytail: DATA_ROOT unset in dev -> fall back to repo-local .data with a warning. 15 + # The PRD's safety property ("never scatter onto system disk if the drive is gone") 16 + # is satisfied by the writability assert below; point DATA_ROOT at /Volumes/EXT in prod. 17 + DATA_ROOT = Path(os.environ.get("DATA_ROOT") or (Path(__file__).resolve().parents[2] / ".data")) 18 + DUCKDB_PATH = Path(os.environ.get("DUCKDB_PATH") or (DATA_ROOT / "duckdb" / "trust.duckdb")) 19 + STAGING_DIR = Path(os.environ.get("STAGING_DIR") or (DATA_ROOT / "staging")) 20 + MODEL_DIR = Path(os.environ.get("MODEL_DIR") or (DATA_ROOT / "models")) 21 + LOG_DIR = Path(os.environ.get("LOG_DIR") or (DATA_ROOT / "logs")) 22 + 23 + 24 + _warned = False 25 + 26 + 27 + def ensure_data_root() -> Path: 28 + """Create the DATA_ROOT subtree and assert it is writable. Call at startup.""" 29 + global _warned 30 + if not os.environ.get("DATA_ROOT") and not _warned: 31 + print(f"[config] DATA_ROOT unset -> dev fallback {DATA_ROOT} (set DATA_ROOT for the external drive)") 32 + _warned = True 33 + for d in (DATA_ROOT, DUCKDB_PATH.parent, STAGING_DIR, MODEL_DIR, LOG_DIR): 34 + d.mkdir(parents=True, exist_ok=True) 35 + probe = DATA_ROOT / ".write_probe" 36 + try: 37 + probe.write_text("ok") 38 + probe.unlink() 39 + except OSError as e: 40 + raise SystemExit(f"DATA_ROOT not writable ({DATA_ROOT}): {e}. Is the external drive mounted?") 41 + return DATA_ROOT 42 + 43 + 44 + # --- Tangled lexicon NSIDs ------------------------------------------------- 45 + # PRD 6.1: CONFIRM these against tangled.org core lexicons + a live Jetstream 46 + # sample before trusting them; `python -m trust.ingest --probe` logs the real 47 + # `collection` values seen on the wire. Override via env without touching code. 48 + # Known fact: Tangled records live under `sh.tangled.*`. 49 + WANTED_COLLECTIONS = os.environ.get("WANTED_COLLECTIONS", "sh.tangled.*,app.bsky.graph.*") 50 + JETSTREAM_URL = os.environ.get( 51 + "JETSTREAM_URL", "wss://jetstream2.us-east.bsky.network/subscribe" 52 + ) 53 + 54 + # collection -> our internal record kind. Patterns are substring matches on the 55 + # NSID. These are best-guess shapes; `--probe` tells you the truth. ponytail: 56 + # substring map over a lexicon parser; swap to exact NSIDs once confirmed. 57 + COLLECTION_KINDS: dict[str, str] = { 58 + "tangled.pull": "pull_request", 59 + "tangled.repo.pull": "pull_request", 60 + "tangled.vouch": "vouch", 61 + "tangled.graph.vouch": "vouch", 62 + "tangled.denounce": "denounce", 63 + "tangled.pipeline": "ci", 64 + "tangled.spindle": "ci", 65 + "tangled.issue": "issue", 66 + "tangled.star": "star", 67 + "tangled.attestation": "attestation", # jurisdiction attestation (6.13); CONFIRM NSID 68 + "tangled.jurisdiction": "attestation", 69 + "bsky.graph.follow": "follow", 70 + } 71 + 72 + 73 + @dataclass 74 + class GateConfig: 75 + """Fusion gate thresholds (PRD 6.7). Tune T_HIGH from calibration so the 76 + historical false-approval rate above it stays under the chosen budget.""" 77 + 78 + T_LOW: float = 0.30 # below -> needs_human 79 + T_HIGH: float = 0.70 # above (and content clean) -> fast_lane 80 + R_LOW: float = 0.20 # content risk considered clean 81 + R_HIGH: float = 0.60 # content risk considered dangerous 82 + 83 + 84 + @dataclass 85 + class EigenConfig: 86 + alpha: float = 0.15 # restart probability 87 + iters: int = 50 88 + age_halflife_days: float = 180.0 # vouch weight time-decay 89 + evidence_boost: float = 1.5 # vouch carrying PR evidence weighs more 90 + 91 + 92 + @dataclass 93 + class ReviewConfig: 94 + model: str = os.environ.get("CLAUDE_MODEL", "claude-sonnet-4-6") 95 + prepass_model: str = "claude-haiku-4-5-20251001" 96 + escalate_model: str = "claude-opus-4-8" 97 + max_diff_chars: int = 24_000 # token budget guard 98 + api_key_env: str = "ANTHROPIC_API_KEY" 99 + 100 + 101 + @dataclass 102 + class Config: 103 + gate: GateConfig = field(default_factory=GateConfig) 104 + eigen: EigenConfig = field(default_factory=EigenConfig) 105 + review: ReviewConfig = field(default_factory=ReviewConfig) 106 + clean_merge_window_days: int = 14 # PRD 6.3 label-mining N 107 + 108 + 109 + CFG = Config()
+168
src/trust/db.py
··· 1 + """DuckDB store: schema, connection, the derive step, and feature/label SQL. 2 + 3 + Single embedded file at DUCKDB_PATH (PRD 2: no graph DB, no server). The 4 + ingester is the only writer; everything else reads. 5 + """ 6 + 7 + from __future__ import annotations 8 + 9 + import time 10 + from contextlib import contextmanager 11 + 12 + import duckdb 13 + 14 + from .config import CFG, DUCKDB_PATH, ensure_data_root 15 + 16 + SCHEMA = """ 17 + CREATE TABLE IF NOT EXISTS events ( 18 + did VARCHAR, time_us BIGINT, operation VARCHAR, collection VARCHAR, 19 + rkey VARCHAR, record JSON, ingested_at TIMESTAMP DEFAULT now() 20 + ); 21 + CREATE TABLE IF NOT EXISTS contributors ( 22 + did VARCHAR PRIMARY KEY, handle VARCHAR, did_created_at TIMESTAMP, 23 + pds_host VARCHAR, first_seen TIMESTAMP DEFAULT now() 24 + ); 25 + -- vouches IS the whole graph (PRD 2): a plain edge list, no graph engine. 26 + CREATE TABLE IF NOT EXISTS vouches ( 27 + voucher_did VARCHAR, subject_did VARCHAR, polarity INTEGER DEFAULT 1, 28 + reason VARCHAR, evidence_uri VARCHAR, created_at TIMESTAMP, weight DOUBLE DEFAULT 1.0, 29 + PRIMARY KEY (voucher_did, subject_did) 30 + ); 31 + CREATE TABLE IF NOT EXISTS pull_requests ( 32 + pr_id VARCHAR PRIMARY KEY, author_did VARCHAR, repo VARCHAR, target VARCHAR, 33 + opened_at TIMESTAMP, ci_status VARCHAR, merged BOOLEAN, merged_at TIMESTAMP, 34 + closed_unmerged BOOLEAN, additions INTEGER, deletions INTEGER, 35 + files_touched INTEGER, diff_text VARCHAR, discussion_len INTEGER 36 + ); 37 + CREATE TABLE IF NOT EXISTS pr_followups ( 38 + pr_id VARCHAR PRIMARY KEY, reverted BOOLEAN DEFAULT FALSE, 39 + patched_same_lines_within_n_days BOOLEAN DEFAULT FALSE 40 + ); 41 + CREATE TABLE IF NOT EXISTS scores ( 42 + did VARCHAR, as_of TIMESTAMP DEFAULT now(), structural_trust DOUBLE, 43 + content_risk DOUBLE, calibrated_prob DOUBLE, decision VARCHAR, explanation_json JSON 44 + ); 45 + CREATE TABLE IF NOT EXISTS ingest_state (stream VARCHAR PRIMARY KEY, last_time_us BIGINT); 46 + -- trusted maintainer seed set for personalized EigenTrust (PRD 6.4) 47 + CREATE TABLE IF NOT EXISTS seeds (did VARCHAR PRIMARY KEY); 48 + -- repo tiering (PRD 6.13): sensitive/dual-use repos gate fast-lane on an attestation 49 + CREATE TABLE IF NOT EXISTS repo_tiers (repo VARCHAR PRIMARY KEY, tier VARCHAR DEFAULT 'public'); 50 + -- contributor-issued jurisdiction attestations (signed records); declared, never inferred 51 + CREATE TABLE IF NOT EXISTS attestations ( 52 + did VARCHAR, jurisdiction VARCHAR, method VARCHAR, created_at TIMESTAMP, 53 + PRIMARY KEY (did, jurisdiction) 54 + ); 55 + -- AT-Proto writeback (PRD 6.11): the at:// URI of each assessment published as a record 56 + CREATE TABLE IF NOT EXISTS published_records ( 57 + did VARCHAR, as_of TIMESTAMP, uri VARCHAR, PRIMARY KEY (did, as_of) 58 + ); 59 + """ 60 + 61 + # Per-DID feature view (PRD 6.3/6.5). eigentrust_score + bsky_* are joined in 62 + # at scoring time (computed in Python / from app.bsky events). 63 + FEATURES_VIEW = f""" 64 + CREATE OR REPLACE VIEW features AS 65 + WITH pr AS ( 66 + SELECT p.*, COALESCE(f.reverted, FALSE) AS reverted, 67 + COALESCE(f.patched_same_lines_within_n_days, FALSE) AS patched_quick, 68 + -- clean_merge label (PRD 6.3); NULL when too recent to have elapsed the N-day window 69 + CASE 70 + WHEN p.opened_at > now() - INTERVAL {CFG.clean_merge_window_days} DAY THEN NULL 71 + WHEN p.merged AND p.ci_status = 'passed' 72 + AND NOT COALESCE(f.reverted, FALSE) 73 + AND NOT COALESCE(f.patched_same_lines_within_n_days, FALSE) THEN 1 74 + ELSE 0 75 + END AS clean_merge 76 + FROM pull_requests p LEFT JOIN pr_followups f USING (pr_id) 77 + ) 78 + SELECT 79 + c.did, 80 + date_diff('day', c.did_created_at, now()) AS did_age_days, 81 + COUNT(*) FILTER (WHERE pr.merged) AS merged_pr_count, 82 + COALESCE(AVG(CASE WHEN pr.reverted THEN 1.0 ELSE 0.0 END), 0) AS revert_rate, 83 + COALESCE(AVG(CASE WHEN pr.ci_status='passed' THEN 1.0 ELSE 0.0 END), 0) AS ci_pass_rate, 84 + COALESCE(AVG(CASE WHEN pr.closed_unmerged THEN 1.0 ELSE 0.0 END), 0) AS close_without_merge_ratio, 85 + COALESCE(AVG(pr.additions + pr.deletions), 0) AS mean_diff_size, 86 + COALESCE(AVG(pr.files_touched), 0) AS mean_files_touched, 87 + COALESCE(SUM(pr.additions + pr.deletions), 0) AS churn, 88 + COALESCE(AVG(pr.discussion_len), 0) AS mean_discussion_len, 89 + (SELECT COUNT(*) FROM vouches v WHERE v.subject_did = c.did AND v.polarity < 0) AS denounce_count, 90 + AVG(pr.clean_merge) AS clean_merge_rate 91 + FROM contributors c 92 + LEFT JOIN pr ON pr.author_did = c.did 93 + GROUP BY c.did, c.did_created_at; 94 + """ 95 + 96 + 97 + # Per-PR clean_merge label (PRD 6.3) for supervised training; NULL when too recent. 98 + PR_LABELS_VIEW = f""" 99 + CREATE OR REPLACE VIEW pr_labels AS 100 + SELECT p.pr_id, p.author_did, p.opened_at, 101 + CASE 102 + WHEN p.opened_at > now() - INTERVAL {CFG.clean_merge_window_days} DAY THEN NULL 103 + WHEN p.merged AND p.ci_status = 'passed' 104 + AND NOT COALESCE(f.reverted, FALSE) 105 + AND NOT COALESCE(f.patched_same_lines_within_n_days, FALSE) THEN 1 106 + ELSE 0 107 + END AS clean_merge 108 + FROM pull_requests p LEFT JOIN pr_followups f USING (pr_id); 109 + """ 110 + 111 + 112 + def connect(read_only: bool = False) -> duckdb.DuckDBPyConnection: 113 + ensure_data_root() 114 + con = duckdb.connect(str(DUCKDB_PATH), read_only=read_only) 115 + return con 116 + 117 + 118 + def init_db(con: duckdb.DuckDBPyConnection | None = None) -> duckdb.DuckDBPyConnection: 119 + con = con or connect() 120 + con.execute(SCHEMA) 121 + con.execute(FEATURES_VIEW) 122 + con.execute(PR_LABELS_VIEW) 123 + return con 124 + 125 + 126 + @contextmanager 127 + def connection(read_only: bool = False, attempts: int = 40, delay: float = 0.25): 128 + """Short-lived connection with retry on DuckDB's cross-process file lock. 129 + 130 + DuckDB allows only one read-write process; a held lock blocks every other 131 + open (even read-only). So long-running processes (API, score loop, ingester) 132 + must open->work->close per operation, letting panes interleave under mprocs. 133 + ponytail: open/close + retry over a single-writer daemon; fine at hackathon 134 + scale, revisit if write throughput matters. 135 + """ 136 + ensure_data_root() 137 + con = last = None 138 + for _ in range(attempts): 139 + try: 140 + con = duckdb.connect(str(DUCKDB_PATH), read_only=read_only) 141 + break 142 + except duckdb.IOException as e: 143 + last = e 144 + time.sleep(delay) 145 + if con is None: 146 + raise last 147 + try: 148 + yield con 149 + finally: 150 + con.close() 151 + 152 + 153 + def ensure_schema() -> None: 154 + """Create tables + the features view once (read-write), then release the lock.""" 155 + with connection(read_only=False) as con: 156 + con.execute(SCHEMA) 157 + con.execute(FEATURES_VIEW) 158 + con.execute(PR_LABELS_VIEW) 159 + 160 + 161 + def main() -> None: 162 + con = init_db() 163 + tables = con.execute("SHOW TABLES").fetchall() 164 + print(f"[db] initialised {DUCKDB_PATH} with {len(tables)} tables/views") 165 + 166 + 167 + if __name__ == "__main__": 168 + main()
+141
src/trust/eigentrust.py
··· 1 + """M3 structural signal: EigenTrust over the vouches edge list (PRD 6.4). 2 + 3 + Reads rows into a SciPy sparse matrix and runs personalized power iteration in 4 + memory. No graph DB (PRD 2). Path explanations come from an in-memory BFS from 5 + the seed, not graph-DB traversal. 6 + """ 7 + 8 + from __future__ import annotations 9 + 10 + import math 11 + from collections import deque 12 + from dataclasses import dataclass 13 + 14 + import numpy as np 15 + from scipy import sparse 16 + 17 + from .config import CFG 18 + 19 + 20 + @dataclass 21 + class EigenResult: 22 + trust: dict[str, float] # did -> structural trust in [0,1] (max-normalized) 23 + index: dict[str, int] 24 + seeds: list[str] 25 + _adj: dict[str, list[str]] # positive-edge adjacency for BFS paths 26 + 27 + def path_from_seed(self, did: str, max_hops: int = 4) -> list[str]: 28 + """Shortest positive-vouch path seed -> did, for the explanation (PRD 6.4).""" 29 + if did in self.seeds: 30 + return [did] 31 + seen = set(self.seeds) 32 + q: deque[list[str]] = deque([[s] for s in self.seeds]) 33 + while q: 34 + path = q.popleft() 35 + if len(path) > max_hops: 36 + continue 37 + for nxt in self._adj.get(path[-1], ()): 38 + if nxt in seen: 39 + continue 40 + if nxt == did: 41 + return path + [nxt] 42 + seen.add(nxt) 43 + q.append(path + [nxt]) 44 + return [] 45 + 46 + 47 + def _age_weight(created_at, now_us: float) -> float: 48 + if created_at is None: 49 + return 1.0 50 + try: 51 + age_days = (now_us - created_at.timestamp()) / 86400.0 52 + except (AttributeError, TypeError): 53 + return 1.0 54 + return 0.5 ** (max(age_days, 0) / CFG.eigen.age_halflife_days) 55 + 56 + 57 + def compute(con) -> EigenResult: 58 + import datetime 59 + 60 + now_us = datetime.datetime.now(datetime.timezone.utc).timestamp() 61 + rows = con.execute( 62 + "SELECT voucher_did, subject_did, polarity, weight, evidence_uri, created_at FROM vouches" 63 + ).fetchall() 64 + seeds = [r[0] for r in con.execute("SELECT did FROM seeds").fetchall()] 65 + dids = {d for (d,) in con.execute("SELECT did FROM contributors").fetchall()} 66 + for v, s, *_ in rows: 67 + dids.update((v, s)) 68 + dids.update(seeds) 69 + 70 + if not dids: 71 + return EigenResult({}, {}, seeds, {}) 72 + index = {d: i for i, d in enumerate(sorted(dids))} 73 + n = len(index) 74 + 75 + # denounced nodes get incoming trust zeroed (PRD 6.4: distrust does NOT flow transitively) 76 + denounced = {s for (v, s, pol, *_) in rows if pol is not None and pol < 0} 77 + 78 + src, dst, data = [], [], [] 79 + adj: dict[str, list[str]] = {} 80 + for voucher, subject, polarity, weight, evidence, created_at in rows: 81 + if polarity is not None and polarity < 0: 82 + continue # denounce: recorded as a feature, never a positive edge 83 + if subject in denounced: 84 + continue # ponytail: any denounce starves the node; per-edge weighting if it matters 85 + w = (weight or 1.0) * _age_weight(created_at, now_us) 86 + if evidence: 87 + w *= CFG.eigen.evidence_boost 88 + src.append(index[voucher]); dst.append(index[subject]); data.append(w) 89 + adj.setdefault(voucher, []).append(subject) 90 + 91 + C = sparse.csr_matrix((data, (src, dst)), shape=(n, n)) 92 + row_sums = np.asarray(C.sum(axis=1)).ravel() 93 + row_sums[row_sums == 0] = 1.0 94 + C = sparse.diags(1.0 / row_sums) @ C # row-normalize: C[i,j] = trust i places in j 95 + 96 + p = np.zeros(n) 97 + if seeds: 98 + for s in seeds: 99 + p[index[s]] = 1.0 100 + else: 101 + p[:] = 1.0 # ponytail: no seed configured -> uniform restart (global PageRank fallback) 102 + p /= p.sum() 103 + 104 + Ct = C.T.tocsr() 105 + t = p.copy() 106 + a = CFG.eigen.alpha 107 + for _ in range(CFG.eigen.iters): 108 + t = (1 - a) * (Ct @ t) + a * p 109 + s = t.sum() 110 + if s > 0: 111 + t /= s 112 + 113 + hi = t.max() or 1.0 114 + trust = {d: float(t[i] / hi) for d, i in index.items()} # max-normalize to [0,1] 115 + return EigenResult(trust, index, seeds, adj) 116 + 117 + 118 + def demo() -> None: 119 + """Self-check: a sybil cluster vouching for itself must be starved (PRD 1.1).""" 120 + from .db import init_db 121 + 122 + con = init_db() 123 + con.execute("DELETE FROM vouches; DELETE FROM seeds; DELETE FROM contributors") 124 + edges = [("seed", "alice"), ("alice", "bob"), ("bob", "carol"), # trusted chain 125 + ("sybil1", "sybil2"), ("sybil2", "sybil1"), ("sybil2", "sybil3"), ("sybil3", "sybil1")] 126 + for v, s in edges: 127 + con.execute("INSERT INTO contributors (did) VALUES (?) ON CONFLICT DO NOTHING", [v]) 128 + con.execute("INSERT INTO contributors (did) VALUES (?) ON CONFLICT DO NOTHING", [s]) 129 + con.execute("INSERT INTO vouches (voucher_did, subject_did) VALUES (?,?)", [v, s]) 130 + con.execute("INSERT INTO seeds VALUES ('seed')") 131 + r = compute(con) 132 + trusted = r.trust.get("bob", 0) 133 + sybil = max(r.trust.get(f"sybil{i}", 0) for i in (1, 2, 3)) 134 + print(f"bob={trusted:.3f} sybil_max={sybil:.3f} path(carol)={r.path_from_seed('carol')}") 135 + assert trusted > sybil, "sybil cluster should be starved relative to the trusted chain" 136 + assert r.path_from_seed("carol") == ["seed", "alice", "bob", "carol"] 137 + print("ok") 138 + 139 + 140 + if __name__ == "__main__": 141 + demo()
+222
src/trust/fusion.py
··· 1 + """M4 fusion gate + scoring worker (PRD 6.7, 6.9). 2 + 3 + The gate is NOT an average: a low structural score can never be lifted into the 4 + fast lane by clean-looking content (constraint 2). Content can only penalize. 5 + """ 6 + 7 + from __future__ import annotations 8 + 9 + import json 10 + 11 + from .config import CFG 12 + from . import eigentrust, review as review_mod 13 + 14 + 15 + def decide(structural_trust: float, content: dict | None, cfg=CFG.gate, *, 16 + attestation_required: bool = False, attested: bool = True): 17 + """PRD 6.7 gate. structural_trust is calibrated P(clean) in [0,1]. 18 + 19 + 6.13: a sensitive/dual-use repo requires a valid jurisdiction attestation; a 20 + missing one forces needs_human regardless of structural trust or content risk. 21 + This is the only control that overrides the score, so it is checked first. 22 + """ 23 + if attestation_required and not attested: 24 + return "needs_human" 25 + risk = 0.0 if content is None else content["content_risk"] 26 + review = False if content is None else content["review_recommended"] 27 + high_flag = bool(content) and any(f["severity"] == "high" for f in content["flags"]) 28 + 29 + if structural_trust < cfg.T_LOW or risk >= cfg.R_HIGH or high_flag: 30 + return "needs_human" 31 + if structural_trust >= cfg.T_HIGH and risk <= cfg.R_LOW and not review: 32 + return "fast_lane" 33 + return "normal_queue" 34 + 35 + 36 + def repo_tier(con, repo: str | None) -> str: 37 + """'sensitive' if the repo is in the sensitive/dual-use tier (6.13), else 'public'.""" 38 + if not repo: 39 + return "public" 40 + row = con.execute("SELECT tier FROM repo_tiers WHERE repo=?", [repo]).fetchone() 41 + return row[0] if row else "public" 42 + 43 + 44 + def is_attested(con, did: str) -> bool: 45 + """True if the DID has a contributor-issued jurisdiction attestation (declared, not inferred).""" 46 + return con.execute("SELECT 1 FROM attestations WHERE did=? LIMIT 1", [did]).fetchone() is not None 47 + 48 + 49 + def displayed_prob(structural_trust: float, content: dict | None) -> float: 50 + """Start from structural P(clean), penalize for content risk. Never lifts (6.7).""" 51 + if content is None: 52 + return structural_trust 53 + return structural_trust * (1.0 - content["content_risk"]) 54 + 55 + 56 + def _scorer(): 57 + """Load the M5 LightGBM scorer if trained AND lightgbm is installed; else None.""" 58 + try: 59 + from . import learned 60 + except ImportError: 61 + return None 62 + return learned.load() 63 + 64 + 65 + def _gnn_winner(): 66 + """M6 GraphSAGE scorer ONLY if it beat M5 on the holdout (PRD guardrail); else None.""" 67 + try: 68 + from . import gnn 69 + except ImportError: 70 + return None 71 + return gnn.load_if_winner() 72 + 73 + 74 + def structural_for(did, er: eigentrust.EigenResult, feats: dict | None): 75 + """Calibrated P(clean) for the gate. Precedence: winning GNN (M6) -> LightGBM (M5) 76 + -> raw EigenTrust (M3). The GNN is used only if it provably beat the baseline.""" 77 + g = _gnn_winner() 78 + if g is not None: 79 + return g.prob(did), None # GNN explanations are weak; reason falls back to path/SHAP 80 + scorer = _scorer() 81 + if scorer is not None: 82 + return scorer.prob(did, feats or {}, er), scorer.contributions(did, feats or {}, er) 83 + return er.trust.get(did, 0.0), None 84 + 85 + 86 + def build_reason(did, structural_trust, content, er: eigentrust.EigenResult, feats: dict | None, 87 + model_factors: list | None = None, gate_note: str | None = None): 88 + """Structured explanation (6.9): EigenTrust path + top factors + Claude's rationale.""" 89 + path = er.path_from_seed(did) 90 + top_factors = [] 91 + if gate_note: # 6.13 compliance override, surfaced first so the human sees why 92 + top_factors.append(gate_note) 93 + if path: 94 + top_factors.append(f"trust reaches {did} via {' -> '.join(path)}") 95 + if feats: 96 + if feats.get("merged_pr_count"): 97 + top_factors.append(f"{int(feats['merged_pr_count'])} merged PRs") 98 + if feats.get("revert_rate") is not None: 99 + top_factors.append(f"revert rate {feats['revert_rate']:.0%}") 100 + if feats.get("denounce_count"): 101 + top_factors.append(f"{int(feats['denounce_count'])} denounce(s)") 102 + if model_factors: # M5 LightGBM TreeSHAP contributions (6.9) 103 + for mf in model_factors: 104 + top_factors.append(f"{mf['feature']} ({mf['contribution'] + 0.0:+.3f})") 105 + reason = { 106 + "structural_trust": round(structural_trust, 4), 107 + "trust_path": path, 108 + "top_factors": top_factors, 109 + "model_factors": model_factors or [], 110 + "compliance_block": gate_note, 111 + } 112 + if content is not None: 113 + reason["content_summary"] = content["summary"] 114 + reason["flags"] = content["flags"] 115 + reason["content_risk"] = content["content_risk"] 116 + return reason 117 + 118 + 119 + def should_review(structural_trust: float, security_sensitive: bool, cfg=CFG.gate) -> bool: 120 + """Cost gate (6.6): skip Sonnet for clearly-trusted unless security-sensitive.""" 121 + if structural_trust >= cfg.T_HIGH: 122 + return security_sensitive 123 + return True # ambiguous band and below: review earns its keep / attaches a reason 124 + 125 + 126 + def score_pr(con, pr_id: str, run_review: bool = True) -> dict: 127 + """Full hybrid score for one PR: EigenTrust + (gated) Claude -> decision + write.""" 128 + row = con.execute( 129 + "SELECT author_did, diff_text, repo, target FROM pull_requests WHERE pr_id=?", [pr_id] 130 + ).fetchone() 131 + if not row: 132 + raise ValueError(f"unknown pr {pr_id}") 133 + did, diff, repo, target = row 134 + 135 + er = eigentrust.compute(con) 136 + feats = _features_for(con, did) 137 + structural, model_factors = structural_for(did, er, feats) 138 + 139 + tier = repo_tier(con, repo) # 6.13 repo tiering 140 + attested = is_attested(con, did) 141 + sensitive = tier == "sensitive" 142 + content = None 143 + if run_review and should_review(structural, sensitive): 144 + content = review_mod.review_pr(diff or "", title=repo or "", discussion="") 145 + 146 + decision = decide(structural, content, attestation_required=sensitive, attested=attested) 147 + prob = displayed_prob(structural, content) 148 + gate_note = ("sensitive-tier repo: a valid jurisdiction attestation is required before " 149 + "fast-lane/merge (6.13)") if sensitive and not attested else None 150 + reason = build_reason(did, structural, content, er, feats, model_factors, gate_note) 151 + 152 + con.execute( 153 + "INSERT INTO scores (did, structural_trust, content_risk, calibrated_prob, decision, explanation_json) " 154 + "VALUES (?,?,?,?,?,?)", 155 + [did, structural, (content or {}).get("content_risk"), prob, decision, json.dumps(reason)], 156 + ) 157 + return {"did": did, "structural_trust": structural, "calibrated_prob": prob, 158 + "decision": decision, "explanation": reason} 159 + 160 + 161 + def _features_for(con, did: str) -> dict | None: 162 + cols = [c[0] for c in con.execute("DESCRIBE features").fetchall()] 163 + row = con.execute("SELECT * FROM features WHERE did=?", [did]).fetchone() 164 + return dict(zip(cols, row)) if row else None 165 + 166 + 167 + def _process_pending(con) -> int: 168 + pending = con.execute( 169 + "SELECT pr_id FROM pull_requests WHERE author_did NOT IN (SELECT did FROM scores)" 170 + ).fetchall() 171 + for (pr_id,) in pending: 172 + r = score_pr(con, pr_id) 173 + print(f"{r['decision']:<13} {r['calibrated_prob']:.3f} {pr_id}", flush=True) 174 + return len(pending) 175 + 176 + 177 + def main() -> None: 178 + """Scoring worker: score PRs that have no score yet, write decisions (6.10). 179 + 180 + Default is one-shot; --loop polls forever (a long-lived pane under mprocs), 181 + opening a short-lived read-write connection per cycle so the API can read 182 + between cycles. 183 + """ 184 + import argparse 185 + import time 186 + 187 + from .db import connection, ensure_schema 188 + 189 + ap = argparse.ArgumentParser(description="trust scoring worker") 190 + ap.add_argument("--loop", action="store_true", help="poll forever instead of one pass") 191 + ap.add_argument("--interval", type=float, default=5.0, help="seconds between polls") 192 + args = ap.parse_args() 193 + 194 + ensure_schema() 195 + while True: 196 + with connection(read_only=False) as con: 197 + n = _process_pending(con) 198 + if not args.loop: 199 + print(f"[score] processed {n} PRs") 200 + return 201 + time.sleep(args.interval) 202 + 203 + 204 + def demo() -> None: 205 + """Self-check: gate never fast-lanes a low-trust DID, even on clean content (constraint 2).""" 206 + clean = {"content_risk": 0.0, "review_recommended": False, "flags": [], "summary": "ok"} 207 + risky = {"content_risk": 0.9, "review_recommended": True, 208 + "flags": [{"severity": "high", "type": "subtle_bug", "location": "x", "explanation": "y"}], 209 + "summary": "bad"} 210 + assert decide(0.1, clean) == "needs_human", "low trust + clean content must NOT fast-lane" 211 + assert decide(0.95, clean) == "fast_lane" 212 + assert decide(0.95, risky) == "needs_human", "high-severity flag forces human" 213 + assert decide(0.5, None) == "normal_queue" 214 + assert displayed_prob(0.9, risky) < 0.9, "content risk must penalize, never lift" 215 + # 6.13: a sensitive-tier repo with no attestation forces human even for a perfect score. 216 + assert decide(0.99, clean, attestation_required=True, attested=False) == "needs_human" 217 + assert decide(0.99, clean, attestation_required=True, attested=True) == "fast_lane" 218 + print("ok") 219 + 220 + 221 + if __name__ == "__main__": 222 + demo()
+212
src/trust/gnn.py
··· 1 + """M6 GraphSAGE (stretch). Inductive node classification on the vouch graph. 2 + 3 + PRD M6 / 6.5: GraphSAGE, 2 layers, hidden 64, out 1; nodes are contributors with 4 + the per-DID feature vector as node features; edges are positive vouches + co- 5 + contribution edges (denounce-count rides as a node feature, no signed-edge GNN). 6 + Trained OFFLINE, served in-process. 7 + 8 + Guardrail (PRD section 8, repeated): SHIP THE GNN ONLY IF IT BEATS THE CALIBRATED 9 + LightGBM BASELINE AND IS STABLE. So `train_and_compare` writes a verdict, and 10 + `load_if_winner` (used by fusion) returns a scorer ONLY when the GNN actually beat 11 + M5 on the time-split holdout. On a small, sparsely-vouched graph it won't, and the 12 + system correctly keeps serving M5 — "always have M4/M5 working first." 13 + 14 + Optional: needs `uv pip install -e '.[gnn]'` (torch + torch-geometric, multi-GB). 15 + """ 16 + 17 + from __future__ import annotations 18 + 19 + import json 20 + from types import SimpleNamespace 21 + # OpenMP dual-libomp guard (lightgbm + torch) is set in trust/__init__.py — it must 22 + # run before either library imports, which package init guarantees. 23 + 24 + from .config import MODEL_DIR 25 + from .db import connection 26 + from . import eigentrust, learned 27 + 28 + CKPT = MODEL_DIR / "gnn.pt" 29 + VERDICT = MODEL_DIR / "gnn_verdict.json" 30 + HIDDEN = 64 31 + 32 + 33 + def _sage(in_dim: int): 34 + import torch 35 + from torch_geometric.nn import SAGEConv 36 + 37 + class SAGE(torch.nn.Module): 38 + def __init__(self): 39 + super().__init__() 40 + self.c1 = SAGEConv(in_dim, HIDDEN) # inductive: generalizes to unseen nodes 41 + self.c2 = SAGEConv(HIDDEN, 1) 42 + 43 + def forward(self, x, ei): 44 + import torch.nn.functional as F 45 + 46 + return self.c2(F.relu(self.c1(x, ei)), ei).squeeze(-1) 47 + 48 + return SAGE() 49 + 50 + 51 + def _build_graph(con, mean=None, std=None): 52 + import torch 53 + 54 + er = eigentrust.compute(con) 55 + dids = [r[0] for r in con.execute("SELECT did FROM contributors ORDER BY did").fetchall()] 56 + didx = {d: i for i, d in enumerate(dids)} 57 + fcols = [c[0] for c in con.execute("DESCRIBE features").fetchall()] 58 + feats = {r[0]: dict(zip(fcols, r)) for r in con.execute("SELECT * FROM features").fetchall()} 59 + 60 + raw = torch.tensor([learned._vec(d, feats.get(d, {}), er) for d in dids], dtype=torch.float) 61 + if mean is None: 62 + mean, std = raw.mean(0, keepdim=True), raw.std(0, keepdim=True).clamp_min(1e-6) 63 + x = (raw - mean) / std 64 + 65 + src, dst = [], [] 66 + for v, s in con.execute("SELECT voucher_did, subject_did FROM vouches WHERE polarity > 0").fetchall(): 67 + if v in didx and s in didx: # undirected edges for SAGE mean-aggregation 68 + src += [didx[v], didx[s]]; dst += [didx[s], didx[v]] 69 + for a, b in con.execute( # co-contribution: authored PRs to the same repo (PRD 6.5) 70 + "SELECT DISTINCT a.author_did, b.author_did FROM pull_requests a JOIN pull_requests b " 71 + "ON a.repo = b.repo AND a.author_did < b.author_did" 72 + ).fetchall(): 73 + if a in didx and b in didx: 74 + src += [didx[a], didx[b]]; dst += [didx[b], didx[a]] 75 + edge_index = (torch.tensor([src, dst], dtype=torch.long) if src 76 + else torch.empty((2, 0), dtype=torch.long)) 77 + 78 + # node labels: soft target = clean_merge_rate; temporal split by latest labelled PR 79 + lab = con.execute( 80 + "SELECT author_did, AVG(clean_merge), MAX(opened_at) FROM pr_labels " 81 + "WHERE clean_merge IS NOT NULL GROUP BY author_did ORDER BY MAX(opened_at)" 82 + ).fetchall() 83 + label = {d: float(r) for d, r, _ in lab} 84 + ordered = [d for d, _, _ in lab] 85 + k = max(1, int(len(ordered) * 0.7)) 86 + train_dids, val_dids = ordered[:k], ordered[k:] 87 + y = torch.zeros(len(dids)) 88 + train_mask = torch.zeros(len(dids), dtype=torch.bool) 89 + val_mask = torch.zeros(len(dids), dtype=torch.bool) 90 + for d in ordered: 91 + y[didx[d]] = label[d] 92 + for d in train_dids: 93 + train_mask[didx[d]] = True 94 + for d in val_dids: 95 + val_mask[didx[d]] = True 96 + 97 + return SimpleNamespace(x=x, edge_index=edge_index, y=y, train_mask=train_mask, val_mask=val_mask, 98 + dids=dids, didx=didx, val_dids=val_dids, label=label, feats=feats, er=er, 99 + mean=mean, std=std) 100 + 101 + 102 + def _acc(prob, y) -> float: 103 + return float(((prob >= 0.5) == (y >= 0.5)).float().mean()) if len(y) else float("nan") 104 + 105 + 106 + def _m5_val_acc(g) -> float | None: 107 + try: 108 + s = learned.load() 109 + except ImportError: 110 + return None 111 + if s is None or not g.val_dids: 112 + return None 113 + hits = sum(int((s.prob(d, g.feats.get(d, {}), g.er) >= 0.5) == (g.label[d] >= 0.5)) 114 + for d in g.val_dids) 115 + return hits / len(g.val_dids) 116 + 117 + 118 + def train_and_compare(epochs: int = 300) -> dict: 119 + import torch 120 + 121 + with connection(read_only=True) as con: 122 + g = _build_graph(con) 123 + if not g.val_dids or int(g.train_mask.sum()) == 0: 124 + raise SystemExit("not enough labelled nodes for a temporal split; seed/ingest more history") 125 + 126 + model = _sage(g.x.size(1)) 127 + opt = torch.optim.Adam(model.parameters(), lr=0.01, weight_decay=5e-4) 128 + lossfn = torch.nn.BCEWithLogitsLoss() 129 + for _ in range(epochs): 130 + model.train(); opt.zero_grad() 131 + loss = lossfn(model(g.x, g.edge_index)[g.train_mask], g.y[g.train_mask]) 132 + loss.backward(); opt.step() 133 + 134 + model.eval() 135 + with torch.no_grad(): 136 + prob = torch.sigmoid(model(g.x, g.edge_index)) 137 + stable = bool(torch.isfinite(prob).all()) 138 + gnn_acc = _acc(prob[g.val_mask], g.y[g.val_mask]) 139 + m5_acc = _m5_val_acc(g) 140 + # Beat the baseline strictly, and only when a baseline exists (PRD guardrail). 141 + gnn_wins = bool(stable and m5_acc is not None and gnn_acc > m5_acc) 142 + 143 + MODEL_DIR.mkdir(parents=True, exist_ok=True) 144 + torch.save({"state": model.state_dict(), "mean": g.mean, "std": g.std, "in_dim": g.x.size(1)}, CKPT) 145 + verdict = {"gnn_val_acc": round(gnn_acc, 3), "m5_val_acc": m5_acc, 146 + "val_nodes": len(g.val_dids), "stable": stable, "gnn_wins": gnn_wins} 147 + VERDICT.write_text(json.dumps(verdict, indent=2)) 148 + return verdict 149 + 150 + 151 + class GNNScorer: 152 + """In-process inductive inference: rebuild the current graph, forward, read the node.""" 153 + 154 + def __init__(self, ckpt): 155 + self.ckpt = ckpt 156 + 157 + def prob(self, did: str) -> float: 158 + import torch 159 + 160 + with connection(read_only=True) as con: 161 + g = _build_graph(con, mean=self.ckpt["mean"], std=self.ckpt["std"]) 162 + model = _sage(self.ckpt["in_dim"]) 163 + model.load_state_dict(self.ckpt["state"]) 164 + model.eval() 165 + with torch.no_grad(): 166 + p = torch.sigmoid(model(g.x, g.edge_index)) 167 + i = g.didx.get(did) 168 + return float(p[i]) if i is not None else 0.0 169 + 170 + 171 + def load_if_winner() -> GNNScorer | None: 172 + """Serving hook used by fusion: a GNN scorer ONLY if it beat M5 (else None).""" 173 + if not (VERDICT.exists() and CKPT.exists()): 174 + return None 175 + if not json.loads(VERDICT.read_text()).get("gnn_wins"): 176 + return None 177 + try: 178 + import torch 179 + except ImportError: 180 + return None 181 + return GNNScorer(torch.load(CKPT, weights_only=False)) 182 + 183 + 184 + def main() -> None: 185 + v = train_and_compare() 186 + print(f"[gnn] val nodes={v['val_nodes']} GNN acc={v['gnn_val_acc']} " 187 + f"M5 acc={v['m5_val_acc']} stable={v['stable']}") 188 + print(f"[gnn] gnn_wins={v['gnn_wins']} -> " 189 + + ("SERVED (beats calibrated M5)" if v["gnn_wins"] 190 + else "NOT served; system keeps M5 (PRD guardrail: ship only if it beats the baseline)")) 191 + 192 + 193 + def demo() -> None: 194 + """Self-check: trains, produces finite probs, writes a verdict — stability, not winning.""" 195 + from .db import connection as conn, init_db 196 + from .seed import seed as load_seed 197 + 198 + with conn(read_only=False) as con: 199 + init_db(con) 200 + load_seed(con) 201 + try: 202 + learned.train() # so there's an M5 baseline to compare against 203 + except Exception: 204 + pass 205 + v = train_and_compare(epochs=200) 206 + assert v["stable"], "GNN produced non-finite outputs" 207 + assert isinstance(v["gnn_wins"], bool) 208 + print(f"gnn_val_acc={v['gnn_val_acc']} m5_val_acc={v['m5_val_acc']} gnn_wins={v['gnn_wins']} ok") 209 + 210 + 211 + if __name__ == "__main__": 212 + demo()
+159
src/trust/ingest.py
··· 1 + """M1 ingest: Jetstream firehose -> events (batched) -> derive typed tables. 2 + 3 + Single writer (PRD 6.1). Buffer in memory, append in batches, persist the 4 + `time_us` cursor so a crash resumes gaplessly. The cursor + the durable events 5 + log ARE the resumability a broker would give (PRD 2). 6 + """ 7 + 8 + from __future__ import annotations 9 + 10 + import argparse 11 + import asyncio 12 + import json 13 + from collections import Counter 14 + 15 + import websockets 16 + 17 + from .config import COLLECTION_KINDS, JETSTREAM_URL, WANTED_COLLECTIONS 18 + from .db import connection, ensure_schema 19 + 20 + STREAM = "jetstream" 21 + BATCH = 200 22 + FLUSH_SECONDS = 2.0 23 + 24 + 25 + def _kind(collection: str) -> str | None: 26 + for needle, kind in COLLECTION_KINDS.items(): 27 + if needle in collection: 28 + return kind 29 + return None 30 + 31 + 32 + def _url(con) -> str: 33 + row = con.execute("SELECT last_time_us FROM ingest_state WHERE stream=?", [STREAM]).fetchone() 34 + cursor = f"&cursor={row[0] - 5_000_000}" if row and row[0] else "" # -5s for gapless replay 35 + cols = "".join(f"&wantedCollections={c}" for c in WANTED_COLLECTIONS.split(",")) 36 + return f"{JETSTREAM_URL}?{cols.lstrip('&')}{cursor}" 37 + 38 + 39 + def flush(con, buf: list[tuple]) -> None: 40 + if not buf: 41 + return 42 + con.executemany( 43 + "INSERT INTO events (did, time_us, operation, collection, rkey, record) VALUES (?,?,?,?,?,?)", 44 + buf, 45 + ) 46 + last = max(e[1] for e in buf) 47 + con.execute( 48 + "INSERT INTO ingest_state (stream, last_time_us) VALUES (?, ?) " 49 + "ON CONFLICT (stream) DO UPDATE SET last_time_us=excluded.last_time_us", 50 + [STREAM, last], 51 + ) 52 + derive(con, buf) 53 + buf.clear() 54 + 55 + 56 + def derive(con, events: list[tuple]) -> None: 57 + """Raw event tuples -> contributors / vouches / pull_requests (PRD 6.1, 6.2).""" 58 + for did, time_us, op, collection, rkey, record_json in events: 59 + kind = _kind(collection) 60 + rec = json.loads(record_json) if record_json else {} 61 + con.execute( 62 + "INSERT INTO contributors (did, first_seen) VALUES (?, now()) ON CONFLICT (did) DO NOTHING", 63 + [did], 64 + ) 65 + if kind in ("vouch", "denounce") and op != "delete": 66 + # Real sh.tangled.graph.vouch: subject is the RKEY (at://voucher/.../<subject_did>), 67 + # not a record field; vouch-vs-denounce is in rec["kind"]. Confirmed via listRecords. 68 + subject = (rec.get("subject") or rec.get("subjectDid") 69 + or (rkey if str(rkey).startswith("did:") else None)) 70 + if not subject: 71 + continue 72 + polarity = -1 if (kind == "denounce" or rec.get("kind") == "denounce") else 1 73 + con.execute( 74 + "INSERT INTO vouches (voucher_did, subject_did, polarity, reason, evidence_uri, created_at, weight) " 75 + "VALUES (?,?,?,?,?,?,1.0) ON CONFLICT (voucher_did, subject_did) DO UPDATE SET " 76 + "polarity=excluded.polarity, reason=excluded.reason", 77 + [did, subject, polarity, rec.get("reason"), rec.get("evidence") or rec.get("uri"), 78 + rec.get("createdAt")], 79 + ) 80 + elif kind == "attestation" and op != "delete": # 6.13 jurisdiction attestation 81 + con.execute( 82 + "INSERT INTO attestations (did, jurisdiction, method, created_at) VALUES (?,?,?,?) " 83 + "ON CONFLICT (did, jurisdiction) DO NOTHING", 84 + [did, rec.get("jurisdiction"), rec.get("method", "signed_record"), rec.get("createdAt")], 85 + ) 86 + elif kind == "pull_request" and op != "delete": 87 + pr_id = f"{did}/{collection}/{rkey}" 88 + tgt = rec.get("target") if isinstance(rec.get("target"), dict) else {} 89 + # Real sh.tangled.repo.pull carries identity + branches + body, but NOT the 90 + # label-bearing outcome: merged / ci_status / diff-stats are appview/knot state, 91 + # absent from the PDS record (merged->NULL here). The diff is rounds[].patchBlob 92 + # (a gzipped blob CID), not inline. ci_status/merged stay NULL until joined from 93 + # the appview; see backfill.py header. ponytail: ingest what's in the record, 94 + # leave outcome columns NULL rather than fabricating bool(missing)=False. 95 + con.execute( 96 + "INSERT INTO pull_requests (pr_id, author_did, repo, target, opened_at, ci_status, " 97 + "merged, closed_unmerged, additions, deletions, files_touched, diff_text, discussion_len) " 98 + "VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?) ON CONFLICT (pr_id) DO NOTHING", 99 + [pr_id, did, tgt.get("repo") or rec.get("repo"), tgt.get("branch") or rec.get("target"), 100 + rec.get("createdAt"), rec.get("ciStatus"), rec.get("merged"), False, 101 + rec.get("additions"), rec.get("deletions"), rec.get("filesTouched"), 102 + rec.get("diff"), len(json.dumps(rec.get("body", "")))], 103 + ) 104 + 105 + 106 + def _flush(buf: list[tuple]) -> None: 107 + # Short-lived read-write connection per batch so the API can read between flushes. 108 + with connection(read_only=False) as con: 109 + flush(con, buf) 110 + 111 + 112 + async def run(probe: bool = False, max_events: int | None = None) -> None: 113 + ensure_schema() 114 + with connection(read_only=True) as con: 115 + url = _url(con) 116 + buf: list[tuple] = [] 117 + seen: Counter[str] = Counter() 118 + n = 0 119 + async with websockets.connect(url, max_size=None) as ws: 120 + loop = asyncio.get_event_loop() 121 + last_flush = loop.time() 122 + async for raw in ws: 123 + evt = json.loads(raw) 124 + if evt.get("kind") != "commit": 125 + continue 126 + c = evt["commit"] 127 + collection = c.get("collection", "") 128 + seen[collection] += 1 129 + if probe: 130 + n += 1 131 + if n % 50 == 0: 132 + print(f"[probe] {n} events; top collections: {seen.most_common(10)}") 133 + if max_events and n >= max_events: 134 + print(f"[probe] distinct collections seen:\n " + 135 + "\n ".join(f"{k}: {v}" for k, v in seen.most_common())) 136 + return 137 + continue 138 + buf.append((evt["did"], evt["time_us"], c.get("operation"), collection, 139 + c.get("rkey"), json.dumps(c.get("record")))) 140 + n += 1 141 + if len(buf) >= BATCH or loop.time() - last_flush > FLUSH_SECONDS: 142 + _flush(buf) 143 + last_flush = loop.time() 144 + if max_events and n >= max_events: 145 + break 146 + _flush(buf) 147 + 148 + 149 + def main() -> None: 150 + ap = argparse.ArgumentParser(description="Jetstream -> DuckDB ingester") 151 + ap.add_argument("--probe", action="store_true", 152 + help="log live `collection` values to CONFIRM NSIDs; writes nothing") 153 + ap.add_argument("--max-events", type=int, default=None) 154 + args = ap.parse_args() 155 + asyncio.run(run(probe=args.probe, max_events=args.max_events)) 156 + 157 + 158 + if __name__ == "__main__": 159 + main()
+160
src/trust/learned.py
··· 1 + """M5 learned signal: LightGBM on per-DID features, isotonic-calibrated (PRD 6.5/6.8). 2 + 3 + Predicts clean_merge from the feature vector (eigentrust_score included as a 4 + feature, so the model builds on the graph signal). Trained offline on a 5 + time-based split, calibrated with isotonic regression so the output is a real 6 + P(clean). Explanations use LightGBM's native TreeSHAP (`pred_contrib`) — no 7 + separate shap/numba dependency. 8 + 9 + Stretch milestone: needs `uv pip install -e '.[learned]'`. The system runs 10 + without it (fusion falls back to raw EigenTrust). 11 + """ 12 + 13 + from __future__ import annotations 14 + 15 + import pickle 16 + 17 + import numpy as np 18 + 19 + from .config import MODEL_DIR 20 + from .db import connection 21 + from . import eigentrust 22 + 23 + # PRD 6.5 feature list, restricted to what the features view currently produces. 24 + # bsky_graph_degree / bsky_account_age join in once the app.bsky social graph is ingested. 25 + FEATURE_COLS = [ 26 + "eigentrust_score", "did_age_days", "merged_pr_count", "revert_rate", "ci_pass_rate", 27 + "close_without_merge_ratio", "mean_diff_size", "mean_files_touched", "churn", 28 + "mean_discussion_len", "denounce_count", 29 + ] 30 + MODEL_PATH = MODEL_DIR / "learned.pkl" 31 + 32 + 33 + def _vec(did: str, feats: dict, er: eigentrust.EigenResult) -> list[float]: 34 + out = [] 35 + for c in FEATURE_COLS: 36 + out.append(er.trust.get(did, 0.0) if c == "eigentrust_score" else float(feats.get(c) or 0.0)) 37 + return out 38 + 39 + 40 + class LearnedScorer: 41 + def __init__(self, booster, iso, cols): 42 + self.booster, self.iso, self.cols = booster, iso, cols 43 + 44 + def prob(self, did, feats, er) -> float: 45 + raw = float(self.booster.predict(np.array([_vec(did, feats, er)]))[0]) 46 + return float(self.iso.predict([raw])[0]) if self.iso is not None else raw 47 + 48 + def contributions(self, did, feats, er, top: int = 3) -> list[dict]: 49 + c = self.booster.predict(np.array([_vec(did, feats, er)]), pred_contrib=True)[0][:-1] 50 + idx = np.argsort(np.abs(c))[::-1][:top] 51 + return [{"feature": self.cols[i], "contribution": round(float(c[i]), 3)} for i in idx] 52 + 53 + 54 + _cache: LearnedScorer | None = None 55 + _loaded = False 56 + 57 + 58 + def load() -> LearnedScorer | None: 59 + global _cache, _loaded 60 + if not _loaded: 61 + _loaded = True 62 + if MODEL_PATH.exists(): 63 + import lightgbm as lgb 64 + 65 + d = pickle.loads(MODEL_PATH.read_bytes()) 66 + _cache = LearnedScorer(lgb.Booster(model_str=d["booster"]), d["iso"], d["cols"]) 67 + return _cache 68 + 69 + 70 + def _matrix(con, er): 71 + rows = con.execute( 72 + "SELECT author_did, opened_at, clean_merge FROM pr_labels WHERE clean_merge IS NOT NULL " 73 + "ORDER BY opened_at" 74 + ).fetchall() 75 + fcols = [c[0] for c in con.execute("DESCRIBE features").fetchall()] 76 + feats = {r[0]: dict(zip(fcols, r)) for r in con.execute("SELECT * FROM features").fetchall()} 77 + X = np.array([_vec(did, feats.get(did, {}), er) for did, _, _ in rows], dtype=float) 78 + y = np.array([int(lbl) for _, _, lbl in rows], dtype=int) 79 + return X, y 80 + 81 + 82 + def _reliability(p, y, bins=5): 83 + """Reliability curve (PRD 6.8): predicted vs empirical P(clean) per bin.""" 84 + edges = np.linspace(0, 1, bins + 1) 85 + out = [] 86 + for lo, hi in zip(edges, edges[1:]): 87 + m = (p >= lo) & (p <= hi if hi == 1 else p < hi) 88 + if m.any(): 89 + out.append({"bin": f"{lo:.1f}-{hi:.1f}", "predicted": round(float(p[m].mean()), 3), 90 + "actual": round(float(y[m].mean()), 3), "n": int(m.sum())}) 91 + return out 92 + 93 + 94 + def train(split: float = 0.7) -> dict: 95 + import lightgbm as lgb 96 + from sklearn.isotonic import IsotonicRegression 97 + 98 + with connection(read_only=True) as con: 99 + er = eigentrust.compute(con) 100 + X, y = _matrix(con, er) 101 + if len(X) < 4 or len(set(y.tolist())) < 2: 102 + raise SystemExit(f"need >=4 labelled PRs spanning both classes; got {len(X)} rows, " 103 + f"classes={set(y.tolist())}. Seed/ingest more history first.") 104 + 105 + k = max(2, int(len(X) * split)) 106 + Xtr, ytr, Xval, yval = X[:k], y[:k], X[k:], y[k:] 107 + params = dict(objective="binary", num_leaves=15, min_data_in_leaf=1, min_data_in_bin=1, 108 + learning_rate=0.1, verbose=-1, feature_pre_filter=False) 109 + booster = lgb.train(params, lgb.Dataset(Xtr, label=ytr, feature_name=FEATURE_COLS), 110 + num_boost_round=60) 111 + 112 + raw_val = booster.predict(Xval) 113 + iso = None 114 + if len(set(yval.tolist())) > 1: # isotonic needs both classes in the holdout 115 + iso = IsotonicRegression(out_of_bounds="clip", y_min=0.0, y_max=1.0).fit(raw_val, yval) 116 + cal_val = iso.predict(raw_val) if iso is not None else raw_val 117 + 118 + MODEL_DIR.mkdir(parents=True, exist_ok=True) 119 + MODEL_PATH.write_bytes(pickle.dumps( 120 + {"booster": booster.model_to_string(), "iso": iso, "cols": FEATURE_COLS})) 121 + global _loaded, _cache 122 + _loaded, _cache = False, None # force reload of the fresh model 123 + 124 + rel = _reliability(np.asarray(cal_val), yval) 125 + return {"rows": len(X), "train": k, "val": len(Xval), 126 + "calibrated": iso is not None, "reliability": rel, "model": str(MODEL_PATH)} 127 + 128 + 129 + def main() -> None: 130 + r = train() 131 + print(f"[train] {r['rows']} labelled PRs (train={r['train']} / val={r['val']}), " 132 + f"calibrated={r['calibrated']} -> {r['model']}") 133 + print("[train] reliability curve (predicted vs actual P(clean)):") 134 + for b in r["reliability"]: 135 + print(f" {b['bin']} predicted={b['predicted']} actual={b['actual']} n={b['n']}") 136 + 137 + 138 + def demo() -> None: 139 + """Self-check: after training, a trusted DID scores above a sybil (graph + history).""" 140 + from .db import connection as conn, init_db 141 + from .seed import seed as load_seed 142 + 143 + with conn(read_only=False) as con: 144 + init_db(con) # schema + features + pr_labels views 145 + load_seed(con) 146 + train() 147 + s = load() 148 + with conn(read_only=True) as con: 149 + er = eigentrust.compute(con) 150 + fcols = [c[0] for c in con.execute("DESCRIBE features").fetchall()] 151 + feats = {r[0]: dict(zip(fcols, r)) for r in con.execute("SELECT * FROM features").fetchall()} 152 + trusted = s.prob("did:plc:carol", feats.get("did:plc:carol", {}), er) 153 + sybil = s.prob("did:plc:sybil2", feats.get("did:plc:sybil2", {}), er) 154 + print(f"calibrated P(clean): carol={trusted:.3f} sybil2={sybil:.3f}") 155 + assert trusted > sybil, "learned score must rank the trusted DID above the sybil" 156 + print("ok") 157 + 158 + 159 + if __name__ == "__main__": 160 + demo()
+121
src/trust/review.py
··· 1 + """M4 content signal: Claude reviews ONE PR's diff + discussion (PRD 6.6). 2 + 3 + Claude judges content, never identity (constraint 2): no author handle, DID, or 4 + history is ever passed in. Output is forced to the schema via tool use, temp 0. 5 + """ 6 + 7 + from __future__ import annotations 8 + 9 + import json 10 + import os 11 + 12 + from .config import CFG 13 + 14 + # Verbatim from PRD 6.6 — do not paraphrase. 15 + SYSTEM_PROMPT = """\ 16 + You are a code-contribution reviewer for an open-source trust system. You assess ONE 17 + pull request's actual content for quality and safety. You do not decide whether to 18 + merge; you produce a structured risk assessment that a separate policy layer combines 19 + with an identity-trust signal. 20 + 21 + Hard rules: 22 + - Judge only the artifact in front of you: the diff, the PR title and description, and 23 + the discussion. You are given NO information about the author's identity, reputation, 24 + or history, and you must not speculate about it. Identity trust is handled elsewhere. 25 + - Your job is to catch problems a reputation signal cannot see: code that looks correct 26 + but is subtly wrong, plausible-looking machine-generated filler ("slop"), 27 + security-sensitive changes, leaked secrets or credentials, license violations, and 28 + changes whose stated intent does not match what the code does. 29 + - Prefer flagging uncertainty over approving. If the diff is large, unclear, or you 30 + cannot verify correctness, say so and set review_recommended. Never rubber-stamp. 31 + - Be specific. Every flag must point to concrete lines or patterns, not vibes. 32 + - Output ONLY the structured object specified by the tool. No prose outside it.\ 33 + """ 34 + 35 + ASSESSMENT_TOOL = { 36 + "name": "submit_assessment", 37 + "description": "Submit the structured risk assessment for this PR.", 38 + "input_schema": { 39 + "type": "object", 40 + "properties": { 41 + "content_risk": {"type": "number", "description": "0.0 safe/trivial to 1.0 broken/dangerous"}, 42 + "flags": { 43 + "type": "array", 44 + "items": { 45 + "type": "object", 46 + "properties": { 47 + "type": {"type": "string", "enum": [ 48 + "subtle_bug", "slop", "security", "secret_leak", "license", 49 + "intent_mismatch", "untested", "oversized", "other"]}, 50 + "severity": {"type": "string", "enum": ["low", "med", "high"]}, 51 + "location": {"type": "string"}, 52 + "explanation": {"type": "string"}, 53 + }, 54 + "required": ["type", "severity", "location", "explanation"], 55 + "additionalProperties": False, 56 + }, 57 + }, 58 + "summary": {"type": "string"}, 59 + "review_recommended": {"type": "boolean"}, 60 + }, 61 + "required": ["content_risk", "flags", "summary", "review_recommended"], 62 + "additionalProperties": False, 63 + }, 64 + } 65 + 66 + # Models that reject the temperature param (Opus 4.7+/Fable). Sonnet 4.6 accepts it. 67 + _NO_TEMPERATURE = ("opus-4-7", "opus-4-8", "fable") 68 + 69 + 70 + def _client(): 71 + if not os.environ.get(CFG.review.api_key_env): 72 + return None 73 + import anthropic 74 + 75 + return anthropic.Anthropic() 76 + 77 + 78 + def review_pr(diff: str, title: str = "", description: str = "", discussion: str = "", 79 + machine_findings: dict | None = None, model: str | None = None) -> dict | None: 80 + """Return the 6.6 schema object, or None if no API key is configured.""" 81 + client = _client() 82 + if client is None: 83 + return None 84 + model = model or CFG.review.model 85 + 86 + parts = [f"PR title: {title}", f"PR description: {description}", 87 + f"Discussion:\n{discussion}", f"Diff:\n{diff[:CFG.review.max_diff_chars]}"] 88 + if machine_findings: # 6.12 structured evidence, no identity 89 + parts.append("Automated scan findings (advisory evidence):\n" 90 + + json.dumps(machine_findings, indent=2)) 91 + user = "\n\n".join(parts) 92 + 93 + kwargs = dict( 94 + model=model, max_tokens=1500, system=SYSTEM_PROMPT, 95 + tools=[ASSESSMENT_TOOL], tool_choice={"type": "tool", "name": "submit_assessment"}, 96 + messages=[{"role": "user", "content": user}], 97 + ) 98 + if not any(m in model for m in _NO_TEMPERATURE): 99 + kwargs["temperature"] = 0 100 + 101 + resp = client.messages.create(**kwargs) 102 + for block in resp.content: 103 + if block.type == "tool_use" and block.name == "submit_assessment": 104 + return block.input 105 + return None 106 + 107 + 108 + def demo() -> None: 109 + """Self-check: schema shape is parseable; live call only if a key is set.""" 110 + out = review_pr("def add(a,b):\n return a-b # says add, does subtract", 111 + title="Add helper", description="adds two numbers") 112 + if out is None: 113 + print("no ANTHROPIC_API_KEY -> content signal skipped (gate treats as None). ok") 114 + return 115 + assert 0.0 <= out["content_risk"] <= 1.0 116 + assert isinstance(out["flags"], list) and "summary" in out 117 + print(f"content_risk={out['content_risk']} flags={len(out['flags'])} :: {out['summary']}") 118 + 119 + 120 + if __name__ == "__main__": 121 + demo()
+6
src/trust/score.py
··· 1 + """Scoring worker entry point. The logic lives in fusion (gate + score_pr).""" 2 + 3 + from .fusion import main 4 + 5 + if __name__ == "__main__": 6 + main()
+123
src/trust/seed.py
··· 1 + """Demo data so the full pipeline runs without a live Jetstream or external drive. 2 + 3 + NOT real Tangled data — a synthetic vouch graph (trusted core + sybil cluster) 4 + plus labelled PRs, enough to demo M3 (EigenTrust triage) and M4 (Claude + gate). 5 + Run real ingest (`python -m trust.ingest`) to replace this with live data. 6 + """ 7 + 8 + from __future__ import annotations 9 + 10 + import datetime as dt 11 + 12 + from .db import connection, ensure_schema 13 + 14 + # did -> handle. The maintainer is the EigenTrust seed. 15 + PEOPLE = { 16 + "did:plc:maintainer": "lewis.tangled.sh", 17 + "did:plc:alice": "alice.dev", 18 + "did:plc:bob": "bob.codes", 19 + "did:plc:carol": "carol.sh", 20 + "did:plc:newcomer": "newcomer.xyz", # legit but unvouched (cold start) 21 + "did:plc:sybil1": "throwaway1", 22 + "did:plc:sybil2": "throwaway2", 23 + "did:plc:sybil3": "throwaway3", 24 + } 25 + # voucher -> subject (positive vouches). Sybils only vouch for each other. 26 + VOUCHES = [ 27 + ("did:plc:maintainer", "did:plc:alice"), 28 + ("did:plc:maintainer", "did:plc:bob"), 29 + ("did:plc:alice", "did:plc:carol"), 30 + ("did:plc:bob", "did:plc:carol"), 31 + ("did:plc:sybil1", "did:plc:sybil2"), 32 + ("did:plc:sybil2", "did:plc:sybil3"), 33 + ("did:plc:sybil3", "did:plc:sybil1"), 34 + ("did:plc:sybil1", "did:plc:sybil3"), 35 + ] 36 + 37 + _CLEAN_DIFF = """--- a/util.py 38 + +++ b/util.py 39 + @@ 40 + -def clamp(x, lo, hi): 41 + - return x 42 + +def clamp(x, lo, hi): 43 + + return max(lo, min(x, hi)) 44 + """ 45 + _BUGGY_DIFF = """--- a/auth.py 46 + +++ b/auth.py 47 + @@ 48 + -def verify(token, secret): 49 + - return hmac.compare_digest(sign(token), secret) 50 + +def verify(token, secret): 51 + + return sign(token) == secret # timing-unsafe; intent says 'verify' but weakens it 52 + """ 53 + 54 + 55 + def seed(con) -> None: 56 + con.execute("DELETE FROM vouches; DELETE FROM contributors; DELETE FROM seeds; " 57 + "DELETE FROM pull_requests; DELETE FROM pr_followups; DELETE FROM scores") 58 + now = dt.datetime.now(dt.timezone.utc) 59 + for did, handle in PEOPLE.items(): 60 + age = 400 if "sybil" not in did and did != "did:plc:newcomer" else 3 61 + con.execute( 62 + "INSERT INTO contributors (did, handle, did_created_at) VALUES (?,?,?)", 63 + [did, handle, now - dt.timedelta(days=age)], 64 + ) 65 + con.execute("INSERT INTO seeds VALUES ('did:plc:maintainer')") 66 + for v, s in VOUCHES: 67 + con.execute( 68 + "INSERT INTO vouches (voucher_did, subject_did, polarity, created_at) VALUES (?,?,1,?)", 69 + [v, s, now - dt.timedelta(days=30)], 70 + ) 71 + 72 + def add_pr(pr_id, did, merged, ci, reverted, diff, is_open=False, age=40, repo="tangled/core", 73 + add=20, dele=5, files=2): 74 + # Historical PRs staggered in time so the M5 train/val split is by-time, not a tie. 75 + opened = now - dt.timedelta(days=1 if is_open else age) 76 + closed_unmerged = (not merged) and (not is_open) 77 + con.execute( 78 + "INSERT INTO pull_requests (pr_id, author_did, repo, target, opened_at, ci_status, " 79 + "merged, closed_unmerged, additions, deletions, files_touched, diff_text, discussion_len) " 80 + "VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)", 81 + [pr_id, did, repo, "main", opened, ci, merged, closed_unmerged, 82 + add, dele, files, diff, 120], 83 + ) 84 + con.execute("INSERT INTO pr_followups (pr_id, reverted) VALUES (?,?)", [pr_id, reverted]) 85 + 86 + for i in range(8): 87 + add_pr(f"alice/{i}", "did:plc:alice", True, "passed", False, _CLEAN_DIFF, age=90 - i * 3) 88 + for i in range(5): 89 + add_pr(f"bob/{i}", "did:plc:bob", True, "passed", i == 0, _CLEAN_DIFF, age=85 - i * 3) # one revert 90 + for i in range(6): 91 + add_pr(f"carol/{i}", "did:plc:carol", True, "passed", False, _CLEAN_DIFF, age=80 - i * 3) 92 + for i in range(3): 93 + add_pr(f"sybil1/{i}", "did:plc:sybil1", i == 0, "failed", False, _BUGGY_DIFF, age=70 - i * 3) 94 + 95 + # Two open PRs for the live demo: one clean from a trusted DID, one buggy from a sybil. 96 + add_pr("live/trusted-clean", "did:plc:carol", False, "passed", False, _CLEAN_DIFF, is_open=True) 97 + add_pr("live/sybil-buggy", "did:plc:sybil2", False, "passed", False, _BUGGY_DIFF, is_open=True) 98 + 99 + # 6.13 repo tiering: a sensitive/dual-use repo gates fast-lane on a jurisdiction attestation. 100 + con.execute("INSERT INTO repo_tiers VALUES ('tangled/secure-enclave', 'sensitive') " 101 + "ON CONFLICT DO NOTHING") # seed() is re-run across tests; keep it idempotent 102 + con.execute("INSERT INTO attestations VALUES (?,?,?,?) ON CONFLICT DO NOTHING", 103 + ["did:plc:carol", "FI", "signed_record", now - dt.timedelta(days=10)]) 104 + # carol is attested -> her clean PR can fast-lane even on the sensitive repo. 105 + add_pr("live/sensitive-attested", "did:plc:carol", False, "passed", False, _CLEAN_DIFF, 106 + is_open=True, repo="tangled/secure-enclave") 107 + # alice is highly trusted but NOT attested -> forced to needs_human on the sensitive repo. 108 + add_pr("live/sensitive-blocked", "did:plc:alice", False, "passed", False, _CLEAN_DIFF, 109 + is_open=True, repo="tangled/secure-enclave") 110 + 111 + 112 + def main() -> None: 113 + ensure_schema() # retries past the cross-process lock if other panes hold it 114 + with connection(read_only=False) as con: 115 + seed(con) 116 + n = con.execute("SELECT count(*) FROM contributors").fetchone()[0] 117 + e = con.execute("SELECT count(*) FROM vouches").fetchone()[0] 118 + p = con.execute("SELECT count(*) FROM pull_requests").fetchone()[0] 119 + print(f"[seed] {n} contributors, {e} vouches, {p} PRs (DEMO DATA)") 120 + 121 + 122 + if __name__ == "__main__": 123 + main()
+50
src/trust/static/dashboard.html
··· 1 + <!doctype html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"> 5 + <title>Dashboard · Tangled trust</title> 6 + <style> 7 + body { font:14px/1.5 system-ui,sans-serif; margin:0; background:#f6f8fa; color:#1f2328; } 8 + header { padding:16px 24px; background:#fff; border-bottom:1px solid #d0d7de; } 9 + h1 { margin:0; font-size:18px; } nav a { margin-right:14px; color:#0969da; text-decoration:none; } 10 + main { max-width:1000px; margin:24px auto; padding:0 24px; display:grid; gap:16px; grid-template-columns:1fr 1fr; } 11 + .card { background:#fff; border:1px solid #d0d7de; border-radius:8px; padding:16px; } 12 + .card.wide { grid-column:1/3; } h2 { font-size:13px; text-transform:uppercase; color:#656d76; margin:0 0 12px; } 13 + .big { font-size:32px; font-weight:700; } .sub { color:#656d76; } 14 + .bars { display:flex; align-items:flex-end; gap:4px; height:120px; } 15 + .bar { flex:1; background:#0969da; border-radius:3px 3px 0 0; min-height:2px; } 16 + .bar span { display:block; text-align:center; font-size:10px; color:#656d76; margin-top:4px; } 17 + .kv { display:flex; justify-content:space-between; padding:4px 0; border-bottom:1px solid #eaeef2; } 18 + </style> 19 + </head> 20 + <body> 21 + <header> 22 + <h1>Observability dashboard</h1> 23 + <nav><a href="/">Triage</a><a href="/dashboard">Dashboard</a><a href="/leaderboard.html">Leaderboard</a></nav> 24 + </header> 25 + <main id="main">loading…</main> 26 + <script> 27 + fetch("/metrics").then(r=>r.json()).then(m=>{ 28 + const maxd = Math.max(1,...m.score_distribution); 29 + const bars = m.score_distribution.map((c,i)=> 30 + `<div style="flex:1"><div class="bar" style="height:${100*c/maxd}%" title="${c}"></div> 31 + <span>.${i}</span></div>`).join(""); 32 + const fa = m.false_approval_rate==null ? "n/a" : (m.false_approval_rate*100).toFixed(1)+"%"; 33 + main.innerHTML = ` 34 + <div class="card wide"><h2>Trust-score distribution</h2><div class="bars">${bars}</div></div> 35 + <div class="card"><h2>Fast-lane rate</h2><div class="big">${(m.fast_lane_rate*100).toFixed(0)}%</div> 36 + <div class="sub">${m.decisions.fast_lane} of ${Object.values(m.decisions).reduce((a,b)=>a+b)} contributors</div></div> 37 + <div class="card"><h2>False-approval (backtest)</h2><div class="big">${fa}</div> 38 + <div class="sub">fast-lane-eligible PRs that were not clean merges</div></div> 39 + <div class="card"><h2>Vouch graph</h2> 40 + <div class="kv"><span>contributors</span><b>${m.vouch_graph.contributors}</b></div> 41 + <div class="kv"><span>edges</span><b>${m.vouch_graph.edges}</b></div> 42 + <div class="kv"><span>seed maintainers</span><b>${m.vouch_graph.seeds}</b></div></div> 43 + <div class="card"><h2>Decisions</h2> 44 + <div class="kv"><span>fast-lane</span><b>${m.decisions.fast_lane}</b></div> 45 + <div class="kv"><span>normal queue</span><b>${m.decisions.normal_queue}</b></div> 46 + <div class="kv"><span>needs human</span><b>${m.decisions.needs_human}</b></div></div>`; 47 + }); 48 + </script> 49 + </body> 50 + </html>
+39
src/trust/static/leaderboard.html
··· 1 + <!doctype html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"> 5 + <title>Leaderboard · Tangled trust</title> 6 + <style> 7 + body { font:14px/1.5 system-ui,sans-serif; margin:0; background:#f6f8fa; color:#1f2328; } 8 + header { padding:16px 24px; background:#fff; border-bottom:1px solid #d0d7de; } 9 + h1 { margin:0; font-size:18px; } nav a { margin-right:14px; color:#0969da; text-decoration:none; } 10 + main { max-width:760px; margin:24px auto; padding:0 24px; } 11 + table { width:100%; border-collapse:collapse; background:#fff; border:1px solid #d0d7de; border-radius:8px; overflow:hidden; } 12 + th,td { text-align:left; padding:10px 16px; border-bottom:1px solid #eaeef2; } 13 + th { font-size:12px; text-transform:uppercase; color:#656d76; } 14 + .rank { color:#656d76; width:40px; } .prob { font-variant-numeric:tabular-nums; font-weight:600; } 15 + .pill { padding:2px 8px; border-radius:999px; color:#fff; font-size:11px; } 16 + .fast_lane{background:#1a7f37} .normal_queue{background:#9a6700} .needs_human{background:#cf222e} 17 + </style> 18 + </head> 19 + <body> 20 + <header> 21 + <h1>Leaderboard</h1> 22 + <nav><a href="/">Triage</a><a href="/dashboard">Dashboard</a><a href="/leaderboard.html">Leaderboard</a></nav> 23 + </header> 24 + <main> 25 + <table><thead><tr><th class="rank">#</th><th>Contributor</th><th>Trust</th><th>Decision</th></tr></thead> 26 + <tbody id="rows"></tbody></table> 27 + </main> 28 + <script> 29 + fetch("/leaderboard").then(r=>r.json()).then(list=>{ 30 + rows.innerHTML = list.map((r,i)=>`<tr> 31 + <td class="rank">${i+1}</td> 32 + <td>${r.handle||r.did}</td> 33 + <td class="prob">${(r.calibrated_prob*100).toFixed(0)}%</td> 34 + <td><span class="pill ${r.decision}">${r.decision.replace("_"," ")}</span></td> 35 + </tr>`).join(""); 36 + }); 37 + </script> 38 + </body> 39 + </html>
+70
src/trust/static/triage.html
··· 1 + <!doctype html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"> 5 + <title>Triage queue · Tangled trust</title> 6 + <style> 7 + :root { --green:#1a7f37; --amber:#9a6700; --red:#cf222e; --bg:#f6f8fa; --card:#fff; --line:#d0d7de; } 8 + body { font:14px/1.5 system-ui,sans-serif; margin:0; background:var(--bg); color:#1f2328; } 9 + header { padding:16px 24px; background:var(--card); border-bottom:1px solid var(--line); } 10 + h1 { margin:0; font-size:18px; } nav a { margin-right:14px; color:#0969da; text-decoration:none; } 11 + main { max-width:1000px; margin:24px auto; padding:0 24px; } 12 + .strip { display:flex; gap:12px; margin-bottom:20px; } 13 + .metric { flex:1; background:var(--card); border:1px solid var(--line); border-radius:8px; padding:12px 16px; } 14 + .metric b { display:block; font-size:24px; } 15 + .group h2 { font-size:13px; text-transform:uppercase; letter-spacing:.04em; color:#656d76; margin:20px 0 8px; } 16 + .row { background:var(--card); border:1px solid var(--line); border-radius:8px; padding:12px 16px; margin-bottom:8px; cursor:pointer; } 17 + .row .top { display:flex; align-items:center; gap:12px; } 18 + .pill { padding:2px 10px; border-radius:999px; color:#fff; font-weight:600; font-size:12px; } 19 + .fast_lane{background:var(--green)} .normal_queue{background:var(--amber)} .needs_human{background:var(--red)} 20 + .handle { font-weight:600; } .repo { color:#656d76; } .reason { color:#424a53; margin-left:auto; } 21 + .detail { display:none; margin-top:10px; padding-top:10px; border-top:1px solid var(--line); font-size:13px; } 22 + .row.open .detail { display:block; } .flag { color:var(--red); } code { background:#eaeef2; padding:1px 4px; border-radius:4px; } 23 + </style> 24 + </head> 25 + <body> 26 + <header> 27 + <h1>Triage queue</h1> 28 + <nav><a href="/">Triage</a><a href="/dashboard">Dashboard</a><a href="/leaderboard.html">Leaderboard</a></nav> 29 + </header> 30 + <main> 31 + <div class="strip" id="strip"></div> 32 + <div id="groups"></div> 33 + </main> 34 + <script> 35 + const ORDER = ["needs_human","normal_queue","fast_lane"]; 36 + const LABEL = {needs_human:"Needs review",normal_queue:"Normal queue",fast_lane:"Fast-lane"}; 37 + fetch("/triage").then(r=>r.json()).then(rows=>{ 38 + const counts={fast_lane:0,normal_queue:0,needs_human:0}; 39 + rows.forEach(r=>counts[r.decision]++); 40 + strip.innerHTML = ` 41 + <div class="metric"><b>${rows.length}</b>open PRs</div> 42 + <div class="metric"><b>${counts.fast_lane}</b>fast-lane</div> 43 + <div class="metric"><b>${counts.normal_queue}</b>normal</div> 44 + <div class="metric"><b>${counts.needs_human}</b>needs review</div>`; 45 + groups.innerHTML = ORDER.map(d=>{ 46 + const rs = rows.filter(r=>r.decision===d); 47 + if(!rs.length) return ""; 48 + return `<div class="group"><h2>${LABEL[d]} (${rs.length})</h2>${rs.map(card).join("")}</div>`; 49 + }).join(""); 50 + document.querySelectorAll(".row").forEach(el=>el.onclick=()=>el.classList.toggle("open")); 51 + }); 52 + function card(r){ 53 + const e=r.explanation||{}; 54 + const path=(e.trust_path||[]).join(" → ")||"no path from seed"; 55 + const factors=(e.top_factors||[]).join(" · "); 56 + const flags=(e.flags||[]).map(f=>`<div class="flag">⚑ [${f.severity}] ${f.type} @ ${f.location}: ${f.explanation}</div>`).join(""); 57 + return `<div class="row"><div class="top"> 58 + <span class="pill ${r.decision}">${(r.calibrated_prob*100).toFixed(0)}%</span> 59 + <span class="handle">${r.handle||r.did}</span> 60 + <span class="repo">${r.repo} · ${r.pr_id}</span> 61 + <span class="reason">${factors||path}</span> 62 + </div><div class="detail"> 63 + <div><b>Structural:</b> ${path}${factors?" · "+factors:""}</div> 64 + ${e.content_summary?`<div><b>Claude:</b> ${e.content_summary}</div>`:"<div><i>No content review ran (cost-gated).</i></div>"} 65 + ${flags} 66 + </div></div>`; 67 + } 68 + </script> 69 + </body> 70 + </html>
+34
tests/test_smoke.py
··· 1 + """Smoke checks: run the module self-checks under pytest (PRD names pytest).""" 2 + 3 + import pytest 4 + 5 + from trust import eigentrust, fusion, review 6 + 7 + 8 + def test_eigentrust_starves_sybils(): 9 + eigentrust.demo() # asserts trusted chain > sybil cluster + correct path 10 + 11 + 12 + def test_gate_never_lifts_untrusted(): 13 + fusion.demo() # asserts constraint 2: clean content can't fast-lane low trust 14 + 15 + 16 + def test_review_schema_shape(): 17 + review.demo() # parses schema, or no-ops cleanly without an API key 18 + 19 + 20 + def test_learned_ranks_trusted_above_sybil(): 21 + pytest.importorskip("lightgbm") # M5 is the .[learned] extra; skip if not installed 22 + from trust import learned 23 + learned.demo() # trains, asserts calibrated P(clean): trusted > sybil 24 + 25 + 26 + def test_gnn_trains_and_is_stable(): 27 + pytest.importorskip("torch_geometric") # M6 is the .[gnn] extra; skip if not installed 28 + from trust import gnn 29 + gnn.demo() # trains GraphSAGE, asserts finite outputs + a written verdict 30 + 31 + 32 + def test_atproto_record_shape(): 33 + from trust import atproto 34 + atproto.demo() # builds a valid sh.tangled.trust.score record (no network)
+848
uv.lock
··· 1 + version = 1 2 + revision = 3 3 + requires-python = ">=3.11" 4 + resolution-markers = [ 5 + "python_full_version >= '3.14' and platform_machine == 'x86_64' and sys_platform == 'darwin'", 6 + "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 'x86_64' and sys_platform == 'darwin'", 7 + "python_full_version >= '3.14' and sys_platform == 'win32'", 8 + "python_full_version >= '3.14' and sys_platform == 'emscripten'", 9 + "(python_full_version >= '3.14' and platform_machine != 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')", 10 + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", 11 + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", 12 + "(python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine != 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')", 13 + "python_full_version < '3.12' and platform_machine == 'x86_64' and sys_platform == 'darwin'", 14 + "python_full_version < '3.12' and sys_platform == 'win32'", 15 + "python_full_version < '3.12' and sys_platform == 'emscripten'", 16 + "(python_full_version < '3.12' and platform_machine != 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')", 17 + ] 18 + 19 + [[package]] 20 + name = "annotated-doc" 21 + version = "0.0.4" 22 + source = { registry = "https://pypi.org/simple" } 23 + sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } 24 + wheels = [ 25 + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, 26 + ] 27 + 28 + [[package]] 29 + name = "annotated-types" 30 + version = "0.7.0" 31 + source = { registry = "https://pypi.org/simple" } 32 + 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" } 33 + wheels = [ 34 + { 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" }, 35 + ] 36 + 37 + [[package]] 38 + name = "anthropic" 39 + version = "0.112.0" 40 + source = { registry = "https://pypi.org/simple" } 41 + dependencies = [ 42 + { name = "anyio" }, 43 + { name = "distro" }, 44 + { name = "docstring-parser" }, 45 + { name = "httpx" }, 46 + { name = "jiter" }, 47 + { name = "pydantic" }, 48 + { name = "sniffio" }, 49 + { name = "typing-extensions" }, 50 + ] 51 + sdist = { url = "https://files.pythonhosted.org/packages/7b/dd/808c144d4a883fcfd12fe0d7689b1d86bbbea6666c1cc957ad19f1017c22/anthropic-0.112.0.tar.gz", hash = "sha256:e180cd91aa5b9b32e4007fe69892ab128d8a86b9f90825103b1903fbc977d0af", size = 937460, upload-time = "2026-06-24T18:45:56.844Z" } 52 + wheels = [ 53 + { url = "https://files.pythonhosted.org/packages/a9/26/ea71185027956325be1903d4fcaf7461d5ef40ca8f0e64f992e24ea9db0e/anthropic-0.112.0-py3-none-any.whl", hash = "sha256:bcc6268612c716dbb77133dd60fc41d26016d1b81dee9a52314d210193638751", size = 931954, upload-time = "2026-06-24T18:45:58.205Z" }, 54 + ] 55 + 56 + [[package]] 57 + name = "anyio" 58 + version = "4.14.1" 59 + source = { registry = "https://pypi.org/simple" } 60 + dependencies = [ 61 + { name = "idna" }, 62 + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, 63 + ] 64 + sdist = { url = "https://files.pythonhosted.org/packages/3b/72/5562aabb8dd7181e8e860622a38bea08d17842b99ecd4c91f84ac95251b0/anyio-4.14.1.tar.gz", hash = "sha256:8d648a3544c1a700e3ff78615cd679e4c5c3f149904287e73687b2596963629e", size = 254831, upload-time = "2026-06-24T20:56:06.017Z" } 65 + wheels = [ 66 + { url = "https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl", hash = "sha256:4e5533c5b8ff0a24f5d7a176cbe6877129cd183893f66b537f8f227d10527d72", size = 124875, upload-time = "2026-06-24T20:56:04.413Z" }, 67 + ] 68 + 69 + [[package]] 70 + name = "certifi" 71 + version = "2026.6.17" 72 + source = { registry = "https://pypi.org/simple" } 73 + sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" } 74 + wheels = [ 75 + { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, 76 + ] 77 + 78 + [[package]] 79 + name = "click" 80 + version = "8.4.2" 81 + source = { registry = "https://pypi.org/simple" } 82 + dependencies = [ 83 + { name = "colorama", marker = "sys_platform == 'win32'" }, 84 + ] 85 + sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } 86 + wheels = [ 87 + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, 88 + ] 89 + 90 + [[package]] 91 + name = "colorama" 92 + version = "0.4.6" 93 + source = { registry = "https://pypi.org/simple" } 94 + 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" } 95 + wheels = [ 96 + { 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" }, 97 + ] 98 + 99 + [[package]] 100 + name = "distro" 101 + version = "1.9.0" 102 + source = { registry = "https://pypi.org/simple" } 103 + 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" } 104 + wheels = [ 105 + { 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" }, 106 + ] 107 + 108 + [[package]] 109 + name = "docstring-parser" 110 + version = "0.18.0" 111 + source = { registry = "https://pypi.org/simple" } 112 + sdist = { url = "https://files.pythonhosted.org/packages/e0/4d/f332313098c1de1b2d2ff91cf2674415cc7cddab2ca1b01ae29774bd5fdf/docstring_parser-0.18.0.tar.gz", hash = "sha256:292510982205c12b1248696f44959db3cdd1740237a968ea1e2e7a900eeb2015", size = 29341, upload-time = "2026-04-14T04:09:19.867Z" } 113 + wheels = [ 114 + { url = "https://files.pythonhosted.org/packages/a7/5f/ed01f9a3cdffbd5a008556fc7b2a08ddb1cc6ace7effa7340604b1d16699/docstring_parser-0.18.0-py3-none-any.whl", hash = "sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b", size = 22484, upload-time = "2026-04-14T04:09:18.638Z" }, 115 + ] 116 + 117 + [[package]] 118 + name = "duckdb" 119 + version = "1.5.4" 120 + source = { registry = "https://pypi.org/simple" } 121 + sdist = { url = "https://files.pythonhosted.org/packages/31/29/9bad86ed7aa812d8c822a27c15c355b6d5423b991feeec86ed18027b6daa/duckdb-1.5.4.tar.gz", hash = "sha256:f9e32f1cdd106793d79d190186bed9e75289d51e68bd9174e47c04bffedeab6f", size = 18046634, upload-time = "2026-06-17T10:48:52.499Z" } 122 + wheels = [ 123 + { url = "https://files.pythonhosted.org/packages/56/bb/7921dabd50daef3969f14cd8a5a14c24eee337db7914a462f2defa8add92/duckdb-1.5.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3fb41d9cfccb7e44511eeeed263ae98143ca63bdb1ef84631ba637c314efa1b5", size = 32663142, upload-time = "2026-06-17T10:47:45.471Z" }, 124 + { url = "https://files.pythonhosted.org/packages/a6/83/2137765eaba6a9aefe3bb9848ddaac7407fe3ba19b292f98b31f3b7ab27f/duckdb-1.5.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8ba7b666bc9c78d6a930ee9f469024149f0c6a23fb7d2c3418aad6774339bec0", size = 17321485, upload-time = "2026-06-17T10:47:47.778Z" }, 125 + { url = "https://files.pythonhosted.org/packages/0e/b2/a02c1ee43fd7e8cf1fc2e3d377f3dcf9d4a3e58a4549557516e1866ff0da/duckdb-1.5.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9d9e6817fcbc09d2605a2c8c041ac7824d738d917c35a4d427e977647e1d7944", size = 15470820, upload-time = "2026-06-17T10:47:49.977Z" }, 126 + { url = "https://files.pythonhosted.org/packages/d8/48/a243d30223b024bc6057abe472b002cff01e97efefb4d2f0b0dcc5aece0b/duckdb-1.5.4-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02dd9f9a6124069213f13e3a474c208028c472fe1acdae12b38761f954fe4fc6", size = 19341849, upload-time = "2026-06-17T10:47:52.205Z" }, 127 + { url = "https://files.pythonhosted.org/packages/08/ff/a5d48de4771e2403a8ef26a20dc7457b1c8f7e398ff0caf9c0cad8805f89/duckdb-1.5.4-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccc7f2694d02b4763fee61021d45e12f7bc5743993686563957df0cef799fbae", size = 21451698, upload-time = "2026-06-17T10:47:54.653Z" }, 128 + { url = "https://files.pythonhosted.org/packages/79/b8/8244d7741b4afae67775cf0cb0d4eb9e923a83110907e4801e17fa078480/duckdb-1.5.4-cp311-cp311-win_amd64.whl", hash = "sha256:4c430e788d99b50854209bf2833ba36a45df75e57f86efb477046cd408bbd077", size = 13132643, upload-time = "2026-06-17T10:47:56.75Z" }, 129 + { url = "https://files.pythonhosted.org/packages/e4/57/8169822a37f6dd7d561c567f9007e3cf04bf97bccb619afe90db849c0962/duckdb-1.5.4-cp311-cp311-win_arm64.whl", hash = "sha256:e2dc8340cfb6006025a798c50f40126d6e945a1d2487be94667bb4166556ce7b", size = 13986386, upload-time = "2026-06-17T10:47:59.345Z" }, 130 + { url = "https://files.pythonhosted.org/packages/c8/f2/e2f4b477ae3a3b40e8b5f429832e48edb62ed9da99807cc4902e157e5646/duckdb-1.5.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:291a9e7502551170af989ff63139a7a49e99d68edbc5ef5017ac27541fe54c65", size = 32708876, upload-time = "2026-06-17T10:48:01.527Z" }, 131 + { url = "https://files.pythonhosted.org/packages/2e/2b/b698d82a5e1e30b6a05748d72045f672994c6b22f4f0f8423523608b991f/duckdb-1.5.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:83e8c089bbb756ca4471d8b05943b80a106058697cf00615e70423106bb783bc", size = 17346125, upload-time = "2026-06-17T10:48:04.035Z" }, 132 + { url = "https://files.pythonhosted.org/packages/71/75/37e13f39268eaf34864453b3a039c4a1ff0b088d3eae45a4289b41c98c1b/duckdb-1.5.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ff96d2a342b200e1ec6f1f19986c77f4ac16a49b6112f71c5b763989203a9d60", size = 15488133, upload-time = "2026-06-17T10:48:06.312Z" }, 133 + { url = "https://files.pythonhosted.org/packages/cc/59/2d082af578f689231798245b54562c61416e49049b0bda81a06c56a4b53e/duckdb-1.5.4-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f935ef210ab00bc94bb1e3052697adaa36bb0ce7bdfeda8b0f34e2ff1643870", size = 19367895, upload-time = "2026-06-17T10:48:08.59Z" }, 134 + { url = "https://files.pythonhosted.org/packages/52/2b/55c34d2863a76ca824ef8274691e84240b4ff1acde3d231709e82557c240/duckdb-1.5.4-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0cda263d8c20addb8d4f95464787cbe0af1144f7ab7e21db3709fb826ee01725", size = 21486499, upload-time = "2026-06-17T10:48:10.963Z" }, 135 + { url = "https://files.pythonhosted.org/packages/cf/30/ade5952b8182fac86fab43b95ebe3836e66381d0ad64eb1e54bd8207c988/duckdb-1.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:266c7c909558ce7377f57d082cee408aadebdd9111be017558ca54e44a031037", size = 13147934, upload-time = "2026-06-17T10:48:13.061Z" }, 136 + { url = "https://files.pythonhosted.org/packages/f5/00/278f0f70e25b9911afe2fd227b9460f2e6d76177f0dcc03f7f1454afefa5/duckdb-1.5.4-cp312-cp312-win_arm64.whl", hash = "sha256:f14e79a006341f29ee5a2692a24dac5114e77533d579c57ec39124adf0135033", size = 13965235, upload-time = "2026-06-17T10:48:15.782Z" }, 137 + { url = "https://files.pythonhosted.org/packages/da/69/3fcb34e523a9bad1f0557a6c7691a71ba66c43a05e5be9ee96a9a841ed65/duckdb-1.5.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:42a612e67d64450b446eb69695290d460713eef46e0f64467ab9dfe96264ee05", size = 32708366, upload-time = "2026-06-17T10:48:18.084Z" }, 138 + { url = "https://files.pythonhosted.org/packages/f5/5f/bff5054c2c1d65decab36aa6296621e51a2a575a9f250db7ab9b83a325d6/duckdb-1.5.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3fb6f07d54ecf4d0d3c5179a2361fdddfafa14de4fc42696de4632479b703421", size = 17345735, upload-time = "2026-06-17T10:48:20.67Z" }, 139 + { url = "https://files.pythonhosted.org/packages/93/12/d1b2b344e9699246aada6f9de5156e708fb476e2780e5bff9b5d95fe11d9/duckdb-1.5.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0f32ad7e0286c1c29ab6c73b29118c86101f8eee46aae54f54d0b50916f542f6", size = 15488568, upload-time = "2026-06-17T10:48:23.038Z" }, 140 + { url = "https://files.pythonhosted.org/packages/c1/d1/ac56c6096e3e95da60b2c5dd5a0f0eb5540a80622e2e4f8faab893ec4e96/duckdb-1.5.4-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:698ec90bd5d5538bd5f6d212a4b61af443d240703cf45f134738535026556ea5", size = 19368184, upload-time = "2026-06-17T10:48:25.601Z" }, 141 + { url = "https://files.pythonhosted.org/packages/1e/0b/2ae4c3e157a19d9b4ac1f09a5dea6f93012334cc2db09f1e0c71eb99693d/duckdb-1.5.4-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136cea7f886b78caf4035485b4b1e766e8b309e999f9e83a966f81ebb8122844", size = 21486523, upload-time = "2026-06-17T10:48:27.817Z" }, 142 + { url = "https://files.pythonhosted.org/packages/64/7b/c3d8d21e0d0db8faa81eeeb3a55b9932f5a0a16466cb968dc713a653d701/duckdb-1.5.4-cp313-cp313-win_amd64.whl", hash = "sha256:bd6777e8ddd74fb603a6d09766bfcff28638189f8aaa61fc0dffd9e9a4baa8e5", size = 13147807, upload-time = "2026-06-17T10:48:30.017Z" }, 143 + { url = "https://files.pythonhosted.org/packages/44/48/ddf8d3740e3d28582944f70d84e720b5dc28c10ec22b668a0e0bd965f2f2/duckdb-1.5.4-cp313-cp313-win_arm64.whl", hash = "sha256:73f4878a3012283024a64a1909e440aac12091ef336f671fc142f7e87449ce0c", size = 13965189, upload-time = "2026-06-17T10:48:32.251Z" }, 144 + { url = "https://files.pythonhosted.org/packages/62/01/67ac4cbc8e552a1e14c029b5c443d828e68f94d5d913c574f577e1db277e/duckdb-1.5.4-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:4647968629d0677bbcc2416c7aeda8685eb84e4ca15a6dbd4f82a66cfc91a532", size = 32714364, upload-time = "2026-06-17T10:48:34.724Z" }, 145 + { url = "https://files.pythonhosted.org/packages/4e/0e/eb44d983fa56b175f971eea251bde284a36d26cbb93fcb68035061f54078/duckdb-1.5.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e8fcef301cf68d3951ea1eb8ac4d76cea0a6f6a08f4c78fe4026fc96d217bebc", size = 17349820, upload-time = "2026-06-17T10:48:37.126Z" }, 146 + { url = "https://files.pythonhosted.org/packages/10/b2/b9dc7624b105d414585b8530451c1162c0b4750c0be9be2e497bb47a8a9b/duckdb-1.5.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f6f39cd0dc6948dee17fd130aec55114f97a8ef6e1db519b9774087962bc5c8c", size = 15498160, upload-time = "2026-06-17T10:48:40.032Z" }, 147 + { url = "https://files.pythonhosted.org/packages/b7/57/61356444f6a8c62dec3c3d129abfc53f428de1d484093d1bb381db441231/duckdb-1.5.4-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:262f068158beb5943f2c618f4e54b46db8306b959f90dce956f90a89f613673d", size = 19374183, upload-time = "2026-06-17T10:48:42.698Z" }, 148 + { url = "https://files.pythonhosted.org/packages/b0/f4/d5d633dd7c5138d8f7c434e6ac2553c831b7fb658494efa8d0bc73df8623/duckdb-1.5.4-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d2307a76d199077b0055b354e90e857479461a0d875437535dd4833172c8b6d", size = 21487202, upload-time = "2026-06-17T10:48:45.407Z" }, 149 + { url = "https://files.pythonhosted.org/packages/c0/26/5be13bbd5c3421dccfc1ad4ca9da4b97c5a3ddd73f66542092f3167ec52c/duckdb-1.5.4-cp314-cp314-win_amd64.whl", hash = "sha256:6dcbb81a1276bc48deb4d562bce4f8895e4fc6348750a096e30052345c6d6552", size = 13666989, upload-time = "2026-06-17T10:48:47.764Z" }, 150 + { url = "https://files.pythonhosted.org/packages/dc/82/4d52f3f9f9703a226b26b80bdae3f6905aeefe5221bf1815fc93ff02ca25/duckdb-1.5.4-cp314-cp314-win_arm64.whl", hash = "sha256:0f8722346024e5d9f02b58bf7b0491a629f97fdc8a04a10e432940f471ee387a", size = 14449863, upload-time = "2026-06-17T10:48:50.18Z" }, 151 + ] 152 + 153 + [[package]] 154 + name = "fastapi" 155 + version = "0.138.0" 156 + source = { registry = "https://pypi.org/simple" } 157 + dependencies = [ 158 + { name = "annotated-doc" }, 159 + { name = "pydantic" }, 160 + { name = "starlette" }, 161 + { name = "typing-extensions" }, 162 + { name = "typing-inspection" }, 163 + ] 164 + sdist = { url = "https://files.pythonhosted.org/packages/5b/58/ff455d9fe47c60abadb34b9e05a304b1f05f5ab8000ac01565156b6f5e43/fastapi-0.138.0.tar.gz", hash = "sha256:d445a4877636ad191e7053e08c9bf98cb921a6756776848400bb773d1740c061", size = 419240, upload-time = "2026-06-20T01:18:05.259Z" } 165 + wheels = [ 166 + { url = "https://files.pythonhosted.org/packages/6c/ff/8496d9847a5fedae775eb49460722d3efaa80487854273e9647ae876218c/fastapi-0.138.0-py3-none-any.whl", hash = "sha256:b6f54fd1bd72c80b0f899f172c61a600f6f7af9b43d4d772a018f35624048cb0", size = 126779, upload-time = "2026-06-20T01:18:03.483Z" }, 167 + ] 168 + 169 + [[package]] 170 + name = "h11" 171 + version = "0.16.0" 172 + source = { registry = "https://pypi.org/simple" } 173 + 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" } 174 + wheels = [ 175 + { 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" }, 176 + ] 177 + 178 + [[package]] 179 + name = "httpcore" 180 + version = "1.0.9" 181 + source = { registry = "https://pypi.org/simple" } 182 + dependencies = [ 183 + { name = "certifi" }, 184 + { name = "h11" }, 185 + ] 186 + 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" } 187 + wheels = [ 188 + { 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" }, 189 + ] 190 + 191 + [[package]] 192 + name = "httpx" 193 + version = "0.28.1" 194 + source = { registry = "https://pypi.org/simple" } 195 + dependencies = [ 196 + { name = "anyio" }, 197 + { name = "certifi" }, 198 + { name = "httpcore" }, 199 + { name = "idna" }, 200 + ] 201 + 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" } 202 + wheels = [ 203 + { 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" }, 204 + ] 205 + 206 + [[package]] 207 + name = "idna" 208 + version = "3.18" 209 + source = { registry = "https://pypi.org/simple" } 210 + sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } 211 + wheels = [ 212 + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, 213 + ] 214 + 215 + [[package]] 216 + name = "jiter" 217 + version = "0.15.0" 218 + source = { registry = "https://pypi.org/simple" } 219 + 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" } 220 + wheels = [ 221 + { 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" }, 222 + { 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" }, 223 + { 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" }, 224 + { 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" }, 225 + { 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" }, 226 + { 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" }, 227 + { 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" }, 228 + { 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" }, 229 + { 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" }, 230 + { 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" }, 231 + { 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" }, 232 + { 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" }, 233 + { 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" }, 234 + { 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" }, 235 + { 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" }, 236 + { 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" }, 237 + { 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" }, 238 + { 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" }, 239 + { 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" }, 240 + { 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" }, 241 + { 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" }, 242 + { 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" }, 243 + { 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" }, 244 + { 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" }, 245 + { 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" }, 246 + { 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" }, 247 + { 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" }, 248 + { 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" }, 249 + { 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" }, 250 + { 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" }, 251 + { 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" }, 252 + { 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" }, 253 + { 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" }, 254 + { 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" }, 255 + { 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" }, 256 + { 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" }, 257 + { 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" }, 258 + { 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" }, 259 + { 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" }, 260 + { 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" }, 261 + { 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" }, 262 + { 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" }, 263 + { 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" }, 264 + { 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" }, 265 + { 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" }, 266 + { 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" }, 267 + { 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" }, 268 + { 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" }, 269 + { 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" }, 270 + { 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" }, 271 + { 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" }, 272 + { 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" }, 273 + { 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" }, 274 + { 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" }, 275 + { 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" }, 276 + { 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" }, 277 + { 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" }, 278 + { 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" }, 279 + { 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" }, 280 + { 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" }, 281 + { 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" }, 282 + { 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" }, 283 + { 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" }, 284 + { 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" }, 285 + { 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" }, 286 + { 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" }, 287 + { 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" }, 288 + { 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" }, 289 + { 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" }, 290 + { 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" }, 291 + { 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" }, 292 + { 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" }, 293 + { 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" }, 294 + { 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" }, 295 + { 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" }, 296 + { 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" }, 297 + { 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" }, 298 + { 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" }, 299 + { 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" }, 300 + { 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" }, 301 + { 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" }, 302 + { 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" }, 303 + ] 304 + 305 + [[package]] 306 + name = "numpy" 307 + version = "2.4.6" 308 + source = { registry = "https://pypi.org/simple" } 309 + resolution-markers = [ 310 + "python_full_version < '3.12' and platform_machine == 'x86_64' and sys_platform == 'darwin'", 311 + "python_full_version < '3.12' and sys_platform == 'win32'", 312 + "python_full_version < '3.12' and sys_platform == 'emscripten'", 313 + "(python_full_version < '3.12' and platform_machine != 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')", 314 + ] 315 + 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" } 316 + wheels = [ 317 + { 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" }, 318 + { 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" }, 319 + { 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" }, 320 + { 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" }, 321 + { 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" }, 322 + { 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" }, 323 + { 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" }, 324 + { 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" }, 325 + { 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" }, 326 + { 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" }, 327 + { 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" }, 328 + { 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" }, 329 + { 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" }, 330 + { 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" }, 331 + { 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" }, 332 + { 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" }, 333 + { 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" }, 334 + { 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" }, 335 + { 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" }, 336 + { 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" }, 337 + { 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" }, 338 + { 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" }, 339 + { 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" }, 340 + { 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" }, 341 + { 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" }, 342 + { 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" }, 343 + { 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" }, 344 + { 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" }, 345 + { 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" }, 346 + { 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" }, 347 + { 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" }, 348 + { 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" }, 349 + { 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" }, 350 + { 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" }, 351 + { 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" }, 352 + { 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" }, 353 + { 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" }, 354 + { 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" }, 355 + { 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" }, 356 + { 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" }, 357 + { 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" }, 358 + { 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" }, 359 + { 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" }, 360 + { 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" }, 361 + { 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" }, 362 + { 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" }, 363 + { 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" }, 364 + { 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" }, 365 + { 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" }, 366 + { 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" }, 367 + { 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" }, 368 + { 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" }, 369 + { 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" }, 370 + { 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" }, 371 + { 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" }, 372 + { 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" }, 373 + { 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" }, 374 + { 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" }, 375 + { 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" }, 376 + { 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" }, 377 + { 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" }, 378 + { 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" }, 379 + { 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" }, 380 + { 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" }, 381 + { 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" }, 382 + { 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" }, 383 + { 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" }, 384 + { 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" }, 385 + { 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" }, 386 + { 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" }, 387 + { 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" }, 388 + ] 389 + 390 + [[package]] 391 + name = "numpy" 392 + version = "2.5.0" 393 + source = { registry = "https://pypi.org/simple" } 394 + resolution-markers = [ 395 + "python_full_version >= '3.14' and platform_machine == 'x86_64' and sys_platform == 'darwin'", 396 + "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 'x86_64' and sys_platform == 'darwin'", 397 + "python_full_version >= '3.14' and sys_platform == 'win32'", 398 + "python_full_version >= '3.14' and sys_platform == 'emscripten'", 399 + "(python_full_version >= '3.14' and platform_machine != 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')", 400 + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", 401 + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", 402 + "(python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine != 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')", 403 + ] 404 + sdist = { url = "https://files.pythonhosted.org/packages/e7/05/3d27272d30698dc0ecb7fdfaa41ad70303b444f81722bb99bce1d818638a/numpy-2.5.0.tar.gz", hash = "sha256:5a129578019311b6e56bdd714250f19b518f7dceeeb8d1af5490f4942d3f891c", size = 20652461, upload-time = "2026-06-21T20:57:51.95Z" } 405 + wheels = [ 406 + { url = "https://files.pythonhosted.org/packages/fa/0a/11486d02add7b1384dff7374d124b1cfbb0ee864dcc9f6a2c0380638cf84/numpy-2.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:489780423903667933b4ed6197b6ec3b75ea5dd17d1d8f0f38d798feb6921561", size = 16789987, upload-time = "2026-06-21T20:56:16.657Z" }, 407 + { url = "https://files.pythonhosted.org/packages/55/b2/285f48640a181947b4587a3766d21ec1eaa7fea833d4b49957e09da467a2/numpy-2.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ece55976ced6bca95a03ae2839e2e5ccffe8eb6a3e7022415645eb154a81e4e6", size = 11760322, upload-time = "2026-06-21T20:56:19.813Z" }, 408 + { url = "https://files.pythonhosted.org/packages/dd/67/b032db1eb03ca30d16eda3b0c22aaa615338b9263c2fd559d0f29451aca4/numpy-2.5.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:c83b664b0e6eee9594fa920cf0639d8af796606d3fad6cc70180c87e4b97c7be", size = 5319605, upload-time = "2026-06-21T20:56:22.173Z" }, 409 + { url = "https://files.pythonhosted.org/packages/b9/83/03fc7300c7c6b6c84c487b1dc80d322817b95fbd1f4dd57a85e23b7198de/numpy-2.5.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:bf80333980bf37f523341ddd72c783f39d6829ec7736b9eb99086388a2d52cc2", size = 6653628, upload-time = "2026-06-21T20:56:23.914Z" }, 410 + { url = "https://files.pythonhosted.org/packages/82/49/2ec21730bc63ccfda829323f7040a8ed4715b3852ce658689cf74ee96a8c/numpy-2.5.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1a4874217b36d5ac8fc876f52e39df56f8182c88463e9e2dceabf7ca8b7efb8", size = 15153691, upload-time = "2026-06-21T20:56:25.631Z" }, 411 + { url = "https://files.pythonhosted.org/packages/bb/6b/f4a3d0637692c49da8ef99d72d52526f92e0a8d6ac4f0ca9f31441b9d9ea/numpy-2.5.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aaa760137137e8d3c920d27927748215b56014f92667dc9b6c27dfc61249255a", size = 16660066, upload-time = "2026-06-21T20:56:28.009Z" }, 412 + { url = "https://files.pythonhosted.org/packages/3a/2f/c354ec86d1f3f5c19649463b0d39652e160736e5b0a4cd18dff0576715c4/numpy-2.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7174ce8265fc7f7417d171c9ea8fe905220748893ea67a2a7abe726ec331c4b0", size = 16514638, upload-time = "2026-06-21T20:56:30.26Z" }, 413 + { url = "https://files.pythonhosted.org/packages/06/34/43efdcb319988648580f93c11f1ae82cf7e2faa74925e98e454ae3aa95f8/numpy-2.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b8c3daaf99de52415d20b42f8e8155c78642cb04207d02f9d317a0dcf1b3fb54", size = 18419647, upload-time = "2026-06-21T20:56:32.41Z" }, 414 + { url = "https://files.pythonhosted.org/packages/71/e2/f5d1676b1d7fb682eb5e9a1641e7ebd2414b3216c370661d1029778908b4/numpy-2.5.0-cp312-cp312-win32.whl", hash = "sha256:6206db0af545d73d068add6d992279145f158428d1da6cc49adc4b630c5d6ee5", size = 6056688, upload-time = "2026-06-21T20:56:34.657Z" }, 415 + { url = "https://files.pythonhosted.org/packages/8f/7c/48f115d1c58a34032facebcd51fdf2d02df2c51d4a46a81dd1197bb2ea6b/numpy-2.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:6f2d6873e2940c860a309d21e25b1e69af6aaffdd80aa056b04c16380db1c4f2", size = 12419237, upload-time = "2026-06-21T20:56:36.24Z" }, 416 + { url = "https://files.pythonhosted.org/packages/86/26/2e0882f4044d1b1a1b63e875151fb2393389032022a8b7f5657a7996d3b2/numpy-2.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:a55e1eb2bca2cfd17a16b213c99dfc8502d47b0d494224d2122277d0400935ca", size = 10339912, upload-time = "2026-06-21T20:56:38.733Z" }, 417 + { url = "https://files.pythonhosted.org/packages/8a/33/07675aaad7f26ea013d5e884d9a0d784b79c6bd7566c333f5a52fa3c610b/numpy-2.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:520e6b8be0a4b65840ac8090d4f51cef4bed66e2b0894d5a520f099adc24a9b2", size = 16784890, upload-time = "2026-06-21T20:56:40.799Z" }, 418 + { url = "https://files.pythonhosted.org/packages/85/4b/953118a730ee3b35e28645e0eb4cf9beec5bdbb954e1ac2f5fcefba6bbc3/numpy-2.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:146b81cdd3967fdb6beca8ba25f00c58741d8f3cbd797f55af0fbe0bfec3469c", size = 11754584, upload-time = "2026-06-21T20:56:43.094Z" }, 419 + { url = "https://files.pythonhosted.org/packages/44/9b/56dd530c367c74ae17411027cea4135ca57e1e0583bf5594cee18bd83217/numpy-2.5.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:126b88d95e8ff9b00c9e717aa540469f21d6180162f84c0caec51b16215d49cd", size = 5313904, upload-time = "2026-06-21T20:56:45.503Z" }, 420 + { url = "https://files.pythonhosted.org/packages/ce/b0/bcd672edad27ecca7da1f7bb0ce72cd1706a4f2d79ae94990afc97c13e1c/numpy-2.5.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d4313cef1594c5ce46c31b6e54e918338f63f16ee9322304e8c9114d6d81c8bd", size = 6648504, upload-time = "2026-06-21T20:56:47.567Z" }, 421 + { url = "https://files.pythonhosted.org/packages/80/9e/15cdfcbd30a1544a46c9e487a00df331c4672450216538705a9e51fa6710/numpy-2.5.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:750fb097caf26fa878746d9d119f6f9da12dedcbff1eea966c3e3447647c4a9e", size = 15150086, upload-time = "2026-06-21T20:56:49.352Z" }, 422 + { url = "https://files.pythonhosted.org/packages/32/4e/8d7656ccaab3e81e97258b8a9bc5f0c8502513a92fb4ceb0a2cbfebc17bf/numpy-2.5.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3893adc2dc7c0412ba76777db55a049215d99c9aa3113003be8f49f4f1290ab9", size = 16647250, upload-time = "2026-06-21T20:56:51.542Z" }, 423 + { url = "https://files.pythonhosted.org/packages/3c/81/97060281b602ed07f21b12f4ec409eac1f75a2f91fbc829ed8b2becf3ad4/numpy-2.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:835e454dd99b238cdc5a3f63bce2371296f5ebc53ca1e0f8e6ddbb6d92a29aab", size = 16512864, upload-time = "2026-06-21T20:56:55.401Z" }, 424 + { url = "https://files.pythonhosted.org/packages/33/ab/4496208146911f8d8ddb54f68a972aafa6c8d44babcb2ea03b0e5cc87c9d/numpy-2.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6f9836778081a0a3c02a6a21493f3e9f5b311f8d2541934f31f05583dc999ea4", size = 18408407, upload-time = "2026-06-21T20:56:57.75Z" }, 425 + { url = "https://files.pythonhosted.org/packages/d4/9f/a4df67c181e4ee8b467aa3332dc2db10fd5c515136831302f3ca48bc0a01/numpy-2.5.0-cp313-cp313-win32.whl", hash = "sha256:0b525be4744b60bb0557ac872d53ef07d085b5f39622bc579c98d3809d05b988", size = 6054431, upload-time = "2026-06-21T20:57:00.016Z" }, 426 + { url = "https://files.pythonhosted.org/packages/30/53/491e1c47c55b62ccc6a63c1c5b8635c73fc2258dddeb9bda27cae4a0ae96/numpy-2.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:44353e2878930039db472b99dc353d749826e4010bd4d2a7f835e94a97a5c748", size = 12414420, upload-time = "2026-06-21T20:57:01.815Z" }, 427 + { url = "https://files.pythonhosted.org/packages/eb/4a/25c2906f541e9d9f4c5769764db732e6627be91a13f4724fa10634d77db4/numpy-2.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:48f54b00711f83a5f796b70c518e8c2b3c5848dda03a54911f23eb68519b9b60", size = 10339533, upload-time = "2026-06-21T20:57:03.961Z" }, 428 + { url = "https://files.pythonhosted.org/packages/86/ad/abc44aaceaf7b17ee1edde2bbb4458da591bc79574cffff50c4bb35f00d1/numpy-2.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f27582c55ba4c750b7c58c8faf021d2cd9324a662b466229db8a417b41368af9", size = 16783807, upload-time = "2026-06-21T20:57:06.253Z" }, 429 + { url = "https://files.pythonhosted.org/packages/5d/39/b72e168daf9c00fb20c9fc996d00437ccecdef3102387775d29d7a62576d/numpy-2.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:28e7137057d551e4a83c4ae414e3451f50568409db7569aacc7f9811ee06a446", size = 11765215, upload-time = "2026-06-21T20:57:08.547Z" }, 430 + { url = "https://files.pythonhosted.org/packages/f7/a0/8400a9c0e3625182347593f5e1f57da9a617a534794805c8df5518154ddc/numpy-2.5.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:e1da54b53e75cd9fcfc23efcc7edab2c6aecf97b6037566d8a0fe804af8ec57c", size = 5324493, upload-time = "2026-06-21T20:57:11.012Z" }, 431 + { url = "https://files.pythonhosted.org/packages/f6/8c/0d104deaa0401c93395a629ec902891618a2eff76d19229139cb5a887bfc/numpy-2.5.0-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:694d8f74e156f7fd01179f1aa8faa2f648ab6ae0f70b6c3fe57a03249aea2303", size = 6645211, upload-time = "2026-06-21T20:57:12.919Z" }, 432 + { url = "https://files.pythonhosted.org/packages/6a/d9/4a4a628c812750363786afc3d33492709a5cd64b215469c16b0f6c7bb811/numpy-2.5.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1a7569a7b53c77716f036bb28cb1c91f166a26ec7d9502cd1e4bdfe502fdec22", size = 15166004, upload-time = "2026-06-21T20:57:14.717Z" }, 433 + { url = "https://files.pythonhosted.org/packages/a0/5e/2a902317d7fc4aa93236e80c932662dadfc459b323d758329e01775125e1/numpy-2.5.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:39a0433bd4086ebd462960cf375e19195bb07b53dc1d87dd5fcf47ad78576f03", size = 16650797, upload-time = "2026-06-21T20:57:16.906Z" }, 434 + { url = "https://files.pythonhosted.org/packages/e9/a0/a0090e6329f4ca5992c07847bb579c5259a19953dc57255bb08793142ffb/numpy-2.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:929f0c79ac38bcbd7154fe631dc907abfeddbcc5027a896bd1f7767323271e7a", size = 16524647, upload-time = "2026-06-21T20:57:19.165Z" }, 435 + { url = "https://files.pythonhosted.org/packages/5e/7d/6caf27734c42b65837e7461ed0dbbd6b6fc835060c9714ec59d673bb383a/numpy-2.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cc4f247a47bbf070bfd70be53ccdcf47b800af563535e7bbe172322197c30e21", size = 18411841, upload-time = "2026-06-21T20:57:21.638Z" }, 436 + { url = "https://files.pythonhosted.org/packages/13/dc/26edadbd812536769a82c2e9e002234e33feb5da43061d47a044f6d309b7/numpy-2.5.0-cp314-cp314-win32.whl", hash = "sha256:5dc71423499fab3f46f7a7201155ade1669ea101f2f429d332df9e72f8161731", size = 6106361, upload-time = "2026-06-21T20:57:23.844Z" }, 437 + { url = "https://files.pythonhosted.org/packages/f2/9e/4dd1459282229a72d92dece2ae9138e5cac94a72263a7ceb48f37434c925/numpy-2.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:ebb81d9d5443e0309d6c54894c3fbed74ad7da0714352a67b6d773cd189eae73", size = 12551749, upload-time = "2026-06-21T20:57:25.945Z" }, 438 + { url = "https://files.pythonhosted.org/packages/05/a7/6bc6384c080b86c7f6c85c5bc5b540b24f4f679cd144791d99574e90d462/numpy-2.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:3b94d0d0deceebfad3e67ae5c0e5eb87371e8f7a0581cd04a779928c2450cf1e", size = 10617072, upload-time = "2026-06-21T20:57:28.175Z" }, 439 + { url = "https://files.pythonhosted.org/packages/86/6b/4a2b71d66ada5608ae02b63f150dfad520f6940721cb7f029ad270befc0e/numpy-2.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:22f3d43e362d650bc39db1f17851302874a148ca95ba6981c1dfb5fa6862f35b", size = 11881067, upload-time = "2026-06-21T20:57:30.104Z" }, 440 + { url = "https://files.pythonhosted.org/packages/dc/b2/d365eb40a20efb49d67e9feb90494ed8511282ee1f5fa16006675c65397d/numpy-2.5.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:243563efb4cd7528a264567e9fd206c87826457322521d06206a00bfa316c927", size = 5440290, upload-time = "2026-06-21T20:57:32.193Z" }, 441 + { url = "https://files.pythonhosted.org/packages/fa/5e/e9c03188de5f9b767e46a8fe988bcfd3efad066a4a3fda8b9cb11a93f895/numpy-2.5.0-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:84881d825ca75249b189bbee875fcfe3238aa5c479e6100893cda566e8e86826", size = 6748371, upload-time = "2026-06-21T20:57:33.933Z" }, 442 + { url = "https://files.pythonhosted.org/packages/fd/1d/68c186a38a5027bae2c4ddd5ea681fdaf8b4d30fb7301def6d8ad270390f/numpy-2.5.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cda12aa4779d42b8771180aba759c96f527d43446d8f380ab59e2b35e8489efd", size = 15214643, upload-time = "2026-06-21T20:57:35.677Z" }, 443 + { url = "https://files.pythonhosted.org/packages/8c/67/73f67b7c7e20635baae9c4c3ead4ae7326a005900297a6110971abd62eb5/numpy-2.5.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c0121101093d2bd74981b10f8837d78e794a8ff57834eb27179f49e1ba11ac6", size = 16690128, upload-time = "2026-06-21T20:57:38.159Z" }, 444 + { url = "https://files.pythonhosted.org/packages/eb/05/d4c1fb0c46d02a27d6b2b8b319a78c90937acec8631c1641874670b31e6f/numpy-2.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d371c92cfa09da00022f501ab67fafaea813d752eb30ac44336d45b1e5b0268a", size = 16577902, upload-time = "2026-06-21T20:57:40.447Z" }, 445 + { url = "https://files.pythonhosted.org/packages/9e/1d/771c797d50fa26e4888989cccf1d50ee51f530d4e455ad2692dcb64fa711/numpy-2.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9990713e9c38154c6861e7547f1e3fc7a87e75ff09bab24ef1cc81d81c2835e9", size = 18452814, upload-time = "2026-06-21T20:57:42.875Z" }, 446 + { url = "https://files.pythonhosted.org/packages/e8/46/52fc0d2a68d7643f0f149eeea5a5d8ea2a3507056ac8afa83c9212606e8b/numpy-2.5.0-cp314-cp314t-win32.whl", hash = "sha256:edadfbd4794b1086c0d822f81863e8a68fc129d132fd0bb9e31e955d7fbbbdb7", size = 6253168, upload-time = "2026-06-21T20:57:45.101Z" }, 447 + { url = "https://files.pythonhosted.org/packages/2a/be/6c8d1118b5f13b2881dc095d5b345de19c6638b8959c17409b6eff84c8aa/numpy-2.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f7e5fa4382967ae6548bd2f174219afb908e294b0d5f625af01166edd5f7d9aa", size = 12736286, upload-time = "2026-06-21T20:57:46.935Z" }, 448 + { url = "https://files.pythonhosted.org/packages/fd/6a/d3a169aaf8536cf228d56a09e04bcb713a2fe4410d4e2105b9419b5a9c89/numpy-2.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:016623417bb330d719d579daf2d6b9a01ddc52e41a9ed61a47f39fde46dcd865", size = 10686451, upload-time = "2026-06-21T20:57:49.313Z" }, 449 + ] 450 + 451 + [[package]] 452 + name = "pydantic" 453 + version = "2.13.4" 454 + source = { registry = "https://pypi.org/simple" } 455 + dependencies = [ 456 + { name = "annotated-types" }, 457 + { name = "pydantic-core" }, 458 + { name = "typing-extensions" }, 459 + { name = "typing-inspection" }, 460 + ] 461 + 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" } 462 + wheels = [ 463 + { 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" }, 464 + ] 465 + 466 + [[package]] 467 + name = "pydantic-core" 468 + version = "2.46.4" 469 + source = { registry = "https://pypi.org/simple" } 470 + dependencies = [ 471 + { name = "typing-extensions" }, 472 + ] 473 + 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" } 474 + wheels = [ 475 + { 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" }, 476 + { 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" }, 477 + { 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" }, 478 + { 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" }, 479 + { 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" }, 480 + { 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" }, 481 + { 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" }, 482 + { 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" }, 483 + { 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" }, 484 + { 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" }, 485 + { 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" }, 486 + { 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" }, 487 + { 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" }, 488 + { 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" }, 489 + { 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" }, 490 + { 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" }, 491 + { 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" }, 492 + { 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" }, 493 + { 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" }, 494 + { 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" }, 495 + { 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" }, 496 + { 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" }, 497 + { 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" }, 498 + { 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" }, 499 + { 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" }, 500 + { 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" }, 501 + { 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" }, 502 + { 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" }, 503 + { 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" }, 504 + { 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" }, 505 + { 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" }, 506 + { 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" }, 507 + { 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" }, 508 + { 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" }, 509 + { 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" }, 510 + { 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" }, 511 + { 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" }, 512 + { 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" }, 513 + { 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" }, 514 + { 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" }, 515 + { 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" }, 516 + { 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" }, 517 + { 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" }, 518 + { 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" }, 519 + { 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" }, 520 + { 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" }, 521 + { 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" }, 522 + { 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" }, 523 + { 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" }, 524 + { 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" }, 525 + { 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" }, 526 + { 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" }, 527 + { 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" }, 528 + { 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" }, 529 + { 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" }, 530 + { 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" }, 531 + { 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" }, 532 + { 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" }, 533 + { 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" }, 534 + { 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" }, 535 + { 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" }, 536 + { 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" }, 537 + { 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" }, 538 + { 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" }, 539 + { 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" }, 540 + { 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" }, 541 + { 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" }, 542 + { 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" }, 543 + { 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" }, 544 + { 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" }, 545 + { 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" }, 546 + { 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" }, 547 + { 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" }, 548 + { 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" }, 549 + { 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" }, 550 + { 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" }, 551 + { 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" }, 552 + { 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" }, 553 + { 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" }, 554 + { 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" }, 555 + { 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" }, 556 + { 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" }, 557 + { 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" }, 558 + { 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" }, 559 + { 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" }, 560 + { 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" }, 561 + { 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" }, 562 + { 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" }, 563 + { 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" }, 564 + { 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" }, 565 + { 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" }, 566 + ] 567 + 568 + [[package]] 569 + name = "scipy" 570 + version = "1.17.1" 571 + source = { registry = "https://pypi.org/simple" } 572 + resolution-markers = [ 573 + "python_full_version < '3.12' and platform_machine == 'x86_64' and sys_platform == 'darwin'", 574 + "python_full_version < '3.12' and sys_platform == 'win32'", 575 + "python_full_version < '3.12' and sys_platform == 'emscripten'", 576 + "(python_full_version < '3.12' and platform_machine != 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')", 577 + ] 578 + dependencies = [ 579 + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, 580 + ] 581 + 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" } 582 + wheels = [ 583 + { 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" }, 584 + { 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" }, 585 + { 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" }, 586 + { 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" }, 587 + { 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" }, 588 + { 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" }, 589 + { 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" }, 590 + { 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" }, 591 + { 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" }, 592 + { 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" }, 593 + { 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" }, 594 + { 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" }, 595 + { 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" }, 596 + { 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" }, 597 + { 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" }, 598 + { 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" }, 599 + { 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" }, 600 + { 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" }, 601 + { 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" }, 602 + { 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" }, 603 + { 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" }, 604 + { 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" }, 605 + { 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" }, 606 + { 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" }, 607 + { 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" }, 608 + { 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" }, 609 + { 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" }, 610 + { 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" }, 611 + { 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" }, 612 + { 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" }, 613 + { 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" }, 614 + { 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" }, 615 + { 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" }, 616 + { 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" }, 617 + { 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" }, 618 + { 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" }, 619 + { 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" }, 620 + { 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" }, 621 + { 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" }, 622 + { 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" }, 623 + { 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" }, 624 + { 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" }, 625 + { 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" }, 626 + { 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" }, 627 + { 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" }, 628 + { 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" }, 629 + { 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" }, 630 + { 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" }, 631 + { 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" }, 632 + { 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" }, 633 + { 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" }, 634 + { 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" }, 635 + { 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" }, 636 + { 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" }, 637 + { 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" }, 638 + { 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" }, 639 + { 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" }, 640 + { 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" }, 641 + { 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" }, 642 + { 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" }, 643 + ] 644 + 645 + [[package]] 646 + name = "scipy" 647 + version = "1.18.0" 648 + source = { registry = "https://pypi.org/simple" } 649 + resolution-markers = [ 650 + "python_full_version >= '3.14' and platform_machine == 'x86_64' and sys_platform == 'darwin'", 651 + "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 'x86_64' and sys_platform == 'darwin'", 652 + "python_full_version >= '3.14' and sys_platform == 'win32'", 653 + "python_full_version >= '3.14' and sys_platform == 'emscripten'", 654 + "(python_full_version >= '3.14' and platform_machine != 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')", 655 + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", 656 + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", 657 + "(python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine != 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')", 658 + ] 659 + dependencies = [ 660 + { name = "numpy", version = "2.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, 661 + ] 662 + sdist = { url = "https://files.pythonhosted.org/packages/a7/25/c2700dfaf6442b4effaa91af24ebce5dc9d31bb4a69706313aae70d72cd0/scipy-1.18.0.tar.gz", hash = "sha256:67b2ad2ad54c72ca6d04975a9b2df8c3638c34ddd5b28738e94fc2b57929d378", size = 30774447, upload-time = "2026-06-19T15:01:43.456Z" } 663 + wheels = [ 664 + { url = "https://files.pythonhosted.org/packages/6a/19/ca10ead60b0acc80b2b833c2c4a4f2ff753d0f58b811f70d911c7e94a25c/scipy-1.18.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:7bd21faaf5a1a3b2eff922d02db5f191b99a6518db9078a8fb23169f6d22259a", size = 31056519, upload-time = "2026-06-19T14:59:45.203Z" }, 665 + { url = "https://files.pythonhosted.org/packages/96/72/1e6442a00cd2924d361aa1b642ab6373ec35c6fabf311a760be9f76e0f13/scipy-1.18.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:265915e79107de9f946b855e50d7470d5893ec3f54b342e1aa6201cbdcd8bb6b", size = 28681889, upload-time = "2026-06-19T14:59:48.103Z" }, 666 + { url = "https://files.pythonhosted.org/packages/9b/2d/11dd93d21e147a73ba22bd75c0b9208d3a2e0ec76d53170ce7d9029b1015/scipy-1.18.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9ab7b758be6940954a713ee466e2043e9f6e2ed965c1fce5c91039f4be3d90a9", size = 20423580, upload-time = "2026-06-19T14:59:50.665Z" }, 667 + { url = "https://files.pythonhosted.org/packages/9c/01/93552f75e0d2a7dd115a45e59209c51e8d514daff02fc887d2623be06fe1/scipy-1.18.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:97b6cddaaee0a779ef6b5ca83c9604b27cc16b2b8fc22c142652df8793319fb8", size = 23054441, upload-time = "2026-06-19T14:59:53.564Z" }, 668 + { url = "https://files.pythonhosted.org/packages/3c/23/21f5e703643d66f21faa6b4c73195bfcad70c55efcb4f1ab327cd7c4101a/scipy-1.18.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:52a96e21517c7292375c0e27dd796a811f03fcea5fd4d108fdfea8145dcf17ab", size = 33968720, upload-time = "2026-06-19T14:59:56.415Z" }, 669 + { url = "https://files.pythonhosted.org/packages/dd/aa/1b939f6c67ed68635bb538e6752d3dacc02f66535182e939a89581a44e9c/scipy-1.18.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f55797419e16e7f30cf88ffb3113ce0467f00cfe3f70d5c281730b21769bfc2", size = 35287115, upload-time = "2026-06-19T14:59:59.411Z" }, 670 + { url = "https://files.pythonhosted.org/packages/b6/ff/eec46be7e9234208f801062b53e1983085eddebd693f6c9bfb03b459830d/scipy-1.18.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ad033410e2e0672ffdc1042110cef20e1c46f8fd0616cee1d44d8d58fad8fc11", size = 35577989, upload-time = "2026-06-19T15:00:02.235Z" }, 671 + { url = "https://files.pythonhosted.org/packages/84/ca/210d4759c7210bb7d269437421959b39a33434e2776b60c5cb8a763bb30a/scipy-1.18.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4a55985d54c769c872e64b7f4c8a81cc30ef700cc04296abbbf3705439c126de", size = 37421717, upload-time = "2026-06-19T15:00:05.102Z" }, 672 + { url = "https://files.pythonhosted.org/packages/2b/54/9a9edb45345bd6744da5ddfb6628e5d5185920494c6a67ec45b6381004cb/scipy-1.18.0-cp312-cp312-win_amd64.whl", hash = "sha256:71ccc8faa2dd16ac310233203474a8b5cb67f10dedd54a3116d34943f4b19132", size = 36597428, upload-time = "2026-06-19T15:00:08.112Z" }, 673 + { url = "https://files.pythonhosted.org/packages/99/0e/33f32a2a58987e26aec0f7df252cbbad1e90ae77bdbc76f40dd4ed0cf0ea/scipy-1.18.0-cp312-cp312-win_arm64.whl", hash = "sha256:d88363fd9d8fbd3511bd273f1a49efb2a540773ddf92a91d57498ce7dd7f3e76", size = 24351481, upload-time = "2026-06-19T15:00:11.103Z" }, 674 + { url = "https://files.pythonhosted.org/packages/05/52/9c0136c2de7ae0779b7b366447766cec6d9f0702c56bb8ffeb04c8fd3af4/scipy-1.18.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:09143f676d157d9f546d663504ef9c1becb819824f1afc018814176411942446", size = 31036107, upload-time = "2026-06-19T15:00:14.03Z" }, 675 + { url = "https://files.pythonhosted.org/packages/02/73/0291a64843270f4efb86cdcf2ee0f2048631b65ec6b405398b2b4dbf11bf/scipy-1.18.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:5efe260f69417b97ddae455bfb5a95e8359f7f66ad7fa9522a60feb66f169520", size = 28663303, upload-time = "2026-06-19T15:00:16.819Z" }, 676 + { url = "https://files.pythonhosted.org/packages/d3/0f/10ffa0b697a572f4e0d48b92a88895d366422f019f723e7e14a84c050dac/scipy-1.18.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:68363b7eaacd8b5dd426df56d782cc156468ac79a127a1b87ca597d6e2e82197", size = 20404960, upload-time = "2026-06-19T15:00:19.635Z" }, 677 + { url = "https://files.pythonhosted.org/packages/7e/d2/e896cea21ba8edd6c81d4c55b1ffcc717e79698dcbebf9641b4cfb4c6622/scipy-1.18.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:c5557d8be5da8e41353fcd4d21491fdbab83b062fc579e94dc09a7c8ab4f669b", size = 23034074, upload-time = "2026-06-19T15:00:22.107Z" }, 678 + { url = "https://files.pythonhosted.org/packages/ea/b2/e83ea34279a52c03374477c74006256ec78df65fc877baa4617d6de1d202/scipy-1.18.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0d13bca67c096d89fb95ced0d8921807300fce0275643aef9533cc63a0773468", size = 33942038, upload-time = "2026-06-19T15:00:24.964Z" }, 679 + { url = "https://files.pythonhosted.org/packages/f6/af/e8fe5fb136f51e2b01678b92cb4106d10d8cd68ec147ead2e7cb0ac75398/scipy-1.18.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a46f9273dbd0eb1cefba61c9b8648b4dfe3cbc14a080176f9a73e44b8336dc7f", size = 35266390, upload-time = "2026-06-19T15:00:28.059Z" }, 680 + { url = "https://files.pythonhosted.org/packages/3a/49/2c5cbb907b56695fc67517811d1db234dfd83381a84814ec220aded2794d/scipy-1.18.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5aba46108853ddfc77906b6557aac839d2b52e900c1d72a1180adaaab58d265f", size = 35551324, upload-time = "2026-06-19T15:00:31.014Z" }, 681 + { url = "https://files.pythonhosted.org/packages/bb/73/eda39f7a2d306ff0ffc574afd13c0bbb6d10a603d9a413998ee269487a80/scipy-1.18.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b6f758e35f12757b5d95c00bc6de2438e229c2664b7a92e96f205959d9f2dfa4", size = 37404785, upload-time = "2026-06-19T15:00:34.072Z" }, 682 + { url = "https://files.pythonhosted.org/packages/b7/d2/ae881ee28d014f38e0ccbfd974a06a919ba9af34f1f74bf42b5301891d63/scipy-1.18.0-cp313-cp313-win_amd64.whl", hash = "sha256:1afac4a847207c7ff8efd321734a50b06d0280b3b2a2c0fc2f413101747ad7c7", size = 36554943, upload-time = "2026-06-19T15:00:36.903Z" }, 683 + { url = "https://files.pythonhosted.org/packages/70/3a/21154e2d54eb3639c6bf4dbae2e531c68356bfe95990daa30df33b30d556/scipy-1.18.0-cp313-cp313-win_arm64.whl", hash = "sha256:c5dbddf60e58c2312316d097271a8e73d40eaf2eabfa4d95ed7d3695bbf2ce7b", size = 24350911, upload-time = "2026-06-19T15:00:40.062Z" }, 684 + { url = "https://files.pythonhosted.org/packages/78/b5/915a19b3de2f7430062b509653563db1633ddbb6f021b06731521115d4e2/scipy-1.18.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4c256ee70c0d1a8a2ace807e199ccd4e3f57037433842abb3fb36bc17eaa9578", size = 31036253, upload-time = "2026-06-19T15:00:43.216Z" }, 685 + { url = "https://files.pythonhosted.org/packages/d7/88/b72def7262e150d16be13fca37a96481138d624e700340bc3362a7588929/scipy-1.18.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:2ef3abc54a4ffc53765374b0d5728532dfdd2585ed23f6b11c206a1f0b1b9af8", size = 28673758, upload-time = "2026-06-19T15:00:46.663Z" }, 686 + { url = "https://files.pythonhosted.org/packages/91/02/2e636a61a525632c373cf6a9c24442a3ffb79e364d38e98b32042964ac32/scipy-1.18.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:f2a6af57bd9e4a75d70e4117e78a1bbee84f79ae3fbb6d0111005d6ebcc4cb8d", size = 20415514, upload-time = "2026-06-19T15:00:49.399Z" }, 687 + { url = "https://files.pythonhosted.org/packages/c9/b6/2135974442f6aba159d9d39d774a1c8cb19947016725d69fecc685df45bf/scipy-1.18.0-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:3f1ac564d3bf6c03d861d2cd87a1bea0da2887136f7fb1bf519c05a8971452d6", size = 23034398, upload-time = "2026-06-19T15:00:51.941Z" }, 688 + { url = "https://files.pythonhosted.org/packages/f6/e6/ba89ec5abf6ee9257c0d1ec985573f3ae32742c24bc03e016388a40b1b15/scipy-1.18.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40395a5fcd1abee49a5c7aaa98c29db393eedc835138560a588c47ec16156690", size = 33998032, upload-time = "2026-06-19T15:00:54.838Z" }, 689 + { url = "https://files.pythonhosted.org/packages/7f/c4/bc41eb19b0fd0db868f4132920879019318d80cc522ad8f2bca4611af808/scipy-1.18.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ca01e8ae69f1b18e9a58d91afead31be3cef0dd905a10249dac559ee15460a0", size = 35283333, upload-time = "2026-06-19T15:00:58.152Z" }, 690 + { url = "https://files.pythonhosted.org/packages/53/a4/cbdeef6eb3830a8462a9d4ada814de5fc984345cc9ecf17cbec51a036f1e/scipy-1.18.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7a7f3b01647384dbc3a711e8c6778e0aabbe93959249fef5c7393396bcac0867", size = 35610216, upload-time = "2026-06-19T15:01:01.155Z" }, 691 + { url = "https://files.pythonhosted.org/packages/80/4d/b2b82502b65f661d1b789c1665dcdf315d5f12194e06fc0b37946294ebae/scipy-1.18.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6aa94e78ec192a30063a5e72e561c28af769dc311190b24fe91774eff1969709", size = 37418960, upload-time = "2026-06-19T15:01:04.155Z" }, 692 + { url = "https://files.pythonhosted.org/packages/93/3e/902d836831474b0ab5a37d16404f7bc5fafd9efba632890e271ba952635f/scipy-1.18.0-cp314-cp314-win_amd64.whl", hash = "sha256:2d8bbdc6c817f5b4006a54d799d4f5bab6f910193cbb9a1ff310833d4d270f61", size = 37288845, upload-time = "2026-06-19T15:01:07.822Z" }, 693 + { url = "https://files.pythonhosted.org/packages/b6/43/8d73b337a3bdb14daa0314f0434210747c02d79d729ce1777574a817dcf6/scipy-1.18.0-cp314-cp314-win_arm64.whl", hash = "sha256:18e9575f1569b2c54174e6159d32942e03731177f63dce7975f0a0c88d102f5b", size = 24988971, upload-time = "2026-06-19T15:01:11.076Z" }, 694 + { url = "https://files.pythonhosted.org/packages/b4/b4/f11918b0508a2787031a0499a03fbe3546f3bb5ca05d01038c45b278c09a/scipy-1.18.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f351e0dd702687d12a402b867a1b4146a256923e1c38317cbc472f6372b94707", size = 31399325, upload-time = "2026-06-19T15:01:13.723Z" }, 695 + { url = "https://files.pythonhosted.org/packages/7b/d1/1f287b57c0ff0ee5185dff3946d92c8017d39b0e431f0ae79a3ff1859512/scipy-1.18.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:7c7a51b33ce387193c97f228320cf8e87361daa1bba750638677729598b3e677", size = 29092110, upload-time = "2026-06-19T15:01:16.908Z" }, 696 + { url = "https://files.pythonhosted.org/packages/ff/1a/7b74eb6c392fdcb27d414c0e7558a6d0231eb3b6d73571f479bb81ea8794/scipy-1.18.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:84031d7b052a54fae2f8632e0ec802073d385476eb9a63079bce6e23ef9283d4", size = 20833811, upload-time = "2026-06-19T15:01:20.488Z" }, 697 + { url = "https://files.pythonhosted.org/packages/7c/ad/f3941716320a7b9cb4d68734a903b45fe16eff5fb7da7e16f2e619304979/scipy-1.18.0-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:56abf29a7c067dde59be8b9a22d606a4ea1b2f2a4b756d9d903c62818f5dacce", size = 23396644, upload-time = "2026-06-19T15:01:23.364Z" }, 698 + { url = "https://files.pythonhosted.org/packages/22/22/1446b62ffe07f9719b7d9b1b6a4e05a772833ae8f441fe4c22c34c9b250f/scipy-1.18.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ad44305cfa24b1ba5803cbbebf033590ccbac1aa5d612d727b785325ab408b0", size = 34079318, upload-time = "2026-06-19T15:01:26.002Z" }, 699 + { url = "https://files.pythonhosted.org/packages/56/3b/b87da667098bb470fa30c7011b0ba351ee976dd395c78798c66e941665a3/scipy-1.18.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:945c1761b93f38d7f99ae81ae80c63e621471608c7eeead563f6df025585cd58", size = 35324320, upload-time = "2026-06-19T15:01:28.881Z" }, 700 + { url = "https://files.pythonhosted.org/packages/f8/a1/c7932f91909759b0267f75fdea34e91309f96b895757534b76a90b6b4344/scipy-1.18.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1a4441f15d620578772a49e5ab48c0ee1f7a0220e387110283062729136b2553", size = 35699541, upload-time = "2026-06-19T15:01:31.968Z" }, 701 + { url = "https://files.pythonhosted.org/packages/f7/86/5185061a1fcc41d18c5dc2463969b3a3964b31d9ac67b2fb05d4c7ff7670/scipy-1.18.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9aac6192fac56bf2ca534389d24623f07b39ff83317d58287285e7fbd622ff76", size = 37472480, upload-time = "2026-06-19T15:01:35.136Z" }, 702 + { url = "https://files.pythonhosted.org/packages/31/8e/f04c68e39919a010d34f2ee1367fd705b0a25a02f609d755f0bfbc0a15fc/scipy-1.18.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e40baea28ae7f5475c779741e2d90b1247c78531207b49c7030e698ff81cee3f", size = 37365390, upload-time = "2026-06-19T15:01:38.091Z" }, 703 + { url = "https://files.pythonhosted.org/packages/d5/19/969dc072906c84dd0a3b05dcf57ea750936087d7873549e408b35cfc3f97/scipy-1.18.0-cp314-cp314t-win_arm64.whl", hash = "sha256:368e0a705903c466aa5f08eefb39e6b1b6b2d659e7352a31fd9e2438365be0f8", size = 25279661, upload-time = "2026-06-19T15:01:40.817Z" }, 704 + ] 705 + 706 + [[package]] 707 + name = "sniffio" 708 + version = "1.3.1" 709 + source = { registry = "https://pypi.org/simple" } 710 + 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" } 711 + wheels = [ 712 + { 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" }, 713 + ] 714 + 715 + [[package]] 716 + name = "starlette" 717 + version = "1.3.1" 718 + source = { registry = "https://pypi.org/simple" } 719 + dependencies = [ 720 + { name = "anyio" }, 721 + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, 722 + ] 723 + sdist = { url = "https://files.pythonhosted.org/packages/eb/e3/7c1dc7381d9f8ab7d854328ebfa884e62cb3f3d8549ddfd37c7814f42afa/starlette-1.3.1.tar.gz", hash = "sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0", size = 2703240, upload-time = "2026-06-12T09:23:11.602Z" } 724 + wheels = [ 725 + { url = "https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6", size = 73632, upload-time = "2026-06-12T09:23:10.017Z" }, 726 + ] 727 + 728 + [[package]] 729 + name = "tangled-trust" 730 + version = "0.1.0" 731 + source = { editable = "." } 732 + dependencies = [ 733 + { name = "anthropic" }, 734 + { name = "duckdb" }, 735 + { name = "fastapi" }, 736 + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, 737 + { name = "numpy", version = "2.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, 738 + { name = "pydantic" }, 739 + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, 740 + { name = "scipy", version = "1.18.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, 741 + { name = "uvicorn" }, 742 + { name = "websockets" }, 743 + ] 744 + 745 + [package.metadata] 746 + requires-dist = [ 747 + { name = "anthropic", specifier = ">=0.40" }, 748 + { name = "duckdb", specifier = ">=1.1" }, 749 + { name = "fastapi", specifier = ">=0.115" }, 750 + { name = "numpy", specifier = ">=1.26" }, 751 + { name = "pydantic", specifier = ">=2.7" }, 752 + { name = "scipy", specifier = ">=1.11" }, 753 + { name = "uvicorn", specifier = ">=0.30" }, 754 + { name = "websockets", specifier = ">=12" }, 755 + ] 756 + 757 + [[package]] 758 + name = "typing-extensions" 759 + version = "4.15.0" 760 + source = { registry = "https://pypi.org/simple" } 761 + 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" } 762 + wheels = [ 763 + { 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" }, 764 + ] 765 + 766 + [[package]] 767 + name = "typing-inspection" 768 + version = "0.4.2" 769 + source = { registry = "https://pypi.org/simple" } 770 + dependencies = [ 771 + { name = "typing-extensions" }, 772 + ] 773 + 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" } 774 + wheels = [ 775 + { 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" }, 776 + ] 777 + 778 + [[package]] 779 + name = "uvicorn" 780 + version = "0.49.0" 781 + source = { registry = "https://pypi.org/simple" } 782 + dependencies = [ 783 + { name = "click" }, 784 + { name = "h11" }, 785 + ] 786 + sdist = { url = "https://files.pythonhosted.org/packages/c4/1f/fa18009dea8469069cca78a4e877a008ab78f08b064bfc9ab891579077ff/uvicorn-0.49.0.tar.gz", hash = "sha256:ebf4271aa580d9de97f93192d4595176df6e91f9aae919ca73e4fc07df1e66a3", size = 91284, upload-time = "2026-06-03T22:01:30.448Z" } 787 + wheels = [ 788 + { url = "https://files.pythonhosted.org/packages/88/fa/e1388bbcf24ef3274f45c0c1c7b501fd14971037c1b6ee23610553307497/uvicorn-0.49.0-py3-none-any.whl", hash = "sha256:ba3d14c3ee7e41c6c654c46c9eb489d33213cdd30aa1696eab1374337c13f68f", size = 71376, upload-time = "2026-06-03T22:01:29.037Z" }, 789 + ] 790 + 791 + [[package]] 792 + name = "websockets" 793 + version = "16.0" 794 + source = { registry = "https://pypi.org/simple" } 795 + sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } 796 + wheels = [ 797 + { url = "https://files.pythonhosted.org/packages/f2/db/de907251b4ff46ae804ad0409809504153b3f30984daf82a1d84a9875830/websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8", size = 177340, upload-time = "2026-01-10T09:22:34.539Z" }, 798 + { url = "https://files.pythonhosted.org/packages/f3/fa/abe89019d8d8815c8781e90d697dec52523fb8ebe308bf11664e8de1877e/websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad", size = 175022, upload-time = "2026-01-10T09:22:36.332Z" }, 799 + { url = "https://files.pythonhosted.org/packages/58/5d/88ea17ed1ded2079358b40d31d48abe90a73c9e5819dbcde1606e991e2ad/websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d", size = 175319, upload-time = "2026-01-10T09:22:37.602Z" }, 800 + { url = "https://files.pythonhosted.org/packages/d2/ae/0ee92b33087a33632f37a635e11e1d99d429d3d323329675a6022312aac2/websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe", size = 184631, upload-time = "2026-01-10T09:22:38.789Z" }, 801 + { url = "https://files.pythonhosted.org/packages/c8/c5/27178df583b6c5b31b29f526ba2da5e2f864ecc79c99dae630a85d68c304/websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b", size = 185870, upload-time = "2026-01-10T09:22:39.893Z" }, 802 + { url = "https://files.pythonhosted.org/packages/87/05/536652aa84ddc1c018dbb7e2c4cbcd0db884580bf8e95aece7593fde526f/websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5", size = 185361, upload-time = "2026-01-10T09:22:41.016Z" }, 803 + { url = "https://files.pythonhosted.org/packages/6d/e2/d5332c90da12b1e01f06fb1b85c50cfc489783076547415bf9f0a659ec19/websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64", size = 184615, upload-time = "2026-01-10T09:22:42.442Z" }, 804 + { url = "https://files.pythonhosted.org/packages/77/fb/d3f9576691cae9253b51555f841bc6600bf0a983a461c79500ace5a5b364/websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6", size = 178246, upload-time = "2026-01-10T09:22:43.654Z" }, 805 + { url = "https://files.pythonhosted.org/packages/54/67/eaff76b3dbaf18dcddabc3b8c1dba50b483761cccff67793897945b37408/websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac", size = 178684, upload-time = "2026-01-10T09:22:44.941Z" }, 806 + { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, 807 + { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, 808 + { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, 809 + { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, 810 + { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, 811 + { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, 812 + { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, 813 + { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, 814 + { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, 815 + { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, 816 + { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, 817 + { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, 818 + { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, 819 + { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, 820 + { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, 821 + { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, 822 + { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, 823 + { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, 824 + { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, 825 + { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, 826 + { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, 827 + { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, 828 + { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, 829 + { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, 830 + { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, 831 + { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, 832 + { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, 833 + { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, 834 + { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, 835 + { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, 836 + { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, 837 + { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, 838 + { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, 839 + { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, 840 + { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, 841 + { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, 842 + { url = "https://files.pythonhosted.org/packages/72/07/c98a68571dcf256e74f1f816b8cc5eae6eb2d3d5cfa44d37f801619d9166/websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d", size = 174947, upload-time = "2026-01-10T09:23:36.166Z" }, 843 + { url = "https://files.pythonhosted.org/packages/7e/52/93e166a81e0305b33fe416338be92ae863563fe7bce446b0f687b9df5aea/websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03", size = 175260, upload-time = "2026-01-10T09:23:37.409Z" }, 844 + { url = "https://files.pythonhosted.org/packages/56/0c/2dbf513bafd24889d33de2ff0368190a0e69f37bcfa19009ef819fe4d507/websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da", size = 176071, upload-time = "2026-01-10T09:23:39.158Z" }, 845 + { url = "https://files.pythonhosted.org/packages/a5/8f/aea9c71cc92bf9b6cc0f7f70df8f0b420636b6c96ef4feee1e16f80f75dd/websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c", size = 176968, upload-time = "2026-01-10T09:23:41.031Z" }, 846 + { url = "https://files.pythonhosted.org/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", size = 178735, upload-time = "2026-01-10T09:23:42.259Z" }, 847 + { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, 848 + ]