diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml index 5944eed92..1c18ddd8b 100644 --- a/.github/workflows/pr-title.yml +++ b/.github/workflows/pr-title.yml @@ -44,5 +44,6 @@ jobs: plugins skills integrations + benchmarks # Allow breaking changes (needs "!" after type/scope) requireScopeForBreakingChange: true diff --git a/benchmarks/.github/workflows/benchmark-nightly.yml b/benchmarks/.github/workflows/benchmark-nightly.yml new file mode 100644 index 000000000..bcc891fb9 --- /dev/null +++ b/benchmarks/.github/workflows/benchmark-nightly.yml @@ -0,0 +1,46 @@ +name: benchmark-nightly + +on: + schedule: + - cron: "0 8 * * *" + workflow_dispatch: + +jobs: + nightly: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: astral-sh/setup-uv@v4 + + - uses: extractions/setup-just@v2 + + - name: Install dependencies + run: uv sync --group dev --extra judge + + - name: Fetch LoCoMo + run: just bench-fetch-locomo + + - name: Convert LoCoMo + run: just bench-convert-locomo + + - name: Run retrieval benchmark + run: | + uv run bm-bench run retrieval \ + --providers bm-local,bm-cloud,mem0-local \ + --dataset-id locomo \ + --dataset-path benchmarks/datasets/locomo/locomo10.json \ + --corpus-dir benchmarks/generated/locomo/docs \ + --queries-path benchmarks/generated/locomo/queries.json \ + --allow-provider-skip + + - name: Run judge benchmark (best effort) + run: | + LATEST_RUN=$(ls -1t benchmarks/runs | head -n1) + uv run bm-bench run judge --run-dir "benchmarks/runs/${LATEST_RUN}" || true + + - name: Upload benchmark artifacts + uses: actions/upload-artifact@v4 + with: + name: benchmark-nightly-artifacts + path: benchmarks/runs diff --git a/benchmarks/.github/workflows/benchmark-smoke.yml b/benchmarks/.github/workflows/benchmark-smoke.yml new file mode 100644 index 000000000..1cdbe3617 --- /dev/null +++ b/benchmarks/.github/workflows/benchmark-smoke.yml @@ -0,0 +1,32 @@ +name: benchmark-smoke + +on: + pull_request: + workflow_dispatch: + +jobs: + smoke: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: astral-sh/setup-uv@v4 + + - uses: extractions/setup-just@v2 + + - name: Install dependencies + run: uv sync --group dev + + - name: Run smoke benchmark + run: just bench-smoke + + - name: Validate latest artifacts + run: | + LATEST_RUN=$(ls -1t benchmarks/runs | head -n1) + uv run bm-bench validate-artifacts --run-dir "benchmarks/runs/${LATEST_RUN}" + + - name: Upload benchmark artifacts + uses: actions/upload-artifact@v4 + with: + name: benchmark-smoke-artifacts + path: benchmarks/runs diff --git a/benchmarks/.gitignore b/benchmarks/.gitignore new file mode 100644 index 000000000..9e8d62d3c --- /dev/null +++ b/benchmarks/.gitignore @@ -0,0 +1,27 @@ +__pycache__/ +*.pyc +.venv/ +.pytest_cache/ +.ruff_cache/ +.idea/ + +# Generated benchmark outputs +benchmarks/runs/ +benchmarks/results/public/ +benchmarks/generated/ +benchmarks/logs/ + +# Downloaded datasets (source distribution may be restricted) +benchmarks/datasets/locomo/locomo10.json +benchmarks/datasets/locomo/locomo10.provenance.json +.env +benchmarks/bm-home/ +benchmarks/datasets/longmemeval/longmemeval_s.json +benchmarks/datasets/longmemeval/longmemeval_s.provenance.json +benchmarks/datasets/locomo-audit/ +benchmarks/.mem0-qdrant/ +benchmarks/datasets/convomem/ +benchmarks/.bm-homes/ +.supermemory/ +.venvs/ +.scratch/ diff --git a/benchmarks/.python-version b/benchmarks/.python-version new file mode 100644 index 000000000..24ee5b1be --- /dev/null +++ b/benchmarks/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/benchmarks/AGENTS.md b/benchmarks/AGENTS.md new file mode 100644 index 000000000..7f8bf1457 --- /dev/null +++ b/benchmarks/AGENTS.md @@ -0,0 +1,124 @@ +# AGENTS.md - basic-memory-benchmarks Guide + +## Project Overview + +`basic-memory-benchmarks` is a standalone benchmark harness for comparing Basic Memory against other memory systems. + +Primary goals: +- Deterministic retrieval benchmarks +- Optional LLM-as-a-judge benchmarks +- Public, reproducible artifact publication (including provenance metadata) + +This repo is intentionally isolated from `basic-memory` so benchmark dependencies do not pollute the product repo. + +## Build / Test Commands + +- Install: `uv sync --group dev` +- Install judge extras: `uv sync --group dev --extra judge` +- Run tests: `uv run pytest -q` +- Lint: `uv run ruff check .` +- Type check: `uv run pyright` + +Recommended local gate before pushing: +1. `uv run pytest -q` +2. `uv run ruff check .` +3. `uv run pyright` + +## Benchmark Commands + +CLI entrypoint: `uv run bm-bench ...` + +Dataset and conversion: +- `uv run bm-bench datasets fetch --dataset locomo` +- `uv run bm-bench convert locomo --dataset-path benchmarks/datasets/locomo/locomo10.json --output-dir benchmarks/generated/locomo` + +Run retrieval: +- `uv run bm-bench run retrieval --providers bm-local,mem0-local --dataset-id locomo --dataset-path benchmarks/datasets/locomo/locomo10.json --corpus-dir benchmarks/generated/locomo/docs --queries-path benchmarks/generated/locomo/queries.json --output-root benchmarks/runs --allow-provider-skip` + +Run judge (optional): +- `uv run bm-bench run judge --run-dir benchmarks/runs/` + +Validate and publish: +- `uv run bm-bench validate-artifacts --run-dir benchmarks/runs/` +- `uv run bm-bench publish --run-dir benchmarks/runs/ --destination benchmarks/results/public` + +`just` shortcuts: +- `just bench-smoke` +- `just bench-fetch-locomo` +- `just bench-convert-locomo` +- `just bench-run-bm-local` +- `just bench-run-mem0-local` +- `just bench-run-full` +- `just bench-judge RUN_DIR=benchmarks/runs/` +- `just bench-publish RUN_DIR=benchmarks/runs/` + +## Repository Layout + +- `src/basic_memory_benchmarks/cli.py` - CLI surface +- `src/basic_memory_benchmarks/runner.py` - run orchestration +- `src/basic_memory_benchmarks/providers/` - provider adapters (`bm-local`, `bm-cloud`, `mem0-local`, `zep-reference`) +- `src/basic_memory_benchmarks/scoring/` - retrieval + judge scoring +- `src/basic_memory_benchmarks/reporting/` - artifact writers / comparison helpers +- `src/basic_memory_benchmarks/converters/` - dataset conversion logic +- `src/basic_memory_benchmarks/datasets/` - dataset fetch/load helpers +- `benchmarks/datasets/` - source metadata + download helpers +- `benchmarks/generated/` - generated corpus/query outputs +- `benchmarks/runs/` - raw run artifacts +- `benchmarks/results/public/` - published bundles +- `tests/`, `test-int/` - unit and integration tests + +## Benchmark Integrity Rules + +These are non-negotiable for headline comparisons: + +1. Use the same query set and same `top_k` across providers. +2. Do not apply provider-specific query rewriting for headline runs. +3. Keep official categories and adversarial breakout separate. +4. Record provider `SKIPPED(reason)` explicitly; do not silently drop providers. +5. Always capture provenance in `manifest.json`: + - benchmark repo SHA + - BM source + resolved BM SHA + - provider versions + - dataset source + checksum + - runtime metadata + +## Provider Notes + +### Basic Memory (`bm-local`) + +- Interact via external `bm` CLI contract, not internal imports from `basic-memory`. +- Repeated runs against the same corpus path may reuse an existing BM project name. +- The benchmark command is typically invoked through `uv run ...` so `.venv/bin/bm` is used. + +### Mem0 (`mem0-local`) + +- Requires `OPENAI_API_KEY` (or equivalent configured model creds). +- Ingest and search use a stable benchmark `user_id` namespace per run. +- Store source metadata (`source_doc_id`, `source_path`, `conversation_id`, `dataset_id`) for grounding. + +## Environment / Secrets + +- Keep secrets in `.env` (already gitignored). +- Avoid exporting unrelated `BASIC_MEMORY_*` environment variables into benchmark runs unless intended. +- Prefer setting only required credentials for run reproducibility. + +## Dataset Policy + +- If redistribution is allowed: publish snapshot + checksum. +- If restricted: publish canonical source link + downloader + checksum verification. +- Always publish conversion code and run artifacts. + +## Coding Guidelines + +- Python 3.12+ style with type hints. +- Keep diffs focused and minimal. +- Fail fast; do not silently swallow benchmark-critical failures. +- Use `apply_patch` for targeted edits when practical. +- Add tests for behavior changes in adapters, scoring, or artifact schemas. + +## Git / Collaboration + +- Use non-interactive git commands. +- Sign commits: `git commit -s`. +- Do not commit `.env`, generated runs, or local editor state. +- If changing benchmark behavior, include a brief note in PR/commit describing fairness or reproducibility impact. diff --git a/benchmarks/README.md b/benchmarks/README.md new file mode 100644 index 000000000..5a9ba75c4 --- /dev/null +++ b/benchmarks/README.md @@ -0,0 +1,275 @@ +# basic-memory-benchmarks + +Standalone, reproducible benchmark suite for comparing Basic Memory against competitor memory systems. + +## Goals + +- Deterministic retrieval benchmarks (Recall@5/10, MRR, Precision@5, content-hit, latency) +- Optional LLM-as-judge scoring (Pydantic Evals) +- Public artifacts with provenance and reproducibility metadata +- Clean dependency isolation from the core `basic-memory` repository + +## Current v1 Scope + +- Providers: + - `bm-local` (warm `bm mcp` stdio session) + - `bm-cloud` (optional, credential-gated) + - `mem0-local` + - `zep-reference` (reference-only in v1) +- Datasets: + - LoCoMo (primary) + - LongMemEval scaffold (placeholder) + - Built-in synthetic smoke corpus + +## Installation + +```bash +uv sync --group dev +``` + +Optional judge dependencies: + +```bash +uv sync --group dev --extra judge +``` + +## Quickstart + +### 1) Fetch LoCoMo dataset + +```bash +uv run bm-bench datasets fetch --dataset locomo +``` + +### 2) Convert LoCoMo into benchmark corpus + +```bash +uv run bm-bench convert locomo +``` + +### 3) Run retrieval benchmark + +```bash +uv run bm-bench run retrieval \ + --providers bm-local,mem0-local \ + --corpus-dir benchmarks/generated/locomo/docs \ + --queries-path benchmarks/generated/locomo/queries.json +``` + +### 4) End-to-end QA scoring + +Generates an answer per query from each provider's retrieved context, then +grades it against the expected answer with an LLM judge. This is the stage that +produces benchmark-comparable accuracy numbers; retrieval metrics alone measure +only the search layer. + +```bash +uv run bm-bench run qa --run-dir benchmarks/runs/ \ + --answerer claude:claude-haiku-4-5 \ + --judge claude:claude-sonnet-4-6 +``` + +Runner specs select the transport: + +- `claude:` — Claude Code CLI in print mode. Bills the operator's Claude + subscription plan; no API key needed. Requires `claude` on PATH. +- `openai-compat:@` — any OpenAI-compatible endpoint (Ollama, + LM Studio, vLLM, OpenAI). Set `OPENAI_API_KEY` if the endpoint requires auth. + +The same answerer and judge are used for every provider in the run, so +cross-provider comparisons hold the model constant. Answer and judge prompts +are fixed in `scoring/qa.py`; the answerer is instructed to abstain ("I don't +know") when the retrieved memories don't contain the answer, and abstention is +graded correct only when the gold answer marks the question unanswerable +(LoCoMo adversarial cases). + +### 5) Optional retrieval-context judge (legacy) + +Scores whether the *retrieved context* contains the expected answer, without +answer generation: + +```bash +uv run bm-bench run judge --run-dir benchmarks/runs/ +``` + +### 6) Publish run artifacts + +```bash +uv run bm-bench publish --run-dir benchmarks/runs/ +``` + +## LoCoMo corrected answer key + +The April 2026 Penfield Labs audit ([locomo-audit](https://github.com/dial481/locomo-audit)) +found 156 answer-key errors in LoCoMo's 1,540 usable questions — hallucinated +facts, temporal arithmetic mistakes, attribution errors, and wrong evidence +citations. Published LoCoMo numbers should score against the corrected key and +say so. + +```bash +uv run bm-bench datasets fetch --dataset locomo-audit # pinned audit revision +uv run bm-bench convert locomo \ + --audit-corrections benchmarks/datasets/locomo-audit/corrections.json \ + --output-dir benchmarks/generated/locomo-corrected +``` + +Corrected queries replace both the expected answer (for QA scoring) and the +evidence citations (for retrieval ground truth), and carry +`audit_corrected: true` + the audit's error type in metadata. Every correction +is cross-checked against the dataset's question text at conversion time, so +audit/dataset drift fails loudly. The adversarial category remains excluded +from headline metrics per the LoCoMo protocol. + +## LongMemEval-S + +LongMemEval (Wu et al., ICLR 2025) gives each of its 500 questions an +independent haystack of ~50 chat sessions, so it runs in **grouped mode**: the +converter writes one corpus per question under `groups//docs`, +and the runner ingests + queries each group in isolation (fresh provider +instance, group-suffixed run id namespacing the BM project / mem0 user). + +```bash +just bench-prepare-longmemeval # fetch (~278MB) + convert all 500 +just bench-convert-longmemeval-dev # or: 25-question dev slice +just bench-run-longmemeval-dev # grouped retrieval, bm-local +``` + +Then score answers with the QA stage as usual (`run qa --run-dir ...`). The +question's ask-date is carried in query metadata and appended to the question +for both the answerer and the judge — temporal-reasoning questions are +unanswerable without it. + +Anti-leakage: the raw dataset marks evidence sessions via an `answer_` +session-id prefix and per-turn `has_answer` flags. The converter remaps all +session ids to neutral positional ids (`-s012`) and drops turn flags, so +ingested corpora carry no evidence markers. + +## ConvoMem (sampled) + +ConvoMem (Salesforce, Apache-2.0) ships ~75K QA pairs as pre-mixed test cases +— each a self-contained haystack of conversations plus questions — which map +1:1 onto grouped mode. The full dataset is multi-GB, so fetching is selective: +batch files are indexed with cheap HTTP Range tail-probes and only files +matching the requested context sizes are downloaded. The probe index +(`index.json`) records every file including the ones not downloaded, so the +selection is auditable. + +```bash +uv run bm-bench datasets fetch --dataset convomem --context-sizes 10,30 +uv run bm-bench convert convomem --sample-per-stratum 25 --seed 42 +``` + +Sampling is stratified by (category, contextSize) with a fixed seed; +`sampling.json` records the seed, per-stratum population, and sample counts — +a published number states exactly which slice of ConvoMem it covers. Note +`--sample-per-stratum` counts *cases* (haystacks); larger-context cases carry +multiple questions each, all sharing one ingested group corpus. + +Anti-leakage: raw conversations carry `containsEvidence`/`model_name` fields; +rendered docs include neither and conversation ids are remapped to neutral +positional ids. + +## Basic Memory source policy + +By default this project tracks Basic Memory from `main`. + +Each run manifest stores: +- BM source (`github main` or local path override) +- resolved BM commit SHA + +Local override: + +```bash +uv run bm-bench run retrieval \ + --bm-local-path /path/to/basic-memory +``` + +## Mem0 local requirements + +`mem0-local` needs a model backend, picked in priority order: + +**1. Local OpenAI-compatible endpoint (zero API spend, e.g. Ollama):** + +```bash +export MEM0_OPENAI_COMPAT_BASE_URL=http://localhost:11434/v1 +# optional overrides (defaults shown): +# MEM0_LLM_MODEL=qwen2.5:3b MEM0_EMBED_MODEL=nomic-embed-text MEM0_EMBED_DIMS=768 +# MEM0_OPENAI_COMPAT_API_KEY=local MEM0_QDRANT_PATH=benchmarks/.mem0-qdrant +``` + +LLM and embeddings both route to the endpoint; the qdrant store is created +per-run with matching dimensions under `MEM0_QDRANT_PATH`. + +**2. OpenAI (mem0's defaults):** `export OPENAI_API_KEY=...` + +With neither set, provider status is recorded as `SKIPPED(reason)`. + +`MEM0_INFER=true` enables mem0's LLM fact-extraction at ingest (its +recommended/headline mode); default is `false` (raw add), which matches the +existing baselines. The active backend, models, and infer mode are recorded in +the run manifest's provider metadata. + +## supermemory local requirements + +`supermemory-local` targets a running [supermemory-server](https://github.com/supermemoryai/supermemory) +(self-hosted binary; pin v0.0.2). The provider does not manage the server — +start it yourself and export: + +```bash +export SUPERMEMORY_API_KEY=sm_... # printed on the server's first boot +# optional: +# SUPERMEMORY_BASE_URL=http://localhost:6767 +# SUPERMEMORY_INGEST_TIMEOUT_S=900 +# SUPERMEMORY_SERVER_VERSION=0.0.2 # recorded in the run manifest +``` + +Server-side notes for fair runs: start from a **fresh** `SUPERMEMORY_DATA_DIR` +per benchmark (upstream issue #1103: upgraded stores return empty searches), +and configure its LLM via `OPENAI_BASE_URL`/`OPENAI_MODEL`. Ingestion is async +(queued → ... → done|failed); the provider polls every document to a terminal +state before searching and fails the run loudly on any failed document rather +than silently scoring partial ingestion. Scoping is one container tag per run +id; cleanup bulk-deletes the container. + +Without `SUPERMEMORY_API_KEY` or with the server unreachable, provider status +is recorded as `SKIPPED(reason)`. + +## BM indexing readiness + +`bm-local` verifies index readiness before querying. + +- If the installed `bm` supports `bm status --json`, readiness is polled from that output. +- If `--json` is not available in the installed `bm`, the benchmark proceeds after reindex. + +## Run Artifacts + +Per run (`benchmarks/runs//`): + +- `manifest.json` +- `provider-status.json` +- `per-query-retrieval.jsonl` +- `retrieval-summary.json` +- `per-query-qa.jsonl` (optional) +- `qa-summary.json` (optional) +- `per-query-judge.jsonl` (optional) +- `judge-summary.json` (optional) +- `summary.md` + +## Just commands + +```bash +just bench-smoke +just bench-fetch-locomo +just bench-convert-locomo +just bench-run-bm-local +just bench-run-mem0-local +just bench-run-full +just bench-judge +just bench-publish RUN_DIR=benchmarks/runs/ +``` + +## Notes on dataset publication + +Dataset publication follows licensing constraints: +- If redistribution is permitted: snapshot + checksum may be published. +- If not: canonical source links + downloader + checksum verification are published. diff --git a/benchmarks/benchmarks/datasets/README.md b/benchmarks/benchmarks/datasets/README.md new file mode 100644 index 000000000..7d6d7a006 --- /dev/null +++ b/benchmarks/benchmarks/datasets/README.md @@ -0,0 +1,13 @@ +# Dataset Policy + +This repository publishes benchmark provenance in the open. + +- If redistribution is allowed, snapshots may be stored in-repo with checksums. +- If redistribution is restricted, we publish canonical source links, download scripts, + and checksums so anyone can reproduce runs. + +Always published: +- conversion scripts +- query manifests +- run artifacts +- provenance metadata diff --git a/benchmarks/benchmarks/datasets/locomo/README.md b/benchmarks/benchmarks/datasets/locomo/README.md new file mode 100644 index 000000000..7b3f2646b --- /dev/null +++ b/benchmarks/benchmarks/datasets/locomo/README.md @@ -0,0 +1,7 @@ +# LoCoMo Dataset + +Primary v1 dataset for benchmark runs. + +- Canonical source URL is configured in `src/basic_memory_benchmarks/datasets/locomo.py`. +- Download via `bm-bench datasets fetch --dataset locomo`. +- Provenance checksum is written alongside the downloaded file. diff --git a/benchmarks/benchmarks/datasets/locomo/download.sh b/benchmarks/benchmarks/datasets/locomo/download.sh new file mode 100755 index 000000000..4c0dae1ce --- /dev/null +++ b/benchmarks/benchmarks/datasets/locomo/download.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail + +uv run bm-bench datasets fetch --dataset locomo --output benchmarks/datasets/locomo/locomo10.json diff --git a/benchmarks/benchmarks/datasets/locomo/source.json b/benchmarks/benchmarks/datasets/locomo/source.json new file mode 100644 index 000000000..fcf794cb9 --- /dev/null +++ b/benchmarks/benchmarks/datasets/locomo/source.json @@ -0,0 +1,6 @@ +{ + "dataset_id": "locomo", + "source_url": "https://raw.githubusercontent.com/snap-research/locomo/main/data/locomo10.json", + "citation": "LoCoMo (ACL 2024, Snap Research)", + "license_note": "Check upstream dataset terms before redistributing snapshots." +} diff --git a/benchmarks/benchmarks/datasets/longmemeval/README.md b/benchmarks/benchmarks/datasets/longmemeval/README.md new file mode 100644 index 000000000..90767e1e3 --- /dev/null +++ b/benchmarks/benchmarks/datasets/longmemeval/README.md @@ -0,0 +1,3 @@ +# LongMemEval (Scaffold) + +LongMemEval integration is scaffolded in v1 and will be implemented in a follow-up. diff --git a/benchmarks/benchmarks/datasets/longmemeval/download.sh b/benchmarks/benchmarks/datasets/longmemeval/download.sh new file mode 100755 index 000000000..28aece390 --- /dev/null +++ b/benchmarks/benchmarks/datasets/longmemeval/download.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail + +echo "LongMemEval download is scaffolded for v1." +exit 1 diff --git a/benchmarks/benchmarks/results/matrix-v1.2-summary.md b/benchmarks/benchmarks/results/matrix-v1.2-summary.md new file mode 100644 index 000000000..156c4e3ad --- /dev/null +++ b/benchmarks/benchmarks/results/matrix-v1.2-summary.md @@ -0,0 +1,152 @@ +> **SUPERSEDED by `matrix-v2-summary.md`** (corrected judge rubric #36/#37, BM +> post-#994, recall-led with the failure diagnostic). Kept for history; the QA +> numbers below predate the rubric correction and the FTS-revival merge. + +# Benchmark Matrix v1.2 — internal results + +Run: 2026-06-12 evening. Local, zero API spend (answerer `claude:claude-haiku-4-5`, +judge `claude:claude-sonnet-4-6` via plan; mem0/embeddings via local Ollama). + +**BM under test: `main` @ d46c6880** (before the FTS-revival fix, PR #994). +mem0 run in raw-add mode (`MEM0_INFER` unset → `infer=false`); see fairness note. + +## QA accuracy (the headline metric) + +| Benchmark | bm-local | mem0-local | baseline-grep | baseline-fullcontext | +|---|---|---|---|---| +| LongMemEval-S (n=60, stratified 6 cats) | **0.617** | 0.417 | 0.300 | 0.217 | +| ConvoMem cs10 (n=274) | 0.792 | 0.474 | 0.398 | **0.825** | + +## Retrieval (deterministic) + +| Benchmark | provider | recall@5 | MRR | content-hit | mean lat | +|---|---|---|---|---|---| +| LongMemEval-S | bm-local | 0.951 | 0.900 | 0.467 | 754ms | +| LongMemEval-S | mem0-local | 0.979 | 0.876 | 0.500 | 146ms | +| LongMemEval-S | baseline-grep | 0.846 | 0.832 | 0.350 | 5ms | +| ConvoMem cs10 | bm-local | 0.982 | 0.929 | 0.128 | 140ms | +| ConvoMem cs10 | mem0-local | 0.996 | 0.956 | 0.131 | 122ms | +| ConvoMem cs10 | baseline-grep | 0.954 | 0.863 | 0.062 | 1ms | + +(full-context retrieval metrics are N/A by design — single whole-corpus hit.) + +## Findings + +1. **BM leads QA accuracy on LongMemEval-S and is a close 2nd on ConvoMem**, + despite mem0 edging it on retrieval recall. BM's retrieved chunks are more + answer-bearing: mem0 abstains far more (LME 30/60, ConvoMem 169/274 vs BM + 20/60, 86/274). Retrieval recall ≠ answer quality. + +2. **Full-context is a poor baseline at small-model scale.** On LongMemEval-S + (~124K-token haystacks, 496K-char assembled context) qwen2.5:3b-class + answering drops to 0.217 — it cannot use the whole haystack and abstains. + On the smaller ConvoMem cs10 it wins (0.825). Confirms the published + pattern: full-context beats retrieval only while the corpus fits the + model's effective working window. + +3. **mem0 raw-add caveat (fairness).** mem0's published numbers use + `infer=true` (LLM fact extraction). We ran raw-add to match the June 10 + baseline and avoid a local-3B extraction step of unknown quality. A future + matrix should run mem0 both ways and report both, with the extraction model + documented. + +## PR #994 (FTS-revival) impact — corrected LoCoMo + +Two measurements, each stating exactly what it covers: + +**Retrieval — full 1,986-query set, same pre-built index, code-only A/B** +(`/tmp/replay_fusion.py` against `matrix-locomo-v2-full`'s index). Definitive. + +| metric | BM main | BM #994 | Δ | +|---|---|---|---| +| recall@5 | 0.745 | 0.823 | **+7.9** | +| MRR | 0.618 | 0.718 | **+10.0** | + +Every category improves; largest on adversarial (+0.12 r5) and open_domain +(+0.095 r5), smallest on temporal (+0.003). + +**QA accuracy — q300 non-adversarial subset, fresh re-index, full QA stage** +(`m994-locomo-q300` vs `matrix-locomo-v2-q300`). + +| | BM main | BM #994 | Δ | +|---|---|---|---| +| accuracy | 0.439 | **0.475** | **+3.7** | +| abstain | 95 | 87 | -8 | + +By category (correct/total): open_domain 105→112, single_hop 18→23, +multi_hop 4→5, temporal 5→3 (n=19, noise). + +The #994-only QA gain is smaller than the retrieval gain because the largest +retrieval improvements land in the adversarial category (excluded from +QA-meaningful scoring) and **multi_hop stayed ~0.08 — bottlenecked by a +separate gap**: BM returns bullet-level matched chunks that strip +document-level context (the session date lives in the title). That gap is +addressed next. + +## #994 + title-passthrough (combined) — corrected LoCoMo q300 + +The harness was discarding the dated `title` that `search_notes` returns +(PR #31, `bm_local._row_to_hit`); surfacing it lets the answerer anchor +relative-date references ("two days ago"). Combined with #994, on the same +q300 subset and QA stage: + +| QA accuracy | BM main | +FTS (#994) | +FTS +title | +|---|---|---|---| +| overall | 0.439 | 0.475 | **0.611** | + +By category (correct/total), main → +FTS → +FTS+title: + +| category | main | +FTS | +FTS+title | +|---|---|---|---| +| single_hop | 18/55 | 23/55 | 20/55 | +| multi_hop | 4/63 | 5/63 | **40/63** | +| temporal | 5/19 | 3/19 | 5/19 | +| open_domain | 105/164 | 112/164 | 119/164 | + +**+17.2 points overall (0.439 → 0.611)**, driven by multi_hop (4 → 40 — these +are mostly relative-date questions that were unanswerable without the session +date). single_hop dips 23 → 20 vs the FTS-only run (the title header +occasionally distracts simple lookups; n=55, within LLM-judge noise, still +2 +vs main). The title fix is harness-side and provider-faithful — it uses data +BM already returns. A product follow-up could fold the parent heading/date +into `matched_chunk` so naive agents get the anchor without reading the title. + +## supermemory-local — head-to-head status + +Provider works end-to-end against the live `supermemory-server 0.0.2` +(local, Ollama-backed). **Preliminary head-to-head** (35-query ConvoMem +smoke, both providers clean): QA tied (BM and supermemory both 0.943), BM +search **3.4× faster** (84ms vs 289ms). On a reconfirm and a 110-query +sample, bm-local scored strongly standalone (110q: **0.845**, 35q: **0.914**). + +**A fair full comparison is currently blocked by upstream issue #1096.** +Server logs confirm it directly: + +``` +[Workflow] Document … memory agent failed (228110ms) +WARN Self-hosted memory agent failed for document, skipping memory generation +``` + +supermemory's "memory agent" (its LLM extraction/summarization step) calls +the OpenAI **Responses API**, which Ollama rejects — and it spends **~228s per +document** timing out before skipping. At ~10 docs/group this exceeds any +practical ingest timeout, so grouped runs fail. Embeddings (on-device WASM) +work but are slow (~3-4× BM's search latency) and showed instability under +sustained load on one server instance. + +Two honest findings stand on their own: +1. **supermemory-local is operationally heavy** — slow on-device embedding and + an extraction step that does not function with a local Ollama LLM, making + large fair runs impractical in this setup without more engineering. +2. On the small slice that completed, **BM matches supermemory on QA and is + markedly faster.** + +**Next step to complete the comparison:** a Responses→ChatCompletions shim +proxy in front of Ollama (the documented #1096 workaround) so supermemory's +memory agent functions, then a full grouped run. Until then the supermemory +number is preliminary and labeled as such. + +## Pending +- Responses→ChatCompletions shim for a fair full supermemory comparison (#1096). +- Product follow-ups stacking on basic-memory #994: fold heading/date into + `matched_chunk`; cross-encoder rerank (#950/#618) for single_hop/temporal. diff --git a/benchmarks/benchmarks/results/matrix-v2-summary.md b/benchmarks/benchmarks/results/matrix-v2-summary.md new file mode 100644 index 000000000..960b3a7a5 --- /dev/null +++ b/benchmarks/benchmarks/results/matrix-v2-summary.md @@ -0,0 +1,136 @@ +# Benchmark Matrix v2 — internal results (publication-ready) + +Supersedes `matrix-v1.2-summary.md` (pre-rubric-correction, pre-#994). Same +harness, corrected judge rubric (basic-memory-benchmarks #36/#37), BM under test +is **`main` post-#994** (FTS-revival merged). Internal-only; written to withstand +external scrutiny. + +## Methodology (what makes these numbers fair) + +- **Zero API spend.** Answering and judging run through the Claude plan + (`claude -p`); competitor internal LLM calls (mem0 extraction) run on local + Ollama. No paid API. +- **Fixed answerer and judge, identical for every provider.** Answerer + `claude:claude-haiku-4-5`, judge `claude:claude-sonnet-4-6`. Each provider + retrieves; the *same* answerer writes an answer from the retrieved memories; + the *same* judge grades it against gold. Holding the answerer constant is what + isolates retrieval — but it also means **absolute QA accuracy is + answerer-dependent** (a stronger answerer would raise every number). Relative + standings are the robust comparison. +- **Corrected rubric**, validated on all gold-answer styles including abstention + (the #36→#37 fix). Judge decisions are auditable per case via `run review` + HTML reports. +- **`run diagnose`** attributes every QA failure to *retrieval* (gold not found) + vs the *answerer* (gold found, answer still wrong) — see below. +- No feature flags: BM runs default `main`. + +## Anchors + +LongMemEval-S (stratified 60, 6 categories) and ConvoMem cs10 (274). LoCoMo is +secondary (Penfield-corrected key, adversarial excluded) and reported separately. + +### 1. Retrieval — the answerer-independent signal (lead with this) + +| Benchmark | provider | recall@5 | recall@10 | MRR | mean lat | +|---|---|---|---|---|---| +| LongMemEval-60 | bm-local | 0.951 | 0.951 | **0.900** | 754ms | +| LongMemEval-60 | mem0-local | **0.979** | 0.992 | 0.876 | 146ms | +| LongMemEval-60 | baseline-grep | 0.846 | 0.937 | 0.832 | 5ms | +| ConvoMem-274 | bm-local | 0.982 | 0.996 | 0.929 | 140ms | +| ConvoMem-274 | mem0-local | **0.996** | 1.000 | **0.956** | 122ms | +| ConvoMem-274 | baseline-grep | 0.954 | 1.000 | 0.863 | 1ms | + +Retrieval is **near-parity**: mem0 marginally leads recall@5, BM leads MRR on +LongMemEval and is within ~0.03 on ConvoMem. Both systems find the gold evidence +almost always (recall@10 ≥ 0.95). grep is a strong lexical baseline here. + +### 2. QA accuracy — corrected rubric (BM leads decisively) + +| Benchmark | bm-local | mem0-local | baseline-grep | full-context | +|---|---|---|---|---| +| LongMemEval-60 | **0.583** | 0.450 | 0.333 | 0.217 | +| ConvoMem-274 | **0.755** | 0.464 | 0.380 | 0.799 | + +Abstention rates (answerer says "I don't know"): LongMemEval BM **20**/60 vs mem0 +30/60; ConvoMem BM **86**/274 vs mem0 169/274. mem0 abstains ~2× more often. + +### 3. Diagnostic — why BM's QA lead is NOT a retrieval effect + +`run diagnose`, per provider, over answerable questions: + +| Benchmark | provider | retrieval ceiling | answerer gap | retrieval gap | of failures: answerer | +|---|---|---|---|---|---| +| LongMemEval-60 | bm-local | 0.983 | 0.400 | 0.017 | 96% | +| LongMemEval-60 | mem0-local | 1.000 | 0.550 | 0.000 | 100% | +| ConvoMem-274 | bm-local | 1.000 | 0.245 | 0.000 | 100% | +| ConvoMem-274 | mem0-local | 1.000 | 0.535 | 0.000 | 100% | + +The retrieval ceiling (max QA if the answerer were perfect) is ~1.0 for **both** +systems — i.e. essentially every QA failure is "the gold evidence *was* retrieved, +the answer was still wrong," not "retrieval missed it." So the BM>mem0 QA gap is +**not** a recall gap. It is that **the memories BM returns are more answerable by +the fixed model** — BM returns dated, in-context chunks; mem0 (raw-add) returns +material the small answerer more often can't commit to, so it abstains. On +ConvoMem the haystack fully fits the retrieval window (ceiling 1.000 for +everyone), so that benchmark measures *presentation + answering*, not recall. + +**Honest reading:** lead published comparisons with **recall@k / MRR** (parity, +answerer-independent). Present QA as a secondary "answer-bearing context" signal +where BM leads — with the mem0-infer caveat below kept prominent. + +## LoCoMo (secondary) — BM product progression + +LoCoMo is Penfield-flagged (~6.4% gold-key errors; adversarial excluded). We +report BM's own improvement across the two shipped fixes, same q300 subset and QA +stage, corrected rubric: + +| QA accuracy | BM main | +FTS (#994) | +FTS +title | +|---|---|---|---| +| overall | 0.439 | 0.475 | **0.641** | + +multi_hop is the driver (4/63 → 40/63): these are relative-date questions that +were unanswerable until the dated session title was surfaced to the answerer +(harness fix #31, provider-faithful — BM already returns the title). BM retrieval +on this slice: recall@5 0.774, recall@10 0.875, MRR 0.697. Diagnostic: ceiling +0.930, 19% of failures are true retrieval misses (the only anchor with real +retrieval headroom). A corrected-rubric mem0 head-to-head on this exact subset is +**not yet run** — the earlier mem0 LoCoMo number (0.535) predates the rubric fix +and is not directly comparable. + +## supermemory-local — preliminary, fair full run blocked + +Provider works end-to-end vs `supermemory-server 0.0.2` (local, Ollama-backed). +Preliminary 35-q ConvoMem smoke: QA tied (both 0.943), BM search **3.4× faster** +(84ms vs 289ms). A fair full run is **blocked by upstream #1096**: supermemory's +memory-agent calls the OpenAI Responses API (Ollama rejects it), spending ~228s/doc +before skipping, so grouped ingest times out. Also #1093: on-device embedding RSS +ballooned to ~24GB. Two findings stand: supermemory-local is operationally heavy +(slow + memory-hungry vs BM's lightweight fastembed), and on the slice that +completed, BM matches its QA and is markedly faster. Needs a +Responses→ChatCompletions shim for a complete comparison. + +## Caveats (read before citing) + +- **mem0 ran raw-add (`infer=false`).** mem0's published numbers use `infer=true` + (LLM fact extraction). We matched the June-10 baseline and avoided an unvetted + local-3B extraction step. mem0's QA could improve under `infer=true`; a fair + external comparison must run mem0 both ways with the extraction model documented. +- **QA is answerer-bound** (the diagnostic). These absolute numbers reflect a + haiku-class answerer; a stronger answerer raises all of them. Don't read QA as a + pure retrieval-quality measure — that's what recall@k/MRR are for. +- **Judge-human agreement not yet measured.** Every QA number rests on the LLM + judge. The `run review` HTML reports support human labeling; the judge-vs-human + agreement pass is the remaining validation before any external publication. +- **LoCoMo** gold key is documented-imperfect; treat as directional. +- **n sizes**: LongMemEval 60, LoCoMo q300 subset — modest; ConvoMem 274. + +## Remaining for external publication + +1. **Judge-human agreement pass.** A balanced 60-case sample is ready at + `benchmarks/runs/judge-agreement-sample/review.html` (seed 42; 24 LongMemEval + / 24 ConvoMem / 12 LoCoMo; 36 BM / 12 mem0 / 12 grep; 20 correct / 20 incorrect + / 20 abstain). Open it, label each verdict agree/disagree/unsure, Export, and + report agreement = agree / (agree + disagree). Publish the agreement rate + alongside the numbers — every QA figure rests on the judge. +2. mem0 `infer=true` (and corrected-rubric mem0 on LoCoMo q300) to complete the matrix. +3. supermemory Responses→ChatCompletions shim (#1096) for a fair full run. diff --git a/benchmarks/benchmarks/results/structured-ingestion-findings.md b/benchmarks/benchmarks/results/structured-ingestion-findings.md new file mode 100644 index 000000000..49ec0f73c --- /dev/null +++ b/benchmarks/benchmarks/results/structured-ingestion-findings.md @@ -0,0 +1,96 @@ +# Structured ingestion: does Basic Memory's native representation help? + +**Date:** 2026-06-14 · **Status:** internal finding · **Harness:** `convert structure-corpus` + +## Question + +The default benchmark ingests each conversation as a flat `## Conversation` +transcript, so Basic Memory is exercised purely as a text-search engine — none +of its native representation (typed `- [category]` observations and +`- relation [[Entity]]` links) is used. Does running BM "at its best", with the +conversations distilled into its knowledge-graph form, change the numbers? + +## Method + +`convert structure-corpus` rewrites each flat conversation doc into a BM-native +note via the plan-billed `claude -p` extractor (no API spend), in two modes: + +- **augment** (faithful): keep the transcript **and** append an extracted + `## Observations` + `## Relations` block. This mirrors how BM actually + works — a note keeps its prose while its parsed observations/relations become + first-class searchable units. +- **replace**: substitute the structured block for the transcript (lossy). + +Document ids and frontmatter are preserved, so retrieval ground truth, recall, +QA, and the failure diagnostic stay directly comparable to the flat corpus. +Answerer (`claude-haiku-4-5`) and judge (`claude-sonnet-4-6`) are held constant; +only the corpus representation changes. + +## Results + +### ConvoMem `implicit_connection` (49 q) — retrieval saturated + +| arm | QA acc | correct | +|---|---|---| +| flat (transcripts) | **0.449** | 22/49 | +| structured-replace (lossy) | 0.367 | 18/49 | +| structured-augment (faithful) | **0.449** | 22/49 | + +Augment **ties** flat exactly (it flips 10/49 questions, +5/−5 — a wash); +replace **hurts** because distillation drops the specific personal details these +questions hinge on. **Why:** ConvoMem cs10 retrieval is *saturated* (the +diagnostic shows a retrieval ceiling of 1.000 — the answerer already receives the +entire 10-doc haystack), so representation can only reshuffle what is already in +context. **Implication:** flat markdown is a *fair* representation for BM on +ConvoMem — the benchmark was not secretly hobbling BM there. + +### LoCoMo `multi_hop` (63 q, full 272-doc haystack) — retrieval has headroom + +| metric | flat | structured-augment | Δ | +|---|---|---|---| +| recall@5 | 0.778 | **0.833** | **+0.055** | +| recall@10 | 0.841 | **0.889** | **+0.048** | +| MRR | 0.679 | **0.715** | +0.036 | +| QA accuracy | 0.556 (35/63) | 0.571 (36/63) | +0.015 | + +Structure **improves retrieval**: +5 questions newly reach their gold evidence in +the top-10 (−2 lost, net +3). Example — *"Where did Joanna travel in July 2022?"*: +flat never retrieves the evidence (→ "I don't know"); structured does (→ +"Woodhaven", correct). QA barely moves (+1) because the diagnostic attributes 81% +of multi_hop failures to the fixed answerer, so retrieval gains don't fully +convert end-to-end. + +## Interpretation + +Structure is a **wash where retrieval saturates** (ConvoMem) and **helps recall +where retrieval has headroom** (LoCoMo multi_hop) — a coherent two-slice story. + +**Mechanism (important for credibility):** Basic Memory does **not** traverse +relations at query time today, so the recall gain is **not** graph multi-hop. It +comes from the concise observations being *better retrieval targets* than the same +facts buried in a long transcript — a better lexical/semantic surface. + +**Confound to close before publishing:** augmented docs are longer (transcript + +structure), giving more match surface. A replace-mode retrieval ablation would +isolate "structure" from "more text". The LoCoMo effect is also modest (n=63); +confirm on the full q300 / multi_hop+temporal slice before any external claim. + +**Product lever this surfaces:** following `[[Entity]]` relations during search +(query-time relation traversal) would turn the structured representation into +genuine multi-hop retrieval — a concrete future basic-memory retrieval +improvement, distinct from the lexical-surface gain measured here. + +## Reproduce + +```bash +# Structure a corpus (flat or grouped layout; augment keeps the transcript) +bm-bench convert structure-corpus \ + --input-dir benchmarks/generated/locomo-corrected-v2/docs \ + --output-dir benchmarks/generated/locomo-corrected-v2-augmented/docs \ + --mode augment --extractor claude:claude-haiku-4-5 + +# Run flat vs structured over the same queries; compare recall + QA + diagnose +bm-bench run retrieval --providers bm-local --corpus-dir --queries-path ... +bm-bench run qa --run-dir --answerer claude:claude-haiku-4-5 --judge claude:claude-sonnet-4-6 +bm-bench run diagnose --run-dir +``` diff --git a/benchmarks/benchmarks/synthetic/docs/agent-memory.md b/benchmarks/benchmarks/synthetic/docs/agent-memory.md new file mode 100644 index 000000000..f13c67b00 --- /dev/null +++ b/benchmarks/benchmarks/synthetic/docs/agent-memory.md @@ -0,0 +1,13 @@ +--- +title: agent-memory +type: note +source_doc_id: agent-memory +dataset_id: synthetic +conversation_id: synthetic-c00 +session_number: 3 +permalink: bm-bench-3bea66c2eddf/agent-memory +--- + +# agent-memory + +The assistant uses graph traversal plus search to build context for follow-up tasks. \ No newline at end of file diff --git a/benchmarks/benchmarks/synthetic/docs/auth-memory.md b/benchmarks/benchmarks/synthetic/docs/auth-memory.md new file mode 100644 index 000000000..ab005935a --- /dev/null +++ b/benchmarks/benchmarks/synthetic/docs/auth-memory.md @@ -0,0 +1,13 @@ +--- +title: auth-memory +type: note +source_doc_id: auth-memory +dataset_id: synthetic +conversation_id: synthetic-c00 +session_number: 1 +permalink: bm-bench-3bea66c2eddf/auth-memory +--- + +# auth-memory + +Users authenticate with short-lived JWT access tokens and rotating refresh tokens. \ No newline at end of file diff --git a/benchmarks/benchmarks/synthetic/docs/sync-memory.md b/benchmarks/benchmarks/synthetic/docs/sync-memory.md new file mode 100644 index 000000000..ffe2d8c2b --- /dev/null +++ b/benchmarks/benchmarks/synthetic/docs/sync-memory.md @@ -0,0 +1,13 @@ +--- +title: sync-memory +type: note +source_doc_id: sync-memory +dataset_id: synthetic +conversation_id: synthetic-c00 +session_number: 2 +permalink: bm-bench-3bea66c2eddf/sync-memory +--- + +# sync-memory + +The sync engine watches filesystem edits and reindexes changed notes using checksums. \ No newline at end of file diff --git a/benchmarks/benchmarks/synthetic/queries.json b/benchmarks/benchmarks/synthetic/queries.json new file mode 100644 index 000000000..b060e6073 --- /dev/null +++ b/benchmarks/benchmarks/synthetic/queries.json @@ -0,0 +1,38 @@ +[ + { + "id": "synthetic-q001", + "query": "How are refresh tokens handled?", + "category": "single_hop", + "category_id": 1, + "ground_truth": ["auth-memory"], + "expected_answer": "rotating refresh tokens", + "metadata": {"dataset_id": "synthetic"} + }, + { + "id": "synthetic-q002", + "query": "How does note change detection trigger indexing?", + "category": "multi_hop", + "category_id": 2, + "ground_truth": ["sync-memory"], + "expected_answer": "reindexes changed notes", + "metadata": {"dataset_id": "synthetic"} + }, + { + "id": "synthetic-q003", + "query": "How does the assistant construct context?", + "category": "open_domain", + "category_id": 4, + "ground_truth": ["agent-memory"], + "expected_answer": "graph traversal plus search", + "metadata": {"dataset_id": "synthetic"} + }, + { + "id": "synthetic-q004", + "query": "What color was the moonlight in the transcript?", + "category": "adversarial", + "category_id": 5, + "ground_truth": [], + "expected_answer": "", + "metadata": {"dataset_id": "synthetic"} + } +] diff --git a/benchmarks/docs/benchmarks.md b/benchmarks/docs/benchmarks.md new file mode 100644 index 000000000..59a9066c8 --- /dev/null +++ b/benchmarks/docs/benchmarks.md @@ -0,0 +1,501 @@ +# Benchmark Runbook + +This document is the canonical operator runbook for benchmark execution in +`basic-memory-benchmarks`. + +It covers: +1. current benchmark workflows and commands, +2. current manual commit-to-commit comparison workflow, +3. planned (not yet implemented) revision matrix workflow. + +## Current vs Planned + +| Area | Status | +| --- | --- | +| Single run execution (`run retrieval`, `run full`, `run judge`) | Implemented | +| `just` one-command pipelines (`bench-full`, `bench-full-judge`) | Implemented | +| Artifact generation and publish/compare commands | Implemented | +| Manual BM revision comparison via worktrees + `--bm-local-path` | Implemented workflow, manual orchestration | +| `bm-bench run revision-matrix` | Planned, not implemented yet | + +## 1) Purpose and Scope + +### Goals + +- deterministic retrieval evaluation for BM and comparator providers, +- reproducible latency and quality tracking over time, +- publishable artifacts with provenance. + +### Headline scoring + +- Official headline: LoCoMo categories 1-4 (`official_headline` in summaries) +- Adversarial breakout: LoCoMo category 5 (`adversarial_breakout`) + +### Fairness contract + +- Same query set for all providers in the same run. +- Same `top_k` for all providers in the same run. +- No provider-specific query rewriting for headline runs. +- Provider failures/skips must be explicit in artifacts (`provider-status.json`). + +## 2) Prerequisites + +### Repositories and paths + +- benchmark repo: clone of `basicmachines-co/basic-memory-benchmarks` +- BM local repo: set `BM_LOCAL_PATH` env var (or in `.env`) to your local `basic-memory` checkout + +### Environment + +- `.env` is auto-loaded by `just` (`set dotenv-load := true`). +- For `mem0-local`, set `OPENAI_API_KEY`. + +### One-time setup + +```bash +cd /path/to/basic-memory-benchmarks +just sync +``` + +If you plan to run judge metrics: + +```bash +just sync-judge +``` + +### Dataset assumptions + +LoCoMo source and converted outputs are created by: + +```bash +just bench-prepare-short +just bench-prepare-long +``` + +## 3) Command Surface (Current Source of Truth) + +### `just` commands (current) + +- `bench-full` +- `bench-full-judge` +- `bench-prepare-short` +- `bench-prepare-long` +- `bench-run-short` +- `bench-run-long` +- `bench-run-full` +- `bench-judge` +- `bench-validate` +- `bench-publish` +- `bench-compare` +- `bench-latest-run` + +### `bm-bench` CLI (current) + +Top-level commands: + +- `datasets fetch` +- `convert locomo` +- `run retrieval` +- `run full` +- `run judge` +- `compare` +- `validate-artifacts` +- `publish` + +## 4) How Runs Work Today (Operator Workflow) + +### One-command full retrieval run + +```bash +cd /path/to/basic-memory-benchmarks +just bench-full +``` + +This runs: +1. `just sync` +2. `just bench-prepare-long` +3. `just bench-run-full` + +### One-command full retrieval + judge + +```bash +cd /path/to/basic-memory-benchmarks +just bench-full-judge +``` + +This runs: +1. `just sync-judge` +2. `just bench-prepare-long` +3. `just bench-run-full-judge` + +### Short vs long workflows + +Short (quick25): + +```bash +just bench-prepare-short +just bench-run-short +``` + +Long (full LoCoMo): + +```bash +just bench-prepare-long +just bench-run-long +``` + +Strict provider mode (fail run if any provider fails/skips): + +```bash +just bench-run-short-strict +just bench-run-long-strict +``` + +## 5) Run Lifecycle Internals (Current Behavior) + +### `bm-local` provider flow + +1. Resolve BM command: + - default: `bm` + - local override: `uv run --project basic-memory` +2. Create/reuse benchmark project: + - `basic-memory project add bm-bench- ` +3. Reindex: + - prefer `reindex --search --embeddings` + - fallback to `reindex --search` +4. Wait for readiness: + - if supported, poll `status --json --project --local` +5. Start a warm MCP stdio session: + - one long-lived `basic-memory mcp` process per provider run +6. Execute `search_notes` calls over MCP for each query. +7. Cleanup MCP session. + +### `mem0-local` provider flow + +1. Requires `OPENAI_API_KEY`. +2. Uses deterministic user namespace: + - `bm-bench--mem0` +3. Ingests markdown corpus with metadata: + - `source_doc_id`, `source_path`, `conversation_id`, `dataset_id` +4. Calls `Memory.search` for each query. +5. Cleans provider state via `delete_all(user_id=...)`. + +## 6) Artifacts and Provenance + +Each run writes to `benchmarks/runs//`. + +Required files: + +- `manifest.json` +- `provider-status.json` +- `per-query-retrieval.jsonl` +- `retrieval-summary.json` +- `summary.md` + +Optional judge files: + +- `per-query-judge.jsonl` +- `judge-summary.json` + +### Key provenance fields + +From `manifest.json`: + +- `benchmark_git_sha` +- `bm_source` +- `bm_resolved_sha` +- `bm_local_path` +- `provider_versions` +- `dataset.checksum_sha256` + +### Useful commands + +Get latest run: + +```bash +just bench-latest-run +``` + +Validate artifacts: + +```bash +just bench-validate run_dir="$(just bench-latest-run)" +``` + +Publish bundle: + +```bash +just bench-publish run_dir="$(just bench-latest-run)" +``` + +## 7) Commit-to-Commit Comparison (Current Manual Method) + +Use this workflow today to compare BM revisions while keeping benchmark tooling fixed. + +### Step 1: Create BM worktrees for target refs + +```bash +BM_REPO=/path/to/basic-memory +WT_ROOT=/path/to/basic-memory-benchmarks/benchmarks/worktrees/basic-memory + +mkdir -p "$WT_ROOT" + +git -C "$BM_REPO" worktree add "$WT_ROOT/pre_fusion" f5a0e942^ +git -C "$BM_REPO" worktree add "$WT_ROOT/fusion" f5a0e942 +git -C "$BM_REPO" worktree add "$WT_ROOT/context_step1" f9b2a075 +git -C "$BM_REPO" worktree add "$WT_ROOT/context_step2" 9331126b +git -C "$BM_REPO" worktree add "$WT_ROOT/current" HEAD +``` + +### Step 2: Prepare benchmark datasets once + +```bash +cd /path/to/basic-memory-benchmarks +just sync +just bench-prepare-short +just bench-prepare-long +``` + +### Step 3: Run BM for each revision with deterministic run IDs + +Run ID convention: + +- short: `-short-r1` +- long: `-long-r1` + +Example for one revision (`fusion`) and long dataset: + +```bash +uv run bm-bench run retrieval \ + --run-id fusion-long-r1 \ + --dataset-id locomo \ + --dataset-path benchmarks/datasets/locomo/locomo10.json \ + --corpus-dir benchmarks/generated/locomo/docs \ + --queries-path benchmarks/generated/locomo/queries.json \ + --providers bm-local \ + --bm-local-path "$WT_ROOT/fusion" \ + --strict-providers +``` + +Example for one revision (`fusion`) and short dataset: + +```bash +uv run bm-bench run retrieval \ + --run-id fusion-short-r1 \ + --dataset-id locomo-c1-quick25 \ + --dataset-path benchmarks/datasets/locomo/locomo10.json \ + --corpus-dir benchmarks/generated/locomo-c1/docs \ + --queries-path benchmarks/generated/locomo-c1/queries.quick25.json \ + --providers bm-local \ + --bm-local-path "$WT_ROOT/fusion" \ + --strict-providers +``` + +Repeat for: + +- `pre_fusion` (`f5a0e942^`) +- `fusion` (`f5a0e942`) +- `context_step1` (`f9b2a075`) +- `context_step2` (`9331126b`) +- `current` (`HEAD`) + +### Step 4: Run mem0 anchor once per dataset (optional but recommended) + +Long anchor: + +```bash +uv run bm-bench run retrieval \ + --run-id mem0-anchor-long-r1 \ + --dataset-id locomo \ + --dataset-path benchmarks/datasets/locomo/locomo10.json \ + --corpus-dir benchmarks/generated/locomo/docs \ + --queries-path benchmarks/generated/locomo/queries.json \ + --providers mem0-local \ + --allow-provider-skip +``` + +Short anchor: + +```bash +uv run bm-bench run retrieval \ + --run-id mem0-anchor-short-r1 \ + --dataset-id locomo-c1-quick25 \ + --dataset-path benchmarks/datasets/locomo/locomo10.json \ + --corpus-dir benchmarks/generated/locomo-c1/docs \ + --queries-path benchmarks/generated/locomo-c1/queries.quick25.json \ + --providers mem0-local \ + --allow-provider-skip +``` + +### Step 5: Compare runs + +```bash +just bench-compare \ + "benchmarks/runs/pre_fusion-long-r1/retrieval-summary.json" \ + "benchmarks/runs/fusion-long-r1/retrieval-summary.json" \ + bm-local \ + recall_at_5 +``` + +Recommended metrics to compare: + +- `recall_at_5` +- `recall_at_10` +- `mrr` +- `mean_latency_ms` +- `p95_latency_ms` + +### Step 6: Record matrix results + +Use a summary table with baseline deltas, for example: + +| Revision | Dataset | Recall@5 | Recall@10 | MRR | Delta R@5 vs pre_fusion | Delta MRR vs pre_fusion | +| --- | --- | --- | --- | --- | --- | --- | +| pre_fusion | long | ... | ... | ... | 0.000 | 0.000 | +| fusion | long | ... | ... | ... | ... | ... | +| context_step1 | long | ... | ... | ... | ... | ... | +| context_step2 | long | ... | ... | ... | ... | ... | +| current | long | ... | ... | ... | ... | ... | + +## 8) Planned Workflow: `run revision-matrix` (Not Implemented Yet) + +Status: planned. + +Planned defaults: + +- worktree-based BM revision execution, +- parallel workers: `2`, +- datasets: `both` (short + long), +- replicates: `1`, +- BM per revision + fixed mem0 anchor. + +Planned output root: + +- `benchmarks/matrices//` + +Planned command shape: + +```bash +uv run bm-bench run revision-matrix \ + --bm-repo-path /path/to/basic-memory \ + --revisions pre_fusion=f5a0e942^ \ + --revisions fusion=f5a0e942 \ + --revisions context_step1=f9b2a075 \ + --revisions context_step2=9331126b \ + --revisions current=HEAD \ + --baseline pre_fusion \ + --datasets both \ + --workers 2 \ + --replicates 1 \ + --providers-mode bm-only-mem0-anchor +``` + +## 9) Troubleshooting + +### `bm-local` fails on `project add` + +Symptoms: + +- `provider-status.json` shows `bm-local` state `error` +- reason contains `basic-memory project add ... returned non-zero exit status` + +Checks: + +1. verify path exists and is a BM repo: + - `ls /path/to/basic-memory/pyproject.toml` +2. verify command works directly: + - `uv run --project /path/to/basic-memory basic-memory --version` +3. rerun with explicit local path: + - `--bm-local-path /path/to/basic-memory` +4. use strict mode while debugging: + - `--strict-providers` + +### `status --json` behavior differs by BM build + +- Some BM environments support `status --json`; some older ones do not. +- Provider auto-detects support. +- If unsupported, benchmark still runs after reindex without JSON readiness polling. + +### Provider `skipped` vs `error` + +- `skipped`: expected gate not met (for example missing `OPENAI_API_KEY` for mem0). +- `error`: provider attempted execution and failed. + +### Long run duration + +- Full LoCoMo runs are expected to take significantly longer than quick25 runs. +- Use `bench-run-short` for quick checks before full runs. + +### Rerun single provider / single revision + +BM-only rerun with explicit revision worktree: + +```bash +uv run bm-bench run retrieval \ + --providers bm-local \ + --bm-local-path "$WT_ROOT/fusion" \ + --run-id fusion-long-r1-retry \ + --dataset-id locomo \ + --dataset-path benchmarks/datasets/locomo/locomo10.json \ + --corpus-dir benchmarks/generated/locomo/docs \ + --queries-path benchmarks/generated/locomo/queries.json +``` + +## 10) FAQ + +### Why use worktrees if we already use `uv`? + +`uv` solves environment/dependency reproducibility. Worktrees solve source isolation. For commit-to-commit benchmarking, worktrees make each revision explicit, auditable, and safe to run in parallel. + +### When should I use strict providers? + +Use strict mode (`--strict-providers`) for regression investigations and CI gates where silent skips are unacceptable. Use allow-skip mode for exploratory local runs where external credentials may be missing. + +### How do I publish run bundles? + +```bash +just bench-publish run_dir="$(just bench-latest-run)" +``` + +Or target a specific run directory: + +```bash +just bench-publish run_dir="benchmarks/runs/" +``` + +## 11) Validation Checklist for This Runbook + +Command surface checks: + +```bash +just --list +uv run bm-bench --help +uv run bm-bench run --help +``` + +Dry-run checks: + +```bash +just --dry-run bench-run-short +just --dry-run bench-run-long +just --dry-run bench-full +just --dry-run bench-full-judge +``` + +Artifact field checks: + +```bash +latest=$(just bench-latest-run) +cat "$latest/manifest.json" +cat "$latest/provider-status.json" +``` + +Comparison check: + +```bash +just bench-compare \ + "benchmarks/runs//retrieval-summary.json" \ + "benchmarks/runs//retrieval-summary.json" \ + bm-local \ + recall_at_5 +``` diff --git a/benchmarks/docs/write-load-benchmark.md b/benchmarks/docs/write-load-benchmark.md new file mode 100644 index 000000000..68b1b03ec --- /dev/null +++ b/benchmarks/docs/write-load-benchmark.md @@ -0,0 +1,515 @@ +# Write-path load benchmark (branch/SHA comparison) + +Living design + decisions + run log for the write-path/load benchmark used to +compare Basic Memory performance across git refs (e.g. `main` vs the +accepted-note refactor branch). + +## Goal + +Measure how the local write path behaves **under concurrency/load**, and +compare two installed versions of `basic-memory`. The hypothesis we want to +prove or disprove: + +> The DB-first **accept** holds the write lock briefly and pushes +> materialize / index / embed onto background work, so the system should +> **sustain more concurrent writers before accept-latency degrades or writes +> start blocking** — whereas the synchronous (`main`) write does parse → write +> file → index → embed inline before returning, so under load requests pile up +> (and on SQLite the single-writer lock makes that stark). + +## Key decisions + +1. **Drive over MCP stdio (`bm mcp`).** Spawning `bm mcp` brings up the *full* + local runtime — the file watcher **and** the background schedulers + (project-index, vector-sync, relation-resolution). That's exactly the async + machinery the refactor introduced, so `write_note` over MCP exercises the + real accept → materialize → background-followup path. CLI / in-process ASGI + would each need us to hand-start the watcher and flip `env` out of test mode. + +2. **Branch comparison via per-ref venvs (not worktrees).** Each ref gets its + own venv with `basic-memory@` installed from GitHub + (`uv pip install 'basic-memory @ git+https://github.com/basicmachines-co/basic-memory@'`). + The driver points the spawned `bm mcp` at that venv's `basic-memory` + executable (`--bm-command /bin/basic-memory`). The harness (this + driver) stays fixed; only the spawned `bm` varies → any two refs, repeatable. + +3. **Isolated runtime per run.** `BASIC_MEMORY_CONFIG_DIR` scopes config / DB / + project registry to a fresh scratch dir, and `BASIC_MEMORY_ENV` is **unset** + (so it is not `test` — test mode disables the watcher + schedulers, which + would defeat the whole measurement). A fresh home also avoids alembic + migration rot between versions. + +4. **Synthetic test driver for now.** We generate deterministic markdown notes + (frontmatter + observations + typed relations) on the fly. This keeps the + workload controllable (size, concurrency) and reproducible. We can later point + the same driver at a real corpus (an existing BM project, or the vendored + LoCoMo/synthetic corpora) for realism. + +5. **Standard JSONL output.** Each scenario emits one + `{"benchmark": "...", "metrics": {...}, "timestamp_utc": "..."}` line, using + the metric-name conventions the generic compare tool understands + (`*_ms` → lower better, `*_per_sec` → higher better). Compare two refs with + `basic-memory/test-int/compare_search_benchmarks.py` (or the package's + `bm-bench compare`). + +## Metrics (per concurrency level C) + +| metric | meaning | better | +| --- | --- | --- | +| `accept_latency_p50/p95/p99_ms` | per `write_note` call (the caller-perceived accept) | lower | +| `accept_throughput_per_sec` | notes accepted / wall-clock at concurrency C | higher | +| `accept_error_rate` | failed/timed-out writes / total | lower | +| `time_to_materialized_ms` | after the burst, until all N files exist on disk | lower | +| `time_to_searchable_ms` | after materialized, until all N notes are FTS-searchable | lower | +| `time_to_embedded_ms` | after searchable, until all N notes are vector-embedded | lower | + +The three drain metrics are measured **sequentially** (each poll starts when the +prior stage is ready), so `time_to_searchable`/`time_to_embedded` are the +*incremental* lag a stage adds beyond the previous one — a small value means that +stage keeps pace; a growing value means it's becoming the bottleneck. +`time_to_embedded_ms` is SQLite-only (peeks `search_vector_chunks`); it needs +fastembed + sqlite-vec in the venv (the warmup probes for this and skips the +stage if semantic is off). + +## Concurrency sweep + +`C ∈ {1, 4, 8, 16, 32}`; fixed burst of N writes per level (unique titles). +The "knee" — the C where throughput plateaus or p95 crosses a budget — is the +"before falling over" point per ref. + +## How to run + +```bash +cd benchmarks + +# 1) Build a per-ref venv (installs basic-memory@ from GitHub) +just bench-venv main +just bench-venv codex/repository-explicit-sessions + +# 2) Run the write-load sweep against a ref's venv +just bench-write-load main +just bench-write-load codex/repository-explicit-sessions + +# 3) Compare the two JSONL outputs +uv run python ../test-int/compare_search_benchmarks.py \ + .scratch/write-load-main.jsonl \ + .scratch/write-load-branch.jsonl --format markdown +``` + +## Run log + +### 2026-06-24 — main baseline (SQLite, semantic on) + +Ref: `main` (basic-memory 0.22.1, fastembed bge-small-en is a transitive dep so +embedding is part of the measured path). 60 writes/level, warmup 8 (model +pre-loaded + drained before measuring). + +| C | accept p50 (ms) | accept p95 (ms) | throughput (/s) | errors | time→searchable (ms) | +| --- | --- | --- | --- | --- | --- | +| 1 | 91 | 416 | 4.8 | 0 | 63 | +| 4 | 465 | 1839 | 5.5 | 0 | 36 | +| 8 | 876 | 2762 | 6.8 | 0 | 125 | +| 16 | 1966 | 3928 | 6.6 | 0 | 67 | +| 32 | 2530 | 5793 | 6.8 | 0 | 129 | + +**Interpretation (main = synchronous write path):** +- Throughput saturates by C≈8 (~6.7/s); extra concurrency does **not** lift it. +- Accept latency grows ~28× from C=1→C=32 (p50 91ms → 2.5s; p95 416ms → 5.8s): + each `write_note` does parse → write file → index → embed inline, so under + load writes queue behind the SQLite single-writer lock + embedding. +- `time_to_searchable` is small/flat (synchronous: the note is basically + indexed by the time `write_note` returns). +- No errors at these levels — it degrades by latency, not by failing (yet). + +This is the baseline the async accepted-note path should beat: flat/low accept +latency as C rises, higher sustained throughput, with the heavy work deferred +to background follow-ups (so `time_to_searchable`/`time_to_embedded` become the +measurable lag instead of accept latency). + +**Measurement notes / gotchas found:** +- The fastembed model download must be fully outside measurement; the warmup now + writes + waits-for-searchable so the model is loaded before level 1. +- A cosmetic "Process group termination failed … Operation not permitted" line + prints on teardown (macOS sandbox); the run still completes. + +### 2026-06-24 — main vs branch (`codex/repository-explicit-sessions`, PR #1002) + +Same harness, same params (60 writes/level, C∈{1,4,8,16,32}, SQLite, semantic +on). Branch venv installed from GitHub. **Single run each — directional, not +statistically settled.** + +| C | accept p50 main→branch | throughput main→branch | verdict | +| --- | --- | --- | --- | +| 1 | 92 → 322 ms (+252%) | 4.8 → 2.2 /s (-54%) | branch slower | +| 4 | 465 → 801 ms (+72%) | 5.5 → 3.9 /s (-29%) | branch slower | +| 8 | 876 → 1488 ms (+70%) | 6.8 → 4.4 /s (-35%) | branch slower | +| 16 | 1966 → 2781 ms (+41%) | 6.6 → 4.7 /s (-29%) | branch slower | +| 32 | 2530 → 4135 ms (+63%) | 6.8 → 5.3 /s (-21%) | branch slower | + +**The async-accept hypothesis did NOT hold on local SQLite.** The accepted-note +branch is consistently slower per accept and lower throughput at every level +(0 errors on both). `time_to_searchable` stays small on both, so the deferred +work is not what's showing up — the **accept path itself got heavier**. + +Leading hypotheses (to isolate next): +1. **Per-write full-project relation resolution (#1015).** The branch schedules + `resolve_project_relations` (a *whole-project* unresolved-relation scan) as a + background task on **every** write. Under a 60-write burst that's 60 scans + over a growing relation set, all contending on the SQLite writer — likely the + dominant cost. The fix's own issue notes the resolver supports coalescing; we + scheduled it per-write **without debounce**. Strong suspect. +2. **Accepted-note DB overhead.** DB-first accept persists an extra `note_content` + row (plus entity + search) per write — more writes per accept = longer SQLite + write-lock hold = more contention under concurrency. +3. **The "offline queue" win is cloud-specific (PGQ).** Locally there is no + durable queue; materialization runs inline and the schedulers are in-process + asyncio tasks. So the throughput benefit the hypothesis assumed may simply not + exist on the local SQLite runtime — it would show on Postgres + PGQ. + +This is a useful result: the benchmark immediately surfaced a probable +write-load regression in the new path (and specifically in the per-write +relation-resolution scheduling added for #1015). + +### 2026-06-24 — coalescing fix did NOT close the gap (hypothesis falsified) + +Pushed `perf(core): coalesce per-write relation resolution` (7ecb672c) so a +burst collapses to one debounced offline pass, reinstalled the branch venv, and +re-ran. The gap is essentially unchanged (within run noise): + +| C | branch p50 before → after fix | main p50 | still worse | +| --- | --- | --- | --- | +| 1 | 322 → 294 ms | 92 | +220% | +| 8 | 1488 → 1563 ms | 876 | +78% | +| 32 | 4135 → 4663 ms | 2530 | +84% | + +**Why it didn't help:** the relation passes were always **background** (async +tasks) — they add system/DB load but do not sit on the synchronous accept +latency we measure. The tell is **C=1**: with zero concurrency/contention the +branch is still ~3× slower per write (92 → 294 ms). That is the **synchronous +accept path itself** being heavier, not the follow-ups. + +**Revised conclusion:** the local write regression is in the accepted-note +accept path, not the schedulers. Locally the path pays for **both** the DB-first +accept (extra `note_content` row + mutation-runner machinery) **and** inline +materialization — whereas the cloud design's win is to *skip* inline +materialization and defer it to a PGQ queue. That queue benefit does not exist on +the local SQLite runtime, so the refactor is net heavier for local writes. + +The coalescing change is still worth keeping (it removes genuinely redundant +whole-project scans and lowers background DB contention) but it is not the fix +for accept latency. This is the benchmark working as intended — it tested a fix +and showed it did not address the measured cost. + +### 2026-06-24 — deferring materialization recovered (and flipped) the result + +Pushed `perf(core): defer local note materialization off the accept path` +(696d71b1): local `materialize_write_change` now schedules the file write + +index as a background task (parity with cloud's PGQ enqueue) and returns the +accepted note_content state at once. Reinstalled the branch venv, re-ran. + +| C | branch p50 before→after defer | main p50 | branch vs main | throughput (branch vs main) | +| --- | --- | --- | --- | --- | +| 1 | 322 → 113 ms | 92 | +23% slower | 3.5 vs 4.8 | +| 4 | 801 → 647 ms | 465 | +39% slower | 5.3 vs 5.5 | +| 8 | 1488 → 903 ms | 876 | ~parity | 6.7 vs 6.8 | +| 16 | 2781 → 1072 ms | 1966 | **45% faster** | **7.2 vs 6.6** | +| 32 | 4135 → 1938 ms | 2530 | **23% faster** | **7.6 vs 6.8** | + +**The hypothesis now holds.** Deferring the file write + index off the accept +path cut branch accept p50 by ~50-60% at C≥16 and made throughput **scale with +concurrency** (3.5 → 7.6/s) instead of plateauing like main's synchronous path +(~6.7/s). Crossover ~C=8 — below it main's lighter per-write path is marginally +faster, above it the deferred accept wins on both latency and throughput. + +Residual: at C=1 the branch is still ~+21ms (113 vs 92) — the intrinsic DB-first +accept overhead (extra `note_content` row + mutation-runner machinery), the +cloud-architecture tax, small in absolute terms and the price of one read model ++ parity. `time_to_searchable` rises under load (index is now async: ~0.5s at +C=32), the expected "DB is the cache, file+index catch up" tradeoff. + +Conclusion (moderate concurrency): the accepted-note refactor is a **net win for +concurrent local writes** once materialization is deferred, and a small per-write +cost at very low concurrency. **But see the high-concurrency caveat below** — the +deferral has no backpressure, so the picture changes under heavy sustained load. + +### 2026-06-24 — high-concurrency scaling: the async deferral hits an unbounded-queue wall + +Pushed concurrency to 128 (notes=200) to find where SQLite's single writer +chokes. The deferral helps the **median** at every level, but the **tail and +throughput regress under heavy sustained load** because the fire-and-forget +background materializations form an *unbounded* queue with no backpressure. + +| C | branch p50 (ms) | branch p99 (ms) | throughput /s | err rate | +| --- | --- | --- | --- | --- | +| 1 | 113 | 1249 | 5.5 | 0.000 | +| 16 | 2718 | 7991 | 5.0 | 0.000 | +| 32 | 4803 | 16362 | 5.1 | 0.000 | +| 64 | 5201 | **121771** | **1.5** | **0.020** | +| 128 | 5737 | **94954** | **2.0** | **0.015** | + +vs main at C=64: p50 9616 / p99 19130 / 5.5 thru / 0 err. The branch's **median +is far better** (5.2s vs 9.6s) but its **p99 is 6× worse** (122s vs 19s), +throughput collapses (1.5 vs 5.5/s), and it starts failing writes (SQLite +"database is locked"). Main's synchronous accept is self-throttling, so its tail +stays bounded. + +**Mechanism:** every accept schedules `asyncio.create_task(materialize)`. The +accept returns fast, but the background tasks pile up unbounded and contend with +incoming accepts for the **single SQLite writer** and the **single event loop**. +Past ~C=64 the backlog starves the request handler → tail explosion + lock +errors. The deferral *reorders* work; on one SQLite it does **not** add capacity. + +The collapse is load-/depth-dependent: a lighter run (notes=100) reached C=64 +with no errors and rising throughput, while notes=200 collapsed at C=64 — bigger +sustained bursts → deeper backlog → worse tail. + +**Implication:** the real scaling fix is **bounded** background materialization — +a worker pool / semaphore that caps in-flight materializations and applies +backpressure (the accept slows instead of the backlog growing unbounded). That +bounds the tail and keeps throughput from collapsing. Next experiment. + +### 2026-06-24 — SQLite vs Postgres (async writes, local): Postgres collapses on NullPool + +Ran the branch's async write path on SQLite vs a local Postgres (pgvector +testcontainer), notes=100. Two local-Postgres bugs had to be cleared first (see +below); after that: + +| C | p50 ms (sql / pg) | p99 ms (sql / pg) | thru/s (sql / pg) | err (sql / pg) | +| --- | --- | --- | --- | --- | +| 1 | 121 / 548 | 1686 / 1361 | 4.1 / 1.7 | 0 / 0 | +| 8 | 1063 / 3336 | 3900 / 5002 | 5.9 / 2.3 | 0 / 0 | +| 32 | 2037 / **9045** | 10335 / **478612** | 6.6 / **0.2** | 0 / **0.21** | +| 64 | 1846 / **93920** | 7570 / 101470 | 10.6 / **0.9** | 0 / 0.04 | + +Result is the **opposite of "on par"**: local Postgres is ~4× slower at C=1 +(round-trips vs in-process SQLite) and **collapses** at C≥32 — p99 of 478s, +0.2/s throughput, 21% write failures, and the materialization backlog never +drains within 300s. + +**Cause:** `db._create_postgres_engine` uses `poolclass=NullPool` — a fresh +connection per request (fine for cloud's per-tenant, low-local-concurrency +model). Under concurrent local writes — plus each background materialization +opening its own connection — that's a connection storm against the default +`max_connections=100`: 25 writes failed in the Postgres phase. MVCC's +concurrent-writer advantage is completely swamped by connection churn. + +**Implication (mirrors the SQLite finding):** the write path needs **resource +bounds** on both axes — bounded background materialization concurrency *and* +connection pooling for Postgres. A pooled, co-located cloud Postgres would look +very different; local NullPool Postgres is not a viable high-concurrency backend +as configured. + +**Local-Postgres bugs found via this benchmark:** +1. *Startup migration crash under uvloop* — `alembic/env.py` applied + `nest_asyncio` under the uvloop policy; the resulting "this event loop is + already running" string evaded the thread-based fallback, crashing `bm mcp` + against Postgres on startup. **Fixed** in `fix(core): run local Postgres + startup migrations under uvloop` (`basic_memory.migration_loop`, 6 unit tests). +2. *No default project for a fresh local Postgres* — `create_memory_project`'s + default lookup raises because the Postgres path uses a cloud-style config + manager that seeds no default and nothing seeds a DB default locally. Worked + around in the harness with `BASIC_MEMORY_DEFAULT_PROJECT`; the product gap + (local Postgres project bootstrap) is left as a finding. + +### 2026-06-24 — bounded materialization worker pool FIXES the wall + +Replaced fire-and-forget `asyncio.create_task` with a config-sized worker pool +(`materialization_workers`, default 4; `_MaterializationWorkerPool`). Re-ran the +C=128 sweep (notes=200): + +| C | no-pool p99 → pool p99 | no-pool thru → pool thru | errors | +| --- | --- | --- | --- | +| 64 | 121771 → **9870** ms (12×) | 1.5 → **11.9**/s (8×) | 2% → **0** | +| 128 | 94954 → **11852** ms (8×) | 2.0 → **10.9**/s (5×) | 1.5% → **0** | + +Throughput now **scales and plateaus** at ~11/s (C=8 10.3 → C=128 10.9) instead +of collapsing; p99 grows gracefully (2.6 → 12s); **zero errors** at every level; +backlog drains fast (`time_to_searchable` 35-98ms). The pooled branch also +**beats main** at high concurrency (main C=128: p50 16.2s / thru 4.6 / vs pool +p50 4.5s / thru 10.9). Bounding the background work is what makes the async write +path actually scale. + +**Worker-count tuning (notes=200, C=64/128):** + +| workers | thru/s (C=64 / C=128) | p99 ms (C=64 / C=128) | +| --- | --- | --- | +| 2 | **15.6 / 16.0** | **7263 / 10517** | +| 4 | 11.9 / 10.9 | 9870 / 11852 | +| 8 | 11.0 / 11.0 | 12307 / 13855 | + +**2 workers is the SQLite sweet spot** — the single writer is the bottleneck, so +fewer workers thrash it less. More workers just contend. Postgres (concurrent +writers, once pooled) should want a higher count — which is exactly why +`materialization_workers` is configurable. Default left at 4 (resilient middle); +tune per backend. + +### 2026-06-24 — Postgres connection pool FIXES the collapse (+ default-project seed) + +Wired `_create_postgres_engine` to the existing `db_pool_size`/`db_pool_overflow`/ +`db_pool_recycle` config (`AsyncAdaptedQueuePool` instead of `NullPool`), and +gated the config's default-project seeding on `skip_initialization_sync` instead +of the Postgres backend, so a fresh local Postgres seeds `main` and no longer +needs the `BASIC_MEMORY_DEFAULT_PROJECT` shim. Re-ran sqlite vs postgres with the +worker pool (4) on both: + +| C | p99: NullPool → pooled (pg) | thru: NullPool → pooled (pg) | err (pg) | +| --- | --- | --- | --- | +| 32 | 478612 → **2021** ms (237×) | 0.2 → **21.9**/s (100×) | 21% → **0** | +| 64 | (p50 93920 → 2043) | 0.9 → **21.5**/s | 4% → **0** | + +Postgres no longer collapses: it scales cleanly to ~21/s with **zero errors** and +without the shim. Full pooled run: + +| C | p50 sql/pg | p99 sql/pg | thru sql/pg (per s) | +| --- | --- | --- | --- | +| 1 | 470 / 133 | 3597 / 597 | 1.4 / 6.1 | +| 8 | 2305 / 468 | 7211 / 1586 | 3.2 / 14.5 | +| 32 | 1204 / 1001 | 4010 / 2021 | 12.6 / 21.9 | +| 64 | 1642 / 2043 | 7346 / 2978 | 11.8 / 21.5 | + +Caveat: this single run's **SQLite was anomalously slow** (C=1 p50 470ms vs +~100ms elsewhere) — machine load from back-to-back container runs. The 3× repeat +below confirms it was noise. + +### 2026-06-24 — 3× repeat (variance bounds): on par at C=1, Postgres scales better + +Ran sqlite vs postgres 3× each, fresh DB per run, notes=100. mean [min-max]: + +| C | SQLite p99 ms | Postgres p99 ms | SQLite thru/s | Postgres thru/s | +| --- | --- | --- | --- | --- | +| 1 | 1310 [1063-1535] | 911 [653-1254] | 5.2 [4.5-6.4] | 6.1 [5.6-6.5] | +| 8 | 3100 [2718-3642] | 1385 [908-2275] | 9.0 [8.3-9.8] | 14.2 [10.0-17.7] | +| 32 | 5226 [4456-6077] | 2463 [2139-2818] | 12.8 [11.3-13.6] | 18.3 [14.9-21.7] | +| 64 | 7487 [5483-10541] | 3488 [2952-4240] | 12.2 [8.6-14.2] | 20.5 [17.7-22.6] | + +(p50 — SQLite: 95/608/1497/2254 ms; Postgres: 117/492/1184/2300 ms at C=1/8/32/64.) + +Findings, now with bounds: +- **Zero failed writes across all 24 cells.** Both backends are stable under load + with the worker pool + (for Postgres) connection pool. No collapse anywhere. +- **The earlier SQLite "470ms" was noise** — clean SQLite C=1 p50 is ~95ms. +- **On par at C=1** (sqlite 95 vs pg 117 ms p50; throughput ranges overlap). +- **Postgres scales better at concurrency, and the ranges separate** — at C=64 + Postgres throughput is ~20.5 vs SQLite ~12.2/s and p99 ~3.5s vs ~7.5s, with no + overlap at C=32/64. The MVCC + connection-pool advantage over SQLite's single + writer is real and reproducible (matches the hypothesis: roughly on par locally, + better headroom on Postgres) — once NullPool is replaced with a real pool. +- SQLite C=64 is the noisiest cell (p99 5.5-10.5s) — single-writer contention is + the most load-sensitive, as expected. + +### 2026-06-24 — controlled 2×2: main (direct writes) vs branch (async writes) + +The three enabling/pool fixes were cherry-picked onto `main` (PR #1018) so `main` +can run a pooled local Postgres, giving a true apples-to-apples comparison. +`main` here = main's **direct/inline** write path (pre-#1002) + the 3 fixes; +`branch` = the #1002 **async** path (deferred materialization + worker pool + +connection pool). All four cells run back-to-back in one session, interleaved per +backend, notes=100. Zero write failures in all 16 cells. + +Concurrency runs down the rows; each metric has a `main` column, a `branch` +column, and the branch's improvement: **faster** = % lower latency +`(main−branch)/main`; **higher** = % more throughput `(branch−main)/main`. +Latency in ms, throughput in writes/s. + +**SQLite — main (direct writes) vs branch (async writes)** + +| C | p50 main | p50 branch | faster | p99 main | p99 branch | faster | thru main | thru branch | higher | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| 1 | 224 | 119 | 47% | 1754 | 1430 | 18% | 3.4 | 4.8 | +41% | +| 8 | 1210 | 645 | 47% | 4934 | 2980 | 40% | 4.9 | 9.3 | +90% | +| 32 | 4944 | 1436 | 71% | 12807 | 4602 | 64% | 4.8 | 12.5 | +160% | +| 64 | 7515 | 1885 | 75% | 13742 | 6656 | 52% | 5.6 | 10.3 | +84% | + +**Postgres — main (direct writes) vs branch (async writes)** + +| C | p50 main | p50 branch | faster | p99 main | p99 branch | faster | thru main | thru branch | higher | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| 1 | 227 | 133 | 41% | 1299 | 743 | 43% | 3.5 | 5.7 | +63% | +| 8 | 971 | 482 | 50% | 1782 | 1150 | 35% | 7.6 | 14.2 | +87% | +| 32 | 2491 | 1323 | 47% | 3367 | 2682 | 20% | 10.8 | 18.2 | +69% | +| 64 | 7002 | 2287 | 67% | 11292 | 3399 | 70% | 7.1 | 18.5 | +160% | + +Conclusions: +- **Branch (async) beats main (direct) on both backends, at every level** — p50 + ~47-75% faster on SQLite / ~41-67% on Postgres; p99 up to ~64%/70% faster; + throughput up to ~160% higher. +- **main plateaus; branch scales.** main throughput tops out (~5/s SQLite, ~10/s + Postgres) and on Postgres *drops* at C=64 (10.8→7.1); branch climbs and holds + (10-18/s). +- **Postgres > SQLite for both, gap wider for the branch** (branch C=64: 18.5 vs + 10.3 /s) — the async pool exploits concurrent writers SQLite's single writer + can't offer. +- **Supersedes the earlier "+21ms at C=1" note** — that was a cross-run artifact; + in a controlled interleaved run the branch is faster at C=1 too (it defers the + file write off the accept). + +### 2026-06-24 — embedding keeps pace; materialization is the dominant stage + +Added `time_to_embedded_ms` (branch, SQLite, notes=100). Embedding is the +heaviest background follow-up (fastembed ONNX per note), so it was the prime +suspect for the *next* backlog after materialization — but it isn't: + +| C | t_materialized | t_searchable | t_embedded | +| --- | --- | --- | --- | +| 1 | 1325 ms | 22 ms | 1 ms | +| 8 | 6095 ms | 48 ms | 1 ms | +| 32 | 8126 ms | 47 ms | 2 ms | +| 64 | 9108 ms | 42 ms | 3 ms | + +`time_to_embedded` stays ~1-3ms while `time_to_materialized` climbs to ~9s — each +note embeds right after it's indexed and finishes well within the materialization +drain. So **materialization (file write + index) is the dominant async stage**, +which is exactly what the worker pool bounds; the local embedding pipeline keeps +up and needs no separate throttling yet. Caveat: this is the *local* fastembed +model — a remote embedding provider (network round-trips, small sync batch) would +likely flip this and make embedding the bottleneck. + +### 2026-06-24 — SQLite PRAGMA tuning: no measurable gain (we're at the work limit) + +Made the SQLite PRAGMAs tunable (`sqlite_synchronous`, `sqlite_mmap_size`, +`sqlite_wal_autocheckpoint`, `sqlite_page_size`) and swept them. 3× at C=32, +notes=100: + +| config | throughput/s (mean [min-max]) | accept p50 | +| --- | --- | --- | +| baseline (synchronous=NORMAL) | 17.6 [12.5-23.1] | 985 ms | +| synchronous=OFF | 18.6 [8.8-28.4] | 1382 ms | +| OFF + mmap=256MB | 19.4 [9.5-24.9] | 954 ms | + +Means trend up ~10% but the ranges overlap almost entirely (±50% run-to-run +variance), so there's **no reliable gain**: +- **`synchronous=OFF` buys nothing** — WAL + NORMAL already skips the per-commit + fsync, so OFF only saves checkpoint fsyncs, which don't bind here. Default kept + at **NORMAL** (durable for free); OFF stays as a knob for callers that knowingly + trade durability (the DB rebuilds from files via sync). +- **`mmap`/`wal_autocheckpoint`/`page_size`**: no measurable effect at this workload. +- **Group-commit** (batch materializations into one transaction) was considered + and **skipped**: it needs restructuring the per-note materialize→write→publish→ + index stack onto a shared transaction (hairy), and with no per-commit fsync to + save its payoff is small. + +**Conclusion — the practical SQLite ceiling here is *work*, not the engine.** At +~17-19 notes/s we're far below SQLite's raw write speed; we are not commit-, +fsync-, or read-IO-bound, so no PRAGMA helps. The cost per note is the markdown +parse → entity/observation/relation upserts → FTS index → file write → and the +watcher's `refresh_unchanged_derived_state` re-index of already-materialized files +(intentional, constrained by permalink resolution). Further speed would come from +reducing that per-note work, not from database tuning. + +## Open questions / next steps + +**Done:** worker pool, Postgres connection pool, local-Postgres default-project +seed, the migration-under-uvloop fix (`basic_memory.migration_loop`), the +controlled main-vs-branch 2×2, `time_to_embedded_ms` (embedding keeps pace +locally), and the SQLite PRAGMA sweep (no gain; we're at the per-note-work limit). +The 3 enabling fixes are PR #1018 to main; the PRAGMA knobs + worker pool are on +the #1002 branch. + +**Remaining harness/measurement work:** +- Decide whether to fold the driver into `bm-bench run write-load` (CLI subcommand). + +**Out of scope (by decision):** remote embedding providers. Local fastembed keeps +pace with the write path (above), so it's the path we optimize for. A remote +provider (network round-trips, small sync batch) would make embedding the +bottleneck, but that's a caveat-emptor tradeoff a user opts into — not something +the write path is tuned for, so no bounded embed pool is planned. diff --git a/benchmarks/justfile b/benchmarks/justfile new file mode 100644 index 000000000..5ac841e47 --- /dev/null +++ b/benchmarks/justfile @@ -0,0 +1,301 @@ +# basic-memory-benchmarks command runner + +set dotenv-load := true + +# --- Paths and defaults --- + +bm_local_path := env_var_or_default("BM_LOCAL_PATH", "") +bm_local_path_flag := if bm_local_path != "" { "--bm-local-path " + bm_local_path } else { "" } +locomo_dataset_path := "benchmarks/datasets/locomo/locomo10.json" +locomo_output_dir := "benchmarks/generated/locomo" +locomo_c1_output_dir := "benchmarks/generated/locomo-c1" +longmemeval_dataset_path := "benchmarks/datasets/longmemeval/longmemeval_s.json" +longmemeval_output_dir := "benchmarks/generated/longmemeval-s" +longmemeval_dev_output_dir := "benchmarks/generated/longmemeval-s-dev" + +# --- Repo maintenance --- + +sync: + uv sync --group dev + +sync-judge: + uv sync --group dev --extra judge + +test: + uv run pytest -q + +lint: + uv run ruff check . + +format: + uv run ruff format . + +typecheck: + uv run pyright + +check: lint typecheck test + +# --- Dataset prep --- + +bench-fetch-locomo: + uv run bm-bench datasets fetch --dataset locomo --output {{locomo_dataset_path}} + +bench-convert-locomo: + uv run bm-bench convert locomo --dataset-path {{locomo_dataset_path}} --output-dir {{locomo_output_dir}} + +bench-convert-locomo-c1: + uv run bm-bench convert locomo --dataset-path {{locomo_dataset_path}} --output-dir {{locomo_c1_output_dir}} --max-conversations 1 + +bench-make-quick25: + uv run python -c 'import json; from pathlib import Path; queries_path=Path("benchmarks/generated/locomo-c1/queries.json"); quick_path=Path("benchmarks/generated/locomo-c1/queries.quick25.json"); queries=json.loads(queries_path.read_text()); quick_path.write_text(json.dumps(queries[:25], indent=2)+"\n"); print(f"Wrote {len(queries[:25])} queries to {quick_path}")' + +bench-prepare-short: bench-fetch-locomo bench-convert-locomo-c1 bench-make-quick25 + +bench-prepare-long: bench-fetch-locomo bench-convert-locomo + +bench-fetch-longmemeval: + uv run bm-bench datasets fetch --dataset longmemeval-s --output {{longmemeval_dataset_path}} + +bench-convert-longmemeval: + uv run bm-bench convert longmemeval --dataset-path {{longmemeval_dataset_path}} --output-dir {{longmemeval_output_dir}} + +# Dev slice: first 25 questions for fast iteration +bench-convert-longmemeval-dev: + uv run bm-bench convert longmemeval --dataset-path {{longmemeval_dataset_path}} --output-dir {{longmemeval_dev_output_dir}} --max-questions 25 + +bench-prepare-longmemeval: bench-fetch-longmemeval bench-convert-longmemeval + +bench-fetch-convomem: + uv run bm-bench datasets fetch --dataset convomem --context-sizes 10,30 + +bench-convert-convomem: + uv run bm-bench convert convomem --sample-per-stratum 25 --seed 42 + +bench-prepare-convomem: bench-fetch-convomem bench-convert-convomem + +bench-fetch-locomo-audit: + uv run bm-bench datasets fetch --dataset locomo-audit + +bench-convert-locomo-corrected: bench-fetch-locomo bench-fetch-locomo-audit + uv run bm-bench convert locomo --dataset-path {{locomo_dataset_path}} --output-dir benchmarks/generated/locomo-corrected --audit-corrections benchmarks/datasets/locomo-audit/corrections.json + +# Grouped retrieval over the LongMemEval-S dev slice (bm-local only) +bench-run-longmemeval-dev: + uv run bm-bench run retrieval \ + --dataset-id longmemeval_s \ + --dataset-path {{longmemeval_dataset_path}} \ + --corpus-dir {{longmemeval_dev_output_dir}}/groups \ + --queries-path {{longmemeval_dev_output_dir}}/queries.json \ + --providers bm-local \ + {{bm_local_path_flag}} \ + --strict-providers + +# Grouped retrieval over full LongMemEval-S (slow: 500 isolated group corpora) +bench-run-longmemeval: + uv run bm-bench run retrieval \ + --dataset-id longmemeval_s \ + --dataset-path {{longmemeval_dataset_path}} \ + --corpus-dir {{longmemeval_output_dir}}/groups \ + --queries-path {{longmemeval_output_dir}}/queries.json \ + --providers bm-local,mem0-local \ + {{bm_local_path_flag}} \ + --allow-provider-skip + +# --- One-command pipelines --- + +# Full retrieval benchmark pipeline: +# 1) sync deps, 2) fetch+convert long dataset, 3) run full retrieval +bench-full: + just sync + just bench-prepare-long + just bench-run-full + +# Full retrieval + judge pipeline: +# 1) sync deps (+judge extras), 2) fetch+convert long dataset, 3) run full with judge +bench-full-judge model="gpt-4o-mini": + just sync-judge + just bench-prepare-long + just bench-run-full-judge model="{{model}}" + +# --- Benchmark execution --- + +bench-smoke: + uv run bm-bench run retrieval \ + --dataset-id synthetic \ + --dataset-path benchmarks/synthetic/queries.json \ + --corpus-dir benchmarks/synthetic/docs \ + --queries-path benchmarks/synthetic/queries.json \ + --providers bm-local,mem0-local \ + --allow-provider-skip + +# Short benchmark: one-conversation LoCoMo slice + 25-query quickset +bench-run-short: + uv run bm-bench run retrieval \ + --dataset-id locomo-c1-quick25 \ + --dataset-path {{locomo_dataset_path}} \ + --corpus-dir benchmarks/generated/locomo-c1/docs \ + --queries-path benchmarks/generated/locomo-c1/queries.quick25.json \ + --providers bm-local,mem0-local \ + {{bm_local_path_flag}} \ + --allow-provider-skip + +bench-run-short-strict: + uv run bm-bench run retrieval \ + --dataset-id locomo-c1-quick25 \ + --dataset-path {{locomo_dataset_path}} \ + --corpus-dir benchmarks/generated/locomo-c1/docs \ + --queries-path benchmarks/generated/locomo-c1/queries.quick25.json \ + --providers bm-local,mem0-local \ + {{bm_local_path_flag}} \ + --strict-providers + +# Long benchmark: full LoCoMo query set +bench-run-long: + uv run bm-bench run retrieval \ + --dataset-id locomo \ + --dataset-path {{locomo_dataset_path}} \ + --corpus-dir benchmarks/generated/locomo/docs \ + --queries-path benchmarks/generated/locomo/queries.json \ + --providers bm-local,mem0-local \ + {{bm_local_path_flag}} \ + --allow-provider-skip + +bench-run-long-strict: + uv run bm-bench run retrieval \ + --dataset-id locomo \ + --dataset-path {{locomo_dataset_path}} \ + --corpus-dir benchmarks/generated/locomo/docs \ + --queries-path benchmarks/generated/locomo/queries.json \ + --providers bm-local,mem0-local \ + {{bm_local_path_flag}} \ + --strict-providers + +bench-run-bm-local: + uv run bm-bench run retrieval \ + --providers bm-local \ + --dataset-id locomo \ + --dataset-path {{locomo_dataset_path}} \ + --corpus-dir benchmarks/generated/locomo/docs \ + --queries-path benchmarks/generated/locomo/queries.json \ + {{bm_local_path_flag}} + +bench-run-mem0-local: + uv run bm-bench run retrieval \ + --providers mem0-local \ + --dataset-id locomo \ + --dataset-path {{locomo_dataset_path}} \ + --corpus-dir benchmarks/generated/locomo/docs \ + --queries-path benchmarks/generated/locomo/queries.json \ + --allow-provider-skip + +bench-run-full: + uv run bm-bench run full \ + --dataset-id locomo \ + --dataset-path {{locomo_dataset_path}} \ + --corpus-dir benchmarks/generated/locomo/docs \ + --queries-path benchmarks/generated/locomo/queries.json \ + --providers bm-local,mem0-local \ + {{bm_local_path_flag}} \ + --allow-provider-skip + +bench-run-full-judge model="gpt-4o-mini": + uv run bm-bench run full \ + --dataset-id locomo \ + --dataset-path {{locomo_dataset_path}} \ + --corpus-dir benchmarks/generated/locomo/docs \ + --queries-path benchmarks/generated/locomo/queries.json \ + --providers bm-local,mem0-local \ + {{bm_local_path_flag}} \ + --allow-provider-skip \ + --judge \ + --judge-model "{{model}}" + +# --- Artifacts and comparison --- + +bench-latest-run: + #!/usr/bin/env bash + set -euo pipefail + ls -1dt benchmarks/runs/* | head -n 1 + +bench-judge run_dir model="gpt-4o-mini": + uv run bm-bench run judge --run-dir "{{run_dir}}" --model "{{model}}" + +bench-validate run_dir: + uv run bm-bench validate-artifacts --run-dir "{{run_dir}}" + +bench-publish run_dir destination="benchmarks/results/public": + uv run bm-bench publish --run-dir "{{run_dir}}" --destination "{{destination}}" + +bench-compare baseline candidate provider="bm-local" metric="recall_at_5": + uv run bm-bench compare "{{baseline}}" "{{candidate}}" --provider "{{provider}}" --metric "{{metric}}" + +# --- Write-path load benchmark (per-ref venv; see docs/write-load-benchmark.md) --- + +# Build (or refresh) a venv with basic-memory@ installed from GitHub. +# ref may be a branch, tag, or SHA. Slashes are slugified for the venv dir. +bench-venv ref: + #!/usr/bin/env bash + set -euo pipefail + slug=$(echo "{{ref}}" | tr '/:' '__') + uv venv ".venvs/$slug" --python 3.12 + uv pip install --python ".venvs/$slug/bin/python" \ + "basic-memory @ git+https://github.com/basicmachines-co/basic-memory@{{ref}}" + echo "installed $(".venvs/$slug/bin/basic-memory" --version) into .venvs/$slug" + +# Run the write-load concurrency sweep against a ref's venv -> .scratch/.jsonl +bench-write-load ref notes="60" concurrency="1,4,8,16,32" drain="120": + #!/usr/bin/env bash + set -euo pipefail + slug=$(echo "{{ref}}" | tr '/:' '__') + test -x ".venvs/$slug/bin/basic-memory" || { echo "no venv for {{ref}}; run: just bench-venv {{ref}}"; exit 1; } + uv run --no-project --with 'mcp>=1.23.1' python scripts/write_load_bench.py \ + --bm-command "$(pwd)/.venvs/$slug/bin/basic-memory" \ + --label "{{ref}}" --notes {{notes}} --concurrency "{{concurrency}}" --warmup 8 \ + --scratch ".scratch/write-load-$slug" \ + --output ".scratch/write-load-$slug.jsonl" --truncate \ + --drain-timeout {{drain}} + +# Scaling sweep: push concurrency high to find where SQLite's single-writer lock +# chokes inline writes (main) while async writes (branch) keep scaling. +bench-write-scale ref notes="200" concurrency="1,2,4,8,16,32,64,128" drain="300": + just bench-write-load "{{ref}}" "{{notes}}" "{{concurrency}}" "{{drain}}" + +# Compare ONE ref's write path on SQLite vs Postgres (async writes on both). +# Postgres runs in a throwaway testcontainer (needs Docker). -> markdown table. +bench-write-backend ref="codex/repository-explicit-sessions" notes="150" concurrency="1,8,32,64" drain="300": + #!/usr/bin/env bash + set -euo pipefail + slug=$(echo "{{ref}}" | tr '/:' '__') + test -x ".venvs/$slug/bin/basic-memory" || { echo "no venv for {{ref}}; run: just bench-venv {{ref}}"; exit 1; } + for backend in sqlite postgres; do + echo "=== $backend ===" + uv run --no-project --with 'mcp>=1.23.1' --with 'testcontainers[postgres]>=4.0.0' \ + python scripts/write_load_bench.py \ + --bm-command "$(pwd)/.venvs/$slug/bin/basic-memory" \ + --backend "$backend" --label "$backend" \ + --notes {{notes}} --concurrency "{{concurrency}}" --warmup 8 \ + --scratch ".scratch/backend-$backend" \ + --output ".scratch/backend-$backend.jsonl" --truncate \ + --drain-timeout {{drain}} + done + uv run --no-project python ../test-int/compare_search_benchmarks.py \ + ".scratch/backend-sqlite.jsonl" ".scratch/backend-postgres.jsonl" --format markdown + +# Diff two refs' write-load JSONL outputs (markdown table for PRs) +bench-write-compare refA refB: + #!/usr/bin/env bash + set -euo pipefail + slugA=$(echo "{{refA}}" | tr '/:' '__'); slugB=$(echo "{{refB}}" | tr '/:' '__') + uv run --no-project python ../test-int/compare_search_benchmarks.py \ + ".scratch/write-load-$slugA.jsonl" ".scratch/write-load-$slugB.jsonl" --format markdown + +# One-shot: build both venvs, run both sweeps, print the comparison +bench-write-matrix refA="main" refB="codex/repository-explicit-sessions": + just bench-venv "{{refA}}" + just bench-venv "{{refB}}" + just bench-write-load "{{refA}}" + just bench-write-load "{{refB}}" + just bench-write-compare "{{refA}}" "{{refB}}" + +default: + @just --list diff --git a/benchmarks/pyproject.toml b/benchmarks/pyproject.toml new file mode 100644 index 000000000..f206ef66e --- /dev/null +++ b/benchmarks/pyproject.toml @@ -0,0 +1,57 @@ +[project] +name = "basic-memory-benchmarks" +version = "0.1.0" +description = "Reproducible benchmark suite for Basic Memory and competitor memory systems" +readme = "README.md" +authors = [ + { name = "Basic Machines", email = "hello@basic-machines.co" } +] +requires-python = ">=3.12" +dependencies = [ + "basic-memory>=0.20.2", + "httpx>=0.28.0", + "mcp>=1.23.1", + "mem0ai[nlp]==2.0.5", + "pydantic>=2.12.0", + "python-frontmatter>=1.1.0", + "rich>=13.9.4", + "typer>=0.16.1", +] + +[project.optional-dependencies] +judge = [ + "pydantic-evals>=0.4.0", +] + +[project.scripts] +bm-bench = "basic_memory_benchmarks.cli:main" +basic-memory-benchmarks = "basic_memory_benchmarks.cli:main" + +[build-system] +requires = ["uv_build>=0.9.28,<0.10.0"] +build-backend = "uv_build" + +[dependency-groups] +dev = [ + "pyright>=1.1.408", + "pytest>=8.4.1", + "pytest-asyncio>=1.2.0", + "ruff>=0.13.0", +] + +[tool.pytest.ini_options] +pythonpath = ["src"] +addopts = "-q" +testpaths = ["tests", "test-int"] +markers = [ + "integration: Integration tests", + "slow: Slow tests", +] + +[tool.ruff] +line-length = 100 +target-version = "py312" + +[tool.pyright] +include = ["src", "tests", "test-int"] +pythonVersion = "3.12" diff --git a/benchmarks/scripts/write_load_bench.py b/benchmarks/scripts/write_load_bench.py new file mode 100644 index 000000000..ba71a6985 --- /dev/null +++ b/benchmarks/scripts/write_load_bench.py @@ -0,0 +1,448 @@ +#!/usr/bin/env python3 +"""Write-path load benchmark for Basic Memory. + +Drives `write_note` over a warm `bm mcp` stdio session at increasing +concurrency and measures caller-perceived accept latency, throughput, error +rate, and how long the background follow-up work takes to make the writes +visible (materialized on disk, then FTS-searchable). + +The whole point is branch/SHA comparison: point `--bm-command` at a venv that +has a specific `basic-memory` ref installed; the harness is otherwise fixed. + +Output: one JSONL record per concurrency level, in the generic compare format +(`{"benchmark", "metrics", "timestamp_utc"}`), so two runs diff with +`test-int/compare_search_benchmarks.py`. +""" + +from __future__ import annotations + +import argparse +import asyncio +import json +import os +import shutil +import sqlite3 +import sys +import time +from datetime import datetime, timezone +from pathlib import Path + +from mcp.client.session import ClientSession +from mcp.client.stdio import StdioServerParameters, stdio_client +from mcp.types import CallToolResult + + +# --- Synthetic corpus ------------------------------------------------------ + +_TOPICS = [ + "coffee", + "sync", + "auth", + "search", + "indexing", + "cloud", + "markdown", + "embeddings", +] + + +def synthetic_note(level: int, index: int) -> dict[str, str]: + """A deterministic note with frontmatter, observations, and a relation. + + Relations reference sibling notes so the runtime's relation-resolution + follow-up has real work to do (part of the async path we measure). + """ + topic = _TOPICS[index % len(_TOPICS)] + title = f"load-c{level}-note-{index:05d}" + prev = f"load-c{level}-note-{(index - 1) % 1:05d}" if index else title + content = ( + f"# {title}\n\n" + f"Synthetic write-load note about {topic}. " + f"It exists to exercise the accepted-note write path under concurrency.\n\n" + "## Observations\n" + f"- [fact] {topic} note number {index} in burst {level} #load\n" + f"- [detail] repeated domain term {topic} {topic} {topic} for indexing #benchmark\n\n" + "## Relations\n" + f"- relates_to [[load-c{level}-note-{max(index - 1, 0):05d}]]\n" + ) + _ = prev + return {"title": title, "directory": f"load/c{level}", "content": content} + + +# --- Metric helpers -------------------------------------------------------- + + +def percentile(values: list[float], pct: float) -> float: + if not values: + return 0.0 + ordered = sorted(values) + if len(ordered) == 1: + return ordered[0] + rank = (pct / 100.0) * (len(ordered) - 1) + low = int(rank) + high = min(low + 1, len(ordered) - 1) + frac = rank - low + return ordered[low] + (ordered[high] - ordered[low]) * frac + + +def emit(output_path: Path | None, record: dict) -> None: + line = json.dumps(record) + print(line, flush=True) + if output_path is not None: + output_path.parent.mkdir(parents=True, exist_ok=True) + with output_path.open("a", encoding="utf-8") as handle: + handle.write(line + "\n") + + +# --- MCP plumbing ---------------------------------------------------------- + + +def _isolated_env(config_dir: Path) -> dict[str, str]: + """Fresh runtime: scoped config/DB, and NOT test mode. + + BASIC_MEMORY_ENV must not be `test` (test mode disables the watcher and the + background schedulers we are trying to measure). We pop it so an inherited + value can't sneak the server into test mode. + """ + env = dict(os.environ) + for key in ("BASIC_MEMORY_ENV", "BASIC_MEMORY_CLOUD_MODE", "BASIC_MEMORY_HOME"): + env.pop(key, None) + env["BASIC_MEMORY_CONFIG_DIR"] = str(config_dir) + return env + + +def _start_postgres(): + """Start a throwaway Postgres testcontainer for a --backend postgres run. + + Deferred import on purpose: testcontainers (and its Ryuk reaper image) are + only needed for the Postgres comparison, so the default SQLite path runs + without it installed. The recipe adds it via `uv run --with`. + """ + from testcontainers.postgres import PostgresContainer + + # pgvector image (matches test-int/conftest.py): Postgres search-index init + # runs CREATE EXTENSION vector, which the stock postgres image lacks. + container = PostgresContainer("pgvector/pgvector:pg16") + container.start() + return container + + +def _asyncpg_url(container) -> str: + """Normalize the testcontainer's connection URL to a SQLAlchemy asyncpg URL. + + testcontainers may emit psycopg2/psycopg/bare-postgresql URLs depending on + version; basic-memory's Postgres backend expects `postgresql+asyncpg://`. + """ + url = container.get_connection_url() + for sync_driver in ("postgresql+psycopg2", "postgresql+psycopg", "postgresql"): + prefix = sync_driver + "://" + if url.startswith(prefix): + return "postgresql+asyncpg://" + url[len(prefix) :] + return url + + +def _result_payload(result: CallToolResult) -> dict: + structured = result.structuredContent + if isinstance(structured, dict): + wrapped = structured.get("result") + return wrapped if isinstance(wrapped, dict) else structured + for item in result.content: + text = getattr(item, "text", None) + if isinstance(text, str) and text.strip(): + try: + parsed = json.loads(text) + except json.JSONDecodeError: + continue + if isinstance(parsed, dict): + return parsed + return {} + + +# --- Workload -------------------------------------------------------------- + + +async def write_burst( + session: ClientSession, + *, + project: str, + level: int, + count: int, + concurrency: int, +) -> tuple[list[float], int]: + """Fire `count` write_note calls with at most `concurrency` in flight.""" + sem = asyncio.Semaphore(concurrency) + latencies: list[float] = [] + errors = 0 + + async def one(index: int) -> None: + nonlocal errors + note = synthetic_note(level, index) + async with sem: + start = time.perf_counter() + try: + result = await session.call_tool( + "write_note", + { + "project": project, + "title": note["title"], + "directory": note["directory"], + "content": note["content"], + "output_format": "json", + }, + ) + failed = bool(result.isError) + except Exception: + failed = True + elapsed_ms = (time.perf_counter() - start) * 1000.0 + latencies.append(elapsed_ms) + if failed: + errors += 1 + + await asyncio.gather(*(one(index) for index in range(count))) + return latencies, errors + + +async def poll_until(predicate, *, timeout_s: float, interval_s: float = 0.1) -> float | None: + """Return ms elapsed when predicate() is true, or None on timeout.""" + start = time.perf_counter() + delay = interval_s + while True: + if await predicate(): + return (time.perf_counter() - start) * 1000.0 + if time.perf_counter() - start >= timeout_s: + return None + await asyncio.sleep(delay) + delay = min(delay * 1.5, 1.0) + + +async def searchable_count(session: ClientSession, project: str, level: int) -> int: + result = await session.call_tool( + "search_notes", + { + "project": project, + "query": f"burst {level}", + "search_type": "text", + "page": 1, + "page_size": 1, + "output_format": "json", + }, + ) + payload = _result_payload(result) + # FTS path reports an exact total; fall back to len(results). + total = payload.get("total") + if isinstance(total, int) and total > 0: + return total + results = payload.get("results") + return len(results) if isinstance(results, list) else 0 + + +def embedded_count(db_path: Path, level: int) -> int: + """Count distinct embedded entities for a burst level (SQLite vector store). + + The embedding (vector-sync) follow-up is the heaviest background stage + (fastembed runs an ONNX model per note); this lets the driver measure how + long after a write a note becomes *semantically* searchable, the same way + searchable_count tracks FTS. `search_vector_chunks` is a regular table + (entity_id, project_id) so a plain read-only sqlite3 connection can read it; + join to `entity` and filter by path so only THIS benchmark's notes count + (other local projects in the shared app DB don't inflate it). Returns 0 when + the DB/table isn't there yet (semantic disabled, or not initialized). + """ + if not db_path.exists(): + return 0 + try: + con = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True, timeout=1.0) + try: + row = con.execute( + "SELECT COUNT(DISTINCT v.entity_id) FROM search_vector_chunks v " + "JOIN entity e ON e.id = v.entity_id WHERE e.file_path LIKE ?", + (f"load/c{level}/%",), + ).fetchone() + return int(row[0]) if row and row[0] is not None else 0 + finally: + con.close() + except sqlite3.Error: + return 0 + + +async def run(args: argparse.Namespace) -> int: + scratch = Path(args.scratch).resolve() + config_dir = scratch / "config" + project_dir = scratch / "project" + main_home = scratch / "main-home" + for path in (config_dir, project_dir, main_home): + if path.exists(): + shutil.rmtree(path) + path.mkdir(parents=True, exist_ok=True) + db_path = config_dir / "memory.db" # SQLite app DB (for the embedding peek) + + # --- Backend selection --- + # Trigger: --backend postgres. Why: compare the async write path on SQLite + # (single-writer lock) vs Postgres (MVCC, concurrent writers). Outcome: the + # bm server connects to a throwaway Postgres testcontainer instead of the + # scoped SQLite file; the workload is otherwise identical. + pg_container = ( + _start_postgres() if args.backend == "postgres" and not args.database_url else None + ) + database_url = args.database_url or (_asyncpg_url(pg_container) if pg_container else None) + + project = "writeload" + env = _isolated_env(config_dir) + # Isolate the auto-seeded "main" project to an empty dir so the index + + # embedding pipeline only processes THIS benchmark's notes — not the user's + # real ~/basic-memory — keeping time_to_embedded (and the workers) clean. + env["BASIC_MEMORY_HOME"] = str(main_home) + if args.backend == "postgres": + assert database_url is not None + env["BASIC_MEMORY_DATABASE_BACKEND"] = "postgres" + env["BASIC_MEMORY_DATABASE_URL"] = database_url + params = StdioServerParameters(command=args.bm_command, args=["mcp"], env=env) + output_path = Path(args.output).resolve() if args.output else None + if output_path and output_path.exists() and args.truncate: + output_path.unlink() + + levels = [int(c) for c in args.concurrency.split(",") if c.strip()] + + async with stdio_client(params) as (read_stream, write_stream): + async with ClientSession(read_stream, write_stream) as session: + await session.initialize() + + create = await session.call_tool( + "create_memory_project", + {"project_name": project, "project_path": str(project_dir)}, + ) + if create.isError: + print("ERROR: could not create project", file=sys.stderr) + return 1 + + # Warmup: pay one-time costs (embedding-model download/load, index + # init, cache warm) OUTSIDE measurement. Wait for the warmup writes + # to become searchable so the model is fully loaded before level 1. + await write_burst(session, project=project, level=0, count=args.warmup, concurrency=1) + + async def warmup_ready() -> bool: + return await searchable_count(session, project, 0) >= args.warmup + + await poll_until(warmup_ready, timeout_s=args.drain_timeout) + + # Wait for the warmup notes to EMBED so the fastembed model is loaded + # before we measure time_to_embedded. The short timeout doubles as a + # semantic-enabled probe: if nothing embeds, skip the stage rather + # than hang every level on a 0-count poll. SQLite only — the peek + # reads the local sqlite-vec table; Postgres stores vectors elsewhere. + embedded_enabled = False + if args.backend == "sqlite": + + async def warmup_embedded() -> bool: + return embedded_count(db_path, 0) >= args.warmup + + warm_embed_ms = await poll_until( + warmup_embedded, timeout_s=min(args.drain_timeout, 120.0) + ) + embedded_enabled = warm_embed_ms is not None + + for level in levels: + wall_start = time.perf_counter() + latencies, errors = await write_burst( + session, + project=project, + level=level, + count=args.notes, + concurrency=level, + ) + wall_s = time.perf_counter() - wall_start + throughput = args.notes / wall_s if wall_s > 0 else 0.0 + + level_dir = project_dir / "load" / f"c{level}" + + async def files_ready() -> bool: + return level_dir.exists() and len(list(level_dir.glob("*.md"))) >= args.notes + + async def search_ready() -> bool: + return await searchable_count(session, project, level) >= args.notes + + materialized_ms = await poll_until(files_ready, timeout_s=args.drain_timeout) + searchable_ms = await poll_until(search_ready, timeout_s=args.drain_timeout) + + # Embedding (vector-sync) drain — only when the warmup probe + # confirmed embeddings land (semantic on, SQLite backend). + embedded_ms = None + if embedded_enabled: + + async def embedded_ready() -> bool: + return embedded_count(db_path, level) >= args.notes + + embedded_ms = await poll_until(embedded_ready, timeout_s=args.drain_timeout) + + record = { + "benchmark": f"write-load c={level}", + "timestamp_utc": datetime.now(timezone.utc).isoformat(), + "label": args.label, + "metrics": { + "concurrency": level, + "notes_written": args.notes, + "accept_latency_p50_ms": round(percentile(latencies, 50), 3), + "accept_latency_p95_ms": round(percentile(latencies, 95), 3), + "accept_latency_p99_ms": round(percentile(latencies, 99), 3), + "accept_latency_max_ms": round(max(latencies), 3) if latencies else 0.0, + "accept_throughput_per_sec": round(throughput, 3), + "accept_error_rate": round(errors / args.notes, 4), + "time_to_materialized_ms": round(materialized_ms, 1) + if materialized_ms is not None + else -1.0, + "time_to_searchable_ms": round(searchable_ms, 1) + if searchable_ms is not None + else -1.0, + "time_to_embedded_ms": round(embedded_ms, 1) + if embedded_ms is not None + else -1.0, + }, + } + emit(output_path, record) + + if pg_container is not None: + # Error paths (e.g. project-create failure) fall back to testcontainers' + # Ryuk reaper, which removes the container when this process exits. + pg_container.stop() + return 0 + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--bm-command", + required=True, + help="Path to the basic-memory executable in the per-ref venv", + ) + parser.add_argument("--label", default="ref", help="Ref label recorded in each row") + parser.add_argument("--notes", type=int, default=200, help="Writes per concurrency level") + parser.add_argument("--warmup", type=int, default=5, help="Warmup writes (not measured)") + parser.add_argument( + "--concurrency", default="1,4,8,16,32", help="Comma-separated concurrency levels" + ) + parser.add_argument( + "--backend", + choices=("sqlite", "postgres"), + default="sqlite", + help="DB backend the bm server runs against (postgres needs Docker + testcontainers)", + ) + parser.add_argument( + "--database-url", + default=None, + help="Postgres URL (postgresql+asyncpg://...). If omitted with --backend postgres, " + "a throwaway Postgres testcontainer is started for the run.", + ) + parser.add_argument( + "--scratch", default=".scratch/write-load", help="Scratch dir for config + project" + ) + parser.add_argument("--output", default=None, help="JSONL output path (also printed)") + parser.add_argument("--truncate", action="store_true", help="Truncate output before writing") + parser.add_argument( + "--drain-timeout", type=float, default=120.0, help="Seconds to wait for visibility" + ) + args = parser.parse_args() + return asyncio.run(run(args)) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/benchmarks/src/basic_memory_benchmarks/__init__.py b/benchmarks/src/basic_memory_benchmarks/__init__.py new file mode 100644 index 000000000..a73c32ca7 --- /dev/null +++ b/benchmarks/src/basic_memory_benchmarks/__init__.py @@ -0,0 +1,5 @@ +"""basic-memory-benchmarks package.""" + +from basic_memory_benchmarks.cli import main + +__all__ = ["main"] diff --git a/benchmarks/src/basic_memory_benchmarks/__main__.py b/benchmarks/src/basic_memory_benchmarks/__main__.py new file mode 100644 index 000000000..73806bcce --- /dev/null +++ b/benchmarks/src/basic_memory_benchmarks/__main__.py @@ -0,0 +1,3 @@ +from basic_memory_benchmarks.cli import main + +main() diff --git a/benchmarks/src/basic_memory_benchmarks/cli.py b/benchmarks/src/basic_memory_benchmarks/cli.py new file mode 100644 index 000000000..493f120a2 --- /dev/null +++ b/benchmarks/src/basic_memory_benchmarks/cli.py @@ -0,0 +1,469 @@ +"""CLI entrypoint for benchmark operations.""" + +from __future__ import annotations + +import shutil +import uuid +from pathlib import Path + +import typer +from rich.console import Console + +from basic_memory_benchmarks.converters.locomo_to_corpus import convert_locomo_to_corpus +from basic_memory_benchmarks.converters.longmemeval_to_corpus import convert_longmemeval_to_corpus +from basic_memory_benchmarks.datasets.locomo import LOCOMO_URL, fetch_locomo_dataset +from basic_memory_benchmarks.converters.convomem_to_corpus import convert_convomem_to_corpus +from basic_memory_benchmarks.datasets.convomem import fetch_convomem_batches +from basic_memory_benchmarks.datasets.locomo_audit import fetch_locomo_audit_corrections +from basic_memory_benchmarks.datasets.longmemeval import ( + LONGMEMEVAL_S_URL, + fetch_longmemeval_dataset, +) +from basic_memory_benchmarks.models import DatasetProvenance, RunConfig +from basic_memory_benchmarks.reporting.compare import ( + compare_provider_metric, + load_retrieval_summary, +) +from basic_memory_benchmarks.runner import ( + run_diagnose_stage, + run_judge, + run_qa_stage, + run_rejudge_stage, + run_review_stage, + run_retrieval, +) +from basic_memory_benchmarks.utils import sha256_file + +app = typer.Typer(help="Basic Memory benchmark suite") +console = Console() + +datasets_app = typer.Typer(help="Dataset management commands") +convert_app = typer.Typer(help="Dataset conversion commands") +run_app = typer.Typer(help="Benchmark execution commands") + +app.add_typer(datasets_app, name="datasets") +app.add_typer(convert_app, name="convert") +app.add_typer(run_app, name="run") + + +@datasets_app.command("fetch") +def datasets_fetch( + dataset: str = typer.Option("locomo", "--dataset"), + output: Path | None = typer.Option(None, "--output"), + url: str | None = typer.Option(None, "--url"), + context_sizes: str = typer.Option( + "10,30", "--context-sizes", help="convomem only: batch context sizes to download" + ), +) -> None: + if dataset == "locomo": + resolved_output = output or Path("benchmarks/datasets/locomo/locomo10.json") + provenance = fetch_locomo_dataset(output_path=resolved_output, url=url or LOCOMO_URL) + elif dataset == "longmemeval-s": + resolved_output = output or Path("benchmarks/datasets/longmemeval/longmemeval_s.json") + provenance = fetch_longmemeval_dataset( + output_path=resolved_output, url=url or LONGMEMEVAL_S_URL + ) + elif dataset == "locomo-audit": + resolved_output = output or Path("benchmarks/datasets/locomo-audit/corrections.json") + provenance = fetch_locomo_audit_corrections(output_path=resolved_output) + elif dataset == "convomem": + resolved_output = output or Path("benchmarks/datasets/convomem") + sizes = tuple(int(s.strip()) for s in context_sizes.split(",") if s.strip()) + provenance = fetch_convomem_batches(output_dir=resolved_output, context_sizes=sizes) + else: + raise typer.BadParameter( + "Supported datasets: locomo, longmemeval-s, locomo-audit, convomem" + ) + + console.print(f"Downloaded {dataset} to [cyan]{resolved_output}[/cyan]") + console.print(f"SHA256: [green]{provenance.checksum_sha256}[/green]") + + +@convert_app.command("locomo") +def convert_locomo( + dataset_path: Path = typer.Option( + Path("benchmarks/datasets/locomo/locomo10.json"), "--dataset-path" + ), + output_dir: Path = typer.Option(Path("benchmarks/generated/locomo"), "--output-dir"), + max_conversations: int | None = typer.Option(None, "--max-conversations"), + audit_corrections: Path | None = typer.Option( + None, + "--audit-corrections", + help="Penfield audit corrections.json; applies corrected answers/evidence", + ), +) -> None: + docs_dir, queries_path, doc_count, query_count = convert_locomo_to_corpus( + dataset_path=dataset_path, + output_dir=output_dir, + max_conversations=max_conversations, + audit_corrections_path=audit_corrections, + ) + console.print(f"Docs: [cyan]{docs_dir}[/cyan] ({doc_count})") + console.print(f"Queries: [cyan]{queries_path}[/cyan] ({query_count})") + + +@convert_app.command("structure-corpus") +def convert_structure_corpus( + input_dir: Path = typer.Option( + ..., "--input-dir", help="Source corpus root (a flat docs dir or a grouped …/groups dir)" + ), + output_dir: Path = typer.Option( + ..., "--output-dir", help="Destination root; the input layout is mirrored beneath it" + ), + mode: str = typer.Option( + "augment", + "--mode", + help="augment: keep transcript + append structure (faithful); replace: structure only (lossy)", + ), + categories: str = typer.Option( + "", + "--categories", + help="Grouped corpora only: comma-separated category labels to restructure (matches group-id prefix). Empty = all docs.", + ), + extractor: str = typer.Option( + "claude:claude-haiku-4-5", "--extractor", help="LLM runner spec for fact extraction" + ), + max_workers: int = typer.Option(4, "--max-workers"), +) -> None: + """Restructure flat conversation docs into Basic Memory observations/relations. + + Produces a structured twin of a corpus with doc ids/frontmatter preserved, so + a flat-vs-structured run isolates the representation and recall stays + comparable. Works on both grouped and flat corpora (layout is mirrored). + """ + from basic_memory_benchmarks.converters.structure_corpus import ( + group_prefix_filter, + structure_corpus, + ) + from basic_memory_benchmarks.llm.runners import create_runner + + if mode not in ("augment", "replace"): + raise typer.BadParameter("--mode must be 'augment' or 'replace'") + cats = {c.strip() for c in categories.split(",") if c.strip()} + path_filter = group_prefix_filter(cats) if cats else None + runner = create_runner(extractor) + output_dir.mkdir(parents=True, exist_ok=True) + + doc_count = structure_corpus( + input_root=input_dir, + output_root=output_dir, + runner=runner, + mode=mode, # type: ignore[arg-type] + path_filter=path_filter, + max_workers=max_workers, + ) + + console.print(f"Structured ([green]{mode}[/green]): [cyan]{output_dir}[/cyan] ({doc_count} docs)") + console.print(f"Extractor: [green]{extractor}[/green]") + if cats: + console.print(f"Filtered to categories: {sorted(cats)}") + + +@convert_app.command("longmemeval") +def convert_longmemeval( + dataset_path: Path = typer.Option( + Path("benchmarks/datasets/longmemeval/longmemeval_s.json"), "--dataset-path" + ), + output_dir: Path = typer.Option(Path("benchmarks/generated/longmemeval-s"), "--output-dir"), + max_questions: int | None = typer.Option(None, "--max-questions"), + stratified: bool = typer.Option( + False, "--stratified", help="Sample max-questions evenly across question types (seed 42)" + ), + seed: int = typer.Option(42, "--seed"), +) -> None: + groups_dir, queries_path, doc_count, query_count = convert_longmemeval_to_corpus( + dataset_path=dataset_path, + output_dir=output_dir, + max_questions=max_questions, + stratified=stratified, + seed=seed, + ) + console.print(f"Groups: [cyan]{groups_dir}[/cyan] ({query_count} groups, {doc_count} docs)") + console.print(f"Queries: [cyan]{queries_path}[/cyan] ({query_count})") + + +@convert_app.command("convomem") +def convert_convomem( + batches_dir: Path = typer.Option(Path("benchmarks/datasets/convomem"), "--batches-dir"), + output_dir: Path = typer.Option(Path("benchmarks/generated/convomem"), "--output-dir"), + sample_per_stratum: int = typer.Option(25, "--sample-per-stratum"), + seed: int = typer.Option(42, "--seed"), + context_sizes: str = typer.Option("10,30", "--context-sizes"), +) -> None: + sizes = tuple(int(s.strip()) for s in context_sizes.split(",") if s.strip()) + groups_dir, queries_path, doc_count, query_count = convert_convomem_to_corpus( + batches_dir=batches_dir, + output_dir=output_dir, + sample_per_stratum=sample_per_stratum, + seed=seed, + context_sizes=sizes, + ) + console.print(f"Groups: [cyan]{groups_dir}[/cyan] ({doc_count} docs)") + console.print(f"Queries: [cyan]{queries_path}[/cyan] ({query_count})") + console.print(f"Sampling manifest: [cyan]{output_dir / 'sampling.json'}[/cyan]") + + +@run_app.command("retrieval") +def run_retrieval_command( + providers: str = typer.Option("bm-local,mem0-local", "--providers"), + dataset_id: str = typer.Option("locomo", "--dataset-id"), + dataset_path: Path = typer.Option( + Path("benchmarks/datasets/locomo/locomo10.json"), "--dataset-path" + ), + corpus_dir: Path = typer.Option(Path("benchmarks/generated/locomo/docs"), "--corpus-dir"), + queries_path: Path = typer.Option( + Path("benchmarks/generated/locomo/queries.json"), "--queries-path" + ), + output_root: Path = typer.Option(Path("benchmarks/runs"), "--output-root"), + run_id: str | None = typer.Option(None, "--run-id"), + top_k: int = typer.Option(10, "--top-k"), + bm_source: str = typer.Option( + "github:basicmachines-co/basic-memory@main", + "--bm-source", + ), + bm_local_path: str | None = typer.Option(None, "--bm-local-path"), + allow_provider_skip: bool = typer.Option(True, "--allow-provider-skip/--strict-providers"), +) -> None: + resolved_run_id = run_id or uuid.uuid4().hex[:12] + provider_list = [item.strip() for item in providers.split(",") if item.strip()] + + if not dataset_path.exists(): + raise typer.BadParameter(f"Dataset path not found: {dataset_path}") + if not corpus_dir.exists(): + raise typer.BadParameter(f"Corpus dir not found: {corpus_dir}") + if not queries_path.exists(): + raise typer.BadParameter(f"Queries file not found: {queries_path}") + + provenance = DatasetProvenance( + dataset_id=dataset_id, + source_url=str(dataset_path), + checksum_sha256=sha256_file(dataset_path), + license_note="See dataset source/license terms", + fetched_at_utc="unknown", + ) + + config = RunConfig( + run_id=resolved_run_id, + dataset_id=dataset_id, + dataset_path=str(dataset_path), + corpus_dir=str(corpus_dir), + queries_path=str(queries_path), + output_root=str(output_root), + providers=provider_list, + top_k=top_k, + bm_source=bm_source, + bm_local_path=bm_local_path, + allow_provider_skip=allow_provider_skip, + ) + + run_dir = run_retrieval(run_config=config, dataset=provenance) + console.print(f"Retrieval run complete: [green]{run_dir}[/green]") + + +@run_app.command("qa") +def run_qa_command( + run_dir: Path = typer.Option(..., "--run-dir"), + answerer: str = typer.Option( + "claude:claude-haiku-4-5", + "--answerer", + help="Runner spec: claude: or openai-compat:@", + ), + judge: str = typer.Option( + "claude:claude-sonnet-4-6", + "--judge", + help="Runner spec: claude: or openai-compat:@", + ), + max_workers: int = typer.Option(4, "--max-workers"), + max_context_chars: int | None = typer.Option( + None, + "--max-context-chars", + help="Override the assembled-context budget (default 12000). Use a large value for full-context baselines.", + ), +) -> None: + out = run_qa_stage( + run_dir=run_dir, + answerer_spec=answerer, + judge_spec=judge, + max_workers=max_workers, + max_context_chars=max_context_chars, + ) + console.print(f"QA run complete: [green]{out}[/green]") + console.print(f"See [cyan]{out / 'qa-summary.json'}[/cyan]") + + +@run_app.command("review") +def run_review_command( + run_dir: Path = typer.Option(..., "--run-dir"), + source: str = typer.Option("auto", "--source", help="qa | rejudge | auto"), +) -> None: + """Render a self-contained judge-review/labeling HTML report for a run.""" + out = run_review_stage(run_dir=run_dir, source=source) + console.print(f"Review report: [green]{out}[/green]") + console.print(f"Open it: [cyan]open {out}[/cyan]") + + +@run_app.command("diagnose") +def run_diagnose_command( + run_dir: Path = typer.Option(..., "--run-dir"), + source: str = typer.Option("auto", "--source", help="qa | rejudge | auto"), + recall_field: str = typer.Option( + "recall_at_10", "--recall-field", help="recall_at_5 | recall_at_10" + ), +) -> None: + """Attribute QA failures to retrieval vs the answerer (per provider). + + Separates "retrieved but unused" (the fixed answerer's fault, identical + across providers) from "truly missed" (a real retrieval failure), so QA + accuracy can be read honestly against the retrieval ceiling. + """ + import json + + from rich.table import Table + + out = run_diagnose_stage(run_dir=run_dir, source=source, recall_field=recall_field) + payload = json.loads(out.read_text(encoding="utf-8")) + + table = Table(title=f"Failure attribution — {run_dir.name} ({payload['source']})") + table.add_column("provider") + table.add_column("answerable", justify="right") + table.add_column("QA acc", justify="right") + table.add_column("retr. ceiling", justify="right") + table.add_column("answerer gap", justify="right") + table.add_column("retrieval gap", justify="right") + table.add_column("of fails: answerer", justify="right") + for prov in payload["providers"]: + table.add_row( + prov["provider"], + str(prov["answerable"]), + f"{prov['qa_accuracy']:.3f}", + f"{prov['retrieval_ceiling']:.3f}", + f"{prov['answerer_gap']:.3f}", + f"{prov['retrieval_gap']:.3f}", + f"{prov['answerer_failure_share']:.0%}", + ) + console.print(table) + console.print(f"Wrote [green]{out}[/green]") + + +@run_app.command("rejudge") +def run_rejudge_command( + run_dir: Path = typer.Option(..., "--run-dir"), + judge: str = typer.Option("claude:claude-sonnet-4-6", "--judge"), + max_workers: int = typer.Option(4, "--max-workers"), +) -> None: + """Re-judge stored QA answers (no regeneration); reports verdict flips.""" + out = run_rejudge_stage(run_dir=run_dir, judge_spec=judge, max_workers=max_workers) + console.print(f"Re-judge complete: [green]{out}[/green]") + console.print(f"Flips: [cyan]{out / 'qa-rejudge-flips.json'}[/cyan]") + + +@run_app.command("judge") +def run_judge_command( + run_dir: Path = typer.Option(..., "--run-dir"), + model: str = typer.Option("gpt-4o-mini", "--model"), +) -> None: + out = run_judge(run_dir=run_dir, model=model) + console.print(f"Judge run complete: [green]{out}[/green]") + + +@run_app.command("full") +def run_full_command( + providers: str = typer.Option("bm-local,mem0-local", "--providers"), + dataset_id: str = typer.Option("locomo", "--dataset-id"), + dataset_path: Path = typer.Option( + Path("benchmarks/datasets/locomo/locomo10.json"), "--dataset-path" + ), + corpus_dir: Path = typer.Option(Path("benchmarks/generated/locomo/docs"), "--corpus-dir"), + queries_path: Path = typer.Option( + Path("benchmarks/generated/locomo/queries.json"), "--queries-path" + ), + output_root: Path = typer.Option(Path("benchmarks/runs"), "--output-root"), + run_id: str | None = typer.Option(None, "--run-id"), + top_k: int = typer.Option(10, "--top-k"), + bm_source: str = typer.Option("github:basicmachines-co/basic-memory@main", "--bm-source"), + bm_local_path: str | None = typer.Option(None, "--bm-local-path"), + allow_provider_skip: bool = typer.Option(True, "--allow-provider-skip/--strict-providers"), + judge: bool = typer.Option(False, "--judge"), + judge_model: str = typer.Option("gpt-4o-mini", "--judge-model"), +) -> None: + run_retrieval_command( + providers=providers, + dataset_id=dataset_id, + dataset_path=dataset_path, + corpus_dir=corpus_dir, + queries_path=queries_path, + output_root=output_root, + run_id=run_id, + top_k=top_k, + bm_source=bm_source, + bm_local_path=bm_local_path, + allow_provider_skip=allow_provider_skip, + ) + + if judge: + resolved_run_id = run_id + if resolved_run_id is None: + # run_retrieval_command generated uuid when run_id is None. infer by latest dir. + run_dirs = sorted(Path(output_root).glob("*"), key=lambda path: path.stat().st_mtime) + if not run_dirs: + raise RuntimeError("Unable to locate run directory for judge step") + run_dir = run_dirs[-1] + else: + run_dir = Path(output_root) / resolved_run_id + run_judge_command(run_dir=run_dir, model=judge_model) + + +@app.command("compare") +def compare_runs( + baseline: Path = typer.Argument(..., help="Path to baseline retrieval-summary.json"), + candidate: Path = typer.Argument(..., help="Path to candidate retrieval-summary.json"), + provider: str = typer.Option("bm-local", "--provider"), + metric: str = typer.Option("recall_at_5", "--metric"), +) -> None: + baseline_payload = load_retrieval_summary(baseline) + candidate_payload = load_retrieval_summary(candidate) + b, c, delta = compare_provider_metric(baseline_payload, candidate_payload, provider, metric) + console.print(f"provider={provider} metric={metric}") + console.print(f"baseline={b}") + console.print(f"candidate={c}") + console.print(f"delta={delta}") + + +@app.command("publish") +def publish_run( + run_dir: Path = typer.Option(..., "--run-dir"), + destination: Path = typer.Option(Path("benchmarks/results/public"), "--destination"), +) -> None: + if not run_dir.exists(): + raise typer.BadParameter(f"Run directory does not exist: {run_dir}") + destination.mkdir(parents=True, exist_ok=True) + target = destination / run_dir.name + if target.exists(): + shutil.rmtree(target) + shutil.copytree(run_dir, target) + console.print(f"Published run to [green]{target}[/green]") + + +@app.command("validate-artifacts") +def validate_artifacts( + run_dir: Path = typer.Option(..., "--run-dir"), +) -> None: + expected = [ + "manifest.json", + "provider-status.json", + "per-query-retrieval.jsonl", + "retrieval-summary.json", + "summary.md", + ] + missing = [name for name in expected if not (run_dir / name).exists()] + if missing: + raise typer.BadParameter(f"Missing artifacts: {missing}") + console.print("Artifacts look complete.") + + +def main() -> None: + app() + + +if __name__ == "__main__": + main() diff --git a/benchmarks/src/basic_memory_benchmarks/converters/__init__.py b/benchmarks/src/basic_memory_benchmarks/converters/__init__.py new file mode 100644 index 000000000..69fc0d5cf --- /dev/null +++ b/benchmarks/src/basic_memory_benchmarks/converters/__init__.py @@ -0,0 +1 @@ +"""Converters for benchmark corpora.""" diff --git a/benchmarks/src/basic_memory_benchmarks/converters/convomem_to_corpus.py b/benchmarks/src/basic_memory_benchmarks/converters/convomem_to_corpus.py new file mode 100644 index 000000000..9700b21ed --- /dev/null +++ b/benchmarks/src/basic_memory_benchmarks/converters/convomem_to_corpus.py @@ -0,0 +1,160 @@ +"""Convert sampled ConvoMem pre-mixed test cases into grouped benchmark corpora. + +Sampling is stratified by (category, contextSize) with a fixed seed, and the +exact sample composition is written to ``sampling.json`` so a published number +can state precisely which slice of ConvoMem it covers. + +Anti-leakage: conversations carry ``containsEvidence`` and ``model_name`` +fields in the raw data; rendered docs include neither, and conversation ids +are remapped to neutral positional ids. +""" + +from __future__ import annotations + +import json +import random +from pathlib import Path + +from basic_memory_benchmarks.datasets.convomem import load_convomem_batches + +DATASET_ID = "convomem" + +# Directory names -> benchmark category labels used in reports. +CATEGORY_LABELS: dict[str, str] = { + "user_evidence": "user_facts", + "assistant_facts_evidence": "assistant_facts", + "changing_evidence": "knowledge_update", + "abstention_evidence": "abstention", + "preference_evidence": "preference", + "implicit_connection_evidence": "implicit_connection", +} + + +def _render_conversation_doc(doc_id: str, messages: list[dict]) -> str: + lines: list[str] = [ + "---", + f"title: {doc_id}", + "type: note", + f"source_doc_id: {doc_id}", + f"dataset_id: {DATASET_ID}", + "---", + "", + f"# {doc_id}", + "", + "## Conversation", + ] + for message in messages: + speaker = str(message.get("speaker", "unknown")).capitalize() + text = str(message.get("text", "")).strip() + if not text: + continue + lines.append(f"- **{speaker}:** {' '.join(text.split())}") + return "\n".join(lines).rstrip() + "\n" + + +def convert_convomem_to_corpus( + batches_dir: Path, + output_dir: Path, + sample_per_stratum: int = 25, + seed: int = 42, + context_sizes: tuple[int, ...] | None = None, +) -> tuple[Path, Path, int, int]: + """Sample cases per (category, contextSize) stratum and emit grouped corpora. + + Returns: + groups_dir, queries_path, doc_count, query_count + """ + strata: dict[tuple[str, int], list[tuple[str, int, dict]]] = {} + for category, file_name, cases in load_convomem_batches(batches_dir): + for case_index, case in enumerate(cases): + context_size = int(case.get("contextSize") or 0) + if context_sizes is not None and context_size not in context_sizes: + continue + strata.setdefault((category, context_size), []).append((file_name, case_index, case)) + + if not strata: + raise ValueError( + f"No ConvoMem cases matched context sizes {context_sizes} in {batches_dir}" + ) + + groups_dir = output_dir / "groups" + groups_dir.mkdir(parents=True, exist_ok=True) + + rng = random.Random(seed) + all_queries: list[dict] = [] + sampling_manifest: dict[str, dict] = {} + doc_count = 0 + + for (category, context_size), members in sorted(strata.items()): + sample_size = min(sample_per_stratum, len(members)) + # Sort first so sampling is deterministic regardless of load order. + members.sort(key=lambda member: (member[0], member[1])) + sampled = rng.sample(members, sample_size) + label = CATEGORY_LABELS.get(category, category) + sampling_manifest[f"{label}/cs{context_size}"] = { + "population": len(members), + "sampled": sample_size, + } + + for file_name, case_index, case in sorted(sampled, key=lambda m: (m[0], m[1])): + batch_tag = file_name.rsplit("__", 1)[-1].removesuffix(".json") + group_id = f"{label}-cs{context_size}-{batch_tag}-{case_index:04d}" + docs_dir = groups_dir / group_id / "docs" + docs_dir.mkdir(parents=True, exist_ok=True) + + doc_id_by_conversation_id: dict[str, str] = {} + for conv_index, conversation in enumerate(case.get("conversations") or []): + doc_id = f"{group_id}-c{conv_index:03d}" + raw_id = str(conversation.get("id") or f"conv-{conv_index}") + doc_id_by_conversation_id[raw_id] = doc_id + (docs_dir / f"{doc_id}.md").write_text( + _render_conversation_doc(doc_id, conversation.get("messages") or []), + encoding="utf-8", + ) + doc_count += 1 + + for query_index, evidence in enumerate(case.get("evidenceItems") or []): + ground_truth: list[str] = [] + for evidence_conversation in evidence.get("conversations") or []: + raw_id = str(evidence_conversation.get("id") or "") + mapped = doc_id_by_conversation_id.get(raw_id) + # Abstention evidence references conversations that are + # intentionally absent from the haystack; skip those. + if mapped is not None: + ground_truth.append(mapped) + + all_queries.append( + { + "id": f"{group_id}-q{query_index}", + "query": str(evidence.get("question", "")).strip(), + "category": label, + "group": group_id, + "ground_truth": sorted(ground_truth), + "expected_answer": str(evidence.get("answer", "")).strip() or None, + "metadata": { + "dataset_id": DATASET_ID, + "context_size": context_size, + "abstention": label == "abstention", + "domain": str(evidence.get("category", "")), + }, + } + ) + + queries_path = output_dir / "queries.json" + queries_path.write_text(json.dumps(all_queries, indent=2), encoding="utf-8") + + sampling_path = output_dir / "sampling.json" + sampling_path.write_text( + json.dumps( + { + "seed": seed, + "sample_per_stratum": sample_per_stratum, + "context_sizes": sorted(context_sizes) if context_sizes else "all", + "strata": sampling_manifest, + }, + indent=2, + ), + encoding="utf-8", + ) + + return groups_dir, queries_path, doc_count, len(all_queries) diff --git a/benchmarks/src/basic_memory_benchmarks/converters/locomo_to_corpus.py b/benchmarks/src/basic_memory_benchmarks/converters/locomo_to_corpus.py new file mode 100644 index 000000000..4991f7919 --- /dev/null +++ b/benchmarks/src/basic_memory_benchmarks/converters/locomo_to_corpus.py @@ -0,0 +1,171 @@ +"""Convert LoCoMo dataset into deterministic benchmark corpus and query files.""" + +from __future__ import annotations + +import json +import re +from pathlib import Path + +from basic_memory_benchmarks.datasets.locomo import load_locomo_dataset + + +CATEGORY_MAP: dict[int, str] = { + 1: "single_hop", + 2: "multi_hop", + 3: "temporal", + 4: "open_domain", + 5: "adversarial", +} + + +def _session_num_from_evidence(evidence_id: str) -> int | None: + match = re.match(r"^D(\d+):", evidence_id) + if not match: + return None + return int(match.group(1)) + + +def _sorted_session_keys(conversation_blob: dict) -> list[str]: + keys = [ + k + for k, v in conversation_blob.items() + if re.match(r"^session_\d+$", k) and isinstance(v, list) + ] + return sorted(keys, key=lambda key: int(key.split("_")[1])) + + +def convert_locomo_to_corpus( + dataset_path: Path, + output_dir: Path, + max_conversations: int | None = None, + audit_corrections_path: Path | None = None, +) -> tuple[Path, Path, int, int]: + """Convert LoCoMo into markdown docs + query manifest. + + When ``audit_corrections_path`` is given, the Penfield audit's corrected + answers and evidence citations replace the originals for the 156 known + answer-key errors. Each corrected query is cross-checked by question text + so audit/dataset drift fails loudly. + + Returns: + docs_dir, queries_path, doc_count, query_count + """ + corrections: dict[str, dict] = {} + if audit_corrections_path is not None: + from basic_memory_benchmarks.datasets.locomo_audit import load_locomo_corrections + + corrections = load_locomo_corrections(audit_corrections_path) + + conversations = load_locomo_dataset(dataset_path) + # Prefix slicing keeps conv_index aligned with the audit's + # locomo__qa ids. + if max_conversations is not None: + conversations = conversations[:max_conversations] + + docs_dir = output_dir / "docs" + docs_dir.mkdir(parents=True, exist_ok=True) + + all_queries: list[dict] = [] + doc_count = 0 + + for conv_index, conversation in enumerate(conversations): + blob = conversation.get("conversation", {}) + session_keys = _sorted_session_keys(blob) + conv_id = f"locomo-c{conv_index:02d}" + session_doc_id: dict[int, str] = {} + + for session_key in session_keys: + session_num = int(session_key.split("_")[1]) + doc_id = f"{conv_id}-s{session_num:02d}" + session_doc_id[session_num] = doc_id + turns = blob.get(session_key, []) + # The session timestamp is the anchor for every relative time + # expression in the dialogue ("yesterday", "last Saturday"). + # Without it in the doc, date questions are unanswerable by any + # provider — LoCoMo multi_hop/temporal collapse to abstention. + session_date = str(blob.get(f"{session_key}_date_time", "")).strip() + date_suffix = f" ({session_date})" if session_date else "" + + lines: list[str] = [ + "---", + f"title: {doc_id}{date_suffix}", + "type: note", + f"source_doc_id: {doc_id}", + "dataset_id: locomo", + f"conversation_id: {conv_id}", + f"session_number: {session_num}", + ] + if session_date: + lines.append(f"session_date: {session_date}") + lines += [ + "---", + "", + f"# Chat session at {session_date}" if session_date else f"# {doc_id}", + "", + "## Conversation", + ] + for turn in turns: + speaker = str(turn.get("speaker", "unknown")) + text = str(turn.get("text", "")).strip() + if not text: + continue + lines.append(f"- **{speaker}:** {text}") + + doc_path = docs_dir / f"{doc_id}.md" + doc_path.write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8") + doc_count += 1 + + for query_index, qa in enumerate(conversation.get("qa", [])): + category_id = int(qa.get("category", 0)) + category = CATEGORY_MAP.get(category_id, f"cat_{category_id}") + + answer = qa.get("answer") or qa.get("adversarial_answer") + evidence = qa.get("evidence") or [] + metadata: dict = { + "dataset_id": "locomo", + "conversation_id": conv_id, + "adversarial": category_id == 5, + } + + correction = corrections.get(f"locomo_{conv_index}_qa{query_index}") + if correction is not None: + audit_question = str(correction.get("question", "")).strip() + actual_question = str(qa.get("question", "")).strip() + if audit_question != actual_question: + raise ValueError( + f"Audit correction locomo_{conv_index}_qa{query_index} does not " + f"match dataset question: {audit_question!r} != {actual_question!r}" + ) + answer = correction["correct_answer"] + if correction.get("correct_evidence"): + evidence = correction["correct_evidence"] + metadata["audit_corrected"] = True + metadata["audit_error_type"] = str(correction["error_type"]) + + ground_truth_docs: set[str] = set() + for evidence_id in evidence: + if not isinstance(evidence_id, str): + continue + session_num = _session_num_from_evidence(evidence_id) + if session_num is None: + continue + if session_num in session_doc_id: + ground_truth_docs.add(session_doc_id[session_num]) + + all_queries.append( + { + "id": f"{conv_id}-q{query_index:04d}", + "query": str(qa.get("question", "")).strip(), + "category": category, + "category_id": category_id, + "ground_truth": sorted(ground_truth_docs), + "expected_answer": str(answer).strip() if answer else None, + "metadata": metadata, + } + ) + + queries_path = output_dir / "queries.json" + queries_path.parent.mkdir(parents=True, exist_ok=True) + queries_path.write_text(json.dumps(all_queries, indent=2), encoding="utf-8") + + return docs_dir, queries_path, doc_count, len(all_queries) diff --git a/benchmarks/src/basic_memory_benchmarks/converters/longmemeval_to_corpus.py b/benchmarks/src/basic_memory_benchmarks/converters/longmemeval_to_corpus.py new file mode 100644 index 000000000..6ac44f4db --- /dev/null +++ b/benchmarks/src/basic_memory_benchmarks/converters/longmemeval_to_corpus.py @@ -0,0 +1,169 @@ +"""Convert LongMemEval-S into grouped benchmark corpora and a query manifest. + +Each LongMemEval question carries its own haystack of chat sessions, so the +output is one corpus directory per question (``groups//docs``) +plus a single ``queries.json`` whose entries name their group. The runner +ingests and queries each group in isolation, matching the official protocol. + +Anti-leakage: the raw dataset marks evidence sessions with an ``answer_`` +session-id prefix and evidence turns with ``has_answer`` flags. Session ids +are remapped to neutral positional ids and turn flags are dropped, so nothing +ingested by a provider distinguishes evidence from filler. +""" + +from __future__ import annotations + +import json +import random +from collections import defaultdict +from pathlib import Path + +from basic_memory_benchmarks.datasets.longmemeval import load_longmemeval_dataset + +DATASET_ID = "longmemeval_s" + + +def _render_session_doc( + doc_id: str, + session_date: str, + turns: list[dict], +) -> str: + lines: list[str] = [ + "---", + f"title: {doc_id} ({session_date})", + "type: note", + f"source_doc_id: {doc_id}", + f"dataset_id: {DATASET_ID}", + f"session_date: {session_date}", + "---", + "", + f"# Chat session on {session_date}", + "", + "## Conversation", + ] + for turn in turns: + role = str(turn.get("role", "unknown")).capitalize() + content = str(turn.get("content", "")).strip() + if not content: + continue + # Keep each turn on one line so bullet-level chunking stays intact. + lines.append(f"- **{role}:** {' '.join(content.split())}") + return "\n".join(lines).rstrip() + "\n" + + +def convert_longmemeval_to_corpus( + dataset_path: Path, + output_dir: Path, + max_questions: int | None = None, + stratified: bool = False, + seed: int = 42, +) -> tuple[Path, Path, int, int]: + """Convert LongMemEval-S into per-question corpora + query manifest. + + ``max_questions`` with ``stratified=False`` takes the file-order prefix + (the file is sorted by question type, so small prefixes are single-type — + fine for smoke tests, misleading for category comparisons). + ``stratified=True`` samples evenly across the six question types with a + fixed seed and records the composition in sampling.json. + + Returns: + groups_dir, queries_path, doc_count, query_count + """ + entries = load_longmemeval_dataset(dataset_path) + sampling_note: dict | None = None + if max_questions is not None and stratified: + by_type: dict[str, list[dict]] = defaultdict(list) + for entry in entries: + by_type[str(entry["question_type"])].append(entry) + rng = random.Random(seed) + per_type = max(1, max_questions // len(by_type)) + sampled: list[dict] = [] + composition: dict[str, int] = {} + for question_type, members in sorted(by_type.items()): + members.sort(key=lambda e: str(e["question_id"])) + take = min(per_type, len(members)) + sampled.extend(rng.sample(members, take)) + composition[question_type] = take + entries = sampled + sampling_note = { + "seed": seed, + "max_questions": max_questions, + "per_type": per_type, + "composition": composition, + } + elif max_questions is not None: + entries = entries[:max_questions] + + groups_dir = output_dir / "groups" + groups_dir.mkdir(parents=True, exist_ok=True) + + all_queries: list[dict] = [] + doc_count = 0 + + for entry in entries: + question_id = str(entry["question_id"]) + sessions = entry["haystack_sessions"] + session_ids = entry["haystack_session_ids"] + session_dates = entry["haystack_dates"] + if not (len(sessions) == len(session_ids) == len(session_dates)): + raise ValueError( + f"Question {question_id}: haystack arrays misaligned " + f"({len(sessions)} sessions, {len(session_ids)} ids, {len(session_dates)} dates)" + ) + + docs_dir = groups_dir / question_id / "docs" + docs_dir.mkdir(parents=True, exist_ok=True) + + # Neutral positional doc ids; the raw ids leak evidence via the + # "answer_" prefix. A few haystacks repeat a session id — keep the + # first occurrence so each session is ingested once. + doc_id_by_session_id: dict[str, str] = {} + for index, (session, session_id, session_date) in enumerate( + zip(sessions, session_ids, session_dates) + ): + if str(session_id) in doc_id_by_session_id: + continue + doc_id = f"{question_id}-s{index:03d}" + doc_id_by_session_id[str(session_id)] = doc_id + doc_path = docs_dir / f"{doc_id}.md" + doc_path.write_text( + _render_session_doc(doc_id, str(session_date), session), + encoding="utf-8", + ) + doc_count += 1 + + ground_truth: list[str] = [] + for answer_session_id in entry["answer_session_ids"]: + mapped = doc_id_by_session_id.get(str(answer_session_id)) + if mapped is None: + raise ValueError( + f"Question {question_id}: answer session {answer_session_id!r} " + "not present in haystack" + ) + ground_truth.append(mapped) + + is_abstention = question_id.endswith("_abs") + all_queries.append( + { + "id": question_id, + "query": str(entry["question"]).strip(), + "category": str(entry["question_type"]), + "group": question_id, + "ground_truth": sorted(ground_truth), + "expected_answer": str(entry["answer"]).strip(), + "metadata": { + "dataset_id": DATASET_ID, + "question_date": str(entry["question_date"]), + "abstention": is_abstention, + }, + } + ) + + queries_path = output_dir / "queries.json" + queries_path.write_text(json.dumps(all_queries, indent=2), encoding="utf-8") + if sampling_note is not None: + (output_dir / "sampling.json").write_text( + json.dumps(sampling_note, indent=2), encoding="utf-8" + ) + + return groups_dir, queries_path, doc_count, len(all_queries) diff --git a/benchmarks/src/basic_memory_benchmarks/converters/structure_corpus.py b/benchmarks/src/basic_memory_benchmarks/converters/structure_corpus.py new file mode 100644 index 000000000..5d12e4e88 --- /dev/null +++ b/benchmarks/src/basic_memory_benchmarks/converters/structure_corpus.py @@ -0,0 +1,156 @@ +"""Restructure a flat conversation corpus into Basic Memory's native form. + +The default benchmark corpus stores each conversation as a flat +``## Conversation`` transcript, so Basic Memory is exercised purely as a +text-search engine over chat logs — none of its actual representation (typed +``- [category]`` observations and ``- relation [[Entity]]`` links) is used. That +can undersell a system whose strength is a structured knowledge graph, +especially where the answer requires joining facts the prose states separately. + +This converter rewrites each conversation doc into the note shape a Basic Memory +user/agent would actually write. Two modes: + +- ``augment`` (default, the faithful one): keep the original transcript and + *append* an LLM-extracted ``## Observations`` + ``## Relations`` block. This is + how Basic Memory really works — a note keeps its prose AND its parsed + observations/relations become first-class searchable units — so it can only + add signal, never lose the source text. +- ``replace``: swap the transcript for the extracted structure only. Useful to + measure the pure distilled representation, but strictly lossy. + +Document ids and frontmatter are always preserved, so retrieval ground truth and +recall stay directly comparable to the flat corpus, and every downstream stage — +provider ingest, retrieval scoring, QA, the failure diagnostic — works unchanged. +The extractor runs through the same plan-billed ``claude -p`` runner used +elsewhere, so there is no API spend. Output mirrors the input directory layout, +so both grouped (``groups//docs``) and flat (``docs``) corpora are supported. +""" + +from __future__ import annotations + +import re +from concurrent.futures import ThreadPoolExecutor +from pathlib import Path +from typing import Callable, Literal + +from basic_memory_benchmarks.llm.runners import LLMRunner + +Mode = Literal["augment", "replace"] + +EXTRACTION_PROMPT = """\ +You are converting one conversation into a Basic Memory structured note. + +Basic Memory represents knowledge as typed observations and relations, NOT prose: +- An observation is a single fact written as: `- [category] the fact #optional-tag` + Categories are short lowercase labels you choose to fit the fact, e.g. + [fact], [event], [preference], [decision], [requirement], [risk], [recommendation]. +- A relation links the note's subject to another entity: + `- relation_type [[Entity Name]]` + Use natural, consistent entity names (e.g. [[Email Communication]], + [[Phishing Incident]]) and verb-like relation types (relates_to, caused_by, + mitigated_by, prefers, concerns, depends_on). + +Extract EVERY concrete, durable fact the conversation establishes — what was +said, decided, preferred, experienced, or recommended — as observations, +preserving specific names, dates, numbers, and personal details verbatim. Then +capture how the entities involved connect to each other as relations. Make +implicit connections explicit: if the conversation links two things (an incident +to a vulnerability, a preference to a constraint), write that as a relation or an +observation. Do not editorialize, summarize the chit-chat, or invent facts that +are not supported by the text. + +Output ONLY markdown in exactly this form, nothing else (no preamble, no fences): + +## Observations +- [category] ... +- [category] ... + +## Relations +- relation_type [[Entity]] +- relation_type [[Entity]] + +Conversation to convert: +{conversation} +""" + +_CONVERSATION_RE = re.compile(r"\n## Conversation\b", re.DOTALL) + + +def _split_doc(flat_text: str) -> tuple[str, str]: + """Return (header, conversation_body): header is everything up to (excluding) + the ``## Conversation`` heading, body is the transcript after it.""" + match = _CONVERSATION_RE.search(flat_text) + if not match: + raise ValueError("Doc has no '## Conversation' section to restructure") + header = flat_text[: match.start()].rstrip() + "\n" + body = flat_text[match.end() :].strip() + return header, body + + +def _clean_extraction(text: str) -> str: + """Strip stray code fences / preamble and ensure it starts at Observations.""" + cleaned = text.strip() + if cleaned.startswith("```"): + cleaned = cleaned.strip("`") + cleaned = re.sub(r"^[a-zA-Z]+\n", "", cleaned).strip() + idx = cleaned.find("## Observations") + if idx > 0: + cleaned = cleaned[idx:] + return cleaned.strip() + + +def structure_doc(flat_text: str, runner: LLMRunner, *, mode: Mode = "augment") -> str: + """Convert one flat conversation doc into a BM-native note (doc id kept). + + ``augment`` retains the transcript and appends the structured block; + ``replace`` substitutes the structured block for the transcript. + """ + header, conversation = _split_doc(flat_text) + result = runner.complete(EXTRACTION_PROMPT.format(conversation=conversation)) + structured = _clean_extraction(result.text) + if "## Observations" not in structured: + # Fail loud so a bad slice can't masquerade as a structured corpus. + raise ValueError(f"Extractor returned no observations for a doc:\n{result.text[:400]}") + if mode == "replace": + return f"{header}\n{structured}\n" + return f"{flat_text.rstrip()}\n\n{structured}\n" + + +def structure_corpus( + *, + input_root: Path, + output_root: Path, + runner: LLMRunner, + mode: Mode = "augment", + path_filter: Callable[[Path], bool] | None = None, + max_workers: int = 4, +) -> int: + """Restructure every ``*.md`` under ``input_root`` into ``output_root``, + mirroring the relative directory layout. ``path_filter`` (given the source + path) selects which docs to convert. Returns the doc count.""" + docs = [ + path + for path in sorted(input_root.rglob("*.md")) + if path_filter is None or path_filter(path) + ] + + def _work(doc_path: Path) -> None: + structured = structure_doc(doc_path.read_text(encoding="utf-8"), runner, mode=mode) + dest = output_root / doc_path.relative_to(input_root) + dest.parent.mkdir(parents=True, exist_ok=True) + dest.write_text(structured, encoding="utf-8") + + with ThreadPoolExecutor(max_workers=max_workers) as pool: + list(pool.map(_work, docs)) + + return len(docs) + + +def group_prefix_filter(categories: set[str]) -> Callable[[Path], bool]: + """Path filter matching grouped corpora: keep docs whose group directory name + starts with one of ``categories`` (e.g. ``implicit_connection-cs10-…``).""" + + def _filter(path: Path) -> bool: + return any(part.startswith(f"{cat}-") for part in path.parts for cat in categories) + + return _filter diff --git a/benchmarks/src/basic_memory_benchmarks/datasets/__init__.py b/benchmarks/src/basic_memory_benchmarks/datasets/__init__.py new file mode 100644 index 000000000..178f3710c --- /dev/null +++ b/benchmarks/src/basic_memory_benchmarks/datasets/__init__.py @@ -0,0 +1 @@ +"""Dataset helpers.""" diff --git a/benchmarks/src/basic_memory_benchmarks/datasets/convomem.py b/benchmarks/src/basic_memory_benchmarks/datasets/convomem.py new file mode 100644 index 000000000..01a54ded1 --- /dev/null +++ b/benchmarks/src/basic_memory_benchmarks/datasets/convomem.py @@ -0,0 +1,169 @@ +"""ConvoMem (Salesforce) dataset utilities. + +ConvoMem ships ~75K QA pairs as pre-mixed test cases on HuggingFace +(Salesforce/ConvoMem, Apache-2.0): each case is a self-contained haystack of +conversations (evidence + filler) plus its questions, organized as +``core_benchmark/pre_mixed_testcases//_evidence/batched_*.json``. +Cases map 1:1 onto the harness's grouped runner mode. + +The full dataset is multi-GB (a single context-size-300 batch is ~850MB), so +fetching is selective: batch files within a directory are ordered by case +context size, and the last few KB of each file contain its final case's +``contextSize``. A cheap HTTP Range tail-probe indexes every file without +downloading it; only files matching the requested context sizes are fetched. +""" + +from __future__ import annotations + +import json +import re +import time +from pathlib import Path + +import httpx + +from basic_memory_benchmarks.models import DatasetProvenance +from basic_memory_benchmarks.utils import sha256_file, utc_now_iso + +CONVOMEM_REPO = "Salesforce/ConvoMem" +CONVOMEM_LICENSE_NOTE = "ConvoMem by Salesforce AI Research (Apache-2.0); see dataset card." +_BASE_PATH = "core_benchmark/pre_mixed_testcases" +_RESOLVE = f"https://huggingface.co/datasets/{CONVOMEM_REPO}/resolve/main" +_TREE = f"https://huggingface.co/api/datasets/{CONVOMEM_REPO}/tree/main" + +# Benchmark categories and their lowest evidence level (per the dataset README; +# 'changing' requires >= 2 evidence items). +CATEGORY_EVIDENCE_LEVELS: dict[str, int] = { + "user_evidence": 1, + "assistant_facts_evidence": 1, + "changing_evidence": 2, + "abstention_evidence": 1, + "preference_evidence": 1, + "implicit_connection_evidence": 1, +} + +DEFAULT_CONTEXT_SIZES = (10, 30) +_TAIL_PROBE_BYTES = 4096 +_CONTEXT_SIZE_PATTERN = re.compile(r'"contextSize":\s*(\d+)') + + +def _get_with_retry(client: httpx.Client, url: str, **kwargs) -> httpx.Response: + """GET with retries: rapid probe sequences trip transient CDN resets.""" + last_error: Exception | None = None + for attempt in range(4): + try: + response = client.get(url, follow_redirects=True, **kwargs) + response.raise_for_status() + return response + except (httpx.TransportError, httpx.HTTPStatusError) as exc: + last_error = exc + time.sleep(0.5 * (attempt + 1)) + raise RuntimeError(f"GET {url} failed after retries: {last_error}") + + +def _tail_context_size(client: httpx.Client, repo_path: str) -> int | None: + """Read the final case's contextSize via an HTTP Range request.""" + response = _get_with_retry( + client, + f"{_RESOLVE}/{repo_path}", + headers={"Range": f"bytes=-{_TAIL_PROBE_BYTES}"}, + ) + matches = _CONTEXT_SIZE_PATTERN.findall(response.text) + return int(matches[-1]) if matches else None + + +def _list_batch_files(client: httpx.Client, category: str, level: int) -> list[str]: + response = _get_with_retry(client, f"{_TREE}/{_BASE_PATH}/{category}/{level}_evidence") + return sorted( + item["path"] + for item in response.json() + if isinstance(item, dict) and item.get("path", "").endswith(".json") + ) + + +def fetch_convomem_batches( + output_dir: Path, + context_sizes: tuple[int, ...] = DEFAULT_CONTEXT_SIZES, + categories: dict[str, int] | None = None, +) -> DatasetProvenance: + """Index every batch file by tail-probe and download only matching ones. + + Downloaded files are stored flat as ``____.json``; + an ``index.json`` records the full probe results (including files NOT + downloaded) so the selection itself is auditable. + """ + categories = categories or CATEGORY_EVIDENCE_LEVELS + output_dir.mkdir(parents=True, exist_ok=True) + wanted = set(context_sizes) + + index: list[dict] = [] + downloaded: list[Path] = [] + with httpx.Client(timeout=120) as client: + for category, level in categories.items(): + for repo_path in _list_batch_files(client, category, level): + # Throttle probes; HF's CDN resets connections on rapid bursts. + time.sleep(0.2) + tail_size = _tail_context_size(client, repo_path) + record = { + "repo_path": repo_path, + "category": category, + "evidence_level": level, + "tail_context_size": tail_size, + "downloaded": tail_size in wanted, + } + index.append(record) + if tail_size not in wanted: + continue + local_name = f"{category}__{level}__{Path(repo_path).name}" + local_path = output_dir / local_name + with client.stream( + "GET", f"{_RESOLVE}/{repo_path}", follow_redirects=True + ) as response: + response.raise_for_status() + with local_path.open("wb") as file: + for chunk in response.iter_bytes(): + file.write(chunk) + record["local_file"] = local_name + record["sha256"] = sha256_file(local_path) + downloaded.append(local_path) + + index_path = output_dir / "index.json" + index_path.write_text( + json.dumps( + {"context_sizes": sorted(wanted), "files": index}, + indent=2, + ), + encoding="utf-8", + ) + + provenance = DatasetProvenance( + dataset_id="convomem", + source_url=f"https://huggingface.co/datasets/{CONVOMEM_REPO}", + checksum_sha256=sha256_file(index_path), + license_note=CONVOMEM_LICENSE_NOTE, + fetched_at_utc=utc_now_iso(), + ) + index_path.with_suffix(".provenance.json").write_text( + json.dumps(provenance.model_dump(mode="json"), indent=2), + encoding="utf-8", + ) + if not downloaded: + raise RuntimeError( + f"No ConvoMem batch files matched context sizes {sorted(wanted)}; " + "see index.json for the probe results" + ) + return provenance + + +def load_convomem_batches(batches_dir: Path) -> list[tuple[str, str, list[dict]]]: + """Load downloaded batches as (category, local_file_name, cases) tuples.""" + results: list[tuple[str, str, list[dict]]] = [] + for path in sorted(batches_dir.glob("*__*__*.json")): + category = path.name.split("__", 1)[0] + payload = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(payload, list): + raise ValueError(f"ConvoMem batch must be a list of cases: {path}") + results.append((category, path.name, payload)) + if not results: + raise FileNotFoundError(f"No ConvoMem batch files found in {batches_dir}") + return results diff --git a/benchmarks/src/basic_memory_benchmarks/datasets/locomo.py b/benchmarks/src/basic_memory_benchmarks/datasets/locomo.py new file mode 100644 index 000000000..e956f5e3c --- /dev/null +++ b/benchmarks/src/basic_memory_benchmarks/datasets/locomo.py @@ -0,0 +1,48 @@ +"""LoCoMo dataset utilities.""" + +from __future__ import annotations + +import json +from pathlib import Path + +import httpx + +from basic_memory_benchmarks.models import DatasetProvenance +from basic_memory_benchmarks.utils import sha256_file, utc_now_iso + + +LOCOMO_URL = "https://raw.githubusercontent.com/snap-research/locomo/main/data/locomo10.json" +LOCOMO_LICENSE_NOTE = "Dataset is owned by source authors; redistribution may be restricted." + + +def fetch_locomo_dataset(output_path: Path, url: str = LOCOMO_URL) -> DatasetProvenance: + output_path.parent.mkdir(parents=True, exist_ok=True) + response = httpx.get(url, timeout=120) + response.raise_for_status() + output_path.write_bytes(response.content) + + checksum = sha256_file(output_path) + provenance = DatasetProvenance( + dataset_id="locomo", + source_url=url, + checksum_sha256=checksum, + license_note=LOCOMO_LICENSE_NOTE, + fetched_at_utc=utc_now_iso(), + ) + output_path.with_suffix(".provenance.json").write_text( + json.dumps(provenance.model_dump(mode="json"), indent=2), + encoding="utf-8", + ) + return provenance + + +def load_locomo_dataset(path: Path) -> list[dict]: + payload = json.loads(path.read_text(encoding="utf-8")) + if isinstance(payload, list): + return payload + if isinstance(payload, dict): + if isinstance(payload.get("data"), list): + return payload["data"] + if isinstance(payload.get("conversations"), list): + return payload["conversations"] + raise ValueError(f"Unsupported LoCoMo payload shape in {path}") diff --git a/benchmarks/src/basic_memory_benchmarks/datasets/locomo_audit.py b/benchmarks/src/basic_memory_benchmarks/datasets/locomo_audit.py new file mode 100644 index 000000000..8818652d6 --- /dev/null +++ b/benchmarks/src/basic_memory_benchmarks/datasets/locomo_audit.py @@ -0,0 +1,93 @@ +"""Penfield Labs LoCoMo audit corrections. + +The April 2026 audit of LoCoMo (github.com/dial481/locomo-audit) found 156 +answer-key errors across the 1,540 usable questions: hallucinated facts, +temporal arithmetic mistakes, speaker-attribution errors, and wrong evidence +citations. Scoring against the corrected key (and citing the audit) is how a +published LoCoMo number survives scrutiny — the original key caps a perfect +system at roughly 93.6%. + +Corrections are fetched from a pinned commit so runs are reproducible, merged +into one corrections.json keyed by the audit's question ids +(``locomo__qa``), and applied at conversion time with a +question-text cross-check so a dataset/audit drift fails loudly instead of +silently mis-correcting. +""" + +from __future__ import annotations + +import json +from pathlib import Path + +import httpx + +from basic_memory_benchmarks.models import DatasetProvenance +from basic_memory_benchmarks.utils import sha256_file, utc_now_iso + +# Pinned audit revision (main as of 2026-04-02). Bump deliberately. +LOCOMO_AUDIT_SHA = "9493fb4b4af4256ed17a18e8fd0b3cfdeec29539" +LOCOMO_AUDIT_REPO = "dial481/locomo-audit" +LOCOMO_AUDIT_LICENSE_NOTE = ( + "Audit corrections by Penfield Labs (github.com/dial481/locomo-audit); " + "see repo LICENSE for terms." +) +_CONVERSATION_COUNT = 10 + +_REQUIRED_KEYS = {"question_id", "question", "error_type", "correct_answer"} + + +def _errors_url(conversation_index: int, sha: str) -> str: + return ( + f"https://raw.githubusercontent.com/{LOCOMO_AUDIT_REPO}/{sha}/" + f"audit/errors_conv_{conversation_index}.json" + ) + + +def fetch_locomo_audit_corrections( + output_path: Path, sha: str = LOCOMO_AUDIT_SHA +) -> DatasetProvenance: + """Download all per-conversation error files and merge them into one list.""" + merged: list[dict] = [] + for conversation_index in range(_CONVERSATION_COUNT): + response = httpx.get(_errors_url(conversation_index, sha), timeout=120) + response.raise_for_status() + records = response.json() + if not isinstance(records, list): + raise ValueError( + f"Audit errors file for conversation {conversation_index} is not a list" + ) + merged.extend(records) + + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text(json.dumps(merged, indent=2), encoding="utf-8") + + provenance = DatasetProvenance( + dataset_id="locomo-audit", + source_url=f"https://github.com/{LOCOMO_AUDIT_REPO}/tree/{sha}/audit", + checksum_sha256=sha256_file(output_path), + license_note=LOCOMO_AUDIT_LICENSE_NOTE, + fetched_at_utc=utc_now_iso(), + ) + output_path.with_suffix(".provenance.json").write_text( + json.dumps(provenance.model_dump(mode="json"), indent=2), + encoding="utf-8", + ) + return provenance + + +def load_locomo_corrections(path: Path) -> dict[str, dict]: + """Load merged corrections keyed by audit question id.""" + payload = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(payload, list): + raise ValueError(f"Corrections file must contain a list: {path}") + + corrections: dict[str, dict] = {} + for record in payload: + if not isinstance(record, dict) or not _REQUIRED_KEYS.issubset(record): + missing = _REQUIRED_KEYS - set(record) if isinstance(record, dict) else _REQUIRED_KEYS + raise ValueError(f"Corrections record missing keys {sorted(missing)}: {path}") + question_id = str(record["question_id"]) + if question_id in corrections: + raise ValueError(f"Duplicate correction for {question_id}: {path}") + corrections[question_id] = record + return corrections diff --git a/benchmarks/src/basic_memory_benchmarks/datasets/longmemeval.py b/benchmarks/src/basic_memory_benchmarks/datasets/longmemeval.py new file mode 100644 index 000000000..60a9f2ff9 --- /dev/null +++ b/benchmarks/src/basic_memory_benchmarks/datasets/longmemeval.py @@ -0,0 +1,69 @@ +"""LongMemEval dataset utilities. + +LongMemEval (Wu et al., ICLR 2025) evaluates long-term conversational memory +across six question types. The -S variant gives each of the 500 questions its +own ~50-session haystack, so the benchmark runs as grouped per-question +corpora rather than one shared corpus. +""" + +from __future__ import annotations + +import json +from pathlib import Path + +import httpx + +from basic_memory_benchmarks.models import DatasetProvenance +from basic_memory_benchmarks.utils import sha256_file, utc_now_iso + +LONGMEMEVAL_S_URL = ( + "https://huggingface.co/datasets/xiaowu0162/longmemeval/resolve/main/longmemeval_s" +) +LONGMEMEVAL_LICENSE_NOTE = "Dataset is owned by source authors; redistribution may be restricted." + +_REQUIRED_KEYS = { + "question_id", + "question_type", + "question", + "answer", + "question_date", + "haystack_session_ids", + "haystack_dates", + "haystack_sessions", + "answer_session_ids", +} + + +def fetch_longmemeval_dataset(output_path: Path, url: str = LONGMEMEVAL_S_URL) -> DatasetProvenance: + output_path.parent.mkdir(parents=True, exist_ok=True) + # The -S file is ~278MB; stream to disk instead of buffering in memory. + with httpx.stream("GET", url, timeout=600, follow_redirects=True) as response: + response.raise_for_status() + with output_path.open("wb") as file: + for chunk in response.iter_bytes(): + file.write(chunk) + + checksum = sha256_file(output_path) + provenance = DatasetProvenance( + dataset_id="longmemeval_s", + source_url=url, + checksum_sha256=checksum, + license_note=LONGMEMEVAL_LICENSE_NOTE, + fetched_at_utc=utc_now_iso(), + ) + output_path.with_suffix(".provenance.json").write_text( + json.dumps(provenance.model_dump(mode="json"), indent=2), + encoding="utf-8", + ) + return provenance + + +def load_longmemeval_dataset(path: Path) -> list[dict]: + payload = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(payload, list): + raise ValueError(f"LongMemEval payload must be a list of questions: {path}") + for index, entry in enumerate(payload): + if not isinstance(entry, dict) or not _REQUIRED_KEYS.issubset(entry): + missing = _REQUIRED_KEYS - set(entry) if isinstance(entry, dict) else _REQUIRED_KEYS + raise ValueError(f"LongMemEval entry {index} missing keys {sorted(missing)}: {path}") + return payload diff --git a/benchmarks/src/basic_memory_benchmarks/exceptions.py b/benchmarks/src/basic_memory_benchmarks/exceptions.py new file mode 100644 index 000000000..a4c211fab --- /dev/null +++ b/benchmarks/src/basic_memory_benchmarks/exceptions.py @@ -0,0 +1,5 @@ +"""Project exceptions.""" + + +class ProviderSkippedError(RuntimeError): + """Raised when provider execution is skipped by design.""" diff --git a/benchmarks/src/basic_memory_benchmarks/fairness.py b/benchmarks/src/basic_memory_benchmarks/fairness.py new file mode 100644 index 000000000..34718e491 --- /dev/null +++ b/benchmarks/src/basic_memory_benchmarks/fairness.py @@ -0,0 +1,34 @@ +"""Fairness validation across provider runs.""" + +from __future__ import annotations + +from collections.abc import Mapping, Sequence + +from basic_memory_benchmarks.models import PerQueryRetrievalResult + + +def validate_fairness( + results_by_provider: Mapping[str, Sequence[PerQueryRetrievalResult]], +) -> list[str]: + """Validate that all providers were scored on the same query set. + + Returns a list of warnings. Empty list means no mismatch detected. + """ + warnings: list[str] = [] + provider_names = sorted(results_by_provider.keys()) + if len(provider_names) < 2: + return warnings + + baseline_provider = provider_names[0] + baseline_ids = {row.query_id for row in results_by_provider[baseline_provider]} + + for provider in provider_names[1:]: + current_ids = {row.query_id for row in results_by_provider[provider]} + if baseline_ids != current_ids: + missing = sorted(baseline_ids - current_ids) + extra = sorted(current_ids - baseline_ids) + warnings.append( + f"Provider '{provider}' query mismatch: missing={missing[:5]} extra={extra[:5]}" + ) + + return warnings diff --git a/benchmarks/src/basic_memory_benchmarks/llm/__init__.py b/benchmarks/src/basic_memory_benchmarks/llm/__init__.py new file mode 100644 index 000000000..423aa35dc --- /dev/null +++ b/benchmarks/src/basic_memory_benchmarks/llm/__init__.py @@ -0,0 +1,19 @@ +"""LLM runner abstractions for answer generation and judging.""" + +from basic_memory_benchmarks.llm.runners import ( + ClaudeCLIRunner, + LLMResult, + LLMRunner, + LLMRunnerError, + OpenAICompatRunner, + create_runner, +) + +__all__ = [ + "ClaudeCLIRunner", + "LLMResult", + "LLMRunner", + "LLMRunnerError", + "OpenAICompatRunner", + "create_runner", +] diff --git a/benchmarks/src/basic_memory_benchmarks/llm/runners.py b/benchmarks/src/basic_memory_benchmarks/llm/runners.py new file mode 100644 index 000000000..2cf72915e --- /dev/null +++ b/benchmarks/src/basic_memory_benchmarks/llm/runners.py @@ -0,0 +1,194 @@ +"""LLM runner abstraction for answer generation and judging. + +Two transports are supported: + +- ``claude``: shells out to the Claude Code CLI in print mode (``claude -p``). + Calls bill against the operator's Claude subscription plan, not an API key. +- ``openai-compat``: POSTs to any OpenAI-compatible ``/chat/completions`` + endpoint (Ollama, LM Studio, vLLM, or the real OpenAI API). + +Runner specs are strings so they can flow through CLI flags and run manifests: + +- ``claude:claude-haiku-4-5`` +- ``openai-compat:llama3.1@http://localhost:11434/v1`` +""" + +from __future__ import annotations + +import json +import subprocess +import time +from abc import ABC, abstractmethod +from dataclasses import dataclass + +import httpx + + +class LLMRunnerError(RuntimeError): + """Raised when an LLM call fails after retries.""" + + +@dataclass +class LLMResult: + text: str + model: str + input_tokens: int + output_tokens: int + latency_ms: float + + +class LLMRunner(ABC): + """A minimal single-prompt completion interface.""" + + spec: str + + @abstractmethod + def complete(self, prompt: str) -> LLMResult: + """Run one prompt to completion and return the text plus usage.""" + + def describe(self) -> dict[str, str]: + return {"spec": self.spec} + + +class ClaudeCLIRunner(LLMRunner): + """Run prompts through ``claude -p`` (plan-billed, no API key required). + + Each call is a fresh CLI session, so it pays the CLI's system-prompt cache + overhead per call. Token counts reported here are the conversation tokens + only (``usage.input_tokens`` + ``output_tokens``), which is what matters + for cross-provider context-size comparisons. + """ + + def __init__( + self, + model: str, + *, + claude_bin: str = "claude", + timeout_seconds: float = 300.0, + max_retries: int = 2, + ) -> None: + self.model = model + self.spec = f"claude:{model}" + self._claude_bin = claude_bin + self._timeout_seconds = timeout_seconds + self._max_retries = max_retries + + def complete(self, prompt: str) -> LLMResult: + command = [ + self._claude_bin, + "-p", + "--model", + self.model, + "--output-format", + "json", + "--max-turns", + "1", + ] + last_error: Exception | None = None + for _ in range(self._max_retries + 1): + started = time.perf_counter() + try: + completed = subprocess.run( + command, + input=prompt, + capture_output=True, + text=True, + timeout=self._timeout_seconds, + check=False, + ) + payload = json.loads(completed.stdout) + if completed.returncode != 0 or payload.get("is_error"): + raise LLMRunnerError( + f"claude -p failed (rc={completed.returncode}): " + f"{payload.get('result') or completed.stderr[:500]}" + ) + usage = payload.get("usage") or {} + return LLMResult( + text=str(payload.get("result") or "").strip(), + model=self.model, + input_tokens=int(usage.get("input_tokens") or 0), + output_tokens=int(usage.get("output_tokens") or 0), + latency_ms=(time.perf_counter() - started) * 1000.0, + ) + except (subprocess.TimeoutExpired, json.JSONDecodeError, LLMRunnerError) as exc: + last_error = exc + raise LLMRunnerError( + f"claude -p failed after {self._max_retries + 1} attempts: {last_error}" + ) + + +class OpenAICompatRunner(LLMRunner): + """Run prompts against an OpenAI-compatible chat-completions endpoint.""" + + def __init__( + self, + model: str, + base_url: str, + *, + api_key: str | None = None, + timeout_seconds: float = 300.0, + max_retries: int = 2, + ) -> None: + self.model = model + self.base_url = base_url.rstrip("/") + self.spec = f"openai-compat:{model}@{self.base_url}" + self._api_key = api_key + self._timeout_seconds = timeout_seconds + self._max_retries = max_retries + + def complete(self, prompt: str) -> LLMResult: + headers = {"Content-Type": "application/json"} + if self._api_key: + headers["Authorization"] = f"Bearer {self._api_key}" + body = { + "model": self.model, + "messages": [{"role": "user", "content": prompt}], + "temperature": 0, + } + last_error: Exception | None = None + for _ in range(self._max_retries + 1): + started = time.perf_counter() + try: + response = httpx.post( + f"{self.base_url}/chat/completions", + json=body, + headers=headers, + timeout=self._timeout_seconds, + ) + response.raise_for_status() + payload = response.json() + usage = payload.get("usage") or {} + return LLMResult( + text=str(payload["choices"][0]["message"]["content"] or "").strip(), + model=self.model, + input_tokens=int(usage.get("prompt_tokens") or 0), + output_tokens=int(usage.get("completion_tokens") or 0), + latency_ms=(time.perf_counter() - started) * 1000.0, + ) + except (httpx.HTTPError, KeyError, IndexError, json.JSONDecodeError) as exc: + last_error = exc + raise LLMRunnerError( + f"openai-compat call to {self.base_url} failed after " + f"{self._max_retries + 1} attempts: {last_error}" + ) + + +def create_runner(spec: str, *, api_key: str | None = None) -> LLMRunner: + """Build a runner from a spec string. + + Formats: ``claude:`` or ``openai-compat:@``. + """ + transport, _, remainder = spec.partition(":") + if transport == "claude" and remainder: + return ClaudeCLIRunner(model=remainder) + if transport == "openai-compat" and remainder: + model, separator, base_url = remainder.partition("@") + if not separator or not model or not base_url: + raise ValueError( + f"openai-compat spec must be 'openai-compat:@', got: {spec}" + ) + return OpenAICompatRunner(model=model, base_url=base_url, api_key=api_key) + raise ValueError( + f"Unknown runner spec '{spec}'. Expected 'claude:' or " + f"'openai-compat:@'." + ) diff --git a/benchmarks/src/basic_memory_benchmarks/models.py b/benchmarks/src/basic_memory_benchmarks/models.py new file mode 100644 index 000000000..92eb92463 --- /dev/null +++ b/benchmarks/src/basic_memory_benchmarks/models.py @@ -0,0 +1,242 @@ +"""Core benchmark models and artifact schemas.""" + +from __future__ import annotations + +from datetime import datetime +from typing import Any, Literal + +from pydantic import BaseModel, Field + + +PROVIDER_STATE = Literal["ok", "skipped", "error"] + + +class DatasetProvenance(BaseModel): + dataset_id: str + source_url: str + checksum_sha256: str + license_note: str + fetched_at_utc: str + + +class QueryCase(BaseModel): + id: str + query: str + category: str + category_id: int | None = None + # Grouped datasets (LongMemEval) scope each query to its own corpus group. + group: str | None = None + ground_truth: list[str] = Field(default_factory=list) + expected_answer: str | None = None + metadata: dict[str, Any] = Field(default_factory=dict) + + +class SearchHit(BaseModel): + id: str | None = None + source_doc_id: str | None = None + source_path: str | None = None + text: str | None = None + score: float | None = None + metadata: dict[str, Any] = Field(default_factory=dict) + + +class RetrievalMetrics(BaseModel): + recall_at_5: float = 0.0 + recall_at_10: float = 0.0 + precision_at_5: float = 0.0 + mrr: float = 0.0 + content_hit_rate: float = 0.0 + mean_latency_ms: float = 0.0 + p95_latency_ms: float = 0.0 + query_count: int = 0 + + +class PerQueryRetrievalResult(BaseModel): + provider: str + query_id: str + query_text: str + category: str + category_id: int | None = None + ground_truth: list[str] = Field(default_factory=list) + expected_answer: str | None = None + hits: list[SearchHit] = Field(default_factory=list) + recall_at_5: float + recall_at_10: float + precision_at_5: float + mrr: float + content_hit: bool + latency_ms: float + top_hit_doc_id: str | None = None + retrieved_context: str = "" + metadata: dict[str, Any] = Field(default_factory=dict) + + +class RetrievalSummary(BaseModel): + provider: str + metrics: RetrievalMetrics + by_category: dict[str, RetrievalMetrics] = Field(default_factory=dict) + official_headline: RetrievalMetrics + adversarial_breakout: RetrievalMetrics + + +class JudgeCaseResult(BaseModel): + provider: str + query_id: str + category: str + passed: bool + score: float + reason: str + evaluator: str + + +class JudgeSummary(BaseModel): + provider: str + evaluator: str + model: str + total_cases: int + pass_count: int + accuracy: float + skipped_reason: str | None = None + + +class QACategoryMetrics(BaseModel): + total: int = 0 + correct: int = 0 + accuracy: float = 0.0 + + +class QACaseResult(BaseModel): + provider: str + query_id: str + category: str + question: str + expected_answer: str + generated_answer: str + abstained: bool + correct: bool + judge_reason: str + answer_model: str + judge_model: str + answer_latency_ms: float + answer_input_tokens: int + answer_output_tokens: int + # Size of the assembled prompt actually sent to the answerer. Runner + # token accounting is transport-dependent (the claude CLI buries the + # prompt in cache-creation alongside its own system overhead), so chars + # are the comparable cross-provider context-size measure. + answer_prompt_chars: int = 0 + error: str | None = None + + +class QASummary(BaseModel): + provider: str + answer_model: str + judge_model: str + total_cases: int + correct_count: int + error_count: int = 0 + abstain_count: int = 0 + accuracy: float + by_category: dict[str, QACategoryMetrics] = Field(default_factory=dict) + mean_answer_latency_ms: float = 0.0 + total_answer_input_tokens: int = 0 + total_answer_output_tokens: int = 0 + mean_answer_prompt_chars: float = 0.0 + skipped_reason: str | None = None + + +class CategoryDiagnosis(BaseModel): + """Per-category answerer-vs-retrieval attribution of QA failures.""" + + answerable: int = 0 + correct: int = 0 + retrieved_but_unused: int = 0 # gold retrieved, answer still wrong → answerer's fault + truly_missed: int = 0 # gold not retrieved → retrieval's fault + + +class ProviderDiagnosis(BaseModel): + """Attributes each provider's QA outcomes to retrieval vs the answerer. + + For every answerable question (non-empty ``ground_truth``) we join the QA + verdict with the retrieval row on ``(provider, query_id)``. A wrong answer + where the gold doc WAS retrieved (recall > 0) is an answerer failure + ("retrieved but unused"); a wrong answer where it was NOT retrieved is a + genuine retrieval miss. This separates "BM didn't find it" from "the fixed + answerer couldn't use what BM found" — the absolute QA number conflates the + two, and the answerer is held constant across providers. + """ + + provider: str + total_cases: int = 0 + answerable: int = 0 + unanswerable: int = 0 # empty ground_truth (abstention items) — no retrieval attribution + errored: int = 0 # QA-stage error (answerer/judge crashed) + unmatched: int = 0 # no retrieval row to join against + correct: int = 0 + retrieved_but_unused: int = 0 + truly_missed: int = 0 + # Derived shares over answerable questions (0.0 when answerable == 0): + qa_accuracy: float = 0.0 # correct / answerable + retrieval_ceiling: float = 0.0 # (correct + retrieved_but_unused) / answerable — max QA if answerer were perfect + answerer_gap: float = 0.0 # retrieved_but_unused / answerable — headroom left on the table by the answerer + retrieval_gap: float = 0.0 # truly_missed / answerable — headroom that needs better retrieval + answerer_failure_share: float = 0.0 # retrieved_but_unused / (answerable failures) — of what we got wrong, how much was the answerer + recall_field: str = "recall_at_10" + by_category: dict[str, CategoryDiagnosis] = Field(default_factory=dict) + + +class ProviderStatus(BaseModel): + provider: str + state: PROVIDER_STATE + reason: str | None = None + metadata: dict[str, Any] = Field(default_factory=dict) + + +class RuntimeInfo(BaseModel): + os: str + python_version: str + started_at_utc: str + + +class RunConfig(BaseModel): + run_id: str + dataset_id: str + dataset_path: str + corpus_dir: str + queries_path: str + output_root: str = "benchmarks/runs" + providers: list[str] = Field(default_factory=list) + top_k: int = 10 + bm_source: str = "github:basicmachines-co/basic-memory@main" + bm_local_path: str | None = None + judge_enabled: bool = False + judge_model: str = "gpt-4o-mini" + allow_provider_skip: bool = True + + +class RunManifest(BaseModel): + run_id: str + created_at_utc: str + benchmark_git_sha: str + bm_source: str + bm_resolved_sha: str | None = None + bm_local_path: str | None = None + mem0_version: str | None = None + provider_versions: dict[str, dict[str, str]] = Field(default_factory=dict) + dataset: DatasetProvenance + runtime: RuntimeInfo + config: RunConfig + + +class RunArtifacts(BaseModel): + manifest: RunManifest + provider_status: list[ProviderStatus] + retrieval_summaries: list[RetrievalSummary] + retrieval_rows: list[PerQueryRetrievalResult] + judge_summaries: list[JudgeSummary] = Field(default_factory=list) + judge_rows: list[JudgeCaseResult] = Field(default_factory=list) + fairness_warnings: list[str] = Field(default_factory=list) + + +def now_utc() -> str: + return datetime.utcnow().isoformat() + "Z" diff --git a/benchmarks/src/basic_memory_benchmarks/providers/__init__.py b/benchmarks/src/basic_memory_benchmarks/providers/__init__.py new file mode 100644 index 000000000..8c65dc7fb --- /dev/null +++ b/benchmarks/src/basic_memory_benchmarks/providers/__init__.py @@ -0,0 +1,43 @@ +"""Provider factory.""" + +from __future__ import annotations + +from basic_memory_benchmarks.providers.base import BenchmarkProvider +from basic_memory_benchmarks.providers.baseline_fullcontext import FullContextProvider +from basic_memory_benchmarks.providers.baseline_grep import FilesystemGrepProvider +from basic_memory_benchmarks.providers.bm_cloud import BasicMemoryCloudProvider +from basic_memory_benchmarks.providers.bm_local import BasicMemoryLocalProvider +from basic_memory_benchmarks.providers.mem0_local import Mem0LocalProvider +from basic_memory_benchmarks.providers.supermemory_local import SupermemoryLocalProvider +from basic_memory_benchmarks.providers.zep_reference import ZepReferenceProvider + + +def create_provider(name: str) -> BenchmarkProvider: + normalized = name.strip().lower() + if normalized == "bm-local": + return BasicMemoryLocalProvider() + if normalized == "bm-cloud": + return BasicMemoryCloudProvider() + if normalized == "mem0-local": + return Mem0LocalProvider() + if normalized == "zep-reference": + return ZepReferenceProvider() + if normalized == "baseline-grep": + return FilesystemGrepProvider() + if normalized == "baseline-fullcontext": + return FullContextProvider() + if normalized == "supermemory-local": + return SupermemoryLocalProvider() + raise ValueError(f"Unknown provider: {name}") + + +def provider_names() -> list[str]: + return [ + "bm-local", + "bm-cloud", + "mem0-local", + "zep-reference", + "baseline-grep", + "baseline-fullcontext", + "supermemory-local", + ] diff --git a/benchmarks/src/basic_memory_benchmarks/providers/base.py b/benchmarks/src/basic_memory_benchmarks/providers/base.py new file mode 100644 index 000000000..5daefee9a --- /dev/null +++ b/benchmarks/src/basic_memory_benchmarks/providers/base.py @@ -0,0 +1,31 @@ +"""Provider abstraction.""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from pathlib import Path + +from basic_memory_benchmarks.models import RunConfig, SearchHit + + +class BenchmarkProvider(ABC): + name: str + # Grouped runs: when True, one provider instance serves every group + # (ingest called per group; cleanup called once at end of run). When + # False, a fresh instance is created and cleaned up per group. + supports_group_reuse: bool = False + + @abstractmethod + def ingest(self, corpus_path: Path, run_config: RunConfig) -> None: + """Ingest corpus into provider-specific memory store.""" + + @abstractmethod + def search(self, query: str, limit: int, run_config: RunConfig) -> list[SearchHit]: + """Search for relevant memories and return normalized hits.""" + + @abstractmethod + def cleanup(self, run_config: RunConfig) -> None: + """Clean up any provider-scoped state.""" + + def version_info(self) -> dict[str, str]: + return {} diff --git a/benchmarks/src/basic_memory_benchmarks/providers/baseline_fullcontext.py b/benchmarks/src/basic_memory_benchmarks/providers/baseline_fullcontext.py new file mode 100644 index 000000000..e7794ed79 --- /dev/null +++ b/benchmarks/src/basic_memory_benchmarks/providers/baseline_fullcontext.py @@ -0,0 +1,74 @@ +"""Full-context baseline provider. + +The other honesty floor: skip retrieval entirely and hand the answering model +the whole corpus. On corpora that fit a modern context window (LoCoMo +conversations, LongMemEval-S groups), published results repeatedly show +full-context beating dedicated memory systems — so a memory system's QA +accuracy should be read against this number, and its token cost against this +provider's token cost. + +Retrieval metrics (recall/MRR) are meaningless for this provider by design: +it returns the corpus as a single hit with no doc-id claims. Use it for the +QA stage only. +""" + +from __future__ import annotations + +from pathlib import Path + +import frontmatter + +from basic_memory_benchmarks.models import RunConfig, SearchHit +from basic_memory_benchmarks.providers.base import BenchmarkProvider + +# Cap the assembled context so a pathological corpus cannot blow past the +# answering model's window. ~600K chars ≈ 150K tokens, inside a 200K window +# with room for prompt scaffolding. LongMemEval-S groups (~115K tokens) fit. +MAX_CONTEXT_CHARS = 600_000 + + +class FullContextProvider(BenchmarkProvider): + """No-retrieval baseline: the whole corpus is the context.""" + + name = "baseline-fullcontext" + + def __init__(self) -> None: + self._context: str = "" + self._truncated: bool = False + + def ingest(self, corpus_path: Path, run_config: RunConfig) -> None: + _ = run_config + sections: list[str] = [] + for note_path in sorted(corpus_path.rglob("*.md")): + with note_path.open("r", encoding="utf-8") as handle: + parsed = frontmatter.load(handle) + sections.append(parsed.content.strip()) + context = "\n\n---\n\n".join(sections) + self._truncated = len(context) > MAX_CONTEXT_CHARS + self._context = context[:MAX_CONTEXT_CHARS] + + def search(self, query: str, limit: int, run_config: RunConfig) -> list[SearchHit]: + _ = (query, limit, run_config) + if not self._context: + return [] + return [ + SearchHit( + id="full-context", + source_doc_id=None, + source_path=None, + text=self._context, + score=1.0, + metadata={"provider": self.name, "truncated": self._truncated}, + ) + ] + + def cleanup(self, run_config: RunConfig) -> None: + _ = run_config + self._context = "" + self._truncated = False + + def version_info(self) -> dict[str, str]: + return { + "baseline": "full-context", + "max_context_chars": str(MAX_CONTEXT_CHARS), + } diff --git a/benchmarks/src/basic_memory_benchmarks/providers/baseline_grep.py b/benchmarks/src/basic_memory_benchmarks/providers/baseline_grep.py new file mode 100644 index 000000000..6c361773b --- /dev/null +++ b/benchmarks/src/basic_memory_benchmarks/providers/baseline_grep.py @@ -0,0 +1,97 @@ +"""Filesystem grep baseline provider. + +The honesty floor for retrieval: case-insensitive term matching over the raw +corpus markdown with TF-style scoring and no index, no embeddings, no LLM. +A memory system that can't beat this isn't adding retrieval value. (Letta's +"filesystem + grep agent" scored 74% on LoCoMo; this is the non-agentic +deterministic analogue.) +""" + +from __future__ import annotations + +import math +import re +from pathlib import Path + +import frontmatter + +from basic_memory_benchmarks.models import RunConfig, SearchHit +from basic_memory_benchmarks.providers.base import BenchmarkProvider + +_TOKEN_PATTERN = re.compile(r"[a-z0-9][a-z0-9'\-]*") + +# Minimal English stopword set so query scoring keys on content-bearing terms. +_STOPWORDS = frozenset( + "a an and are as at be but by did do does for from had has have how i in is it of on or " + "that the their they this to was we were what when where which who whom whose why will " + "with you your".split() +) + + +def _tokenize(text: str) -> list[str]: + return _TOKEN_PATTERN.findall(text.lower()) + + +class FilesystemGrepProvider(BenchmarkProvider): + """Deterministic grep-style ranking over corpus files.""" + + name = "baseline-grep" + + def __init__(self) -> None: + self._docs: list[tuple[str, str, str]] = [] # (doc_id, rel_path, body) + + def ingest(self, corpus_path: Path, run_config: RunConfig) -> None: + _ = run_config + self._docs = [] + for note_path in sorted(corpus_path.rglob("*.md")): + with note_path.open("r", encoding="utf-8") as handle: + parsed = frontmatter.load(handle) + doc_id = str(parsed.get("source_doc_id") or note_path.stem) + rel_path = note_path.relative_to(corpus_path).as_posix() + self._docs.append((doc_id, rel_path, parsed.content)) + + def search(self, query: str, limit: int, run_config: RunConfig) -> list[SearchHit]: + _ = run_config + terms = [t for t in _tokenize(query) if t not in _STOPWORDS] + if not terms: + return [] + + scored: list[tuple[float, str, str, str]] = [] + for doc_id, rel_path, body in self._docs: + body_lower = body.lower() + # TF with log damping per term; coverage bonus rewards docs + # matching more distinct query terms over many hits of one term. + matched = 0 + score = 0.0 + for term in terms: + count = body_lower.count(term) + if count: + matched += 1 + score += 1.0 + math.log(count) + if matched: + # Squared coverage: a doc matching every distinct query term + # must outrank a doc spamming one term many times. + score *= (matched / len(terms)) ** 2 + scored.append((score, doc_id, rel_path, body)) + + scored.sort(key=lambda item: item[0], reverse=True) + hits: list[SearchHit] = [] + for score, doc_id, rel_path, body in scored[:limit]: + hits.append( + SearchHit( + id=doc_id, + source_doc_id=doc_id, + source_path=rel_path, + text=body[:2000], + score=score, + metadata={"provider": self.name}, + ) + ) + return hits + + def cleanup(self, run_config: RunConfig) -> None: + _ = run_config + self._docs = [] + + def version_info(self) -> dict[str, str]: + return {"baseline": "filesystem-grep", "index": "none"} diff --git a/benchmarks/src/basic_memory_benchmarks/providers/bm_cloud.py b/benchmarks/src/basic_memory_benchmarks/providers/bm_cloud.py new file mode 100644 index 000000000..4edd54ba0 --- /dev/null +++ b/benchmarks/src/basic_memory_benchmarks/providers/bm_cloud.py @@ -0,0 +1,88 @@ +"""Basic Memory cloud provider (optional).""" + +from __future__ import annotations + +import json +import os +from pathlib import Path +from typing import Any, cast + +from basic_memory_benchmarks.exceptions import ProviderSkippedError +from basic_memory_benchmarks.models import RunConfig, SearchHit +from basic_memory_benchmarks.providers.base import BenchmarkProvider +from basic_memory_benchmarks.utils import run_command + + +class BasicMemoryCloudProvider(BenchmarkProvider): + name = "bm-cloud" + + def _enabled(self) -> bool: + return os.getenv("BASIC_MEMORY_BENCH_ENABLE_CLOUD", "").lower() in {"1", "true", "yes"} + + def _project_name(self, run_config: RunConfig) -> str: + return f"bm-bench-{run_config.run_id}" + + def ingest(self, corpus_path: Path, run_config: RunConfig) -> None: + _ = corpus_path + _ = run_config + if not self._enabled(): + raise ProviderSkippedError( + "Cloud benchmark disabled. Set BASIC_MEMORY_BENCH_ENABLE_CLOUD=true to enable." + ) + + def search(self, query: str, limit: int, run_config: RunConfig) -> list[SearchHit]: + if not self._enabled(): + raise ProviderSkippedError( + "Cloud benchmark disabled. Set BASIC_MEMORY_BENCH_ENABLE_CLOUD=true to enable." + ) + + project_name = self._project_name(run_config) + completed = run_command( + [ + "bm", + "tool", + "search-notes", + query, + "--project", + project_name, + "--page-size", + str(limit), + "--hybrid", + "--cloud", + ] + ) + payload = json.loads(completed.stdout.strip() or "{}") + rows = payload.get("results") if isinstance(payload, dict) else [] + + hits: list[SearchHit] = [] + for row in rows or []: + if not isinstance(row, dict): + continue + raw_path = row.get("file_path") or row.get("permalink") + doc_id = None + if raw_path: + tail = str(raw_path).rstrip("/").split("/")[-1] + doc_id = tail[:-3] if tail.endswith(".md") else tail + metadata_raw = row.get("metadata") + metadata: dict[str, Any] + if isinstance(metadata_raw, dict): + metadata = cast(dict[str, Any], metadata_raw) + else: + metadata = {} + hits.append( + SearchHit( + id=str(row.get("entity_id") or ""), + source_doc_id=doc_id, + source_path=raw_path, + text=row.get("matched_chunk") or row.get("content"), + score=float(row.get("score", 0.0) or 0.0), + metadata=metadata, + ) + ) + return hits + + def cleanup(self, run_config: RunConfig) -> None: + _ = run_config + + def version_info(self) -> dict[str, str]: + return {} diff --git a/benchmarks/src/basic_memory_benchmarks/providers/bm_local.py b/benchmarks/src/basic_memory_benchmarks/providers/bm_local.py new file mode 100644 index 000000000..c71016dd0 --- /dev/null +++ b/benchmarks/src/basic_memory_benchmarks/providers/bm_local.py @@ -0,0 +1,424 @@ +"""Basic Memory local provider via warm MCP stdio contract.""" + +from __future__ import annotations + +import asyncio +import json +import os +import re +import subprocess +import tempfile +import threading +import time +from concurrent.futures import Future +from dataclasses import dataclass +from pathlib import Path +from queue import Queue +from typing import Any, cast + +import anyio +from mcp.client.session import ClientSession +from mcp.client.stdio import StdioServerParameters, stdio_client +from mcp.types import CallToolResult + +from basic_memory_benchmarks.models import RunConfig, SearchHit +from basic_memory_benchmarks.providers.base import BenchmarkProvider +from basic_memory_benchmarks.utils import run_command + + +@dataclass +class _McpToolRequest: + name: str + arguments: dict[str, Any] + response: Future[CallToolResult] + + +class _WarmMcpClient: + def __init__( + self, + *, + command: str = "bm", + args: list[str] | None = None, + env: dict[str, str] | None = None, + startup_timeout_seconds: float = 30.0, + request_timeout_seconds: float = 60.0, + ) -> None: + self._command = command + self._args = args or ["mcp"] + self._env = env + self._startup_timeout_seconds = startup_timeout_seconds + self._request_timeout_seconds = request_timeout_seconds + self._requests: Queue[_McpToolRequest | None] = Queue() + self._ready = threading.Event() + self._startup_error: Exception | None = None + self._thread: threading.Thread | None = None + + async def _serve(self) -> None: + params = StdioServerParameters(command=self._command, args=self._args, env=self._env) + async with stdio_client(params) as (read_stream, write_stream): + async with ClientSession(read_stream, write_stream) as session: + await session.initialize() + tools = await session.list_tools() + tool_names = {tool.name for tool in tools.tools} + if "search_notes" not in tool_names: + raise RuntimeError("bm mcp server does not expose 'search_notes'") + + self._ready.set() + + while True: + loop = asyncio.get_running_loop() + request = await loop.run_in_executor(None, self._requests.get) + if request is None: + break + try: + result = await session.call_tool(request.name, request.arguments) + except Exception as exc: + request.response.set_exception(exc) + else: + request.response.set_result(result) + + def _thread_main(self) -> None: + try: + anyio.run(self._serve) + except Exception as exc: + self._startup_error = exc + self._ready.set() + + def start(self) -> None: + if self._thread is not None and self._thread.is_alive(): + return + self._thread = threading.Thread( + target=self._thread_main, + name="bm-benchmark-mcp-client", + daemon=True, + ) + self._thread.start() + + if not self._ready.wait(timeout=self._startup_timeout_seconds): + raise TimeoutError("Timed out starting bm mcp session") + if self._startup_error is not None: + raise RuntimeError("Failed to start bm mcp session") from self._startup_error + + def call_tool(self, name: str, arguments: dict[str, Any]) -> CallToolResult: + if self._thread is None or not self._thread.is_alive(): + raise RuntimeError("bm mcp session is not running") + + response: Future[CallToolResult] = Future() + self._requests.put(_McpToolRequest(name=name, arguments=arguments, response=response)) + return response.result(timeout=self._request_timeout_seconds) + + def stop(self) -> None: + if self._thread is None: + return + if self._thread.is_alive(): + self._requests.put(None) + self._thread.join(timeout=self._startup_timeout_seconds) + self._thread = None + + +class BasicMemoryLocalProvider(BenchmarkProvider): + name = "bm-local" + # One instance serves every group in a grouped run: the warm MCP session + # and isolated config dir are shared, projects are per-group. + supports_group_reuse = True + + def __init__(self) -> None: + # run_id -> resolved project name (grouped runs ingest many projects). + self._resolved_project_names: dict[str, str] = {} + self._status_json_supported: bool | None = None + self._mcp: _WarmMcpClient | None = None + self._bm_command_prefix: list[str] = ["bm"] + self._bm_env: dict[str, str] | None = None + self._config_dir: Path | None = None + + def _isolated_bm_env(self) -> dict[str, str]: + # The benchmark must not depend on (or mutate) the operator's personal + # Basic Memory config — e.g. a cloud-mode setup would route search_notes + # through cloud.basicmemory.com. BASIC_MEMORY_CONFIG_DIR scopes config, + # database, and project registry to a benchmark-owned directory. + # + # The directory is FRESH per provider instance: a persistent shared + # home rots across basic-memory versions (alembic migrations from a + # newer dev build brick older binaries) and leaks projects between + # runs. BASIC_MEMORY_HOME is dropped for the same reason. + if self._config_dir is None: + root = Path("benchmarks/.bm-homes") + root.mkdir(parents=True, exist_ok=True) + self._config_dir = Path(tempfile.mkdtemp(prefix="bm-home-", dir=root)) + env = dict(os.environ) + env.pop("BASIC_MEMORY_CLOUD_MODE", None) + env.pop("BASIC_MEMORY_HOME", None) + env["BASIC_MEMORY_CONFIG_DIR"] = str(self._config_dir) + return env + + def _project_name(self, run_config: RunConfig) -> str: + resolved = self._resolved_project_names.get(run_config.run_id) + if resolved is not None: + return resolved + return f"bm-bench-{run_config.run_id}" + + @staticmethod + def _resolve_bm_command_prefix(run_config: RunConfig) -> list[str]: + if run_config.bm_local_path: + local_path = Path(run_config.bm_local_path) + if not local_path.exists(): + raise ValueError(f"--bm-local-path not found: {local_path}") + return ["uv", "run", "--project", str(local_path), "basic-memory"] + return ["bm"] + + def _run_bm( + self, + args: list[str], + *, + check: bool = True, + ) -> subprocess.CompletedProcess: + return run_command(self._bm_command_prefix + args, check=check, env=self._bm_env) + + @staticmethod + def _extract_existing_project_name(message: str) -> str | None: + match = re.search(r"existing project\s+'([^']+)'", message, flags=re.IGNORECASE) + if match: + return match.group(1) + return None + + @staticmethod + def _status_json_is_ready(payload: dict[str, Any]) -> bool: + total = payload.get("total") + if isinstance(total, int): + return total == 0 + + for list_key in ("new", "modified", "deleted", "skipped_files"): + value = payload.get(list_key) + if isinstance(value, list) and len(value) > 0: + return False + + for dict_key in ("moves", "checksums"): + value = payload.get(dict_key) + if isinstance(value, dict) and len(value) > 0: + return False + + status = payload.get("status") + if isinstance(status, str): + lowered = status.lower() + if "no changes" in lowered or "up to date" in lowered: + return True + if "sync" in lowered or "index" in lowered or "pending" in lowered: + return False + + for key in ("is_syncing", "is_indexing", "sync_in_progress", "index_in_progress"): + value = payload.get(key) + if isinstance(value, bool): + return not value + + for key in ("pending_files", "pending", "unindexed_files", "queued_files", "queue_size"): + value = payload.get(key) + if isinstance(value, int) and value != 0: + return False + + # If the schema is unknown and no busy signal exists, treat status as ready. + return True + + @staticmethod + def _error_text_from_result(result: CallToolResult) -> str: + for item in result.content: + text = getattr(item, "text", None) + if isinstance(text, str) and text.strip(): + return text.strip() + return "Unknown MCP tool error" + + @classmethod + def _payload_from_call_tool_result(cls, result: CallToolResult) -> dict[str, Any]: + if result.isError: + raise RuntimeError(cls._error_text_from_result(result)) + + structured = result.structuredContent + if isinstance(structured, dict): + wrapped = structured.get("result") + if isinstance(wrapped, dict): + return cast(dict[str, Any], wrapped) + return cast(dict[str, Any], structured) + + for item in result.content: + text = getattr(item, "text", None) + if not isinstance(text, str): + continue + maybe_json = text.strip() + if not maybe_json: + continue + try: + parsed = json.loads(maybe_json) + except json.JSONDecodeError: + continue + if isinstance(parsed, dict): + return cast(dict[str, Any], parsed) + + return {} + + def _supports_status_json(self) -> bool: + if self._status_json_supported is not None: + return self._status_json_supported + + probe = self._run_bm(["status", "--json", "--local"], check=False) + merged = ((probe.stdout or "") + "\n" + (probe.stderr or "")).lower() + self._status_json_supported = "no such option: --json" not in merged + return self._status_json_supported + + def _wait_for_index_ready(self, project_name: str) -> None: + if not self._supports_status_json(): + return + + deadline = time.monotonic() + 120.0 + delay = 0.25 + while True: + completed = self._run_bm( + [ + "status", + "--project", + project_name, + "--json", + "--local", + ] + ) + payload = json.loads(completed.stdout.strip() or "{}") + if isinstance(payload, dict) and self._status_json_is_ready( + cast(dict[str, Any], payload) + ): + return + + if time.monotonic() >= deadline: + raise TimeoutError( + f"Timed out waiting for bm status --json readiness for project '{project_name}'" + ) + # Small corpora index in well under a second; start polling fast + # and back off instead of paying a fixed 2s floor per group. + time.sleep(delay) + delay = min(delay * 2, 2.0) + + def ingest(self, corpus_path: Path, run_config: RunConfig) -> None: + self._bm_command_prefix = self._resolve_bm_command_prefix(run_config) + self._bm_env = self._isolated_bm_env() + self._status_json_supported = None + project_name = self._project_name(run_config) + + # Trigger: benchmark corpus needs indexing in BM + # Why: keep provider path external to BM internals + # Outcome: project exists, index is updated, and warm MCP session is ready + add_args = ["project", "add", project_name, str(corpus_path)] + try: + self._run_bm(add_args) + except subprocess.CalledProcessError as exc: + merged = (exc.stdout or "") + "\n" + (exc.stderr or "") + merged_lower = merged.lower() + if "already exists" in merged_lower: + pass + else: + existing = self._extract_existing_project_name(merged) + if existing is None: + raise + project_name = existing + + self._resolved_project_names[run_config.run_id] = project_name + + try: + self._run_bm(["reindex", "--search", "--embeddings", "-p", project_name]) + except subprocess.CalledProcessError: + self._run_bm(["reindex", "--search", "-p", project_name]) + + self._wait_for_index_ready(project_name) + + # Trigger: first ingest of this provider instance + # Why: an MCP session serves projects added after it started, so one + # warm session covers every group in a grouped run — restarting it + # per group wastes seconds x hundreds of groups. + # Outcome: session starts once; cleanup() at end of run stops it. + if self._mcp is None: + mcp_command = self._bm_command_prefix[0] + mcp_args = self._bm_command_prefix[1:] + ["mcp"] + self._mcp = _WarmMcpClient(command=mcp_command, args=mcp_args, env=self._bm_env) + self._mcp.start() + + @staticmethod + def _doc_id_from_item(item: dict) -> str | None: + raw = item.get("source_doc_id") or item.get("permalink") or item.get("file_path") + if not raw: + return None + name = str(raw).rstrip("/").split("/")[-1] + if name.endswith(".md"): + name = name[:-3] + return name + + @classmethod + def _row_to_hit(cls, row: dict[str, Any]) -> SearchHit: + """Normalize a search_notes result row into a SearchHit. + + Carries the document title into hit metadata: BM's matched_chunk is a + single bullet that strips document context, but the title holds it (for + LoCoMo/LongMemEval the session date lives there, e.g. + ``locomo-c00-s07 (4:33 pm on 12 July, 2023)``). Surfacing it lets the QA + assembler anchor relative dates ("two days ago") that bullets alone + leave unresolvable. + """ + metadata_raw = row.get("metadata") + metadata: dict[str, Any] + if isinstance(metadata_raw, dict): + metadata = cast(dict[str, Any], metadata_raw) + else: + metadata = {} + if row.get("title"): + metadata = {**metadata, "title": str(row["title"])} + return SearchHit( + id=str( + row.get("entity_id") or row.get("observation_id") or row.get("relation_id") or "" + ), + source_doc_id=cls._doc_id_from_item(row), + source_path=row.get("file_path") or row.get("permalink"), + text=row.get("matched_chunk") or row.get("content"), + score=float(row.get("score", 0.0) or 0.0), + metadata=metadata, + ) + + def search(self, query: str, limit: int, run_config: RunConfig) -> list[SearchHit]: + if self._mcp is None: + raise RuntimeError("bm-local MCP session is not initialized") + + project_name = self._project_name(run_config) + tool_result = self._mcp.call_tool( + "search_notes", + { + "query": query, + "project": project_name, + "page": 1, + "page_size": limit, + "search_type": "hybrid", + "output_format": "json", + }, + ) + payload = self._payload_from_call_tool_result(tool_result) + rows = payload.get("results") if isinstance(payload, dict) else [] + hits: list[SearchHit] = [] + for row in rows or []: + if not isinstance(row, dict): + continue + hits.append(self._row_to_hit(row)) + return hits + + def cleanup(self, run_config: RunConfig) -> None: + _ = run_config + if self._mcp is None: + return + self._mcp.stop() + self._mcp = None + + def version_info(self) -> dict[str, str]: + metadata: dict[str, str] = {"bm_transport": "mcp-stdio"} + if self._status_json_supported is not None: + metadata["bm_status_json_supported"] = ( + "true" if self._status_json_supported else "false" + ) + metadata["bm_command"] = " ".join(self._bm_command_prefix) + try: + result = self._run_bm(["--version"]) + metadata["bm_version"] = result.stdout.strip() + except Exception: + pass + return metadata diff --git a/benchmarks/src/basic_memory_benchmarks/providers/mem0_local.py b/benchmarks/src/basic_memory_benchmarks/providers/mem0_local.py new file mode 100644 index 000000000..80f8cee9c --- /dev/null +++ b/benchmarks/src/basic_memory_benchmarks/providers/mem0_local.py @@ -0,0 +1,213 @@ +"""Mem0 local provider using the mem0ai package. + +Model backends, in priority order: + +1. **Local OpenAI-compatible endpoint** (zero API spend): set + ``MEM0_OPENAI_COMPAT_BASE_URL`` (e.g. ``http://localhost:11434/v1`` for + Ollama). LLM and embeddings both route there via mem0's ``openai`` + provider, and the qdrant store is configured to the local embedder's + dimensions under a benchmark-scoped on-disk path. +2. **OpenAI** (mem0's defaults): ``OPENAI_API_KEY`` set, no base-url override. + +With neither configured the provider is skipped. +""" + +from __future__ import annotations + +import os +from importlib.metadata import version +from pathlib import Path +from typing import Any, cast + +import frontmatter + +from basic_memory_benchmarks.exceptions import ProviderSkippedError +from basic_memory_benchmarks.models import RunConfig, SearchHit +from basic_memory_benchmarks.providers.base import BenchmarkProvider + +# Defaults for the local backend; override via env. +_DEFAULT_LOCAL_LLM_MODEL = "qwen2.5:3b" +_DEFAULT_LOCAL_EMBED_MODEL = "nomic-embed-text" +_DEFAULT_LOCAL_EMBED_DIMS = 768 + + +class Mem0LocalProvider(BenchmarkProvider): + name = "mem0-local" + + def __init__(self) -> None: + self._memory = None + self._backend: str | None = None + self._infer: bool = os.getenv("MEM0_INFER", "false").strip().lower() == "true" + + def _user_id(self, run_config: RunConfig) -> str: + return f"bm-bench-{run_config.run_id}-mem0" + + def _local_config(self, base_url: str, run_config: RunConfig) -> dict: + api_key = os.getenv("MEM0_OPENAI_COMPAT_API_KEY", "local") + embed_dims = int(os.getenv("MEM0_EMBED_DIMS", str(_DEFAULT_LOCAL_EMBED_DIMS))) + qdrant_root = os.getenv("MEM0_QDRANT_PATH", "benchmarks/.mem0-qdrant") + # Collection + path are run-scoped: local embedding dims differ from + # OpenAI's, and qdrant rejects dim changes within a collection. + collection = f"bm_bench_{run_config.run_id}".replace("-", "_") + return { + "llm": { + "provider": "openai", + "config": { + "model": os.getenv("MEM0_LLM_MODEL", _DEFAULT_LOCAL_LLM_MODEL), + "openai_base_url": base_url, + "api_key": api_key, + "temperature": 0, + }, + }, + "embedder": { + "provider": "openai", + "config": { + "model": os.getenv("MEM0_EMBED_MODEL", _DEFAULT_LOCAL_EMBED_MODEL), + "openai_base_url": base_url, + "api_key": api_key, + "embedding_dims": embed_dims, + }, + }, + "vector_store": { + "provider": "qdrant", + "config": { + "collection_name": collection, + "embedding_model_dims": embed_dims, + "path": str(Path(qdrant_root) / collection), + }, + }, + } + + def _ensure_memory(self, run_config: RunConfig): + if self._memory is not None: + return self._memory + + # Trigger: mem0 with MEM0_TELEMETRY on (its default) opens a qdrant + # client at a FIXED path (~/.mem0/migrations_qdrant) inside every + # Memory(); qdrant local mode allows one client per path per process, + # so the second provider instance in a grouped run dies with + # "Storage folder ... is already accessed" (matrix v1: 24/25 LME and + # 30/30 ConvoMem groups lost). + # Why: benchmark runs should not emit telemetry anyway. + # Outcome: telemetry store never created; operator can force-enable. + os.environ.setdefault("MEM0_TELEMETRY", "false") + + from mem0 import Memory # Deferred import to keep startup lightweight + + base_url = os.getenv("MEM0_OPENAI_COMPAT_BASE_URL") + if base_url: + self._backend = f"openai-compat:{base_url}" + self._memory = Memory.from_config(self._local_config(base_url, run_config)) + elif os.getenv("OPENAI_API_KEY"): + self._backend = "openai-default" + self._memory = Memory() + else: + raise ProviderSkippedError( + "mem0-local needs MEM0_OPENAI_COMPAT_BASE_URL (local endpoint) or OPENAI_API_KEY" + ) + return self._memory + + @staticmethod + def _doc_id_from_path(path: Path) -> str: + if path.suffix == ".md": + return path.name[:-3] + return path.name + + @staticmethod + def _conversation_id(path: Path) -> str: + parts = list(path.parts) + for part in parts: + if part.startswith("locomo-c"): + return part + return "unknown" + + def ingest(self, corpus_path: Path, run_config: RunConfig) -> None: + memory = self._ensure_memory(run_config) + user_id = self._user_id(run_config) + + for note_path in sorted(corpus_path.rglob("*.md")): + rel_path = note_path.relative_to(corpus_path).as_posix() + with note_path.open("r", encoding="utf-8") as handle: + parsed = frontmatter.load(handle) + doc_id = str(parsed.get("source_doc_id") or self._doc_id_from_path(note_path)) + conversation_id = str(parsed.get("conversation_id") or self._conversation_id(note_path)) + metadata = { + "source_doc_id": doc_id, + "source_path": rel_path, + "conversation_id": conversation_id, + "dataset_id": run_config.dataset_id, + } + memory.add(parsed.content, user_id=user_id, metadata=metadata, infer=self._infer) + + @staticmethod + def _normalize_item(item: dict) -> SearchHit: + metadata_raw = item.get("metadata") + metadata: dict[str, Any] + if isinstance(metadata_raw, dict): + metadata = cast(dict[str, Any], metadata_raw) + else: + metadata = {} + source_doc_id = item.get("source_doc_id") or metadata.get("source_doc_id") + source_path = item.get("source_path") or metadata.get("source_path") + score_raw = item.get("score") + try: + score = float(score_raw) if score_raw is not None else None + except (TypeError, ValueError): + score = None + + return SearchHit( + id=str(item.get("id") or ""), + source_doc_id=source_doc_id, + source_path=source_path, + text=item.get("memory") or item.get("text"), + score=score, + metadata=metadata, + ) + + def search(self, query: str, limit: int, run_config: RunConfig) -> list[SearchHit]: + memory = self._ensure_memory(run_config) + user_id = self._user_id(run_config) + # mem0ai 2.0: entity scoping moved from top-level user_id= to filters=, + # and limit= became top_k=. + payload = memory.search(query=query, top_k=limit, filters={"user_id": user_id}) + rows = payload.get("results") if isinstance(payload, dict) else [] + + hits: list[SearchHit] = [] + for item in rows or []: + if isinstance(item, dict): + hits.append(self._normalize_item(item)) + return hits + + def cleanup(self, run_config: RunConfig) -> None: + if self._memory is None: + return + try: + self._memory.delete_all(user_id=self._user_id(run_config)) + except Exception: + # Cleanup should never break the main benchmark flow. + pass + # Release qdrant local-mode file locks even while this instance is + # still referenced (the grouped runner keeps the last provider for + # version_info), or the next instance cannot open its stores. + for store_attr in ("vector_store", "_telemetry_vector_store"): + client = getattr(getattr(self._memory, store_attr, None), "client", None) + if client is not None and hasattr(client, "close"): + try: + client.close() + except Exception: + pass + self._memory = None + + def version_info(self) -> dict[str, str]: + metadata: dict[str, str] = { + "mem0_backend": self._backend or "unconfigured", + "mem0_infer": "true" if self._infer else "false", + } + if self._backend and self._backend.startswith("openai-compat"): + metadata["mem0_llm_model"] = os.getenv("MEM0_LLM_MODEL", _DEFAULT_LOCAL_LLM_MODEL) + metadata["mem0_embed_model"] = os.getenv("MEM0_EMBED_MODEL", _DEFAULT_LOCAL_EMBED_MODEL) + try: + metadata["mem0ai"] = version("mem0ai") + except Exception: + pass + return metadata diff --git a/benchmarks/src/basic_memory_benchmarks/providers/supermemory_local.py b/benchmarks/src/basic_memory_benchmarks/providers/supermemory_local.py new file mode 100644 index 000000000..7e588e5b1 --- /dev/null +++ b/benchmarks/src/basic_memory_benchmarks/providers/supermemory_local.py @@ -0,0 +1,199 @@ +"""supermemory self-hosted server provider. + +Targets supermemory-server (github.com/supermemoryai/supermemory, local +binary serving the Memory API on localhost:6767). The provider does not +manage the server process — the operator starts it and supplies: + +- ``SUPERMEMORY_BASE_URL`` (default ``http://localhost:6767``) +- ``SUPERMEMORY_API_KEY`` (the ``sm_...`` key printed on the server's + first boot; required — without it the provider is skipped) + +Ingestion is async on the server (queued → extracting → chunking → +embedding → done | failed), so ingest() polls every document to a terminal +state before returning; searching before that point silently misses +content. Failed documents are reaped server-side after ~2 minutes, so a +poll 404 is treated as failed. + +Benchmark scoping uses one container tag per run id; grouped runs get a +fresh tag per group automatically. cleanup() bulk-deletes the container. +""" + +from __future__ import annotations + +import os +import time +from pathlib import Path +from typing import Any + +import frontmatter +import httpx + +from basic_memory_benchmarks.exceptions import ProviderSkippedError +from basic_memory_benchmarks.models import RunConfig, SearchHit +from basic_memory_benchmarks.providers.base import BenchmarkProvider + +DEFAULT_BASE_URL = "http://localhost:6767" +_TERMINAL_STATUSES = {"done", "failed"} +_POLL_INTERVAL_SECONDS = 1.0 + + +class SupermemoryLocalProvider(BenchmarkProvider): + name = "supermemory-local" + + def __init__(self, transport: httpx.BaseTransport | None = None) -> None: + self._transport = transport + self._client: httpx.Client | None = None + self._ingest_timeout_seconds = float(os.getenv("SUPERMEMORY_INGEST_TIMEOUT_S", "900")) + self._ingested_failures: list[str] = [] + + def _container_tag(self, run_config: RunConfig) -> str: + return f"bm-bench-{run_config.run_id}" + + def _ensure_client(self) -> httpx.Client: + if self._client is not None: + return self._client + + api_key = os.getenv("SUPERMEMORY_API_KEY") + if not api_key: + raise ProviderSkippedError( + "SUPERMEMORY_API_KEY missing for supermemory-local provider " + "(printed on supermemory-server first boot)" + ) + base_url = os.getenv("SUPERMEMORY_BASE_URL", DEFAULT_BASE_URL) + self._client = httpx.Client( + base_url=base_url, + headers={"Authorization": f"Bearer {api_key}"}, + timeout=60.0, + transport=self._transport, + ) + + # Fail fast (and skip cleanly) when the server isn't up at all. + try: + self._client.post("/v3/documents/list", json={"limit": 1, "page": 1}) + except httpx.ConnectError as exc: + client = self._client + self._client = None + client.close() + raise ProviderSkippedError( + f"supermemory-server unreachable at {base_url}: {exc}" + ) from exc + return self._client + + def ingest(self, corpus_path: Path, run_config: RunConfig) -> None: + client = self._ensure_client() + container_tag = self._container_tag(run_config) + self._ingested_failures = [] + + pending: dict[str, str] = {} # server doc id -> our doc id + for note_path in sorted(corpus_path.rglob("*.md")): + rel_path = note_path.relative_to(corpus_path).as_posix() + with note_path.open("r", encoding="utf-8") as handle: + parsed = frontmatter.load(handle) + doc_id = str(parsed.get("source_doc_id") or note_path.stem) + response = client.post( + "/v3/documents", + json={ + "content": parsed.content, + "customId": doc_id, + "containerTags": [container_tag], + "metadata": { + "source_doc_id": doc_id, + "source_path": rel_path, + "dataset_id": run_config.dataset_id, + }, + }, + ) + response.raise_for_status() + server_id = str(response.json().get("id") or "") + if not server_id: + raise RuntimeError(f"supermemory add returned no id for {doc_id}") + pending[server_id] = doc_id + + # Ingestion is async server-side; wait for every document to reach a + # terminal state or searches will silently miss content. + deadline = time.monotonic() + self._ingest_timeout_seconds + while pending: + if time.monotonic() >= deadline: + raise TimeoutError( + f"supermemory ingestion timed out with {len(pending)} documents " + f"still processing (timeout {self._ingest_timeout_seconds}s)" + ) + for server_id, doc_id in list(pending.items()): + response = client.get(f"/v3/documents/{server_id}") + if response.status_code == 404: + # Failed documents are reaped server-side after ~2 minutes. + self._ingested_failures.append(doc_id) + del pending[server_id] + continue + response.raise_for_status() + status = str(response.json().get("status") or "") + if status in _TERMINAL_STATUSES: + if status == "failed": + self._ingested_failures.append(doc_id) + del pending[server_id] + if pending: + time.sleep(_POLL_INTERVAL_SECONDS) + + if self._ingested_failures: + # Partial ingestion silently skews retrieval metrics; fail the + # group/run loudly instead. + raise RuntimeError( + f"supermemory failed to ingest {len(self._ingested_failures)} documents: " + f"{sorted(self._ingested_failures)[:10]}" + ) + + @staticmethod + def _normalize_result(item: dict) -> SearchHit: + metadata_raw = item.get("metadata") + metadata: dict[str, Any] = metadata_raw if isinstance(metadata_raw, dict) else {} + chunks = item.get("chunks") or [] + chunk_texts = [ + str(chunk.get("content") or "") + for chunk in chunks + if isinstance(chunk, dict) and chunk.get("content") + ] + return SearchHit( + id=str(item.get("documentId") or ""), + source_doc_id=metadata.get("source_doc_id") or item.get("customId"), + source_path=metadata.get("source_path"), + text="\n".join(chunk_texts) if chunk_texts else item.get("title"), + score=float(item.get("score") or 0.0), + metadata=metadata, + ) + + def search(self, query: str, limit: int, run_config: RunConfig) -> list[SearchHit]: + client = self._ensure_client() + response = client.post( + "/v3/search", + json={ + "q": query, + "limit": limit, + "containerTags": [self._container_tag(run_config)], + }, + ) + response.raise_for_status() + rows = response.json().get("results") or [] + return [self._normalize_result(item) for item in rows if isinstance(item, dict)] + + def cleanup(self, run_config: RunConfig) -> None: + if self._client is None: + return + try: + self._client.request( + "DELETE", + "/v3/documents/bulk", + json={"containerTags": [self._container_tag(run_config)]}, + ) + except Exception: + # Cleanup should never break the main benchmark flow. + pass + finally: + self._client.close() + self._client = None + + def version_info(self) -> dict[str, str]: + return { + "supermemory_base_url": os.getenv("SUPERMEMORY_BASE_URL", DEFAULT_BASE_URL), + "supermemory_server_version": os.getenv("SUPERMEMORY_SERVER_VERSION", "unknown"), + "supermemory_transport": "http-v3", + } diff --git a/benchmarks/src/basic_memory_benchmarks/providers/zep_reference.py b/benchmarks/src/basic_memory_benchmarks/providers/zep_reference.py new file mode 100644 index 000000000..ef72c2440 --- /dev/null +++ b/benchmarks/src/basic_memory_benchmarks/providers/zep_reference.py @@ -0,0 +1,28 @@ +"""Reference-only Zep provider. + +This provider is intentionally non-live for v1. It exists to keep report schema stable +for externally published benchmark references. +""" + +from __future__ import annotations + +from basic_memory_benchmarks.exceptions import ProviderSkippedError +from basic_memory_benchmarks.models import RunConfig, SearchHit +from basic_memory_benchmarks.providers.base import BenchmarkProvider + + +class ZepReferenceProvider(BenchmarkProvider): + name = "zep-reference" + + def ingest(self, corpus_path, run_config: RunConfig) -> None: + _ = corpus_path + _ = run_config + + def search(self, query: str, limit: int, run_config: RunConfig) -> list[SearchHit]: + _ = query + _ = limit + _ = run_config + raise ProviderSkippedError("Zep is reference-only in v1 and does not execute live queries") + + def cleanup(self, run_config: RunConfig) -> None: + _ = run_config diff --git a/benchmarks/src/basic_memory_benchmarks/reporting/__init__.py b/benchmarks/src/basic_memory_benchmarks/reporting/__init__.py new file mode 100644 index 000000000..c173af676 --- /dev/null +++ b/benchmarks/src/basic_memory_benchmarks/reporting/__init__.py @@ -0,0 +1 @@ +"""Reporting and artifact utilities.""" diff --git a/benchmarks/src/basic_memory_benchmarks/reporting/artifacts.py b/benchmarks/src/basic_memory_benchmarks/reporting/artifacts.py new file mode 100644 index 000000000..6ca49d22c --- /dev/null +++ b/benchmarks/src/basic_memory_benchmarks/reporting/artifacts.py @@ -0,0 +1,177 @@ +"""Artifact writing helpers.""" + +from __future__ import annotations + +import json +from pathlib import Path + +from basic_memory_benchmarks.models import ( + JudgeCaseResult, + JudgeSummary, + PerQueryRetrievalResult, + ProviderStatus, + RetrievalSummary, + RunManifest, +) + + +def _write_json(path: Path, payload: object) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + +def _write_jsonl(path: Path, rows: list[dict]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("w", encoding="utf-8") as file: + for row in rows: + file.write(json.dumps(row, sort_keys=True) + "\n") + + +def write_artifacts( + *, + run_dir: Path, + manifest: RunManifest, + provider_status: list[ProviderStatus], + retrieval_rows: list[PerQueryRetrievalResult], + retrieval_summaries: list[RetrievalSummary], + fairness_warnings: list[str], + judge_rows: list[JudgeCaseResult] | None = None, + judge_summaries: list[JudgeSummary] | None = None, +) -> None: + run_dir.mkdir(parents=True, exist_ok=True) + _write_json(run_dir / "manifest.json", manifest.model_dump(mode="json")) + _write_json( + run_dir / "provider-status.json", + [row.model_dump(mode="json") for row in provider_status], + ) + _write_jsonl( + run_dir / "per-query-retrieval.jsonl", + [row.model_dump(mode="json") for row in retrieval_rows], + ) + _write_json( + run_dir / "retrieval-summary.json", + { + "providers": [row.model_dump(mode="json") for row in retrieval_summaries], + "fairness_warnings": fairness_warnings, + }, + ) + + if judge_rows is not None: + _write_jsonl( + run_dir / "per-query-judge.jsonl", + [row.model_dump(mode="json") for row in judge_rows], + ) + if judge_summaries is not None: + _write_json( + run_dir / "judge-summary.json", + { + "providers": [row.model_dump(mode="json") for row in judge_summaries], + }, + ) + + summary_markdown = build_summary_markdown( + manifest=manifest, + provider_status=provider_status, + retrieval_summaries=retrieval_summaries, + fairness_warnings=fairness_warnings, + judge_summaries=judge_summaries or [], + ) + (run_dir / "summary.md").write_text(summary_markdown, encoding="utf-8") + + +def build_summary_markdown( + *, + manifest: RunManifest, + provider_status: list[ProviderStatus], + retrieval_summaries: list[RetrievalSummary], + fairness_warnings: list[str], + judge_summaries: list[JudgeSummary], +) -> str: + lines: list[str] = [] + lines.append(f"# Benchmark Run `{manifest.run_id}`") + lines.append("") + lines.append("## Provenance") + lines.append("") + lines.append(f"- Benchmark SHA: `{manifest.benchmark_git_sha}`") + lines.append(f"- BM source: `{manifest.bm_source}`") + lines.append(f"- BM resolved SHA: `{manifest.bm_resolved_sha or 'unknown'}`") + lines.append(f"- Dataset: `{manifest.dataset.dataset_id}`") + lines.append(f"- Dataset source: {manifest.dataset.source_url}") + lines.append(f"- Dataset checksum: `{manifest.dataset.checksum_sha256}`") + lines.append("") + + lines.append("## Provider Status") + lines.append("") + lines.append("| Provider | State | Reason |") + lines.append("| --- | --- | --- |") + for item in provider_status: + lines.append(f"| {item.provider} | {item.state} | {item.reason or ''} |") + lines.append("") + + lines.append("## Retrieval Summary") + lines.append("") + lines.append( + "| Provider | Recall@5 | Recall@10 | MRR | Precision@5 | Content Hit | Mean ms | P95 ms |" + ) + lines.append("| --- | --- | --- | --- | --- | --- | --- | --- |") + for summary in retrieval_summaries: + metric = summary.metrics + lines.append( + "| " + f"{summary.provider} | {metric.recall_at_5:.3f} | {metric.recall_at_10:.3f} | " + f"{metric.mrr:.3f} | {metric.precision_at_5:.3f} | {metric.content_hit_rate:.3f} | " + f"{metric.mean_latency_ms:.1f} | {metric.p95_latency_ms:.1f} |" + ) + lines.append("") + + lines.append("## Official Headline (LoCoMo Categories 1-4)") + lines.append("") + lines.append("| Provider | Recall@5 | Recall@10 | MRR |") + lines.append("| --- | --- | --- | --- |") + for summary in retrieval_summaries: + metric = summary.official_headline + lines.append( + f"| {summary.provider} | {metric.recall_at_5:.3f} | {metric.recall_at_10:.3f} | {metric.mrr:.3f} |" + ) + lines.append("") + + lines.append("## Adversarial (Category 5)") + lines.append("") + lines.append("| Provider | Recall@5 | Recall@10 | MRR |") + lines.append("| --- | --- | --- | --- |") + for summary in retrieval_summaries: + metric = summary.adversarial_breakout + lines.append( + f"| {summary.provider} | {metric.recall_at_5:.3f} | {metric.recall_at_10:.3f} | {metric.mrr:.3f} |" + ) + lines.append("") + + if judge_summaries: + lines.append("## Judge Summary") + lines.append("") + lines.append("| Provider | Evaluator | Model | Cases | Accuracy | Note |") + lines.append("| --- | --- | --- | --- | --- | --- |") + for summary in judge_summaries: + lines.append( + "| " + f"{summary.provider} | {summary.evaluator} | {summary.model} | " + f"{summary.total_cases} | {summary.accuracy:.3f} | {summary.skipped_reason or ''} |" + ) + lines.append("") + + if fairness_warnings: + lines.append("## Fairness Warnings") + lines.append("") + for warning in fairness_warnings: + lines.append(f"- {warning}") + lines.append("") + + lines.append("## Reproduce") + lines.append("") + lines.append("```bash") + lines.append( + f"uv run bm-bench run retrieval --run-id {manifest.run_id} --dataset-id {manifest.config.dataset_id}" + ) + lines.append("```") + + return "\n".join(lines).strip() + "\n" diff --git a/benchmarks/src/basic_memory_benchmarks/reporting/compare.py b/benchmarks/src/basic_memory_benchmarks/reporting/compare.py new file mode 100644 index 000000000..31797a594 --- /dev/null +++ b/benchmarks/src/basic_memory_benchmarks/reporting/compare.py @@ -0,0 +1,50 @@ +"""Comparison helpers for retrieval summary outputs.""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any, Mapping + + +def load_retrieval_summary(path: Path) -> dict[str, Any]: + payload = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(payload, dict): + raise ValueError(f"Expected JSON object in {path}") + return dict(payload) + + +def compare_provider_metric( + baseline: Mapping[str, Any], + candidate: Mapping[str, Any], + provider: str, + metric_name: str, +) -> tuple[float | None, float | None, float | None]: + def _value(blob: Mapping[str, Any]) -> float | None: + providers_raw = blob.get("providers") + if not isinstance(providers_raw, list): + return None + for item in providers_raw: + if not isinstance(item, Mapping): + continue + if item.get("provider") != provider: + continue + metrics_raw = item.get("metrics") + if not isinstance(metrics_raw, Mapping): + return None + raw = metrics_raw.get(metric_name) + if isinstance(raw, (int, float)): + return float(raw) + if isinstance(raw, str): + try: + return float(raw) + except ValueError: + return None + return None + return None + + b = _value(baseline) + c = _value(candidate) + if b is None or c is None: + return b, c, None + return b, c, c - b diff --git a/benchmarks/src/basic_memory_benchmarks/runner.py b/benchmarks/src/basic_memory_benchmarks/runner.py new file mode 100644 index 000000000..2a4f63622 --- /dev/null +++ b/benchmarks/src/basic_memory_benchmarks/runner.py @@ -0,0 +1,536 @@ +"""Benchmark runner orchestration.""" + +from __future__ import annotations + +import json +import time +from pathlib import Path +from typing import Callable + +from basic_memory_benchmarks.exceptions import ProviderSkippedError +from basic_memory_benchmarks.fairness import validate_fairness +from basic_memory_benchmarks.models import ( + DatasetProvenance, + PerQueryRetrievalResult, + ProviderStatus, + QueryCase, + RetrievalSummary, + RunConfig, + RunManifest, + RuntimeInfo, +) +from basic_memory_benchmarks.providers import create_provider +from basic_memory_benchmarks.providers.base import BenchmarkProvider +from basic_memory_benchmarks.reporting.artifacts import write_artifacts +from basic_memory_benchmarks.scoring.judge import run_optional_judge +from basic_memory_benchmarks.scoring.retrieval import evaluate_query, summarize_provider +from basic_memory_benchmarks.utils import ( + git_sha, + resolve_remote_main_sha, + runtime_info, + utc_now_iso, +) + + +def load_queries(path: Path) -> list[QueryCase]: + payload = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(payload, list): + raise ValueError(f"Query file must contain a list: {path}") + return [QueryCase.model_validate(item) for item in payload] + + +def _resolve_bm_sha(run_config: RunConfig) -> str | None: + if run_config.bm_local_path: + local_sha = git_sha(Path(run_config.bm_local_path)) + if local_sha: + return local_sha + return resolve_remote_main_sha("https://github.com/basicmachines-co/basic-memory") + + +def _execute_provider_flat( + *, + provider: BenchmarkProvider, + provider_name: str, + queries: list[QueryCase], + corpus_path: Path, + run_config: RunConfig, + cleanup_after: bool = True, +) -> list[PerQueryRetrievalResult]: + """Classic single-corpus execution: one ingest, then every query. + + ``cleanup_after=False`` is the group-reuse path: the grouped executor owns + the provider's lifecycle and cleans up once at end of run. + """ + provider_rows: list[PerQueryRetrievalResult] = [] + try: + provider.ingest(corpus_path, run_config) + for query in queries: + started = time.perf_counter() + hits = provider.search(query.query, run_config.top_k, run_config) + latency_ms = (time.perf_counter() - started) * 1000.0 + provider_rows.append( + evaluate_query( + provider=provider_name, + query=query, + hits=hits, + latency_ms=latency_ms, + ) + ) + finally: + if cleanup_after: + try: + provider.cleanup(run_config) + except Exception: + # Cleanup errors should not mask run state. + pass + return provider_rows + + +def _execute_provider_grouped( + *, + provider_factory: Callable[[str], BenchmarkProvider], + provider_name: str, + queries: list[QueryCase], + corpus_path: Path, + run_config: RunConfig, +) -> tuple[list[PerQueryRetrievalResult], BenchmarkProvider, dict[str, str]]: + """Grouped execution (LongMemEval): each group is its own isolated corpus. + + Per group, a fresh provider instance ingests ``//docs`` + under a group-suffixed run id, so provider-side namespaces (BM project + name, mem0 user id) never leak content across groups. A failing group is + recorded and skipped rather than aborting the run; ProviderSkippedError on + the first group means the provider is unavailable and propagates. + """ + groups: dict[str, list[QueryCase]] = {} + for query in queries: + if query.group is None: + raise ValueError( + f"Query {query.id} has no group but the query set is grouped; " + "mixed grouped/ungrouped query files are not supported" + ) + groups.setdefault(query.group, []).append(query) + + provider_rows: list[PerQueryRetrievalResult] = [] + failed_groups: list[str] = [] + failed_group_errors: dict[str, str] = {} + last_provider: BenchmarkProvider | None = None + shared_provider = provider_factory(provider_name) + reuse = shared_provider.supports_group_reuse + try: + for group_index, (group_id, group_queries) in enumerate(sorted(groups.items())): + group_corpus = corpus_path / group_id / "docs" + if not group_corpus.exists(): + raise FileNotFoundError(f"Missing group corpus: {group_corpus}") + group_config = run_config.model_copy( + update={"run_id": f"{run_config.run_id}-{group_id}"} + ) + # Non-reuse providers still use the shared instance for the first + # group so capability probing doesn't cost an extra instance. + if reuse or group_index == 0: + provider = shared_provider + else: + provider = provider_factory(provider_name) + try: + provider_rows.extend( + _execute_provider_flat( + provider=provider, + provider_name=provider_name, + queries=group_queries, + corpus_path=group_corpus, + run_config=group_config, + cleanup_after=not reuse, + ) + ) + last_provider = provider + except ProviderSkippedError: + # Trigger: provider signals it cannot run at all (missing creds). + # Why: the first group is representative; retrying hundreds of + # groups against an unavailable provider wastes hours. + # Outcome: the provider is recorded as skipped for the whole run. + if group_index == 0: + raise + failed_groups.append(group_id) + except Exception as exc: + failed_groups.append(group_id) + # Keep the first few error messages: a silent failed-group + # list is undiagnosable after a multi-hour run. + if len(failed_group_errors) < 3: + failed_group_errors[group_id] = f"{type(exc).__name__}: {exc}"[:300] + finally: + if reuse: + try: + shared_provider.cleanup(run_config) + except Exception: + # Cleanup errors should not mask run state. + pass + + if last_provider is None: + # Surface the first captured errors: an opaque "all failed" with no + # cause is undiagnosable after a multi-hour run. + detail = "; ".join(f"{gid}: {msg}" for gid, msg in sorted(failed_group_errors.items())) + raise RuntimeError( + f"All {len(failed_groups)} groups failed for provider {provider_name}" + + (f" — {detail}" if detail else "") + ) + + group_metadata: dict[str, str] = { + "grouped_mode": "true", + "group_count": str(len(groups)), + } + if failed_groups: + group_metadata["failed_group_count"] = str(len(failed_groups)) + group_metadata["failed_groups"] = ",".join(sorted(failed_groups)[:50]) + for index, (group_id, message) in enumerate(sorted(failed_group_errors.items())): + group_metadata[f"failed_group_error_{index}"] = f"{group_id}: {message}" + return provider_rows, last_provider, group_metadata + + +def run_retrieval( + *, + run_config: RunConfig, + dataset: DatasetProvenance, + provider_factory: Callable[[str], BenchmarkProvider] = create_provider, +) -> Path: + queries = load_queries(Path(run_config.queries_path)) + corpus_path = Path(run_config.corpus_dir) + output_root = Path(run_config.output_root) + run_dir = output_root / run_config.run_id + grouped = any(query.group is not None for query in queries) + + retrieval_rows: list[PerQueryRetrievalResult] = [] + provider_status: list[ProviderStatus] = [] + summaries: list[RetrievalSummary] = [] + + rows_by_provider: dict[str, list[PerQueryRetrievalResult]] = {} + + for provider_name in run_config.providers: + try: + group_metadata: dict[str, str] = {} + if grouped: + provider_rows, version_provider, group_metadata = _execute_provider_grouped( + provider_factory=provider_factory, + provider_name=provider_name, + queries=queries, + corpus_path=corpus_path, + run_config=run_config, + ) + else: + version_provider = provider_factory(provider_name) + provider_rows = _execute_provider_flat( + provider=version_provider, + provider_name=provider_name, + queries=queries, + corpus_path=corpus_path, + run_config=run_config, + ) + + summary = summarize_provider(provider_name, provider_rows) + summaries.append(summary) + retrieval_rows.extend(provider_rows) + rows_by_provider[provider_name] = provider_rows + provider_status.append( + ProviderStatus( + provider=provider_name, + state="ok", + metadata={**version_provider.version_info(), **group_metadata}, + ) + ) + except ProviderSkippedError as exc: + provider_status.append( + ProviderStatus(provider=provider_name, state="skipped", reason=str(exc)) + ) + if not run_config.allow_provider_skip: + raise + except Exception as exc: + provider_status.append( + ProviderStatus(provider=provider_name, state="error", reason=str(exc)) + ) + if not run_config.allow_provider_skip: + raise + + fairness_warnings = validate_fairness(rows_by_provider) + + os_name, py_version = runtime_info() + manifest = RunManifest( + run_id=run_config.run_id, + created_at_utc=utc_now_iso(), + benchmark_git_sha=git_sha(Path.cwd()) or "unknown", + bm_source=run_config.bm_source, + bm_resolved_sha=_resolve_bm_sha(run_config), + bm_local_path=run_config.bm_local_path, + mem0_version=next( + ( + status.metadata.get("mem0ai") + for status in provider_status + if status.provider == "mem0-local" and status.metadata + ), + None, + ), + provider_versions={ + status.provider: status.metadata for status in provider_status if status.metadata + }, + dataset=dataset, + runtime=RuntimeInfo(os=os_name, python_version=py_version, started_at_utc=utc_now_iso()), + config=run_config, + ) + + write_artifacts( + run_dir=run_dir, + manifest=manifest, + provider_status=provider_status, + retrieval_rows=retrieval_rows, + retrieval_summaries=summaries, + fairness_warnings=fairness_warnings, + ) + return run_dir + + +def _load_retrieval_rows(run_dir: Path) -> list[PerQueryRetrievalResult]: + retrieval_path = run_dir / "per-query-retrieval.jsonl" + if not retrieval_path.exists(): + raise FileNotFoundError(f"Missing retrieval artifact: {retrieval_path}") + rows: list[PerQueryRetrievalResult] = [] + with retrieval_path.open("r", encoding="utf-8") as file: + for line in file: + line = line.strip() + if line: + rows.append(PerQueryRetrievalResult.model_validate(json.loads(line))) + return rows + + +def run_qa_stage( + *, + run_dir: Path, + answerer_spec: str, + judge_spec: str, + max_workers: int = 4, + max_context_chars: int | None = None, +) -> Path: + """Generate answers from each provider's retrieved context and judge them. + + Reads per-query-retrieval.jsonl, writes per-query-qa.jsonl and + qa-summary.json into the same run directory. + """ + from basic_memory_benchmarks.llm.runners import create_runner + from basic_memory_benchmarks.scoring.qa import CONTEXT_MAX_CHARS, run_qa + + answerer = create_runner(answerer_spec) + judge = create_runner(judge_spec) + + grouped: dict[str, list[PerQueryRetrievalResult]] = {} + for row in _load_retrieval_rows(run_dir): + grouped.setdefault(row.provider, []).append(row) + + qa_rows = [] + qa_summaries = [] + for provider, provider_rows in grouped.items(): + provider_cases, provider_summary = run_qa( + provider_rows, + provider=provider, + answerer=answerer, + judge=judge, + max_workers=max_workers, + max_context_chars=max_context_chars or CONTEXT_MAX_CHARS, + ) + qa_rows.extend(provider_cases) + qa_summaries.append(provider_summary) + + qa_jsonl = run_dir / "per-query-qa.jsonl" + with qa_jsonl.open("w", encoding="utf-8") as file: + for row in qa_rows: + file.write(json.dumps(row.model_dump(mode="json"), sort_keys=True) + "\n") + + qa_summary_path = run_dir / "qa-summary.json" + qa_summary_path.write_text( + json.dumps( + {"providers": [item.model_dump(mode="json") for item in qa_summaries]}, + indent=2, + ), + encoding="utf-8", + ) + return run_dir + + +def run_review_stage( + *, + run_dir: Path, + source: str = "auto", +) -> Path: + """Render a self-contained judge-review HTML report for a run. + + ``source``: 'qa' uses per-query-qa.jsonl, 'rejudge' uses + per-query-qa-rejudge.jsonl, 'auto' prefers the re-judged file when present. + Writes review.html into the run dir and returns its path. + """ + from basic_memory_benchmarks.scoring.review import build_review_html + + cases, _chosen = _load_qa_cases(run_dir, source) + + review_path = run_dir / "review.html" + review_path.write_text(build_review_html(cases, run_id=run_dir.name), encoding="utf-8") + return review_path + + +def _load_qa_cases(run_dir: Path, source: str): + """Load QA cases, preferring the re-judged artifact under 'auto'.""" + from basic_memory_benchmarks.models import QACaseResult + + rejudge_path = run_dir / "per-query-qa-rejudge.jsonl" + qa_path = run_dir / "per-query-qa.jsonl" + if source == "rejudge" or (source == "auto" and rejudge_path.exists()): + chosen = rejudge_path + else: + chosen = qa_path + if not chosen.exists(): + raise FileNotFoundError(f"No QA artifact to load: {chosen}") + + cases = [] + with chosen.open("r", encoding="utf-8") as file: + for line in file: + line = line.strip() + if line: + cases.append(QACaseResult.model_validate(json.loads(line))) + return cases, chosen + + +def run_diagnose_stage( + *, + run_dir: Path, + source: str = "auto", + recall_field: str = "recall_at_10", +) -> Path: + """Attribute QA failures to retrieval vs the answerer for each provider. + + Joins QA verdicts (per-query-qa[-rejudge].jsonl) with retrieval rows + (per-query-retrieval.jsonl) and writes qa-diagnosis.json, separating + "retrieved but unused" (answerer) failures from "truly missed" (retrieval) + failures. ``source``: 'qa' | 'rejudge' | 'auto' (prefers re-judged). + Returns the path to qa-diagnosis.json. + """ + from basic_memory_benchmarks.scoring.diagnose import diagnose_run + + qa_cases, chosen = _load_qa_cases(run_dir, source) + retrieval_rows = _load_retrieval_rows(run_dir) + diagnoses = diagnose_run(qa_cases, retrieval_rows, recall_field=recall_field) + + out_path = run_dir / "qa-diagnosis.json" + out_path.write_text( + json.dumps( + { + "source": chosen.name, + "recall_field": recall_field, + "providers": [d.model_dump(mode="json") for d in diagnoses], + }, + indent=2, + ), + encoding="utf-8", + ) + return out_path + + +def run_rejudge_stage( + *, + run_dir: Path, + judge_spec: str, + max_workers: int = 4, +) -> Path: + """Re-judge stored QA answers with a (possibly different) judge. + + Reads per-query-qa.jsonl, re-runs only the judge on each stored generated + answer, and writes per-query-qa-rejudge.jsonl, qa-rejudge-summary.json (per + provider), and qa-rejudge-flips.json (cases whose verdict changed) for + judge-calibration review. The original QA artifacts are left untouched. + """ + from basic_memory_benchmarks.llm.runners import create_runner + from basic_memory_benchmarks.models import QACaseResult + from basic_memory_benchmarks.scoring.qa import rejudge_cases + + qa_path = run_dir / "per-query-qa.jsonl" + if not qa_path.exists(): + raise FileNotFoundError(f"Missing QA artifact: {qa_path}") + + cases_by_provider: dict[str, list[QACaseResult]] = {} + with qa_path.open("r", encoding="utf-8") as file: + for line in file: + line = line.strip() + if line: + case = QACaseResult.model_validate(json.loads(line)) + cases_by_provider.setdefault(case.provider, []).append(case) + + judge = create_runner(judge_spec) + all_rejudged = [] + summaries = [] + all_flips = [] + for provider, cases in cases_by_provider.items(): + rejudged, summary, flips = rejudge_cases(cases, judge=judge, max_workers=max_workers) + all_rejudged.extend(rejudged) + summaries.append(summary) + all_flips.extend(flips) + + rejudge_jsonl = run_dir / "per-query-qa-rejudge.jsonl" + with rejudge_jsonl.open("w", encoding="utf-8") as file: + for case in all_rejudged: + file.write(json.dumps(case.model_dump(mode="json"), sort_keys=True) + "\n") + + (run_dir / "qa-rejudge-summary.json").write_text( + json.dumps( + {"judge": judge_spec, "providers": [s.model_dump(mode="json") for s in summaries]}, + indent=2, + ), + encoding="utf-8", + ) + (run_dir / "qa-rejudge-flips.json").write_text( + json.dumps( + {"judge": judge_spec, "flip_count": len(all_flips), "flips": all_flips}, indent=2 + ), + encoding="utf-8", + ) + return run_dir + + +def run_judge( + *, + run_dir: Path, + model: str, +) -> Path: + retrieval_path = run_dir / "per-query-retrieval.jsonl" + if not retrieval_path.exists(): + raise FileNotFoundError(f"Missing retrieval artifact: {retrieval_path}") + + rows: list[PerQueryRetrievalResult] = [] + with retrieval_path.open("r", encoding="utf-8") as file: + for line in file: + line = line.strip() + if not line: + continue + rows.append(PerQueryRetrievalResult.model_validate(json.loads(line))) + + grouped: dict[str, list[PerQueryRetrievalResult]] = {} + for row in rows: + grouped.setdefault(row.provider, []).append(row) + + judge_rows = [] + judge_summaries = [] + for provider, provider_rows in grouped.items(): + provider_case_results, provider_summary = run_optional_judge( + provider_rows, + provider=provider, + model=model, + ) + judge_rows.extend(provider_case_results) + judge_summaries.append(provider_summary) + + judge_jsonl = run_dir / "per-query-judge.jsonl" + with judge_jsonl.open("w", encoding="utf-8") as file: + for row in judge_rows: + file.write(json.dumps(row.model_dump(mode="json"), sort_keys=True) + "\n") + + judge_summary_path = run_dir / "judge-summary.json" + judge_summary_path.write_text( + json.dumps( + {"providers": [item.model_dump(mode="json") for item in judge_summaries]}, indent=2 + ), + encoding="utf-8", + ) + return run_dir diff --git a/benchmarks/src/basic_memory_benchmarks/scoring/__init__.py b/benchmarks/src/basic_memory_benchmarks/scoring/__init__.py new file mode 100644 index 000000000..3b79b608a --- /dev/null +++ b/benchmarks/src/basic_memory_benchmarks/scoring/__init__.py @@ -0,0 +1 @@ +"""Scoring modules.""" diff --git a/benchmarks/src/basic_memory_benchmarks/scoring/diagnose.py b/benchmarks/src/basic_memory_benchmarks/scoring/diagnose.py new file mode 100644 index 000000000..52277cd18 --- /dev/null +++ b/benchmarks/src/basic_memory_benchmarks/scoring/diagnose.py @@ -0,0 +1,135 @@ +"""Answerer-vs-retrieval failure attribution. + +The QA pipeline is: provider retrieves → a FIXED answerer generates from the +retrieved context → a FIXED judge grades vs gold. Because the answerer is held +constant across providers, an absolute QA accuracy of, say, 0.75 conflates two +very different things: + + * retrieval found the gold evidence but the answerer didn't use it correctly + ("retrieved but unused" — an answerer failure, identical for every provider + given the same retrieved context), versus + * retrieval never surfaced the gold evidence ("truly missed" — a real + retrieval failure, the thing this project actually optimizes). + +This module joins the QA verdicts with the retrieval rows on +``(provider, query_id)`` and splits each answerable failure into those two +buckets. It turns "BM QA is 0.75" into "BM retrieval ceiling is 1.00; the 0.25 +gap is the answerer, not retrieval" — which is the honest, scrutiny-proof way to +report what retrieval improvements can and cannot move. + +Unanswerable items (empty ``ground_truth`` — the abstention set) have no +retrieval target, so they are counted separately and excluded from the +attribution. +""" + +from __future__ import annotations + +from basic_memory_benchmarks.models import ( + CategoryDiagnosis, + PerQueryRetrievalResult, + ProviderDiagnosis, + QACaseResult, +) + + +def _recall_value(row: PerQueryRetrievalResult, recall_field: str) -> float: + return float(getattr(row, recall_field)) + + +def diagnose_provider( + qa_cases: list[QACaseResult], + retrieval_rows: list[PerQueryRetrievalResult], + *, + provider: str, + recall_field: str = "recall_at_10", +) -> ProviderDiagnosis: + """Attribute one provider's QA outcomes to retrieval vs the answerer. + + ``qa_cases`` and ``retrieval_rows`` may contain rows for other providers; + only those matching ``provider`` are considered. A question counts as + "retrieved" when its joined retrieval row has ``recall_field`` > 0. + """ + recall_by_qid: dict[str, float] = { + row.query_id: _recall_value(row, recall_field) + for row in retrieval_rows + if row.provider == provider + } + + diag = ProviderDiagnosis(provider=provider, recall_field=recall_field) + by_cat: dict[str, CategoryDiagnosis] = {} + + for case in qa_cases: + if case.provider != provider: + continue + diag.total_cases += 1 + + if case.error: + diag.errored += 1 + continue + + # Unanswerable / abstention items carry no retrieval ground truth. + retrieval_row = next( + (r for r in retrieval_rows if r.provider == provider and r.query_id == case.query_id), + None, + ) + if retrieval_row is not None and not retrieval_row.ground_truth: + diag.unanswerable += 1 + continue + if retrieval_row is None and case.query_id not in recall_by_qid: + diag.unmatched += 1 + continue + + diag.answerable += 1 + cat = by_cat.setdefault(case.category, CategoryDiagnosis()) + cat.answerable += 1 + + if case.correct: + diag.correct += 1 + cat.correct += 1 + continue + + # Wrong answer: attribute to retrieval (gold not found) or answerer. + retrieved = recall_by_qid.get(case.query_id, 0.0) > 0 + if retrieved: + diag.retrieved_but_unused += 1 + cat.retrieved_but_unused += 1 + else: + diag.truly_missed += 1 + cat.truly_missed += 1 + + _finalize(diag) + diag.by_category = dict(sorted(by_cat.items())) + return diag + + +def _finalize(diag: ProviderDiagnosis) -> None: + answerable = diag.answerable + if answerable: + diag.qa_accuracy = diag.correct / answerable + diag.retrieval_ceiling = (diag.correct + diag.retrieved_but_unused) / answerable + diag.answerer_gap = diag.retrieved_but_unused / answerable + diag.retrieval_gap = diag.truly_missed / answerable + failures = diag.retrieved_but_unused + diag.truly_missed + if failures: + diag.answerer_failure_share = diag.retrieved_but_unused / failures + + +def diagnose_run( + qa_cases: list[QACaseResult], + retrieval_rows: list[PerQueryRetrievalResult], + *, + recall_field: str = "recall_at_10", +) -> list[ProviderDiagnosis]: + """Diagnose every provider present in ``qa_cases`` (stable provider order).""" + providers: list[str] = [] + seen: set[str] = set() + for case in qa_cases: + if case.provider not in seen: + seen.add(case.provider) + providers.append(case.provider) + return [ + diagnose_provider( + qa_cases, retrieval_rows, provider=provider, recall_field=recall_field + ) + for provider in providers + ] diff --git a/benchmarks/src/basic_memory_benchmarks/scoring/judge.py b/benchmarks/src/basic_memory_benchmarks/scoring/judge.py new file mode 100644 index 000000000..755205559 --- /dev/null +++ b/benchmarks/src/basic_memory_benchmarks/scoring/judge.py @@ -0,0 +1,140 @@ +"""Optional judge scoring. + +Primary mode tries Pydantic Evals. If unavailable or misconfigured, it falls back to +an explicit deterministic checker and records the reason. +""" + +from __future__ import annotations + +import os +from typing import Any + +from basic_memory_benchmarks.models import JudgeCaseResult, JudgeSummary, PerQueryRetrievalResult + + +def _deterministic_contains_eval( + rows: list[PerQueryRetrievalResult], provider: str +) -> tuple[list[JudgeCaseResult], JudgeSummary]: + case_results: list[JudgeCaseResult] = [] + for row in rows: + expected = (row.expected_answer or "").strip().lower() + context = row.retrieved_context.strip().lower() + passed = bool(expected and expected in context) + case_results.append( + JudgeCaseResult( + provider=provider, + query_id=row.query_id, + category=row.category, + passed=passed, + score=1.0 if passed else 0.0, + reason="Deterministic contains check", + evaluator="deterministic-fallback", + ) + ) + + pass_count = sum(1 for item in case_results if item.passed) + total = len(case_results) + summary = JudgeSummary( + provider=provider, + evaluator="deterministic-fallback", + model="none", + total_cases=total, + pass_count=pass_count, + accuracy=(pass_count / total) if total else 0.0, + ) + return case_results, summary + + +def run_optional_judge( + rows: list[PerQueryRetrievalResult], + provider: str, + model: str, +) -> tuple[list[JudgeCaseResult], JudgeSummary]: + """Run optional judge scoring. + + If pydantic-evals + OpenAI credentials are available, we attempt to use it. + Otherwise we return deterministic fallback scores. + """ + relevant_rows = [row for row in rows if row.expected_answer] + if not relevant_rows: + return [], JudgeSummary( + provider=provider, + evaluator="none", + model=model, + total_cases=0, + pass_count=0, + accuracy=0.0, + skipped_reason="No expected answers available for judge scoring", + ) + + if not os.getenv("OPENAI_API_KEY"): + case_results, summary = _deterministic_contains_eval(relevant_rows, provider) + summary.skipped_reason = "OPENAI_API_KEY missing; used deterministic fallback" + return case_results, summary + + try: + from pydantic_evals import Case, Dataset # type: ignore + from pydantic_evals.evaluators import LLMJudge # type: ignore + except Exception: + case_results, summary = _deterministic_contains_eval(relevant_rows, provider) + summary.skipped_reason = "pydantic-evals not installed; used deterministic fallback" + return case_results, summary + + # Best-effort pydantic-evals integration. + # Trigger: runtime has judge deps + API key + # Why: align with competitor methodology + # Outcome: if API shape changes, fallback remains deterministic and explicit + try: + cases: list[Any] = [] + for row in relevant_rows: + rubric = ( + "PASS if the candidate response contains the same core factual answer as expected. " + "FAIL if key facts are missing or contradictory." + ) + candidate = row.retrieved_context + expected = row.expected_answer or "" + cases.append( + Case( + name=row.query_id, + inputs=f"question: {row.query_text}\nexpected: {expected}\ncandidate: {candidate}", + evaluators=[LLMJudge(rubric=rubric, include_input=True, model=model)], + metadata={"provider": provider, "category": row.category}, + ) + ) + + dataset = Dataset(cases=cases) + report = dataset.evaluate_sync(lambda value: value) + + case_results: list[JudgeCaseResult] = [] + pass_count = 0 + for idx, row in enumerate(relevant_rows): + case_report = report.cases[idx] + passed = bool(getattr(case_report, "passed", False)) + pass_count += 1 if passed else 0 + reason = str(getattr(case_report, "reason", "")) or "LLM judge result" + case_results.append( + JudgeCaseResult( + provider=provider, + query_id=row.query_id, + category=row.category, + passed=passed, + score=1.0 if passed else 0.0, + reason=reason, + evaluator="pydantic-evals-llmjudge", + ) + ) + + total = len(case_results) + summary = JudgeSummary( + provider=provider, + evaluator="pydantic-evals-llmjudge", + model=model, + total_cases=total, + pass_count=pass_count, + accuracy=(pass_count / total) if total else 0.0, + ) + return case_results, summary + except Exception: + case_results, summary = _deterministic_contains_eval(relevant_rows, provider) + summary.skipped_reason = "pydantic-evals execution failed; used deterministic fallback" + return case_results, summary diff --git a/benchmarks/src/basic_memory_benchmarks/scoring/qa.py b/benchmarks/src/basic_memory_benchmarks/scoring/qa.py new file mode 100644 index 000000000..0f8ed430a --- /dev/null +++ b/benchmarks/src/basic_memory_benchmarks/scoring/qa.py @@ -0,0 +1,403 @@ +"""End-to-end QA scoring: answer generation over retrieved context, then judging. + +This is the stage that produces comparable "memory benchmark accuracy" numbers. +Retrieval metrics (recall/MRR) measure the search layer; QA accuracy measures +whether an LLM holding only the retrieved memories can actually answer the +question. Both prompts below are fixed and ship with the repo so any published +number can be audited. +""" + +from __future__ import annotations + +import json +import re +from concurrent.futures import ThreadPoolExecutor + +from basic_memory_benchmarks.llm.runners import LLMRunner, LLMRunnerError +from basic_memory_benchmarks.models import ( + PerQueryRetrievalResult, + QACaseResult, + QACategoryMetrics, + QASummary, + SearchHit, +) + +# Context assembly budget. Multi-fact answers (LoCoMo multi_hop, LongMemEval +# multi-session) need material from several hits; the previous top-5 +# matched-chunk join (~1K chars) capped every provider near zero on those +# categories despite ~0.8 retrieval recall. +CONTEXT_MAX_HITS = 10 +CONTEXT_MAX_CHARS = 12_000 +CONTEXT_CHARS_PER_HIT = 2_500 + +# The exact abstention sentinel the answer prompt requests. Judged correct only +# when the gold answer itself indicates the question is unanswerable (e.g. +# LoCoMo adversarial cases). +ABSTAIN_SENTINEL = "I don't know" + +ANSWER_PROMPT_TEMPLATE = """\ +You are answering a question using only the retrieved memories below. The memories +come from past conversations and notes; they may be incomplete. + +Question: {question} + +Retrieved memories: +{context} + +Instructions: +- Answer concisely using only facts found in the retrieved memories. +- If the memories do not contain the information needed to answer, reply with + exactly: {abstain} +- Do not use outside knowledge. Do not explain your reasoning. + +Answer:""" + +JUDGE_PROMPT_TEMPLATE = """\ +You are grading a question-answering system against a reference (gold) answer. + +Question: {question} +Gold answer: {gold} +Candidate answer: {candidate} + +FIRST, handle the unanswerable case. If the gold answer indicates the +information is NOT available (e.g. "no information", "not mentioned", "cannot +be determined"), then judge ONLY on whether the candidate declines: +- correct = true if the candidate also declines / abstains (e.g. "{abstain}"). +- correct = false if the candidate asserts a specific factual answer. +Do not apply the fact-matching rules below to this case. + +OTHERWISE (the gold answer contains real facts), the gold answer may be +INCOMPLETE: it lists the key fact(s) the candidate must cover, but is not +necessarily an exhaustive list. + +Mark correct = true when BOTH hold: +- The candidate states every key fact in the gold answer (paraphrase and + formatting differences are fine). +- The candidate does not contradict the gold answer. +Additional specific facts in the candidate beyond the gold answer are NOT errors; +do not treat them as hallucination unless they directly contradict the gold. + +Mark correct = false when ANY holds: +- A key fact from the gold answer is missing from the candidate. +- The candidate contradicts the gold answer. +- The candidate declines to answer (e.g. "{abstain}") even though the gold + answer contains a real fact. + +Reply with only a JSON object: {{"correct": true or false, "reason": ""}}""" + +_JSON_OBJECT_PATTERN = re.compile(r"\{.*\}", re.DOTALL) + + +def build_answer_prompt(question: str, context: str) -> str: + return ANSWER_PROMPT_TEMPLATE.format( + question=question, + context=context if context.strip() else "(no memories were retrieved)", + abstain=ABSTAIN_SENTINEL, + ) + + +def build_judge_prompt(question: str, gold: str, candidate: str) -> str: + return JUDGE_PROMPT_TEMPLATE.format( + question=question, + gold=gold, + candidate=candidate, + abstain=ABSTAIN_SENTINEL, + ) + + +def parse_judge_verdict(raw: str) -> tuple[bool, str]: + """Extract a (correct, reason) verdict from judge output. + + Judges occasionally wrap JSON in prose or code fences; take the first JSON + object found. A malformed verdict raises so the case is recorded as an + explicit error rather than silently scored. + """ + match = _JSON_OBJECT_PATTERN.search(raw) + if not match: + raise ValueError(f"Judge returned no JSON object: {raw[:200]}") + payload = json.loads(match.group(0)) + if not isinstance(payload.get("correct"), bool): + raise ValueError(f"Judge JSON missing boolean 'correct': {raw[:200]}") + return payload["correct"], str(payload.get("reason") or "") + + +def _is_abstention(answer: str) -> bool: + normalized = answer.strip().strip(".").lower() + return normalized == ABSTAIN_SENTINEL.strip(".").lower() + + +def assemble_context(hits: list[SearchHit], max_chars: int = CONTEXT_MAX_CHARS) -> str: + """Build answering context from ranked hits under a character budget. + + Hits are taken in rank order under a total budget of ``max_chars``. The + per-hit cap is the larger of CONTEXT_CHARS_PER_HIT and an even split of + the budget across available hits, so a single-hit provider (the + full-context baseline) can use the whole budget rather than being + truncated to one hit-slice. Sections are numbered with their source doc + so the answerer can ground multi-fact answers across memories. Identical + assembly for every provider in a run. + """ + take = hits[:CONTEXT_MAX_HITS] + if not take: + return "" + per_hit_cap = max(CONTEXT_CHARS_PER_HIT, max_chars // len(take)) + sections: list[str] = [] + used = 0 + for rank, hit in enumerate(take, start=1): + text = (hit.text or "").strip() + if not text: + continue + snippet = text[:per_hit_cap] + if used + len(snippet) > max_chars: + snippet = snippet[: max_chars - used] + if not snippet: + break + source = hit.source_doc_id or hit.source_path or "unknown" + title = (hit.metadata or {}).get("title") + header = f"[Memory {rank} | source: {source}]" + if title and str(title) not in snippet: + header = f"[Memory {rank} | source: {source} | {title}]" + sections.append(f"{header}\n{snippet}") + used += len(snippet) + if used >= max_chars: + break + return "\n\n".join(sections) + + +def _row_context(row: PerQueryRetrievalResult, max_context_chars: int) -> str: + # Prefer assembling from stored hits (richer, budget-controlled); fall + # back to the legacy pre-joined context for old artifacts without hits. + if row.hits: + assembled = assemble_context(row.hits, max_chars=max_context_chars) + if assembled: + return assembled + return row.retrieved_context + + +def _question_display(row: PerQueryRetrievalResult) -> str: + """Render the question with its ask-date when the dataset provides one. + + Temporal-reasoning questions ("how many weeks ago...") are unanswerable + without the reference date, and both the answerer and the judge need the + same framing. + """ + question_date = row.metadata.get("question_date") + if question_date: + return f"{row.query_text} (question asked on {question_date})" + return row.query_text + + +def _score_case( + row: PerQueryRetrievalResult, + provider: str, + answerer: LLMRunner, + judge: LLMRunner, + max_context_chars: int = CONTEXT_MAX_CHARS, +) -> QACaseResult: + question = _question_display(row) + answer_prompt = build_answer_prompt(question, _row_context(row, max_context_chars)) + try: + answer_result = answerer.complete(answer_prompt) + judge_result = judge.complete( + build_judge_prompt(question, row.expected_answer or "", answer_result.text) + ) + correct, reason = parse_judge_verdict(judge_result.text) + return QACaseResult( + provider=provider, + query_id=row.query_id, + category=row.category, + question=row.query_text, + expected_answer=row.expected_answer or "", + generated_answer=answer_result.text, + abstained=_is_abstention(answer_result.text), + correct=correct, + judge_reason=reason, + answer_model=answerer.spec, + judge_model=judge.spec, + answer_latency_ms=answer_result.latency_ms, + answer_input_tokens=answer_result.input_tokens, + answer_output_tokens=answer_result.output_tokens, + answer_prompt_chars=len(answer_prompt), + ) + except (LLMRunnerError, ValueError, json.JSONDecodeError) as exc: + return QACaseResult( + provider=provider, + query_id=row.query_id, + category=row.category, + question=row.query_text, + expected_answer=row.expected_answer or "", + generated_answer="", + abstained=False, + correct=False, + judge_reason="", + answer_model=answerer.spec, + judge_model=judge.spec, + answer_latency_ms=0.0, + answer_input_tokens=0, + answer_output_tokens=0, + answer_prompt_chars=len(answer_prompt), + error=str(exc), + ) + + +def _rejudge_case(case: QACaseResult, judge: LLMRunner) -> QACaseResult: + """Re-judge one stored case against its generated answer (no regeneration). + + Errored cases (no generated answer) are returned unchanged. The answerer + fields are preserved; only the verdict and judge_model are updated. + """ + if case.error: + return case + try: + verdict = judge.complete( + build_judge_prompt(case.question, case.expected_answer, case.generated_answer) + ) + correct, reason = parse_judge_verdict(verdict.text) + except (LLMRunnerError, ValueError, json.JSONDecodeError) as exc: + return case.model_copy(update={"error": f"rejudge failed: {exc}"}) + return case.model_copy( + update={"correct": correct, "judge_reason": reason, "judge_model": judge.spec} + ) + + +def rejudge_cases( + cases: list[QACaseResult], + *, + judge: LLMRunner, + max_workers: int = 4, +) -> tuple[list[QACaseResult], QASummary, list[dict]]: + """Re-judge stored QA cases with a (possibly different) judge. + + Returns the re-judged cases, a summary, and the list of verdict flips + (cases whose correctness changed) for calibration review. + """ + if not cases: + return ( + [], + QASummary( + provider="rejudge", + answer_model="stored", + judge_model=judge.spec, + total_cases=0, + correct_count=0, + error_count=0, + abstain_count=0, + accuracy=0.0, + skipped_reason="No stored cases to re-judge", + ), + [], + ) + + provider = cases[0].provider + with ThreadPoolExecutor(max_workers=max_workers) as pool: + rejudged = list(pool.map(lambda c: _rejudge_case(c, judge), cases)) + + flips = [ + { + "query_id": new.query_id, + "category": new.category, + "question": new.question, + "expected_answer": new.expected_answer, + "generated_answer": new.generated_answer, + "was_correct": old.correct, + "now_correct": new.correct, + "old_reason": old.judge_reason, + "new_reason": new.judge_reason, + } + for old, new in zip(cases, rejudged) + if old.correct != new.correct + ] + summary = _summarize_cases(rejudged, provider=provider, answer_model="stored", judge=judge) + return rejudged, summary, flips + + +def _summarize_cases( + case_results: list[QACaseResult], + *, + provider: str, + answer_model: str, + judge: LLMRunner, +) -> QASummary: + by_category: dict[str, QACategoryMetrics] = {} + for case in case_results: + bucket = by_category.setdefault(case.category, QACategoryMetrics()) + bucket.total += 1 + bucket.correct += 1 if case.correct else 0 + for bucket in by_category.values(): + bucket.accuracy = bucket.correct / bucket.total if bucket.total else 0.0 + correct_count = sum(1 for case in case_results if case.correct) + return QASummary( + provider=provider, + answer_model=answer_model, + judge_model=judge.spec, + total_cases=len(case_results), + correct_count=correct_count, + error_count=sum(1 for case in case_results if case.error), + abstain_count=sum(1 for case in case_results if case.abstained), + accuracy=correct_count / len(case_results) if case_results else 0.0, + by_category=by_category, + ) + + +def run_qa( + rows: list[PerQueryRetrievalResult], + *, + provider: str, + answerer: LLMRunner, + judge: LLMRunner, + max_workers: int = 4, + max_context_chars: int = CONTEXT_MAX_CHARS, +) -> tuple[list[QACaseResult], QASummary]: + """Answer and judge every row that carries an expected answer.""" + scorable = [row for row in rows if row.expected_answer] + if not scorable: + return [], QASummary( + provider=provider, + answer_model=answerer.spec, + judge_model=judge.spec, + total_cases=0, + correct_count=0, + error_count=0, + abstain_count=0, + accuracy=0.0, + skipped_reason="No expected answers available for QA scoring", + ) + + with ThreadPoolExecutor(max_workers=max_workers) as pool: + case_results = list( + pool.map( + lambda row: _score_case(row, provider, answerer, judge, max_context_chars), + scorable, + ) + ) + + by_category: dict[str, QACategoryMetrics] = {} + for case in case_results: + bucket = by_category.setdefault(case.category, QACategoryMetrics()) + bucket.total += 1 + bucket.correct += 1 if case.correct else 0 + for bucket in by_category.values(): + bucket.accuracy = bucket.correct / bucket.total if bucket.total else 0.0 + + correct_count = sum(1 for case in case_results if case.correct) + error_count = sum(1 for case in case_results if case.error) + summary = QASummary( + provider=provider, + answer_model=answerer.spec, + judge_model=judge.spec, + total_cases=len(case_results), + correct_count=correct_count, + error_count=error_count, + abstain_count=sum(1 for case in case_results if case.abstained), + accuracy=correct_count / len(case_results), + by_category=by_category, + mean_answer_latency_ms=( + sum(case.answer_latency_ms for case in case_results) / len(case_results) + ), + total_answer_input_tokens=sum(case.answer_input_tokens for case in case_results), + total_answer_output_tokens=sum(case.answer_output_tokens for case in case_results), + mean_answer_prompt_chars=( + sum(case.answer_prompt_chars for case in case_results) / len(case_results) + ), + ) + return case_results, summary diff --git a/benchmarks/src/basic_memory_benchmarks/scoring/retrieval.py b/benchmarks/src/basic_memory_benchmarks/scoring/retrieval.py new file mode 100644 index 000000000..d3d97c5b9 --- /dev/null +++ b/benchmarks/src/basic_memory_benchmarks/scoring/retrieval.py @@ -0,0 +1,159 @@ +"""Deterministic retrieval metric calculations.""" + +from __future__ import annotations + +import math +from pathlib import Path +from statistics import mean + +from basic_memory_benchmarks.models import ( + PerQueryRetrievalResult, + QueryCase, + RetrievalMetrics, + RetrievalSummary, + SearchHit, +) + + +OFFICIAL_HEADLINE_CATEGORIES = {"single_hop", "multi_hop", "temporal", "open_domain"} + + +def _basename(identifier: str) -> str: + return Path(identifier).name.rsplit(".", 1)[0] + + +def is_match(hit: SearchHit, ground_truth: set[str]) -> bool: + """Return whether a search hit matches any ground-truth doc id.""" + candidates = set(ground_truth) + if hit.source_doc_id: + if hit.source_doc_id in candidates: + return True + if _basename(hit.source_doc_id) in {_basename(item) for item in candidates}: + return True + + if hit.source_path: + hit_name = _basename(hit.source_path) + for item in candidates: + if hit_name == _basename(item): + return True + + return False + + +def recall_at_k(hits: list[SearchHit], ground_truth: set[str], k: int) -> float: + if not ground_truth: + return 0.0 + top_hits = hits[:k] + matched = {truth for truth in ground_truth if any(is_match(hit, {truth}) for hit in top_hits)} + return len(matched) / len(ground_truth) + + +def precision_at_k(hits: list[SearchHit], ground_truth: set[str], k: int) -> float: + if k <= 0: + return 0.0 + top_hits = hits[:k] + if not top_hits: + return 0.0 + relevant = sum(1 for hit in top_hits if is_match(hit, ground_truth)) + return relevant / len(top_hits) + + +def reciprocal_rank(hits: list[SearchHit], ground_truth: set[str]) -> float: + for index, hit in enumerate(hits, start=1): + if is_match(hit, ground_truth): + return 1.0 / index + return 0.0 + + +def content_hit(expected_answer: str | None, hits: list[SearchHit]) -> bool: + if not expected_answer: + return False + needle = expected_answer.strip().lower() + if not needle: + return False + haystack = "\n".join((hit.text or "") for hit in hits).lower() + return needle in haystack + + +def evaluate_query( + *, + provider: str, + query: QueryCase, + hits: list[SearchHit], + latency_ms: float, +) -> PerQueryRetrievalResult: + ground_truth = set(query.ground_truth) + rec5 = recall_at_k(hits, ground_truth, 5) + rec10 = recall_at_k(hits, ground_truth, 10) + prec5 = precision_at_k(hits, ground_truth, 5) + mrr = reciprocal_rank(hits, ground_truth) + ch = content_hit(query.expected_answer, hits) + top_hit_doc_id = hits[0].source_doc_id if hits else None + context = "\n".join((hit.text or "") for hit in hits[:5]).strip() + + return PerQueryRetrievalResult( + provider=provider, + query_id=query.id, + query_text=query.query, + category=query.category, + category_id=query.category_id, + ground_truth=query.ground_truth, + expected_answer=query.expected_answer, + hits=hits, + recall_at_5=rec5, + recall_at_10=rec10, + precision_at_5=prec5, + mrr=mrr, + content_hit=ch, + latency_ms=latency_ms, + top_hit_doc_id=top_hit_doc_id, + retrieved_context=context, + metadata=query.metadata, + ) + + +def _aggregate_metrics(rows: list[PerQueryRetrievalResult]) -> RetrievalMetrics: + if not rows: + return RetrievalMetrics() + + latencies = [row.latency_ms for row in rows] + latencies_sorted = sorted(latencies) + p95_index = max(0, min(len(latencies_sorted) - 1, math.ceil(len(latencies_sorted) * 0.95) - 1)) + + return RetrievalMetrics( + recall_at_5=mean(row.recall_at_5 for row in rows), + recall_at_10=mean(row.recall_at_10 for row in rows), + precision_at_5=mean(row.precision_at_5 for row in rows), + mrr=mean(row.mrr for row in rows), + content_hit_rate=mean(1.0 if row.content_hit else 0.0 for row in rows), + mean_latency_ms=mean(latencies), + p95_latency_ms=latencies_sorted[p95_index], + query_count=len(rows), + ) + + +def summarize_provider(provider: str, rows: list[PerQueryRetrievalResult]) -> RetrievalSummary: + by_category: dict[str, list[PerQueryRetrievalResult]] = {} + for row in rows: + by_category.setdefault(row.category, []).append(row) + + category_metrics = { + category: _aggregate_metrics(group) for category, group in by_category.items() + } + + official_rows = [ + row + for row in rows + if row.category in OFFICIAL_HEADLINE_CATEGORIES or row.category_id in {1, 2, 3, 4} + ] + adversarial_rows = [ + row for row in rows if row.category == "adversarial" or row.category_id == 5 + ] + + return RetrievalSummary( + provider=provider, + metrics=_aggregate_metrics(rows), + by_category=category_metrics, + official_headline=_aggregate_metrics(official_rows), + adversarial_breakout=_aggregate_metrics(adversarial_rows), + ) diff --git a/benchmarks/src/basic_memory_benchmarks/scoring/review.py b/benchmarks/src/basic_memory_benchmarks/scoring/review.py new file mode 100644 index 000000000..f0a3f2971 --- /dev/null +++ b/benchmarks/src/basic_memory_benchmarks/scoring/review.py @@ -0,0 +1,208 @@ +"""Human-readable judge-review report. + +Renders QA judge decisions as a single self-contained HTML file (no external +deps) so a human can skim, sanity-check, and — for judge calibration — label +each verdict agree/disagree/unsure. Labels persist in the browser's +localStorage and export to JSON, feeding the judge-vs-human agreement step. + +The point is transparency: every published number rests on the LLM judge, so +the judge's decisions must be auditable at a glance, not buried in JSONL. +""" + +from __future__ import annotations + +import html +import json + +from basic_memory_benchmarks.models import QACaseResult + +# --- Page template --------------------------------------------------------- +# Data is injected as a JSON blob; the table is built and filtered client-side. + +_HTML_TEMPLATE = """ + + + +Judge review — {run_id} + + + +

Judge review — {run_id}

+

{n} decisions · judge {judge_model} · labels save in this browser; use Export when done.

+
+ + + + + + + +
+ + + + + + +
#categoryproviderquestiongoldcandidateverdictjudge reasonyour label
+ + + + +""" + + +def _case_to_view(case: QACaseResult) -> dict: + """HTML-escape user-facing text so answers with markup render safely.""" + return { + "query_id": case.query_id, + "provider": case.provider, + "category": case.category, + "question": html.escape(case.question), + "expected_answer": html.escape(case.expected_answer), + "generated_answer": html.escape(case.generated_answer), + "judge_reason": html.escape(case.judge_reason), + "correct": case.correct, + "abstained": case.abstained, + "error": html.escape(case.error) if case.error else None, + } + + +def build_review_html(cases: list[QACaseResult], *, run_id: str) -> str: + """Render judge decisions as a self-contained review/labeling page.""" + views = [_case_to_view(case) for case in cases] + judge_model = cases[0].judge_model if cases else "n/a" + return _HTML_TEMPLATE.format( + run_id=html.escape(run_id), + run_id_json=json.dumps(run_id), + judge_model=html.escape(judge_model), + n=len(cases), + data_json=json.dumps(views), + ) diff --git a/benchmarks/src/basic_memory_benchmarks/utils.py b/benchmarks/src/basic_memory_benchmarks/utils.py new file mode 100644 index 000000000..6899b7081 --- /dev/null +++ b/benchmarks/src/basic_memory_benchmarks/utils.py @@ -0,0 +1,65 @@ +"""Shared utility functions.""" + +from __future__ import annotations + +import hashlib +import platform +import subprocess +import sys +from datetime import datetime, timezone +from pathlib import Path + + +def utc_now_iso() -> str: + return datetime.now(timezone.utc).isoformat() + + +def runtime_info() -> tuple[str, str]: + return platform.platform(), sys.version.split()[0] + + +def sha256_file(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as file: + for chunk in iter(lambda: file.read(65536), b""): + digest.update(chunk) + return digest.hexdigest() + + +def run_command( + args: list[str], + cwd: Path | None = None, + check: bool = True, + env: dict[str, str] | None = None, +) -> subprocess.CompletedProcess: + return subprocess.run(args, cwd=cwd, text=True, capture_output=True, check=check, env=env) + + +def git_sha(path: Path) -> str | None: + try: + result = run_command(["git", "-C", str(path), "rev-parse", "HEAD"]) + except subprocess.CalledProcessError: + return None + return result.stdout.strip() or None + + +def resolve_remote_main_sha(repo_url: str) -> str | None: + try: + result = run_command(["git", "ls-remote", repo_url, "refs/heads/main"]) + except subprocess.CalledProcessError: + return None + raw = result.stdout.strip() + if not raw: + return None + return raw.split()[0] + + +def safe_float(value: object, default: float = 0.0) -> float: + if isinstance(value, (int, float)): + return float(value) + if isinstance(value, str): + try: + return float(value) + except ValueError: + return default + return default diff --git a/benchmarks/test-int/test_compare_regression.py b/benchmarks/test-int/test_compare_regression.py new file mode 100644 index 000000000..c2196e65d --- /dev/null +++ b/benchmarks/test-int/test_compare_regression.py @@ -0,0 +1,49 @@ +import json +from pathlib import Path + +import pytest + +from basic_memory_benchmarks.reporting.compare import compare_provider_metric, load_retrieval_summary + + +def test_compare_provider_metric(tmp_path: Path) -> None: + baseline = tmp_path / "baseline.json" + candidate = tmp_path / "candidate.json" + + baseline.write_text( + json.dumps( + { + "providers": [ + { + "provider": "bm-local", + "metrics": { + "recall_at_5": 0.4, + }, + } + ] + } + ), + encoding="utf-8", + ) + candidate.write_text( + json.dumps( + { + "providers": [ + { + "provider": "bm-local", + "metrics": { + "recall_at_5": 0.5, + }, + } + ] + } + ), + encoding="utf-8", + ) + + b = load_retrieval_summary(baseline) + c = load_retrieval_summary(candidate) + base_value, candidate_value, delta = compare_provider_metric(b, c, "bm-local", "recall_at_5") + assert base_value == 0.4 + assert candidate_value == 0.5 + assert delta == pytest.approx(0.1) diff --git a/benchmarks/test-int/test_runner_orchestration.py b/benchmarks/test-int/test_runner_orchestration.py new file mode 100644 index 000000000..6227c9427 --- /dev/null +++ b/benchmarks/test-int/test_runner_orchestration.py @@ -0,0 +1,81 @@ +import json +from pathlib import Path + +from basic_memory_benchmarks.models import DatasetProvenance, RunConfig, SearchHit +from basic_memory_benchmarks.providers.base import BenchmarkProvider +from basic_memory_benchmarks.runner import run_retrieval + + +class FakeProvider(BenchmarkProvider): + def __init__(self, name: str, mapping: dict[str, str]): + self.name = name + self.mapping = mapping + + def ingest(self, corpus_path: Path, run_config: RunConfig) -> None: + _ = corpus_path + _ = run_config + + def search(self, query: str, limit: int, run_config: RunConfig) -> list[SearchHit]: + _ = limit + _ = run_config + doc_id = self.mapping.get(query) + if not doc_id: + return [] + return [SearchHit(source_doc_id=doc_id, source_path=f"docs/{doc_id}.md", text="match", score=1.0)] + + def cleanup(self, run_config: RunConfig) -> None: + _ = run_config + + +def test_runner_writes_artifacts(tmp_path: Path) -> None: + corpus_dir = tmp_path / "docs" + corpus_dir.mkdir(parents=True) + (corpus_dir / "doc-a.md").write_text("# doc-a\n", encoding="utf-8") + + queries = [ + { + "id": "q1", + "query": "alpha", + "category": "single_hop", + "category_id": 1, + "ground_truth": ["doc-a"], + "expected_answer": "match", + "metadata": {}, + } + ] + queries_path = tmp_path / "queries.json" + queries_path.write_text(json.dumps(queries), encoding="utf-8") + + config = RunConfig( + run_id="testrun", + dataset_id="synthetic", + dataset_path=str(tmp_path / "dataset.json"), + corpus_dir=str(corpus_dir), + queries_path=str(queries_path), + output_root=str(tmp_path / "runs"), + providers=["p1", "p2"], + allow_provider_skip=False, + ) + + dataset = DatasetProvenance( + dataset_id="synthetic", + source_url="local", + checksum_sha256="abc", + license_note="test", + fetched_at_utc="2026-01-01T00:00:00Z", + ) + + providers = { + "p1": FakeProvider("p1", {"alpha": "doc-a"}), + "p2": FakeProvider("p2", {"alpha": "doc-a"}), + } + + run_dir = run_retrieval( + run_config=config, + dataset=dataset, + provider_factory=lambda name: providers[name], + ) + + assert (run_dir / "manifest.json").exists() + assert (run_dir / "retrieval-summary.json").exists() + assert (run_dir / "per-query-retrieval.jsonl").exists() diff --git a/benchmarks/tests/converters/__init__.py b/benchmarks/tests/converters/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/benchmarks/tests/converters/test_convomem_converter.py b/benchmarks/tests/converters/test_convomem_converter.py new file mode 100644 index 000000000..f4cdcd2fd --- /dev/null +++ b/benchmarks/tests/converters/test_convomem_converter.py @@ -0,0 +1,182 @@ +"""Tests for the ConvoMem stratified sampler/converter.""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from basic_memory_benchmarks.converters.convomem_to_corpus import ( + convert_convomem_to_corpus, +) + + +def _case( + context_size: int, + conversation_ids: list[str], + evidence_conv_ids: list[str], + question: str = "What did I say?", + answer: str = "You said the thing.", +) -> dict: + return { + "contextSize": context_size, + "conversations": [ + { + "id": conv_id, + "containsEvidence": conv_id in evidence_conv_ids, + "model_name": "gemini-2.5-pro", + "messages": [ + {"speaker": "user", "text": f"hello from conversation {index}"}, + {"speaker": "assistant", "text": "hi there"}, + ], + } + for index, conv_id in enumerate(conversation_ids) + ], + "evidenceItems": [ + { + "question": question, + "answer": answer, + "category": "Personal Life", + "conversations": [{"id": conv_id} for conv_id in evidence_conv_ids], + "message_evidences": [], + } + ], + } + + +def _write_batch(batches_dir: Path, category: str, level: int, name: str, cases: list[dict]): + batches_dir.mkdir(parents=True, exist_ok=True) + (batches_dir / f"{category}__{level}__{name}.json").write_text( + json.dumps(cases), encoding="utf-8" + ) + + +class TestConvertConvomem: + def test_grouped_output_and_ground_truth(self, tmp_path): + batches = tmp_path / "batches" + _write_batch( + batches, + "user_evidence", + 1, + "batched_000", + [_case(2, ["conv-a", "conv-b"], ["conv-b"])], + ) + + groups_dir, queries_path, doc_count, query_count = convert_convomem_to_corpus( + batches_dir=batches, + output_dir=tmp_path / "out", + sample_per_stratum=10, + context_sizes=(2,), + ) + + assert doc_count == 2 + assert query_count == 1 + queries = json.loads(queries_path.read_text()) + query = queries[0] + assert query["category"] == "user_facts" + assert query["group"].startswith("user_facts-cs2-batched_000-") + # conv-b was index 1 -> doc id suffix c001. + assert query["ground_truth"] == [f"{query['group']}-c001"] + assert query["metadata"]["context_size"] == 2 + assert query["metadata"]["abstention"] is False + + def test_leakage_fields_scrubbed(self, tmp_path): + batches = tmp_path / "batches" + _write_batch( + batches, + "user_evidence", + 1, + "batched_000", + [_case(2, ["conv-a", "conv-b"], ["conv-b"])], + ) + groups_dir, _, _, _ = convert_convomem_to_corpus( + batches_dir=batches, output_dir=tmp_path / "out", context_sizes=(2,) + ) + + corpus_text = "".join(path.read_text() for path in groups_dir.rglob("*.md")) + assert "containsEvidence" not in corpus_text + assert "gemini" not in corpus_text + assert "conv-a" not in corpus_text # raw conversation ids remapped + + def test_sampling_is_deterministic(self, tmp_path): + batches = tmp_path / "batches" + cases = [_case(2, [f"conv-{i}-a", f"conv-{i}-b"], [f"conv-{i}-b"]) for i in range(20)] + _write_batch(batches, "user_evidence", 1, "batched_000", cases) + + ids_by_run = [] + for run in range(2): + out = tmp_path / f"out{run}" + _, queries_path, _, _ = convert_convomem_to_corpus( + batches_dir=batches, + output_dir=out, + sample_per_stratum=5, + seed=42, + context_sizes=(2,), + ) + ids_by_run.append([q["id"] for q in json.loads(queries_path.read_text())]) + + assert ids_by_run[0] == ids_by_run[1] + assert len(ids_by_run[0]) == 5 + + def test_different_seed_changes_sample(self, tmp_path): + batches = tmp_path / "batches" + cases = [_case(2, [f"conv-{i}-a", f"conv-{i}-b"], [f"conv-{i}-b"]) for i in range(20)] + _write_batch(batches, "user_evidence", 1, "batched_000", cases) + + samples = [] + for seed in (42, 43): + out = tmp_path / f"out-seed{seed}" + _, queries_path, _, _ = convert_convomem_to_corpus( + batches_dir=batches, + output_dir=out, + sample_per_stratum=5, + seed=seed, + context_sizes=(2,), + ) + samples.append({q["id"] for q in json.loads(queries_path.read_text())}) + assert samples[0] != samples[1] + + def test_stratification_and_manifest(self, tmp_path): + batches = tmp_path / "batches" + _write_batch( + batches, + "user_evidence", + 1, + "batched_000", + [_case(2, ["a1", "a2"], ["a2"]), _case(4, ["b1", "b2", "b3", "b4"], ["b1"])], + ) + _write_batch( + batches, + "abstention_evidence", + 1, + "batched_000", + [_case(2, ["c1", "c2"], [])], + ) + + out = tmp_path / "out" + _, queries_path, _, query_count = convert_convomem_to_corpus( + batches_dir=batches, output_dir=out, context_sizes=(2, 4) + ) + + assert query_count == 3 + manifest = json.loads((out / "sampling.json").read_text()) + assert manifest["seed"] == 42 + assert manifest["strata"]["user_facts/cs2"] == {"population": 1, "sampled": 1} + assert manifest["strata"]["user_facts/cs4"] == {"population": 1, "sampled": 1} + assert manifest["strata"]["abstention/cs2"] == {"population": 1, "sampled": 1} + + queries = json.loads(queries_path.read_text()) + abstention = [q for q in queries if q["category"] == "abstention"][0] + # Abstention evidence may reference conversations absent from the + # haystack; ground truth is simply empty then. + assert abstention["ground_truth"] == [] + assert abstention["metadata"]["abstention"] is True + + def test_context_size_filter_excludes(self, tmp_path): + batches = tmp_path / "batches" + _write_batch(batches, "user_evidence", 1, "batched_000", [_case(2, ["a1"], ["a1"])]) + with pytest.raises(ValueError, match="No ConvoMem cases matched"): + convert_convomem_to_corpus( + batches_dir=batches, output_dir=tmp_path / "out", context_sizes=(30,) + ) diff --git a/benchmarks/tests/converters/test_locomo_audit_corrections.py b/benchmarks/tests/converters/test_locomo_audit_corrections.py new file mode 100644 index 000000000..2061c83cf --- /dev/null +++ b/benchmarks/tests/converters/test_locomo_audit_corrections.py @@ -0,0 +1,155 @@ +"""Tests for applying Penfield audit corrections during LoCoMo conversion.""" + +from __future__ import annotations + +import json + +import pytest + +from basic_memory_benchmarks.converters.locomo_to_corpus import convert_locomo_to_corpus +from basic_memory_benchmarks.datasets.locomo_audit import load_locomo_corrections + + +def _locomo_blob() -> list[dict]: + return [ + { + "conversation": { + "session_1": [ + {"speaker": "Melanie", "text": "I painted a sunrise last year."}, + ], + "session_2": [ + {"speaker": "Caroline", "text": "I studied counseling."}, + ], + }, + "qa": [ + { + "question": "When did Melanie paint a sunrise?", + "answer": "2022", + "category": 2, + "evidence": ["D1:12"], + }, + { + "question": "What did Caroline study?", + "answer": "Psychology, counseling certification", + "category": 1, + "evidence": ["D2:3"], + }, + ], + } + ] + + +def _corrections() -> list[dict]: + return [ + { + "question_id": "locomo_0_qa1", + "question": "What did Caroline study?", + "golden_answer": "Psychology, counseling certification", + "category": 1, + "error_type": "HALLUCINATION", + "cited_evidence": ["D2:3"], + "correct_evidence": ["D1:1"], + "correct_answer": "Counseling or mental health", + } + ] + + +@pytest.fixture +def converted(tmp_path): + dataset = tmp_path / "locomo10.json" + dataset.write_text(json.dumps(_locomo_blob()), encoding="utf-8") + corrections = tmp_path / "corrections.json" + corrections.write_text(json.dumps(_corrections()), encoding="utf-8") + + _, queries_path, _, _ = convert_locomo_to_corpus( + dataset_path=dataset, + output_dir=tmp_path / "out", + audit_corrections_path=corrections, + ) + return json.loads(queries_path.read_text()) + + +class TestAuditCorrections: + def test_corrected_answer_and_evidence_applied(self, converted): + corrected = converted[1] + assert corrected["expected_answer"] == "Counseling or mental health" + # Corrected evidence D1:1 maps to session 1's doc. + assert corrected["ground_truth"] == ["locomo-c00-s01"] + assert corrected["metadata"]["audit_corrected"] is True + assert corrected["metadata"]["audit_error_type"] == "HALLUCINATION" + + def test_uncorrected_query_untouched(self, converted): + untouched = converted[0] + assert untouched["expected_answer"] == "2022" + assert untouched["ground_truth"] == ["locomo-c00-s01"] + assert "audit_corrected" not in untouched["metadata"] + + def test_question_text_mismatch_raises(self, tmp_path): + dataset = tmp_path / "locomo10.json" + dataset.write_text(json.dumps(_locomo_blob()), encoding="utf-8") + bad = _corrections() + bad[0]["question"] = "A different question entirely?" + corrections = tmp_path / "corrections.json" + corrections.write_text(json.dumps(bad), encoding="utf-8") + + with pytest.raises(ValueError, match="does not match dataset question"): + convert_locomo_to_corpus( + dataset_path=dataset, + output_dir=tmp_path / "out", + audit_corrections_path=corrections, + ) + + def test_no_corrections_path_is_unchanged_behavior(self, tmp_path): + dataset = tmp_path / "locomo10.json" + dataset.write_text(json.dumps(_locomo_blob()), encoding="utf-8") + _, queries_path, _, _ = convert_locomo_to_corpus( + dataset_path=dataset, output_dir=tmp_path / "out" + ) + queries = json.loads(queries_path.read_text()) + assert queries[1]["expected_answer"] == "Psychology, counseling certification" + + +class TestLoadCorrections: + def test_load_and_key_by_question_id(self, tmp_path): + path = tmp_path / "corrections.json" + path.write_text(json.dumps(_corrections()), encoding="utf-8") + loaded = load_locomo_corrections(path) + assert set(loaded) == {"locomo_0_qa1"} + + def test_duplicate_question_id_raises(self, tmp_path): + path = tmp_path / "corrections.json" + path.write_text(json.dumps(_corrections() * 2), encoding="utf-8") + with pytest.raises(ValueError, match="Duplicate correction"): + load_locomo_corrections(path) + + def test_missing_keys_raises(self, tmp_path): + path = tmp_path / "corrections.json" + path.write_text(json.dumps([{"question_id": "locomo_0_qa1"}]), encoding="utf-8") + with pytest.raises(ValueError, match="missing keys"): + load_locomo_corrections(path) + + +class TestSessionDates: + def test_session_date_in_doc(self, tmp_path): + blob = _locomo_blob() + blob[0]["conversation"]["session_1_date_time"] = "1:56 pm on 8 May, 2023" + dataset = tmp_path / "locomo10.json" + dataset.write_text(json.dumps(blob), encoding="utf-8") + + docs_dir, _, _, _ = convert_locomo_to_corpus( + dataset_path=dataset, output_dir=tmp_path / "out" + ) + doc = (docs_dir / "locomo-c00-s01.md").read_text() + assert "session_date: 1:56 pm on 8 May, 2023" in doc + assert "# Chat session at 1:56 pm on 8 May, 2023" in doc + assert "title: locomo-c00-s01 (1:56 pm on 8 May, 2023)" in doc + + def test_missing_date_keeps_plain_heading(self, tmp_path): + dataset = tmp_path / "locomo10.json" + dataset.write_text(json.dumps(_locomo_blob()), encoding="utf-8") + docs_dir, _, _, _ = convert_locomo_to_corpus( + dataset_path=dataset, output_dir=tmp_path / "out" + ) + doc = (docs_dir / "locomo-c00-s01.md").read_text() + assert "session_date:" not in doc + assert "# locomo-c00-s01" in doc diff --git a/benchmarks/tests/converters/test_longmemeval_converter.py b/benchmarks/tests/converters/test_longmemeval_converter.py new file mode 100644 index 000000000..d2ee27d80 --- /dev/null +++ b/benchmarks/tests/converters/test_longmemeval_converter.py @@ -0,0 +1,182 @@ +"""Tests for the LongMemEval-S grouped corpus converter.""" + +from __future__ import annotations + +import json + +import pytest + +from basic_memory_benchmarks.converters.longmemeval_to_corpus import ( + convert_longmemeval_to_corpus, +) + + +def _entry( + question_id: str = "q1", + question_type: str = "single-session-user", + answer_session_ids: list[str] | None = None, + session_ids: list[str] | None = None, +) -> dict: + session_ids = session_ids or ["filler_001", "answer_abc", "filler_002"] + return { + "question_id": question_id, + "question_type": question_type, + "question": "What degree did I graduate with?", + "answer": "Business Administration", + "question_date": "2023/05/30 (Tue) 23:40", + "haystack_session_ids": session_ids, + "haystack_dates": [f"2023/05/{i + 1:02d} (Mon) 10:00" for i in range(len(session_ids))], + "haystack_sessions": [ + [ + { + "role": "user", + "content": f"hello in session number {index}", + "has_answer": sid.startswith("answer_"), + }, + {"role": "assistant", "content": "hi there"}, + ] + for index, sid in enumerate(session_ids) + ], + "answer_session_ids": answer_session_ids or ["answer_abc"], + } + + +def _write_dataset(tmp_path, entries): + path = tmp_path / "longmemeval_s.json" + path.write_text(json.dumps(entries), encoding="utf-8") + return path + + +class TestConvertLongMemEval: + def test_grouped_layout_and_queries(self, tmp_path): + dataset = _write_dataset(tmp_path, [_entry("q1"), _entry("q2")]) + out = tmp_path / "out" + + groups_dir, queries_path, doc_count, query_count = convert_longmemeval_to_corpus( + dataset_path=dataset, output_dir=out + ) + + assert doc_count == 6 + assert query_count == 2 + assert (groups_dir / "q1" / "docs").is_dir() + assert (groups_dir / "q2" / "docs").is_dir() + + queries = json.loads(queries_path.read_text()) + by_id = {q["id"]: q for q in queries} + assert by_id["q1"]["group"] == "q1" + assert by_id["q1"]["category"] == "single-session-user" + assert by_id["q1"]["expected_answer"] == "Business Administration" + assert by_id["q1"]["metadata"]["question_date"] == "2023/05/30 (Tue) 23:40" + assert by_id["q1"]["metadata"]["abstention"] is False + + def test_ground_truth_leakage_is_scrubbed(self, tmp_path): + """Evidence markers in the raw data must not survive conversion.""" + dataset = _write_dataset(tmp_path, [_entry("q1")]) + out = tmp_path / "out" + + groups_dir, queries_path, _, _ = convert_longmemeval_to_corpus( + dataset_path=dataset, output_dir=out + ) + + doc_paths = sorted((groups_dir / "q1" / "docs").glob("*.md")) + assert [p.stem for p in doc_paths] == ["q1-s000", "q1-s001", "q1-s002"] + corpus_text = "".join(p.read_text() for p in doc_paths) + assert "answer_" not in corpus_text + assert "has_answer" not in corpus_text + + queries = json.loads(queries_path.read_text()) + # Ground truth maps through the neutral ids: answer_abc was index 1. + assert queries[0]["ground_truth"] == ["q1-s001"] + + def test_duplicate_sessions_kept_once(self, tmp_path): + entry = _entry( + "q1", + session_ids=["filler_001", "answer_abc", "filler_001"], + ) + dataset = _write_dataset(tmp_path, [entry]) + + _, _, doc_count, _ = convert_longmemeval_to_corpus( + dataset_path=dataset, output_dir=tmp_path / "out" + ) + assert doc_count == 2 + + def test_abstention_flag_from_question_id(self, tmp_path): + dataset = _write_dataset(tmp_path, [_entry("q9_abs")]) + _, queries_path, _, _ = convert_longmemeval_to_corpus( + dataset_path=dataset, output_dir=tmp_path / "out" + ) + queries = json.loads(queries_path.read_text()) + assert queries[0]["metadata"]["abstention"] is True + + def test_session_date_in_doc(self, tmp_path): + dataset = _write_dataset(tmp_path, [_entry("q1")]) + groups_dir, _, _, _ = convert_longmemeval_to_corpus( + dataset_path=dataset, output_dir=tmp_path / "out" + ) + doc = (groups_dir / "q1" / "docs" / "q1-s000.md").read_text() + assert "session_date: 2023/05/01 (Mon) 10:00" in doc + assert "# Chat session on 2023/05/01 (Mon) 10:00" in doc + assert "- **User:** hello in session number 0" in doc + + def test_missing_answer_session_raises(self, tmp_path): + entry = _entry("q1", answer_session_ids=["not_in_haystack"]) + dataset = _write_dataset(tmp_path, [entry]) + with pytest.raises(ValueError, match="not present in haystack"): + convert_longmemeval_to_corpus(dataset_path=dataset, output_dir=tmp_path / "out") + + def test_max_questions(self, tmp_path): + dataset = _write_dataset(tmp_path, [_entry("q1"), _entry("q2"), _entry("q3")]) + _, _, _, query_count = convert_longmemeval_to_corpus( + dataset_path=dataset, output_dir=tmp_path / "out", max_questions=2 + ) + assert query_count == 2 + + +class TestStratifiedSlice: + def test_stratified_covers_types_evenly(self, tmp_path): + entries = [] + for t in ["single-session-user", "multi-session", "temporal-reasoning"]: + for i in range(10): + entries.append(_entry(f"{t[:4]}{i}", question_type=t)) + dataset = _write_dataset(tmp_path, entries) + + _, queries_path, _, count = convert_longmemeval_to_corpus( + dataset_path=dataset, + output_dir=tmp_path / "out", + max_questions=9, + stratified=True, + ) + + import collections + + queries = json.loads(queries_path.read_text()) + by_type = collections.Counter(q["category"] for q in queries) + assert count == 9 + assert all(v == 3 for v in by_type.values()) + sampling = json.loads((tmp_path / "out" / "sampling.json").read_text()) + assert sampling["seed"] == 42 + + def test_stratified_deterministic(self, tmp_path): + entries = [_entry(f"q{i}", question_type="multi-session") for i in range(20)] + dataset = _write_dataset(tmp_path, entries) + ids = [] + for run in range(2): + _, qp, _, _ = convert_longmemeval_to_corpus( + dataset_path=dataset, + output_dir=tmp_path / f"out{run}", + max_questions=5, + stratified=True, + ) + ids.append([q["id"] for q in json.loads(qp.read_text())]) + assert ids[0] == ids[1] + + def test_prefix_mode_unchanged(self, tmp_path): + entries = [_entry(f"q{i}") for i in range(5)] + dataset = _write_dataset(tmp_path, entries) + _, qp, _, count = convert_longmemeval_to_corpus( + dataset_path=dataset, + output_dir=tmp_path / "out", + max_questions=2, + ) + assert count == 2 + assert [q["id"] for q in json.loads(qp.read_text())] == ["q0", "q1"] diff --git a/benchmarks/tests/llm/test_runners.py b/benchmarks/tests/llm/test_runners.py new file mode 100644 index 000000000..55d74a829 --- /dev/null +++ b/benchmarks/tests/llm/test_runners.py @@ -0,0 +1,102 @@ +"""Tests for LLM runner spec parsing and transports.""" + +from __future__ import annotations + +import json +import subprocess + +import pytest + +from basic_memory_benchmarks.llm.runners import ( + ClaudeCLIRunner, + LLMRunnerError, + OpenAICompatRunner, + create_runner, +) + + +class TestCreateRunner: + def test_claude_spec(self): + runner = create_runner("claude:claude-haiku-4-5") + assert isinstance(runner, ClaudeCLIRunner) + assert runner.model == "claude-haiku-4-5" + assert runner.spec == "claude:claude-haiku-4-5" + + def test_openai_compat_spec(self): + runner = create_runner("openai-compat:llama3.1@http://localhost:11434/v1") + assert isinstance(runner, OpenAICompatRunner) + assert runner.model == "llama3.1" + assert runner.base_url == "http://localhost:11434/v1" + + def test_openai_compat_spec_requires_base_url(self): + with pytest.raises(ValueError): + create_runner("openai-compat:llama3.1") + + def test_unknown_transport_rejected(self): + with pytest.raises(ValueError): + create_runner("gemini:flash") + + def test_empty_model_rejected(self): + with pytest.raises(ValueError): + create_runner("claude:") + + +class TestClaudeCLIRunner: + def _completed(self, payload: dict, returncode: int = 0) -> subprocess.CompletedProcess: + return subprocess.CompletedProcess( + args=[], returncode=returncode, stdout=json.dumps(payload), stderr="" + ) + + def test_parses_result_and_usage(self, monkeypatch): + payload = { + "is_error": False, + "result": "Paris", + "usage": {"input_tokens": 120, "output_tokens": 8}, + } + captured: dict = {} + + def fake_run(command, **kwargs): + captured["command"] = command + captured["input"] = kwargs.get("input") + return self._completed(payload) + + monkeypatch.setattr(subprocess, "run", fake_run) + runner = ClaudeCLIRunner(model="claude-haiku-4-5") + result = runner.complete("What is the capital of France?") + + assert result.text == "Paris" + assert result.input_tokens == 120 + assert result.output_tokens == 8 + assert captured["input"] == "What is the capital of France?" + assert "--max-turns" in captured["command"] + assert "claude-haiku-4-5" in captured["command"] + + def test_error_payload_raises_after_retries(self, monkeypatch): + attempts = {"count": 0} + + def fake_run(command, **kwargs): + attempts["count"] += 1 + return self._completed({"is_error": True, "result": "overloaded"}, returncode=1) + + monkeypatch.setattr(subprocess, "run", fake_run) + runner = ClaudeCLIRunner(model="claude-haiku-4-5", max_retries=1) + with pytest.raises(LLMRunnerError): + runner.complete("hello") + assert attempts["count"] == 2 + + def test_retry_then_success(self, monkeypatch): + attempts = {"count": 0} + good = {"is_error": False, "result": "ok", "usage": {}} + + def fake_run(command, **kwargs): + attempts["count"] += 1 + if attempts["count"] == 1: + return subprocess.CompletedProcess( + args=[], returncode=0, stdout="not json", stderr="" + ) + return self._completed(good) + + monkeypatch.setattr(subprocess, "run", fake_run) + runner = ClaudeCLIRunner(model="claude-haiku-4-5", max_retries=1) + assert runner.complete("hello").text == "ok" + assert attempts["count"] == 2 diff --git a/benchmarks/tests/providers/test_baseline_providers.py b/benchmarks/tests/providers/test_baseline_providers.py new file mode 100644 index 000000000..4cd8e389a --- /dev/null +++ b/benchmarks/tests/providers/test_baseline_providers.py @@ -0,0 +1,114 @@ +"""Tests for the grep and full-context baseline providers.""" + +from __future__ import annotations + +from pathlib import Path + +from basic_memory_benchmarks.models import RunConfig +from basic_memory_benchmarks.providers import create_provider +from basic_memory_benchmarks.providers.baseline_fullcontext import ( + MAX_CONTEXT_CHARS, + FullContextProvider, +) +from basic_memory_benchmarks.providers.baseline_grep import FilesystemGrepProvider + + +def _write_doc(corpus: Path, doc_id: str, body: str) -> None: + corpus.mkdir(parents=True, exist_ok=True) + (corpus / f"{doc_id}.md").write_text( + f"---\ntitle: {doc_id}\nsource_doc_id: {doc_id}\n---\n\n{body}\n", + encoding="utf-8", + ) + + +def _config(tmp_path: Path) -> RunConfig: + return RunConfig( + run_id="t", + dataset_id="t", + dataset_path="t", + corpus_dir=str(tmp_path), + queries_path="t", + ) + + +class TestFilesystemGrep: + def test_ranks_matching_doc_first(self, tmp_path): + corpus = tmp_path / "docs" + _write_doc(corpus, "doc-a", "Joanna moved to Austin and loves the food scene in Austin.") + _write_doc(corpus, "doc-b", "Anthony trains for marathons every morning.") + provider = FilesystemGrepProvider() + provider.ingest(corpus, _config(tmp_path)) + + hits = provider.search("Where does Joanna live in Austin?", 5, _config(tmp_path)) + + assert hits[0].source_doc_id == "doc-a" + assert hits[0].score > 0 + assert "Joanna" in (hits[0].text or "") + + def test_coverage_beats_repetition(self, tmp_path): + corpus = tmp_path / "docs" + # doc-spam repeats one term; doc-both matches both distinct terms. + _write_doc(corpus, "doc-spam", "dentist " * 30) + _write_doc(corpus, "doc-both", "I visited the dentist in November.") + provider = FilesystemGrepProvider() + provider.ingest(corpus, _config(tmp_path)) + + hits = provider.search("dentist November", 5, _config(tmp_path)) + + assert hits[0].source_doc_id == "doc-both" + + def test_no_content_terms_returns_empty(self, tmp_path): + corpus = tmp_path / "docs" + _write_doc(corpus, "doc-a", "anything") + provider = FilesystemGrepProvider() + provider.ingest(corpus, _config(tmp_path)) + assert provider.search("what did they do", 5, _config(tmp_path)) == [] + + def test_limit_respected(self, tmp_path): + corpus = tmp_path / "docs" + for i in range(10): + _write_doc(corpus, f"doc-{i}", "Austin is great.") + provider = FilesystemGrepProvider() + provider.ingest(corpus, _config(tmp_path)) + assert len(provider.search("Austin", 3, _config(tmp_path))) == 3 + + def test_factory_registration(self): + assert isinstance(create_provider("baseline-grep"), FilesystemGrepProvider) + + +class TestFullContext: + def test_returns_whole_corpus_as_single_hit(self, tmp_path): + corpus = tmp_path / "docs" + _write_doc(corpus, "doc-a", "Joanna moved to Austin.") + _write_doc(corpus, "doc-b", "Anthony runs marathons.") + provider = FullContextProvider() + provider.ingest(corpus, _config(tmp_path)) + + hits = provider.search("anything", 10, _config(tmp_path)) + + assert len(hits) == 1 + assert "Joanna moved to Austin." in (hits[0].text or "") + assert "Anthony runs marathons." in (hits[0].text or "") + assert hits[0].source_doc_id is None # no retrieval claims + + def test_truncation_capped_and_flagged(self, tmp_path): + corpus = tmp_path / "docs" + _write_doc(corpus, "doc-big", "x" * (MAX_CONTEXT_CHARS + 1000)) + provider = FullContextProvider() + provider.ingest(corpus, _config(tmp_path)) + + hits = provider.search("anything", 10, _config(tmp_path)) + + assert len(hits[0].text or "") <= MAX_CONTEXT_CHARS + assert hits[0].metadata["truncated"] is True + + def test_cleanup_clears_context(self, tmp_path): + corpus = tmp_path / "docs" + _write_doc(corpus, "doc-a", "content") + provider = FullContextProvider() + provider.ingest(corpus, _config(tmp_path)) + provider.cleanup(_config(tmp_path)) + assert provider.search("anything", 10, _config(tmp_path)) == [] + + def test_factory_registration(self): + assert isinstance(create_provider("baseline-fullcontext"), FullContextProvider) diff --git a/benchmarks/tests/providers/test_bm_local_provider.py b/benchmarks/tests/providers/test_bm_local_provider.py new file mode 100644 index 000000000..4ed3fe5b7 --- /dev/null +++ b/benchmarks/tests/providers/test_bm_local_provider.py @@ -0,0 +1,176 @@ +from pathlib import Path + +import pytest +from mcp.types import CallToolResult, TextContent + +from basic_memory_benchmarks.models import RunConfig +from basic_memory_benchmarks.providers.bm_local import BasicMemoryLocalProvider + + +def test_extract_existing_project_name_from_bm_error() -> None: + message = ( + "Error adding project: Cannot create project at '/tmp/docs': " + "path is nested within existing project 'bm-bench-abc123' at '/tmp/docs'." + ) + assert BasicMemoryLocalProvider._extract_existing_project_name(message) == "bm-bench-abc123" + + +def test_extract_existing_project_name_none_without_match() -> None: + message = "Error adding project: unknown failure" + assert BasicMemoryLocalProvider._extract_existing_project_name(message) is None + + +def test_extract_existing_project_name_from_wrapped_bm_error() -> None: + message = ( + "Error adding project: path is nested within existing project \n" + "'bm-bench-wrap999' at '/tmp/docs'." + ) + assert BasicMemoryLocalProvider._extract_existing_project_name(message) == "bm-bench-wrap999" + + +def test_payload_from_call_tool_result_uses_structured_content() -> None: + result = CallToolResult( + content=[TextContent(type="text", text='{"results":[{"entity_id":"x"}]}')], + structuredContent={"results": [{"entity_id": "y"}]}, + isError=False, + ) + payload = BasicMemoryLocalProvider._payload_from_call_tool_result(result) + assert payload["results"][0]["entity_id"] == "y" + + +def test_payload_from_call_tool_result_unwraps_structured_result() -> None: + result = CallToolResult( + content=[TextContent(type="text", text='{"results":[{"entity_id":"x"}]}')], + structuredContent={"result": {"results": [{"entity_id": "wrapped"}]}}, + isError=False, + ) + payload = BasicMemoryLocalProvider._payload_from_call_tool_result(result) + assert payload["results"][0]["entity_id"] == "wrapped" + + +def test_payload_from_call_tool_result_parses_text_json() -> None: + result = CallToolResult( + content=[TextContent(type="text", text='{"results":[{"entity_id":"x"}]}')], + structuredContent=None, + isError=False, + ) + payload = BasicMemoryLocalProvider._payload_from_call_tool_result(result) + assert payload["results"][0]["entity_id"] == "x" + + +def test_payload_from_call_tool_result_raises_on_error() -> None: + result = CallToolResult( + content=[TextContent(type="text", text="search failed")], + structuredContent=None, + isError=True, + ) + with pytest.raises(RuntimeError, match="search failed"): + BasicMemoryLocalProvider._payload_from_call_tool_result(result) + + +def test_status_json_is_ready_no_changes() -> None: + payload = {"status": "No changes"} + assert BasicMemoryLocalProvider._status_json_is_ready(payload) + + +def test_status_json_is_ready_sync_payload_total_zero() -> None: + payload = { + "new": [], + "modified": [], + "deleted": [], + "moves": {}, + "checksums": {}, + "skipped_files": [], + "total": 0, + } + assert BasicMemoryLocalProvider._status_json_is_ready(payload) + + +def test_status_json_is_not_ready_sync_payload_with_changes() -> None: + payload = { + "new": ["a.md"], + "modified": [], + "deleted": [], + "moves": {}, + "checksums": {}, + "skipped_files": [], + "total": 1, + } + assert not BasicMemoryLocalProvider._status_json_is_ready(payload) + + +def test_status_json_is_not_ready_when_indexing() -> None: + payload = {"is_indexing": True} + assert not BasicMemoryLocalProvider._status_json_is_ready(payload) + + +def test_resolve_bm_command_prefix_default_uses_bm() -> None: + run_config = RunConfig( + run_id="r1", + dataset_id="synthetic", + dataset_path="dataset.json", + corpus_dir="docs", + queries_path="queries.json", + ) + assert BasicMemoryLocalProvider._resolve_bm_command_prefix(run_config) == ["bm"] + + +def test_resolve_bm_command_prefix_local_path_uses_uv_project(tmp_path: Path) -> None: + run_config = RunConfig( + run_id="r1", + dataset_id="synthetic", + dataset_path="dataset.json", + corpus_dir="docs", + queries_path="queries.json", + bm_local_path=str(tmp_path), + ) + assert BasicMemoryLocalProvider._resolve_bm_command_prefix(run_config) == [ + "uv", + "run", + "--project", + str(tmp_path), + "basic-memory", + ] + + +def test_resolve_bm_command_prefix_local_path_missing_raises() -> None: + run_config = RunConfig( + run_id="r1", + dataset_id="synthetic", + dataset_path="dataset.json", + corpus_dir="docs", + queries_path="queries.json", + bm_local_path="/path/that/does/not/exist", + ) + with pytest.raises(ValueError, match="--bm-local-path not found"): + BasicMemoryLocalProvider._resolve_bm_command_prefix(run_config) + + +def test_row_to_hit_surfaces_title_for_date_anchoring() -> None: + """The document title (carrying the session date) reaches hit metadata.""" + from basic_memory_benchmarks.providers.bm_local import BasicMemoryLocalProvider + + row = { + "title": "locomo-c00-s07 (4:33 pm on 12 July, 2023)", + "entity_id": 7, + "file_path": "locomo-c00-s07.md", + "matched_chunk": "- **Caroline:** I went to an LGBTQ conference two days ago", + "content": "# Chat session at 4:33 pm on 12 July, 2023\n...", + "score": 1.13, + "metadata": {"note_type": "note"}, + } + hit = BasicMemoryLocalProvider._row_to_hit(row) + assert hit.metadata["title"] == "locomo-c00-s07 (4:33 pm on 12 July, 2023)" + assert hit.metadata["note_type"] == "note" # existing metadata preserved + assert hit.source_doc_id == "locomo-c00-s07" + assert "two days ago" in (hit.text or "") + assert hit.score == 1.13 + + +def test_row_to_hit_without_title_omits_key() -> None: + from basic_memory_benchmarks.providers.bm_local import BasicMemoryLocalProvider + + hit = BasicMemoryLocalProvider._row_to_hit( + {"entity_id": 1, "matched_chunk": "text", "file_path": "doc.md"} + ) + assert "title" not in hit.metadata diff --git a/benchmarks/tests/providers/test_mem0_normalization.py b/benchmarks/tests/providers/test_mem0_normalization.py new file mode 100644 index 000000000..393f336a6 --- /dev/null +++ b/benchmarks/tests/providers/test_mem0_normalization.py @@ -0,0 +1,126 @@ +import pytest + +from basic_memory_benchmarks.providers.mem0_local import Mem0LocalProvider + + +def test_mem0_normalization_prefers_nested_metadata() -> None: + item = { + "id": "abc", + "memory": "hello", + "score": 0.42, + "metadata": { + "source_doc_id": "doc-1", + "source_path": "docs/doc-1.md", + }, + } + hit = Mem0LocalProvider._normalize_item(item) + assert hit.source_doc_id == "doc-1" + assert hit.source_path == "docs/doc-1.md" + assert hit.score == 0.42 + + +class TestLocalBackendConfig: + def _config(self, run_id: str = "run1"): + from basic_memory_benchmarks.models import RunConfig + + return RunConfig( + run_id=run_id, + dataset_id="t", + dataset_path="t", + corpus_dir="t", + queries_path="t", + ) + + def test_local_config_shape(self, monkeypatch): + from basic_memory_benchmarks.providers.mem0_local import Mem0LocalProvider + + monkeypatch.delenv("MEM0_LLM_MODEL", raising=False) + monkeypatch.delenv("MEM0_EMBED_MODEL", raising=False) + monkeypatch.delenv("MEM0_EMBED_DIMS", raising=False) + provider = Mem0LocalProvider() + config = provider._local_config("http://localhost:11434/v1", self._config("abc-q1")) + + assert config["llm"]["provider"] == "openai" + assert config["llm"]["config"]["openai_base_url"] == "http://localhost:11434/v1" + assert config["embedder"]["config"]["embedding_dims"] == 768 + # Run-scoped, qdrant-safe collection name (no dashes). + assert config["vector_store"]["config"]["collection_name"] == "bm_bench_abc_q1" + assert config["vector_store"]["config"]["embedding_model_dims"] == 768 + + def test_skipped_without_any_backend(self, monkeypatch): + from basic_memory_benchmarks.exceptions import ProviderSkippedError + from basic_memory_benchmarks.providers.mem0_local import Mem0LocalProvider + + monkeypatch.delenv("OPENAI_API_KEY", raising=False) + monkeypatch.delenv("MEM0_OPENAI_COMPAT_BASE_URL", raising=False) + provider = Mem0LocalProvider() + with pytest.raises(ProviderSkippedError, match="MEM0_OPENAI_COMPAT_BASE_URL"): + provider._ensure_memory(self._config()) + + def test_infer_env_flag(self, monkeypatch): + from basic_memory_benchmarks.providers.mem0_local import Mem0LocalProvider + + monkeypatch.setenv("MEM0_INFER", "true") + assert Mem0LocalProvider()._infer is True + monkeypatch.setenv("MEM0_INFER", "false") + assert Mem0LocalProvider()._infer is False + monkeypatch.delenv("MEM0_INFER") + assert Mem0LocalProvider()._infer is False + + +class TestTelemetryLockAvoidance: + def test_telemetry_disabled_before_import(self, monkeypatch): + import os + + from basic_memory_benchmarks.providers.mem0_local import Mem0LocalProvider + + monkeypatch.setenv("MEM0_OPENAI_COMPAT_BASE_URL", "http://localhost:1/v1") + monkeypatch.delenv("MEM0_TELEMETRY", raising=False) + provider = Mem0LocalProvider() + # _ensure_memory sets the env var before importing mem0; construction + # against the dead endpoint may fail later, which is fine here. + try: + provider._ensure_memory( + __import__("basic_memory_benchmarks.models", fromlist=["RunConfig"]).RunConfig( + run_id="t", dataset_id="t", dataset_path="t", corpus_dir="t", queries_path="t" + ) + ) + except Exception: + pass + assert os.environ["MEM0_TELEMETRY"] == "false" + + def test_cleanup_closes_clients_and_drops_memory(self): + from basic_memory_benchmarks.providers.mem0_local import Mem0LocalProvider + + class FakeClient: + closed = False + + def close(self): + self.closed = True + + class FakeStore: + def __init__(self): + self.client = FakeClient() + + class FakeMemory: + def __init__(self): + self.vector_store = FakeStore() + self._telemetry_vector_store = FakeStore() + + def delete_all(self, user_id): + pass + + provider = Mem0LocalProvider() + memory = FakeMemory() + provider._memory = memory + + from basic_memory_benchmarks.models import RunConfig + + provider.cleanup( + RunConfig( + run_id="t", dataset_id="t", dataset_path="t", corpus_dir="t", queries_path="t" + ) + ) + assert memory.vector_store.client.closed + assert memory._telemetry_vector_store.client.closed + assert provider._memory is None diff --git a/benchmarks/tests/providers/test_supermemory_provider.py b/benchmarks/tests/providers/test_supermemory_provider.py new file mode 100644 index 000000000..15c30f0e1 --- /dev/null +++ b/benchmarks/tests/providers/test_supermemory_provider.py @@ -0,0 +1,201 @@ +"""Tests for the supermemory-local provider against a mocked v3 API.""" + +from __future__ import annotations + +import json +from pathlib import Path + +import httpx +import pytest + +from basic_memory_benchmarks.exceptions import ProviderSkippedError +from basic_memory_benchmarks.models import RunConfig +from basic_memory_benchmarks.providers.supermemory_local import SupermemoryLocalProvider + + +def _config(run_id: str = "smrun") -> RunConfig: + return RunConfig( + run_id=run_id, dataset_id="t", dataset_path="t", corpus_dir="t", queries_path="t" + ) + + +def _write_doc(corpus: Path, doc_id: str, body: str) -> None: + corpus.mkdir(parents=True, exist_ok=True) + (corpus / f"{doc_id}.md").write_text( + f"---\ntitle: {doc_id}\nsource_doc_id: {doc_id}\n---\n\n{body}\n", + encoding="utf-8", + ) + + +class FakeServer: + """Minimal in-memory v3 API: add, status poll, search, bulk delete.""" + + def __init__(self, fail_doc_ids: set[str] | None = None, polls_to_done: int = 1): + self.docs: dict[str, dict] = {} + self.deleted_containers: list[list[str]] = [] + self.fail_doc_ids = fail_doc_ids or set() + self.polls_to_done = polls_to_done + self.requests: list[tuple[str, str]] = [] + + def handler(self, request: httpx.Request) -> httpx.Response: + self.requests.append((request.method, request.url.path)) + if request.headers.get("Authorization") != "Bearer sm_test": + return httpx.Response(401, json={"error": "unauthorized"}) + + if request.method == "POST" and request.url.path == "/v3/documents/list": + return httpx.Response(200, json={"memories": [], "pagination": {}}) + + if request.method == "POST" and request.url.path == "/v3/documents": + body = json.loads(request.content) + server_id = f"doc_{body['customId']}" + self.docs[server_id] = {"body": body, "polls": 0} + return httpx.Response(200, json={"id": server_id, "status": "queued"}) + + if request.method == "GET" and request.url.path.startswith("/v3/documents/"): + server_id = request.url.path.rsplit("/", 1)[-1] + doc = self.docs.get(server_id) + if doc is None: + return httpx.Response(404, json={"error": "not found"}) + doc["polls"] += 1 + custom_id = doc["body"]["customId"] + if doc["polls"] < self.polls_to_done: + return httpx.Response(200, json={"id": server_id, "status": "embedding"}) + status = "failed" if custom_id in self.fail_doc_ids else "done" + return httpx.Response(200, json={"id": server_id, "status": status}) + + if request.method == "POST" and request.url.path == "/v3/search": + body = json.loads(request.content) + results = [] + for server_id, doc in self.docs.items(): + content = doc["body"]["content"] + if any(term.lower() in content.lower() for term in body["q"].split()): + results.append( + { + "documentId": server_id, + "title": doc["body"]["customId"], + "score": 0.9, + "chunks": [{"content": content, "score": 0.95, "isRelevant": True}], + "metadata": doc["body"]["metadata"], + } + ) + return httpx.Response( + 200, json={"results": results[: body["limit"]], "total": len(results)} + ) + + if request.method == "DELETE" and request.url.path == "/v3/documents/bulk": + body = json.loads(request.content) + self.deleted_containers.append(body.get("containerTags") or []) + return httpx.Response(200, json={"deleted": len(self.docs)}) + + return httpx.Response(500, json={"error": f"unhandled {request.method} {request.url.path}"}) + + +@pytest.fixture +def env(monkeypatch): + monkeypatch.setenv("SUPERMEMORY_API_KEY", "sm_test") + monkeypatch.setenv("SUPERMEMORY_INGEST_TIMEOUT_S", "5") + + +def _provider(server: FakeServer) -> SupermemoryLocalProvider: + return SupermemoryLocalProvider(transport=httpx.MockTransport(server.handler)) + + +class TestSupermemoryProvider: + def test_ingest_polls_to_done_then_search(self, env, tmp_path, monkeypatch): + monkeypatch.setattr("time.sleep", lambda s: None) + server = FakeServer(polls_to_done=3) + corpus = tmp_path / "docs" + _write_doc(corpus, "doc-austin", "Joanna moved to Austin last spring.") + _write_doc(corpus, "doc-marathon", "Anthony runs marathons.") + provider = _provider(server) + + provider.ingest(corpus, _config()) + hits = provider.search("Austin", 5, _config()) + + assert [h.source_doc_id for h in hits] == ["doc-austin"] + assert hits[0].score == 0.9 + assert "Joanna" in (hits[0].text or "") + # Each doc was polled to terminal state. + assert all(doc["polls"] >= 3 for doc in server.docs.values()) + + def test_container_tag_scopes_run(self, env, tmp_path): + server = FakeServer() + corpus = tmp_path / "docs" + _write_doc(corpus, "doc-a", "content here") + provider = _provider(server) + + provider.ingest(corpus, _config("groupx")) + + added = server.docs["doc_doc-a"]["body"] + assert added["containerTags"] == ["bm-bench-groupx"] + + def test_failed_document_raises(self, env, tmp_path, monkeypatch): + monkeypatch.setattr("time.sleep", lambda s: None) + server = FakeServer(fail_doc_ids={"doc-bad"}) + corpus = tmp_path / "docs" + _write_doc(corpus, "doc-good", "fine content") + _write_doc(corpus, "doc-bad", "doomed content") + provider = _provider(server) + + with pytest.raises(RuntimeError, match="failed to ingest 1 documents"): + provider.ingest(corpus, _config()) + + def test_poll_404_treated_as_failure(self, env, tmp_path, monkeypatch): + monkeypatch.setattr("time.sleep", lambda s: None) + server = FakeServer() + corpus = tmp_path / "docs" + _write_doc(corpus, "doc-a", "content") + provider = _provider(server) + + original_handler = server.handler + + def handler(request: httpx.Request) -> httpx.Response: + if request.method == "GET" and request.url.path.startswith("/v3/documents/"): + return httpx.Response(404, json={"error": "reaped"}) + return original_handler(request) + + provider = SupermemoryLocalProvider(transport=httpx.MockTransport(handler)) + with pytest.raises(RuntimeError, match="failed to ingest"): + provider.ingest(corpus, _config()) + + def test_cleanup_bulk_deletes_container(self, env, tmp_path): + server = FakeServer() + corpus = tmp_path / "docs" + _write_doc(corpus, "doc-a", "content") + provider = _provider(server) + + provider.ingest(corpus, _config("wipeme")) + provider.cleanup(_config("wipeme")) + + assert server.deleted_containers == [["bm-bench-wipeme"]] + + def test_skipped_without_api_key(self, monkeypatch): + monkeypatch.delenv("SUPERMEMORY_API_KEY", raising=False) + provider = SupermemoryLocalProvider() + with pytest.raises(ProviderSkippedError, match="SUPERMEMORY_API_KEY"): + provider.search("anything", 5, _config()) + + def test_skipped_when_server_unreachable(self, env): + def refuse(request: httpx.Request) -> httpx.Response: + raise httpx.ConnectError("connection refused", request=request) + + provider = SupermemoryLocalProvider(transport=httpx.MockTransport(refuse)) + with pytest.raises(ProviderSkippedError, match="unreachable"): + provider.search("anything", 5, _config()) + + def test_ingest_timeout_raises(self, env, tmp_path, monkeypatch): + monkeypatch.setattr("time.sleep", lambda s: None) + # Never reaches done within the 5s budget because polls_to_done is huge. + server = FakeServer(polls_to_done=10_000_000) + monkeypatch.setenv("SUPERMEMORY_INGEST_TIMEOUT_S", "0.1") + corpus = tmp_path / "docs" + _write_doc(corpus, "doc-a", "content") + provider = _provider(server) + + with pytest.raises(TimeoutError, match="timed out"): + provider.ingest(corpus, _config()) + + def test_factory_registration(self, env): + from basic_memory_benchmarks.providers import create_provider + + assert isinstance(create_provider("supermemory-local"), SupermemoryLocalProvider) diff --git a/benchmarks/tests/test_diagnose.py b/benchmarks/tests/test_diagnose.py new file mode 100644 index 000000000..e2df503a6 --- /dev/null +++ b/benchmarks/tests/test_diagnose.py @@ -0,0 +1,196 @@ +"""Tests for answerer-vs-retrieval failure attribution.""" + +from __future__ import annotations + +import json + +from basic_memory_benchmarks.models import PerQueryRetrievalResult, QACaseResult +from basic_memory_benchmarks.scoring.diagnose import diagnose_provider, diagnose_run + + +def _qa(qid, correct, *, provider="bm-local", category="single_hop", abstained=False, error=None): + return QACaseResult( + provider=provider, + query_id=qid, + category=category, + question="q?", + expected_answer="gold", + generated_answer="ans", + abstained=abstained, + correct=correct, + judge_reason="reason", + answer_model="claude:haiku", + judge_model="claude:sonnet", + answer_latency_ms=1.0, + answer_input_tokens=1, + answer_output_tokens=1, + error=error, + ) + + +def _retr(qid, recall_at_10, *, provider="bm-local", ground_truth=("d1",)): + return PerQueryRetrievalResult( + provider=provider, + query_id=qid, + query_text="q?", + category="single_hop", + ground_truth=list(ground_truth), + hits=[], + recall_at_5=recall_at_10, + recall_at_10=recall_at_10, + precision_at_5=0.0, + mrr=0.0, + content_hit=False, + latency_ms=1.0, + ) + + +def test_attributes_failures_to_answerer_vs_retrieval(): + qa = [ + _qa("q1", True), # correct + _qa("q2", False), # wrong, gold retrieved -> answerer + _qa("q3", False), # wrong, gold NOT retrieved -> retrieval + ] + retr = [_retr("q1", 1.0), _retr("q2", 1.0), _retr("q3", 0.0)] + + d = diagnose_provider(qa, retr, provider="bm-local") + + assert d.answerable == 3 + assert d.correct == 1 + assert d.retrieved_but_unused == 1 + assert d.truly_missed == 1 + assert d.qa_accuracy == 1 / 3 + assert d.retrieval_ceiling == 2 / 3 # correct + retrieved-but-unused + assert d.answerer_gap == 1 / 3 + assert d.retrieval_gap == 1 / 3 + assert d.answerer_failure_share == 0.5 # 1 of 2 failures was the answerer + + +def test_unanswerable_items_excluded_from_attribution(): + qa = [_qa("q1", True), _qa("q2", False)] + retr = [_retr("q1", 1.0), _retr("q2", 0.0, ground_truth=())] # q2 has no gold + + d = diagnose_provider(qa, retr, provider="bm-local") + + assert d.answerable == 1 + assert d.unanswerable == 1 + assert d.correct == 1 + assert d.truly_missed == 0 # q2 not counted as a retrieval miss + + +def test_errored_and_unmatched_are_bucketed_not_attributed(): + qa = [ + _qa("q1", False, error="llm died"), # errored + _qa("q2", False), # no retrieval row -> unmatched + ] + retr = [_retr("q1", 0.0)] + + d = diagnose_provider(qa, retr, provider="bm-local") + + assert d.total_cases == 2 + assert d.errored == 1 + assert d.unmatched == 1 + assert d.answerable == 0 + assert d.qa_accuracy == 0.0 # no division by zero + + +def test_per_category_breakdown(): + qa = [ + _qa("q1", True, category="single_hop"), + _qa("q2", False, category="multi_hop"), # retrieved + _qa("q3", False, category="multi_hop"), # missed + ] + retr = [_retr("q1", 1.0), _retr("q2", 1.0), _retr("q3", 0.0)] + + d = diagnose_provider(qa, retr, provider="bm-local") + + assert d.by_category["single_hop"].correct == 1 + assert d.by_category["multi_hop"].answerable == 2 + assert d.by_category["multi_hop"].retrieved_but_unused == 1 + assert d.by_category["multi_hop"].truly_missed == 1 + + +def test_recall_field_is_honored(): + qa = [_qa("q1", False)] + # gold in top-10 but not top-5: answerer failure under r@10, retrieval miss under r@5 + retr = [ + PerQueryRetrievalResult( + provider="bm-local", + query_id="q1", + query_text="q?", + category="single_hop", + ground_truth=["d1"], + hits=[], + recall_at_5=0.0, + recall_at_10=1.0, + precision_at_5=0.0, + mrr=0.0, + content_hit=False, + latency_ms=1.0, + ) + ] + + assert diagnose_provider(qa, retr, provider="bm-local").truly_missed == 0 + assert ( + diagnose_provider(qa, retr, provider="bm-local", recall_field="recall_at_5").truly_missed + == 1 + ) + + +def test_diagnose_run_covers_all_providers_in_stable_order(): + qa = [_qa("q1", True, provider="bm-local"), _qa("q1", False, provider="mem0-local")] + retr = [_retr("q1", 1.0, provider="bm-local"), _retr("q1", 1.0, provider="mem0-local")] + + diags = diagnose_run(qa, retr) + + assert [d.provider for d in diags] == ["bm-local", "mem0-local"] + assert diags[0].correct == 1 + assert diags[1].retrieved_but_unused == 1 + + +def test_diagnose_stage_writes_json(tmp_path): + from basic_memory_benchmarks import runner as runner_module + + (tmp_path / "per-query-qa.jsonl").write_text( + "\n".join( + json.dumps(c.model_dump(mode="json")) + for c in [_qa("q1", True), _qa("q2", False), _qa("q3", False)] + ) + + "\n", + encoding="utf-8", + ) + (tmp_path / "per-query-retrieval.jsonl").write_text( + "\n".join( + json.dumps(r.model_dump(mode="json")) + for r in [_retr("q1", 1.0), _retr("q2", 1.0), _retr("q3", 0.0)] + ) + + "\n", + encoding="utf-8", + ) + + out = runner_module.run_diagnose_stage(run_dir=tmp_path, source="qa") + assert out.name == "qa-diagnosis.json" + payload = json.loads(out.read_text(encoding="utf-8")) + prov = payload["providers"][0] + assert prov["retrieved_but_unused"] == 1 + assert prov["truly_missed"] == 1 + + +def test_diagnose_stage_prefers_rejudge_when_auto(tmp_path): + from basic_memory_benchmarks import runner as runner_module + + (tmp_path / "per-query-retrieval.jsonl").write_text( + json.dumps(_retr("q1", 1.0).model_dump(mode="json")) + "\n", encoding="utf-8" + ) + # Original judged it wrong; rejudge flips it to correct. + (tmp_path / "per-query-qa.jsonl").write_text( + json.dumps(_qa("q1", False).model_dump(mode="json")) + "\n", encoding="utf-8" + ) + (tmp_path / "per-query-qa-rejudge.jsonl").write_text( + json.dumps(_qa("q1", True).model_dump(mode="json")) + "\n", encoding="utf-8" + ) + + out = runner_module.run_diagnose_stage(run_dir=tmp_path, source="auto") + payload = json.loads(out.read_text(encoding="utf-8")) + assert payload["source"] == "per-query-qa-rejudge.jsonl" + assert payload["providers"][0]["correct"] == 1 diff --git a/benchmarks/tests/test_fairness.py b/benchmarks/tests/test_fairness.py new file mode 100644 index 000000000..12418c8a2 --- /dev/null +++ b/benchmarks/tests/test_fairness.py @@ -0,0 +1,41 @@ +from basic_memory_benchmarks.fairness import validate_fairness +from basic_memory_benchmarks.models import PerQueryRetrievalResult + + +def _row(provider: str, query_id: str) -> PerQueryRetrievalResult: + return PerQueryRetrievalResult( + provider=provider, + query_id=query_id, + query_text="q", + category="single_hop", + category_id=1, + ground_truth=[], + expected_answer=None, + hits=[], + recall_at_5=0.0, + recall_at_10=0.0, + precision_at_5=0.0, + mrr=0.0, + content_hit=False, + latency_ms=1.0, + ) + + +def test_validate_fairness_ok() -> None: + warnings = validate_fairness( + { + "a": [_row("a", "q1")], + "b": [_row("b", "q1")], + } + ) + assert warnings == [] + + +def test_validate_fairness_mismatch() -> None: + warnings = validate_fairness( + { + "a": [_row("a", "q1")], + "b": [_row("b", "q2")], + } + ) + assert len(warnings) == 1 diff --git a/benchmarks/tests/test_manifest_schema.py b/benchmarks/tests/test_manifest_schema.py new file mode 100644 index 000000000..0c16441bd --- /dev/null +++ b/benchmarks/tests/test_manifest_schema.py @@ -0,0 +1,32 @@ +from basic_memory_benchmarks.models import DatasetProvenance, RunConfig, RunManifest, RuntimeInfo + + +def test_manifest_schema_roundtrip() -> None: + config = RunConfig( + run_id="run1", + dataset_id="synthetic", + dataset_path="benchmarks/synthetic/queries.json", + corpus_dir="benchmarks/synthetic/docs", + queries_path="benchmarks/synthetic/queries.json", + providers=["bm-local"], + ) + manifest = RunManifest( + run_id="run1", + created_at_utc="2026-01-01T00:00:00Z", + benchmark_git_sha="abc", + bm_source="github:main", + dataset=DatasetProvenance( + dataset_id="synthetic", + source_url="local", + checksum_sha256="123", + license_note="test", + fetched_at_utc="2026-01-01T00:00:00Z", + ), + runtime=RuntimeInfo( + os="test", python_version="3.12", started_at_utc="2026-01-01T00:00:00Z" + ), + config=config, + ) + payload = manifest.model_dump(mode="json") + assert payload["run_id"] == "run1" + assert payload["config"]["providers"] == ["bm-local"] diff --git a/benchmarks/tests/test_qa_scoring.py b/benchmarks/tests/test_qa_scoring.py new file mode 100644 index 000000000..3891ed899 --- /dev/null +++ b/benchmarks/tests/test_qa_scoring.py @@ -0,0 +1,480 @@ +"""Tests for the end-to-end QA scoring stage.""" + +from __future__ import annotations + +import json + +import pytest + +from basic_memory_benchmarks.llm.runners import LLMResult, LLMRunner, LLMRunnerError +from basic_memory_benchmarks.models import PerQueryRetrievalResult +from basic_memory_benchmarks.scoring.qa import ( + ABSTAIN_SENTINEL, + build_answer_prompt, + build_judge_prompt, + parse_judge_verdict, + run_qa, +) + + +class FakeRunner(LLMRunner): + """Returns canned responses keyed by substring match against the prompt.""" + + def __init__(self, responses: dict[str, str], default: str = ""): + self.spec = "fake:test" + self.responses = responses + self.default = default + self.prompts: list[str] = [] + + def complete(self, prompt: str) -> LLMResult: + self.prompts.append(prompt) + for needle, response in self.responses.items(): + if needle in prompt: + return LLMResult( + text=response, model="fake", input_tokens=10, output_tokens=5, latency_ms=1.0 + ) + if self.default: + return LLMResult( + text=self.default, model="fake", input_tokens=10, output_tokens=5, latency_ms=1.0 + ) + raise LLMRunnerError(f"No canned response for prompt: {prompt[:80]}") + + +def _row( + query_id: str, + question: str, + expected: str | None, + context: str, + category: str = "single_hop", +) -> PerQueryRetrievalResult: + return PerQueryRetrievalResult( + provider="bm-local", + query_id=query_id, + query_text=question, + category=category, + ground_truth=[], + expected_answer=expected, + recall_at_5=0.0, + recall_at_10=0.0, + precision_at_5=0.0, + mrr=0.0, + content_hit=False, + latency_ms=1.0, + retrieved_context=context, + ) + + +class TestPrompts: + def test_answer_prompt_includes_question_and_context(self): + prompt = build_answer_prompt("Where does Joanna live?", "Joanna lives in Austin.") + assert "Where does Joanna live?" in prompt + assert "Joanna lives in Austin." in prompt + assert ABSTAIN_SENTINEL in prompt + + def test_answer_prompt_marks_empty_context(self): + prompt = build_answer_prompt("Where does Joanna live?", " ") + assert "(no memories were retrieved)" in prompt + + def test_judge_prompt_includes_all_parts(self): + prompt = build_judge_prompt("Q?", "gold fact", "candidate fact") + assert "Q?" in prompt + assert "gold fact" in prompt + assert "candidate fact" in prompt + + def test_judge_prompt_allows_incomplete_gold(self): + # The rubric must tell the judge gold answers may be incomplete and that + # extra non-contradicting facts are not errors (LoCoMo gold answers are + # documented-incomplete; the prior rubric over-failed correct answers). + prompt = build_judge_prompt("Q?", "gold", "candidate") + assert "INCOMPLETE" in prompt + assert "not errors" in prompt.lower() + + def test_judge_prompt_handles_unanswerable_before_fact_matching(self): + # Abstention/unanswerable cases (ConvoMem abstention, LoCoMo adversarial) + # must be judged FIRST on whether the candidate declines — otherwise the + # fact-matching rules wrongly fail a correct "I don't know" against a + # "no information" gold (regression caught during number regeneration). + prompt = build_judge_prompt("Q?", "gold", "candidate") + first = prompt.split("OTHERWISE")[0] + assert "not available" in first.lower() + assert ABSTAIN_SENTINEL in first + assert "declines" in first.lower() + + +class TestParseJudgeVerdict: + def test_plain_json(self): + correct, reason = parse_judge_verdict('{"correct": true, "reason": "matches"}') + assert correct is True + assert reason == "matches" + + def test_json_in_code_fence(self): + raw = 'Here is my verdict:\n```json\n{"correct": false, "reason": "missing fact"}\n```' + correct, reason = parse_judge_verdict(raw) + assert correct is False + assert reason == "missing fact" + + def test_no_json_raises(self): + with pytest.raises(ValueError): + parse_judge_verdict("The answer is correct.") + + def test_non_boolean_correct_raises(self): + with pytest.raises(ValueError): + parse_judge_verdict('{"correct": "yes"}') + + +class TestRunQA: + def test_correct_and_incorrect_cases(self): + rows = [ + _row("q1", "Where does Joanna live?", "Austin", "Joanna lives in Austin."), + _row("q2", "What is Anthony's job?", "Engineer", "Anthony enjoys hiking."), + ] + answerer = FakeRunner( + { + "Where does Joanna live?": "Austin", + "What is Anthony's job?": ABSTAIN_SENTINEL, + } + ) + judge = FakeRunner( + { + "Candidate answer: Austin": '{"correct": true, "reason": "match"}', + f"Candidate answer: {ABSTAIN_SENTINEL}": '{"correct": false, "reason": "abstained on answerable"}', + } + ) + + cases, summary = run_qa( + rows, provider="bm-local", answerer=answerer, judge=judge, max_workers=1 + ) + + assert summary.total_cases == 2 + assert summary.correct_count == 1 + assert summary.accuracy == 0.5 + assert summary.abstain_count == 1 + by_id = {case.query_id: case for case in cases} + assert by_id["q1"].correct is True + assert by_id["q2"].correct is False + assert by_id["q2"].abstained is True + + def test_category_breakdown(self): + rows = [ + _row("q1", "Q1?", "A1", "ctx", category="single_hop"), + _row("q2", "Q2?", "A2", "ctx", category="temporal"), + ] + answerer = FakeRunner({}, default="some answer") + judge = FakeRunner( + { + "Q1?": '{"correct": true, "reason": "ok"}', + "Q2?": '{"correct": false, "reason": "wrong"}', + } + ) + _, summary = run_qa( + rows, provider="bm-local", answerer=answerer, judge=judge, max_workers=1 + ) + + assert summary.by_category["single_hop"].accuracy == 1.0 + assert summary.by_category["temporal"].accuracy == 0.0 + + def test_rows_without_expected_answer_are_skipped(self): + rows = [_row("q1", "Q1?", None, "ctx")] + answerer = FakeRunner({}) + judge = FakeRunner({}) + cases, summary = run_qa(rows, provider="bm-local", answerer=answerer, judge=judge) + assert cases == [] + assert summary.skipped_reason is not None + + def test_runner_error_recorded_not_raised(self): + rows = [ + _row("q1", "Q1?", "A1", "ctx"), + _row("q2", "Q2?", "A2", "ctx"), + ] + answerer = FakeRunner({"Q2?": "answer two"}) # Q1 has no canned response -> error + judge = FakeRunner({}, default='{"correct": true, "reason": "ok"}') + + cases, summary = run_qa( + rows, provider="bm-local", answerer=answerer, judge=judge, max_workers=1 + ) + + by_id = {case.query_id: case for case in cases} + assert by_id["q1"].error is not None + assert by_id["q1"].correct is False + assert by_id["q2"].correct is True + assert summary.error_count == 1 + assert summary.total_cases == 2 + + def test_token_and_latency_accounting(self): + rows = [_row("q1", "Q1?", "A1", "ctx")] + answerer = FakeRunner({}, default="answer") + judge = FakeRunner({}, default='{"correct": true, "reason": "ok"}') + _, summary = run_qa(rows, provider="bm-local", answerer=answerer, judge=judge) + assert summary.total_answer_input_tokens == 10 + assert summary.total_answer_output_tokens == 5 + assert summary.mean_answer_latency_ms == pytest.approx(1.0) + + +class TestQAStageArtifacts: + def test_run_qa_stage_writes_artifacts(self, tmp_path, monkeypatch): + from basic_memory_benchmarks import runner as runner_module + + rows = [ + _row("q1", "Where does Joanna live?", "Austin", "Joanna lives in Austin."), + ] + retrieval_path = tmp_path / "per-query-retrieval.jsonl" + with retrieval_path.open("w", encoding="utf-8") as file: + for row in rows: + file.write(json.dumps(row.model_dump(mode="json")) + "\n") + + fake = FakeRunner({}, default='{"correct": true, "reason": "ok"}') + monkeypatch.setattr("basic_memory_benchmarks.llm.runners.create_runner", lambda spec: fake) + + runner_module.run_qa_stage( + run_dir=tmp_path, + answerer_spec="fake:test", + judge_spec="fake:test", + max_workers=1, + ) + + assert (tmp_path / "per-query-qa.jsonl").exists() + summary = json.loads((tmp_path / "qa-summary.json").read_text()) + assert summary["providers"][0]["provider"] == "bm-local" + assert summary["providers"][0]["total_cases"] == 1 + + +class TestQuestionDate: + def test_question_date_reaches_answerer_and_judge(self): + row = _row("q1", "How many weeks ago did I visit the dentist?", "Three weeks ago", "ctx") + row = row.model_copy(update={"metadata": {"question_date": "2023/05/30 (Tue) 23:40"}}) + answerer = FakeRunner({}, default="Three weeks ago") + judge = FakeRunner({}, default='{"correct": true, "reason": "ok"}') + + run_qa([row], provider="bm-local", answerer=answerer, judge=judge, max_workers=1) + + assert "question asked on 2023/05/30 (Tue) 23:40" in answerer.prompts[0] + assert "question asked on 2023/05/30 (Tue) 23:40" in judge.prompts[0] + + def test_no_date_means_plain_question(self): + row = _row("q1", "Where does Joanna live?", "Austin", "ctx") + answerer = FakeRunner({}, default="Austin") + judge = FakeRunner({}, default='{"correct": true, "reason": "ok"}') + + run_qa([row], provider="bm-local", answerer=answerer, judge=judge, max_workers=1) + + assert "question asked on" not in answerer.prompts[0] + + +class TestAssembleContext: + def _hit(self, doc_id: str, text: str): + from basic_memory_benchmarks.models import SearchHit + + return SearchHit(source_doc_id=doc_id, text=text, score=1.0) + + def test_sections_numbered_with_source(self): + from basic_memory_benchmarks.scoring.qa import assemble_context + + ctx = assemble_context( + [self._hit("doc-a", "alpha facts"), self._hit("doc-b", "beta facts")] + ) + assert "[Memory 1 | source: doc-a]" in ctx + assert "alpha facts" in ctx + assert "[Memory 2 | source: doc-b]" in ctx + + def test_budget_caps_total(self): + from basic_memory_benchmarks.scoring.qa import CONTEXT_MAX_CHARS, assemble_context + + hits = [self._hit(f"doc-{i}", "x" * 3000) for i in range(10)] + ctx = assemble_context(hits) + # Section text alone stays within the global budget (headers excluded). + text_chars = sum(len(part.split("]\n", 1)[1]) for part in ctx.split("\n\n")) + assert text_chars <= CONTEXT_MAX_CHARS + + def test_per_hit_cap_with_many_hits(self): + from basic_memory_benchmarks.scoring.qa import CONTEXT_CHARS_PER_HIT, assemble_context + + # With a full hit list the per-hit cap is the slice constant; a lone + # hit instead gets the whole budget (see TestContextBudgetOverride). + hits = [self._hit(f"doc-{i}", "z" * (CONTEXT_CHARS_PER_HIT + 500)) for i in range(10)] + ctx = assemble_context(hits) + first_section = ctx.split("\n\n")[0] + assert first_section.count("z") == CONTEXT_CHARS_PER_HIT + + def test_empty_hits_skipped(self): + from basic_memory_benchmarks.scoring.qa import assemble_context + + ctx = assemble_context([self._hit("doc-a", ""), self._hit("doc-b", "real")]) + assert "[Memory 2 | source: doc-b]" in ctx + assert "doc-a" not in ctx + + def test_qa_uses_assembled_context(self): + from basic_memory_benchmarks.models import SearchHit + + row = _row("q1", "Where does Joanna live?", "Austin", "legacy joined context") + row = row.model_copy( + update={ + "hits": [ + SearchHit(source_doc_id="doc-a", text="Joanna lives in Austin.", score=1.0) + ] + } + ) + answerer = FakeRunner({}, default="Austin") + judge = FakeRunner({}, default='{"correct": true, "reason": "ok"}') + run_qa([row], provider="bm-local", answerer=answerer, judge=judge, max_workers=1) + assert "[Memory 1 | source: doc-a]" in answerer.prompts[0] + assert "legacy joined context" not in answerer.prompts[0] + + def test_fallback_to_legacy_context_without_hits(self): + row = _row("q1", "Where does Joanna live?", "Austin", "legacy joined context") + answerer = FakeRunner({}, default="Austin") + judge = FakeRunner({}, default='{"correct": true, "reason": "ok"}') + run_qa([row], provider="bm-local", answerer=answerer, judge=judge, max_workers=1) + assert "legacy joined context" in answerer.prompts[0] + + +class TestPromptCharsAccounting: + def test_prompt_chars_recorded(self): + rows = [_row("q1", "Q1?", "A1", "some retrieved context here")] + answerer = FakeRunner({}, default="answer") + judge = FakeRunner({}, default='{"correct": true, "reason": "ok"}') + cases, summary = run_qa(rows, provider="bm-local", answerer=answerer, judge=judge) + assert cases[0].answer_prompt_chars == len(answerer.prompts[0]) + assert summary.mean_answer_prompt_chars == cases[0].answer_prompt_chars + + +class TestContextBudgetOverride: + def _hit(self, doc_id: str, text: str): + from basic_memory_benchmarks.models import SearchHit + + return SearchHit(source_doc_id=doc_id, text=text, score=1.0) + + def test_single_hit_uses_full_budget(self): + from basic_memory_benchmarks.scoring.qa import assemble_context + + # One massive hit (full-context baseline) gets the whole budget, + # not the per-hit slice. + ctx = assemble_context([self._hit("all", "z" * 50_000)], max_chars=40_000) + assert ctx.count("z") == 40_000 + + def test_budget_override_flows_through_run_qa(self): + from basic_memory_benchmarks.models import SearchHit + + row = _row("q1", "Q?", "A", "legacy") + row = row.model_copy( + update={"hits": [SearchHit(source_doc_id="all", text="z" * 50_000, score=1.0)]} + ) + answerer = FakeRunner({}, default="answer") + judge = FakeRunner({}, default='{"correct": true, "reason": "ok"}') + run_qa( + [row], + provider="p", + answerer=answerer, + judge=judge, + max_workers=1, + max_context_chars=30_000, + ) + assert answerer.prompts[0].count("z") == 30_000 + + +class TestHitTitleInContext: + def test_title_metadata_lands_in_header(self): + from basic_memory_benchmarks.models import SearchHit + from basic_memory_benchmarks.scoring.qa import assemble_context + + hit = SearchHit( + source_doc_id="doc-a", + text="- **Melanie:** I hiked yesterday!", + score=1.0, + metadata={"title": "locomo-c00-s18 (3:01 pm on 20 October, 2023)"}, + ) + ctx = assemble_context([hit]) + assert "| locomo-c00-s18 (3:01 pm on 20 October, 2023)]" in ctx + + def test_title_skipped_when_already_in_text(self): + from basic_memory_benchmarks.models import SearchHit + from basic_memory_benchmarks.scoring.qa import assemble_context + + hit = SearchHit( + source_doc_id="doc-a", + text="# Chat session at 8 May 2023\nfull body", + score=1.0, + metadata={"title": "Chat session at 8 May 2023"}, + ) + ctx = assemble_context([hit]) + assert ctx.count("Chat session at 8 May 2023") == 1 + + +class TestRejudge: + def _case(self, qid, correct, generated="ans", error=None, category="single_hop"): + from basic_memory_benchmarks.models import QACaseResult + + return QACaseResult( + provider="bm-local", + query_id=qid, + category=category, + question="Q?", + expected_answer="gold", + generated_answer=generated, + abstained=False, + correct=correct, + judge_reason="orig", + answer_model="claude:haiku", + judge_model="claude:sonnet", + answer_latency_ms=1.0, + answer_input_tokens=1, + answer_output_tokens=1, + error=error, + ) + + def test_rejudge_flips_and_summary(self): + from basic_memory_benchmarks.scoring.qa import rejudge_cases + + cases = [self._case("q1", correct=False), self._case("q2", correct=True)] + # New judge flips q1 to correct, keeps q2 correct. + judge = FakeRunner( + {}, + default='{"correct": true, "reason": "incomplete gold is fine"}', + ) + rejudged, summary, flips = rejudge_cases(cases, judge=judge, max_workers=1) + + assert summary.correct_count == 2 + assert summary.accuracy == 1.0 + assert len(flips) == 1 + assert flips[0]["query_id"] == "q1" + assert flips[0]["was_correct"] is False and flips[0]["now_correct"] is True + # answerer fields preserved; judge_model updated. + assert rejudged[0].judge_model == "fake:test" + assert rejudged[0].answer_model == "claude:haiku" + + def test_rejudge_preserves_errored_cases(self): + from basic_memory_benchmarks.scoring.qa import rejudge_cases + + cases = [self._case("q1", correct=False, error="llm died")] + judge = FakeRunner({}, default='{"correct": true, "reason": "x"}') + rejudged, summary, flips = rejudge_cases(cases, judge=judge, max_workers=1) + # Errored case is untouched (no generated answer to judge). + assert rejudged[0].correct is False + assert rejudged[0].error == "llm died" + assert flips == [] + + def test_rejudge_empty(self): + from basic_memory_benchmarks.scoring.qa import rejudge_cases + + judge = FakeRunner({}, default='{"correct": true, "reason": "x"}') + rejudged, summary, flips = rejudge_cases([], judge=judge) + assert rejudged == [] and flips == [] + assert summary.skipped_reason is not None + + def test_rejudge_stage_artifacts(self, tmp_path, monkeypatch): + import json as _json + + from basic_memory_benchmarks import runner as runner_module + + case = self._case("q1", correct=False) + (tmp_path / "per-query-qa.jsonl").write_text( + _json.dumps(case.model_dump(mode="json")) + "\n", encoding="utf-8" + ) + fake = FakeRunner({}, default='{"correct": true, "reason": "flipped"}') + monkeypatch.setattr("basic_memory_benchmarks.llm.runners.create_runner", lambda spec: fake) + runner_module.run_rejudge_stage(run_dir=tmp_path, judge_spec="fake:test", max_workers=1) + + assert (tmp_path / "per-query-qa-rejudge.jsonl").exists() + summary = _json.loads((tmp_path / "qa-rejudge-summary.json").read_text()) + assert summary["providers"][0]["correct_count"] == 1 + flips = _json.loads((tmp_path / "qa-rejudge-flips.json").read_text()) + assert flips["flip_count"] == 1 diff --git a/benchmarks/tests/test_retrieval_metrics.py b/benchmarks/tests/test_retrieval_metrics.py new file mode 100644 index 000000000..c3afe88c7 --- /dev/null +++ b/benchmarks/tests/test_retrieval_metrics.py @@ -0,0 +1,42 @@ +from basic_memory_benchmarks.models import QueryCase, SearchHit +from basic_memory_benchmarks.scoring.retrieval import ( + evaluate_query, + precision_at_k, + recall_at_k, + reciprocal_rank, + summarize_provider, +) + + +def test_metric_primitives() -> None: + hits = [ + SearchHit(source_doc_id="doc-a", text="A", score=0.9), + SearchHit(source_doc_id="doc-b", text="B", score=0.8), + ] + truth = {"doc-a"} + assert recall_at_k(hits, truth, 5) == 1.0 + assert precision_at_k(hits, truth, 5) == 0.5 + assert reciprocal_rank(hits, truth) == 1.0 + + +def test_evaluate_and_summary() -> None: + query = QueryCase( + id="q1", + query="question", + category="single_hop", + category_id=1, + ground_truth=["doc-a"], + expected_answer="token", + ) + hits = [ + SearchHit(source_doc_id="doc-a", text="token refresh", score=0.9), + SearchHit(source_doc_id="doc-x", text="other", score=0.2), + ] + + row = evaluate_query(provider="bm-local", query=query, hits=hits, latency_ms=10.0) + assert row.recall_at_5 == 1.0 + assert row.content_hit is True + + summary = summarize_provider("bm-local", [row]) + assert summary.metrics.recall_at_5 == 1.0 + assert summary.official_headline.query_count == 1 diff --git a/benchmarks/tests/test_review.py b/benchmarks/tests/test_review.py new file mode 100644 index 000000000..3e11cd6b1 --- /dev/null +++ b/benchmarks/tests/test_review.py @@ -0,0 +1,88 @@ +"""Tests for the judge-review HTML report.""" + +from __future__ import annotations + +import json + +from basic_memory_benchmarks.models import QACaseResult +from basic_memory_benchmarks.scoring.review import build_review_html + + +def _case(qid, correct, *, abstained=False, error=None, generated="ans", category="single_hop"): + return QACaseResult( + provider="bm-local", + query_id=qid, + category=category, + question="What did Joanna say?", # markup to test escaping + expected_answer="gold & more", + generated_answer=generated, + abstained=abstained, + correct=correct, + judge_reason="reason with ", + answer_model="claude:haiku", + judge_model="claude:sonnet", + answer_latency_ms=1.0, + answer_input_tokens=1, + answer_output_tokens=1, + error=error, + ) + + +def test_report_renders_and_embeds_cases(): + html_out = build_review_html([_case("q1", True), _case("q2", False)], run_id="myrun") + assert "" in html_out.lower() + assert "Judge review — myrun" in html_out + assert "claude:sonnet" in html_out # judge model surfaced + # Data embedded as a JSON blob the page parses. + blob = html_out.split('id="data" type="application/json">', 1)[1].split("", 1)[0] + cases = json.loads(blob) + assert {c["query_id"] for c in cases} == {"q1", "q2"} + + +def test_report_escapes_user_text(): + html_out = build_review_html([_case("q1", True)], run_id="r") + # Raw markup from question/gold/reason must be escaped, not live HTML. + assert "Joanna" not in html_out + assert "<b>Joanna</b>" in html_out + assert "gold & more" in html_out + assert "<tag>" in html_out + + +def test_report_handles_empty_and_errored_cases(): + html_out = build_review_html([_case("q1", False, error="llm died", generated="")], run_id="r") + blob = html_out.split('id="data" type="application/json">', 1)[1].split("", 1)[0] + case = json.loads(blob)[0] + assert case["error"] == "llm died" + assert case["generated_answer"] == "" + + +def test_report_empty_run(): + html_out = build_review_html([], run_id="empty") + assert "0 decisions" in html_out + assert "n/a" in html_out # no judge model + + +def test_review_stage_writes_html(tmp_path): + from basic_memory_benchmarks import runner as runner_module + + case = _case("q1", True) + (tmp_path / "per-query-qa.jsonl").write_text( + json.dumps(case.model_dump(mode="json")) + "\n", encoding="utf-8" + ) + out = runner_module.run_review_stage(run_dir=tmp_path, source="qa") + assert out.name == "review.html" + assert "Judge review" in out.read_text(encoding="utf-8") + + +def test_review_stage_prefers_rejudge_when_auto(tmp_path): + from basic_memory_benchmarks import runner as runner_module + + (tmp_path / "per-query-qa.jsonl").write_text( + json.dumps(_case("orig", True).model_dump(mode="json")) + "\n", encoding="utf-8" + ) + (tmp_path / "per-query-qa-rejudge.jsonl").write_text( + json.dumps(_case("rejudged", True).model_dump(mode="json")) + "\n", encoding="utf-8" + ) + out = runner_module.run_review_stage(run_dir=tmp_path, source="auto") + text = out.read_text(encoding="utf-8") + assert "rejudged" in text and "orig" not in text diff --git a/benchmarks/tests/test_runner_grouped.py b/benchmarks/tests/test_runner_grouped.py new file mode 100644 index 000000000..593cdee3c --- /dev/null +++ b/benchmarks/tests/test_runner_grouped.py @@ -0,0 +1,323 @@ +"""Tests for grouped (per-question corpus) retrieval execution.""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from basic_memory_benchmarks.exceptions import ProviderSkippedError +from basic_memory_benchmarks.models import DatasetProvenance, RunConfig, SearchHit +from basic_memory_benchmarks.providers.base import BenchmarkProvider +from basic_memory_benchmarks.runner import run_retrieval + + +class RecordingProvider(BenchmarkProvider): + """Records lifecycle calls; class-level log shared across instances.""" + + name = "recording" + calls: list[tuple[str, str, str]] = [] # (event, corpus_or_query, run_id) + fail_groups: set[str] = set() + skip_all = False + instances = 0 + + def __init__(self) -> None: + type(self).instances += 1 + + def ingest(self, corpus_path: Path, run_config: RunConfig) -> None: + if type(self).skip_all: + raise ProviderSkippedError("creds missing") + for group_id in type(self).fail_groups: + if f"-{group_id}" in run_config.run_id: + raise RuntimeError(f"boom in {group_id}") + type(self).calls.append(("ingest", str(corpus_path), run_config.run_id)) + + def search(self, query: str, limit: int, run_config: RunConfig) -> list[SearchHit]: + type(self).calls.append(("search", query, run_config.run_id)) + return [SearchHit(source_doc_id="doc-1", text="ctx", score=1.0)] + + def cleanup(self, run_config: RunConfig) -> None: + type(self).calls.append(("cleanup", "", run_config.run_id)) + + def version_info(self) -> dict[str, str]: + return {"recording": "1.0"} + + +@pytest.fixture(autouse=True) +def _reset_recording_provider(): + RecordingProvider.calls = [] + RecordingProvider.fail_groups = set() + RecordingProvider.skip_all = False + RecordingProvider.instances = 0 + + +def _setup_grouped_corpus(tmp_path: Path, groups: list[str]) -> tuple[Path, Path]: + corpus_root = tmp_path / "groups" + queries = [] + for group_id in groups: + docs = corpus_root / group_id / "docs" + docs.mkdir(parents=True) + (docs / f"{group_id}-s000.md").write_text(f"# {group_id}\n", encoding="utf-8") + queries.append( + { + "id": group_id, + "query": f"question for {group_id}", + "category": "single-session-user", + "group": group_id, + "ground_truth": ["doc-1"], + "expected_answer": "answer", + "metadata": {"question_date": "2023/05/30"}, + } + ) + queries_path = tmp_path / "queries.json" + queries_path.write_text(json.dumps(queries), encoding="utf-8") + return corpus_root, queries_path + + +def _run_config(tmp_path: Path, corpus_root: Path, queries_path: Path) -> RunConfig: + return RunConfig( + run_id="testrun", + dataset_id="longmemeval_s", + dataset_path=str(queries_path), + corpus_dir=str(corpus_root), + queries_path=str(queries_path), + output_root=str(tmp_path / "runs"), + providers=["recording"], + ) + + +def _provenance() -> DatasetProvenance: + return DatasetProvenance( + dataset_id="longmemeval_s", + source_url="test", + checksum_sha256="0" * 64, + license_note="test", + fetched_at_utc="now", + ) + + +class TestGroupedExecution: + def test_each_group_isolated(self, tmp_path): + corpus_root, queries_path = _setup_grouped_corpus(tmp_path, ["qa", "qb"]) + config = _run_config(tmp_path, corpus_root, queries_path) + + run_dir = run_retrieval( + run_config=config, + dataset=_provenance(), + provider_factory=lambda name: RecordingProvider(), + ) + + ingests = [c for c in RecordingProvider.calls if c[0] == "ingest"] + assert len(ingests) == 2 + # Group-suffixed run ids isolate provider namespaces. + assert {run_id for _, _, run_id in ingests} == {"testrun-qa", "testrun-qb"} + # Each ingest points at its own group corpus. + assert {Path(corpus).parent.name for _, corpus, _ in ingests} == {"qa", "qb"} + # Fresh provider instance per group (plus one flat instance is never made). + assert RecordingProvider.instances == 2 + + rows = [ + json.loads(line) + for line in (run_dir / "per-query-retrieval.jsonl").read_text().splitlines() + ] + assert len(rows) == 2 + assert {row["query_id"] for row in rows} == {"qa", "qb"} + # Metadata flows into retrieval rows for the QA stage. + assert all(row["metadata"]["question_date"] == "2023/05/30" for row in rows) + + status = json.loads((run_dir / "provider-status.json").read_text()) + provider_meta = status[0]["metadata"] + assert provider_meta["grouped_mode"] == "true" + assert provider_meta["group_count"] == "2" + + def test_failed_group_recorded_and_run_continues(self, tmp_path): + corpus_root, queries_path = _setup_grouped_corpus(tmp_path, ["qa", "qb", "qc"]) + RecordingProvider.fail_groups = {"qb"} + config = _run_config(tmp_path, corpus_root, queries_path) + + run_dir = run_retrieval( + run_config=config, + dataset=_provenance(), + provider_factory=lambda name: RecordingProvider(), + ) + + rows = [ + json.loads(line) + for line in (run_dir / "per-query-retrieval.jsonl").read_text().splitlines() + ] + assert {row["query_id"] for row in rows} == {"qa", "qc"} + + status = json.loads((run_dir / "provider-status.json").read_text()) + provider_meta = status[0]["metadata"] + assert provider_meta["failed_group_count"] == "1" + assert provider_meta["failed_groups"] == "qb" + + def test_skip_on_first_group_skips_provider(self, tmp_path): + corpus_root, queries_path = _setup_grouped_corpus(tmp_path, ["qa", "qb"]) + RecordingProvider.skip_all = True + config = _run_config(tmp_path, corpus_root, queries_path) + + run_dir = run_retrieval( + run_config=config, + dataset=_provenance(), + provider_factory=lambda name: RecordingProvider(), + ) + + status = json.loads((run_dir / "provider-status.json").read_text()) + assert status[0]["state"] == "skipped" + # Only the first group was attempted. + assert RecordingProvider.instances == 1 + + def test_missing_group_corpus_raises(self, tmp_path): + corpus_root, queries_path = _setup_grouped_corpus(tmp_path, ["qa"]) + queries = json.loads(queries_path.read_text()) + queries.append({**queries[0], "id": "missing", "group": "missing"}) + queries_path.write_text(json.dumps(queries), encoding="utf-8") + config = _run_config(tmp_path, corpus_root, queries_path) + config = config.model_copy(update={"allow_provider_skip": False}) + + with pytest.raises(FileNotFoundError, match="Missing group corpus"): + run_retrieval( + run_config=config, + dataset=_provenance(), + provider_factory=lambda name: RecordingProvider(), + ) + + def test_mixed_grouped_and_ungrouped_rejected(self, tmp_path): + corpus_root, queries_path = _setup_grouped_corpus(tmp_path, ["qa"]) + queries = json.loads(queries_path.read_text()) + queries.append( + { + "id": "flat", + "query": "ungrouped question", + "category": "single_hop", + "ground_truth": [], + } + ) + queries_path.write_text(json.dumps(queries), encoding="utf-8") + config = _run_config(tmp_path, corpus_root, queries_path) + config = config.model_copy(update={"allow_provider_skip": False}) + + with pytest.raises(ValueError, match="mixed grouped/ungrouped"): + run_retrieval( + run_config=config, + dataset=_provenance(), + provider_factory=lambda name: RecordingProvider(), + ) + + +class ReusingProvider(RecordingProvider): + """RecordingProvider variant that opts into group reuse.""" + + name = "reusing" + supports_group_reuse = True + calls: list[tuple[str, str, str]] = [] + fail_groups: set[str] = set() + skip_all = False + instances = 0 + + +@pytest.fixture(autouse=True) +def _reset_reusing_provider(): + ReusingProvider.calls = [] + ReusingProvider.fail_groups = set() + ReusingProvider.skip_all = False + ReusingProvider.instances = 0 + + +class TestGroupReuse: + def test_single_instance_serves_all_groups(self, tmp_path): + corpus_root, queries_path = _setup_grouped_corpus(tmp_path, ["qa", "qb", "qc"]) + config = _run_config(tmp_path, corpus_root, queries_path) + config = config.model_copy(update={"providers": ["reusing"]}) + + run_dir = run_retrieval( + run_config=config, + dataset=_provenance(), + provider_factory=lambda name: ReusingProvider(), + ) + + assert ReusingProvider.instances == 1 + ingests = [c for c in ReusingProvider.calls if c[0] == "ingest"] + assert len(ingests) == 3 + # Per-group run ids still namespace projects within the one instance. + assert {run_id for _, _, run_id in ingests} == { + "testrun-qa", + "testrun-qb", + "testrun-qc", + } + # Cleanup exactly once, with the BASE run id, after all groups. + cleanups = [c for c in ReusingProvider.calls if c[0] == "cleanup"] + assert [run_id for _, _, run_id in cleanups] == ["testrun"] + assert ReusingProvider.calls[-1][0] == "cleanup" + + rows = [ + json.loads(line) + for line in (run_dir / "per-query-retrieval.jsonl").read_text().splitlines() + ] + assert {row["query_id"] for row in rows} == {"qa", "qb", "qc"} + + def test_failed_group_does_not_stop_reuse(self, tmp_path): + corpus_root, queries_path = _setup_grouped_corpus(tmp_path, ["qa", "qb", "qc"]) + ReusingProvider.fail_groups = {"qb"} + config = _run_config(tmp_path, corpus_root, queries_path) + config = config.model_copy(update={"providers": ["reusing"]}) + + run_dir = run_retrieval( + run_config=config, + dataset=_provenance(), + provider_factory=lambda name: ReusingProvider(), + ) + + assert ReusingProvider.instances == 1 + rows = [ + json.loads(line) + for line in (run_dir / "per-query-retrieval.jsonl").read_text().splitlines() + ] + assert {row["query_id"] for row in rows} == {"qa", "qc"} + # Cleanup still ran exactly once at the end. + cleanups = [c for c in ReusingProvider.calls if c[0] == "cleanup"] + assert len(cleanups) == 1 + + +class TestGroupErrorCapture: + def test_first_errors_recorded_in_metadata(self, tmp_path): + corpus_root, queries_path = _setup_grouped_corpus(tmp_path, ["qa", "qb", "qc"]) + RecordingProvider.fail_groups = {"qb", "qc"} + config = _run_config(tmp_path, corpus_root, queries_path) + + run_dir = run_retrieval( + run_config=config, + dataset=_provenance(), + provider_factory=lambda name: RecordingProvider(), + ) + + status = json.loads((run_dir / "provider-status.json").read_text()) + meta = status[0]["metadata"] + assert meta["failed_group_count"] == "2" + assert "qb: RuntimeError: boom in qb" in meta["failed_group_error_0"] + assert "qc: RuntimeError: boom in qc" in meta["failed_group_error_1"] + + +class TestAllGroupsFailedDetail: + def test_all_failed_error_includes_group_causes(self, tmp_path): + """When every group fails, the raised error must name the causes. + + An opaque 'all N groups failed' with no reason is undiagnosable after + a long run (hit during the supermemory integration). + """ + corpus_root, queries_path = _setup_grouped_corpus(tmp_path, ["qa", "qb"]) + RecordingProvider.fail_groups = {"qa", "qb"} + config = _run_config(tmp_path, corpus_root, queries_path) + config = config.model_copy(update={"allow_provider_skip": False}) + + with pytest.raises(RuntimeError, match="All 2 groups failed") as excinfo: + run_retrieval( + run_config=config, + dataset=_provenance(), + provider_factory=lambda name: RecordingProvider(), + ) + message = str(excinfo.value) + assert "qa: RuntimeError: boom in qa" in message + assert "qb: RuntimeError: boom in qb" in message diff --git a/benchmarks/tests/test_structure_corpus.py b/benchmarks/tests/test_structure_corpus.py new file mode 100644 index 000000000..de1943f4f --- /dev/null +++ b/benchmarks/tests/test_structure_corpus.py @@ -0,0 +1,142 @@ +"""Tests for the flat->structured corpus converter (no live LLM calls).""" + +from __future__ import annotations + +import pytest + +from basic_memory_benchmarks.converters.structure_corpus import ( + _clean_extraction, + _split_doc, + group_prefix_filter, + structure_corpus, + structure_doc, +) +from basic_memory_benchmarks.llm.runners import LLMResult, LLMRunner + +FLAT_DOC = """\ +--- +title: grp-c000 +type: note +source_doc_id: grp-c000 +dataset_id: convomem +permalink: bm-bench/grp-c000 +--- + +# grp-c000 + +## Conversation +- **User:** We had a phishing attack that impersonated the CFO. +- **Assistant:** Email is a major vector for spoofing; use a secure portal. +""" + +STRUCTURED_OUT = """\ +## Observations +- [event] A phishing attack impersonated the CFO #security +- [risk] Email is a major vector for spoofing attacks + +## Relations +- caused_by [[Phishing Incident]] +- mitigated_by [[Secure Internal Portal]] +""" + + +class _FakeRunner(LLMRunner): + """Returns canned structured text; records the prompts it was given.""" + + spec = "fake:extractor" + + def __init__(self, output: str = STRUCTURED_OUT) -> None: + self.output = output + self.prompts: list[str] = [] + + def complete(self, prompt: str) -> LLMResult: + self.prompts.append(prompt) + return LLMResult( + text=self.output, model="fake", input_tokens=1, output_tokens=1, latency_ms=1.0 + ) + + +def test_split_doc_separates_header_and_transcript(): + header, body = _split_doc(FLAT_DOC) + assert "# grp-c000" in header + assert "## Conversation" not in header + assert "phishing attack" in body + + +def test_split_doc_rejects_doc_without_conversation(): + with pytest.raises(ValueError): + _split_doc("---\ntitle: x\n---\n# x\n\n## Observations\n- [fact] hi\n") + + +def test_clean_extraction_strips_fences_and_preamble(): + fenced = "Here you go:\n```markdown\n## Observations\n- [fact] x\n```" + assert _clean_extraction(fenced).startswith("## Observations") + + +def test_replace_mode_swaps_body_and_keeps_identity(): + out = structure_doc(FLAT_DOC, _FakeRunner(), mode="replace") + # Identity-bearing frontmatter preserved so recall stays comparable. + assert "source_doc_id: grp-c000" in out + assert "permalink: bm-bench/grp-c000" in out + assert "# grp-c000" in out + # Body is BM-native, transcript gone. + assert "## Observations" in out and "## Relations" in out + assert "## Conversation" not in out + + +def test_augment_mode_keeps_transcript_and_appends_structure(): + runner = _FakeRunner() + out = structure_doc(FLAT_DOC, runner, mode="augment") + # Transcript retained AND structure appended. + assert "## Conversation" in out + assert "phishing attack that impersonated the CFO" in out + assert "## Observations" in out and "## Relations" in out + # The conversation text was handed to the extractor. + assert "phishing attack" in runner.prompts[0] + + +def test_structure_doc_fails_loud_on_empty_extraction(): + with pytest.raises(ValueError): + structure_doc(FLAT_DOC, _FakeRunner(output="I could not find anything.")) + + +def _write(root, rel, doc_id): + path = root / rel + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(FLAT_DOC.replace("grp-c000", doc_id), encoding="utf-8") + + +def test_structure_corpus_mirrors_flat_layout(tmp_path): + src = tmp_path / "docs" + _write(src, "locomo-c00-s01.md", "locomo-c00-s01") + _write(src, "locomo-c00-s02.md", "locomo-c00-s02") + + out = tmp_path / "structured" + n = structure_corpus(input_root=src, output_root=out, runner=_FakeRunner(), max_workers=2) + + assert n == 2 + assert sorted(p.name for p in (out).rglob("*.md")) == [ + "locomo-c00-s01.md", + "locomo-c00-s02.md", + ] + assert "## Observations" in (out / "locomo-c00-s01.md").read_text() + + +def test_structure_corpus_grouped_layout_and_category_filter(tmp_path): + groups = tmp_path / "groups" + _write(groups, "implicit_connection-cs10-b1-0001/docs/c000.md", "g1-c000") + _write(groups, "implicit_connection-cs10-b1-0001/docs/c001.md", "g1-c001") + _write(groups, "preference-cs10-b2-0002/docs/c000.md", "g2-c000") # other category + + out = tmp_path / "structured" + n = structure_corpus( + input_root=groups, + output_root=out, + runner=_FakeRunner(), + path_filter=group_prefix_filter({"implicit_connection"}), + max_workers=2, + ) + + assert n == 2 # preference group filtered out + assert (out / "implicit_connection-cs10-b1-0001" / "docs" / "c000.md").exists() + assert not (out / "preference-cs10-b2-0002").exists() diff --git a/benchmarks/uv.lock b/benchmarks/uv.lock new file mode 100644 index 000000000..4948a715f --- /dev/null +++ b/benchmarks/uv.lock @@ -0,0 +1,3664 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version == '3.13.*'", + "python_full_version < '3.13'", +] + +[[package]] +name = "aiofile" +version = "3.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "caio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/e2/d7cb819de8df6b5c1968a2756c3cb4122d4fa2b8fc768b53b7c9e5edb646/aiofile-3.9.0.tar.gz", hash = "sha256:e5ad718bb148b265b6df1b3752c4d1d83024b93da9bd599df74b9d9ffcf7919b", size = 17943, upload-time = "2024-10-08T10:39:35.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/25/da1f0b4dd970e52bf5a36c204c107e11a0c6d3ed195eba0bfbc664c312b2/aiofile-3.9.0-py3-none-any.whl", hash = "sha256:ce2f6c1571538cbdfa0143b04e16b208ecb0e9cb4148e528af8a640ed51cc8aa", size = 19539, upload-time = "2024-10-08T10:39:32.955Z" }, +] + +[[package]] +name = "aiofiles" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/c3/534eac40372d8ee36ef40df62ec129bee4fdb5ad9706e58a29be53b2c970/aiofiles-25.1.0.tar.gz", hash = "sha256:a8d728f0a29de45dc521f18f07297428d56992a742f0cd2701ba86e44d23d5b2", size = 46354, upload-time = "2025-10-09T20:51:04.358Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/8a/340a1555ae33d7354dbca4faa54948d76d89a27ceef032c8c3bc661d003e/aiofiles-25.1.0-py3-none-any.whl", hash = "sha256:abe311e527c862958650f9438e859c1fa7568a141b22abcd015e120e86a85695", size = 14668, upload-time = "2025-10-09T20:51:03.174Z" }, +] + +[[package]] +name = "aiosqlite" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/8a/64761f4005f17809769d23e518d915db74e6310474e733e3593cfc854ef1/aiosqlite-0.22.1.tar.gz", hash = "sha256:043e0bd78d32888c0a9ca90fc788b38796843360c855a7262a532813133a0650", size = 14821, upload-time = "2025-12-23T19:25:43.997Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/b7/e3bf5133d697a08128598c8d0abc5e16377b51465a33756de24fa7dee953/aiosqlite-0.22.1-py3-none-any.whl", hash = "sha256:21c002eb13823fad740196c5a2e9d8e62f6243bd9e7e4a1f87fb5e44ecb4fceb", size = 17405, upload-time = "2025-12-23T19:25:42.139Z" }, +] + +[[package]] +name = "alembic" +version = "1.18.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mako" }, + { name = "sqlalchemy" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/13/8b084e0f2efb0275a1d534838844926f798bd766566b1375174e2448cd31/alembic-1.18.4.tar.gz", hash = "sha256:cb6e1fd84b6174ab8dbb2329f86d631ba9559dd78df550b57804d607672cedbc", size = 2056725, upload-time = "2026-02-10T16:00:47.195Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/29/6533c317b74f707ea28f8d633734dbda2119bbadfc61b2f3640ba835d0f7/alembic-1.18.4-py3-none-any.whl", hash = "sha256:a5ed4adcf6d8a4cb575f3d759f071b03cd6e5c7618eb796cb52497be25bfe19a", size = 263893, upload-time = "2026-02-10T16:00:49.997Z" }, +] + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[[package]] +name = "asyncpg" +version = "0.31.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/cc/d18065ce2380d80b1bcce927c24a2642efd38918e33fd724bc4bca904877/asyncpg-0.31.0.tar.gz", hash = "sha256:c989386c83940bfbd787180f2b1519415e2d3d6277a70d9d0f0145ac73500735", size = 993667, upload-time = "2025-11-24T23:27:00.812Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/a6/59d0a146e61d20e18db7396583242e32e0f120693b67a8de43f1557033e2/asyncpg-0.31.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b44c31e1efc1c15188ef183f287c728e2046abb1d26af4d20858215d50d91fad", size = 662042, upload-time = "2025-11-24T23:25:49.578Z" }, + { url = "https://files.pythonhosted.org/packages/36/01/ffaa189dcb63a2471720615e60185c3f6327716fdc0fc04334436fbb7c65/asyncpg-0.31.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0c89ccf741c067614c9b5fc7f1fc6f3b61ab05ae4aaa966e6fd6b93097c7d20d", size = 638504, upload-time = "2025-11-24T23:25:51.501Z" }, + { url = "https://files.pythonhosted.org/packages/9f/62/3f699ba45d8bd24c5d65392190d19656d74ff0185f42e19d0bbd973bb371/asyncpg-0.31.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:12b3b2e39dc5470abd5e98c8d3373e4b1d1234d9fbdedf538798b2c13c64460a", size = 3426241, upload-time = "2025-11-24T23:25:53.278Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d1/a867c2150f9c6e7af6462637f613ba67f78a314b00db220cd26ff559d532/asyncpg-0.31.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:aad7a33913fb8bcb5454313377cc330fbb19a0cd5faa7272407d8a0c4257b671", size = 3520321, upload-time = "2025-11-24T23:25:54.982Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1a/cce4c3f246805ecd285a3591222a2611141f1669d002163abef999b60f98/asyncpg-0.31.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3df118d94f46d85b2e434fd62c84cb66d5834d5a890725fe625f498e72e4d5ec", size = 3316685, upload-time = "2025-11-24T23:25:57.43Z" }, + { url = "https://files.pythonhosted.org/packages/40/ae/0fc961179e78cc579e138fad6eb580448ecae64908f95b8cb8ee2f241f67/asyncpg-0.31.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bd5b6efff3c17c3202d4b37189969acf8927438a238c6257f66be3c426beba20", size = 3471858, upload-time = "2025-11-24T23:25:59.636Z" }, + { url = "https://files.pythonhosted.org/packages/52/b2/b20e09670be031afa4cbfabd645caece7f85ec62d69c312239de568e058e/asyncpg-0.31.0-cp312-cp312-win32.whl", hash = "sha256:027eaa61361ec735926566f995d959ade4796f6a49d3bde17e5134b9964f9ba8", size = 527852, upload-time = "2025-11-24T23:26:01.084Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f0/f2ed1de154e15b107dc692262395b3c17fc34eafe2a78fc2115931561730/asyncpg-0.31.0-cp312-cp312-win_amd64.whl", hash = "sha256:72d6bdcbc93d608a1158f17932de2321f68b1a967a13e014998db87a72ed3186", size = 597175, upload-time = "2025-11-24T23:26:02.564Z" }, + { url = "https://files.pythonhosted.org/packages/95/11/97b5c2af72a5d0b9bc3fa30cd4b9ce22284a9a943a150fdc768763caf035/asyncpg-0.31.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c204fab1b91e08b0f47e90a75d1b3c62174dab21f670ad6c5d0f243a228f015b", size = 661111, upload-time = "2025-11-24T23:26:04.467Z" }, + { url = "https://files.pythonhosted.org/packages/1b/71/157d611c791a5e2d0423f09f027bd499935f0906e0c2a416ce712ba51ef3/asyncpg-0.31.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:54a64f91839ba59008eccf7aad2e93d6e3de688d796f35803235ea1c4898ae1e", size = 636928, upload-time = "2025-11-24T23:26:05.944Z" }, + { url = "https://files.pythonhosted.org/packages/2e/fc/9e3486fb2bbe69d4a867c0b76d68542650a7ff1574ca40e84c3111bb0c6e/asyncpg-0.31.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0e0822b1038dc7253b337b0f3f676cadc4ac31b126c5d42691c39691962e403", size = 3424067, upload-time = "2025-11-24T23:26:07.957Z" }, + { url = "https://files.pythonhosted.org/packages/12/c6/8c9d076f73f07f995013c791e018a1cd5f31823c2a3187fc8581706aa00f/asyncpg-0.31.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bef056aa502ee34204c161c72ca1f3c274917596877f825968368b2c33f585f4", size = 3518156, upload-time = "2025-11-24T23:26:09.591Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3b/60683a0baf50fbc546499cfb53132cb6835b92b529a05f6a81471ab60d0c/asyncpg-0.31.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0bfbcc5b7ffcd9b75ab1558f00db2ae07db9c80637ad1b2469c43df79d7a5ae2", size = 3319636, upload-time = "2025-11-24T23:26:11.168Z" }, + { url = "https://files.pythonhosted.org/packages/50/dc/8487df0f69bd398a61e1792b3cba0e47477f214eff085ba0efa7eac9ce87/asyncpg-0.31.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22bc525ebbdc24d1261ecbf6f504998244d4e3be1721784b5f64664d61fbe602", size = 3472079, upload-time = "2025-11-24T23:26:13.164Z" }, + { url = "https://files.pythonhosted.org/packages/13/a1/c5bbeeb8531c05c89135cb8b28575ac2fac618bcb60119ee9696c3faf71c/asyncpg-0.31.0-cp313-cp313-win32.whl", hash = "sha256:f890de5e1e4f7e14023619399a471ce4b71f5418cd67a51853b9910fdfa73696", size = 527606, upload-time = "2025-11-24T23:26:14.78Z" }, + { url = "https://files.pythonhosted.org/packages/91/66/b25ccb84a246b470eb943b0107c07edcae51804912b824054b3413995a10/asyncpg-0.31.0-cp313-cp313-win_amd64.whl", hash = "sha256:dc5f2fa9916f292e5c5c8b2ac2813763bcd7f58e130055b4ad8a0531314201ab", size = 596569, upload-time = "2025-11-24T23:26:16.189Z" }, + { url = "https://files.pythonhosted.org/packages/3c/36/e9450d62e84a13aea6580c83a47a437f26c7ca6fa0f0fd40b6670793ea30/asyncpg-0.31.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f6b56b91bb0ffc328c4e3ed113136cddd9deefdf5f79ab448598b9772831df44", size = 660867, upload-time = "2025-11-24T23:26:17.631Z" }, + { url = "https://files.pythonhosted.org/packages/82/4b/1d0a2b33b3102d210439338e1beea616a6122267c0df459ff0265cd5807a/asyncpg-0.31.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:334dec28cf20d7f5bb9e45b39546ddf247f8042a690bff9b9573d00086e69cb5", size = 638349, upload-time = "2025-11-24T23:26:19.689Z" }, + { url = "https://files.pythonhosted.org/packages/41/aa/e7f7ac9a7974f08eff9183e392b2d62516f90412686532d27e196c0f0eeb/asyncpg-0.31.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98cc158c53f46de7bb677fd20c417e264fc02b36d901cc2a43bd6cb0dc6dbfd2", size = 3410428, upload-time = "2025-11-24T23:26:21.275Z" }, + { url = "https://files.pythonhosted.org/packages/6f/de/bf1b60de3dede5c2731e6788617a512bc0ebd9693eac297ee74086f101d7/asyncpg-0.31.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9322b563e2661a52e3cdbc93eed3be7748b289f792e0011cb2720d278b366ce2", size = 3471678, upload-time = "2025-11-24T23:26:23.627Z" }, + { url = "https://files.pythonhosted.org/packages/46/78/fc3ade003e22d8bd53aaf8f75f4be48f0b460fa73738f0391b9c856a9147/asyncpg-0.31.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19857a358fc811d82227449b7ca40afb46e75b33eb8897240c3839dd8b744218", size = 3313505, upload-time = "2025-11-24T23:26:25.235Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e9/73eb8a6789e927816f4705291be21f2225687bfa97321e40cd23055e903a/asyncpg-0.31.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ba5f8886e850882ff2c2ace5732300e99193823e8107e2c53ef01c1ebfa1e85d", size = 3434744, upload-time = "2025-11-24T23:26:26.944Z" }, + { url = "https://files.pythonhosted.org/packages/08/4b/f10b880534413c65c5b5862f79b8e81553a8f364e5238832ad4c0af71b7f/asyncpg-0.31.0-cp314-cp314-win32.whl", hash = "sha256:cea3a0b2a14f95834cee29432e4ddc399b95700eb1d51bbc5bfee8f31fa07b2b", size = 532251, upload-time = "2025-11-24T23:26:28.404Z" }, + { url = "https://files.pythonhosted.org/packages/d3/2d/7aa40750b7a19efa5d66e67fc06008ca0f27ba1bd082e457ad82f59aba49/asyncpg-0.31.0-cp314-cp314-win_amd64.whl", hash = "sha256:04d19392716af6b029411a0264d92093b6e5e8285ae97a39957b9a9c14ea72be", size = 604901, upload-time = "2025-11-24T23:26:30.34Z" }, + { url = "https://files.pythonhosted.org/packages/ce/fe/b9dfe349b83b9dee28cc42360d2c86b2cdce4cb551a2c2d27e156bcac84d/asyncpg-0.31.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bdb957706da132e982cc6856bb2f7b740603472b54c3ebc77fe60ea3e57e1bd2", size = 702280, upload-time = "2025-11-24T23:26:32Z" }, + { url = "https://files.pythonhosted.org/packages/6a/81/e6be6e37e560bd91e6c23ea8a6138a04fd057b08cf63d3c5055c98e81c1d/asyncpg-0.31.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6d11b198111a72f47154fa03b85799f9be63701e068b43f84ac25da0bda9cb31", size = 682931, upload-time = "2025-11-24T23:26:33.572Z" }, + { url = "https://files.pythonhosted.org/packages/a6/45/6009040da85a1648dd5bc75b3b0a062081c483e75a1a29041ae63a0bf0dc/asyncpg-0.31.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18c83b03bc0d1b23e6230f5bf8d4f217dc9bc08644ce0502a9d91dc9e634a9c7", size = 3581608, upload-time = "2025-11-24T23:26:35.638Z" }, + { url = "https://files.pythonhosted.org/packages/7e/06/2e3d4d7608b0b2b3adbee0d0bd6a2d29ca0fc4d8a78f8277df04e2d1fd7b/asyncpg-0.31.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e009abc333464ff18b8f6fd146addffd9aaf63e79aa3bb40ab7a4c332d0c5e9e", size = 3498738, upload-time = "2025-11-24T23:26:37.275Z" }, + { url = "https://files.pythonhosted.org/packages/7d/aa/7d75ede780033141c51d83577ea23236ba7d3a23593929b32b49db8ed36e/asyncpg-0.31.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3b1fbcb0e396a5ca435a8826a87e5c2c2cc0c8c68eb6fadf82168056b0e53a8c", size = 3401026, upload-time = "2025-11-24T23:26:39.423Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7a/15e37d45e7f7c94facc1e9148c0e455e8f33c08f0b8a0b1deb2c5171771b/asyncpg-0.31.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8df714dba348efcc162d2adf02d213e5fab1bd9f557e1305633e851a61814a7a", size = 3429426, upload-time = "2025-11-24T23:26:41.032Z" }, + { url = "https://files.pythonhosted.org/packages/13/d5/71437c5f6ae5f307828710efbe62163974e71237d5d46ebd2869ea052d10/asyncpg-0.31.0-cp314-cp314t-win32.whl", hash = "sha256:1b41f1afb1033f2b44f3234993b15096ddc9cd71b21a42dbd87fc6a57b43d65d", size = 614495, upload-time = "2025-11-24T23:26:42.659Z" }, + { url = "https://files.pythonhosted.org/packages/3c/d7/8fb3044eaef08a310acfe23dae9a8e2e07d305edc29a53497e52bc76eca7/asyncpg-0.31.0-cp314-cp314t-win_amd64.whl", hash = "sha256:bd4107bb7cdd0e9e65fae66a62afd3a249663b844fa34d479f6d5b3bef9c04c3", size = 706062, upload-time = "2025-11-24T23:26:44.086Z" }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, +] + +[[package]] +name = "authlib" +version = "1.6.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6b/6c/c88eac87468c607f88bc24df1f3b31445ee6fc9ba123b09e666adf687cd9/authlib-1.6.8.tar.gz", hash = "sha256:41ae180a17cf672bc784e4a518e5c82687f1fe1e98b0cafaeda80c8e4ab2d1cb", size = 165074, upload-time = "2026-02-14T04:02:17.941Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/73/f7084bf12755113cd535ae586782ff3a6e710bfbe6a0d13d1c2f81ffbbfa/authlib-1.6.8-py2.py3-none-any.whl", hash = "sha256:97286fd7a15e6cfefc32771c8ef9c54f0ed58028f1322de6a2a7c969c3817888", size = 244116, upload-time = "2026-02-14T04:02:15.579Z" }, +] + +[[package]] +name = "backoff" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/d7/5bbeb12c44d7c4f2fb5b56abce497eb5ed9f34d85701de869acedd602619/backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba", size = 17001, upload-time = "2022-10-05T19:19:32.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8", size = 15148, upload-time = "2022-10-05T19:19:30.546Z" }, +] + +[[package]] +name = "basic-memory" +version = "0.20.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiofiles" }, + { name = "aiosqlite" }, + { name = "alembic" }, + { name = "anyio" }, + { name = "asyncpg" }, + { name = "dateparser" }, + { name = "fastapi", extra = ["standard"] }, + { name = "fastembed" }, + { name = "fastmcp" }, + { name = "greenlet" }, + { name = "httpx" }, + { name = "loguru" }, + { name = "markdown-it-py" }, + { name = "mcp" }, + { name = "mdformat" }, + { name = "mdformat-frontmatter" }, + { name = "mdformat-gfm" }, + { name = "nest-asyncio" }, + { name = "openai" }, + { name = "pillow" }, + { name = "psycopg" }, + { name = "pybars3" }, + { name = "pydantic", extra = ["email", "timezone"] }, + { name = "pydantic-settings" }, + { name = "pyjwt" }, + { name = "pyright" }, + { name = "pytest-aio" }, + { name = "pytest-asyncio" }, + { name = "python-dotenv" }, + { name = "python-frontmatter" }, + { name = "pyyaml" }, + { name = "rich" }, + { name = "sniffio" }, + { name = "sqlalchemy" }, + { name = "sqlite-vec" }, + { name = "typer" }, + { name = "unidecode" }, + { name = "watchfiles" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/e2/3d8cc83327b51bb4467ba3bde74bd307bee29b15ae286663261b131a017d/basic_memory-0.20.2.tar.gz", hash = "sha256:6b408e4ebe4cb0fc0600d774b9d0122bce3fd9fe903a5968d582cab68c6a9eb3", size = 1698360, upload-time = "2026-03-11T04:15:01.539Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/48/8fe5bee09b8b253a2295e8ba84170aee7438ee3261ba13fb61ec9faf39dd/basic_memory-0.20.2-py3-none-any.whl", hash = "sha256:75ec349073ce03b1bdcc3c0f9199c009940348166f82bf79351dea1be9c1e230", size = 786773, upload-time = "2026-03-11T04:14:58.714Z" }, +] + +[[package]] +name = "basic-memory-benchmarks" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "basic-memory" }, + { name = "httpx" }, + { name = "mcp" }, + { name = "mem0ai", extra = ["nlp"] }, + { name = "pydantic" }, + { name = "python-frontmatter" }, + { name = "rich" }, + { name = "typer" }, +] + +[package.optional-dependencies] +judge = [ + { name = "pydantic-evals" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pyright" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "basic-memory", specifier = ">=0.20.2" }, + { name = "httpx", specifier = ">=0.28.0" }, + { name = "mcp", specifier = ">=1.23.1" }, + { name = "mem0ai", extras = ["nlp"], specifier = "==2.0.5" }, + { name = "pydantic", specifier = ">=2.12.0" }, + { name = "pydantic-evals", marker = "extra == 'judge'", specifier = ">=0.4.0" }, + { name = "python-frontmatter", specifier = ">=1.1.0" }, + { name = "rich", specifier = ">=13.9.4" }, + { name = "typer", specifier = ">=0.16.1" }, +] +provides-extras = ["judge"] + +[package.metadata.requires-dev] +dev = [ + { name = "pyright", specifier = ">=1.1.408" }, + { name = "pytest", specifier = ">=8.4.1" }, + { name = "pytest-asyncio", specifier = ">=1.2.0" }, + { name = "ruff", specifier = ">=0.13.0" }, +] + +[[package]] +name = "beartype" +version = "0.22.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/94/1009e248bbfbab11397abca7193bea6626806be9a327d399810d523a07cb/beartype-0.22.9.tar.gz", hash = "sha256:8f82b54aa723a2848a56008d18875f91c1db02c32ef6a62319a002e3e25a975f", size = 1608866, upload-time = "2025-12-13T06:50:30.72Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/cc/18245721fa7747065ab478316c7fea7c74777d07f37ae60db2e84f8172e8/beartype-0.22.9-py3-none-any.whl", hash = "sha256:d16c9bbc61ea14637596c5f6fbff2ee99cbe3573e46a716401734ef50c3060c2", size = 1333658, upload-time = "2025-12-13T06:50:28.266Z" }, +] + +[[package]] +name = "blis" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d0/d0/d8cc8c9a4488a787e7fa430f6055e5bd1ddb22c340a751d9e901b82e2efe/blis-1.3.3.tar.gz", hash = "sha256:034d4560ff3cc43e8aa37e188451b0440e3261d989bb8a42ceee865607715ecd", size = 2644873, upload-time = "2025-11-17T12:28:30.511Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/d1/429cf0cf693d4c7dc2efed969bd474e315aab636e4a95f66c4ed7264912d/blis-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2a1c74e100665f8e918ebdbae2794576adf1f691680b5cdb8b29578432f623ef", size = 6929663, upload-time = "2025-11-17T12:27:44.482Z" }, + { url = "https://files.pythonhosted.org/packages/11/69/363c8df8d98b3cc97be19aad6aabb2c9c53f372490d79316bdee92d476e7/blis-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3f6c595185176ce021316263e1a1d636a3425b6c48366c1fd712d08d0b71849a", size = 1230939, upload-time = "2025-11-17T12:27:46.19Z" }, + { url = "https://files.pythonhosted.org/packages/96/2a/fbf65d906d823d839076c5150a6f8eb5ecbc5f9135e0b6510609bda1e6b7/blis-1.3.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d734b19fba0be7944f272dfa7b443b37c61f9476d9ab054a9ac53555ceadd2e0", size = 2818835, upload-time = "2025-11-17T12:27:48.167Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ad/58deaa3ad856dd3cc96493e40ffd2ed043d18d4d304f85a65cde1ccbf644/blis-1.3.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ef6d6e2b599a3a2788eb6d9b443533961265aa4ec49d574ed4bb846e548dcdb", size = 11366550, upload-time = "2025-11-17T12:27:49.958Z" }, + { url = "https://files.pythonhosted.org/packages/78/82/816a7adfe1f7acc8151f01ec86ef64467a3c833932d8f19f8e06613b8a4e/blis-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8c888438ae99c500422d50698e3028b65caa8ebb44e24204d87fda2df64058f7", size = 3023686, upload-time = "2025-11-17T12:27:52.062Z" }, + { url = "https://files.pythonhosted.org/packages/1e/e2/0e93b865f648b5519360846669a35f28ee8f4e1d93d054f6850d8afbabde/blis-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8177879fd3590b5eecdd377f9deafb5dc8af6d684f065bd01553302fb3fcf9a7", size = 14250939, upload-time = "2025-11-17T12:27:53.847Z" }, + { url = "https://files.pythonhosted.org/packages/20/07/fb43edc2ff0a6a367e4a94fc39eb3b85aa1e55e24cc857af2db145ce9f0d/blis-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:f20f7ad69aaffd1ce14fe77de557b6df9b61e0c9e582f75a843715d836b5c8af", size = 6192759, upload-time = "2025-11-17T12:27:56.176Z" }, + { url = "https://files.pythonhosted.org/packages/e6/f7/d26e62d9be3d70473a63e0a5d30bae49c2fe138bebac224adddcdef8a7ce/blis-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1e647341f958421a86b028a2efe16ce19c67dba2a05f79e8f7e80b1ff45328aa", size = 6928322, upload-time = "2025-11-17T12:27:57.965Z" }, + { url = "https://files.pythonhosted.org/packages/4a/78/750d12da388f714958eb2f2fd177652323bbe7ec528365c37129edd6eb84/blis-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d563160f874abb78a57e346f07312c5323f7ad67b6370052b6b17087ef234a8e", size = 1229635, upload-time = "2025-11-17T12:28:00.118Z" }, + { url = "https://files.pythonhosted.org/packages/e8/36/eac4199c5b200a5f3e93cad197da8d26d909f218eb444c4f552647c95240/blis-1.3.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:30b8a5b90cb6cb81d1ada9ae05aa55fb8e70d9a0ae9db40d2401bb9c1c8f14c4", size = 2815650, upload-time = "2025-11-17T12:28:02.544Z" }, + { url = "https://files.pythonhosted.org/packages/bf/51/472e7b36a6bedb5242a9757e7486f702c3619eff76e256735d0c8b1679c6/blis-1.3.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e9f5c53b277f6ac5b3ca30bc12ebab7ea16c8f8c36b14428abb56924213dc127", size = 11359008, upload-time = "2025-11-17T12:28:04.589Z" }, + { url = "https://files.pythonhosted.org/packages/84/da/d0dfb6d6e6321ae44df0321384c32c322bd07b15740d7422727a1a49fc5d/blis-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6297e7616c158b305c9a8a4e47ca5fc9b0785194dd96c903b1a1591a7ca21ddf", size = 3011959, upload-time = "2025-11-17T12:28:06.862Z" }, + { url = "https://files.pythonhosted.org/packages/20/c5/2b0b5e556fa0364ed671051ea078a6d6d7b979b1cfef78d64ad3ca5f0c7f/blis-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3f966ca74f89f8a33e568b9a1d71992fc9a0d29a423e047f0a212643e21b5458", size = 14232456, upload-time = "2025-11-17T12:28:08.779Z" }, + { url = "https://files.pythonhosted.org/packages/31/07/4cdc81a47bf862c0b06d91f1bc6782064e8b69ac9b5d4ff51d97e4ff03da/blis-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:7a0fc4b237a3a453bdc3c7ab48d91439fcd2d013b665c46948d9eaf9c3e45a97", size = 6192624, upload-time = "2025-11-17T12:28:14.197Z" }, + { url = "https://files.pythonhosted.org/packages/5f/8a/80f7c68fbc24a76fc9c18522c46d6d69329c320abb18e26a707a5d874083/blis-1.3.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c3e33cfbf22a418373766816343fcfcd0556012aa3ffdf562c29cddec448a415", size = 6934081, upload-time = "2025-11-17T12:28:16.436Z" }, + { url = "https://files.pythonhosted.org/packages/e5/52/d1aa3a51a7fc299b0c89dcaa971922714f50b1202769eebbdaadd1b5cff7/blis-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6f165930e8d3a85c606d2003211497e28d528c7416fbfeafb6b15600963f7c9b", size = 1231486, upload-time = "2025-11-17T12:28:18.008Z" }, + { url = "https://files.pythonhosted.org/packages/99/4f/badc7bd7f74861b26c10123bba7b9d16f99cd9535ad0128780360713820f/blis-1.3.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:878d4d96d8f2c7a2459024f013f2e4e5f46d708b23437dae970d998e7bff14a0", size = 2814944, upload-time = "2025-11-17T12:28:19.654Z" }, + { url = "https://files.pythonhosted.org/packages/72/a6/f62a3bd814ca19ec7e29ac889fd354adea1217df3183e10217de51e2eb8b/blis-1.3.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f36c0ca84a05ee5d3dbaa38056c4423c1fc29948b17a7923dd2fed8967375d74", size = 11345825, upload-time = "2025-11-17T12:28:21.354Z" }, + { url = "https://files.pythonhosted.org/packages/d4/6c/671af79ee42bc4c968cae35c091ac89e8721c795bfa4639100670dc59139/blis-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e5a662c48cd4aad5dae1a950345df23957524f071315837a4c6feb7d3b288990", size = 3008771, upload-time = "2025-11-17T12:28:23.637Z" }, + { url = "https://files.pythonhosted.org/packages/be/92/7cd7f8490da7c98ee01557f2105885cc597217b0e7fd2eeb9e22cdd4ef23/blis-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9de26fbd72bac900c273b76d46f0b45b77a28eace2e01f6ac6c2239531a413bb", size = 14219213, upload-time = "2025-11-17T12:28:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/0a/de/acae8e9f9a1f4bb393d41c8265898b0f29772e38eac14e9f69d191e2c006/blis-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:9e5fdf4211b1972400f8ff6dafe87cb689c5d84f046b4a76b207c0bd2270faaf", size = 6324695, upload-time = "2025-11-17T12:28:28.401Z" }, +] + +[[package]] +name = "cachetools" +version = "7.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/07/56595285564e90777d758ebd383d6b0b971b87729bbe2184a849932a3736/cachetools-7.0.1.tar.gz", hash = "sha256:e31e579d2c5b6e2944177a0397150d312888ddf4e16e12f1016068f0c03b8341", size = 36126, upload-time = "2026-02-10T22:24:05.03Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/9e/5faefbf9db1db466d633735faceda1f94aa99ce506ac450d232536266b32/cachetools-7.0.1-py3-none-any.whl", hash = "sha256:8f086515c254d5664ae2146d14fc7f65c9a4bce75152eb247e5a9c5e6d7b2ecf", size = 13484, upload-time = "2026-02-10T22:24:03.741Z" }, +] + +[[package]] +name = "caio" +version = "0.9.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/92/88/b8527e1b00c1811db339a1df8bd1ae49d146fcea9d6a5c40e3a80aaeb38d/caio-0.9.25.tar.gz", hash = "sha256:16498e7f81d1d0f5a4c0ad3f2540e65fe25691376e0a5bd367f558067113ed10", size = 26781, upload-time = "2025-12-26T15:21:36.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/25/79c98ebe12df31548ba4eaf44db11b7cad6b3e7b4203718335620939083c/caio-0.9.25-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fb7ff95af4c31ad3f03179149aab61097a71fd85e05f89b4786de0359dffd044", size = 36983, upload-time = "2025-12-26T15:21:36.075Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2b/21288691f16d479945968a0a4f2856818c1c5be56881d51d4dac9b255d26/caio-0.9.25-cp312-cp312-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:97084e4e30dfa598449d874c4d8e0c8d5ea17d2f752ef5e48e150ff9d240cd64", size = 82012, upload-time = "2025-12-26T15:22:20.983Z" }, + { url = "https://files.pythonhosted.org/packages/03/c4/8a1b580875303500a9c12b9e0af58cb82e47f5bcf888c2457742a138273c/caio-0.9.25-cp312-cp312-manylinux_2_34_aarch64.whl", hash = "sha256:4fa69eba47e0f041b9d4f336e2ad40740681c43e686b18b191b6c5f4c5544bfb", size = 81502, upload-time = "2026-03-04T22:08:22.381Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1c/0fe770b8ffc8362c48134d1592d653a81a3d8748d764bec33864db36319d/caio-0.9.25-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:6bebf6f079f1341d19f7386db9b8b1f07e8cc15ae13bfdaff573371ba0575d69", size = 80200, upload-time = "2026-03-04T22:08:23.382Z" }, + { url = "https://files.pythonhosted.org/packages/31/57/5e6ff127e6f62c9f15d989560435c642144aa4210882f9494204bc892305/caio-0.9.25-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d6c2a3411af97762a2b03840c3cec2f7f728921ff8adda53d7ea2315a8563451", size = 36979, upload-time = "2025-12-26T15:21:35.484Z" }, + { url = "https://files.pythonhosted.org/packages/a3/9f/f21af50e72117eb528c422d4276cbac11fb941b1b812b182e0a9c70d19c5/caio-0.9.25-cp313-cp313-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0998210a4d5cd5cb565b32ccfe4e53d67303f868a76f212e002a8554692870e6", size = 81900, upload-time = "2025-12-26T15:22:21.919Z" }, + { url = "https://files.pythonhosted.org/packages/9c/12/c39ae2a4037cb10ad5eb3578eb4d5f8c1a2575c62bba675f3406b7ef0824/caio-0.9.25-cp313-cp313-manylinux_2_34_aarch64.whl", hash = "sha256:1a177d4777141b96f175fe2c37a3d96dec7911ed9ad5f02bac38aaa1c936611f", size = 81523, upload-time = "2026-03-04T22:08:25.187Z" }, + { url = "https://files.pythonhosted.org/packages/22/59/f8f2e950eb4f1a5a3883e198dca514b9d475415cb6cd7b78b9213a0dd45a/caio-0.9.25-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:9ed3cfb28c0e99fec5e208c934e5c157d0866aa9c32aa4dc5e9b6034af6286b7", size = 80243, upload-time = "2026-03-04T22:08:26.449Z" }, + { url = "https://files.pythonhosted.org/packages/69/ca/a08fdc7efdcc24e6a6131a93c85be1f204d41c58f474c42b0670af8c016b/caio-0.9.25-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fab6078b9348e883c80a5e14b382e6ad6aabbc4429ca034e76e730cf464269db", size = 36978, upload-time = "2025-12-26T15:21:41.055Z" }, + { url = "https://files.pythonhosted.org/packages/5e/6c/d4d24f65e690213c097174d26eda6831f45f4734d9d036d81790a27e7b78/caio-0.9.25-cp314-cp314-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:44a6b58e52d488c75cfaa5ecaa404b2b41cc965e6c417e03251e868ecd5b6d77", size = 81832, upload-time = "2025-12-26T15:22:22.757Z" }, + { url = "https://files.pythonhosted.org/packages/87/a4/e534cf7d2d0e8d880e25dd61e8d921ffcfe15bd696734589826f5a2df727/caio-0.9.25-cp314-cp314-manylinux_2_34_aarch64.whl", hash = "sha256:628a630eb7fb22381dd8e3c8ab7f59e854b9c806639811fc3f4310c6bd711d79", size = 81565, upload-time = "2026-03-04T22:08:27.483Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ed/bf81aeac1d290017e5e5ac3e880fd56ee15e50a6d0353986799d1bc5cfd5/caio-0.9.25-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:0ba16aa605ccb174665357fc729cf500679c2d94d5f1458a6f0d5ca48f2060a7", size = 80071, upload-time = "2026-03-04T22:08:28.751Z" }, + { url = "https://files.pythonhosted.org/packages/86/93/1f76c8d1bafe3b0614e06b2195784a3765bbf7b0a067661af9e2dd47fc33/caio-0.9.25-py3-none-any.whl", hash = "sha256:06c0bb02d6b929119b1cfbe1ca403c768b2013a369e2db46bfa2a5761cf82e40", size = 19087, upload-time = "2025-12-26T15:22:00.221Z" }, +] + +[[package]] +name = "catalogue" +version = "2.0.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/38/b4/244d58127e1cdf04cf2dc7d9566f0d24ef01d5ce21811bab088ecc62b5ea/catalogue-2.0.10.tar.gz", hash = "sha256:4f56daa940913d3f09d589c191c74e5a6d51762b3a9e37dd53b7437afd6cda15", size = 19561, upload-time = "2023-09-25T06:29:24.962Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/96/d32b941a501ab566a16358d68b6eb4e4acc373fab3c3c4d7d9e649f7b4bb/catalogue-2.0.10-py3-none-any.whl", hash = "sha256:58c2de0020aa90f4a2da7dfad161bf7b3b054c86a5f09fcedc0b2b740c109a9f", size = 17325, upload-time = "2023-09-25T06:29:23.337Z" }, +] + +[[package]] +name = "certifi" +version = "2026.2.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[package]] +name = "cloudpathlib" +version = "0.24.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/19/58bc6b5d7d0f81c7209b05445af477e147c486552f96665a5912211839b9/cloudpathlib-0.24.0.tar.gz", hash = "sha256:c521a984e77b47e656fe78e20a7e3e260e0ab45fc69e33ac01094227c979e34a", size = 53600, upload-time = "2026-04-30T00:54:43.265Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/5b/ba933f896d9b0b07608d575a8501e2b4e32166b60d84c430a4a7285ebe64/cloudpathlib-0.24.0-py3-none-any.whl", hash = "sha256:b1c51e2d2ec7dc4fed6538991f4aea849d6cf11a7e6b9069f86e461aa1f9b5b4", size = 63214, upload-time = "2026-04-30T00:54:42.06Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "confection" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/65/efd0fe8a936fc8ca2978cb7b82581fb20d901c6039e746a808f746b7647b/confection-1.3.3.tar.gz", hash = "sha256:f0f6810d567ff73993fe74d218ca5e1ffb6a44fb03f391257fc5d033546cbfaa", size = 54895, upload-time = "2026-03-24T18:45:24.331Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/e4/d66708bdf0d92fb4d49b22cdff4b10cec38aca5dcd7e81d909bb55c65cd7/confection-1.3.3-py3-none-any.whl", hash = "sha256:b9fef9ee84b237ef4611ec3eb5797b70e13063e6310ad9f15536373f5e313c82", size = 35902, upload-time = "2026-03-24T18:45:22.664Z" }, +] + +[[package]] +name = "cryptography" +version = "46.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size = 750064, upload-time = "2026-02-10T19:18:38.255Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/81/b0bb27f2ba931a65409c6b8a8b358a7f03c0e46eceacddff55f7c84b1f3b/cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad", size = 7176289, upload-time = "2026-02-10T19:17:08.274Z" }, + { url = "https://files.pythonhosted.org/packages/ff/9e/6b4397a3e3d15123de3b1806ef342522393d50736c13b20ec4c9ea6693a6/cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", size = 4275637, upload-time = "2026-02-10T19:17:10.53Z" }, + { url = "https://files.pythonhosted.org/packages/63/e7/471ab61099a3920b0c77852ea3f0ea611c9702f651600397ac567848b897/cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", size = 4424742, upload-time = "2026-02-10T19:17:12.388Z" }, + { url = "https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", size = 4277528, upload-time = "2026-02-10T19:17:13.853Z" }, + { url = "https://files.pythonhosted.org/packages/22/29/c2e812ebc38c57b40e7c583895e73c8c5adb4d1e4a0cc4c5a4fdab2b1acc/cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", size = 4947993, upload-time = "2026-02-10T19:17:15.618Z" }, + { url = "https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", size = 4456855, upload-time = "2026-02-10T19:17:17.221Z" }, + { url = "https://files.pythonhosted.org/packages/2d/87/fc628a7ad85b81206738abbd213b07702bcbdada1dd43f72236ef3cffbb5/cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", size = 3984635, upload-time = "2026-02-10T19:17:18.792Z" }, + { url = "https://files.pythonhosted.org/packages/84/29/65b55622bde135aedf4565dc509d99b560ee4095e56989e815f8fd2aa910/cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", size = 4277038, upload-time = "2026-02-10T19:17:20.256Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/45e76c68d7311432741faf1fbf7fac8a196a0a735ca21f504c75d37e2558/cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", size = 4912181, upload-time = "2026-02-10T19:17:21.825Z" }, + { url = "https://files.pythonhosted.org/packages/6d/1a/c1ba8fead184d6e3d5afcf03d569acac5ad063f3ac9fb7258af158f7e378/cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", size = 4456482, upload-time = "2026-02-10T19:17:25.133Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e5/3fb22e37f66827ced3b902cf895e6a6bc1d095b5b26be26bd13c441fdf19/cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", size = 4405497, upload-time = "2026-02-10T19:17:26.66Z" }, + { url = "https://files.pythonhosted.org/packages/1a/df/9d58bb32b1121a8a2f27383fabae4d63080c7ca60b9b5c88be742be04ee7/cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", size = 4667819, upload-time = "2026-02-10T19:17:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ed/325d2a490c5e94038cdb0117da9397ece1f11201f425c4e9c57fe5b9f08b/cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48", size = 3028230, upload-time = "2026-02-10T19:17:30.518Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5a/ac0f49e48063ab4255d9e3b79f5def51697fce1a95ea1370f03dc9db76f6/cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4", size = 3480909, upload-time = "2026-02-10T19:17:32.083Z" }, + { url = "https://files.pythonhosted.org/packages/00/13/3d278bfa7a15a96b9dc22db5a12ad1e48a9eb3d40e1827ef66a5df75d0d0/cryptography-46.0.5-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2", size = 7119287, upload-time = "2026-02-10T19:17:33.801Z" }, + { url = "https://files.pythonhosted.org/packages/67/c8/581a6702e14f0898a0848105cbefd20c058099e2c2d22ef4e476dfec75d7/cryptography-46.0.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678", size = 4265728, upload-time = "2026-02-10T19:17:35.569Z" }, + { url = "https://files.pythonhosted.org/packages/dd/4a/ba1a65ce8fc65435e5a849558379896c957870dd64fecea97b1ad5f46a37/cryptography-46.0.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87", size = 4408287, upload-time = "2026-02-10T19:17:36.938Z" }, + { url = "https://files.pythonhosted.org/packages/f8/67/8ffdbf7b65ed1ac224d1c2df3943553766914a8ca718747ee3871da6107e/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee", size = 4270291, upload-time = "2026-02-10T19:17:38.748Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e5/f52377ee93bc2f2bba55a41a886fd208c15276ffbd2569f2ddc89d50e2c5/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981", size = 4927539, upload-time = "2026-02-10T19:17:40.241Z" }, + { url = "https://files.pythonhosted.org/packages/3b/02/cfe39181b02419bbbbcf3abdd16c1c5c8541f03ca8bda240debc467d5a12/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9", size = 4442199, upload-time = "2026-02-10T19:17:41.789Z" }, + { url = "https://files.pythonhosted.org/packages/c0/96/2fcaeb4873e536cf71421a388a6c11b5bc846e986b2b069c79363dc1648e/cryptography-46.0.5-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648", size = 3960131, upload-time = "2026-02-10T19:17:43.379Z" }, + { url = "https://files.pythonhosted.org/packages/d8/d2/b27631f401ddd644e94c5cf33c9a4069f72011821cf3dc7309546b0642a0/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4", size = 4270072, upload-time = "2026-02-10T19:17:45.481Z" }, + { url = "https://files.pythonhosted.org/packages/f4/a7/60d32b0370dae0b4ebe55ffa10e8599a2a59935b5ece1b9f06edb73abdeb/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0", size = 4892170, upload-time = "2026-02-10T19:17:46.997Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b9/cf73ddf8ef1164330eb0b199a589103c363afa0cf794218c24d524a58eab/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663", size = 4441741, upload-time = "2026-02-10T19:17:48.661Z" }, + { url = "https://files.pythonhosted.org/packages/5f/eb/eee00b28c84c726fe8fa0158c65afe312d9c3b78d9d01daf700f1f6e37ff/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826", size = 4396728, upload-time = "2026-02-10T19:17:50.058Z" }, + { url = "https://files.pythonhosted.org/packages/65/f4/6bc1a9ed5aef7145045114b75b77c2a8261b4d38717bd8dea111a63c3442/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d", size = 4652001, upload-time = "2026-02-10T19:17:51.54Z" }, + { url = "https://files.pythonhosted.org/packages/86/ef/5d00ef966ddd71ac2e6951d278884a84a40ffbd88948ef0e294b214ae9e4/cryptography-46.0.5-cp314-cp314t-win32.whl", hash = "sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a", size = 3003637, upload-time = "2026-02-10T19:17:52.997Z" }, + { url = "https://files.pythonhosted.org/packages/b7/57/f3f4160123da6d098db78350fdfd9705057aad21de7388eacb2401dceab9/cryptography-46.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4", size = 3469487, upload-time = "2026-02-10T19:17:54.549Z" }, + { url = "https://files.pythonhosted.org/packages/e2/fa/a66aa722105ad6a458bebd64086ca2b72cdd361fed31763d20390f6f1389/cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31", size = 7170514, upload-time = "2026-02-10T19:17:56.267Z" }, + { url = "https://files.pythonhosted.org/packages/0f/04/c85bdeab78c8bc77b701bf0d9bdcf514c044e18a46dcff330df5448631b0/cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", size = 4275349, upload-time = "2026-02-10T19:17:58.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/32/9b87132a2f91ee7f5223b091dc963055503e9b442c98fc0b8a5ca765fab0/cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", size = 4420667, upload-time = "2026-02-10T19:18:00.619Z" }, + { url = "https://files.pythonhosted.org/packages/a1/a6/a7cb7010bec4b7c5692ca6f024150371b295ee1c108bdc1c400e4c44562b/cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", size = 4276980, upload-time = "2026-02-10T19:18:02.379Z" }, + { url = "https://files.pythonhosted.org/packages/8e/7c/c4f45e0eeff9b91e3f12dbd0e165fcf2a38847288fcfd889deea99fb7b6d/cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", size = 4939143, upload-time = "2026-02-10T19:18:03.964Z" }, + { url = "https://files.pythonhosted.org/packages/37/19/e1b8f964a834eddb44fa1b9a9976f4e414cbb7aa62809b6760c8803d22d1/cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", size = 4453674, upload-time = "2026-02-10T19:18:05.588Z" }, + { url = "https://files.pythonhosted.org/packages/db/ed/db15d3956f65264ca204625597c410d420e26530c4e2943e05a0d2f24d51/cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", size = 3978801, upload-time = "2026-02-10T19:18:07.167Z" }, + { url = "https://files.pythonhosted.org/packages/41/e2/df40a31d82df0a70a0daf69791f91dbb70e47644c58581d654879b382d11/cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", size = 4276755, upload-time = "2026-02-10T19:18:09.813Z" }, + { url = "https://files.pythonhosted.org/packages/33/45/726809d1176959f4a896b86907b98ff4391a8aa29c0aaaf9450a8a10630e/cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", size = 4901539, upload-time = "2026-02-10T19:18:11.263Z" }, + { url = "https://files.pythonhosted.org/packages/99/0f/a3076874e9c88ecb2ecc31382f6e7c21b428ede6f55aafa1aa272613e3cd/cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", size = 4452794, upload-time = "2026-02-10T19:18:12.914Z" }, + { url = "https://files.pythonhosted.org/packages/02/ef/ffeb542d3683d24194a38f66ca17c0a4b8bf10631feef44a7ef64e631b1a/cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", size = 4404160, upload-time = "2026-02-10T19:18:14.375Z" }, + { url = "https://files.pythonhosted.org/packages/96/93/682d2b43c1d5f1406ed048f377c0fc9fc8f7b0447a478d5c65ab3d3a66eb/cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", size = 4667123, upload-time = "2026-02-10T19:18:15.886Z" }, + { url = "https://files.pythonhosted.org/packages/45/2d/9c5f2926cb5300a8eefc3f4f0b3f3df39db7f7ce40c8365444c49363cbda/cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72", size = 3010220, upload-time = "2026-02-10T19:18:17.361Z" }, + { url = "https://files.pythonhosted.org/packages/48/ef/0c2f4a8e31018a986949d34a01115dd057bf536905dca38897bacd21fac3/cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", size = 3467050, upload-time = "2026-02-10T19:18:18.899Z" }, +] + +[[package]] +name = "cyclopts" +version = "4.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "docstring-parser" }, + { name = "rich" }, + { name = "rich-rst" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/5c/88a4068c660a096bbe87efc5b7c190080c9e86919c36ec5f092cb08d852f/cyclopts-4.6.0.tar.gz", hash = "sha256:483c4704b953ea6da742e8de15972f405d2e748d19a848a4d61595e8e5360ee5", size = 162724, upload-time = "2026-02-23T15:44:49.286Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/eb/1e8337755a70dc7d7ff10a73dc8f20e9352c9ad6c2256ed863ac95cd3539/cyclopts-4.6.0-py3-none-any.whl", hash = "sha256:0a891cb55bfd79a3cdce024db8987b33316aba11071e5258c21ac12a640ba9f2", size = 200518, upload-time = "2026-02-23T15:44:47.854Z" }, +] + +[[package]] +name = "cymem" +version = "2.0.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/2f0fbb32535c3731b7c2974c569fb9325e0a38ed5565a08e1139a3b71e82/cymem-2.0.13.tar.gz", hash = "sha256:1c91a92ae8c7104275ac26bd4d29b08ccd3e7faff5893d3858cb6fadf1bc1588", size = 12320, upload-time = "2025-11-14T14:58:36.902Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/52/478a2911ab5028cb710b4900d64aceba6f4f882fcb13fd8d40a456a1b6dc/cymem-2.0.13-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e8afbc5162a0fe14b6463e1c4e45248a1b2fe2cbcecc8a5b9e511117080da0eb", size = 43745, upload-time = "2025-11-14T14:57:32.52Z" }, + { url = "https://files.pythonhosted.org/packages/f9/71/f0f8adee945524774b16af326bd314a14a478ed369a728a22834e6785a18/cymem-2.0.13-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9251d889348fe79a75e9b3e4d1b5fa651fca8a64500820685d73a3acc21b6a8", size = 42927, upload-time = "2025-11-14T14:57:33.827Z" }, + { url = "https://files.pythonhosted.org/packages/62/6d/159780fe162ff715d62b809246e5fc20901cef87ca28b67d255a8d741861/cymem-2.0.13-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:742fc19764467a49ed22e56a4d2134c262d73a6c635409584ae3bf9afa092c33", size = 258346, upload-time = "2025-11-14T14:57:34.917Z" }, + { url = "https://files.pythonhosted.org/packages/eb/12/678d16f7aa1996f947bf17b8cfb917ea9c9674ef5e2bd3690c04123d5680/cymem-2.0.13-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f190a92fe46197ee64d32560eb121c2809bb843341733227f51538ce77b3410d", size = 260843, upload-time = "2025-11-14T14:57:36.503Z" }, + { url = "https://files.pythonhosted.org/packages/31/5d/0dd8c167c08cd85e70d274b7235cfe1e31b3cebc99221178eaf4bbb95c6f/cymem-2.0.13-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d670329ee8dbbbf241b7c08069fe3f1d3a1a3e2d69c7d05ea008a7010d826298", size = 254607, upload-time = "2025-11-14T14:57:38.036Z" }, + { url = "https://files.pythonhosted.org/packages/b7/c9/d6514a412a1160aa65db539836b3d47f9b59f6675f294ec34ae32f867c82/cymem-2.0.13-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a84ba3178d9128b9ffb52ce81ebab456e9fe959125b51109f5b73ebdfc6b60d6", size = 262421, upload-time = "2025-11-14T14:57:39.265Z" }, + { url = "https://files.pythonhosted.org/packages/dd/fe/3ee37d02ca4040f2fb22d34eb415198f955862b5dd47eee01df4c8f5454c/cymem-2.0.13-cp312-cp312-win_amd64.whl", hash = "sha256:2ff1c41fd59b789579fdace78aa587c5fc091991fa59458c382b116fc36e30dc", size = 40176, upload-time = "2025-11-14T14:57:40.706Z" }, + { url = "https://files.pythonhosted.org/packages/94/fb/1b681635bfd5f2274d0caa8f934b58435db6c091b97f5593738065ddb786/cymem-2.0.13-cp312-cp312-win_arm64.whl", hash = "sha256:6bbd701338df7bf408648191dff52472a9b334f71bcd31a21a41d83821050f67", size = 35959, upload-time = "2025-11-14T14:57:41.682Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0f/95a4d1e3bebfdfa7829252369357cf9a764f67569328cd9221f21e2c952e/cymem-2.0.13-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:891fd9030293a8b652dc7fb9fdc79a910a6c76fc679cd775e6741b819ffea476", size = 43478, upload-time = "2025-11-14T14:57:42.682Z" }, + { url = "https://files.pythonhosted.org/packages/bf/a0/8fc929cc29ae466b7b4efc23ece99cbd3ea34992ccff319089c624d667fd/cymem-2.0.13-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:89c4889bd16513ce1644ccfe1e7c473ba7ca150f0621e66feac3a571bde09e7e", size = 42695, upload-time = "2025-11-14T14:57:43.741Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b3/deeb01354ebaf384438083ffe0310209ef903db3e7ba5a8f584b06d28387/cymem-2.0.13-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:45dcaba0f48bef9cc3d8b0b92058640244a95a9f12542210b51318da97c2cf28", size = 250573, upload-time = "2025-11-14T14:57:44.81Z" }, + { url = "https://files.pythonhosted.org/packages/36/36/bc980b9a14409f3356309c45a8d88d58797d02002a9d794dd6c84e809d3a/cymem-2.0.13-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e96848faaafccc0abd631f1c5fb194eac0caee4f5a8777fdbb3e349d3a21741c", size = 254572, upload-time = "2025-11-14T14:57:46.023Z" }, + { url = "https://files.pythonhosted.org/packages/fd/dd/a12522952624685bd0f8968e26d2ed6d059c967413ce6eb52292f538f1b0/cymem-2.0.13-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e02d3e2c3bfeb21185d5a4a70790d9df40629a87d8d7617dc22b4e864f665fa3", size = 248060, upload-time = "2025-11-14T14:57:47.605Z" }, + { url = "https://files.pythonhosted.org/packages/08/11/5dc933ddfeb2dfea747a0b935cb965b9a7580b324d96fc5f5a1b5ff8df29/cymem-2.0.13-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fece5229fd5ecdcd7a0738affb8c59890e13073ae5626544e13825f26c019d3c", size = 254601, upload-time = "2025-11-14T14:57:48.861Z" }, + { url = "https://files.pythonhosted.org/packages/70/66/d23b06166864fa94e13a98e5922986ce774832936473578febce64448d75/cymem-2.0.13-cp313-cp313-win_amd64.whl", hash = "sha256:38aefeb269597c1a0c2ddf1567dd8605489b661fa0369c6406c1acd433b4c7ba", size = 40103, upload-time = "2025-11-14T14:57:50.396Z" }, + { url = "https://files.pythonhosted.org/packages/2f/9e/c7b21271ab88a21760f3afdec84d2bc09ffa9e6c8d774ad9d4f1afab0416/cymem-2.0.13-cp313-cp313-win_arm64.whl", hash = "sha256:717270dcfd8c8096b479c42708b151002ff98e434a7b6f1f916387a6c791e2ad", size = 36016, upload-time = "2025-11-14T14:57:51.611Z" }, + { url = "https://files.pythonhosted.org/packages/7f/28/d3b03427edc04ae04910edf1c24b993881c3ba93a9729a42bcbb816a1808/cymem-2.0.13-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7e1a863a7f144ffb345397813701509cfc74fc9ed360a4d92799805b4b865dd1", size = 46429, upload-time = "2025-11-14T14:57:52.582Z" }, + { url = "https://files.pythonhosted.org/packages/35/a9/7ed53e481f47ebfb922b0b42e980cec83e98ccb2137dc597ea156642440c/cymem-2.0.13-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c16cb80efc017b054f78998c6b4b013cef509c7b3d802707ce1f85a1d68361bf", size = 46205, upload-time = "2025-11-14T14:57:53.64Z" }, + { url = "https://files.pythonhosted.org/packages/61/39/a3d6ad073cf7f0fbbb8bbf09698c3c8fac11be3f791d710239a4e8dd3438/cymem-2.0.13-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0d78a27c88b26c89bd1ece247d1d5939dba05a1dae6305aad8fd8056b17ddb51", size = 296083, upload-time = "2025-11-14T14:57:55.922Z" }, + { url = "https://files.pythonhosted.org/packages/36/0c/20697c8bc19f624a595833e566f37d7bcb9167b0ce69de896eba7cfc9c2d/cymem-2.0.13-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6d36710760f817194dacb09d9fc45cb6a5062ed75e85f0ef7ad7aeeb13d80cc3", size = 286159, upload-time = "2025-11-14T14:57:57.106Z" }, + { url = "https://files.pythonhosted.org/packages/82/d4/9326e3422d1c2d2b4a8fb859bdcce80138f6ab721ddafa4cba328a505c71/cymem-2.0.13-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c8f30971cadd5dcf73bcfbbc5849b1f1e1f40db8cd846c4aa7d3b5e035c7b583", size = 288186, upload-time = "2025-11-14T14:57:58.334Z" }, + { url = "https://files.pythonhosted.org/packages/ed/bc/68da7dd749b72884dc22e898562f335002d70306069d496376e5ff3b6153/cymem-2.0.13-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9d441d0e45798ec1fd330373bf7ffa6b795f229275f64016b6a193e6e2a51522", size = 290353, upload-time = "2025-11-14T14:58:00.562Z" }, + { url = "https://files.pythonhosted.org/packages/50/23/dbf2ad6ecd19b99b3aab6203b1a06608bbd04a09c522d836b854f2f30f73/cymem-2.0.13-cp313-cp313t-win_amd64.whl", hash = "sha256:d1c950eebb9f0f15e3ef3591313482a5a611d16fc12d545e2018cd607f40f472", size = 44764, upload-time = "2025-11-14T14:58:01.793Z" }, + { url = "https://files.pythonhosted.org/packages/54/3f/35701c13e1fc7b0895198c8b20068c569a841e0daf8e0b14d1dc0816b28f/cymem-2.0.13-cp313-cp313t-win_arm64.whl", hash = "sha256:042e8611ef862c34a97b13241f5d0da86d58aca3cecc45c533496678e75c5a1f", size = 38964, upload-time = "2025-11-14T14:58:02.87Z" }, + { url = "https://files.pythonhosted.org/packages/a7/2e/f0e1596010a9a57fa9ebd124a678c07c5b2092283781ae51e79edcf5cb98/cymem-2.0.13-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d2a4bf67db76c7b6afc33de44fb1c318207c3224a30da02c70901936b5aafdf1", size = 43812, upload-time = "2025-11-14T14:58:04.227Z" }, + { url = "https://files.pythonhosted.org/packages/bc/45/8ccc21df08fcbfa6aa3efeb7efc11a1c81c90e7476e255768bb9c29ba02a/cymem-2.0.13-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:92a2ce50afa5625fb5ce7c9302cee61e23a57ccac52cd0410b4858e572f8614b", size = 42951, upload-time = "2025-11-14T14:58:05.424Z" }, + { url = "https://files.pythonhosted.org/packages/01/8c/fe16531631f051d3d1226fa42e2d76fd2c8d5cfa893ec93baee90c7a9d90/cymem-2.0.13-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bc116a70cc3a5dc3d1684db5268eff9399a0be8603980005e5b889564f1ea42f", size = 249878, upload-time = "2025-11-14T14:58:06.95Z" }, + { url = "https://files.pythonhosted.org/packages/47/4b/39d67b80ffb260457c05fcc545de37d82e9e2dbafc93dd6b64f17e09b933/cymem-2.0.13-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:68489bf0035c4c280614067ab6a82815b01dc9fcd486742a5306fe9f68deb7ef", size = 252571, upload-time = "2025-11-14T14:58:08.232Z" }, + { url = "https://files.pythonhosted.org/packages/53/0e/76f6531f74dfdfe7107899cce93ab063bb7ee086ccd3910522b31f623c08/cymem-2.0.13-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:03cb7bdb55718d5eb6ef0340b1d2430ba1386db30d33e9134d01ba9d6d34d705", size = 248555, upload-time = "2025-11-14T14:58:09.429Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7c/eee56757db81f0aefc2615267677ae145aff74228f529838425057003c0d/cymem-2.0.13-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1710390e7fb2510a8091a1991024d8ae838fd06b02cdfdcd35f006192e3c6b0e", size = 254177, upload-time = "2025-11-14T14:58:10.594Z" }, + { url = "https://files.pythonhosted.org/packages/77/e0/a4b58ec9e53c836dce07ef39837a64a599f4a21a134fc7ca57a3a8f9a4b5/cymem-2.0.13-cp314-cp314-win_amd64.whl", hash = "sha256:ac699c8ec72a3a9de8109bd78821ab22f60b14cf2abccd970b5ff310e14158ed", size = 40853, upload-time = "2025-11-14T14:58:12.116Z" }, + { url = "https://files.pythonhosted.org/packages/61/81/9931d1f83e5aeba175440af0b28f0c2e6f71274a5a7b688bc3e907669388/cymem-2.0.13-cp314-cp314-win_arm64.whl", hash = "sha256:90c2d0c04bcda12cd5cebe9be93ce3af6742ad8da96e1b1907e3f8e00291def1", size = 36970, upload-time = "2025-11-14T14:58:13.114Z" }, + { url = "https://files.pythonhosted.org/packages/b7/ef/af447c2184dec6dec973be14614df8ccb4d16d1c74e0784ab4f02538433c/cymem-2.0.13-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ff036bbc1464993552fd1251b0a83fe102af334b301e3896d7aa05a4999ad042", size = 46804, upload-time = "2025-11-14T14:58:14.113Z" }, + { url = "https://files.pythonhosted.org/packages/8c/95/e10f33a8d4fc17f9b933d451038218437f9326c2abb15a3e7f58ce2a06ec/cymem-2.0.13-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fb8291691ba7ff4e6e000224cc97a744a8d9588418535c9454fd8436911df612", size = 46254, upload-time = "2025-11-14T14:58:15.156Z" }, + { url = "https://files.pythonhosted.org/packages/e7/7a/5efeb2d2ea6ebad2745301ad33a4fa9a8f9a33b66623ee4d9185683007a6/cymem-2.0.13-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d8d06ea59006b1251ad5794bcc00121e148434826090ead0073c7b7fedebe431", size = 296061, upload-time = "2025-11-14T14:58:16.254Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/2a3f65842cc8443c2c0650cf23d525be06c8761ab212e0a095a88627be1b/cymem-2.0.13-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c0046a619ecc845ccb4528b37b63426a0cbcb4f14d7940add3391f59f13701e6", size = 285784, upload-time = "2025-11-14T14:58:17.412Z" }, + { url = "https://files.pythonhosted.org/packages/98/73/dd5f9729398f0108c2e71d942253d0d484d299d08b02e474d7cfc43ed0b0/cymem-2.0.13-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:18ad5b116a82fa3674bc8838bd3792891b428971e2123ae8c0fd3ca472157c5e", size = 288062, upload-time = "2025-11-14T14:58:20.225Z" }, + { url = "https://files.pythonhosted.org/packages/5a/01/ffe51729a8f961a437920560659073e47f575d4627445216c1177ecd4a41/cymem-2.0.13-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:666ce6146bc61b9318aa70d91ce33f126b6344a25cf0b925621baed0c161e9cc", size = 290465, upload-time = "2025-11-14T14:58:21.815Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ac/c9e7d68607f71ef978c81e334ab2898b426944c71950212b1467186f69f9/cymem-2.0.13-cp314-cp314t-win_amd64.whl", hash = "sha256:84c1168c563d9d1e04546cb65e3e54fde2bf814f7c7faf11fc06436598e386d1", size = 46665, upload-time = "2025-11-14T14:58:23.512Z" }, + { url = "https://files.pythonhosted.org/packages/66/66/150e406a2db5535533aa3c946de58f0371f2e412e23f050c704588023e6e/cymem-2.0.13-cp314-cp314t-win_arm64.whl", hash = "sha256:e9027764dc5f1999fb4b4cabee1d0322c59e330c0a6485b436a68275f614277f", size = 39715, upload-time = "2025-11-14T14:58:24.773Z" }, +] + +[[package]] +name = "dateparser" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "regex" }, + { name = "tzlocal" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/2c/668dfb8c073a5dde3efb80fa382de1502e3b14002fd386a8c1b0b49e92a9/dateparser-1.3.0.tar.gz", hash = "sha256:5bccf5d1ec6785e5be71cc7ec80f014575a09b4923e762f850e57443bddbf1a5", size = 337152, upload-time = "2026-02-04T16:00:06.162Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/c7/95349670e193b2891176e1b8e5f43e12b31bff6d9994f70e74ab385047f6/dateparser-1.3.0-py3-none-any.whl", hash = "sha256:8dc678b0a526e103379f02ae44337d424bd366aac727d3c6cf52ce1b01efbb5a", size = 318688, upload-time = "2026-02-04T16:00:04.652Z" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + +[[package]] +name = "dnspython" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, +] + +[[package]] +name = "docstring-parser" +version = "0.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" }, +] + +[[package]] +name = "docutils" +version = "0.22.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, +] + +[[package]] +name = "email-validator" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "fastapi" +version = "0.133.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/6f/0eafed8349eea1fa462238b54a624c8b408cd1ba2795c8e64aa6c34f8ab7/fastapi-0.133.1.tar.gz", hash = "sha256:ed152a45912f102592976fde6cbce7dae1a8a1053da94202e51dd35d184fadd6", size = 378741, upload-time = "2026-02-25T18:18:17.398Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/c9/a175a7779f3599dfa4adfc97a6ce0e157237b3d7941538604aadaf97bfb6/fastapi-0.133.1-py3-none-any.whl", hash = "sha256:658f34ba334605b1617a65adf2ea6461901bdb9af3a3080d63ff791ecf7dc2e2", size = 109029, upload-time = "2026-02-25T18:18:18.578Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "email-validator" }, + { name = "fastapi-cli", extra = ["standard"] }, + { name = "httpx" }, + { name = "jinja2" }, + { name = "pydantic-extra-types" }, + { name = "pydantic-settings" }, + { name = "python-multipart" }, + { name = "uvicorn", extra = ["standard"] }, +] + +[[package]] +name = "fastapi-cli" +version = "0.0.24" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "rich-toolkit" }, + { name = "typer" }, + { name = "uvicorn", extra = ["standard"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6e/58/74797ae9e4610cfa0c6b34c8309096d3b20bb29be3b8b5fbf1004d10fa5f/fastapi_cli-0.0.24.tar.gz", hash = "sha256:1afc9c9e21d7ebc8a3ca5e31790cd8d837742be7e4f8b9236e99cb3451f0de00", size = 19043, upload-time = "2026-02-24T10:45:10.476Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/4b/68f9fe268e535d79c76910519530026a4f994ce07189ac0dded45c6af825/fastapi_cli-0.0.24-py3-none-any.whl", hash = "sha256:4a1f78ed798f106b4fee85ca93b85d8fe33c0a3570f775964d37edb80b8f0edc", size = 12304, upload-time = "2026-02-24T10:45:09.552Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "fastapi-cloud-cli" }, + { name = "uvicorn", extra = ["standard"] }, +] + +[[package]] +name = "fastapi-cloud-cli" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastar" }, + { name = "httpx" }, + { name = "pydantic", extra = ["email"] }, + { name = "rich-toolkit" }, + { name = "rignore" }, + { name = "sentry-sdk" }, + { name = "typer" }, + { name = "uvicorn", extra = ["standard"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2b/eb/e78ebd05a714c62a0578cdce4339cb6cd138421a7d865fbddedd7242420b/fastapi_cloud_cli-0.14.0.tar.gz", hash = "sha256:d3ecb8c942685a71df0af7bd59f463b5eff76f5818b48e5a03c6159726831e68", size = 39822, upload-time = "2026-02-25T14:19:53.535Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/18/7bf922ee0b6a737a9d88cf613182ecd6031f52298da893556f158eba763f/fastapi_cloud_cli-0.14.0-py3-none-any.whl", hash = "sha256:325fcb4b45e661184152da6db861d9fb718739fbcd561a4d334dbe78c026586f", size = 28350, upload-time = "2026-02-25T14:19:52.416Z" }, +] + +[[package]] +name = "fastar" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/69/e7/f89d54fb04104114dd0552836dc2b47914f416cc0e200b409dd04a33de5e/fastar-0.8.0.tar.gz", hash = "sha256:f4d4d68dbf1c4c2808f0e730fac5843493fc849f70fe3ad3af60dfbaf68b9a12", size = 68524, upload-time = "2025-11-26T02:36:00.72Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/f1/5b2ff898abac7f1a418284aad285e3a4f68d189c572ab2db0f6c9079dd16/fastar-0.8.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0f10d2adfe40f47ff228f4efaa32d409d732ded98580e03ed37c9535b5fc923d", size = 706369, upload-time = "2025-11-26T02:34:37.783Z" }, + { url = "https://files.pythonhosted.org/packages/23/60/8046a386dca39154f80c927cbbeeb4b1c1267a3271bffe61552eb9995757/fastar-0.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b930da9d598e3bc69513d131f397e6d6be4643926ef3de5d33d1e826631eb036", size = 629097, upload-time = "2025-11-26T02:34:21.888Z" }, + { url = "https://files.pythonhosted.org/packages/22/7e/1ae005addc789924a9268da2394d3bb5c6f96836f7e37b7e3d23c2362675/fastar-0.8.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9d210da2de733ca801de83e931012349d209f38b92d9630ccaa94bd445bdc9b8", size = 868938, upload-time = "2025-11-26T02:33:51.119Z" }, + { url = "https://files.pythonhosted.org/packages/a6/77/290a892b073b84bf82e6b2259708dfe79c54f356e252c2dd40180b16fe07/fastar-0.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa02270721517078a5bd61a38719070ac2537a4aa6b6c48cf369cf2abc59174a", size = 765204, upload-time = "2025-11-26T02:32:47.02Z" }, + { url = "https://files.pythonhosted.org/packages/d0/00/c3155171b976003af3281f5258189f1935b15d1221bfc7467b478c631216/fastar-0.8.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:83c391e5b789a720e4d0029b9559f5d6dee3226693c5b39c0eab8eaece997e0f", size = 764717, upload-time = "2025-11-26T02:33:02.453Z" }, + { url = "https://files.pythonhosted.org/packages/b7/43/405b7ad76207b2c11b7b59335b70eac19e4a2653977f5588a1ac8fed54f4/fastar-0.8.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3258d7a78a72793cdd081545da61cabe85b1f37634a1d0b97ffee0ff11d105ef", size = 931502, upload-time = "2025-11-26T02:33:18.619Z" }, + { url = "https://files.pythonhosted.org/packages/da/8a/a3dde6d37cc3da4453f2845cdf16675b5686b73b164f37e2cc579b057c2c/fastar-0.8.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e6eab95dd985cdb6a50666cbeb9e4814676e59cfe52039c880b69d67cfd44767", size = 821454, upload-time = "2025-11-26T02:33:33.427Z" }, + { url = "https://files.pythonhosted.org/packages/da/c1/904fe2468609c8990dce9fe654df3fbc7324a8d8e80d8240ae2c89757064/fastar-0.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:829b1854166141860887273c116c94e31357213fa8e9fe8baeb18bd6c38aa8d9", size = 821647, upload-time = "2025-11-26T02:34:07Z" }, + { url = "https://files.pythonhosted.org/packages/c8/73/a0642ab7a400bc07528091785e868ace598fde06fcd139b8f865ec1b6f3c/fastar-0.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b1667eae13f9457a3c737f4376d68e8c3e548353538b28f7e4273a30cb3965cd", size = 986342, upload-time = "2025-11-26T02:34:53.371Z" }, + { url = "https://files.pythonhosted.org/packages/af/af/60c1bfa6edab72366461a95f053d0f5f7ab1825fe65ca2ca367432cd8629/fastar-0.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b864a95229a7db0814cd9ef7987cb713fd43dce1b0d809dd17d9cd6f02fdde3e", size = 1040207, upload-time = "2025-11-26T02:35:10.65Z" }, + { url = "https://files.pythonhosted.org/packages/f6/a0/0d624290dec622e7fa084b6881f456809f68777d54a314f5dde932714506/fastar-0.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c05fbc5618ce17675a42576fa49858d79734627f0a0c74c0875ab45ee8de340c", size = 1045031, upload-time = "2025-11-26T02:35:28.108Z" }, + { url = "https://files.pythonhosted.org/packages/a7/74/cf663af53c4706ba88e6b4af44a6b0c3bd7d7ca09f079dc40647a8f06585/fastar-0.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7f41c51ee96f338662ee3c3df4840511ba3f9969606840f1b10b7cb633a3c716", size = 994877, upload-time = "2025-11-26T02:35:45.797Z" }, + { url = "https://files.pythonhosted.org/packages/52/17/444c8be6e77206050e350da7c338102b6cab384be937fa0b1d6d1f9ede73/fastar-0.8.0-cp312-cp312-win32.whl", hash = "sha256:d949a1a2ea7968b734632c009df0571c94636a5e1622c87a6e2bf712a7334f47", size = 455996, upload-time = "2025-11-26T02:36:26.938Z" }, + { url = "https://files.pythonhosted.org/packages/dc/34/fc3b5e56d71a17b1904800003d9251716e8fd65f662e1b10a26881698a74/fastar-0.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:fc645994d5b927d769121094e8a649b09923b3c13a8b0b98696d8f853f23c532", size = 490429, upload-time = "2025-11-26T02:36:12.707Z" }, + { url = "https://files.pythonhosted.org/packages/35/a8/5608cc837417107c594e2e7be850b9365bcb05e99645966a5d6a156285fe/fastar-0.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:d81ee82e8dc78a0adb81728383bd39611177d642a8fa2d601d4ad5ad59e5f3bd", size = 461297, upload-time = "2025-11-26T02:36:03.546Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a5/79ecba3646e22d03eef1a66fb7fc156567213e2e4ab9faab3bbd4489e483/fastar-0.8.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:a3253a06845462ca2196024c7a18f5c0ba4de1532ab1c4bad23a40b332a06a6a", size = 706112, upload-time = "2025-11-26T02:34:39.237Z" }, + { url = "https://files.pythonhosted.org/packages/0a/03/4f883bce878218a8676c2d7ca09b50c856a5470bb3b7f63baf9521ea6995/fastar-0.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5cbeb3ebfa0980c68ff8b126295cc6b208ccd81b638aebc5a723d810a7a0e5d2", size = 628954, upload-time = "2025-11-26T02:34:23.705Z" }, + { url = "https://files.pythonhosted.org/packages/4f/f1/892e471f156b03d10ba48ace9384f5a896702a54506137462545f38e40b8/fastar-0.8.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1c0d5956b917daac77d333d48b3f0f3ff927b8039d5b32d8125462782369f761", size = 868685, upload-time = "2025-11-26T02:33:53.077Z" }, + { url = "https://files.pythonhosted.org/packages/39/ba/e24915045852e30014ec6840446975c03f4234d1c9270394b51d3ad18394/fastar-0.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27b404db2b786b65912927ce7f3790964a4bcbde42cdd13091b82a89cd655e1c", size = 765044, upload-time = "2025-11-26T02:32:48.187Z" }, + { url = "https://files.pythonhosted.org/packages/14/2c/1aa11ac21a99984864c2fca4994e094319ff3a2046e7a0343c39317bd5b9/fastar-0.8.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0902fc89dcf1e7f07b8563032a4159fe2b835e4c16942c76fd63451d0e5f76a3", size = 764322, upload-time = "2025-11-26T02:33:03.859Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f0/4b91902af39fe2d3bae7c85c6d789586b9fbcf618d7fdb3d37323915906d/fastar-0.8.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:069347e2f0f7a8b99bbac8cd1bc0e06c7b4a31dc964fc60d84b95eab3d869dc1", size = 931016, upload-time = "2025-11-26T02:33:19.902Z" }, + { url = "https://files.pythonhosted.org/packages/c9/97/8fc43a5a9c0a2dc195730f6f7a0f367d171282cd8be2511d0e87c6d2dad0/fastar-0.8.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7fd135306f6bfe9a835918280e0eb440b70ab303e0187d90ab51ca86e143f70d", size = 821308, upload-time = "2025-11-26T02:33:34.664Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e9/058615b63a7fd27965e8c5966f393ed0c169f7ff5012e1674f21684de3ba/fastar-0.8.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78d06d6897f43c27154b5f2d0eb930a43a81b7eec73f6f0b0114814d4a10ab38", size = 821171, upload-time = "2025-11-26T02:34:08.498Z" }, + { url = "https://files.pythonhosted.org/packages/ca/cf/69e16a17961570a755c37ffb5b5aa7610d2e77807625f537989da66f2a9d/fastar-0.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a922f8439231fa0c32b15e8d70ff6d415619b9d40492029dabbc14a0c53b5f18", size = 986227, upload-time = "2025-11-26T02:34:55.06Z" }, + { url = "https://files.pythonhosted.org/packages/fb/83/2100192372e59b56f4ace37d7d9cabda511afd71b5febad1643d1c334271/fastar-0.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a739abd51eb766384b4caff83050888e80cd75bbcfec61e6d1e64875f94e4a40", size = 1039395, upload-time = "2025-11-26T02:35:12.166Z" }, + { url = "https://files.pythonhosted.org/packages/75/15/cdd03aca972f55872efbb7cf7540c3fa7b97a75d626303a3ea46932163dc/fastar-0.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5a65f419d808b23ac89d5cd1b13a2f340f15bc5d1d9af79f39fdb77bba48ff1b", size = 1044766, upload-time = "2025-11-26T02:35:29.62Z" }, + { url = "https://files.pythonhosted.org/packages/3d/29/945e69e4e2652329ace545999334ec31f1431fbae3abb0105587e11af2ae/fastar-0.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7bb2ae6c0cce58f0db1c9f20495e7557cca2c1ee9c69bbd90eafd54f139171c5", size = 994740, upload-time = "2025-11-26T02:35:47.887Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5d/dbfe28f8cd1eb484bba0c62e5259b2cf6fea229d6ef43e05c06b5a78c034/fastar-0.8.0-cp313-cp313-win32.whl", hash = "sha256:b28753e0d18a643272597cb16d39f1053842aa43131ad3e260c03a2417d38401", size = 455990, upload-time = "2025-11-26T02:36:28.502Z" }, + { url = "https://files.pythonhosted.org/packages/e1/01/e965740bd36e60ef4c5aa2cbe42b6c4eb1dc3551009238a97c2e5e96bd23/fastar-0.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:620e5d737dce8321d49a5ebb7997f1fd0047cde3512082c27dc66d6ac8c1927a", size = 490227, upload-time = "2025-11-26T02:36:14.363Z" }, + { url = "https://files.pythonhosted.org/packages/dd/10/c99202719b83e5249f26902ae53a05aea67d840eeb242019322f20fc171c/fastar-0.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:c4c4bd08df563120cd33e854fe0a93b81579e8571b11f9b7da9e84c37da2d6b6", size = 461078, upload-time = "2025-11-26T02:36:04.94Z" }, + { url = "https://files.pythonhosted.org/packages/96/4a/9573b87a0ef07580ed111e7230259aec31bb33ca3667963ebee77022ec61/fastar-0.8.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:50b36ce654ba44b0e13fae607ae17ee6e1597b69f71df1bee64bb8328d881dfc", size = 706041, upload-time = "2025-11-26T02:34:40.638Z" }, + { url = "https://files.pythonhosted.org/packages/4a/19/f95444a1d4f375333af49300aa75ee93afa3335c0e40fda528e460ed859c/fastar-0.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:63a892762683d7ab00df0227d5ea9677c62ff2cde9b875e666c0be569ed940f3", size = 628617, upload-time = "2025-11-26T02:34:24.893Z" }, + { url = "https://files.pythonhosted.org/packages/b3/c9/b51481b38b7e3f16ef2b9e233b1a3623386c939d745d6e41bbd389eaae30/fastar-0.8.0-cp314-cp314-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4ae6a145c1bff592644bde13f2115e0239f4b7babaf506d14e7d208483cf01a5", size = 869299, upload-time = "2025-11-26T02:33:54.274Z" }, + { url = "https://files.pythonhosted.org/packages/bf/02/3ba1267ee5ba7314e29c431cf82eaa68586f2c40cdfa08be3632b7d07619/fastar-0.8.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ae0ff7c0a1c7e1428404b81faee8aebef466bfd0be25bfe4dabf5d535c68741", size = 764667, upload-time = "2025-11-26T02:32:49.606Z" }, + { url = "https://files.pythonhosted.org/packages/1b/84/bf33530fd015b5d7c2cc69e0bce4a38d736754a6955487005aab1af6adcd/fastar-0.8.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dbfd87dbd217b45c898b2dbcd0169aae534b2c1c5cbe3119510881f6a5ac8ef5", size = 763993, upload-time = "2025-11-26T02:33:05.782Z" }, + { url = "https://files.pythonhosted.org/packages/da/e0/9564d24e7cea6321a8d921c6d2a457044a476ef197aa4708e179d3d97f0d/fastar-0.8.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a5abd99fcba83ef28c8fe6ae2927edc79053db43a0457a962ed85c9bf150d37", size = 930153, upload-time = "2025-11-26T02:33:21.53Z" }, + { url = "https://files.pythonhosted.org/packages/35/b1/6f57fcd8d6e192cfebf97e58eb27751640ad93784c857b79039e84387b51/fastar-0.8.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:91d4c685620c3a9d6b5ae091dbabab4f98b20049b7ecc7976e19cc9016c0d5d6", size = 821177, upload-time = "2025-11-26T02:33:35.839Z" }, + { url = "https://files.pythonhosted.org/packages/b3/78/9e004ea9f3aa7466f5ddb6f9518780e1d2f0ed3ca55f093632982598bace/fastar-0.8.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f77c2f2cad76e9dc7b6701297adb1eba87d0485944b416fc2ccf5516c01219a3", size = 820652, upload-time = "2025-11-26T02:34:09.776Z" }, + { url = "https://files.pythonhosted.org/packages/42/95/b604ed536544005c9f1aee7c4c74b00150db3d8d535cd8232dc20f947063/fastar-0.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e7f07c4a3dada7757a8fc430a5b4a29e6ef696d2212747213f57086ffd970316", size = 985961, upload-time = "2025-11-26T02:34:56.401Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7b/fa9d4d96a5d494bdb8699363bb9de8178c0c21a02e1d89cd6f913d127018/fastar-0.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:90c0c3fe55105c0aed8a83135dbdeb31e683455dbd326a1c48fa44c378b85616", size = 1039316, upload-time = "2025-11-26T02:35:13.807Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f9/8462789243bc3f33e8401378ec6d54de4e20cfa60c96a0e15e3e9d1389bb/fastar-0.8.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:fb9ee51e5bffe0dab3d3126d3a4fac8d8f7235cedcb4b8e74936087ce1c157f3", size = 1045028, upload-time = "2025-11-26T02:35:31.079Z" }, + { url = "https://files.pythonhosted.org/packages/a5/71/9abb128777e616127194b509e98fcda3db797d76288c1a8c23dd22afc14f/fastar-0.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e380b1e8d30317f52406c43b11e98d11e1d68723bbd031e18049ea3497b59a6d", size = 994677, upload-time = "2025-11-26T02:35:49.391Z" }, + { url = "https://files.pythonhosted.org/packages/de/c1/b81b3f194853d7ad232a67a1d768f5f51a016f165cfb56cb31b31bbc6177/fastar-0.8.0-cp314-cp314-win32.whl", hash = "sha256:1c4ffc06e9c4a8ca498c07e094670d8d8c0d25b17ca6465b9774da44ea997ab1", size = 456687, upload-time = "2025-11-26T02:36:30.205Z" }, + { url = "https://files.pythonhosted.org/packages/cb/87/9e0cd4768a98181d56f0cdbab2363404cc15deb93f4aad3b99cd2761bbaa/fastar-0.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:5517a8ad4726267c57a3e0e2a44430b782e00b230bf51c55b5728e758bb3a692", size = 490578, upload-time = "2025-11-26T02:36:16.218Z" }, + { url = "https://files.pythonhosted.org/packages/aa/1e/580a76cf91847654f2ad6520e956e93218f778540975bc4190d363f709e2/fastar-0.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:58030551046ff4a8616931e52a36c83545ff05996db5beb6e0cd2b7e748aa309", size = 461473, upload-time = "2025-11-26T02:36:06.373Z" }, + { url = "https://files.pythonhosted.org/packages/58/4c/bdb5c6efe934f68708529c8c9d4055ebef5c4be370621966438f658b29bd/fastar-0.8.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:1e7d29b6bfecb29db126a08baf3c04a5ab667f6cea2b7067d3e623a67729c4a6", size = 705570, upload-time = "2025-11-26T02:34:42.01Z" }, + { url = "https://files.pythonhosted.org/packages/6d/78/f01ac7e71d5a37621bd13598a26e948a12b85ca8042f7ee1a0a8c9f59cda/fastar-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:05eb7b96940f9526b485f1d0b02393839f0f61cac4b1f60024984f8b326d2640", size = 627761, upload-time = "2025-11-26T02:34:26.152Z" }, + { url = "https://files.pythonhosted.org/packages/06/45/6df0ecda86ea9d2e95053c1a655d153dee55fc121b6e13ea6d1e246a50b6/fastar-0.8.0-cp314-cp314t-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:619352d8ac011794e2345c462189dc02ba634750d23cd9d86a9267dd71b1f278", size = 869414, upload-time = "2025-11-26T02:33:55.618Z" }, + { url = "https://files.pythonhosted.org/packages/b2/72/486421f5a8c0c377cc82e7a50c8a8ea899a6ec2aa72bde8f09fb667a2dc8/fastar-0.8.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74ebfecef3fe6d7a90355fac1402fd30636988332a1d33f3e80019a10782bb24", size = 763863, upload-time = "2025-11-26T02:32:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/d4/64/39f654dbb41a3867fb1f2c8081c014d8f1d32ea10585d84cacbef0b32995/fastar-0.8.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2975aca5a639e26a3ab0d23b4b0628d6dd6d521146c3c11486d782be621a35aa", size = 763065, upload-time = "2025-11-26T02:33:07.274Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bd/c011a34fb3534c4c3301f7c87c4ffd7e47f6113c904c092ddc8a59a303ea/fastar-0.8.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:afc438eaed8ff0dcdd9308268be5cb38c1db7e94c3ccca7c498ca13a4a4535a3", size = 930530, upload-time = "2025-11-26T02:33:23.117Z" }, + { url = "https://files.pythonhosted.org/packages/55/9d/aa6e887a7033c571b1064429222bbe09adc9a3c1e04f3d1788ba5838ebd5/fastar-0.8.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6ced0a5399cc0a84a858ef0a31ca2d0c24d3bbec4bcda506a9192d8119f3590a", size = 820572, upload-time = "2025-11-26T02:33:37.542Z" }, + { url = "https://files.pythonhosted.org/packages/ad/9c/7a3a2278a1052e1a5d98646de7c095a00cffd2492b3b84ce730e2f1cd93a/fastar-0.8.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec9b23da8c4c039da3fe2e358973c66976a0c8508aa06d6626b4403cb5666c19", size = 820649, upload-time = "2025-11-26T02:34:11.108Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/d38edc1f4438cd047e56137c26d94783ffade42e1b3bde620ccf17b771ef/fastar-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:dfba078fcd53478032fd0ceed56960ec6b7ff0511cfc013a8a3a4307e3a7bac4", size = 985653, upload-time = "2025-11-26T02:34:57.884Z" }, + { url = "https://files.pythonhosted.org/packages/69/d9/2147d0c19757e165cd62d41cec3f7b38fad2ad68ab784978b5f81716c7ea/fastar-0.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:ade56c94c14be356d295fecb47a3fcd473dd43a8803ead2e2b5b9e58feb6dcfa", size = 1038140, upload-time = "2025-11-26T02:35:15.778Z" }, + { url = "https://files.pythonhosted.org/packages/7f/1d/ec4c717ffb8a308871e9602ec3197d957e238dc0227127ac573ec9bca952/fastar-0.8.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e48d938f9366db5e59441728f70b7f6c1ccfab7eff84f96f9b7e689b07786c52", size = 1045195, upload-time = "2025-11-26T02:35:32.865Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/637334dc8c8f3bb391388b064ae13f0ad9402bc5a6c3e77b8887d0c31921/fastar-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:79c441dc1482ff51a54fb3f57ae6f7bb3d2cff88fa2cc5d196c519f8aab64a56", size = 994686, upload-time = "2025-11-26T02:35:51.392Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e2/dfa19a4b260b8ab3581b7484dcb80c09b25324f4daa6b6ae1c7640d1607a/fastar-0.8.0-cp314-cp314t-win32.whl", hash = "sha256:187f61dc739afe45ac8e47ed7fd1adc45d52eac110cf27d579155720507d6fbe", size = 455767, upload-time = "2025-11-26T02:36:34.758Z" }, + { url = "https://files.pythonhosted.org/packages/51/47/df65c72afc1297797b255f90c4778b5d6f1f0f80282a134d5ab610310ed9/fastar-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:40e9d763cf8bf85ce2fa256e010aa795c0fe3d3bd1326d5c3084e6ce7857127e", size = 489971, upload-time = "2025-11-26T02:36:22.081Z" }, + { url = "https://files.pythonhosted.org/packages/85/11/0aa8455af26f0ae89e42be67f3a874255ee5d7f0f026fc86e8d56f76b428/fastar-0.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:e59673307b6a08210987059a2bdea2614fe26e3335d0e5d1a3d95f49a05b1418", size = 460467, upload-time = "2025-11-26T02:36:07.978Z" }, +] + +[[package]] +name = "fastembed" +version = "0.7.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "loguru" }, + { name = "mmh3" }, + { name = "numpy" }, + { name = "onnxruntime" }, + { name = "pillow" }, + { name = "py-rust-stemmers" }, + { name = "requests" }, + { name = "tokenizers" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4c/c2/9c708680de1b54480161e0505f9d6d3d8eb47a1dc1a1f7f3c5106ba355d2/fastembed-0.7.4.tar.gz", hash = "sha256:8b8a4ea860ca295002f4754e8f5820a636e1065a9444959e18d5988d7f27093b", size = 68807, upload-time = "2025-12-05T12:08:10.447Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/3b/8da01492bc8b69184257d0c951bf0e77aec8ce110f06d8ce16c6ed9084f7/fastembed-0.7.4-py3-none-any.whl", hash = "sha256:79250a775f70bd6addb0e054204df042b5029ecae501e40e5bbd08e75844ad83", size = 108491, upload-time = "2025-12-05T12:08:09.059Z" }, +] + +[[package]] +name = "fastmcp" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "authlib" }, + { name = "cyclopts" }, + { name = "exceptiongroup" }, + { name = "httpx" }, + { name = "jsonref" }, + { name = "jsonschema-path" }, + { name = "mcp" }, + { name = "openapi-pydantic" }, + { name = "opentelemetry-api" }, + { name = "packaging" }, + { name = "platformdirs" }, + { name = "py-key-value-aio", extra = ["filetree", "keyring", "memory"] }, + { name = "pydantic", extra = ["email"] }, + { name = "pyperclip" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "rich" }, + { name = "uvicorn" }, + { name = "watchfiles" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/11/6b/1a7ec89727797fb07ec0928e9070fa2f45e7b35718e1fe01633a34c35e45/fastmcp-3.0.2.tar.gz", hash = "sha256:6bd73b4a3bab773ee6932df5249dcbcd78ed18365ed0aeeb97bb42702a7198d7", size = 17239351, upload-time = "2026-02-22T16:32:28.843Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/5a/f410a9015cfde71adf646dab4ef2feae49f92f34f6050fcfb265eb126b30/fastmcp-3.0.2-py3-none-any.whl", hash = "sha256:f513d80d4b30b54749fe8950116b1aab843f3c293f5cb971fc8665cb48dbb028", size = 606268, upload-time = "2026-02-22T16:32:30.992Z" }, +] + +[[package]] +name = "filelock" +version = "3.24.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/73/92/a8e2479937ff39185d20dd6a851c1a63e55849e447a55e798cc2e1f49c65/filelock-3.24.3.tar.gz", hash = "sha256:011a5644dc937c22699943ebbfc46e969cdde3e171470a6e40b9533e5a72affa", size = 37935, upload-time = "2026-02-19T00:48:20.543Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/0f/5d0c71a1aefeb08efff26272149e07ab922b64f46c63363756224bd6872e/filelock-3.24.3-py3-none-any.whl", hash = "sha256:426e9a4660391f7f8a810d71b0555bce9008b0a1cc342ab1f6947d37639e002d", size = 24331, upload-time = "2026-02-19T00:48:18.465Z" }, +] + +[[package]] +name = "flatbuffers" +version = "25.12.19" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl", hash = "sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4", size = 26661, upload-time = "2025-12-19T23:16:13.622Z" }, +] + +[[package]] +name = "fsspec" +version = "2026.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/7c/f60c259dcbf4f0c47cc4ddb8f7720d2dcdc8888c8e5ad84c73ea4531cc5b/fsspec-2026.2.0.tar.gz", hash = "sha256:6544e34b16869f5aacd5b90bdf1a71acb37792ea3ddf6125ee69a22a53fb8bff", size = 313441, upload-time = "2026-02-05T21:50:53.743Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl", hash = "sha256:98de475b5cb3bd66bedd5c4679e87b4fdfe1a3bf4d707b151b3c07e58c9a2437", size = 202505, upload-time = "2026-02-05T21:50:51.819Z" }, +] + +[[package]] +name = "genai-prices" +version = "0.0.54" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8e/20/d64d04c7870db01232f8f8e36dec2072281b1f594b924200aa017778eec2/genai_prices-0.0.54.tar.gz", hash = "sha256:9d985affc19d055be16613b0c5e49d182d4c2164cf1587129f9996dfb9a3cb8d", size = 59588, upload-time = "2026-02-17T20:26:06.849Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/71/d2a941b0ca01186912fedb096d8eef7b3e1680c86fdcf8fe3dc84e76d5a9/genai_prices-0.0.54-py3-none-any.whl", hash = "sha256:5b45012b2981b7d4d42c49c8614ee95420fec244c87542542045786b36fc2235", size = 62198, upload-time = "2026-02-17T20:26:05.186Z" }, +] + +[[package]] +name = "greenlet" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/51/1664f6b78fc6ebbd98019a1fd730e83fa78f2db7058f72b1463d3612b8db/greenlet-3.3.2.tar.gz", hash = "sha256:2eaf067fc6d886931c7962e8c6bede15d2f01965560f3359b27c80bde2d151f2", size = 188267, upload-time = "2026-02-20T20:54:15.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/ab/1608e5a7578e62113506740b88066bf09888322a311cff602105e619bd87/greenlet-3.3.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ac8d61d4343b799d1e526db579833d72f23759c71e07181c2d2944e429eb09cd", size = 280358, upload-time = "2026-02-20T20:17:43.971Z" }, + { url = "https://files.pythonhosted.org/packages/a5/23/0eae412a4ade4e6623ff7626e38998cb9b11e9ff1ebacaa021e4e108ec15/greenlet-3.3.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ceec72030dae6ac0c8ed7591b96b70410a8be370b6a477b1dbc072856ad02bd", size = 601217, upload-time = "2026-02-20T20:47:31.462Z" }, + { url = "https://files.pythonhosted.org/packages/f8/16/5b1678a9c07098ecb9ab2dd159fafaf12e963293e61ee8d10ecb55273e5e/greenlet-3.3.2-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2a5be83a45ce6188c045bcc44b0ee037d6a518978de9a5d97438548b953a1ac", size = 611792, upload-time = "2026-02-20T20:55:58.423Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c5/cc09412a29e43406eba18d61c70baa936e299bc27e074e2be3806ed29098/greenlet-3.3.2-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ae9e21c84035c490506c17002f5c8ab25f980205c3e61ddb3a2a2a2e6c411fcb", size = 626250, upload-time = "2026-02-20T21:02:46.596Z" }, + { url = "https://files.pythonhosted.org/packages/50/1f/5155f55bd71cabd03765a4aac9ac446be129895271f73872c36ebd4b04b6/greenlet-3.3.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e99d1749147ac21dde49b99c9abffcbc1e2d55c67501465ef0930d6e78e070", size = 613875, upload-time = "2026-02-20T20:21:01.102Z" }, + { url = "https://files.pythonhosted.org/packages/fc/dd/845f249c3fcd69e32df80cdab059b4be8b766ef5830a3d0aa9d6cad55beb/greenlet-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c956a19350e2c37f2c48b336a3afb4bff120b36076d9d7fb68cb44e05d95b79", size = 1571467, upload-time = "2026-02-20T20:49:33.495Z" }, + { url = "https://files.pythonhosted.org/packages/2a/50/2649fe21fcc2b56659a452868e695634722a6655ba245d9f77f5656010bf/greenlet-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c6f8ba97d17a1e7d664151284cb3315fc5f8353e75221ed4324f84eb162b395", size = 1640001, upload-time = "2026-02-20T20:21:09.154Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/cc802e067d02af8b60b6771cea7d57e21ef5e6659912814babb42b864713/greenlet-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:34308836d8370bddadb41f5a7ce96879b72e2fdfb4e87729330c6ab52376409f", size = 231081, upload-time = "2026-02-20T20:17:28.121Z" }, + { url = "https://files.pythonhosted.org/packages/58/2e/fe7f36ff1982d6b10a60d5e0740c759259a7d6d2e1dc41da6d96de32fff6/greenlet-3.3.2-cp312-cp312-win_arm64.whl", hash = "sha256:d3a62fa76a32b462a97198e4c9e99afb9ab375115e74e9a83ce180e7a496f643", size = 230331, upload-time = "2026-02-20T20:17:23.34Z" }, + { url = "https://files.pythonhosted.org/packages/ac/48/f8b875fa7dea7dd9b33245e37f065af59df6a25af2f9561efa8d822fde51/greenlet-3.3.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:aa6ac98bdfd716a749b84d4034486863fd81c3abde9aa3cf8eff9127981a4ae4", size = 279120, upload-time = "2026-02-20T20:19:01.9Z" }, + { url = "https://files.pythonhosted.org/packages/49/8d/9771d03e7a8b1ee456511961e1b97a6d77ae1dea4a34a5b98eee706689d3/greenlet-3.3.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab0c7e7901a00bc0a7284907273dc165b32e0d109a6713babd04471327ff7986", size = 603238, upload-time = "2026-02-20T20:47:32.873Z" }, + { url = "https://files.pythonhosted.org/packages/59/0e/4223c2bbb63cd5c97f28ffb2a8aee71bdfb30b323c35d409450f51b91e3e/greenlet-3.3.2-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d248d8c23c67d2291ffd47af766e2a3aa9fa1c6703155c099feb11f526c63a92", size = 614219, upload-time = "2026-02-20T20:55:59.817Z" }, + { url = "https://files.pythonhosted.org/packages/94/2b/4d012a69759ac9d77210b8bfb128bc621125f5b20fc398bce3940d036b1c/greenlet-3.3.2-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ccd21bb86944ca9be6d967cf7691e658e43417782bce90b5d2faeda0ff78a7dd", size = 628268, upload-time = "2026-02-20T21:02:48.024Z" }, + { url = "https://files.pythonhosted.org/packages/7a/34/259b28ea7a2a0c904b11cd36c79b8cef8019b26ee5dbe24e73b469dea347/greenlet-3.3.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6997d360a4e6a4e936c0f9625b1c20416b8a0ea18a8e19cabbefc712e7397ab", size = 616774, upload-time = "2026-02-20T20:21:02.454Z" }, + { url = "https://files.pythonhosted.org/packages/0a/03/996c2d1689d486a6e199cb0f1cf9e4aa940c500e01bdf201299d7d61fa69/greenlet-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64970c33a50551c7c50491671265d8954046cb6e8e2999aacdd60e439b70418a", size = 1571277, upload-time = "2026-02-20T20:49:34.795Z" }, + { url = "https://files.pythonhosted.org/packages/d9/c4/2570fc07f34a39f2caf0bf9f24b0a1a0a47bc2e8e465b2c2424821389dfc/greenlet-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1a9172f5bf6bd88e6ba5a84e0a68afeac9dc7b6b412b245dd64f52d83c81e55b", size = 1640455, upload-time = "2026-02-20T20:21:10.261Z" }, + { url = "https://files.pythonhosted.org/packages/91/39/5ef5aa23bc545aa0d31e1b9b55822b32c8da93ba657295840b6b34124009/greenlet-3.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:a7945dd0eab63ded0a48e4dcade82939783c172290a7903ebde9e184333ca124", size = 230961, upload-time = "2026-02-20T20:16:58.461Z" }, + { url = "https://files.pythonhosted.org/packages/62/6b/a89f8456dcb06becff288f563618e9f20deed8dd29beea14f9a168aef64b/greenlet-3.3.2-cp313-cp313-win_arm64.whl", hash = "sha256:394ead29063ee3515b4e775216cb756b2e3b4a7e55ae8fd884f17fa579e6b327", size = 230221, upload-time = "2026-02-20T20:17:37.152Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ae/8bffcbd373b57a5992cd077cbe8858fff39110480a9d50697091faea6f39/greenlet-3.3.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8d1658d7291f9859beed69a776c10822a0a799bc4bfe1bd4272bb60e62507dab", size = 279650, upload-time = "2026-02-20T20:18:00.783Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c0/45f93f348fa49abf32ac8439938726c480bd96b2a3c6f4d949ec0124b69f/greenlet-3.3.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18cb1b7337bca281915b3c5d5ae19f4e76d35e1df80f4ad3c1a7be91fadf1082", size = 650295, upload-time = "2026-02-20T20:47:34.036Z" }, + { url = "https://files.pythonhosted.org/packages/b3/de/dd7589b3f2b8372069ab3e4763ea5329940fc7ad9dcd3e272a37516d7c9b/greenlet-3.3.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2e47408e8ce1c6f1ceea0dffcdf6ebb85cc09e55c7af407c99f1112016e45e9", size = 662163, upload-time = "2026-02-20T20:56:01.295Z" }, + { url = "https://files.pythonhosted.org/packages/cd/ac/85804f74f1ccea31ba518dcc8ee6f14c79f73fe36fa1beba38930806df09/greenlet-3.3.2-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e3cb43ce200f59483eb82949bf1835a99cf43d7571e900d7c8d5c62cdf25d2f9", size = 675371, upload-time = "2026-02-20T21:02:49.664Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d8/09bfa816572a4d83bccd6750df1926f79158b1c36c5f73786e26dbe4ee38/greenlet-3.3.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63d10328839d1973e5ba35e98cccbca71b232b14051fd957b6f8b6e8e80d0506", size = 664160, upload-time = "2026-02-20T20:21:04.015Z" }, + { url = "https://files.pythonhosted.org/packages/48/cf/56832f0c8255d27f6c35d41b5ec91168d74ec721d85f01a12131eec6b93c/greenlet-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e4ab3cfb02993c8cc248ea73d7dae6cec0253e9afa311c9b37e603ca9fad2ce", size = 1619181, upload-time = "2026-02-20T20:49:36.052Z" }, + { url = "https://files.pythonhosted.org/packages/0a/23/b90b60a4aabb4cec0796e55f25ffbfb579a907c3898cd2905c8918acaa16/greenlet-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94ad81f0fd3c0c0681a018a976e5c2bd2ca2d9d94895f23e7bb1af4e8af4e2d5", size = 1687713, upload-time = "2026-02-20T20:21:11.684Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ca/2101ca3d9223a1dc125140dbc063644dca76df6ff356531eb27bc267b446/greenlet-3.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:8c4dd0f3997cf2512f7601563cc90dfb8957c0cff1e3a1b23991d4ea1776c492", size = 232034, upload-time = "2026-02-20T20:20:08.186Z" }, + { url = "https://files.pythonhosted.org/packages/f6/4a/ecf894e962a59dea60f04877eea0fd5724618da89f1867b28ee8b91e811f/greenlet-3.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:cd6f9e2bbd46321ba3bbb4c8a15794d32960e3b0ae2cc4d49a1a53d314805d71", size = 231437, upload-time = "2026-02-20T20:18:59.722Z" }, + { url = "https://files.pythonhosted.org/packages/98/6d/8f2ef704e614bcf58ed43cfb8d87afa1c285e98194ab2cfad351bf04f81e/greenlet-3.3.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:e26e72bec7ab387ac80caa7496e0f908ff954f31065b0ffc1f8ecb1338b11b54", size = 286617, upload-time = "2026-02-20T20:19:29.856Z" }, + { url = "https://files.pythonhosted.org/packages/5e/0d/93894161d307c6ea237a43988f27eba0947b360b99ac5239ad3fe09f0b47/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b466dff7a4ffda6ca975979bab80bdadde979e29fc947ac3be4451428d8b0e4", size = 655189, upload-time = "2026-02-20T20:47:35.742Z" }, + { url = "https://files.pythonhosted.org/packages/f5/2c/d2d506ebd8abcb57386ec4f7ba20f4030cbe56eae541bc6fd6ef399c0b41/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8bddc5b73c9720bea487b3bffdb1840fe4e3656fba3bd40aa1489e9f37877ff", size = 658225, upload-time = "2026-02-20T20:56:02.527Z" }, + { url = "https://files.pythonhosted.org/packages/d1/67/8197b7e7e602150938049d8e7f30de1660cfb87e4c8ee349b42b67bdb2e1/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:59b3e2c40f6706b05a9cd299c836c6aa2378cabe25d021acd80f13abf81181cf", size = 666581, upload-time = "2026-02-20T21:02:51.526Z" }, + { url = "https://files.pythonhosted.org/packages/8e/30/3a09155fbf728673a1dea713572d2d31159f824a37c22da82127056c44e4/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b26b0f4428b871a751968285a1ac9648944cea09807177ac639b030bddebcea4", size = 657907, upload-time = "2026-02-20T20:21:05.259Z" }, + { url = "https://files.pythonhosted.org/packages/f3/fd/d05a4b7acd0154ed758797f0a43b4c0962a843bedfe980115e842c5b2d08/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1fb39a11ee2e4d94be9a76671482be9398560955c9e568550de0224e41104727", size = 1618857, upload-time = "2026-02-20T20:49:37.309Z" }, + { url = "https://files.pythonhosted.org/packages/6f/e1/50ee92a5db521de8f35075b5eff060dd43d39ebd46c2181a2042f7070385/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:20154044d9085151bc309e7689d6f7ba10027f8f5a8c0676ad398b951913d89e", size = 1680010, upload-time = "2026-02-20T20:21:13.427Z" }, + { url = "https://files.pythonhosted.org/packages/29/4b/45d90626aef8e65336bed690106d1382f7a43665e2249017e9527df8823b/greenlet-3.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c04c5e06ec3e022cbfe2cd4a846e1d4e50087444f875ff6d2c2ad8445495cf1a", size = 237086, upload-time = "2026-02-20T20:20:45.786Z" }, +] + +[[package]] +name = "griffelib" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ad/06/eccbd311c9e2b3ca45dbc063b93134c57a1ccc7607c5e545264ad092c4a9/griffelib-2.0.0.tar.gz", hash = "sha256:e504d637a089f5cab9b5daf18f7645970509bf4f53eda8d79ed71cce8bd97934", size = 166312, upload-time = "2026-03-23T21:06:55.954Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/51/c936033e16d12b627ea334aaaaf42229c37620d0f15593456ab69ab48161/griffelib-2.0.0-py3-none-any.whl", hash = "sha256:01284878c966508b6d6f1dbff9b6fa607bc062d8261c5c7253cb285b06422a7f", size = 142004, upload-time = "2026-02-09T19:09:40.561Z" }, +] + +[[package]] +name = "grpcio" +version = "1.78.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/de/de568532d9907552700f80dcec38219d8d298ad9e71f5e0a095abaf2761e/grpcio-1.78.1.tar.gz", hash = "sha256:27c625532d33ace45d57e775edf1982e183ff8641c72e4e91ef7ba667a149d72", size = 12835760, upload-time = "2026-02-20T01:16:10.869Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/ed/d2eb9d27fded1a76b2a80eb9aa8b12101da7e41ce2bac0ad3651e88a14ae/grpcio-1.78.1-cp312-cp312-linux_armv7l.whl", hash = "sha256:41e4605c923e0e9a84a2718e4948a53a530172bfaf1a6d1ded16ef9c5849fca2", size = 5913389, upload-time = "2026-02-20T01:13:49.005Z" }, + { url = "https://files.pythonhosted.org/packages/69/1b/40034e9ab010eeb3fa41ec61d8398c6dbf7062f3872c866b8f72700e2522/grpcio-1.78.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:39da1680d260c0c619c3b5fa2dc47480ca24d5704c7a548098bca7de7f5dd17f", size = 11811839, upload-time = "2026-02-20T01:13:51.839Z" }, + { url = "https://files.pythonhosted.org/packages/b4/69/fe16ef2979ea62b8aceb3a3f1e7a8bbb8b717ae2a44b5899d5d426073273/grpcio-1.78.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b5d5881d72a09b8336a8f874784a8eeffacde44a7bc1a148bce5a0243a265ef0", size = 6475805, upload-time = "2026-02-20T01:13:55.423Z" }, + { url = "https://files.pythonhosted.org/packages/5b/1e/069e0a9062167db18446917d7c00ae2e91029f96078a072bedc30aaaa8c3/grpcio-1.78.1-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:888ceb7821acd925b1c90f0cdceaed1386e69cfe25e496e0771f6c35a156132f", size = 7169955, upload-time = "2026-02-20T01:13:59.553Z" }, + { url = "https://files.pythonhosted.org/packages/38/fc/44a57e2bb4a755e309ee4e9ed2b85c9af93450b6d3118de7e69410ee05fa/grpcio-1.78.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8942bdfc143b467c264b048862090c4ba9a0223c52ae28c9ae97754361372e42", size = 6690767, upload-time = "2026-02-20T01:14:02.31Z" }, + { url = "https://files.pythonhosted.org/packages/b8/87/21e16345d4c75046d453916166bc72a3309a382c8e97381ec4b8c1a54729/grpcio-1.78.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:716a544969660ed609164aff27b2effd3ff84e54ac81aa4ce77b1607ca917d22", size = 7266846, upload-time = "2026-02-20T01:14:12.974Z" }, + { url = "https://files.pythonhosted.org/packages/11/df/d6261983f9ca9ef4d69893765007a9a3211b91d9faf85a2591063df381c7/grpcio-1.78.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4d50329b081c223d444751076bb5b389d4f06c2b32d51b31a1e98172e6cecfb9", size = 8253522, upload-time = "2026-02-20T01:14:17.407Z" }, + { url = "https://files.pythonhosted.org/packages/de/7c/4f96a0ff113c5d853a27084d7590cd53fdb05169b596ea9f5f27f17e021e/grpcio-1.78.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7e836778c13ff70edada16567e8da0c431e8818eaae85b80d11c1ba5782eccbb", size = 7698070, upload-time = "2026-02-20T01:14:20.032Z" }, + { url = "https://files.pythonhosted.org/packages/17/3c/7b55c0b5af88fbeb3d0c13e25492d3ace41ac9dbd0f5f8f6c0fb613b6706/grpcio-1.78.1-cp312-cp312-win32.whl", hash = "sha256:07eb016ea7444a22bef465cce045512756956433f54450aeaa0b443b8563b9ca", size = 4066474, upload-time = "2026-02-20T01:14:22.602Z" }, + { url = "https://files.pythonhosted.org/packages/5d/17/388c12d298901b0acf10b612b650692bfed60e541672b1d8965acbf2d722/grpcio-1.78.1-cp312-cp312-win_amd64.whl", hash = "sha256:02b82dcd2fa580f5e82b4cf62ecde1b3c7cc9ba27b946421200706a6e5acaf85", size = 4797537, upload-time = "2026-02-20T01:14:25.444Z" }, + { url = "https://files.pythonhosted.org/packages/df/72/754754639cfd16ad04619e1435a518124b2d858e5752225376f9285d4c51/grpcio-1.78.1-cp313-cp313-linux_armv7l.whl", hash = "sha256:2b7ad2981550ce999e25ce3f10c8863f718a352a2fd655068d29ea3fd37b4907", size = 5919437, upload-time = "2026-02-20T01:14:29.403Z" }, + { url = "https://files.pythonhosted.org/packages/5c/84/6267d1266f8bc335d3a8b7ccf981be7de41e3ed8bd3a49e57e588212b437/grpcio-1.78.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:409bfe22220889b9906739910a0ee4c197a967c21b8dd14b4b06dd477f8819ce", size = 11803701, upload-time = "2026-02-20T01:14:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/f3/56/c9098e8b920a54261cd605bbb040de0cde1ca4406102db0aa2c0b11d1fb4/grpcio-1.78.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:34b6cb16f4b67eeb5206250dc5b4d5e8e3db939535e58efc330e4c61341554bd", size = 6479416, upload-time = "2026-02-20T01:14:35.926Z" }, + { url = "https://files.pythonhosted.org/packages/86/cf/5d52024371ee62658b7ed72480200524087528844ec1b65265bbcd31c974/grpcio-1.78.1-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:39d21fd30d38a5afb93f0e2e71e2ec2bd894605fb75d41d5a40060c2f98f8d11", size = 7174087, upload-time = "2026-02-20T01:14:39.98Z" }, + { url = "https://files.pythonhosted.org/packages/31/e6/5e59551afad4279e27335a6d60813b8aa3ae7b14fb62cea1d329a459c118/grpcio-1.78.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:09fbd4bcaadb6d8604ed1504b0bdf7ac18e48467e83a9d930a70a7fefa27e862", size = 6692881, upload-time = "2026-02-20T01:14:42.466Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/940062de2d14013c02f51b079eb717964d67d46f5d44f22038975c9d9576/grpcio-1.78.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:db681513a1bdd879c0b24a5a6a70398da5eaaba0e077a306410dc6008426847a", size = 7269092, upload-time = "2026-02-20T01:14:45.826Z" }, + { url = "https://files.pythonhosted.org/packages/09/87/9db657a4b5f3b15560ec591db950bc75a1a2f9e07832578d7e2b23d1a7bd/grpcio-1.78.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f81816faa426da461e9a597a178832a351d6f1078102590a4b32c77d251b71eb", size = 8252037, upload-time = "2026-02-20T01:14:48.57Z" }, + { url = "https://files.pythonhosted.org/packages/e2/37/b980e0265479ec65e26b6e300a39ceac33ecb3f762c2861d4bac990317cf/grpcio-1.78.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffbb760df1cd49e0989f9826b2fd48930700db6846ac171eaff404f3cfbe5c28", size = 7695243, upload-time = "2026-02-20T01:14:51.376Z" }, + { url = "https://files.pythonhosted.org/packages/98/46/5fc42c100ab702fa1ea41a75c890c563c3f96432b4a287d5a6369654f323/grpcio-1.78.1-cp313-cp313-win32.whl", hash = "sha256:1a56bf3ee99af5cf32d469de91bf5de79bdac2e18082b495fc1063ea33f4f2d0", size = 4065329, upload-time = "2026-02-20T01:14:53.952Z" }, + { url = "https://files.pythonhosted.org/packages/b0/da/806d60bb6611dfc16cf463d982bd92bd8b6bd5f87dfac66b0a44dfe20995/grpcio-1.78.1-cp313-cp313-win_amd64.whl", hash = "sha256:8991c2add0d8505178ff6c3ae54bd9386279e712be82fa3733c54067aae9eda1", size = 4797637, upload-time = "2026-02-20T01:14:57.276Z" }, + { url = "https://files.pythonhosted.org/packages/96/3a/2d2ec4d2ce2eb9d6a2b862630a0d9d4ff4239ecf1474ecff21442a78612a/grpcio-1.78.1-cp314-cp314-linux_armv7l.whl", hash = "sha256:d101fe49b1e0fb4a7aa36ed0c3821a0f67a5956ef572745452d2cd790d723a3f", size = 5920256, upload-time = "2026-02-20T01:15:00.23Z" }, + { url = "https://files.pythonhosted.org/packages/9c/92/dccb7d087a1220ed358753945230c1ddeeed13684b954cb09db6758f1271/grpcio-1.78.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:5ce1855e8cfc217cdf6bcfe0cf046d7cf81ddcc3e6894d6cfd075f87a2d8f460", size = 11813749, upload-time = "2026-02-20T01:15:03.312Z" }, + { url = "https://files.pythonhosted.org/packages/ef/47/c20e87f87986da9998f30f14776ce27e61f02482a3a030ffe265089342c6/grpcio-1.78.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd26048d066b51f39fe9206e2bcc2cea869a5e5b2d13c8d523f4179193047ebd", size = 6488739, upload-time = "2026-02-20T01:15:14.349Z" }, + { url = "https://files.pythonhosted.org/packages/a6/c2/088bd96e255133d7d87c3eed0d598350d16cde1041bdbe2bb065967aaf91/grpcio-1.78.1-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4b8d7fda614cf2af0f73bbb042f3b7fee2ecd4aea69ec98dbd903590a1083529", size = 7173096, upload-time = "2026-02-20T01:15:17.687Z" }, + { url = "https://files.pythonhosted.org/packages/60/ce/168db121073a03355ce3552b3b1f790b5ded62deffd7d98c5f642b9d3d81/grpcio-1.78.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:656a5bd142caeb8b1efe1fe0b4434ecc7781f44c97cfc7927f6608627cf178c0", size = 6693861, upload-time = "2026-02-20T01:15:20.911Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d0/90b30ec2d9425215dd56922d85a90babbe6ee7e8256ba77d866b9c0d3aba/grpcio-1.78.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:99550e344482e3c21950c034f74668fccf8a546d50c1ecb4f717543bbdc071ba", size = 7278083, upload-time = "2026-02-20T01:15:23.698Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fb/73f9ba0b082bcd385d46205095fd9c917754685885b28fce3741e9f54529/grpcio-1.78.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:8f27683ca68359bd3f0eb4925824d71e538f84338b3ae337ead2ae43977d7541", size = 8252546, upload-time = "2026-02-20T01:15:26.517Z" }, + { url = "https://files.pythonhosted.org/packages/85/c5/6a89ea3cb5db6c3d9ed029b0396c49f64328c0cf5d2630ffeed25711920a/grpcio-1.78.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a40515b69ac50792f9b8ead260f194ba2bb3285375b6c40c7ff938f14c3df17d", size = 7696289, upload-time = "2026-02-20T01:15:29.718Z" }, + { url = "https://files.pythonhosted.org/packages/3d/05/63a7495048499ef437b4933d32e59b7f737bd5368ad6fb2479e2bd83bf2c/grpcio-1.78.1-cp314-cp314-win32.whl", hash = "sha256:2c473b54ef1618f4fb85e82ff4994de18143b74efc088b91b5a935a3a45042ba", size = 4142186, upload-time = "2026-02-20T01:15:32.786Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ce/adfe7e5f701d503be7778291757452e3fab6b19acf51917c79f5d1cf7f8a/grpcio-1.78.1-cp314-cp314-win_amd64.whl", hash = "sha256:e2a6b33d1050dce2c6f563c5caf7f7cbeebf7fba8cde37ffe3803d50526900d1", size = 4932000, upload-time = "2026-02-20T01:15:36.127Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "h2" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "hpack" }, + { name = "hyperframe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/17/afa56379f94ad0fe8defd37d6eb3f89a25404ffc71d4d848893d270325fc/h2-4.3.0.tar.gz", hash = "sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1", size = 2152026, upload-time = "2025-08-23T18:12:19.778Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/b2/119f6e6dcbd96f9069ce9a2665e0146588dc9f88f29549711853645e736a/h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd", size = 61779, upload-time = "2025-08-23T18:12:17.779Z" }, +] + +[[package]] +name = "hf-xet" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/d0/73454ef7ca885598a3194d07d5c517d91a840753c5b35d272600d7907f64/hf_xet-1.3.1.tar.gz", hash = "sha256:513aa75f8dc39a63cc44dbc8d635ccf6b449e07cdbd8b2e2d006320d2e4be9bb", size = 641393, upload-time = "2026-02-25T00:57:56.701Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/79/9b6a5614230d7a871442d8d8e1c270496821638ba3a9baac16a5b9166200/hf_xet-1.3.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:08b231260c68172c866f7aa7257c165d0c87887491aafc5efeee782731725366", size = 3759716, upload-time = "2026-02-25T00:57:41.052Z" }, + { url = "https://files.pythonhosted.org/packages/d4/de/72acb8d7702b3cf9b36a68e8380f3114bf04f9f21cf9e25317457fe31f00/hf_xet-1.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0810b69c64e96dee849036193848007f665dca2311879c9ea8693f4fc37f1795", size = 3518075, upload-time = "2026-02-25T00:57:39.605Z" }, + { url = "https://files.pythonhosted.org/packages/1d/5c/ed728d8530fec28da88ee882b522fccf00dc98e9d7bae4cdb0493070cb17/hf_xet-1.3.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ecd38f98e7f0f41108e30fd4a9a5553ec30cf726df7473dd3e75a1b6d56728c2", size = 4174369, upload-time = "2026-02-25T00:57:32.697Z" }, + { url = "https://files.pythonhosted.org/packages/3c/db/785a0e20aa3086948a26573f1d4ff5c090e63564bf0a52d32eb5b4d82e8d/hf_xet-1.3.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:65411867d46700765018b1990eb1604c3bf0bf576d9e65fc57fdcc10797a2eb9", size = 3953249, upload-time = "2026-02-25T00:57:30.096Z" }, + { url = "https://files.pythonhosted.org/packages/c4/6a/51b669c1e3dbd9374b61356f554e8726b9e1c1d6a7bee5d727d3913b10ad/hf_xet-1.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1684c840c60da12d76c2a031ba40e4b154fdbf9593836fcf5ff090d95a033c61", size = 4152989, upload-time = "2026-02-25T00:57:48.308Z" }, + { url = "https://files.pythonhosted.org/packages/df/31/de07e26e396f46d13a09251df69df9444190e93e06a9d30d639e96c8a0ed/hf_xet-1.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:b3012c0f2ce1f0863338491a2bc0fd3f84aded0e147ab25f230da1f5249547fd", size = 4390709, upload-time = "2026-02-25T00:57:49.845Z" }, + { url = "https://files.pythonhosted.org/packages/e3/c1/fcb010b54488c2c112224f55b71f80e44d1706d9b764a0966310b283f86e/hf_xet-1.3.1-cp313-cp313t-win_amd64.whl", hash = "sha256:4eb432e1aa707a65a7e1f8455e40c5b47431d44fe0fb1b0c5d53848c27469398", size = 3634142, upload-time = "2026-02-25T00:57:59.063Z" }, + { url = "https://files.pythonhosted.org/packages/da/a6/9ef49cc601c68209979661b3e0b6659fc5a47bfb40f3ebf29eae9ee09e5c/hf_xet-1.3.1-cp313-cp313t-win_arm64.whl", hash = "sha256:e56104c84b2a88b9c7b23ba11a2d7ed0ccbe96886b3f985a50cedd2f0e99853f", size = 3494918, upload-time = "2026-02-25T00:57:57.654Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f5/66adbb1f54a1b3c6da002fa36d4405901ddbcb7d927d780db17ce18ab99d/hf_xet-1.3.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:6517a245e41df3eae5adc5f9e8c86fa52abd548de798cbcd989f0082152860aa", size = 3759781, upload-time = "2026-02-25T00:57:47.017Z" }, + { url = "https://files.pythonhosted.org/packages/1e/75/189d91a90480c142cc710c1baa35ece20e8652d5fe5c9b2364a13573d827/hf_xet-1.3.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4a322d506c513f98fdc1aa2aaa825daefd535b686e80ca789e6d33fcb146f524", size = 3517533, upload-time = "2026-02-25T00:57:45.812Z" }, + { url = "https://files.pythonhosted.org/packages/c6/52/52dd1ab6c29661e29585f3c10d14572e2535a3a472f27a0a46215b0f4659/hf_xet-1.3.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8f16ec9d26badec46334a798e01b5d86af536924789c95b1a1ec6a05f26523e0", size = 4174082, upload-time = "2026-02-25T00:57:38.171Z" }, + { url = "https://files.pythonhosted.org/packages/14/03/460add181c79e2ea1527d2ad27788ecccaee1d5a82563f9402e25ee627e4/hf_xet-1.3.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:e1f5d72bd5b73e61530fff573bcff34bdb64af2bf4862cdd516e6c1dab4dc75b", size = 3952874, upload-time = "2026-02-25T00:57:36.942Z" }, + { url = "https://files.pythonhosted.org/packages/01/56/bf78f18890dfc8caa907830e95424dce0887d5c45efde13f23c9ebbaa8ef/hf_xet-1.3.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4bc71afd853508b2ddf123b8fc9de71b0afa4c956ec730b69fb76103781e94cd", size = 4152325, upload-time = "2026-02-25T00:57:54.081Z" }, + { url = "https://files.pythonhosted.org/packages/3c/94/91685c6a4a7f513097a6a73b1e879024304cd0eae78080e3d737622f2fd9/hf_xet-1.3.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:541b4b00ed294ae6cfd9416de9506e58971013714d7316189c9638ed54e362d4", size = 4390499, upload-time = "2026-02-25T00:57:55.258Z" }, + { url = "https://files.pythonhosted.org/packages/79/1b/1e72c8ea1f31ef94640d1f265630d35b97b2ef31fe12696bbcc32dbcdc95/hf_xet-1.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f85480b4fe3e8e4cdbc59ef1d235152b732fd57ca439cc983c291892945ae818", size = 3634352, upload-time = "2026-02-25T00:58:04.749Z" }, + { url = "https://files.pythonhosted.org/packages/cf/61/b59e87a7a10b95c4578a6ce555339b2f002035569dfd366662b9f59975a8/hf_xet-1.3.1-cp314-cp314t-win_arm64.whl", hash = "sha256:83a8830160392ef4bea78d443ea2cf1febe65783b3843a8f12c64b368981e7e2", size = 3494371, upload-time = "2026-02-25T00:58:03.422Z" }, + { url = "https://files.pythonhosted.org/packages/75/f8/c2da4352c0335df6ae41750cf5bab09fdbfc30d3b4deeed9d621811aa835/hf_xet-1.3.1-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:581d1809a016f7881069d86a072168a8199a46c839cf394ff53970a47e4f1ca1", size = 3761755, upload-time = "2026-02-25T00:57:43.621Z" }, + { url = "https://files.pythonhosted.org/packages/c0/e5/a2f3eaae09da57deceb16a96ebe9ae1f6f7b9b94145a9cd3c3f994e7782a/hf_xet-1.3.1-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:329c80c86f2dda776bafd2e4813a46a3ee648dce3ac0c84625902c70d7a6ddba", size = 3523677, upload-time = "2026-02-25T00:57:42.3Z" }, + { url = "https://files.pythonhosted.org/packages/61/cd/acbbf9e51f17d8cef2630e61741228e12d4050716619353efc1ac119f902/hf_xet-1.3.1-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2973c3ff594c3a8da890836308cae1444c8af113c6f10fe6824575ddbc37eca7", size = 4178557, upload-time = "2026-02-25T00:57:35.399Z" }, + { url = "https://files.pythonhosted.org/packages/df/4f/014c14c4ae3461d9919008d0bed2f6f35ba1741e28b31e095746e8dac66f/hf_xet-1.3.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ed4bfd2e6d10cb86c9b0f3483df1d7dd2d0220f75f27166925253bacbc1c2dbe", size = 3958975, upload-time = "2026-02-25T00:57:34.004Z" }, + { url = "https://files.pythonhosted.org/packages/86/50/043f5c5a26f3831c3fa2509c17fcd468fd02f1f24d363adc7745fbe661cb/hf_xet-1.3.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:713913387cc76e300116030705d843a9f15aee86158337eeffb9eb8d26f47fcd", size = 4158298, upload-time = "2026-02-25T00:57:51.14Z" }, + { url = "https://files.pythonhosted.org/packages/08/9c/b667098a636a88358dbeb2caf90e3cb9e4b961f61f6c55bb312793424def/hf_xet-1.3.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e5063789c9d21f51e9ed4edbee8539655d3486e9cad37e96b7af967da20e8b16", size = 4395743, upload-time = "2026-02-25T00:57:52.783Z" }, + { url = "https://files.pythonhosted.org/packages/70/37/4db0e4e1534270800cfffd5a7e0b338f2137f8ceb5768000147650d34ea9/hf_xet-1.3.1-cp37-abi3-win_amd64.whl", hash = "sha256:607d5bbc2730274516714e2e442a26e40e3330673ac0d0173004461409147dee", size = 3638145, upload-time = "2026-02-25T00:58:02.167Z" }, + { url = "https://files.pythonhosted.org/packages/4e/46/1ba8d36f8290a4b98f78898bdce2b0e8fe6d9a59df34a1399eb61a8d877f/hf_xet-1.3.1-cp37-abi3-win_arm64.whl", hash = "sha256:851b1be6597a87036fe7258ce7578d5df3c08176283b989c3b165f94125c5097", size = 3500490, upload-time = "2026-02-25T00:58:00.667Z" }, +] + +[[package]] +name = "hpack" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/48/71de9ed269fdae9c8057e5a4c0aa7402e8bb16f2c6e90b3aa53327b113f8/hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca", size = 51276, upload-time = "2025-01-22T21:44:58.347Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/c6/80c95b1b2b94682a72cbdbfb85b81ae2daffa4291fbfa1b1464502ede10d/hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496", size = 34357, upload-time = "2025-01-22T21:44:56.92Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httptools" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/7f/403e5d787dc4942316e515e949b0c8a013d84078a915910e9f391ba9b3ed/httptools-0.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5", size = 206280, upload-time = "2025-10-10T03:54:39.274Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0d/7f3fd28e2ce311ccc998c388dd1c53b18120fda3b70ebb022b135dc9839b/httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5", size = 110004, upload-time = "2025-10-10T03:54:40.403Z" }, + { url = "https://files.pythonhosted.org/packages/84/a6/b3965e1e146ef5762870bbe76117876ceba51a201e18cc31f5703e454596/httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03", size = 517655, upload-time = "2025-10-10T03:54:41.347Z" }, + { url = "https://files.pythonhosted.org/packages/11/7d/71fee6f1844e6fa378f2eddde6c3e41ce3a1fb4b2d81118dd544e3441ec0/httptools-0.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2", size = 511440, upload-time = "2025-10-10T03:54:42.452Z" }, + { url = "https://files.pythonhosted.org/packages/22/a5/079d216712a4f3ffa24af4a0381b108aa9c45b7a5cc6eb141f81726b1823/httptools-0.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362", size = 495186, upload-time = "2025-10-10T03:54:43.937Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9e/025ad7b65278745dee3bd0ebf9314934c4592560878308a6121f7f812084/httptools-0.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c", size = 499192, upload-time = "2025-10-10T03:54:45.003Z" }, + { url = "https://files.pythonhosted.org/packages/6d/de/40a8f202b987d43afc4d54689600ff03ce65680ede2f31df348d7f368b8f/httptools-0.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321", size = 86694, upload-time = "2025-10-10T03:54:45.923Z" }, + { url = "https://files.pythonhosted.org/packages/09/8f/c77b1fcbfd262d422f12da02feb0d218fa228d52485b77b953832105bb90/httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3", size = 202889, upload-time = "2025-10-10T03:54:47.089Z" }, + { url = "https://files.pythonhosted.org/packages/0a/1a/22887f53602feaa066354867bc49a68fc295c2293433177ee90870a7d517/httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca", size = 108180, upload-time = "2025-10-10T03:54:48.052Z" }, + { url = "https://files.pythonhosted.org/packages/32/6a/6aaa91937f0010d288d3d124ca2946d48d60c3a5ee7ca62afe870e3ea011/httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c", size = 478596, upload-time = "2025-10-10T03:54:48.919Z" }, + { url = "https://files.pythonhosted.org/packages/6d/70/023d7ce117993107be88d2cbca566a7c1323ccbaf0af7eabf2064fe356f6/httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66", size = 473268, upload-time = "2025-10-10T03:54:49.993Z" }, + { url = "https://files.pythonhosted.org/packages/32/4d/9dd616c38da088e3f436e9a616e1d0cc66544b8cdac405cc4e81c8679fc7/httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346", size = 455517, upload-time = "2025-10-10T03:54:51.066Z" }, + { url = "https://files.pythonhosted.org/packages/1d/3a/a6c595c310b7df958e739aae88724e24f9246a514d909547778d776799be/httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650", size = 458337, upload-time = "2025-10-10T03:54:52.196Z" }, + { url = "https://files.pythonhosted.org/packages/fd/82/88e8d6d2c51edc1cc391b6e044c6c435b6aebe97b1abc33db1b0b24cd582/httptools-0.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6", size = 85743, upload-time = "2025-10-10T03:54:53.448Z" }, + { url = "https://files.pythonhosted.org/packages/34/50/9d095fcbb6de2d523e027a2f304d4551855c2f46e0b82befd718b8b20056/httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270", size = 203619, upload-time = "2025-10-10T03:54:54.321Z" }, + { url = "https://files.pythonhosted.org/packages/07/f0/89720dc5139ae54b03f861b5e2c55a37dba9a5da7d51e1e824a1f343627f/httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3", size = 108714, upload-time = "2025-10-10T03:54:55.163Z" }, + { url = "https://files.pythonhosted.org/packages/b3/cb/eea88506f191fb552c11787c23f9a405f4c7b0c5799bf73f2249cd4f5228/httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1", size = 472909, upload-time = "2025-10-10T03:54:56.056Z" }, + { url = "https://files.pythonhosted.org/packages/e0/4a/a548bdfae6369c0d078bab5769f7b66f17f1bfaa6fa28f81d6be6959066b/httptools-0.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b", size = 470831, upload-time = "2025-10-10T03:54:57.219Z" }, + { url = "https://files.pythonhosted.org/packages/4d/31/14df99e1c43bd132eec921c2e7e11cda7852f65619bc0fc5bdc2d0cb126c/httptools-0.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60", size = 452631, upload-time = "2025-10-10T03:54:58.219Z" }, + { url = "https://files.pythonhosted.org/packages/22/d2/b7e131f7be8d854d48cb6d048113c30f9a46dca0c9a8b08fcb3fcd588cdc/httptools-0.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca", size = 452910, upload-time = "2025-10-10T03:54:59.366Z" }, + { url = "https://files.pythonhosted.org/packages/53/cf/878f3b91e4e6e011eff6d1fa9ca39f7eb17d19c9d7971b04873734112f30/httptools-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96", size = 88205, upload-time = "2025-10-10T03:55:00.389Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[package.optional-dependencies] +http2 = [ + { name = "h2" }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, +] + +[[package]] +name = "huggingface-hub" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "httpx" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "shellingham" }, + { name = "tqdm" }, + { name = "typer-slim" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/fc/eb9bc06130e8bbda6a616e1b80a7aa127681c448d6b49806f61db2670b61/huggingface_hub-1.4.1.tar.gz", hash = "sha256:b41131ec35e631e7383ab26d6146b8d8972abc8b6309b963b306fbcca87f5ed5", size = 642156, upload-time = "2026-02-06T09:20:03.013Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/ae/2f6d96b4e6c5478d87d606a1934b5d436c4a2bce6bb7c6fdece891c128e3/huggingface_hub-1.4.1-py3-none-any.whl", hash = "sha256:9931d075fb7a79af5abc487106414ec5fba2c0ae86104c0c62fd6cae38873d18", size = 553326, upload-time = "2026-02-06T09:20:00.728Z" }, +] + +[[package]] +name = "hyperframe" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/e7/94f8232d4a74cc99514c13a9f995811485a6903d48e5d952771ef6322e30/hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08", size = 26566, upload-time = "2025-01-22T21:41:49.302Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007, upload-time = "2025-01-22T21:41:47.295Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "jaraco-classes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777, upload-time = "2024-03-31T07:27:34.792Z" }, +] + +[[package]] +name = "jaraco-context" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/9c/a788f5bb29c61e456b8ee52ce76dbdd32fd72cd73dd67bc95f42c7a8d13c/jaraco_context-6.1.0.tar.gz", hash = "sha256:129a341b0a85a7db7879e22acd66902fda67882db771754574338898b2d5d86f", size = 15850, upload-time = "2026-01-13T02:53:53.847Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/48/aa685dbf1024c7bd82bede569e3a85f82c32fd3d79ba5fea578f0159571a/jaraco_context-6.1.0-py3-none-any.whl", hash = "sha256:a43b5ed85815223d0d3cfdb6d7ca0d2bc8946f28f30b6f3216bda070f68badda", size = 7065, upload-time = "2026-01-13T02:53:53.031Z" }, +] + +[[package]] +name = "jaraco-functools" +version = "4.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/27/056e0638a86749374d6f57d0b0db39f29509cce9313cf91bdc0ac4d91084/jaraco_functools-4.4.0.tar.gz", hash = "sha256:da21933b0417b89515562656547a77b4931f98176eb173644c0d35032a33d6bb", size = 19943, upload-time = "2025-12-21T09:29:43.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl", hash = "sha256:9eec1e36f45c818d9bf307c8948eb03b2b56cd44087b3cdc989abca1f20b9176", size = 10481, upload-time = "2025-12-21T09:29:42.27Z" }, +] + +[[package]] +name = "jeepney" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "jiter" +version = "0.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/5e/4ec91646aee381d01cdb9974e30882c9cd3b8c5d1079d6b5ff4af522439a/jiter-0.13.0.tar.gz", hash = "sha256:f2839f9c2c7e2dffc1bc5929a510e14ce0a946be9365fd1219e7ef342dae14f4", size = 164847, upload-time = "2026-02-02T12:37:56.441Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/30/7687e4f87086829955013ca12a9233523349767f69653ebc27036313def9/jiter-0.13.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0a2bd69fc1d902e89925fc34d1da51b2128019423d7b339a45d9e99c894e0663", size = 307958, upload-time = "2026-02-02T12:35:57.165Z" }, + { url = "https://files.pythonhosted.org/packages/c3/27/e57f9a783246ed95481e6749cc5002a8a767a73177a83c63ea71f0528b90/jiter-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f917a04240ef31898182f76a332f508f2cc4b57d2b4d7ad2dbfebbfe167eb505", size = 318597, upload-time = "2026-02-02T12:35:58.591Z" }, + { url = "https://files.pythonhosted.org/packages/cf/52/e5719a60ac5d4d7c5995461a94ad5ef962a37c8bf5b088390e6fad59b2ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1e2b199f446d3e82246b4fd9236d7cb502dc2222b18698ba0d986d2fecc6152", size = 348821, upload-time = "2026-02-02T12:36:00.093Z" }, + { url = "https://files.pythonhosted.org/packages/61/db/c1efc32b8ba4c740ab3fc2d037d8753f67685f475e26b9d6536a4322bcdd/jiter-0.13.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04670992b576fa65bd056dbac0c39fe8bd67681c380cb2b48efa885711d9d726", size = 364163, upload-time = "2026-02-02T12:36:01.937Z" }, + { url = "https://files.pythonhosted.org/packages/55/8a/fb75556236047c8806995671a18e4a0ad646ed255276f51a20f32dceaeec/jiter-0.13.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a1aff1fbdb803a376d4d22a8f63f8e7ccbce0b4890c26cc7af9e501ab339ef0", size = 483709, upload-time = "2026-02-02T12:36:03.41Z" }, + { url = "https://files.pythonhosted.org/packages/7e/16/43512e6ee863875693a8e6f6d532e19d650779d6ba9a81593ae40a9088ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b3fb8c2053acaef8580809ac1d1f7481a0a0bdc012fd7f5d8b18fb696a5a089", size = 370480, upload-time = "2026-02-02T12:36:04.791Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4c/09b93e30e984a187bc8aaa3510e1ec8dcbdcd71ca05d2f56aac0492453aa/jiter-0.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdaba7d87e66f26a2c45d8cbadcbfc4bf7884182317907baf39cfe9775bb4d93", size = 360735, upload-time = "2026-02-02T12:36:06.994Z" }, + { url = "https://files.pythonhosted.org/packages/1a/1b/46c5e349019874ec5dfa508c14c37e29864ea108d376ae26d90bee238cd7/jiter-0.13.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b88d649135aca526da172e48083da915ec086b54e8e73a425ba50999468cc08", size = 391814, upload-time = "2026-02-02T12:36:08.368Z" }, + { url = "https://files.pythonhosted.org/packages/15/9e/26184760e85baee7162ad37b7912797d2077718476bf91517641c92b3639/jiter-0.13.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e404ea551d35438013c64b4f357b0474c7abf9f781c06d44fcaf7a14c69ff9e2", size = 513990, upload-time = "2026-02-02T12:36:09.993Z" }, + { url = "https://files.pythonhosted.org/packages/e9/34/2c9355247d6debad57a0a15e76ab1566ab799388042743656e566b3b7de1/jiter-0.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1f4748aad1b4a93c8bdd70f604d0f748cdc0e8744c5547798acfa52f10e79228", size = 548021, upload-time = "2026-02-02T12:36:11.376Z" }, + { url = "https://files.pythonhosted.org/packages/ac/4a/9f2c23255d04a834398b9c2e0e665382116911dc4d06b795710503cdad25/jiter-0.13.0-cp312-cp312-win32.whl", hash = "sha256:0bf670e3b1445fc4d31612199f1744f67f889ee1bbae703c4b54dc097e5dd394", size = 203024, upload-time = "2026-02-02T12:36:12.682Z" }, + { url = "https://files.pythonhosted.org/packages/09/ee/f0ae675a957ae5a8f160be3e87acea6b11dc7b89f6b7ab057e77b2d2b13a/jiter-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:15db60e121e11fe186c0b15236bd5d18381b9ddacdcf4e659feb96fc6c969c92", size = 205424, upload-time = "2026-02-02T12:36:13.93Z" }, + { url = "https://files.pythonhosted.org/packages/1b/02/ae611edf913d3cbf02c97cdb90374af2082c48d7190d74c1111dde08bcdd/jiter-0.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:41f92313d17989102f3cb5dd533a02787cdb99454d494344b0361355da52fcb9", size = 186818, upload-time = "2026-02-02T12:36:15.308Z" }, + { url = "https://files.pythonhosted.org/packages/91/9c/7ee5a6ff4b9991e1a45263bfc46731634c4a2bde27dfda6c8251df2d958c/jiter-0.13.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1f8a55b848cbabf97d861495cd65f1e5c590246fabca8b48e1747c4dfc8f85bf", size = 306897, upload-time = "2026-02-02T12:36:16.748Z" }, + { url = "https://files.pythonhosted.org/packages/7c/02/be5b870d1d2be5dd6a91bdfb90f248fbb7dcbd21338f092c6b89817c3dbf/jiter-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f556aa591c00f2c45eb1b89f68f52441a016034d18b65da60e2d2875bbbf344a", size = 317507, upload-time = "2026-02-02T12:36:18.351Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/b25d2ec333615f5f284f3a4024f7ce68cfa0604c322c6808b2344c7f5d2b/jiter-0.13.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7e1d61da332ec412350463891923f960c3073cf1aae93b538f0bb4c8cd46efb", size = 350560, upload-time = "2026-02-02T12:36:19.746Z" }, + { url = "https://files.pythonhosted.org/packages/be/ec/74dcb99fef0aca9fbe56b303bf79f6bd839010cb18ad41000bf6cc71eec0/jiter-0.13.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3097d665a27bc96fd9bbf7f86178037db139f319f785e4757ce7ccbf390db6c2", size = 363232, upload-time = "2026-02-02T12:36:21.243Z" }, + { url = "https://files.pythonhosted.org/packages/1b/37/f17375e0bb2f6a812d4dd92d7616e41917f740f3e71343627da9db2824ce/jiter-0.13.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d01ecc3a8cbdb6f25a37bd500510550b64ddf9f7d64a107d92f3ccb25035d0f", size = 483727, upload-time = "2026-02-02T12:36:22.688Z" }, + { url = "https://files.pythonhosted.org/packages/77/d2/a71160a5ae1a1e66c1395b37ef77da67513b0adba73b993a27fbe47eb048/jiter-0.13.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed9bbc30f5d60a3bdf63ae76beb3f9db280d7f195dfcfa61af792d6ce912d159", size = 370799, upload-time = "2026-02-02T12:36:24.106Z" }, + { url = "https://files.pythonhosted.org/packages/01/99/ed5e478ff0eb4e8aa5fd998f9d69603c9fd3f32de3bd16c2b1194f68361c/jiter-0.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98fbafb6e88256f4454de33c1f40203d09fc33ed19162a68b3b257b29ca7f663", size = 359120, upload-time = "2026-02-02T12:36:25.519Z" }, + { url = "https://files.pythonhosted.org/packages/16/be/7ffd08203277a813f732ba897352797fa9493faf8dc7995b31f3d9cb9488/jiter-0.13.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5467696f6b827f1116556cb0db620440380434591e93ecee7fd14d1a491b6daa", size = 390664, upload-time = "2026-02-02T12:36:26.866Z" }, + { url = "https://files.pythonhosted.org/packages/d1/84/e0787856196d6d346264d6dcccb01f741e5f0bd014c1d9a2ebe149caf4f3/jiter-0.13.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2d08c9475d48b92892583df9da592a0e2ac49bcd41fae1fec4f39ba6cf107820", size = 513543, upload-time = "2026-02-02T12:36:28.217Z" }, + { url = "https://files.pythonhosted.org/packages/65/50/ecbd258181c4313cf79bca6c88fb63207d04d5bf5e4f65174114d072aa55/jiter-0.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:aed40e099404721d7fcaf5b89bd3b4568a4666358bcac7b6b15c09fb6252ab68", size = 547262, upload-time = "2026-02-02T12:36:29.678Z" }, + { url = "https://files.pythonhosted.org/packages/27/da/68f38d12e7111d2016cd198161b36e1f042bd115c169255bcb7ec823a3bf/jiter-0.13.0-cp313-cp313-win32.whl", hash = "sha256:36ebfbcffafb146d0e6ffb3e74d51e03d9c35ce7c625c8066cdbfc7b953bdc72", size = 200630, upload-time = "2026-02-02T12:36:31.808Z" }, + { url = "https://files.pythonhosted.org/packages/25/65/3bd1a972c9a08ecd22eb3b08a95d1941ebe6938aea620c246cf426ae09c2/jiter-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:8d76029f077379374cf0dbc78dbe45b38dec4a2eb78b08b5194ce836b2517afc", size = 202602, upload-time = "2026-02-02T12:36:33.679Z" }, + { url = "https://files.pythonhosted.org/packages/15/fe/13bd3678a311aa67686bb303654792c48206a112068f8b0b21426eb6851e/jiter-0.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:bb7613e1a427cfcb6ea4544f9ac566b93d5bf67e0d48c787eca673ff9c9dff2b", size = 185939, upload-time = "2026-02-02T12:36:35.065Z" }, + { url = "https://files.pythonhosted.org/packages/49/19/a929ec002ad3228bc97ca01dbb14f7632fffdc84a95ec92ceaf4145688ae/jiter-0.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fa476ab5dd49f3bf3a168e05f89358c75a17608dbabb080ef65f96b27c19ab10", size = 316616, upload-time = "2026-02-02T12:36:36.579Z" }, + { url = "https://files.pythonhosted.org/packages/52/56/d19a9a194afa37c1728831e5fb81b7722c3de18a3109e8f282bfc23e587a/jiter-0.13.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ade8cb6ff5632a62b7dbd4757d8c5573f7a2e9ae285d6b5b841707d8363205ef", size = 346850, upload-time = "2026-02-02T12:36:38.058Z" }, + { url = "https://files.pythonhosted.org/packages/36/4a/94e831c6bf287754a8a019cb966ed39ff8be6ab78cadecf08df3bb02d505/jiter-0.13.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9950290340acc1adaded363edd94baebcee7dabdfa8bee4790794cd5cfad2af6", size = 358551, upload-time = "2026-02-02T12:36:39.417Z" }, + { url = "https://files.pythonhosted.org/packages/a2/ec/a4c72c822695fa80e55d2b4142b73f0012035d9fcf90eccc56bc060db37c/jiter-0.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2b4972c6df33731aac0742b64fd0d18e0a69bc7d6e03108ce7d40c85fd9e3e6d", size = 201950, upload-time = "2026-02-02T12:36:40.791Z" }, + { url = "https://files.pythonhosted.org/packages/b6/00/393553ec27b824fbc29047e9c7cd4a3951d7fbe4a76743f17e44034fa4e4/jiter-0.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:701a1e77d1e593c1b435315ff625fd071f0998c5f02792038a5ca98899261b7d", size = 185852, upload-time = "2026-02-02T12:36:42.077Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f5/f1997e987211f6f9bd71b8083047b316208b4aca0b529bb5f8c96c89ef3e/jiter-0.13.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:cc5223ab19fe25e2f0bf2643204ad7318896fe3729bf12fde41b77bfc4fafff0", size = 308804, upload-time = "2026-02-02T12:36:43.496Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8f/5482a7677731fd44881f0204981ce2d7175db271f82cba2085dd2212e095/jiter-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9776ebe51713acf438fd9b4405fcd86893ae5d03487546dae7f34993217f8a91", size = 318787, upload-time = "2026-02-02T12:36:45.071Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b9/7257ac59778f1cd025b26a23c5520a36a424f7f1b068f2442a5b499b7464/jiter-0.13.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:879e768938e7b49b5e90b7e3fecc0dbec01b8cb89595861fb39a8967c5220d09", size = 353880, upload-time = "2026-02-02T12:36:47.365Z" }, + { url = "https://files.pythonhosted.org/packages/c3/87/719eec4a3f0841dad99e3d3604ee4cba36af4419a76f3cb0b8e2e691ad67/jiter-0.13.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:682161a67adea11e3aae9038c06c8b4a9a71023228767477d683f69903ebc607", size = 366702, upload-time = "2026-02-02T12:36:48.871Z" }, + { url = "https://files.pythonhosted.org/packages/d2/65/415f0a75cf6921e43365a1bc227c565cb949caca8b7532776e430cbaa530/jiter-0.13.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a13b68cd1cd8cc9de8f244ebae18ccb3e4067ad205220ef324c39181e23bbf66", size = 486319, upload-time = "2026-02-02T12:36:53.006Z" }, + { url = "https://files.pythonhosted.org/packages/54/a2/9e12b48e82c6bbc6081fd81abf915e1443add1b13d8fc586e1d90bb02bb8/jiter-0.13.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87ce0f14c6c08892b610686ae8be350bf368467b6acd5085a5b65441e2bf36d2", size = 372289, upload-time = "2026-02-02T12:36:54.593Z" }, + { url = "https://files.pythonhosted.org/packages/4e/c1/e4693f107a1789a239c759a432e9afc592366f04e901470c2af89cfd28e1/jiter-0.13.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c365005b05505a90d1c47856420980d0237adf82f70c4aff7aebd3c1cc143ad", size = 360165, upload-time = "2026-02-02T12:36:56.112Z" }, + { url = "https://files.pythonhosted.org/packages/17/08/91b9ea976c1c758240614bd88442681a87672eebc3d9a6dde476874e706b/jiter-0.13.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1317fdffd16f5873e46ce27d0e0f7f4f90f0cdf1d86bf6abeaea9f63ca2c401d", size = 389634, upload-time = "2026-02-02T12:36:57.495Z" }, + { url = "https://files.pythonhosted.org/packages/18/23/58325ef99390d6d40427ed6005bf1ad54f2577866594bcf13ce55675f87d/jiter-0.13.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c05b450d37ba0c9e21c77fef1f205f56bcee2330bddca68d344baebfc55ae0df", size = 514933, upload-time = "2026-02-02T12:36:58.909Z" }, + { url = "https://files.pythonhosted.org/packages/5b/25/69f1120c7c395fd276c3996bb8adefa9c6b84c12bb7111e5c6ccdcd8526d/jiter-0.13.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:775e10de3849d0631a97c603f996f518159272db00fdda0a780f81752255ee9d", size = 548842, upload-time = "2026-02-02T12:37:00.433Z" }, + { url = "https://files.pythonhosted.org/packages/18/05/981c9669d86850c5fbb0d9e62bba144787f9fba84546ba43d624ee27ef29/jiter-0.13.0-cp314-cp314-win32.whl", hash = "sha256:632bf7c1d28421c00dd8bbb8a3bac5663e1f57d5cd5ed962bce3c73bf62608e6", size = 202108, upload-time = "2026-02-02T12:37:01.718Z" }, + { url = "https://files.pythonhosted.org/packages/8d/96/cdcf54dd0b0341db7d25413229888a346c7130bd20820530905fdb65727b/jiter-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:f22ef501c3f87ede88f23f9b11e608581c14f04db59b6a801f354397ae13739f", size = 204027, upload-time = "2026-02-02T12:37:03.075Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f9/724bcaaab7a3cd727031fe4f6995cb86c4bd344909177c186699c8dec51a/jiter-0.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:07b75fe09a4ee8e0c606200622e571e44943f47254f95e2436c8bdcaceb36d7d", size = 187199, upload-time = "2026-02-02T12:37:04.414Z" }, + { url = "https://files.pythonhosted.org/packages/62/92/1661d8b9fd6a3d7a2d89831db26fe3c1509a287d83ad7838831c7b7a5c7e/jiter-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:964538479359059a35fb400e769295d4b315ae61e4105396d355a12f7fef09f0", size = 318423, upload-time = "2026-02-02T12:37:05.806Z" }, + { url = "https://files.pythonhosted.org/packages/4f/3b/f77d342a54d4ebcd128e520fc58ec2f5b30a423b0fd26acdfc0c6fef8e26/jiter-0.13.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e104da1db1c0991b3eaed391ccd650ae8d947eab1480c733e5a3fb28d4313e40", size = 351438, upload-time = "2026-02-02T12:37:07.189Z" }, + { url = "https://files.pythonhosted.org/packages/76/b3/ba9a69f0e4209bd3331470c723c2f5509e6f0482e416b612431a5061ed71/jiter-0.13.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e3a5f0cde8ff433b8e88e41aa40131455420fb3649a3c7abdda6145f8cb7202", size = 364774, upload-time = "2026-02-02T12:37:08.579Z" }, + { url = "https://files.pythonhosted.org/packages/b3/16/6cdb31fa342932602458dbb631bfbd47f601e03d2e4950740e0b2100b570/jiter-0.13.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:57aab48f40be1db920a582b30b116fe2435d184f77f0e4226f546794cedd9cf0", size = 487238, upload-time = "2026-02-02T12:37:10.066Z" }, + { url = "https://files.pythonhosted.org/packages/ed/b1/956cc7abaca8d95c13aa8d6c9b3f3797241c246cd6e792934cc4c8b250d2/jiter-0.13.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7772115877c53f62beeb8fd853cab692dbc04374ef623b30f997959a4c0e7e95", size = 372892, upload-time = "2026-02-02T12:37:11.656Z" }, + { url = "https://files.pythonhosted.org/packages/26/c4/97ecde8b1e74f67b8598c57c6fccf6df86ea7861ed29da84629cdbba76c4/jiter-0.13.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1211427574b17b633cfceba5040de8081e5abf114f7a7602f73d2e16f9fdaa59", size = 360309, upload-time = "2026-02-02T12:37:13.244Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d7/eabe3cf46715854ccc80be2cd78dd4c36aedeb30751dbf85a1d08c14373c/jiter-0.13.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7beae3a3d3b5212d3a55d2961db3c292e02e302feb43fce6a3f7a31b90ea6dfe", size = 389607, upload-time = "2026-02-02T12:37:14.881Z" }, + { url = "https://files.pythonhosted.org/packages/df/2d/03963fc0804e6109b82decfb9974eb92df3797fe7222428cae12f8ccaa0c/jiter-0.13.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e5562a0f0e90a6223b704163ea28e831bd3a9faa3512a711f031611e6b06c939", size = 514986, upload-time = "2026-02-02T12:37:16.326Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6c/8c83b45eb3eb1c1e18d841fe30b4b5bc5619d781267ca9bc03e005d8fd0a/jiter-0.13.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:6c26a424569a59140fb51160a56df13f438a2b0967365e987889186d5fc2f6f9", size = 548756, upload-time = "2026-02-02T12:37:17.736Z" }, + { url = "https://files.pythonhosted.org/packages/47/66/eea81dfff765ed66c68fd2ed8c96245109e13c896c2a5015c7839c92367e/jiter-0.13.0-cp314-cp314t-win32.whl", hash = "sha256:24dc96eca9f84da4131cdf87a95e6ce36765c3b156fc9ae33280873b1c32d5f6", size = 201196, upload-time = "2026-02-02T12:37:19.101Z" }, + { url = "https://files.pythonhosted.org/packages/ff/32/4ac9c7a76402f8f00d00842a7f6b83b284d0cf7c1e9d4227bc95aa6d17fa/jiter-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0a8d76c7524087272c8ae913f5d9d608bd839154b62c4322ef65723d2e5bb0b8", size = 204215, upload-time = "2026-02-02T12:37:20.495Z" }, + { url = "https://files.pythonhosted.org/packages/f9/8e/7def204fea9f9be8b3c21a6f2dd6c020cf56c7d5ff753e0e23ed7f9ea57e/jiter-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2c26cf47e2cad140fa23b6d58d435a7c0161f5c514284802f25e87fddfe11024", size = 187152, upload-time = "2026-02-02T12:37:22.124Z" }, + { url = "https://files.pythonhosted.org/packages/80/60/e50fa45dd7e2eae049f0ce964663849e897300433921198aef94b6ffa23a/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:3d744a6061afba08dd7ae375dcde870cffb14429b7477e10f67e9e6d68772a0a", size = 305169, upload-time = "2026-02-02T12:37:50.376Z" }, + { url = "https://files.pythonhosted.org/packages/d2/73/a009f41c5eed71c49bec53036c4b33555afcdee70682a18c6f66e396c039/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:ff732bd0a0e778f43d5009840f20b935e79087b4dc65bd36f1cd0f9b04b8ff7f", size = 303808, upload-time = "2026-02-02T12:37:52.092Z" }, + { url = "https://files.pythonhosted.org/packages/c4/10/528b439290763bff3d939268085d03382471b442f212dca4ff5f12802d43/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab44b178f7981fcaea7e0a5df20e773c663d06ffda0198f1a524e91b2fde7e59", size = 337384, upload-time = "2026-02-02T12:37:53.582Z" }, + { url = "https://files.pythonhosted.org/packages/67/8a/a342b2f0251f3dac4ca17618265d93bf244a2a4d089126e81e4c1056ac50/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb00b6d26db67a05fe3e12c76edc75f32077fb51deed13822dc648fa373bc19", size = 343768, upload-time = "2026-02-02T12:37:55.055Z" }, +] + +[[package]] +name = "jsonref" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/0d/c1f3277e90ccdb50d33ed5ba1ec5b3f0a242ed8c1b1a85d3afeb68464dca/jsonref-1.1.0.tar.gz", hash = "sha256:32fe8e1d85af0fdefbebce950af85590b22b60f9e95443176adbde4e1ecea552", size = 8814, upload-time = "2023-01-16T16:10:04.455Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/ec/e1db9922bceb168197a558a2b8c03a7963f1afe93517ddd3cf99f202f996/jsonref-1.1.0-py3-none-any.whl", hash = "sha256:590dc7773df6c21cbf948b5dac07a72a251db28b0238ceecce0a2abfa8ec30a9", size = 9425, upload-time = "2023-01-16T16:10:02.255Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[[package]] +name = "jsonschema-path" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pathable" }, + { name = "pyyaml" }, + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/da/1ebeb1c0ff579c330e200e8b06e6200653e3d0758136d8bd86762d63e7de/jsonschema_path-0.4.2.tar.gz", hash = "sha256:5f5ff183150030ea24bb51cf1ddac9bf5dbf030272e2792a7ffe8262f7eea2a5", size = 13417, upload-time = "2026-02-23T16:21:36.602Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/10/96f8fe82137979fcd1e46fff243ce7d80cd03b9e1cee8f22476ce780f38c/jsonschema_path-0.4.2-py3-none-any.whl", hash = "sha256:9c3d88e727cc4f1a88e51dbbed4211dbcd815d27799d2685efd904435c3d39e7", size = 16702, upload-time = "2026-02-23T16:21:35.119Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "keyring" +version = "25.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jaraco-classes" }, + { name = "jaraco-context" }, + { name = "jaraco-functools" }, + { name = "jeepney", marker = "sys_platform == 'linux'" }, + { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, + { name = "secretstorage", marker = "sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b", size = 63516, upload-time = "2025-11-16T16:26:09.482Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160, upload-time = "2025-11-16T16:26:08.402Z" }, +] + +[[package]] +name = "logfire-api" +version = "4.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/5c/026cec30d85394aec8f5f12d70edbe2d706837bc9a411bd71a542cedae50/logfire_api-4.25.0.tar.gz", hash = "sha256:7562d5adfe3987291039dddb21947c86cb9d832d068c87d9aa23db86ef07095b", size = 75853, upload-time = "2026-02-19T15:27:29.518Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/39/83414c0fadb4f11f90e6b80b631aa79f62a605664f0c4693e2ebc7ee73f3/logfire_api-4.25.0-py3-none-any.whl", hash = "sha256:0d607eb09ef5426e26f376ff277a8d401bc5b7b4178ea66db404e13c368494cf", size = 120473, upload-time = "2026-02-19T15:27:25.832Z" }, +] + +[[package]] +name = "loguru" +version = "0.7.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "win32-setctime", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6", size = 63559, upload-time = "2024-12-06T11:20:56.608Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595, upload-time = "2024-12-06T11:20:54.538Z" }, +] + +[[package]] +name = "mako" +version = "1.3.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/38/bd5b78a920a64d708fe6bc8e0a2c075e1389d53bef8413725c63ba041535/mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28", size = 392474, upload-time = "2025-04-10T12:44:31.16Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59", size = 78509, upload-time = "2025-04-10T12:50:53.297Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mcp" +version = "1.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "jsonschema" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "python-multipart" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/6d/62e76bbb8144d6ed86e202b5edd8a4cb631e7c8130f3f4893c3f90262b10/mcp-1.26.0.tar.gz", hash = "sha256:db6e2ef491eecc1a0d93711a76f28dec2e05999f93afd48795da1c1137142c66", size = 608005, upload-time = "2026-01-24T19:40:32.468Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/d9/eaa1f80170d2b7c5ba23f3b59f766f3a0bb41155fbc32a69adfa1adaaef9/mcp-1.26.0-py3-none-any.whl", hash = "sha256:904a21c33c25aa98ddbeb47273033c435e595bbacfdb177f4bd87f6dceebe1ca", size = 233615, upload-time = "2026-01-24T19:40:30.652Z" }, +] + +[[package]] +name = "mdformat" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3f/05/32b5e14b192b0a8a309f32232c580aefedd9d06017cb8fe8fce34bec654c/mdformat-1.0.0.tar.gz", hash = "sha256:4954045fcae797c29f86d4ad879e43bb151fa55dbaf74ac6eaeacf1d45bb3928", size = 56953, upload-time = "2025-10-16T12:05:03.695Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/9a/8fe71b95985ca7a4001effbcc58e5a07a1f2a2884203f74dcf48a3b08315/mdformat-1.0.0-py3-none-any.whl", hash = "sha256:bca015d65a1d063a02e885a91daee303057bc7829c2cd37b2075a50dbb65944b", size = 53288, upload-time = "2025-10-16T12:05:02.607Z" }, +] + +[[package]] +name = "mdformat-frontmatter" +version = "2.0.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdformat" }, + { name = "mdit-py-plugins" }, + { name = "ruamel-yaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/94/ccb15e0535f35c21b2b533cafb232f11ac0e7046dd122029b7ec0513c82e/mdformat_frontmatter-2.0.10.tar.gz", hash = "sha256:decefcb4beb66cf111f17e8c0d82e60b208104c7922ec09564d05365db551bd8", size = 3958, upload-time = "2026-01-19T23:42:23.672Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/40/e61f8ed549bed3159a9677eadb89849ccca6de30eb90bf61e1b6026df96d/mdformat_frontmatter-2.0.10-py3-none-any.whl", hash = "sha256:c99f7c52de0403e39f48b6a1c2bb79a4c8ce69304a799ff0e403e3957aa3669b", size = 4636, upload-time = "2026-01-19T23:42:22.341Z" }, +] + +[[package]] +name = "mdformat-gfm" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "mdformat" }, + { name = "mdit-py-plugins" }, + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/56/6f/a626ebb142a290474401b67e2d61e73ce096bf7798ee22dfe6270f924b3f/mdformat_gfm-1.0.0.tar.gz", hash = "sha256:d1d49a409a6acb774ce7635c72d69178df7dce1dc8cdd10e19f78e8e57b72623", size = 10112, upload-time = "2025-10-16T09:12:22.402Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/18/6bc2189b744dd383cad03764f41f30352b1278d2205096f77a29c0b327ad/mdformat_gfm-1.0.0-py3-none-any.whl", hash = "sha256:7305a50efd2a140d7c83505b58e3ac5df2b09e293f9bbe72f6c7bee8c678b005", size = 10970, upload-time = "2025-10-16T09:12:21.276Z" }, +] + +[[package]] +name = "mdit-py-plugins" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/fd/a756d36c0bfba5f6e39a1cdbdbfdd448dc02692467d83816dff4592a1ebc/mdit_py_plugins-0.5.0.tar.gz", hash = "sha256:f4918cb50119f50446560513a8e311d574ff6aaed72606ddae6d35716fe809c6", size = 44655, upload-time = "2025-08-11T07:25:49.083Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl", hash = "sha256:07a08422fc1936a5d26d146759e9155ea466e842f5ab2f7d2266dd084c8dab1f", size = 57205, upload-time = "2025-08-11T07:25:47.597Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mem0ai" +version = "2.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "openai" }, + { name = "posthog" }, + { name = "protobuf" }, + { name = "pydantic" }, + { name = "pytz" }, + { name = "qdrant-client" }, + { name = "sqlalchemy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/5e/9ad014216cab07c5a89d3e4d7e98012a9424beb018ff0473a0fa3f68a7e7/mem0ai-2.0.5.tar.gz", hash = "sha256:ca179a62f945f1102e490ecf1e14fee72d625d0f109a78c7c5efcb03567cfcd0", size = 218989, upload-time = "2026-06-10T17:03:59.107Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/92/e1b927ac47ca662d5d6f4451ae938f90c225f5665abd19c8ef72d0bd8573/mem0ai-2.0.5-py3-none-any.whl", hash = "sha256:a8440b68b52b346206113a924de453caf13e58a228242c0381cf054801264db0", size = 308129, upload-time = "2026-06-10T17:03:57.327Z" }, +] + +[package.optional-dependencies] +nlp = [ + { name = "spacy" }, +] + +[[package]] +name = "mmh3" +version = "5.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/af/f28c2c2f51f31abb4725f9a64bc7863d5f491f6539bd26aee2a1d21a649e/mmh3-5.2.0.tar.gz", hash = "sha256:1efc8fec8478e9243a78bb993422cf79f8ff85cb4cf6b79647480a31e0d950a8", size = 33582, upload-time = "2025-07-29T07:43:48.49Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/6a/d5aa7edb5c08e0bd24286c7d08341a0446f9a2fbbb97d96a8a6dd81935ee/mmh3-5.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:384eda9361a7bf83a85e09447e1feafe081034af9dd428893701b959230d84be", size = 56141, upload-time = "2025-07-29T07:42:13.456Z" }, + { url = "https://files.pythonhosted.org/packages/08/49/131d0fae6447bc4a7299ebdb1a6fb9d08c9f8dcf97d75ea93e8152ddf7ab/mmh3-5.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2c9da0d568569cc87315cb063486d761e38458b8ad513fedd3dc9263e1b81bcd", size = 40681, upload-time = "2025-07-29T07:42:14.306Z" }, + { url = "https://files.pythonhosted.org/packages/8f/6f/9221445a6bcc962b7f5ff3ba18ad55bba624bacdc7aa3fc0a518db7da8ec/mmh3-5.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86d1be5d63232e6eb93c50881aea55ff06eb86d8e08f9b5417c8c9b10db9db96", size = 40062, upload-time = "2025-07-29T07:42:15.08Z" }, + { url = "https://files.pythonhosted.org/packages/1e/d4/6bb2d0fef81401e0bb4c297d1eb568b767de4ce6fc00890bc14d7b51ecc4/mmh3-5.2.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bf7bee43e17e81671c447e9c83499f53d99bf440bc6d9dc26a841e21acfbe094", size = 97333, upload-time = "2025-07-29T07:42:16.436Z" }, + { url = "https://files.pythonhosted.org/packages/44/e0/ccf0daff8134efbb4fbc10a945ab53302e358c4b016ada9bf97a6bdd50c1/mmh3-5.2.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7aa18cdb58983ee660c9c400b46272e14fa253c675ed963d3812487f8ca42037", size = 103310, upload-time = "2025-07-29T07:42:17.796Z" }, + { url = "https://files.pythonhosted.org/packages/02/63/1965cb08a46533faca0e420e06aff8bbaf9690a6f0ac6ae6e5b2e4544687/mmh3-5.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae9d032488fcec32d22be6542d1a836f00247f40f320844dbb361393b5b22773", size = 106178, upload-time = "2025-07-29T07:42:19.281Z" }, + { url = "https://files.pythonhosted.org/packages/c2/41/c883ad8e2c234013f27f92061200afc11554ea55edd1bcf5e1accd803a85/mmh3-5.2.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1861fb6b1d0453ed7293200139c0a9011eeb1376632e048e3766945b13313c5", size = 113035, upload-time = "2025-07-29T07:42:20.356Z" }, + { url = "https://files.pythonhosted.org/packages/df/b5/1ccade8b1fa625d634a18bab7bf08a87457e09d5ec8cf83ca07cbea9d400/mmh3-5.2.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:99bb6a4d809aa4e528ddfe2c85dd5239b78b9dd14be62cca0329db78505e7b50", size = 120784, upload-time = "2025-07-29T07:42:21.377Z" }, + { url = "https://files.pythonhosted.org/packages/77/1c/919d9171fcbdcdab242e06394464ccf546f7d0f3b31e0d1e3a630398782e/mmh3-5.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1f8d8b627799f4e2fcc7c034fed8f5f24dc7724ff52f69838a3d6d15f1ad4765", size = 99137, upload-time = "2025-07-29T07:42:22.344Z" }, + { url = "https://files.pythonhosted.org/packages/66/8a/1eebef5bd6633d36281d9fc83cf2e9ba1ba0e1a77dff92aacab83001cee4/mmh3-5.2.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b5995088dd7023d2d9f310a0c67de5a2b2e06a570ecfd00f9ff4ab94a67cde43", size = 98664, upload-time = "2025-07-29T07:42:23.269Z" }, + { url = "https://files.pythonhosted.org/packages/13/41/a5d981563e2ee682b21fb65e29cc0f517a6734a02b581359edd67f9d0360/mmh3-5.2.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1a5f4d2e59d6bba8ef01b013c472741835ad961e7c28f50c82b27c57748744a4", size = 106459, upload-time = "2025-07-29T07:42:24.238Z" }, + { url = "https://files.pythonhosted.org/packages/24/31/342494cd6ab792d81e083680875a2c50fa0c5df475ebf0b67784f13e4647/mmh3-5.2.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fd6e6c3d90660d085f7e73710eab6f5545d4854b81b0135a3526e797009dbda3", size = 110038, upload-time = "2025-07-29T07:42:25.629Z" }, + { url = "https://files.pythonhosted.org/packages/28/44/efda282170a46bb4f19c3e2b90536513b1d821c414c28469a227ca5a1789/mmh3-5.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c4a2f3d83879e3de2eb8cbf562e71563a8ed15ee9b9c2e77ca5d9f73072ac15c", size = 97545, upload-time = "2025-07-29T07:42:27.04Z" }, + { url = "https://files.pythonhosted.org/packages/68/8f/534ae319c6e05d714f437e7206f78c17e66daca88164dff70286b0e8ea0c/mmh3-5.2.0-cp312-cp312-win32.whl", hash = "sha256:2421b9d665a0b1ad724ec7332fb5a98d075f50bc51a6ff854f3a1882bd650d49", size = 40805, upload-time = "2025-07-29T07:42:28.032Z" }, + { url = "https://files.pythonhosted.org/packages/b8/f6/f6abdcfefcedab3c964868048cfe472764ed358c2bf6819a70dd4ed4ed3a/mmh3-5.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:72d80005b7634a3a2220f81fbeb94775ebd12794623bb2e1451701ea732b4aa3", size = 41597, upload-time = "2025-07-29T07:42:28.894Z" }, + { url = "https://files.pythonhosted.org/packages/15/fd/f7420e8cbce45c259c770cac5718badf907b302d3a99ec587ba5ce030237/mmh3-5.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:3d6bfd9662a20c054bc216f861fa330c2dac7c81e7fb8307b5e32ab5b9b4d2e0", size = 39350, upload-time = "2025-07-29T07:42:29.794Z" }, + { url = "https://files.pythonhosted.org/packages/d8/fa/27f6ab93995ef6ad9f940e96593c5dd24744d61a7389532b0fec03745607/mmh3-5.2.0-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:e79c00eba78f7258e5b354eccd4d7907d60317ced924ea4a5f2e9d83f5453065", size = 40874, upload-time = "2025-07-29T07:42:30.662Z" }, + { url = "https://files.pythonhosted.org/packages/11/9c/03d13bcb6a03438bc8cac3d2e50f80908d159b31a4367c2e1a7a077ded32/mmh3-5.2.0-cp313-cp313-android_21_x86_64.whl", hash = "sha256:956127e663d05edbeec54df38885d943dfa27406594c411139690485128525de", size = 42012, upload-time = "2025-07-29T07:42:31.539Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/0865d9765408a7d504f1789944e678f74e0888b96a766d578cb80b040999/mmh3-5.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:c3dca4cb5b946ee91b3d6bb700d137b1cd85c20827f89fdf9c16258253489044", size = 39197, upload-time = "2025-07-29T07:42:32.374Z" }, + { url = "https://files.pythonhosted.org/packages/3e/12/76c3207bd186f98b908b6706c2317abb73756d23a4e68ea2bc94825b9015/mmh3-5.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:e651e17bfde5840e9e4174b01e9e080ce49277b70d424308b36a7969d0d1af73", size = 39840, upload-time = "2025-07-29T07:42:33.227Z" }, + { url = "https://files.pythonhosted.org/packages/5d/0d/574b6cce5555c9f2b31ea189ad44986755eb14e8862db28c8b834b8b64dc/mmh3-5.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:9f64bf06f4bf623325fda3a6d02d36cd69199b9ace99b04bb2d7fd9f89688504", size = 40644, upload-time = "2025-07-29T07:42:34.099Z" }, + { url = "https://files.pythonhosted.org/packages/52/82/3731f8640b79c46707f53ed72034a58baad400be908c87b0088f1f89f986/mmh3-5.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ddc63328889bcaee77b743309e5c7d2d52cee0d7d577837c91b6e7cc9e755e0b", size = 56153, upload-time = "2025-07-29T07:42:35.031Z" }, + { url = "https://files.pythonhosted.org/packages/4f/34/e02dca1d4727fd9fdeaff9e2ad6983e1552804ce1d92cc796e5b052159bb/mmh3-5.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bb0fdc451fb6d86d81ab8f23d881b8d6e37fc373a2deae1c02d27002d2ad7a05", size = 40684, upload-time = "2025-07-29T07:42:35.914Z" }, + { url = "https://files.pythonhosted.org/packages/8f/36/3dee40767356e104967e6ed6d102ba47b0b1ce2a89432239b95a94de1b89/mmh3-5.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b29044e1ffdb84fe164d0a7ea05c7316afea93c00f8ed9449cf357c36fc4f814", size = 40057, upload-time = "2025-07-29T07:42:36.755Z" }, + { url = "https://files.pythonhosted.org/packages/31/58/228c402fccf76eb39a0a01b8fc470fecf21965584e66453b477050ee0e99/mmh3-5.2.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:58981d6ea9646dbbf9e59a30890cbf9f610df0e4a57dbfe09215116fd90b0093", size = 97344, upload-time = "2025-07-29T07:42:37.675Z" }, + { url = "https://files.pythonhosted.org/packages/34/82/fc5ce89006389a6426ef28e326fc065b0fbaaed230373b62d14c889f47ea/mmh3-5.2.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7e5634565367b6d98dc4aa2983703526ef556b3688ba3065edb4b9b90ede1c54", size = 103325, upload-time = "2025-07-29T07:42:38.591Z" }, + { url = "https://files.pythonhosted.org/packages/09/8c/261e85777c6aee1ebd53f2f17e210e7481d5b0846cd0b4a5c45f1e3761b8/mmh3-5.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0271ac12415afd3171ab9a3c7cbfc71dee2c68760a7dc9d05bf8ed6ddfa3a7a", size = 106240, upload-time = "2025-07-29T07:42:39.563Z" }, + { url = "https://files.pythonhosted.org/packages/70/73/2f76b3ad8a3d431824e9934403df36c0ddacc7831acf82114bce3c4309c8/mmh3-5.2.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:45b590e31bc552c6f8e2150ff1ad0c28dd151e9f87589e7eaf508fbdd8e8e908", size = 113060, upload-time = "2025-07-29T07:42:40.585Z" }, + { url = "https://files.pythonhosted.org/packages/9f/b9/7ea61a34e90e50a79a9d87aa1c0b8139a7eaf4125782b34b7d7383472633/mmh3-5.2.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bdde97310d59604f2a9119322f61b31546748499a21b44f6715e8ced9308a6c5", size = 120781, upload-time = "2025-07-29T07:42:41.618Z" }, + { url = "https://files.pythonhosted.org/packages/0f/5b/ae1a717db98c7894a37aeedbd94b3f99e6472a836488f36b6849d003485b/mmh3-5.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fc9c5f280438cf1c1a8f9abb87dc8ce9630a964120cfb5dd50d1e7ce79690c7a", size = 99174, upload-time = "2025-07-29T07:42:42.587Z" }, + { url = "https://files.pythonhosted.org/packages/e3/de/000cce1d799fceebb6d4487ae29175dd8e81b48e314cba7b4da90bcf55d7/mmh3-5.2.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c903e71fd8debb35ad2a4184c1316b3cb22f64ce517b4e6747f25b0a34e41266", size = 98734, upload-time = "2025-07-29T07:42:43.996Z" }, + { url = "https://files.pythonhosted.org/packages/79/19/0dc364391a792b72fbb22becfdeacc5add85cc043cd16986e82152141883/mmh3-5.2.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:eed4bba7ff8a0d37106ba931ab03bdd3915fbb025bcf4e1f0aa02bc8114960c5", size = 106493, upload-time = "2025-07-29T07:42:45.07Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b1/bc8c28e4d6e807bbb051fefe78e1156d7f104b89948742ad310612ce240d/mmh3-5.2.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:1fdb36b940e9261aff0b5177c5b74a36936b902f473180f6c15bde26143681a9", size = 110089, upload-time = "2025-07-29T07:42:46.122Z" }, + { url = "https://files.pythonhosted.org/packages/3b/a2/d20f3f5c95e9c511806686c70d0a15479cc3941c5f322061697af1c1ff70/mmh3-5.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7303aab41e97adcf010a09efd8f1403e719e59b7705d5e3cfed3dd7571589290", size = 97571, upload-time = "2025-07-29T07:42:47.18Z" }, + { url = "https://files.pythonhosted.org/packages/7b/23/665296fce4f33488deec39a750ffd245cfc07aafb0e3ef37835f91775d14/mmh3-5.2.0-cp313-cp313-win32.whl", hash = "sha256:03e08c6ebaf666ec1e3d6ea657a2d363bb01effd1a9acfe41f9197decaef0051", size = 40806, upload-time = "2025-07-29T07:42:48.166Z" }, + { url = "https://files.pythonhosted.org/packages/59/b0/92e7103f3b20646e255b699e2d0327ce53a3f250e44367a99dc8be0b7c7a/mmh3-5.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:7fddccd4113e7b736706e17a239a696332360cbaddf25ae75b57ba1acce65081", size = 41600, upload-time = "2025-07-29T07:42:49.371Z" }, + { url = "https://files.pythonhosted.org/packages/99/22/0b2bd679a84574647de538c5b07ccaa435dbccc37815067fe15b90fe8dad/mmh3-5.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:fa0c966ee727aad5406d516375593c5f058c766b21236ab8985693934bb5085b", size = 39349, upload-time = "2025-07-29T07:42:50.268Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ca/a20db059a8a47048aaf550da14a145b56e9c7386fb8280d3ce2962dcebf7/mmh3-5.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:e5015f0bb6eb50008bed2d4b1ce0f2a294698a926111e4bb202c0987b4f89078", size = 39209, upload-time = "2025-07-29T07:42:51.559Z" }, + { url = "https://files.pythonhosted.org/packages/98/dd/e5094799d55c7482d814b979a0fd608027d0af1b274bfb4c3ea3e950bfd5/mmh3-5.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:e0f3ed828d709f5b82d8bfe14f8856120718ec4bd44a5b26102c3030a1e12501", size = 39843, upload-time = "2025-07-29T07:42:52.536Z" }, + { url = "https://files.pythonhosted.org/packages/f4/6b/7844d7f832c85400e7cc89a1348e4e1fdd38c5a38415bb5726bbb8fcdb6c/mmh3-5.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:f35727c5118aba95f0397e18a1a5b8405425581bfe53e821f0fb444cbdc2bc9b", size = 40648, upload-time = "2025-07-29T07:42:53.392Z" }, + { url = "https://files.pythonhosted.org/packages/1f/bf/71f791f48a21ff3190ba5225807cbe4f7223360e96862c376e6e3fb7efa7/mmh3-5.2.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3bc244802ccab5220008cb712ca1508cb6a12f0eb64ad62997156410579a1770", size = 56164, upload-time = "2025-07-29T07:42:54.267Z" }, + { url = "https://files.pythonhosted.org/packages/70/1f/f87e3d34d83032b4f3f0f528c6d95a98290fcacf019da61343a49dccfd51/mmh3-5.2.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ff3d50dc3fe8a98059f99b445dfb62792b5d006c5e0b8f03c6de2813b8376110", size = 40692, upload-time = "2025-07-29T07:42:55.234Z" }, + { url = "https://files.pythonhosted.org/packages/a6/e2/db849eaed07117086f3452feca8c839d30d38b830ac59fe1ce65af8be5ad/mmh3-5.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:37a358cc881fe796e099c1db6ce07ff757f088827b4e8467ac52b7a7ffdca647", size = 40068, upload-time = "2025-07-29T07:42:56.158Z" }, + { url = "https://files.pythonhosted.org/packages/df/6b/209af927207af77425b044e32f77f49105a0b05d82ff88af6971d8da4e19/mmh3-5.2.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b9a87025121d1c448f24f27ff53a5fe7b6ef980574b4a4f11acaabe702420d63", size = 97367, upload-time = "2025-07-29T07:42:57.037Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e0/78adf4104c425606a9ce33fb351f790c76a6c2314969c4a517d1ffc92196/mmh3-5.2.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ba55d6ca32eeef8b2625e1e4bfc3b3db52bc63014bd7e5df8cc11bf2b036b12", size = 103306, upload-time = "2025-07-29T07:42:58.522Z" }, + { url = "https://files.pythonhosted.org/packages/a3/79/c2b89f91b962658b890104745b1b6c9ce38d50a889f000b469b91eeb1b9e/mmh3-5.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9ff37ba9f15637e424c2ab57a1a590c52897c845b768e4e0a4958084ec87f22", size = 106312, upload-time = "2025-07-29T07:42:59.552Z" }, + { url = "https://files.pythonhosted.org/packages/4b/14/659d4095528b1a209be90934778c5ffe312177d51e365ddcbca2cac2ec7c/mmh3-5.2.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a094319ec0db52a04af9fdc391b4d39a1bc72bc8424b47c4411afb05413a44b5", size = 113135, upload-time = "2025-07-29T07:43:00.745Z" }, + { url = "https://files.pythonhosted.org/packages/8d/6f/cd7734a779389a8a467b5c89a48ff476d6f2576e78216a37551a97e9e42a/mmh3-5.2.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c5584061fd3da584659b13587f26c6cad25a096246a481636d64375d0c1f6c07", size = 120775, upload-time = "2025-07-29T07:43:02.124Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ca/8256e3b96944408940de3f9291d7e38a283b5761fe9614d4808fcf27bd62/mmh3-5.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecbfc0437ddfdced5e7822d1ce4855c9c64f46819d0fdc4482c53f56c707b935", size = 99178, upload-time = "2025-07-29T07:43:03.182Z" }, + { url = "https://files.pythonhosted.org/packages/8a/32/39e2b3cf06b6e2eb042c984dab8680841ac2a0d3ca6e0bea30db1f27b565/mmh3-5.2.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:7b986d506a8e8ea345791897ba5d8ba0d9d8820cd4fc3e52dbe6de19388de2e7", size = 98738, upload-time = "2025-07-29T07:43:04.207Z" }, + { url = "https://files.pythonhosted.org/packages/61/d3/7bbc8e0e8cf65ebbe1b893ffa0467b7ecd1bd07c3bbf6c9db4308ada22ec/mmh3-5.2.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:38d899a156549da8ef6a9f1d6f7ef231228d29f8f69bce2ee12f5fba6d6fd7c5", size = 106510, upload-time = "2025-07-29T07:43:05.656Z" }, + { url = "https://files.pythonhosted.org/packages/10/99/b97e53724b52374e2f3859046f0eb2425192da356cb19784d64bc17bb1cf/mmh3-5.2.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d86651fa45799530885ba4dab3d21144486ed15285e8784181a0ab37a4552384", size = 110053, upload-time = "2025-07-29T07:43:07.204Z" }, + { url = "https://files.pythonhosted.org/packages/ac/62/3688c7d975ed195155671df68788c83fed6f7909b6ec4951724c6860cb97/mmh3-5.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c463d7c1c4cfc9d751efeaadd936bbba07b5b0ed81a012b3a9f5a12f0872bd6e", size = 97546, upload-time = "2025-07-29T07:43:08.226Z" }, + { url = "https://files.pythonhosted.org/packages/ca/3b/c6153250f03f71a8b7634cded82939546cdfba02e32f124ff51d52c6f991/mmh3-5.2.0-cp314-cp314-win32.whl", hash = "sha256:bb4fe46bdc6104fbc28db7a6bacb115ee6368ff993366bbd8a2a7f0076e6f0c0", size = 41422, upload-time = "2025-07-29T07:43:09.216Z" }, + { url = "https://files.pythonhosted.org/packages/74/01/a27d98bab083a435c4c07e9d1d720d4c8a578bf4c270bae373760b1022be/mmh3-5.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:7c7f0b342fd06044bedd0b6e72177ddc0076f54fd89ee239447f8b271d919d9b", size = 42135, upload-time = "2025-07-29T07:43:10.183Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c9/dbba5507e95429b8b380e2ba091eff5c20a70a59560934dff0ad8392b8c8/mmh3-5.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:3193752fc05ea72366c2b63ff24b9a190f422e32d75fdeae71087c08fff26115", size = 39879, upload-time = "2025-07-29T07:43:11.106Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d1/c8c0ef839c17258b9de41b84f663574fabcf8ac2007b7416575e0f65ff6e/mmh3-5.2.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:69fc339d7202bea69ef9bd7c39bfdf9fdabc8e6822a01eba62fb43233c1b3932", size = 57696, upload-time = "2025-07-29T07:43:11.989Z" }, + { url = "https://files.pythonhosted.org/packages/2f/55/95e2b9ff201e89f9fe37036037ab61a6c941942b25cdb7b6a9df9b931993/mmh3-5.2.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:12da42c0a55c9d86ab566395324213c319c73ecb0c239fad4726324212b9441c", size = 41421, upload-time = "2025-07-29T07:43:13.269Z" }, + { url = "https://files.pythonhosted.org/packages/77/79/9be23ad0b7001a4b22752e7693be232428ecc0a35068a4ff5c2f14ef8b20/mmh3-5.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f7f9034c7cf05ddfaac8d7a2e63a3c97a840d4615d0a0e65ba8bdf6f8576e3be", size = 40853, upload-time = "2025-07-29T07:43:14.888Z" }, + { url = "https://files.pythonhosted.org/packages/ac/1b/96b32058eda1c1dee8264900c37c359a7325c1f11f5ff14fd2be8e24eff9/mmh3-5.2.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:11730eeb16dfcf9674fdea9bb6b8e6dd9b40813b7eb839bc35113649eef38aeb", size = 109694, upload-time = "2025-07-29T07:43:15.816Z" }, + { url = "https://files.pythonhosted.org/packages/8d/6f/a2ae44cd7dad697b6dea48390cbc977b1e5ca58fda09628cbcb2275af064/mmh3-5.2.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:932a6eec1d2e2c3c9e630d10f7128d80e70e2d47fe6b8c7ea5e1afbd98733e65", size = 117438, upload-time = "2025-07-29T07:43:16.865Z" }, + { url = "https://files.pythonhosted.org/packages/a0/08/bfb75451c83f05224a28afeaf3950c7b793c0b71440d571f8e819cfb149a/mmh3-5.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ca975c51c5028947bbcfc24966517aac06a01d6c921e30f7c5383c195f87991", size = 120409, upload-time = "2025-07-29T07:43:18.207Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ea/8b118b69b2ff8df568f742387d1a159bc654a0f78741b31437dd047ea28e/mmh3-5.2.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5b0b58215befe0f0e120b828f7645e97719bbba9f23b69e268ed0ac7adde8645", size = 125909, upload-time = "2025-07-29T07:43:19.39Z" }, + { url = "https://files.pythonhosted.org/packages/3e/11/168cc0b6a30650032e351a3b89b8a47382da541993a03af91e1ba2501234/mmh3-5.2.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29c2b9ce61886809d0492a274a5a53047742dea0f703f9c4d5d223c3ea6377d3", size = 135331, upload-time = "2025-07-29T07:43:20.435Z" }, + { url = "https://files.pythonhosted.org/packages/31/05/e3a9849b1c18a7934c64e831492c99e67daebe84a8c2f2c39a7096a830e3/mmh3-5.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a367d4741ac0103f8198c82f429bccb9359f543ca542b06a51f4f0332e8de279", size = 110085, upload-time = "2025-07-29T07:43:21.92Z" }, + { url = "https://files.pythonhosted.org/packages/d9/d5/a96bcc306e3404601418b2a9a370baec92af84204528ba659fdfe34c242f/mmh3-5.2.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:5a5dba98e514fb26241868f6eb90a7f7ca0e039aed779342965ce24ea32ba513", size = 111195, upload-time = "2025-07-29T07:43:23.066Z" }, + { url = "https://files.pythonhosted.org/packages/af/29/0fd49801fec5bff37198684e0849b58e0dab3a2a68382a357cfffb0fafc3/mmh3-5.2.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:941603bfd75a46023807511c1ac2f1b0f39cccc393c15039969806063b27e6db", size = 116919, upload-time = "2025-07-29T07:43:24.178Z" }, + { url = "https://files.pythonhosted.org/packages/2d/04/4f3c32b0a2ed762edca45d8b46568fc3668e34f00fb1e0a3b5451ec1281c/mmh3-5.2.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:132dd943451a7c7546978863d2f5a64977928410782e1a87d583cb60eb89e667", size = 123160, upload-time = "2025-07-29T07:43:25.26Z" }, + { url = "https://files.pythonhosted.org/packages/91/76/3d29eaa38821730633d6a240d36fa8ad2807e9dfd432c12e1a472ed211eb/mmh3-5.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f698733a8a494466432d611a8f0d1e026f5286dee051beea4b3c3146817e35d5", size = 110206, upload-time = "2025-07-29T07:43:26.699Z" }, + { url = "https://files.pythonhosted.org/packages/44/1c/ccf35892684d3a408202e296e56843743e0b4fb1629e59432ea88cdb3909/mmh3-5.2.0-cp314-cp314t-win32.whl", hash = "sha256:6d541038b3fc360ec538fc116de87462627944765a6750308118f8b509a8eec7", size = 41970, upload-time = "2025-07-29T07:43:27.666Z" }, + { url = "https://files.pythonhosted.org/packages/75/b2/b9e4f1e5adb5e21eb104588fcee2cd1eaa8308255173481427d5ecc4284e/mmh3-5.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e912b19cf2378f2967d0c08e86ff4c6c360129887f678e27e4dde970d21b3f4d", size = 43063, upload-time = "2025-07-29T07:43:28.582Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fc/0e61d9a4e29c8679356795a40e48f647b4aad58d71bfc969f0f8f56fb912/mmh3-5.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:e7884931fe5e788163e7b3c511614130c2c59feffdc21112290a194487efb2e9", size = 40455, upload-time = "2025-07-29T07:43:29.563Z" }, +] + +[[package]] +name = "more-itertools" +version = "10.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431, upload-time = "2025-09-02T15:23:11.018Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" }, +] + +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, +] + +[[package]] +name = "murmurhash" +version = "1.0.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/23/2e/88c147931ea9725d634840d538622e94122bceaf346233349b7b5c62964b/murmurhash-1.0.15.tar.gz", hash = "sha256:58e2b27b7847f9e2a6edf10b47a8c8dd70a4705f45dccb7bf76aeadacf56ba01", size = 13291, upload-time = "2025-11-14T09:51:15.272Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/46/be8522d3456fdccf1b8b049c6d82e7a3c1114c4fc2cfe14b04cba4b3e701/murmurhash-1.0.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d37e3ae44746bca80b1a917c2ea625cf216913564ed43f69d2888e5df97db0cb", size = 27884, upload-time = "2025-11-14T09:50:13.133Z" }, + { url = "https://files.pythonhosted.org/packages/ed/cc/630449bf4f6178d7daf948ce46ad00b25d279065fc30abd8d706be3d87e0/murmurhash-1.0.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0861cb11039409eaf46878456b7d985ef17b6b484103a6fc367b2ecec846891d", size = 27855, upload-time = "2025-11-14T09:50:14.859Z" }, + { url = "https://files.pythonhosted.org/packages/ff/30/ea8f601a9bf44db99468696efd59eb9cff1157cd55cb586d67116697583f/murmurhash-1.0.15-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5a301decfaccfec70fe55cb01dde2a012c3014a874542eaa7cc73477bb749616", size = 134088, upload-time = "2025-11-14T09:50:15.958Z" }, + { url = "https://files.pythonhosted.org/packages/c9/de/c40ce8c0877d406691e735b8d6e9c815f36a82b499d358313db5dbe219d7/murmurhash-1.0.15-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:32c6fde7bd7e9407003370a07b5f4addacabe1556ad3dc2cac246b7a2bba3400", size = 133978, upload-time = "2025-11-14T09:50:17.572Z" }, + { url = "https://files.pythonhosted.org/packages/47/84/bd49963ecd84ebab2fe66595e2d1ed41d5e8b5153af5dc930f0bd827007c/murmurhash-1.0.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5d8b43a7011540dc3c7ce66f2134df9732e2bc3bbb4a35f6458bc755e48bde26", size = 132956, upload-time = "2025-11-14T09:50:18.742Z" }, + { url = "https://files.pythonhosted.org/packages/4f/7c/2530769c545074417c862583f05f4245644599f1e9ff619b3dfe2969aafc/murmurhash-1.0.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:43bf4541892ecd95963fcd307bf1c575fc0fee1682f41c93007adee71ca2bb40", size = 134184, upload-time = "2025-11-14T09:50:19.941Z" }, + { url = "https://files.pythonhosted.org/packages/84/a4/b249b042f5afe34d14ada2dc4afc777e883c15863296756179652e081c44/murmurhash-1.0.15-cp312-cp312-win_amd64.whl", hash = "sha256:f4ac15a2089dc42e6eb0966622d42d2521590a12c92480aafecf34c085302cca", size = 25647, upload-time = "2025-11-14T09:50:21.049Z" }, + { url = "https://files.pythonhosted.org/packages/13/bf/028179259aebc18fd4ba5cae2601d1d47517427a537ab44336446431a215/murmurhash-1.0.15-cp312-cp312-win_arm64.whl", hash = "sha256:4a70ca4ae19e600d9be3da64d00710e79dde388a4d162f22078d64844d0ebdda", size = 23338, upload-time = "2025-11-14T09:50:22.359Z" }, + { url = "https://files.pythonhosted.org/packages/29/2f/ba300b5f04dae0409202d6285668b8a9d3ade43a846abee3ef611cb388d5/murmurhash-1.0.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fe50dc70e52786759358fd1471e309b94dddfffb9320d9dfea233c7684c894ba", size = 27861, upload-time = "2025-11-14T09:50:23.804Z" }, + { url = "https://files.pythonhosted.org/packages/34/02/29c19d268e6f4ea1ed2a462c901eed1ed35b454e2cbc57da592fad663ac6/murmurhash-1.0.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1349a7c23f6092e7998ddc5bd28546cc31a595afc61e9fdb3afc423feec3d7ad", size = 27840, upload-time = "2025-11-14T09:50:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/e2/63/58e2de2b5232cd294c64092688c422196e74f9fa8b3958bdf02d33df24b9/murmurhash-1.0.15-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3ba6d05de2613535b5a9227d4ad8ef40a540465f64660d4a8800634ae10e04f", size = 133080, upload-time = "2025-11-14T09:50:26.566Z" }, + { url = "https://files.pythonhosted.org/packages/aa/9a/d13e2e9f8ba1ced06840921a50f7cece0a475453284158a3018b72679761/murmurhash-1.0.15-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fa1b70b3cc2801ab44179c65827bbd12009c68b34e9d9ce7125b6a0bd35af63c", size = 132648, upload-time = "2025-11-14T09:50:27.788Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e1/47994f1813fa205c84977b0ff51ae6709f8539af052c7491a5f863d82bdc/murmurhash-1.0.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:213d710fb6f4ef3bc11abbfad0fa94a75ffb675b7dc158c123471e5de869f9af", size = 131502, upload-time = "2025-11-14T09:50:29.339Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ea/90c1fd00b4aeb704fb5e84cd666b33ffd7f245155048071ffbb51d2bb57d/murmurhash-1.0.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b65a5c4e7f5d71f7ccac2d2b60bdf7092d7976270878cfec59d5a66a533db823", size = 132736, upload-time = "2025-11-14T09:50:30.545Z" }, + { url = "https://files.pythonhosted.org/packages/00/db/da73462dbfa77f6433b128d2120ba7ba300f8c06dc4f4e022c38d240a5f5/murmurhash-1.0.15-cp313-cp313-win_amd64.whl", hash = "sha256:9aba94c5d841e1904cd110e94ceb7f49cfb60a874bbfb27e0373622998fb7c7c", size = 25682, upload-time = "2025-11-14T09:50:31.624Z" }, + { url = "https://files.pythonhosted.org/packages/bb/83/032729ef14971b938fbef41ee125fc8800020ee229bd35178b6ede8ee934/murmurhash-1.0.15-cp313-cp313-win_arm64.whl", hash = "sha256:263807eca40d08c7b702413e45cca75ecb5883aa337237dc5addb660f1483378", size = 23370, upload-time = "2025-11-14T09:50:33.264Z" }, + { url = "https://files.pythonhosted.org/packages/10/83/7547d9205e9bd2f8e5dfd0b682cc9277594f98909f228eb359489baec1df/murmurhash-1.0.15-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:694fd42a74b7ce257169d14c24aa616aa6cd4ccf8abe50eca0557e08da99d055", size = 29955, upload-time = "2025-11-14T09:50:34.488Z" }, + { url = "https://files.pythonhosted.org/packages/b7/c7/3afd5de7a5b3ae07fe2d3a3271b327ee1489c58ba2b2f2159bd31a25edb9/murmurhash-1.0.15-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a2ea4546ba426390beff3cd10db8f0152fdc9072c4f2583ec7d8aa9f3e4ac070", size = 30108, upload-time = "2025-11-14T09:50:35.53Z" }, + { url = "https://files.pythonhosted.org/packages/02/69/d6637ee67d78ebb2538c00411f28ea5c154886bbe1db16c49435a8a4ab16/murmurhash-1.0.15-cp313-cp313t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:34e5a91139c40b10f98d0b297907f5d5267b4b1b2e5dd2eb74a021824f751b98", size = 164054, upload-time = "2025-11-14T09:50:36.591Z" }, + { url = "https://files.pythonhosted.org/packages/ab/4c/89e590165b4c7da6bf941441212a721a270195332d3aacfdfdf527d466ca/murmurhash-1.0.15-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:dc35606868a5961cf42e79314ca0bddf5a400ce377b14d83192057928d6252ec", size = 168153, upload-time = "2025-11-14T09:50:37.856Z" }, + { url = "https://files.pythonhosted.org/packages/07/7a/95c42df0c21d2e413b9fcd17317a7587351daeb264dc29c6aec1fdbd26f8/murmurhash-1.0.15-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:43cc6ac3b91ca0f7a5ae9c063ba4d6c26972c97fd7c25280ecc666413e4c5535", size = 164345, upload-time = "2025-11-14T09:50:39.346Z" }, + { url = "https://files.pythonhosted.org/packages/d0/22/9d02c880a88b83bb3ce7d6a38fb727373ab78d82e5f3d8d9fc5612219f90/murmurhash-1.0.15-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:847d712136cb462f0e4bd6229ee2d9eb996d8854eb8312dff3d20c8f5181fda5", size = 161990, upload-time = "2025-11-14T09:50:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/750232524e0dc262e8dcede6536dafc766faadd9a52f1d23746b02948ad8/murmurhash-1.0.15-cp313-cp313t-win_amd64.whl", hash = "sha256:2680851af6901dbe66cc4aa7ef8e263de47e6e1b425ae324caa571bdf18f8d58", size = 28812, upload-time = "2025-11-14T09:50:41.971Z" }, + { url = "https://files.pythonhosted.org/packages/ff/89/4ad9d215ef6ade89f27a72dc4e86b98ef1a43534cc3e6a6900a362a0bf0a/murmurhash-1.0.15-cp313-cp313t-win_arm64.whl", hash = "sha256:189a8de4d657b5da9efd66601b0636330b08262b3a55431f2379097c986995d0", size = 25398, upload-time = "2025-11-14T09:50:43.023Z" }, + { url = "https://files.pythonhosted.org/packages/1c/69/726df275edf07688146966e15eaaa23168100b933a2e1a29b37eb56c6db8/murmurhash-1.0.15-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7c4280136b738e85ff76b4bdc4341d0b867ee753e73fd8b6994288080c040d0b", size = 28029, upload-time = "2025-11-14T09:50:44.124Z" }, + { url = "https://files.pythonhosted.org/packages/59/8f/24ecf9061bc2b20933df8aba47c73e904274ea8811c8300cab92f6f82372/murmurhash-1.0.15-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d4d681f474830489e2ec1d912095cfff027fbaf2baa5414c7e9d25b89f0fab68", size = 27912, upload-time = "2025-11-14T09:50:45.266Z" }, + { url = "https://files.pythonhosted.org/packages/ba/26/fff3caba25aa3c0622114e03c69fb66c839b22335b04d7cce91a3a126d44/murmurhash-1.0.15-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d7e47c5746785db6a43b65fac47b9e63dd71dfbd89a8c92693425b9715e68c6e", size = 131847, upload-time = "2025-11-14T09:50:46.819Z" }, + { url = "https://files.pythonhosted.org/packages/df/e4/0f2b9fc533467a27afb4e906c33f32d5f637477de87dd94690e0c44335a6/murmurhash-1.0.15-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e8e674f02a99828c8a671ba99cd03299381b2f0744e6f25c29cadfc6151dc724", size = 132267, upload-time = "2025-11-14T09:50:48.298Z" }, + { url = "https://files.pythonhosted.org/packages/da/bf/9d1c107989728ec46e25773d503aa54070b32822a18cfa7f9d5f41bc17a5/murmurhash-1.0.15-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:26fd7c7855ac4850ad8737991d7b0e3e501df93ebaf0cf45aa5954303085fdba", size = 131894, upload-time = "2025-11-14T09:50:49.485Z" }, + { url = "https://files.pythonhosted.org/packages/0d/81/dcf27c71445c0e993b10e33169a098ca60ee702c5c58fcbde205fa6332a6/murmurhash-1.0.15-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cb8ebafae60d5f892acff533cc599a359954d8c016a829514cb3f6e9ee10f322", size = 132054, upload-time = "2025-11-14T09:50:50.747Z" }, + { url = "https://files.pythonhosted.org/packages/bc/32/e874a14b2d2246bd2d16f80f49fad393a3865d4ee7d66d2cae939a67a29a/murmurhash-1.0.15-cp314-cp314-win_amd64.whl", hash = "sha256:898a629bf111f1aeba4437e533b5b836c0a9d2dd12d6880a9c75f6ca13e30e22", size = 26579, upload-time = "2025-11-14T09:50:52.278Z" }, + { url = "https://files.pythonhosted.org/packages/af/8e/4fca051ed8ae4d23a15aaf0a82b18cb368e8cf84f1e3b474d5749ec46069/murmurhash-1.0.15-cp314-cp314-win_arm64.whl", hash = "sha256:88dc1dd53b7b37c0df1b8b6bce190c12763014492f0269ff7620dc6027f470f4", size = 24341, upload-time = "2025-11-14T09:50:53.295Z" }, + { url = "https://files.pythonhosted.org/packages/38/9c/c72c2a4edd86aac829337ab9f83cf04cdb15e5d503e4c9a3a243f30a261c/murmurhash-1.0.15-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:6cb4e962ec4f928b30c271b2d84e6707eff6d942552765b663743cfa618b294b", size = 30146, upload-time = "2025-11-14T09:50:54.705Z" }, + { url = "https://files.pythonhosted.org/packages/ac/d7/72b47ebc86436cd0aa1fd4c6e8779521ec389397ac11389990278d0f7a47/murmurhash-1.0.15-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5678a3ea4fbf0cbaaca2bed9b445f556f294d5f799c67185d05ffcb221a77faf", size = 30141, upload-time = "2025-11-14T09:50:55.829Z" }, + { url = "https://files.pythonhosted.org/packages/64/bb/6d2f09135079c34dc2d26e961c52742d558b320c61503f273eab6ba743d9/murmurhash-1.0.15-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ef19f38c6b858eef83caf710773db98c8f7eb2193b4c324650c74f3d8ba299e0", size = 163898, upload-time = "2025-11-14T09:50:56.946Z" }, + { url = "https://files.pythonhosted.org/packages/b9/e2/9c1b462e33f9cb2d632056f07c90b502fc20bd7da50a15d0557343bd2fed/murmurhash-1.0.15-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22aa3ceaedd2e57078b491ed08852d512b84ff4ff9bb2ff3f9bf0eec7f214c9e", size = 168040, upload-time = "2025-11-14T09:50:58.234Z" }, + { url = "https://files.pythonhosted.org/packages/e8/73/8694db1408fcdfa73589f7df6c445437ea146986fa1e393ec60d26d6e30c/murmurhash-1.0.15-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bba0e0262c0d08682b028cb963ac477bd9839029486fa1333fc5c01fb6072749", size = 164239, upload-time = "2025-11-14T09:50:59.95Z" }, + { url = "https://files.pythonhosted.org/packages/2d/f9/8e360bdfc3c44e267e7e046f0e0b9922766da92da26959a6963f597e6bb5/murmurhash-1.0.15-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4fd8189ee293a09f30f4931408f40c28ccd42d9de4f66595f8814879339378bc", size = 161811, upload-time = "2025-11-14T09:51:01.289Z" }, + { url = "https://files.pythonhosted.org/packages/f9/31/97649680595b1096803d877ababb9a67c07f4378f177ec885eea28b9db6d/murmurhash-1.0.15-cp314-cp314t-win_amd64.whl", hash = "sha256:66395b1388f7daa5103db92debe06842ae3be4c0749ef6db68b444518666cdcc", size = 29817, upload-time = "2025-11-14T09:51:02.493Z" }, + { url = "https://files.pythonhosted.org/packages/76/66/4fce8755f25d77324401886c00017c556be7ca3039575b94037aff905385/murmurhash-1.0.15-cp314-cp314t-win_arm64.whl", hash = "sha256:c22e56c6a0b70598a66e456de5272f76088bc623688da84ef403148a6d41851d", size = 26219, upload-time = "2025-11-14T09:51:03.563Z" }, +] + +[[package]] +name = "nest-asyncio" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + +[[package]] +name = "numpy" +version = "2.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/fd/0005efbd0af48e55eb3c7208af93f2862d4b1a56cd78e84309a2d959208d/numpy-2.4.2.tar.gz", hash = "sha256:659a6107e31a83c4e33f763942275fd278b21d095094044eb35569e86a21ddae", size = 20723651, upload-time = "2026-01-31T23:13:10.135Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/6e/6f394c9c77668153e14d4da83bcc247beb5952f6ead7699a1a2992613bea/numpy-2.4.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:21982668592194c609de53ba4933a7471880ccbaadcc52352694a59ecc860b3a", size = 16667963, upload-time = "2026-01-31T23:10:52.147Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f8/55483431f2b2fd015ae6ed4fe62288823ce908437ed49db5a03d15151678/numpy-2.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40397bda92382fcec844066efb11f13e1c9a3e2a8e8f318fb72ed8b6db9f60f1", size = 14693571, upload-time = "2026-01-31T23:10:54.789Z" }, + { url = "https://files.pythonhosted.org/packages/2f/20/18026832b1845cdc82248208dd929ca14c9d8f2bac391f67440707fff27c/numpy-2.4.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:b3a24467af63c67829bfaa61eecf18d5432d4f11992688537be59ecd6ad32f5e", size = 5203469, upload-time = "2026-01-31T23:10:57.343Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/2eb97c8a77daaba34eaa3fa7241a14ac5f51c46a6bd5911361b644c4a1e2/numpy-2.4.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:805cc8de9fd6e7a22da5aed858e0ab16be5a4db6c873dde1d7451c541553aa27", size = 6550820, upload-time = "2026-01-31T23:10:59.429Z" }, + { url = "https://files.pythonhosted.org/packages/b1/91/b97fdfd12dc75b02c44e26c6638241cc004d4079a0321a69c62f51470c4c/numpy-2.4.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d82351358ffbcdcd7b686b90742a9b86632d6c1c051016484fa0b326a0a1548", size = 15663067, upload-time = "2026-01-31T23:11:01.291Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c6/a18e59f3f0b8071cc85cbc8d80cd02d68aa9710170b2553a117203d46936/numpy-2.4.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e35d3e0144137d9fdae62912e869136164534d64a169f86438bc9561b6ad49f", size = 16619782, upload-time = "2026-01-31T23:11:03.669Z" }, + { url = "https://files.pythonhosted.org/packages/b7/83/9751502164601a79e18847309f5ceec0b1446d7b6aa12305759b72cf98b2/numpy-2.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adb6ed2ad29b9e15321d167d152ee909ec73395901b70936f029c3bc6d7f4460", size = 17013128, upload-time = "2026-01-31T23:11:05.913Z" }, + { url = "https://files.pythonhosted.org/packages/61/c4/c4066322256ec740acc1c8923a10047818691d2f8aec254798f3dd90f5f2/numpy-2.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8906e71fd8afcb76580404e2a950caef2685df3d2a57fe82a86ac8d33cc007ba", size = 18345324, upload-time = "2026-01-31T23:11:08.248Z" }, + { url = "https://files.pythonhosted.org/packages/ab/af/6157aa6da728fa4525a755bfad486ae7e3f76d4c1864138003eb84328497/numpy-2.4.2-cp312-cp312-win32.whl", hash = "sha256:ec055f6dae239a6299cace477b479cca2fc125c5675482daf1dd886933a1076f", size = 5960282, upload-time = "2026-01-31T23:11:10.497Z" }, + { url = "https://files.pythonhosted.org/packages/92/0f/7ceaaeaacb40567071e94dbf2c9480c0ae453d5bb4f52bea3892c39dc83c/numpy-2.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:209fae046e62d0ce6435fcfe3b1a10537e858249b3d9b05829e2a05218296a85", size = 12314210, upload-time = "2026-01-31T23:11:12.176Z" }, + { url = "https://files.pythonhosted.org/packages/2f/a3/56c5c604fae6dd40fa2ed3040d005fca97e91bd320d232ac9931d77ba13c/numpy-2.4.2-cp312-cp312-win_arm64.whl", hash = "sha256:fbde1b0c6e81d56f5dccd95dd4a711d9b95df1ae4009a60887e56b27e8d903fa", size = 10220171, upload-time = "2026-01-31T23:11:14.684Z" }, + { url = "https://files.pythonhosted.org/packages/a1/22/815b9fe25d1d7ae7d492152adbc7226d3eff731dffc38fe970589fcaaa38/numpy-2.4.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:25f2059807faea4b077a2b6837391b5d830864b3543627f381821c646f31a63c", size = 16663696, upload-time = "2026-01-31T23:11:17.516Z" }, + { url = "https://files.pythonhosted.org/packages/09/f0/817d03a03f93ba9c6c8993de509277d84e69f9453601915e4a69554102a1/numpy-2.4.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bd3a7a9f5847d2fb8c2c6d1c862fa109c31a9abeca1a3c2bd5a64572955b2979", size = 14688322, upload-time = "2026-01-31T23:11:19.883Z" }, + { url = "https://files.pythonhosted.org/packages/da/b4/f805ab79293c728b9a99438775ce51885fd4f31b76178767cfc718701a39/numpy-2.4.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8e4549f8a3c6d13d55041925e912bfd834285ef1dd64d6bc7d542583355e2e98", size = 5198157, upload-time = "2026-01-31T23:11:22.375Z" }, + { url = "https://files.pythonhosted.org/packages/74/09/826e4289844eccdcd64aac27d13b0fd3f32039915dd5b9ba01baae1f436c/numpy-2.4.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:aea4f66ff44dfddf8c2cffd66ba6538c5ec67d389285292fe428cb2c738c8aef", size = 6546330, upload-time = "2026-01-31T23:11:23.958Z" }, + { url = "https://files.pythonhosted.org/packages/19/fb/cbfdbfa3057a10aea5422c558ac57538e6acc87ec1669e666d32ac198da7/numpy-2.4.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3cd545784805de05aafe1dde61752ea49a359ccba9760c1e5d1c88a93bbf2b7", size = 15660968, upload-time = "2026-01-31T23:11:25.713Z" }, + { url = "https://files.pythonhosted.org/packages/04/dc/46066ce18d01645541f0186877377b9371b8fa8017fa8262002b4ef22612/numpy-2.4.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0d9b7c93578baafcbc5f0b83eaf17b79d345c6f36917ba0c67f45226911d499", size = 16607311, upload-time = "2026-01-31T23:11:28.117Z" }, + { url = "https://files.pythonhosted.org/packages/14/d9/4b5adfc39a43fa6bf918c6d544bc60c05236cc2f6339847fc5b35e6cb5b0/numpy-2.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f74f0f7779cc7ae07d1810aab8ac6b1464c3eafb9e283a40da7309d5e6e48fbb", size = 17012850, upload-time = "2026-01-31T23:11:30.888Z" }, + { url = "https://files.pythonhosted.org/packages/b7/20/adb6e6adde6d0130046e6fdfb7675cc62bc2f6b7b02239a09eb58435753d/numpy-2.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7ac672d699bf36275c035e16b65539931347d68b70667d28984c9fb34e07fa7", size = 18334210, upload-time = "2026-01-31T23:11:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/78/0e/0a73b3dff26803a8c02baa76398015ea2a5434d9b8265a7898a6028c1591/numpy-2.4.2-cp313-cp313-win32.whl", hash = "sha256:8e9afaeb0beff068b4d9cd20d322ba0ee1cecfb0b08db145e4ab4dd44a6b5110", size = 5958199, upload-time = "2026-01-31T23:11:35.385Z" }, + { url = "https://files.pythonhosted.org/packages/43/bc/6352f343522fcb2c04dbaf94cb30cca6fd32c1a750c06ad6231b4293708c/numpy-2.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:7df2de1e4fba69a51c06c28f5a3de36731eb9639feb8e1cf7e4a7b0daf4cf622", size = 12310848, upload-time = "2026-01-31T23:11:38.001Z" }, + { url = "https://files.pythonhosted.org/packages/6e/8d/6da186483e308da5da1cc6918ce913dcfe14ffde98e710bfeff2a6158d4e/numpy-2.4.2-cp313-cp313-win_arm64.whl", hash = "sha256:0fece1d1f0a89c16b03442eae5c56dc0be0c7883b5d388e0c03f53019a4bfd71", size = 10221082, upload-time = "2026-01-31T23:11:40.392Z" }, + { url = "https://files.pythonhosted.org/packages/25/a1/9510aa43555b44781968935c7548a8926274f815de42ad3997e9e83680dd/numpy-2.4.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5633c0da313330fd20c484c78cdd3f9b175b55e1a766c4a174230c6b70ad8262", size = 14815866, upload-time = "2026-01-31T23:11:42.495Z" }, + { url = "https://files.pythonhosted.org/packages/36/30/6bbb5e76631a5ae46e7923dd16ca9d3f1c93cfa8d4ed79a129814a9d8db3/numpy-2.4.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d9f64d786b3b1dd742c946c42d15b07497ed14af1a1f3ce840cce27daa0ce913", size = 5325631, upload-time = "2026-01-31T23:11:44.7Z" }, + { url = "https://files.pythonhosted.org/packages/46/00/3a490938800c1923b567b3a15cd17896e68052e2145d8662aaf3e1ffc58f/numpy-2.4.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:b21041e8cb6a1eb5312dd1d2f80a94d91efffb7a06b70597d44f1bd2dfc315ab", size = 6646254, upload-time = "2026-01-31T23:11:46.341Z" }, + { url = "https://files.pythonhosted.org/packages/d3/e9/fac0890149898a9b609caa5af7455a948b544746e4b8fe7c212c8edd71f8/numpy-2.4.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00ab83c56211a1d7c07c25e3217ea6695e50a3e2f255053686b081dc0b091a82", size = 15720138, upload-time = "2026-01-31T23:11:48.082Z" }, + { url = "https://files.pythonhosted.org/packages/ea/5c/08887c54e68e1e28df53709f1893ce92932cc6f01f7c3d4dc952f61ffd4e/numpy-2.4.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fb882da679409066b4603579619341c6d6898fc83a8995199d5249f986e8e8f", size = 16655398, upload-time = "2026-01-31T23:11:50.293Z" }, + { url = "https://files.pythonhosted.org/packages/4d/89/253db0fa0e66e9129c745e4ef25631dc37d5f1314dad2b53e907b8538e6d/numpy-2.4.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:66cb9422236317f9d44b67b4d18f44efe6e9c7f8794ac0462978513359461554", size = 17079064, upload-time = "2026-01-31T23:11:52.927Z" }, + { url = "https://files.pythonhosted.org/packages/2a/d5/cbade46ce97c59c6c3da525e8d95b7abe8a42974a1dc5c1d489c10433e88/numpy-2.4.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0f01dcf33e73d80bd8dc0f20a71303abbafa26a19e23f6b68d1aa9990af90257", size = 18379680, upload-time = "2026-01-31T23:11:55.22Z" }, + { url = "https://files.pythonhosted.org/packages/40/62/48f99ae172a4b63d981babe683685030e8a3df4f246c893ea5c6ef99f018/numpy-2.4.2-cp313-cp313t-win32.whl", hash = "sha256:52b913ec40ff7ae845687b0b34d8d93b60cb66dcee06996dd5c99f2fc9328657", size = 6082433, upload-time = "2026-01-31T23:11:58.096Z" }, + { url = "https://files.pythonhosted.org/packages/07/38/e054a61cfe48ad9f1ed0d188e78b7e26859d0b60ef21cd9de4897cdb5326/numpy-2.4.2-cp313-cp313t-win_amd64.whl", hash = "sha256:5eea80d908b2c1f91486eb95b3fb6fab187e569ec9752ab7d9333d2e66bf2d6b", size = 12451181, upload-time = "2026-01-31T23:11:59.782Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a4/a05c3a6418575e185dd84d0b9680b6bb2e2dc3e4202f036b7b4e22d6e9dc/numpy-2.4.2-cp313-cp313t-win_arm64.whl", hash = "sha256:fd49860271d52127d61197bb50b64f58454e9f578cb4b2c001a6de8b1f50b0b1", size = 10290756, upload-time = "2026-01-31T23:12:02.438Z" }, + { url = "https://files.pythonhosted.org/packages/18/88/b7df6050bf18fdcfb7046286c6535cabbdd2064a3440fca3f069d319c16e/numpy-2.4.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:444be170853f1f9d528428eceb55f12918e4fda5d8805480f36a002f1415e09b", size = 16663092, upload-time = "2026-01-31T23:12:04.521Z" }, + { url = "https://files.pythonhosted.org/packages/25/7a/1fee4329abc705a469a4afe6e69b1ef7e915117747886327104a8493a955/numpy-2.4.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d1240d50adff70c2a88217698ca844723068533f3f5c5fa6ee2e3220e3bdb000", size = 14698770, upload-time = "2026-01-31T23:12:06.96Z" }, + { url = "https://files.pythonhosted.org/packages/fb/0b/f9e49ba6c923678ad5bc38181c08ac5e53b7a5754dbca8e581aa1a56b1ff/numpy-2.4.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:7cdde6de52fb6664b00b056341265441192d1291c130e99183ec0d4b110ff8b1", size = 5208562, upload-time = "2026-01-31T23:12:09.632Z" }, + { url = "https://files.pythonhosted.org/packages/7d/12/d7de8f6f53f9bb76997e5e4c069eda2051e3fe134e9181671c4391677bb2/numpy-2.4.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:cda077c2e5b780200b6b3e09d0b42205a3d1c68f30c6dceb90401c13bff8fe74", size = 6543710, upload-time = "2026-01-31T23:12:11.969Z" }, + { url = "https://files.pythonhosted.org/packages/09/63/c66418c2e0268a31a4cf8a8b512685748200f8e8e8ec6c507ce14e773529/numpy-2.4.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d30291931c915b2ab5717c2974bb95ee891a1cf22ebc16a8006bd59cd210d40a", size = 15677205, upload-time = "2026-01-31T23:12:14.33Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6c/7f237821c9642fb2a04d2f1e88b4295677144ca93285fd76eff3bcba858d/numpy-2.4.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bba37bc29d4d85761deed3954a1bc62be7cf462b9510b51d367b769a8c8df325", size = 16611738, upload-time = "2026-01-31T23:12:16.525Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a7/39c4cdda9f019b609b5c473899d87abff092fc908cfe4d1ecb2fcff453b0/numpy-2.4.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b2f0073ed0868db1dcd86e052d37279eef185b9c8db5bf61f30f46adac63c909", size = 17028888, upload-time = "2026-01-31T23:12:19.306Z" }, + { url = "https://files.pythonhosted.org/packages/da/b3/e84bb64bdfea967cc10950d71090ec2d84b49bc691df0025dddb7c26e8e3/numpy-2.4.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7f54844851cdb630ceb623dcec4db3240d1ac13d4990532446761baede94996a", size = 18339556, upload-time = "2026-01-31T23:12:21.816Z" }, + { url = "https://files.pythonhosted.org/packages/88/f5/954a291bc1192a27081706862ac62bb5920fbecfbaa302f64682aa90beed/numpy-2.4.2-cp314-cp314-win32.whl", hash = "sha256:12e26134a0331d8dbd9351620f037ec470b7c75929cb8a1537f6bfe411152a1a", size = 6006899, upload-time = "2026-01-31T23:12:24.14Z" }, + { url = "https://files.pythonhosted.org/packages/05/cb/eff72a91b2efdd1bc98b3b8759f6a1654aa87612fc86e3d87d6fe4f948c4/numpy-2.4.2-cp314-cp314-win_amd64.whl", hash = "sha256:068cdb2d0d644cdb45670810894f6a0600797a69c05f1ac478e8d31670b8ee75", size = 12443072, upload-time = "2026-01-31T23:12:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/37/75/62726948db36a56428fce4ba80a115716dc4fad6a3a4352487f8bb950966/numpy-2.4.2-cp314-cp314-win_arm64.whl", hash = "sha256:6ed0be1ee58eef41231a5c943d7d1375f093142702d5723ca2eb07db9b934b05", size = 10494886, upload-time = "2026-01-31T23:12:28.488Z" }, + { url = "https://files.pythonhosted.org/packages/36/2f/ee93744f1e0661dc267e4b21940870cabfae187c092e1433b77b09b50ac4/numpy-2.4.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:98f16a80e917003a12c0580f97b5f875853ebc33e2eaa4bccfc8201ac6869308", size = 14818567, upload-time = "2026-01-31T23:12:30.709Z" }, + { url = "https://files.pythonhosted.org/packages/a7/24/6535212add7d76ff938d8bdc654f53f88d35cddedf807a599e180dcb8e66/numpy-2.4.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:20abd069b9cda45874498b245c8015b18ace6de8546bf50dfa8cea1696ed06ef", size = 5328372, upload-time = "2026-01-31T23:12:32.962Z" }, + { url = "https://files.pythonhosted.org/packages/5e/9d/c48f0a035725f925634bf6b8994253b43f2047f6778a54147d7e213bc5a7/numpy-2.4.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:e98c97502435b53741540a5717a6749ac2ada901056c7db951d33e11c885cc7d", size = 6649306, upload-time = "2026-01-31T23:12:34.797Z" }, + { url = "https://files.pythonhosted.org/packages/81/05/7c73a9574cd4a53a25907bad38b59ac83919c0ddc8234ec157f344d57d9a/numpy-2.4.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da6cad4e82cb893db4b69105c604d805e0c3ce11501a55b5e9f9083b47d2ffe8", size = 15722394, upload-time = "2026-01-31T23:12:36.565Z" }, + { url = "https://files.pythonhosted.org/packages/35/fa/4de10089f21fc7d18442c4a767ab156b25c2a6eaf187c0db6d9ecdaeb43f/numpy-2.4.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e4424677ce4b47fe73c8b5556d876571f7c6945d264201180db2dc34f676ab5", size = 16653343, upload-time = "2026-01-31T23:12:39.188Z" }, + { url = "https://files.pythonhosted.org/packages/b8/f9/d33e4ffc857f3763a57aa85650f2e82486832d7492280ac21ba9efda80da/numpy-2.4.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2b8f157c8a6f20eb657e240f8985cc135598b2b46985c5bccbde7616dc9c6b1e", size = 17078045, upload-time = "2026-01-31T23:12:42.041Z" }, + { url = "https://files.pythonhosted.org/packages/c8/b8/54bdb43b6225badbea6389fa038c4ef868c44f5890f95dd530a218706da3/numpy-2.4.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5daf6f3914a733336dab21a05cdec343144600e964d2fcdabaac0c0269874b2a", size = 18380024, upload-time = "2026-01-31T23:12:44.331Z" }, + { url = "https://files.pythonhosted.org/packages/a5/55/6e1a61ded7af8df04016d81b5b02daa59f2ea9252ee0397cb9f631efe9e5/numpy-2.4.2-cp314-cp314t-win32.whl", hash = "sha256:8c50dd1fc8826f5b26a5ee4d77ca55d88a895f4e4819c7ecc2a9f5905047a443", size = 6153937, upload-time = "2026-01-31T23:12:47.229Z" }, + { url = "https://files.pythonhosted.org/packages/45/aa/fa6118d1ed6d776b0983f3ceac9b1a5558e80df9365b1c3aa6d42bf9eee4/numpy-2.4.2-cp314-cp314t-win_amd64.whl", hash = "sha256:fcf92bee92742edd401ba41135185866f7026c502617f422eb432cfeca4fe236", size = 12631844, upload-time = "2026-01-31T23:12:48.997Z" }, + { url = "https://files.pythonhosted.org/packages/32/0a/2ec5deea6dcd158f254a7b372fb09cfba5719419c8d66343bab35237b3fb/numpy-2.4.2-cp314-cp314t-win_arm64.whl", hash = "sha256:1f92f53998a17265194018d1cc321b2e96e900ca52d54c7c77837b71b9465181", size = 10565379, upload-time = "2026-01-31T23:12:51.345Z" }, +] + +[[package]] +name = "onnxruntime" +version = "1.24.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flatbuffers" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "protobuf" }, + { name = "sympy" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/1c/38af1cfe82c75d2b205eb5019834b0f2b0b6647ec8a20a3086168e413570/onnxruntime-1.24.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:d8a50b422d45c0144864c0977d04ad4fa50a8a48e5153056ab1f7d06ea9fc3e2", size = 17217857, upload-time = "2026-02-19T17:15:14.297Z" }, + { url = "https://files.pythonhosted.org/packages/01/8a/e2d4332ae18d6383376e75141cd914256bee12c3cc439f42260eb176ceb9/onnxruntime-1.24.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76c44fc9a89dcefcd5a4ab5c6bbbb9ff1604325ab2d5d0bc9ff5a9cba7b37f4a", size = 15027167, upload-time = "2026-02-19T17:13:21.92Z" }, + { url = "https://files.pythonhosted.org/packages/35/af/ad86cfbfd65d5a86204b3a30893e92c0cf3f1a56280efc5a12e69d81f52d/onnxruntime-1.24.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:09aa6f8d766b4afc3cfba68dd10be39586b49f9462fbd1386c5d5644239461ca", size = 17106547, upload-time = "2026-02-19T17:14:05.758Z" }, + { url = "https://files.pythonhosted.org/packages/ee/62/9d725326f933bf8323e309956a17e52d33fb59d35bb5dda1886f94352938/onnxruntime-1.24.2-cp312-cp312-win_amd64.whl", hash = "sha256:ebcee9276420a65e5fa08b05f18379c2271b5992617e5bdc0d0d6c5ea395c1a1", size = 12506161, upload-time = "2026-02-19T17:14:59.377Z" }, + { url = "https://files.pythonhosted.org/packages/aa/a9/7b06efd5802db881860d961a7cb4efacb058ed694c1c8f096c0c1499d017/onnxruntime-1.24.2-cp312-cp312-win_arm64.whl", hash = "sha256:8d770a934513f6e17937baf3438eaaec5983a23cdaedb81c9fc0dfcf26831c24", size = 12169884, upload-time = "2026-02-19T17:14:49.962Z" }, + { url = "https://files.pythonhosted.org/packages/9c/98/8f5b9ae63f7f6dd5fb2d192454b915ec966a421fdd0effeeef5be7f7221f/onnxruntime-1.24.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:038ebcd8363c3835ea83eed66129e1d11d8219438892dfb7dc7656c4d4dfa1f9", size = 17217884, upload-time = "2026-02-19T17:13:36.193Z" }, + { url = "https://files.pythonhosted.org/packages/55/e6/dc4dc59565c93506c45017c0dd3f536f6d1b7bc97047821af13fba2e3def/onnxruntime-1.24.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8235cc11e118ad749c497ba93288c04073eccd8cc6cc508c8a7988ae36ab52d8", size = 15026995, upload-time = "2026-02-19T17:13:25.029Z" }, + { url = "https://files.pythonhosted.org/packages/ac/62/6f2851cf3237a91bc04cdb35434293a623d4f6369f79836929600da574ba/onnxruntime-1.24.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e92b46cc6d8be4286436a05382a881c88d85a2ae1ea9cfe5e6fab89f2c3e89cc", size = 17106308, upload-time = "2026-02-19T17:14:09.817Z" }, + { url = "https://files.pythonhosted.org/packages/62/5a/1e2b874daf24f26e98af14281fdbdd6ae1ed548ba471c01ea2a3084c55bb/onnxruntime-1.24.2-cp313-cp313-win_amd64.whl", hash = "sha256:1fd824ee4f6fb811bc47ffec2b25f129f31a087214ca91c8b4f6fda32962b78f", size = 12506095, upload-time = "2026-02-19T17:15:02.434Z" }, + { url = "https://files.pythonhosted.org/packages/2d/6f/8fac5eecb94f861d56a43ede3c2ebcdce60132952d3b72003f3e3d91483c/onnxruntime-1.24.2-cp313-cp313-win_arm64.whl", hash = "sha256:d8cf0acbf90771fff012c33eb2749e8aca2a8b4c66c672f30ee77c140a6fba5b", size = 12168564, upload-time = "2026-02-19T17:14:52.28Z" }, + { url = "https://files.pythonhosted.org/packages/35/e4/7dfed3f445f7289a0abff709d012439c6c901915390704dd918e5f47aad3/onnxruntime-1.24.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e22fb5d9ac51b61f50cca155ce2927576cc2c42501ede6c0df23a1aeb070bdd5", size = 15036844, upload-time = "2026-02-19T17:13:27.928Z" }, + { url = "https://files.pythonhosted.org/packages/90/45/9d52397e30b0d8c1692afcec5184ca9372ff4d6b0f6039bba9ad479a2563/onnxruntime-1.24.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2956f5220e7be8b09482ae5726caabf78eb549142cdb28523191a38e57fb6119", size = 17117779, upload-time = "2026-02-19T17:14:13.862Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c8/2321cd06ddbb4321326df365ccb8345cdb4e05643f539729f3943c706e97/onnxruntime-1.24.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:487e3fdedc24bc93f2acdf47c622de49b3999fb5754e7cfa466e5533a0215051", size = 17219405, upload-time = "2026-02-19T17:13:39.925Z" }, + { url = "https://files.pythonhosted.org/packages/ad/ff/a2cdf95d2647f2a5076eb3fc49ae662e375c4eb5c7b6b675f910f96c8e15/onnxruntime-1.24.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c33398bd6ab1a6b7de9410af7360cd8b6312bc0c4848ddb738456c13dfbec4b", size = 15027713, upload-time = "2026-02-19T17:13:30.693Z" }, + { url = "https://files.pythonhosted.org/packages/0d/74/a1913b3a0fc2f27fe1751e9545745a3f35fd7833e3438a4208b4e215778f/onnxruntime-1.24.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2658b3ce6cb33bdeddfcd74c6da509510310717611220cf2106e6c401febabe5", size = 17106108, upload-time = "2026-02-19T17:14:16.619Z" }, + { url = "https://files.pythonhosted.org/packages/0a/bd/fca80d282bca9848b2c8e101c764432dd61a0e9d2377d1c8b3bab13235d0/onnxruntime-1.24.2-cp314-cp314-win_amd64.whl", hash = "sha256:45b4f68ffec95b2cc0dc96b2b413f69ace9a80a0e5400023c5ac61f73a7a3fdf", size = 12808967, upload-time = "2026-02-19T17:15:05.1Z" }, + { url = "https://files.pythonhosted.org/packages/6d/eb/6b154dd61cac410cacf27a9f53bbf49f4dbfe5b3982f3f5b0247c7bf7b78/onnxruntime-1.24.2-cp314-cp314-win_arm64.whl", hash = "sha256:6c501aaaaa674e689aaac501e26eb96aba908ebc067fe761fbcbed868bd694a6", size = 12491892, upload-time = "2026-02-19T17:14:54.584Z" }, + { url = "https://files.pythonhosted.org/packages/6f/84/14e5e804836476d3ef6ac07afe3ed6bdf01b69f8ef3ce6ae82c6c80b6d62/onnxruntime-1.24.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5360d3fd9c08ce17fff757759ce4b152852be14d597130f41174d8271f954630", size = 15036834, upload-time = "2026-02-19T17:13:33.65Z" }, + { url = "https://files.pythonhosted.org/packages/3a/27/ecdd3ae7d49d9f54820ededce2d88ddc3333b9ac9bb5f1d0d6aa3148c686/onnxruntime-1.24.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05a2792b5ef9278a89415a1f39d0a22192a872168257100503a5157165a38e7b", size = 17117770, upload-time = "2026-02-19T17:14:20.048Z" }, +] + +[[package]] +name = "openai" +version = "2.24.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/13/17e87641b89b74552ed408a92b231283786523edddc95f3545809fab673c/openai-2.24.0.tar.gz", hash = "sha256:1e5769f540dbd01cb33bc4716a23e67b9d695161a734aff9c5f925e2bf99a673", size = 658717, upload-time = "2026-02-24T20:02:07.958Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/30/844dc675ee6902579b8eef01ed23917cc9319a1c9c0c14ec6e39340c96d0/openai-2.24.0-py3-none-any.whl", hash = "sha256:fed30480d7d6c884303287bde864980a4b137b60553ffbcf9ab4a233b7a73d94", size = 1120122, upload-time = "2026-02-24T20:02:05.669Z" }, +] + +[[package]] +name = "openapi-pydantic" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/2e/58d83848dd1a79cb92ed8e63f6ba901ca282c5f09d04af9423ec26c56fd7/openapi_pydantic-0.5.1.tar.gz", hash = "sha256:ff6835af6bde7a459fb93eb93bb92b8749b754fc6e51b2f1590a19dc3005ee0d", size = 60892, upload-time = "2025-01-08T19:29:27.083Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/cf/03675d8bd8ecbf4445504d8071adab19f5f993676795708e36402ab38263/openapi_pydantic-0.5.1-py3-none-any.whl", hash = "sha256:a3a09ef4586f5bd760a8df7f43028b60cafb6d9f61de2acba9574766255ab146", size = 96381, upload-time = "2025-01-08T19:29:25.275Z" }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.39.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/97/b9/3161be15bb8e3ad01be8be5a968a9237c3027c5be504362ff800fca3e442/opentelemetry_api-1.39.1.tar.gz", hash = "sha256:fbde8c80e1b937a2c61f20347e91c0c18a1940cecf012d62e65a7caf08967c9c", size = 65767, upload-time = "2025-12-11T13:32:39.182Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/df/d3f1ddf4bb4cb50ed9b1139cc7b1c54c34a1e7ce8fd1b9a37c0d1551a6bd/opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950", size = 66356, upload-time = "2025-12-11T13:32:17.304Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "pathable" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/55/b748445cb4ea6b125626f15379be7c96d1035d4fa3e8fee362fa92298abf/pathable-0.5.0.tar.gz", hash = "sha256:d81938348a1cacb525e7c75166270644782c0fb9c8cecc16be033e71427e0ef1", size = 16655, upload-time = "2026-02-20T08:47:00.748Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/96/5a770e5c461462575474468e5af931cff9de036e7c2b4fea23c1c58d2cbe/pathable-0.5.0-py3-none-any.whl", hash = "sha256:646e3d09491a6351a0c82632a09c02cdf70a252e73196b36d8a15ba0a114f0a6", size = 16867, upload-time = "2026-02-20T08:46:59.536Z" }, +] + +[[package]] +name = "pillow" +version = "11.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069, upload-time = "2025-07-01T09:16:30.666Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/fe/1bc9b3ee13f68487a99ac9529968035cca2f0a51ec36892060edcc51d06a/pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4", size = 5278800, upload-time = "2025-07-01T09:14:17.648Z" }, + { url = "https://files.pythonhosted.org/packages/2c/32/7e2ac19b5713657384cec55f89065fb306b06af008cfd87e572035b27119/pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69", size = 4686296, upload-time = "2025-07-01T09:14:19.828Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1e/b9e12bbe6e4c2220effebc09ea0923a07a6da1e1f1bfbc8d7d29a01ce32b/pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d", size = 5871726, upload-time = "2025-07-03T13:10:04.448Z" }, + { url = "https://files.pythonhosted.org/packages/8d/33/e9200d2bd7ba00dc3ddb78df1198a6e80d7669cce6c2bdbeb2530a74ec58/pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6", size = 7644652, upload-time = "2025-07-03T13:10:10.391Z" }, + { url = "https://files.pythonhosted.org/packages/41/f1/6f2427a26fc683e00d985bc391bdd76d8dd4e92fac33d841127eb8fb2313/pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7", size = 5977787, upload-time = "2025-07-01T09:14:21.63Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c9/06dd4a38974e24f932ff5f98ea3c546ce3f8c995d3f0985f8e5ba48bba19/pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024", size = 6645236, upload-time = "2025-07-01T09:14:23.321Z" }, + { url = "https://files.pythonhosted.org/packages/40/e7/848f69fb79843b3d91241bad658e9c14f39a32f71a301bcd1d139416d1be/pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809", size = 6086950, upload-time = "2025-07-01T09:14:25.237Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1a/7cff92e695a2a29ac1958c2a0fe4c0b2393b60aac13b04a4fe2735cad52d/pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d", size = 6723358, upload-time = "2025-07-01T09:14:27.053Z" }, + { url = "https://files.pythonhosted.org/packages/26/7d/73699ad77895f69edff76b0f332acc3d497f22f5d75e5360f78cbcaff248/pillow-11.3.0-cp312-cp312-win32.whl", hash = "sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149", size = 6275079, upload-time = "2025-07-01T09:14:30.104Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ce/e7dfc873bdd9828f3b6e5c2bbb74e47a98ec23cc5c74fc4e54462f0d9204/pillow-11.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d", size = 6986324, upload-time = "2025-07-01T09:14:31.899Z" }, + { url = "https://files.pythonhosted.org/packages/16/8f/b13447d1bf0b1f7467ce7d86f6e6edf66c0ad7cf44cf5c87a37f9bed9936/pillow-11.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542", size = 2423067, upload-time = "2025-07-01T09:14:33.709Z" }, + { url = "https://files.pythonhosted.org/packages/1e/93/0952f2ed8db3a5a4c7a11f91965d6184ebc8cd7cbb7941a260d5f018cd2d/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd", size = 2128328, upload-time = "2025-07-01T09:14:35.276Z" }, + { url = "https://files.pythonhosted.org/packages/4b/e8/100c3d114b1a0bf4042f27e0f87d2f25e857e838034e98ca98fe7b8c0a9c/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8", size = 2170652, upload-time = "2025-07-01T09:14:37.203Z" }, + { url = "https://files.pythonhosted.org/packages/aa/86/3f758a28a6e381758545f7cdb4942e1cb79abd271bea932998fc0db93cb6/pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f", size = 2227443, upload-time = "2025-07-01T09:14:39.344Z" }, + { url = "https://files.pythonhosted.org/packages/01/f4/91d5b3ffa718df2f53b0dc109877993e511f4fd055d7e9508682e8aba092/pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c", size = 5278474, upload-time = "2025-07-01T09:14:41.843Z" }, + { url = "https://files.pythonhosted.org/packages/f9/0e/37d7d3eca6c879fbd9dba21268427dffda1ab00d4eb05b32923d4fbe3b12/pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd", size = 4686038, upload-time = "2025-07-01T09:14:44.008Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b0/3426e5c7f6565e752d81221af9d3676fdbb4f352317ceafd42899aaf5d8a/pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e", size = 5864407, upload-time = "2025-07-03T13:10:15.628Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c1/c6c423134229f2a221ee53f838d4be9d82bab86f7e2f8e75e47b6bf6cd77/pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1", size = 7639094, upload-time = "2025-07-03T13:10:21.857Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c9/09e6746630fe6372c67c648ff9deae52a2bc20897d51fa293571977ceb5d/pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805", size = 5973503, upload-time = "2025-07-01T09:14:45.698Z" }, + { url = "https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8", size = 6642574, upload-time = "2025-07-01T09:14:47.415Z" }, + { url = "https://files.pythonhosted.org/packages/36/de/d5cc31cc4b055b6c6fd990e3e7f0f8aaf36229a2698501bcb0cdf67c7146/pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2", size = 6084060, upload-time = "2025-07-01T09:14:49.636Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ea/502d938cbaeec836ac28a9b730193716f0114c41325db428e6b280513f09/pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b", size = 6721407, upload-time = "2025-07-01T09:14:51.962Z" }, + { url = "https://files.pythonhosted.org/packages/45/9c/9c5e2a73f125f6cbc59cc7087c8f2d649a7ae453f83bd0362ff7c9e2aee2/pillow-11.3.0-cp313-cp313-win32.whl", hash = "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3", size = 6273841, upload-time = "2025-07-01T09:14:54.142Z" }, + { url = "https://files.pythonhosted.org/packages/23/85/397c73524e0cd212067e0c969aa245b01d50183439550d24d9f55781b776/pillow-11.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51", size = 6978450, upload-time = "2025-07-01T09:14:56.436Z" }, + { url = "https://files.pythonhosted.org/packages/17/d2/622f4547f69cd173955194b78e4d19ca4935a1b0f03a302d655c9f6aae65/pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580", size = 2423055, upload-time = "2025-07-01T09:14:58.072Z" }, + { url = "https://files.pythonhosted.org/packages/dd/80/a8a2ac21dda2e82480852978416cfacd439a4b490a501a288ecf4fe2532d/pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e", size = 5281110, upload-time = "2025-07-01T09:14:59.79Z" }, + { url = "https://files.pythonhosted.org/packages/44/d6/b79754ca790f315918732e18f82a8146d33bcd7f4494380457ea89eb883d/pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d", size = 4689547, upload-time = "2025-07-01T09:15:01.648Z" }, + { url = "https://files.pythonhosted.org/packages/49/20/716b8717d331150cb00f7fdd78169c01e8e0c219732a78b0e59b6bdb2fd6/pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced", size = 5901554, upload-time = "2025-07-03T13:10:27.018Z" }, + { url = "https://files.pythonhosted.org/packages/74/cf/a9f3a2514a65bb071075063a96f0a5cf949c2f2fce683c15ccc83b1c1cab/pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c", size = 7669132, upload-time = "2025-07-03T13:10:33.01Z" }, + { url = "https://files.pythonhosted.org/packages/98/3c/da78805cbdbee9cb43efe8261dd7cc0b4b93f2ac79b676c03159e9db2187/pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8", size = 6005001, upload-time = "2025-07-01T09:15:03.365Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fa/ce044b91faecf30e635321351bba32bab5a7e034c60187fe9698191aef4f/pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59", size = 6668814, upload-time = "2025-07-01T09:15:05.655Z" }, + { url = "https://files.pythonhosted.org/packages/7b/51/90f9291406d09bf93686434f9183aba27b831c10c87746ff49f127ee80cb/pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe", size = 6113124, upload-time = "2025-07-01T09:15:07.358Z" }, + { url = "https://files.pythonhosted.org/packages/cd/5a/6fec59b1dfb619234f7636d4157d11fb4e196caeee220232a8d2ec48488d/pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c", size = 6747186, upload-time = "2025-07-01T09:15:09.317Z" }, + { url = "https://files.pythonhosted.org/packages/49/6b/00187a044f98255225f172de653941e61da37104a9ea60e4f6887717e2b5/pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788", size = 6277546, upload-time = "2025-07-01T09:15:11.311Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5c/6caaba7e261c0d75bab23be79f1d06b5ad2a2ae49f028ccec801b0e853d6/pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31", size = 6985102, upload-time = "2025-07-01T09:15:13.164Z" }, + { url = "https://files.pythonhosted.org/packages/f3/7e/b623008460c09a0cb38263c93b828c666493caee2eb34ff67f778b87e58c/pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e", size = 2424803, upload-time = "2025-07-01T09:15:15.695Z" }, + { url = "https://files.pythonhosted.org/packages/73/f4/04905af42837292ed86cb1b1dabe03dce1edc008ef14c473c5c7e1443c5d/pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12", size = 5278520, upload-time = "2025-07-01T09:15:17.429Z" }, + { url = "https://files.pythonhosted.org/packages/41/b0/33d79e377a336247df6348a54e6d2a2b85d644ca202555e3faa0cf811ecc/pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a", size = 4686116, upload-time = "2025-07-01T09:15:19.423Z" }, + { url = "https://files.pythonhosted.org/packages/49/2d/ed8bc0ab219ae8768f529597d9509d184fe8a6c4741a6864fea334d25f3f/pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632", size = 5864597, upload-time = "2025-07-03T13:10:38.404Z" }, + { url = "https://files.pythonhosted.org/packages/b5/3d/b932bb4225c80b58dfadaca9d42d08d0b7064d2d1791b6a237f87f661834/pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673", size = 7638246, upload-time = "2025-07-03T13:10:44.987Z" }, + { url = "https://files.pythonhosted.org/packages/09/b5/0487044b7c096f1b48f0d7ad416472c02e0e4bf6919541b111efd3cae690/pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027", size = 5973336, upload-time = "2025-07-01T09:15:21.237Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2d/524f9318f6cbfcc79fbc004801ea6b607ec3f843977652fdee4857a7568b/pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77", size = 6642699, upload-time = "2025-07-01T09:15:23.186Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d2/a9a4f280c6aefedce1e8f615baaa5474e0701d86dd6f1dede66726462bbd/pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874", size = 6083789, upload-time = "2025-07-01T09:15:25.1Z" }, + { url = "https://files.pythonhosted.org/packages/fe/54/86b0cd9dbb683a9d5e960b66c7379e821a19be4ac5810e2e5a715c09a0c0/pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a", size = 6720386, upload-time = "2025-07-01T09:15:27.378Z" }, + { url = "https://files.pythonhosted.org/packages/e7/95/88efcaf384c3588e24259c4203b909cbe3e3c2d887af9e938c2022c9dd48/pillow-11.3.0-cp314-cp314-win32.whl", hash = "sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214", size = 6370911, upload-time = "2025-07-01T09:15:29.294Z" }, + { url = "https://files.pythonhosted.org/packages/2e/cc/934e5820850ec5eb107e7b1a72dd278140731c669f396110ebc326f2a503/pillow-11.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635", size = 7117383, upload-time = "2025-07-01T09:15:31.128Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e9/9c0a616a71da2a5d163aa37405e8aced9a906d574b4a214bede134e731bc/pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6", size = 2511385, upload-time = "2025-07-01T09:15:33.328Z" }, + { url = "https://files.pythonhosted.org/packages/1a/33/c88376898aff369658b225262cd4f2659b13e8178e7534df9e6e1fa289f6/pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae", size = 5281129, upload-time = "2025-07-01T09:15:35.194Z" }, + { url = "https://files.pythonhosted.org/packages/1f/70/d376247fb36f1844b42910911c83a02d5544ebd2a8bad9efcc0f707ea774/pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653", size = 4689580, upload-time = "2025-07-01T09:15:37.114Z" }, + { url = "https://files.pythonhosted.org/packages/eb/1c/537e930496149fbac69efd2fc4329035bbe2e5475b4165439e3be9cb183b/pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6", size = 5902860, upload-time = "2025-07-03T13:10:50.248Z" }, + { url = "https://files.pythonhosted.org/packages/bd/57/80f53264954dcefeebcf9dae6e3eb1daea1b488f0be8b8fef12f79a3eb10/pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36", size = 7670694, upload-time = "2025-07-03T13:10:56.432Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/4727d3b71a8578b4587d9c276e90efad2d6fe0335fd76742a6da08132e8c/pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b", size = 6005888, upload-time = "2025-07-01T09:15:39.436Z" }, + { url = "https://files.pythonhosted.org/packages/05/ae/716592277934f85d3be51d7256f3636672d7b1abfafdc42cf3f8cbd4b4c8/pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477", size = 6670330, upload-time = "2025-07-01T09:15:41.269Z" }, + { url = "https://files.pythonhosted.org/packages/e7/bb/7fe6cddcc8827b01b1a9766f5fdeb7418680744f9082035bdbabecf1d57f/pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50", size = 6114089, upload-time = "2025-07-01T09:15:43.13Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f5/06bfaa444c8e80f1a8e4bff98da9c83b37b5be3b1deaa43d27a0db37ef84/pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b", size = 6748206, upload-time = "2025-07-01T09:15:44.937Z" }, + { url = "https://files.pythonhosted.org/packages/f0/77/bc6f92a3e8e6e46c0ca78abfffec0037845800ea38c73483760362804c41/pillow-11.3.0-cp314-cp314t-win32.whl", hash = "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12", size = 6377370, upload-time = "2025-07-01T09:15:46.673Z" }, + { url = "https://files.pythonhosted.org/packages/4a/82/3a721f7d69dca802befb8af08b7c79ebcab461007ce1c18bd91a5d5896f9/pillow-11.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db", size = 7121500, upload-time = "2025-07-01T09:15:48.512Z" }, + { url = "https://files.pythonhosted.org/packages/89/c7/5572fa4a3f45740eaab6ae86fcdf7195b55beac1371ac8c619d880cfe948/pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa", size = 2512835, upload-time = "2025-07-01T09:15:50.399Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.9.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/04/fea538adf7dbbd6d186f551d595961e564a3b6715bdf276b477460858672/platformdirs-4.9.2.tar.gz", hash = "sha256:9a33809944b9db043ad67ca0db94b14bf452cc6aeaac46a88ea55b26e2e9d291", size = 28394, upload-time = "2026-02-16T03:56:10.574Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/31/05e764397056194206169869b50cf2fee4dbbbc71b344705b9c0d878d4d8/platformdirs-4.9.2-py3-none-any.whl", hash = "sha256:9170634f126f8efdae22fb58ae8a0eaa86f38365bc57897a6c4f781d1f5875bd", size = 21168, upload-time = "2026-02-16T03:56:08.891Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "portalocker" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pywin32", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/77/65b857a69ed876e1951e88aaba60f5ce6120c33703f7cb61a3c894b8c1b6/portalocker-3.2.0.tar.gz", hash = "sha256:1f3002956a54a8c3730586c5c77bf18fae4149e07eaf1c29fc3faf4d5a3f89ac", size = 95644, upload-time = "2025-06-14T13:20:40.03Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/a6/38c8e2f318bf67d338f4d629e93b0b4b9af331f455f0390ea8ce4a099b26/portalocker-3.2.0-py3-none-any.whl", hash = "sha256:3cdc5f565312224bc570c49337bd21428bba0ef363bbcf58b9ef4a9f11779968", size = 22424, upload-time = "2025-06-14T13:20:38.083Z" }, +] + +[[package]] +name = "posthog" +version = "7.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backoff" }, + { name = "distro" }, + { name = "python-dateutil" }, + { name = "requests" }, + { name = "six" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/50/5c0d9232118fdc1434c1b7bbc1a14de5b310498ede09a7e2123ae1f5f8bd/posthog-7.9.4.tar.gz", hash = "sha256:50acc94ef6267d7030575d2ff54e89e748fac2e98525ac672aeb0423160f77cf", size = 172973, upload-time = "2026-02-25T15:28:47.065Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/6f/794a4e94e3640282e75013ce18e65f0a01afc8d71f733664b4a272f98bce/posthog-7.9.4-py3-none-any.whl", hash = "sha256:414125ddd7a48b9c67feb24d723df1f666af41ad10f8a9a8bbaf5e3b536a2e26", size = 198651, upload-time = "2026-02-25T15:28:45.398Z" }, +] + +[[package]] +name = "preshed" +version = "3.0.13" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cymem" }, + { name = "murmurhash" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/75/fe6b7bbd0dea530a001b0e24c331b21a0be2786e402abf3c57f5dce43d4b/preshed-3.0.13.tar.gz", hash = "sha256:d75f718bbfd97e992f7827e0fa7faf6a91bdd9c922d5baa4b50d62731396cb89", size = 18338, upload-time = "2026-03-23T08:57:31.378Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/fb/ccff23c44c04088c248539005fcda78b9014512a34d170c5360f02ad908b/preshed-3.0.13-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5d14eea14bd01291388928991d7df7d60b9fd19ae970e55006eb4d29b0c1e8eb", size = 138497, upload-time = "2026-03-23T08:56:35.321Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ce/cad5a8145881a771e6c0d002f2e585fc19b962f120860b54d32af5baa342/preshed-3.0.13-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f05b08ce92399c0655b5e0eb5a1cc1f9e295703ed3aabdfaf6538dfa8ae23d57", size = 138010, upload-time = "2026-03-23T08:56:36.399Z" }, + { url = "https://files.pythonhosted.org/packages/a7/a2/c5fed4fb3e946699259d11e4036a3cfdd8c89b3e542e3077d46781642425/preshed-3.0.13-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:62cf7f3113132891d6bba70ff547ad81c6fe50a31930bbbb8499f1d47cd122b7", size = 861498, upload-time = "2026-03-23T08:56:37.67Z" }, + { url = "https://files.pythonhosted.org/packages/51/94/8c9bc48a6ea4903f53a1a0031ce8e35687526949f25821762ef21493c007/preshed-3.0.13-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8b8de3f58043070a354477995acdd98626ce43e4193c708ebd0f694e467f5155", size = 868988, upload-time = "2026-03-23T08:56:39.324Z" }, + { url = "https://files.pythonhosted.org/packages/b6/df/ecd2f40055ff52527ca117ffbfafb888c1a3079b59fbabe03c5b8f9b7240/preshed-3.0.13-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:183b339956a9e1d7a4a00038a3b9587a734db9e8bd915939a49791bd1b372156", size = 1847382, upload-time = "2026-03-23T08:56:40.89Z" }, + { url = "https://files.pythonhosted.org/packages/e6/88/bdb244e40284ded3632a9f88c23bc80230bd7b2ae4a8b7f2cc91adead7a8/preshed-3.0.13-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2e77bed56aded7cbe5d28d6bd2178bc5b13eda0e0e464dab205fb578fa915000", size = 1919236, upload-time = "2026-03-23T08:56:42.616Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c9/c91ea56342e6c364fc69b444a1ac5432327857199c44032c9cc9dc4c3a23/preshed-3.0.13-cp312-cp312-win_amd64.whl", hash = "sha256:04d8f13f2986e5d11af5ac51f55ce3106c70c41b483d20ea392e6180bdd0f870", size = 122938, upload-time = "2026-03-23T08:56:44.271Z" }, + { url = "https://files.pythonhosted.org/packages/b2/0b/6a99d99619fd83b14c696e2489caed7070647488d4d3ac0b723d35db2de0/preshed-3.0.13-cp312-cp312-win_arm64.whl", hash = "sha256:19318dc1cd8cac6663c6c830bf7e0002d2de853769fb03e056774e97c21bedfd", size = 109194, upload-time = "2026-03-23T08:56:45.346Z" }, + { url = "https://files.pythonhosted.org/packages/0e/2a/401158195d6dc7f6aef0b354d74d0e95c9da124499448c2b3dbb95b71204/preshed-3.0.13-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c0d0c14187dc0078d8a63bf190ec045a4d13e7748b6caeb557a7d575e411410b", size = 137289, upload-time = "2026-03-23T08:56:46.516Z" }, + { url = "https://files.pythonhosted.org/packages/88/8f/e20e64573988528785447a6893b2e7ab287ecfd85b3888e978b28812fd20/preshed-3.0.13-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7770987c2e57497cd26124a9be5f652b5b3ccd0def89859ab0da8bca6144a3de", size = 136847, upload-time = "2026-03-23T08:56:47.572Z" }, + { url = "https://files.pythonhosted.org/packages/b9/72/18168f881359c4482d312f8dc196371bdd61c1583a52b34390da4c88bbea/preshed-3.0.13-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4a7bc48220de579be6bdb0a8715482cf36e2a625a6fd5ad26c9f43485a4a23b5", size = 831478, upload-time = "2026-03-23T08:56:48.769Z" }, + { url = "https://files.pythonhosted.org/packages/fd/3a/3543476091087102775568cea9885dde3453569e9aeee365809108de572f/preshed-3.0.13-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e5c8462472f790c16708306aef3a102a762bd19dfe3d2f8ee08bd5e12f51b835", size = 839913, upload-time = "2026-03-23T08:56:49.937Z" }, + { url = "https://files.pythonhosted.org/packages/cf/65/b13f01329decc44ef53cfb6b4601ba85382dcb2a4ec78d9250f03a418066/preshed-3.0.13-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c046736239cc8d72670749b79b526e4111839a2fc461a58545d212797649129c", size = 1816452, upload-time = "2026-03-23T08:56:51.233Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c7/f1a996c6832234efd4d543041b582418d41ac480ee55c557ec9e65344637/preshed-3.0.13-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7c333f18e9a81c8a6de0603fd8781e17115324b117c445ca91abdf7bfb1abe49", size = 1888978, upload-time = "2026-03-23T08:56:52.591Z" }, + { url = "https://files.pythonhosted.org/packages/e3/b9/96fb71499049885ce19545903fdd38877bbc2be0da47e37c04d01f3e9f66/preshed-3.0.13-cp313-cp313-win_amd64.whl", hash = "sha256:461327f8dd36520dcf1fd55a671e0c3c2c97a2d95e22fc85faa31173f4785dda", size = 122134, upload-time = "2026-03-23T08:56:54.392Z" }, + { url = "https://files.pythonhosted.org/packages/ef/a7/32a4903019d936a2316fdd330bedddac287ac26326107d24fb76a1fbc60a/preshed-3.0.13-cp313-cp313-win_arm64.whl", hash = "sha256:35d6c5acb3ee3b12b87a551913063f0cec784055c2af16e028c19fe875f079d0", size = 108497, upload-time = "2026-03-23T08:56:55.816Z" }, + { url = "https://files.pythonhosted.org/packages/bb/b5/993886c98f5caaa6f07a648cac97a7c62a3093091cad65e1e43a1bd41cc4/preshed-3.0.13-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d2f1efae396cadab5f3890a2fd43d2ee65373ef9096ccbb805e51e8d8bcc563b", size = 137882, upload-time = "2026-03-23T08:56:56.878Z" }, + { url = "https://files.pythonhosted.org/packages/c6/86/b7fd137cbf140afd6c45e895946068a15f5b55642916de0075e6eb18581c/preshed-3.0.13-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8d6acc1f5031a535a55a6f7148e2f274554a8343a16309c700cebea0fe7aee8c", size = 138233, upload-time = "2026-03-23T08:56:58.318Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ca/21a7e79625614134273dfed32bca5bb4c2ec1313e33fbd12d41657536f1f/preshed-3.0.13-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7da9d931e7660dcdd757e5870269f0c159126d682ed73ed313971d199eb0f334", size = 834835, upload-time = "2026-03-23T08:56:59.48Z" }, + { url = "https://files.pythonhosted.org/packages/8f/3a/2dbd299516461831ae90e0d5b0637137bf28520c4e6dd0b01d6f1886659a/preshed-3.0.13-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d4ae5cfe075bb7a07982e382bca44f41ddf041f4d24cbd358e8cccfc049259b8", size = 834928, upload-time = "2026-03-23T08:57:01.075Z" }, + { url = "https://files.pythonhosted.org/packages/7c/d3/af654eba4f6587c4ee02c5043e62c194b0a1c4431ffef0c67b9518f6b61c/preshed-3.0.13-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7557963d0125a3a7bcdb2eb6948f3e45da31b5a7f066b55320de3dea22d7557f", size = 1820368, upload-time = "2026-03-23T08:57:02.351Z" }, + { url = "https://files.pythonhosted.org/packages/bf/9b/ebcb2b9e8cb881e40b55b0bf450f8a6b187e2ef3ae0c685cce81d2d85026/preshed-3.0.13-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c4bc60dc994864095d784b7e4d77dba3e64188d169ac88722b699d175561fddb", size = 1888251, upload-time = "2026-03-23T08:57:04.158Z" }, + { url = "https://files.pythonhosted.org/packages/97/f7/c6c012779edcaa6e2cd092c554e98dc53e77f41205b07208655ba77e2327/preshed-3.0.13-cp314-cp314-win_amd64.whl", hash = "sha256:208dcebbe294bf1881ce33fb015d56ab2a7587aece85a09147727174207892e4", size = 125211, upload-time = "2026-03-23T08:57:05.83Z" }, + { url = "https://files.pythonhosted.org/packages/f8/82/390ef87d732ef64e673ef6bf9e5d898453986e979efa50fb3a400e2c0766/preshed-3.0.13-cp314-cp314-win_arm64.whl", hash = "sha256:cf8e1a7a1823b2a7765121446c630140ac6e8650c07a6efbf375e168d1fef4f7", size = 111942, upload-time = "2026-03-23T08:57:06.996Z" }, + { url = "https://files.pythonhosted.org/packages/80/3a/a9dde3167bcecb27ae82ce4567b5ab1aa3989113ae6814c092ce223cc4ef/preshed-3.0.13-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9ca43ecbc3783eda4d6ab3416ae2ecd9ef23dca5f53995843f69f7457bcd0677", size = 144997, upload-time = "2026-03-23T08:57:08.064Z" }, + { url = "https://files.pythonhosted.org/packages/74/d4/22d9355b50b6a13b407dcad0a81df83fb1d5602092d1f05834674dde8fda/preshed-3.0.13-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c8596e41a258ff213553a441e0bb3eb388fd8158e84a7bf3aae6d8ede2c166d3", size = 147294, upload-time = "2026-03-23T08:57:09.411Z" }, + { url = "https://files.pythonhosted.org/packages/70/42/a225ee83fdb306d2a503f21a627953b820f4e079c90c8a84338957cb8ff5/preshed-3.0.13-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4f8856ca3d88e9b250630d70abb4f260d8933151ddfb413024784b25b009868e", size = 952110, upload-time = "2026-03-23T08:57:10.592Z" }, + { url = "https://files.pythonhosted.org/packages/40/ba/09a9dfe3d22d7e745483fd5d7f2a82cd4d39c161f7d2daa0faa4bd6402be/preshed-3.0.13-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e5b2865aecbd2e1e10e5d19bb8bfad765863c1307c6c3e51f2a08bd64122409", size = 932217, upload-time = "2026-03-23T08:57:12.124Z" }, + { url = "https://files.pythonhosted.org/packages/6c/5c/e10e2e05133e7fcbd7c40536af1148c82dd24357b8f5726e2c7bc51cfd53/preshed-3.0.13-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:09f96b477c987755b3c945df214ea1c1c80bfb350e9f34e78da89585535b77e8", size = 1896542, upload-time = "2026-03-23T08:57:13.525Z" }, + { url = "https://files.pythonhosted.org/packages/37/aa/51e5b4109a4cdfae28c3613eeeb10764a3794ebef8de93ffbb109465bea3/preshed-3.0.13-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:670db59a52e1823b5f088c764df474e65b686592d4093adbeef14581c95ee2cb", size = 1959473, upload-time = "2026-03-23T08:57:15.706Z" }, + { url = "https://files.pythonhosted.org/packages/0e/6a/1d966f367a14c703dde629d150d996c1b727d442f620300b21c9ec1a24d1/preshed-3.0.13-cp314-cp314t-win_amd64.whl", hash = "sha256:b03e21b0bf95eb56e23973f32cabb930e94f352228652f81c0955dbd6967d904", size = 146229, upload-time = "2026-03-23T08:57:17.457Z" }, + { url = "https://files.pythonhosted.org/packages/22/80/368139067603e590a000122355f9c8576c8ebed4fb0b8849feaa2698489d/preshed-3.0.13-cp314-cp314t-win_arm64.whl", hash = "sha256:b980f3ea9bb74b7f94464bc3d6eb3c9162b6b79b531febd14c6465c24344d2cc", size = 119339, upload-time = "2026-03-23T08:57:18.882Z" }, +] + +[[package]] +name = "protobuf" +version = "5.29.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/57/394a763c103e0edf87f0938dafcd918d53b4c011dfc5c8ae80f3b0452dbb/protobuf-5.29.6.tar.gz", hash = "sha256:da9ee6a5424b6b30fd5e45c5ea663aef540ca95f9ad99d1e887e819cdf9b8723", size = 425623, upload-time = "2026-02-04T22:54:40.584Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/88/9ee58ff7863c479d6f8346686d4636dd4c415b0cbeed7a6a7d0617639c2a/protobuf-5.29.6-cp310-abi3-win32.whl", hash = "sha256:62e8a3114992c7c647bce37dcc93647575fc52d50e48de30c6fcb28a6a291eb1", size = 423357, upload-time = "2026-02-04T22:54:25.805Z" }, + { url = "https://files.pythonhosted.org/packages/1c/66/2dc736a4d576847134fb6d80bd995c569b13cdc7b815d669050bf0ce2d2c/protobuf-5.29.6-cp310-abi3-win_amd64.whl", hash = "sha256:7e6ad413275be172f67fdee0f43484b6de5a904cc1c3ea9804cb6fe2ff366eda", size = 435175, upload-time = "2026-02-04T22:54:28.592Z" }, + { url = "https://files.pythonhosted.org/packages/06/db/49b05966fd208ae3f44dcd33837b6243b4915c57561d730a43f881f24dea/protobuf-5.29.6-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:b5a169e664b4057183a34bdc424540e86eea47560f3c123a0d64de4e137f9269", size = 418619, upload-time = "2026-02-04T22:54:30.266Z" }, + { url = "https://files.pythonhosted.org/packages/b7/d7/48cbf6b0c3c39761e47a99cb483405f0fde2be22cf00d71ef316ce52b458/protobuf-5.29.6-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:a8866b2cff111f0f863c1b3b9e7572dc7eaea23a7fae27f6fc613304046483e6", size = 320284, upload-time = "2026-02-04T22:54:31.782Z" }, + { url = "https://files.pythonhosted.org/packages/e3/dd/cadd6ec43069247d91f6345fa7a0d2858bef6af366dbd7ba8f05d2c77d3b/protobuf-5.29.6-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:e3387f44798ac1106af0233c04fb8abf543772ff241169946f698b3a9a3d3ab9", size = 320478, upload-time = "2026-02-04T22:54:32.909Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cb/e3065b447186cb70aa65acc70c86baf482d82bf75625bf5a2c4f6919c6a3/protobuf-5.29.6-py3-none-any.whl", hash = "sha256:6b9edb641441b2da9fa8f428760fc136a49cf97a52076010cf22a2ff73438a86", size = 173126, upload-time = "2026-02-04T22:54:39.462Z" }, +] + +[[package]] +name = "psycopg" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/ed/3a30e8ef82d4128c76aa9bd6b2a7fe6c16c283811e6655997f5047801b47/psycopg-3.3.1.tar.gz", hash = "sha256:ccfa30b75874eef809c0fbbb176554a2640cc1735a612accc2e2396a92442fc6", size = 165596, upload-time = "2025-12-02T21:09:55.545Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/f3/0b4a4c25a47c2d907afa97674287dab61bc9941c9ac3972a67100e33894d/psycopg-3.3.1-py3-none-any.whl", hash = "sha256:e44d8eae209752efe46318f36dd0fdf5863e928009338d736843bb1084f6435c", size = 212760, upload-time = "2025-12-02T21:02:36.029Z" }, +] + +[[package]] +name = "py-key-value-aio" +version = "0.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beartype" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/3c/0397c072a38d4bc580994b42e0c90c5f44f679303489e4376289534735e5/py_key_value_aio-0.4.4.tar.gz", hash = "sha256:e3012e6243ed7cc09bb05457bd4d03b1ba5c2b1ca8700096b3927db79ffbbe55", size = 92300, upload-time = "2026-02-16T21:21:43.245Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/69/f1b537ee70b7def42d63124a539ed3026a11a3ffc3086947a1ca6e861868/py_key_value_aio-0.4.4-py3-none-any.whl", hash = "sha256:18e17564ecae61b987f909fc2cd41ee2012c84b4b1dcb8c055cf8b4bc1bf3f5d", size = 152291, upload-time = "2026-02-16T21:21:44.241Z" }, +] + +[package.optional-dependencies] +filetree = [ + { name = "aiofile" }, + { name = "anyio" }, +] +keyring = [ + { name = "keyring" }, +] +memory = [ + { name = "cachetools" }, +] + +[[package]] +name = "py-rust-stemmers" +version = "0.1.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/63/4fbc14810c32d2a884e2e94e406a7d5bf8eee53e1103f558433817230342/py_rust_stemmers-0.1.5.tar.gz", hash = "sha256:e9c310cfb5c2470d7c7c8a0484725965e7cab8b1237e106a0863d5741da3e1f7", size = 9388, upload-time = "2025-02-19T13:56:28.708Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e1/ea8ac92454a634b1bb1ee0a89c2f75a4e6afec15a8412527e9bbde8c6b7b/py_rust_stemmers-0.1.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:29772837126a28263bf54ecd1bc709dd569d15a94d5e861937813ce51e8a6df4", size = 286085, upload-time = "2025-02-19T13:55:23.871Z" }, + { url = "https://files.pythonhosted.org/packages/cb/32/fe1cc3d36a19c1ce39792b1ed151ddff5ee1d74c8801f0e93ff36e65f885/py_rust_stemmers-0.1.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4d62410ada44a01e02974b85d45d82f4b4c511aae9121e5f3c1ba1d0bea9126b", size = 272021, upload-time = "2025-02-19T13:55:25.685Z" }, + { url = "https://files.pythonhosted.org/packages/0a/38/b8f94e5e886e7ab181361a0911a14fb923b0d05b414de85f427e773bf445/py_rust_stemmers-0.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b28ef729a4c83c7d9418be3c23c0372493fcccc67e86783ff04596ef8a208cdf", size = 310547, upload-time = "2025-02-19T13:55:26.891Z" }, + { url = "https://files.pythonhosted.org/packages/a9/08/62e97652d359b75335486f4da134a6f1c281f38bd3169ed6ecfb276448c3/py_rust_stemmers-0.1.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a979c3f4ff7ad94a0d4cf566ca7bfecebb59e66488cc158e64485cf0c9a7879f", size = 315237, upload-time = "2025-02-19T13:55:28.116Z" }, + { url = "https://files.pythonhosted.org/packages/1c/b9/fc0278432f288d2be4ee4d5cc80fd8013d604506b9b0503e8b8cae4ba1c3/py_rust_stemmers-0.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c3593d895453fa06bf70a7b76d6f00d06def0f91fc253fe4260920650c5e078", size = 324419, upload-time = "2025-02-19T13:55:29.211Z" }, + { url = "https://files.pythonhosted.org/packages/6b/5b/74e96eaf622fe07e83c5c389d101540e305e25f76a6d0d6fb3d9e0506db8/py_rust_stemmers-0.1.5-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:96ccc7fd042ffc3f7f082f2223bb7082ed1423aa6b43d5d89ab23e321936c045", size = 324792, upload-time = "2025-02-19T13:55:30.948Z" }, + { url = "https://files.pythonhosted.org/packages/4f/f7/b76816d7d67166e9313915ad486c21d9e7da0ac02703e14375bb1cb64b5a/py_rust_stemmers-0.1.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef18cfced2c9c676e0d7d172ba61c3fab2aa6969db64cc8f5ca33a7759efbefe", size = 488014, upload-time = "2025-02-19T13:55:32.066Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ed/7d9bed02f78d85527501f86a867cd5002d97deb791b9a6b1b45b00100010/py_rust_stemmers-0.1.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:541d4b5aa911381e3d37ec483abb6a2cf2351b4f16d5e8d77f9aa2722956662a", size = 575582, upload-time = "2025-02-19T13:55:34.005Z" }, + { url = "https://files.pythonhosted.org/packages/93/40/eafd1b33688e8e8ae946d1ef25c4dc93f5b685bd104b9c5573405d7e1d30/py_rust_stemmers-0.1.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ffd946a36e9ac17ca96821963663012e04bc0ee94d21e8b5ae034721070b436c", size = 493267, upload-time = "2025-02-19T13:55:35.294Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6a/15135b69e4fd28369433eb03264d201b1b0040ba534b05eddeb02a276684/py_rust_stemmers-0.1.5-cp312-none-win_amd64.whl", hash = "sha256:6ed61e1207f3b7428e99b5d00c055645c6415bb75033bff2d06394cbe035fd8e", size = 209395, upload-time = "2025-02-19T13:55:36.519Z" }, + { url = "https://files.pythonhosted.org/packages/80/b8/030036311ec25952bf3083b6c105be5dee052a71aa22d5fbeb857ebf8c1c/py_rust_stemmers-0.1.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:398b3a843a9cd4c5d09e726246bc36f66b3d05b0a937996814e91f47708f5db5", size = 286086, upload-time = "2025-02-19T13:55:37.581Z" }, + { url = "https://files.pythonhosted.org/packages/ed/be/0465dcb3a709ee243d464e89231e3da580017f34279d6304de291d65ccb0/py_rust_stemmers-0.1.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4e308fc7687901f0c73603203869908f3156fa9c17c4ba010a7fcc98a7a1c5f2", size = 272019, upload-time = "2025-02-19T13:55:39.183Z" }, + { url = "https://files.pythonhosted.org/packages/ab/b6/76ca5b1f30cba36835938b5d9abee0c130c81833d51b9006264afdf8df3c/py_rust_stemmers-0.1.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f9efc4da5e734bdd00612e7506de3d0c9b7abc4b89d192742a0569d0d1fe749", size = 310545, upload-time = "2025-02-19T13:55:40.339Z" }, + { url = "https://files.pythonhosted.org/packages/56/8f/5be87618cea2fe2e70e74115a20724802bfd06f11c7c43514b8288eb6514/py_rust_stemmers-0.1.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cc2cc8d2b36bc05b8b06506199ac63d437360ae38caefd98cd19e479d35afd42", size = 315236, upload-time = "2025-02-19T13:55:41.55Z" }, + { url = "https://files.pythonhosted.org/packages/00/02/ea86a316aee0f0a9d1449ad4dbffff38f4cf0a9a31045168ae8b95d8bdf8/py_rust_stemmers-0.1.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a231dc6f0b2a5f12a080dfc7abd9e6a4ea0909290b10fd0a4620e5a0f52c3d17", size = 324419, upload-time = "2025-02-19T13:55:42.693Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fd/1612c22545dcc0abe2f30fc08f30a2332f2224dd536fa1508444a9ca0e39/py_rust_stemmers-0.1.5-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5845709d48afc8b29e248f42f92431155a3d8df9ba30418301c49c6072b181b0", size = 324794, upload-time = "2025-02-19T13:55:43.896Z" }, + { url = "https://files.pythonhosted.org/packages/66/18/8a547584d7edac9e7ac9c7bdc53228d6f751c0f70a317093a77c386c8ddc/py_rust_stemmers-0.1.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e48bfd5e3ce9d223bfb9e634dc1425cf93ee57eef6f56aa9a7120ada3990d4be", size = 488014, upload-time = "2025-02-19T13:55:45.088Z" }, + { url = "https://files.pythonhosted.org/packages/3b/87/4619c395b325e26048a6e28a365afed754614788ba1f49b2eefb07621a03/py_rust_stemmers-0.1.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:35d32f6e7bdf6fd90e981765e32293a8be74def807147dea9fdc1f65d6ce382f", size = 575582, upload-time = "2025-02-19T13:55:46.436Z" }, + { url = "https://files.pythonhosted.org/packages/98/6e/214f1a889142b7df6d716e7f3fea6c41e87bd6c29046aa57e175d452b104/py_rust_stemmers-0.1.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:191ea8bf922c984631ffa20bf02ef0ad7eec0465baeaed3852779e8f97c7e7a3", size = 493269, upload-time = "2025-02-19T13:55:49.057Z" }, + { url = "https://files.pythonhosted.org/packages/e1/b9/c5185df277576f995ae34418eb2b2ac12f30835412270f9e05c52face521/py_rust_stemmers-0.1.5-cp313-none-win_amd64.whl", hash = "sha256:e564c9efdbe7621704e222b53bac265b0e4fbea788f07c814094f0ec6b80adcf", size = 209397, upload-time = "2025-02-19T13:55:50.853Z" }, +] + +[[package]] +name = "pybars3" +version = "0.9.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pymeta3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ec/1a/2fb847db017f9f89ab8519d96e35fb3dacb6170a0643fddba3b366af0af1/pybars3-0.9.7.tar.gz", hash = "sha256:6ac847e905e53b9c5b936af112c910475e27bf767f79f4528c16f9af1ec0e252", size = 29203, upload-time = "2019-11-05T09:45:24.07Z" } + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[package.optional-dependencies] +email = [ + { name = "email-validator" }, +] +timezone = [ + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] + +[[package]] +name = "pydantic-ai-slim" +version = "1.63.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "genai-prices" }, + { name = "griffelib" }, + { name = "httpx" }, + { name = "opentelemetry-api" }, + { name = "pydantic" }, + { name = "pydantic-graph" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/6d/2b5c0c60b42e6af49830f6a09b5d38fecdb1f20d9659152691eba95613b4/pydantic_ai_slim-1.63.0.tar.gz", hash = "sha256:9377afecdfe4bc17f5c9ed72c758e460703ac5876931aa2f18ace8ac0e69312a", size = 426862, upload-time = "2026-02-23T17:56:36.215Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/ca/c4e39eec1cff5a294b64313a8a959b38d326819e0f0a41f48e61ce019a22/pydantic_ai_slim-1.63.0-py3-none-any.whl", hash = "sha256:ed393b0f871b748171f65bec5191c3025b5abb8a4fc616afee17eb9dc2dfa15d", size = 554190, upload-time = "2026-02-23T17:56:29.533Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, +] + +[[package]] +name = "pydantic-evals" +version = "1.63.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "logfire-api" }, + { name = "pydantic" }, + { name = "pydantic-ai-slim" }, + { name = "pyyaml" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/99/43/21b6ddf65b56f7401c344f98e4e6258a02d2868c8a52a8b79c0e0e701029/pydantic_evals-1.63.0.tar.gz", hash = "sha256:eed56a7192e07c8be8cf16e53bb2ef652b4f7f7b8527650ac45fde865a4ecf9d", size = 56365, upload-time = "2026-02-23T17:56:37.71Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/f2/7174ad6abca2457e35a1b902ca4fa78aa8ee72e4ec2e9cd5dc8904014ec9/pydantic_evals-1.63.0-py3-none-any.whl", hash = "sha256:2e92a3af579a5670b2babf2044081d0ef99ab5a9ef141972616d71fd7e5bfd0e", size = 67279, upload-time = "2026-02-23T17:56:31.008Z" }, +] + +[[package]] +name = "pydantic-extra-types" +version = "2.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/35/2fee58b1316a73e025728583d3b1447218a97e621933fc776fb8c0f2ebdd/pydantic_extra_types-2.11.0.tar.gz", hash = "sha256:4e9991959d045b75feb775683437a97991d02c138e00b59176571db9ce634f0e", size = 157226, upload-time = "2025-12-31T16:18:27.944Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/17/fabd56da47096d240dd45ba627bead0333b0cf0ee8ada9bec579287dadf3/pydantic_extra_types-2.11.0-py3-none-any.whl", hash = "sha256:84b864d250a0fc62535b7ec591e36f2c5b4d1325fa0017eb8cda9aeb63b374a6", size = 74296, upload-time = "2025-12-31T16:18:26.38Z" }, +] + +[[package]] +name = "pydantic-graph" +version = "1.63.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "logfire-api" }, + { name = "pydantic" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/c8/aa3cb56552562b799f31e9de291c8bd88306308cfc9647d220dfff2bea18/pydantic_graph-1.63.0.tar.gz", hash = "sha256:5fd98bb22fa6181f0357a6ffad38a3214af12868bd46492d6456c5db434466b4", size = 58528, upload-time = "2026-02-23T17:56:39.118Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/1c/8dcae24c824dd2690fbe7375083b369b10ed1ad773e2b9d1122bb6c0fcdc/pydantic_graph-1.63.0-py3-none-any.whl", hash = "sha256:d9b7a387116f358d470c042b07aa08125cadfcfa8c08ef01769746a489aef0d5", size = 72353, upload-time = "2026-02-23T17:56:32.304Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/6d/fffca34caecc4a3f97bda81b2098da5e8ab7efc9a66e819074a11955d87e/pydantic_settings-2.13.1.tar.gz", hash = "sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025", size = 223826, upload-time = "2026-02-19T13:45:08.055Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/4b/ccc026168948fec4f7555b9164c724cf4125eac006e176541483d2c959be/pydantic_settings-2.13.1-py3-none-any.whl", hash = "sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237", size = 58929, upload-time = "2026-02-19T13:45:06.034Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pyjwt" +version = "2.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/5a/b46fa56bf322901eee5b0454a34343cdbdae202cd421775a8ee4e42fd519/pyjwt-2.11.0.tar.gz", hash = "sha256:35f95c1f0fbe5d5ba6e43f00271c275f7a1a4db1dab27bf708073b75318ea623", size = 98019, upload-time = "2026-01-30T19:59:55.694Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/01/c26ce75ba460d5cd503da9e13b21a33804d38c2165dec7b716d06b13010c/pyjwt-2.11.0-py3-none-any.whl", hash = "sha256:94a6bde30eb5c8e04fee991062b534071fd1439ef58d2adc9ccb823e7bcd0469", size = 28224, upload-time = "2026-01-30T19:59:54.539Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + +[[package]] +name = "pymeta3" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/af/409edba35fc597f1e386e3860303791ab5a28d6cc9a8aecbc567051b19a9/PyMeta3-0.5.1.tar.gz", hash = "sha256:18bda326d9a9bbf587bfc0ee0bc96864964d78b067288bcf55d4d98681d05bcb", size = 29566, upload-time = "2015-02-22T16:30:06.858Z" } + +[[package]] +name = "pyperclip" +version = "1.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/52/d87eba7cb129b81563019d1679026e7a112ef76855d6159d24754dbd2a51/pyperclip-1.11.0.tar.gz", hash = "sha256:244035963e4428530d9e3a6101a1ef97209c6825edab1567beac148ccc1db1b6", size = 12185, upload-time = "2025-09-26T14:40:37.245Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/80/fc9d01d5ed37ba4c42ca2b55b4339ae6e200b456be3a1aaddf4a9fa99b8c/pyperclip-1.11.0-py3-none-any.whl", hash = "sha256:299403e9ff44581cb9ba2ffeed69c7aa96a008622ad0c46cb575ca75b5b84273", size = 11063, upload-time = "2025-09-26T14:40:36.069Z" }, +] + +[[package]] +name = "pyright" +version = "1.1.408" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodeenv" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/b2/5db700e52554b8f025faa9c3c624c59f1f6c8841ba81ab97641b54322f16/pyright-1.1.408.tar.gz", hash = "sha256:f28f2321f96852fa50b5829ea492f6adb0e6954568d1caa3f3af3a5f555eb684", size = 4400578, upload-time = "2026-01-08T08:07:38.795Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/82/a2c93e32800940d9573fb28c346772a14778b84ba7524e691b324620ab89/pyright-1.1.408-py3-none-any.whl", hash = "sha256:090b32865f4fdb1e0e6cd82bf5618480d48eecd2eb2e70f960982a3d9a4c17c1", size = 6399144, upload-time = "2026-01-08T08:07:37.082Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "pytest-aio" +version = "2.1.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/38/9aab892a064592dde69b68554550a7f773d9120d5cbd77c9c033a91bba71/pytest_aio-2.1.7.tar.gz", hash = "sha256:c9c2fa05787a48cc6abf90f57d2333dc4fe356a1b49db7bcb52ec32ada8bb684", size = 5509, upload-time = "2026-02-12T15:58:50.899Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/39/a9a065eaf0336041225443e79ecbaa27146c241514782990480a70df0ac0/pytest_aio-2.1.7-py3-none-any.whl", hash = "sha256:72eca30834d254d3f36336295a19119b42900c84bdd8155a0c242467987d0e73", size = 6577, upload-time = "2026-02-12T15:58:51.524Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, +] + +[[package]] +name = "python-frontmatter" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/de/910fa208120314a12f9a88ea63e03707261692af782c99283f1a2c8a5e6f/python-frontmatter-1.1.0.tar.gz", hash = "sha256:7118d2bd56af9149625745c58c9b51fb67e8d1294a0c76796dafdc72c36e5f6d", size = 16256, upload-time = "2024-01-16T18:50:04.052Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/87/3c8da047b3ec5f99511d1b4d7a5bc72d4b98751c7e78492d14dc736319c5/python_frontmatter-1.1.0-py3-none-any.whl", hash = "sha256:335465556358d9d0e6c98bbeb69b1c969f2a4a21360587b9873bfc3b213407c1", size = 9834, upload-time = "2024-01-16T18:50:00.911Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, +] + +[[package]] +name = "pytz" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, +] + +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, +] + +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "qdrant-client" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "grpcio" }, + { name = "httpx", extra = ["http2"] }, + { name = "numpy" }, + { name = "portalocker" }, + { name = "protobuf" }, + { name = "pydantic" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/20/fb/c9c4cecf6e7fdff2dbaeee0de40e93fe495379eb5fe2775b184ea45315da/qdrant_client-1.17.0.tar.gz", hash = "sha256:47eb033edb9be33a4babb4d87b0d8d5eaf03d52112dca0218db7f2030bf41ba9", size = 344839, upload-time = "2026-02-19T16:03:17.069Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/15/dfadbc9d8c9872e8ac45fa96f5099bb2855f23426bfea1bbcdc85e64ef6e/qdrant_client-1.17.0-py3-none-any.whl", hash = "sha256:f5b452c68c42b3580d3d266446fb00d3c6e3aae89c916e16585b3c704e108438", size = 390381, upload-time = "2026-02-19T16:03:15.486Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "regex" +version = "2026.2.19" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ff/c0/d8079d4f6342e4cec5c3e7d7415b5cd3e633d5f4124f7a4626908dbe84c7/regex-2026.2.19.tar.gz", hash = "sha256:6fb8cb09b10e38f3ae17cc6dc04a1df77762bd0351b6ba9041438e7cc85ec310", size = 414973, upload-time = "2026-02-19T19:03:47.899Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/73/13b39c7c9356f333e564ab4790b6cb0df125b8e64e8d6474e73da49b1955/regex-2026.2.19-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c1665138776e4ac1aa75146669236f7a8a696433ec4e525abf092ca9189247cc", size = 489541, upload-time = "2026-02-19T19:00:52.728Z" }, + { url = "https://files.pythonhosted.org/packages/15/77/fcc7bd9a67000d07fbcc11ed226077287a40d5c84544e62171d29d3ef59c/regex-2026.2.19-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d792b84709021945597e05656aac059526df4e0c9ef60a0eaebb306f8fafcaa8", size = 291414, upload-time = "2026-02-19T19:00:54.51Z" }, + { url = "https://files.pythonhosted.org/packages/f9/87/3997fc72dc59233426ef2e18dfdd105bb123812fff740ee9cc348f1a3243/regex-2026.2.19-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:db970bcce4d63b37b3f9eb8c893f0db980bbf1d404a1d8d2b17aa8189de92c53", size = 289140, upload-time = "2026-02-19T19:00:56.841Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d0/b7dd3883ed1cff8ee0c0c9462d828aaf12be63bf5dc55453cbf423523b13/regex-2026.2.19-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:03d706fbe7dfec503c8c3cb76f9352b3e3b53b623672aa49f18a251a6c71b8e6", size = 798767, upload-time = "2026-02-19T19:00:59.014Z" }, + { url = "https://files.pythonhosted.org/packages/4a/7e/8e2d09103832891b2b735a2515abf377db21144c6dd5ede1fb03c619bf09/regex-2026.2.19-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8dbff048c042beef60aa1848961384572c5afb9e8b290b0f1203a5c42cf5af65", size = 864436, upload-time = "2026-02-19T19:01:00.772Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2e/afea8d23a6db1f67f45e3a0da3057104ce32e154f57dd0c8997274d45fcd/regex-2026.2.19-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ccaaf9b907ea6b4223d5cbf5fa5dff5f33dc66f4907a25b967b8a81339a6e332", size = 912391, upload-time = "2026-02-19T19:01:02.865Z" }, + { url = "https://files.pythonhosted.org/packages/59/3c/ea5a4687adaba5e125b9bd6190153d0037325a0ba3757cc1537cc2c8dd90/regex-2026.2.19-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:75472631eee7898e16a8a20998d15106cb31cfde21cdf96ab40b432a7082af06", size = 803702, upload-time = "2026-02-19T19:01:05.298Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c5/624a0705e8473a26488ec1a3a4e0b8763ecfc682a185c302dfec71daea35/regex-2026.2.19-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d89f85a5ccc0cec125c24be75610d433d65295827ebaf0d884cbe56df82d4774", size = 775980, upload-time = "2026-02-19T19:01:07.047Z" }, + { url = "https://files.pythonhosted.org/packages/4d/4b/ed776642533232b5599b7c1f9d817fe11faf597e8a92b7a44b841daaae76/regex-2026.2.19-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0d9f81806abdca3234c3dd582b8a97492e93de3602c8772013cb4affa12d1668", size = 788122, upload-time = "2026-02-19T19:01:08.744Z" }, + { url = "https://files.pythonhosted.org/packages/8c/58/e93e093921d13b9784b4f69896b6e2a9e09580a265c59d9eb95e87d288f2/regex-2026.2.19-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9dadc10d1c2bbb1326e572a226d2ec56474ab8aab26fdb8cf19419b372c349a9", size = 858910, upload-time = "2026-02-19T19:01:10.488Z" }, + { url = "https://files.pythonhosted.org/packages/85/77/ff1d25a0c56cd546e0455cbc93235beb33474899690e6a361fa6b52d265b/regex-2026.2.19-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:6bc25d7e15f80c9dc7853cbb490b91c1ec7310808b09d56bd278fe03d776f4f6", size = 764153, upload-time = "2026-02-19T19:01:12.156Z" }, + { url = "https://files.pythonhosted.org/packages/cd/ef/8ec58df26d52d04443b1dc56f9be4b409f43ed5ae6c0248a287f52311fc4/regex-2026.2.19-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:965d59792f5037d9138da6fed50ba943162160443b43d4895b182551805aff9c", size = 850348, upload-time = "2026-02-19T19:01:14.147Z" }, + { url = "https://files.pythonhosted.org/packages/f5/b3/c42fd5ed91639ce5a4225b9df909180fc95586db071f2bf7c68d2ccbfbe6/regex-2026.2.19-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:38d88c6ed4a09ed61403dbdf515d969ccba34669af3961ceb7311ecd0cef504a", size = 789977, upload-time = "2026-02-19T19:01:15.838Z" }, + { url = "https://files.pythonhosted.org/packages/b6/22/bc3b58ebddbfd6ca5633e71fd41829ee931963aad1ebeec55aad0c23044e/regex-2026.2.19-cp312-cp312-win32.whl", hash = "sha256:5df947cabab4b643d4791af5e28aecf6bf62e6160e525651a12eba3d03755e6b", size = 266381, upload-time = "2026-02-19T19:01:17.952Z" }, + { url = "https://files.pythonhosted.org/packages/fc/4a/6ff550b63e67603ee60e69dc6bd2d5694e85046a558f663b2434bdaeb285/regex-2026.2.19-cp312-cp312-win_amd64.whl", hash = "sha256:4146dc576ea99634ae9c15587d0c43273b4023a10702998edf0fa68ccb60237a", size = 277274, upload-time = "2026-02-19T19:01:19.826Z" }, + { url = "https://files.pythonhosted.org/packages/cc/29/9ec48b679b1e87e7bc8517dff45351eab38f74fbbda1fbcf0e9e6d4e8174/regex-2026.2.19-cp312-cp312-win_arm64.whl", hash = "sha256:cdc0a80f679353bd68450d2a42996090c30b2e15ca90ded6156c31f1a3b63f3b", size = 270509, upload-time = "2026-02-19T19:01:22.075Z" }, + { url = "https://files.pythonhosted.org/packages/d2/2d/a849835e76ac88fcf9e8784e642d3ea635d183c4112150ca91499d6703af/regex-2026.2.19-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8df08decd339e8b3f6a2eb5c05c687fe9d963ae91f352bc57beb05f5b2ac6879", size = 489329, upload-time = "2026-02-19T19:01:23.841Z" }, + { url = "https://files.pythonhosted.org/packages/da/aa/78ff4666d3855490bae87845a5983485e765e1f970da20adffa2937b241d/regex-2026.2.19-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3aa0944f1dc6e92f91f3b306ba7f851e1009398c84bfd370633182ee4fc26a64", size = 291308, upload-time = "2026-02-19T19:01:25.605Z" }, + { url = "https://files.pythonhosted.org/packages/cd/58/714384efcc07ae6beba528a541f6e99188c5cc1bc0295337f4e8a868296d/regex-2026.2.19-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c13228fbecb03eadbfd8f521732c5fda09ef761af02e920a3148e18ad0e09968", size = 289033, upload-time = "2026-02-19T19:01:27.243Z" }, + { url = "https://files.pythonhosted.org/packages/75/ec/6438a9344d2869cf5265236a06af1ca6d885e5848b6561e10629bc8e5a11/regex-2026.2.19-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0d0e72703c60d68b18b27cde7cdb65ed2570ae29fb37231aa3076bfb6b1d1c13", size = 798798, upload-time = "2026-02-19T19:01:28.877Z" }, + { url = "https://files.pythonhosted.org/packages/c2/be/b1ce2d395e3fd2ce5f2fde2522f76cade4297cfe84cd61990ff48308749c/regex-2026.2.19-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:46e69a4bf552e30e74a8aa73f473c87efcb7f6e8c8ece60d9fd7bf13d5c86f02", size = 864444, upload-time = "2026-02-19T19:01:30.933Z" }, + { url = "https://files.pythonhosted.org/packages/d5/97/a3406460c504f7136f140d9461960c25f058b0240e4424d6fb73c7a067ab/regex-2026.2.19-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8edda06079bd770f7f0cf7f3bba1a0b447b96b4a543c91fe0c142d034c166161", size = 912633, upload-time = "2026-02-19T19:01:32.744Z" }, + { url = "https://files.pythonhosted.org/packages/8b/d9/e5dbef95008d84e9af1dc0faabbc34a7fbc8daa05bc5807c5cf86c2bec49/regex-2026.2.19-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9cbc69eae834afbf634f7c902fc72ff3e993f1c699156dd1af1adab5d06b7fe7", size = 803718, upload-time = "2026-02-19T19:01:34.61Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e5/61d80132690a1ef8dc48e0f44248036877aebf94235d43f63a20d1598888/regex-2026.2.19-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bcf57d30659996ee5c7937999874504c11b5a068edc9515e6a59221cc2744dd1", size = 775975, upload-time = "2026-02-19T19:01:36.525Z" }, + { url = "https://files.pythonhosted.org/packages/05/32/ae828b3b312c972cf228b634447de27237d593d61505e6ad84723f8eabba/regex-2026.2.19-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8e6e77cd92216eb489e21e5652a11b186afe9bdefca8a2db739fd6b205a9e0a4", size = 788129, upload-time = "2026-02-19T19:01:38.498Z" }, + { url = "https://files.pythonhosted.org/packages/cb/25/d74f34676f22bec401eddf0e5e457296941e10cbb2a49a571ca7a2c16e5a/regex-2026.2.19-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b9ab8dec42afefa6314ea9b31b188259ffdd93f433d77cad454cd0b8d235ce1c", size = 858818, upload-time = "2026-02-19T19:01:40.409Z" }, + { url = "https://files.pythonhosted.org/packages/1e/eb/0bc2b01a6b0b264e1406e5ef11cae3f634c3bd1a6e61206fd3227ce8e89c/regex-2026.2.19-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:294c0fb2e87c6bcc5f577c8f609210f5700b993151913352ed6c6af42f30f95f", size = 764186, upload-time = "2026-02-19T19:01:43.009Z" }, + { url = "https://files.pythonhosted.org/packages/eb/37/5fe5a630d0d99ecf0c3570f8905dafbc160443a2d80181607770086c9812/regex-2026.2.19-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:c0924c64b082d4512b923ac016d6e1dcf647a3560b8a4c7e55cbbd13656cb4ed", size = 850363, upload-time = "2026-02-19T19:01:45.015Z" }, + { url = "https://files.pythonhosted.org/packages/c3/45/ef68d805294b01ec030cfd388724ba76a5a21a67f32af05b17924520cb0b/regex-2026.2.19-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:790dbf87b0361606cb0d79b393c3e8f4436a14ee56568a7463014565d97da02a", size = 790026, upload-time = "2026-02-19T19:01:47.51Z" }, + { url = "https://files.pythonhosted.org/packages/d6/3a/40d3b66923dfc5aeba182f194f0ca35d09afe8c031a193e6ae46971a0a0e/regex-2026.2.19-cp313-cp313-win32.whl", hash = "sha256:43cdde87006271be6963896ed816733b10967baaf0e271d529c82e93da66675b", size = 266372, upload-time = "2026-02-19T19:01:49.469Z" }, + { url = "https://files.pythonhosted.org/packages/3d/f2/39082e8739bfd553497689e74f9d5e5bb531d6f8936d0b94f43e18f219c0/regex-2026.2.19-cp313-cp313-win_amd64.whl", hash = "sha256:127ea69273485348a126ebbf3d6052604d3c7da284f797bba781f364c0947d47", size = 277253, upload-time = "2026-02-19T19:01:51.208Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c2/852b9600d53fb47e47080c203e2cdc0ac7e84e37032a57e0eaa37446033a/regex-2026.2.19-cp313-cp313-win_arm64.whl", hash = "sha256:5e56c669535ac59cbf96ca1ece0ef26cb66809990cda4fa45e1e32c3b146599e", size = 270505, upload-time = "2026-02-19T19:01:52.865Z" }, + { url = "https://files.pythonhosted.org/packages/a9/a2/e0b4575b93bc84db3b1fab24183e008691cd2db5c0ef14ed52681fbd94dd/regex-2026.2.19-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:93d881cab5afdc41a005dba1524a40947d6f7a525057aa64aaf16065cf62faa9", size = 492202, upload-time = "2026-02-19T19:01:54.816Z" }, + { url = "https://files.pythonhosted.org/packages/24/b5/b84fec8cbb5f92a7eed2b6b5353a6a9eed9670fee31817c2da9eb85dc797/regex-2026.2.19-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:80caaa1ddcc942ec7be18427354f9d58a79cee82dea2a6b3d4fd83302e1240d7", size = 292884, upload-time = "2026-02-19T19:01:58.254Z" }, + { url = "https://files.pythonhosted.org/packages/70/0c/fe89966dfae43da46f475362401f03e4d7dc3a3c955b54f632abc52669e0/regex-2026.2.19-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d793c5b4d2b4c668524cd1651404cfc798d40694c759aec997e196fe9729ec60", size = 291236, upload-time = "2026-02-19T19:01:59.966Z" }, + { url = "https://files.pythonhosted.org/packages/f2/f7/bda2695134f3e63eb5cccbbf608c2a12aab93d261ff4e2fe49b47fabc948/regex-2026.2.19-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5100acb20648d9efd3f4e7e91f51187f95f22a741dcd719548a6cf4e1b34b3f", size = 807660, upload-time = "2026-02-19T19:02:01.632Z" }, + { url = "https://files.pythonhosted.org/packages/11/56/6e3a4bf5e60d17326b7003d91bbde8938e439256dec211d835597a44972d/regex-2026.2.19-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5e3a31e94d10e52a896adaa3adf3621bd526ad2b45b8c2d23d1bbe74c7423007", size = 873585, upload-time = "2026-02-19T19:02:03.522Z" }, + { url = "https://files.pythonhosted.org/packages/35/5e/c90c6aa4d1317cc11839359479cfdd2662608f339e84e81ba751c8a4e461/regex-2026.2.19-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8497421099b981f67c99eba4154cf0dfd8e47159431427a11cfb6487f7791d9e", size = 915243, upload-time = "2026-02-19T19:02:05.608Z" }, + { url = "https://files.pythonhosted.org/packages/90/7c/981ea0694116793001496aaf9524e5c99e122ec3952d9e7f1878af3a6bf1/regex-2026.2.19-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e7a08622f7d51d7a068f7e4052a38739c412a3e74f55817073d2e2418149619", size = 812922, upload-time = "2026-02-19T19:02:08.115Z" }, + { url = "https://files.pythonhosted.org/packages/2d/be/9eda82afa425370ffdb3fa9f3ea42450b9ae4da3ff0a4ec20466f69e371b/regex-2026.2.19-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8abe671cf0f15c26b1ad389bf4043b068ce7d3b1c5d9313e12895f57d6738555", size = 781318, upload-time = "2026-02-19T19:02:10.072Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d5/50f0bbe56a8199f60a7b6c714e06e54b76b33d31806a69d0703b23ce2a9e/regex-2026.2.19-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5a8f28dd32a4ce9c41758d43b5b9115c1c497b4b1f50c457602c1d571fa98ce1", size = 795649, upload-time = "2026-02-19T19:02:11.96Z" }, + { url = "https://files.pythonhosted.org/packages/c5/09/d039f081e44a8b0134d0bb2dd805b0ddf390b69d0b58297ae098847c572f/regex-2026.2.19-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:654dc41a5ba9b8cc8432b3f1aa8906d8b45f3e9502442a07c2f27f6c63f85db5", size = 868844, upload-time = "2026-02-19T19:02:14.043Z" }, + { url = "https://files.pythonhosted.org/packages/ef/53/e2903b79a19ec8557fe7cd21cd093956ff2dbc2e0e33969e3adbe5b184dd/regex-2026.2.19-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:4a02faea614e7fdd6ba8b3bec6c8e79529d356b100381cec76e638f45d12ca04", size = 770113, upload-time = "2026-02-19T19:02:16.161Z" }, + { url = "https://files.pythonhosted.org/packages/8f/e2/784667767b55714ebb4e59bf106362327476b882c0b2f93c25e84cc99b1a/regex-2026.2.19-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d96162140bb819814428800934c7b71b7bffe81fb6da2d6abc1dcca31741eca3", size = 854922, upload-time = "2026-02-19T19:02:18.155Z" }, + { url = "https://files.pythonhosted.org/packages/59/78/9ef4356bd4aed752775bd18071034979b85f035fec51f3a4f9dea497a254/regex-2026.2.19-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c227f2922153ee42bbeb355fd6d009f8c81d9d7bdd666e2276ce41f53ed9a743", size = 799636, upload-time = "2026-02-19T19:02:20.04Z" }, + { url = "https://files.pythonhosted.org/packages/cf/54/fcfc9287f20c5c9bd8db755aafe3e8cf4d99a6a3f1c7162ee182e0ca9374/regex-2026.2.19-cp313-cp313t-win32.whl", hash = "sha256:a178df8ec03011153fbcd2c70cb961bc98cbbd9694b28f706c318bee8927c3db", size = 268968, upload-time = "2026-02-19T19:02:22.816Z" }, + { url = "https://files.pythonhosted.org/packages/1e/a0/ff24c6cb1273e42472706d277147fc38e1f9074a280fb6034b0fc9b69415/regex-2026.2.19-cp313-cp313t-win_amd64.whl", hash = "sha256:2c1693ca6f444d554aa246b592355b5cec030ace5a2729eae1b04ab6e853e768", size = 280390, upload-time = "2026-02-19T19:02:25.231Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b6/a3f6ad89d780ffdeebb4d5e2e3e30bd2ef1f70f6a94d1760e03dd1e12c60/regex-2026.2.19-cp313-cp313t-win_arm64.whl", hash = "sha256:c0761d7ae8d65773e01515ebb0b304df1bf37a0a79546caad9cbe79a42c12af7", size = 271643, upload-time = "2026-02-19T19:02:27.175Z" }, + { url = "https://files.pythonhosted.org/packages/2d/e2/7ad4e76a6dddefc0d64dbe12a4d3ca3947a19ddc501f864a5df2a8222ddd/regex-2026.2.19-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:03d191a9bcf94d31af56d2575210cb0d0c6a054dbcad2ea9e00aa4c42903b919", size = 489306, upload-time = "2026-02-19T19:02:29.058Z" }, + { url = "https://files.pythonhosted.org/packages/14/95/ee1736135733afbcf1846c58671046f99c4d5170102a150ebb3dd8d701d9/regex-2026.2.19-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:516ee067c6c721d0d0bfb80a2004edbd060fffd07e456d4e1669e38fe82f922e", size = 291218, upload-time = "2026-02-19T19:02:31.083Z" }, + { url = "https://files.pythonhosted.org/packages/ef/08/180d1826c3d7065200a5168c6b993a44947395c7bb6e04b2c2a219c34225/regex-2026.2.19-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:997862c619994c4a356cb7c3592502cbd50c2ab98da5f61c5c871f10f22de7e5", size = 289097, upload-time = "2026-02-19T19:02:33.485Z" }, + { url = "https://files.pythonhosted.org/packages/28/93/0651924c390c5740f5f896723f8ddd946a6c63083a7d8647231c343912ff/regex-2026.2.19-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02b9e1b8a7ebe2807cd7bbdf662510c8e43053a23262b9f46ad4fc2dfc9d204e", size = 799147, upload-time = "2026-02-19T19:02:35.669Z" }, + { url = "https://files.pythonhosted.org/packages/a7/00/2078bd8bcd37d58a756989adbfd9f1d0151b7ca4085a9c2a07e917fbac61/regex-2026.2.19-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6c8fb3b19652e425ff24169dad3ee07f99afa7996caa9dfbb3a9106cd726f49a", size = 865239, upload-time = "2026-02-19T19:02:38.012Z" }, + { url = "https://files.pythonhosted.org/packages/2a/13/75195161ec16936b35a365fa8c1dd2ab29fd910dd2587765062b174d8cfc/regex-2026.2.19-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50f1ee9488dd7a9fda850ec7c68cad7a32fa49fd19733f5403a3f92b451dcf73", size = 911904, upload-time = "2026-02-19T19:02:40.737Z" }, + { url = "https://files.pythonhosted.org/packages/96/72/ac42f6012179343d1c4bd0ffee8c948d841cb32ea188d37e96d80527fcc9/regex-2026.2.19-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ab780092b1424d13200aa5a62996e95f65ee3db8509be366437439cdc0af1a9f", size = 803518, upload-time = "2026-02-19T19:02:42.923Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d1/75a08e2269b007b9783f0f86aa64488e023141219cb5f14dc1e69cda56c6/regex-2026.2.19-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:17648e1a88e72d88641b12635e70e6c71c5136ba14edba29bf8fc6834005a265", size = 775866, upload-time = "2026-02-19T19:02:45.189Z" }, + { url = "https://files.pythonhosted.org/packages/92/41/70e7d05faf6994c2ca7a9fcaa536da8f8e4031d45b0ec04b57040ede201f/regex-2026.2.19-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f914ae8c804c8a8a562fe216100bc156bfb51338c1f8d55fe32cf407774359a", size = 788224, upload-time = "2026-02-19T19:02:47.804Z" }, + { url = "https://files.pythonhosted.org/packages/c8/83/34a2dd601f9deb13c20545c674a55f4a05c90869ab73d985b74d639bac43/regex-2026.2.19-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c7e121a918bbee3f12ac300ce0a0d2f2c979cf208fb071ed8df5a6323281915c", size = 859682, upload-time = "2026-02-19T19:02:50.583Z" }, + { url = "https://files.pythonhosted.org/packages/8e/30/136db9a09a7f222d6e48b806f3730e7af6499a8cad9c72ac0d49d52c746e/regex-2026.2.19-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2fedd459c791da24914ecc474feecd94cf7845efb262ac3134fe27cbd7eda799", size = 764223, upload-time = "2026-02-19T19:02:52.777Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ea/bb947743c78a16df481fa0635c50aa1a439bb80b0e6dc24cd4e49c716679/regex-2026.2.19-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:ea8dfc99689240e61fb21b5fc2828f68b90abf7777d057b62d3166b7c1543c4c", size = 850101, upload-time = "2026-02-19T19:02:55.87Z" }, + { url = "https://files.pythonhosted.org/packages/25/27/e3bfe6e97a99f7393665926be02fef772da7f8aa59e50bc3134e4262a032/regex-2026.2.19-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9fff45852160960f29e184ec8a5be5ab4063cfd0b168d439d1fc4ac3744bf29e", size = 789904, upload-time = "2026-02-19T19:02:58.523Z" }, + { url = "https://files.pythonhosted.org/packages/84/7b/7e2be6f00cea59d08761b027ad237002e90cac74b1607200ebaa2ba3d586/regex-2026.2.19-cp314-cp314-win32.whl", hash = "sha256:5390b130cce14a7d1db226a3896273b7b35be10af35e69f1cca843b6e5d2bb2d", size = 271784, upload-time = "2026-02-19T19:03:00.418Z" }, + { url = "https://files.pythonhosted.org/packages/f7/f6/639911530335773e7ec60bcaa519557b719586024c1d7eaad1daf87b646b/regex-2026.2.19-cp314-cp314-win_amd64.whl", hash = "sha256:e581f75d5c0b15669139ca1c2d3e23a65bb90e3c06ba9d9ea194c377c726a904", size = 280506, upload-time = "2026-02-19T19:03:02.302Z" }, + { url = "https://files.pythonhosted.org/packages/cd/ec/2582b56b4e036d46bb9b5d74a18548439ffa16c11cf59076419174d80f48/regex-2026.2.19-cp314-cp314-win_arm64.whl", hash = "sha256:7187fdee1be0896c1499a991e9bf7c78e4b56b7863e7405d7bb687888ac10c4b", size = 273557, upload-time = "2026-02-19T19:03:04.836Z" }, + { url = "https://files.pythonhosted.org/packages/49/0b/f901cfeb4efd83e4f5c3e9f91a6de77e8e5ceb18555698aca3a27e215ed3/regex-2026.2.19-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:5ec1d7c080832fdd4e150c6f5621fe674c70c63b3ae5a4454cebd7796263b175", size = 492196, upload-time = "2026-02-19T19:03:08.188Z" }, + { url = "https://files.pythonhosted.org/packages/94/0a/349b959e3da874e15eda853755567b4cde7e5309dbb1e07bfe910cfde452/regex-2026.2.19-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8457c1bc10ee9b29cdfd897ccda41dce6bde0e9abd514bcfef7bcd05e254d411", size = 292878, upload-time = "2026-02-19T19:03:10.272Z" }, + { url = "https://files.pythonhosted.org/packages/98/b0/9d81b3c2c5ddff428f8c506713737278979a2c476f6e3675a9c51da0c389/regex-2026.2.19-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cce8027010d1ffa3eb89a0b19621cdc78ae548ea2b49fea1f7bfb3ea77064c2b", size = 291235, upload-time = "2026-02-19T19:03:12.5Z" }, + { url = "https://files.pythonhosted.org/packages/04/e7/be7818df8691dbe9508c381ea2cc4c1153e4fdb1c4b06388abeaa93bd712/regex-2026.2.19-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11c138febb40546ff9e026dbbc41dc9fb8b29e61013fa5848ccfe045f5b23b83", size = 807893, upload-time = "2026-02-19T19:03:15.064Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b6/b898a8b983190cfa0276031c17beb73cfd1db07c03c8c37f606d80b655e2/regex-2026.2.19-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:74ff212aa61532246bb3036b3dfea62233414b0154b8bc3676975da78383cac3", size = 873696, upload-time = "2026-02-19T19:03:17.848Z" }, + { url = "https://files.pythonhosted.org/packages/1a/98/126ba671d54f19080ec87cad228fb4f3cc387fff8c4a01cb4e93f4ff9d94/regex-2026.2.19-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d00c95a2b6bfeb3ea1cb68d1751b1dfce2b05adc2a72c488d77a780db06ab867", size = 915493, upload-time = "2026-02-19T19:03:20.343Z" }, + { url = "https://files.pythonhosted.org/packages/b2/10/550c84a1a1a7371867fe8be2bea7df55e797cbca4709974811410e195c5d/regex-2026.2.19-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:311fcccb76af31be4c588d5a17f8f1a059ae8f4b097192896ebffc95612f223a", size = 813094, upload-time = "2026-02-19T19:03:23.287Z" }, + { url = "https://files.pythonhosted.org/packages/29/fb/ba221d2fc76a27b6b7d7a60f73a7a6a7bac21c6ba95616a08be2bcb434b0/regex-2026.2.19-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:77cfd6b5e7c4e8bf7a39d243ea05882acf5e3c7002b0ef4756de6606893b0ecd", size = 781583, upload-time = "2026-02-19T19:03:26.872Z" }, + { url = "https://files.pythonhosted.org/packages/26/f1/af79231301297c9e962679efc04a31361b58dc62dec1fc0cb4b8dd95956a/regex-2026.2.19-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6380f29ff212ec922b6efb56100c089251940e0526a0d05aa7c2d9b571ddf2fe", size = 795875, upload-time = "2026-02-19T19:03:29.223Z" }, + { url = "https://files.pythonhosted.org/packages/a0/90/1e1d76cb0a2d0a4f38a039993e1c5cd971ae50435d751c5bae4f10e1c302/regex-2026.2.19-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:655f553a1fa3ab8a7fd570eca793408b8d26a80bfd89ed24d116baaf13a38969", size = 868916, upload-time = "2026-02-19T19:03:31.415Z" }, + { url = "https://files.pythonhosted.org/packages/9a/67/a1c01da76dbcfed690855a284c665cc0a370e7d02d1bd635cf9ff7dd74b8/regex-2026.2.19-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:015088b8558502f1f0bccd58754835aa154a7a5b0bd9d4c9b7b96ff4ae9ba876", size = 770386, upload-time = "2026-02-19T19:03:33.972Z" }, + { url = "https://files.pythonhosted.org/packages/49/6f/94842bf294f432ff3836bfd91032e2ecabea6d284227f12d1f935318c9c4/regex-2026.2.19-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9e6693b8567a59459b5dda19104c4a4dbbd4a1c78833eacc758796f2cfef1854", size = 855007, upload-time = "2026-02-19T19:03:36.238Z" }, + { url = "https://files.pythonhosted.org/packages/ff/93/393cd203ca0d1d368f05ce12d2c7e91a324bc93c240db2e6d5ada05835f4/regex-2026.2.19-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4071209fd4376ab5ceec72ad3507e9d3517c59e38a889079b98916477a871868", size = 799863, upload-time = "2026-02-19T19:03:38.497Z" }, + { url = "https://files.pythonhosted.org/packages/43/d9/35afda99bd92bf1a5831e55a4936d37ea4bed6e34c176a3c2238317faf4f/regex-2026.2.19-cp314-cp314t-win32.whl", hash = "sha256:2905ff4a97fad42f2d0834d8b1ea3c2f856ec209837e458d71a061a7d05f9f01", size = 274742, upload-time = "2026-02-19T19:03:40.804Z" }, + { url = "https://files.pythonhosted.org/packages/ae/42/7edc3344dcc87b698e9755f7f685d463852d481302539dae07135202d3ca/regex-2026.2.19-cp314-cp314t-win_amd64.whl", hash = "sha256:64128549b600987e0f335c2365879895f860a9161f283b14207c800a6ed623d3", size = 284443, upload-time = "2026-02-19T19:03:42.954Z" }, + { url = "https://files.pythonhosted.org/packages/3a/45/affdf2d851b42adf3d13fc5b3b059372e9bd299371fd84cf5723c45871fa/regex-2026.2.19-cp314-cp314t-win_arm64.whl", hash = "sha256:a09ae430e94c049dc6957f6baa35ee3418a3a77f3c12b6e02883bd80a2b679b0", size = 274932, upload-time = "2026-02-19T19:03:45.488Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "rich" +version = "14.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" }, +] + +[[package]] +name = "rich-rst" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/6d/a506aaa4a9eaa945ed8ab2b7347859f53593864289853c5d6d62b77246e0/rich_rst-1.3.2.tar.gz", hash = "sha256:a1196fdddf1e364b02ec68a05e8ff8f6914fee10fbca2e6b6735f166bb0da8d4", size = 14936, upload-time = "2025-10-14T16:49:45.332Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/2f/b4530fbf948867702d0a3f27de4a6aab1d156f406d72852ab902c4d04de9/rich_rst-1.3.2-py3-none-any.whl", hash = "sha256:a99b4907cbe118cf9d18b0b44de272efa61f15117c61e39ebdc431baf5df722a", size = 12567, upload-time = "2025-10-14T16:49:42.953Z" }, +] + +[[package]] +name = "rich-toolkit" +version = "0.19.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/ba/dae9e3096651042754da419a4042bc1c75e07d615f9b15066d738838e4df/rich_toolkit-0.19.7.tar.gz", hash = "sha256:133c0915872da91d4c25d85342d5ec1dfacc69b63448af1a08a0d4b4f23ef46e", size = 195877, upload-time = "2026-02-24T16:06:20.555Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/3c/c923619f6d2f5fafcc96fec0aaf9550a46cd5b6481f06e0c6b66a2a4fed0/rich_toolkit-0.19.7-py3-none-any.whl", hash = "sha256:0288e9203728c47c5a4eb60fd2f0692d9df7455a65901ab6f898437a2ba5989d", size = 32963, upload-time = "2026-02-24T16:06:22.066Z" }, +] + +[[package]] +name = "rignore" +version = "0.7.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/f5/8bed2310abe4ae04b67a38374a4d311dd85220f5d8da56f47ae9361be0b0/rignore-0.7.6.tar.gz", hash = "sha256:00d3546cd793c30cb17921ce674d2c8f3a4b00501cb0e3dd0e82217dbeba2671", size = 57140, upload-time = "2025-11-05T21:41:21.968Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/0e/012556ef3047a2628842b44e753bb15f4dc46806780ff090f1e8fe4bf1eb/rignore-0.7.6-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:03e82348cb7234f8d9b2834f854400ddbbd04c0f8f35495119e66adbd37827a8", size = 883488, upload-time = "2025-11-05T20:42:41.359Z" }, + { url = "https://files.pythonhosted.org/packages/93/b0/d4f1f3fe9eb3f8e382d45ce5b0547ea01c4b7e0b4b4eb87bcd66a1d2b888/rignore-0.7.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9e624f6be6116ea682e76c5feb71ea91255c67c86cb75befe774365b2931961", size = 820411, upload-time = "2025-11-05T20:42:24.782Z" }, + { url = "https://files.pythonhosted.org/packages/4a/c8/dea564b36dedac8de21c18e1851789545bc52a0c22ece9843444d5608a6a/rignore-0.7.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bda49950d405aa8d0ebe26af807c4e662dd281d926530f03f29690a2e07d649a", size = 897821, upload-time = "2025-11-05T20:40:52.613Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2b/ee96db17ac1835e024c5d0742eefb7e46de60020385ac883dd3d1cde2c1f/rignore-0.7.6-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5fd5ab3840b8c16851d327ed06e9b8be6459702a53e5ab1fc4073b684b3789e", size = 873963, upload-time = "2025-11-05T20:41:07.49Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8c/ad5a57bbb9d14d5c7e5960f712a8a0b902472ea3f4a2138cbf70d1777b75/rignore-0.7.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ced2a248352636a5c77504cb755dc02c2eef9a820a44d3f33061ce1bb8a7f2d2", size = 1169216, upload-time = "2025-11-05T20:41:23.73Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/5b00bc2a6bc1701e6878fca798cf5d9125eb3113193e33078b6fc0d99123/rignore-0.7.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a04a3b73b75ddc12c9c9b21efcdaab33ca3832941d6f1d67bffd860941cd448a", size = 942942, upload-time = "2025-11-05T20:41:39.393Z" }, + { url = "https://files.pythonhosted.org/packages/85/e5/7f99bd0cc9818a91d0e8b9acc65b792e35750e3bdccd15a7ee75e64efca4/rignore-0.7.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d24321efac92140b7ec910ac7c53ab0f0c86a41133d2bb4b0e6a7c94967f44dd", size = 959787, upload-time = "2025-11-05T20:42:09.765Z" }, + { url = "https://files.pythonhosted.org/packages/55/54/2ffea79a7c1eabcede1926347ebc2a81bc6b81f447d05b52af9af14948b9/rignore-0.7.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:73c7aa109d41e593785c55fdaa89ad80b10330affa9f9d3e3a51fa695f739b20", size = 984245, upload-time = "2025-11-05T20:41:54.062Z" }, + { url = "https://files.pythonhosted.org/packages/41/f7/e80f55dfe0f35787fa482aa18689b9c8251e045076c35477deb0007b3277/rignore-0.7.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1734dc49d1e9501b07852ef44421f84d9f378da9fbeda729e77db71f49cac28b", size = 1078647, upload-time = "2025-11-05T21:40:13.463Z" }, + { url = "https://files.pythonhosted.org/packages/d4/cf/2c64f0b6725149f7c6e7e5a909d14354889b4beaadddaa5fff023ec71084/rignore-0.7.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5719ea14ea2b652c0c0894be5dfde954e1853a80dea27dd2fbaa749618d837f5", size = 1139186, upload-time = "2025-11-05T21:40:31.27Z" }, + { url = "https://files.pythonhosted.org/packages/75/95/a86c84909ccc24af0d094b50d54697951e576c252a4d9f21b47b52af9598/rignore-0.7.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8e23424fc7ce35726854f639cb7968151a792c0c3d9d082f7f67e0c362cfecca", size = 1117604, upload-time = "2025-11-05T21:40:48.07Z" }, + { url = "https://files.pythonhosted.org/packages/7f/5e/13b249613fd5d18d58662490ab910a9f0be758981d1797789913adb4e918/rignore-0.7.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3efdcf1dd84d45f3e2bd2f93303d9be103888f56dfa7c3349b5bf4f0657ec696", size = 1127725, upload-time = "2025-11-05T21:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/c7/28/fa5dcd1e2e16982c359128664e3785f202d3eca9b22dd0b2f91c4b3d242f/rignore-0.7.6-cp312-cp312-win32.whl", hash = "sha256:ccca9d1a8b5234c76b71546fc3c134533b013f40495f394a65614a81f7387046", size = 646145, upload-time = "2025-11-05T21:41:51.096Z" }, + { url = "https://files.pythonhosted.org/packages/26/87/69387fb5dd81a0f771936381431780b8cf66fcd2cfe9495e1aaf41548931/rignore-0.7.6-cp312-cp312-win_amd64.whl", hash = "sha256:c96a285e4a8bfec0652e0bfcf42b1aabcdda1e7625f5006d188e3b1c87fdb543", size = 726090, upload-time = "2025-11-05T21:41:36.485Z" }, + { url = "https://files.pythonhosted.org/packages/24/5f/e8418108dcda8087fb198a6f81caadbcda9fd115d61154bf0df4d6d3619b/rignore-0.7.6-cp312-cp312-win_arm64.whl", hash = "sha256:a64a750e7a8277a323f01ca50b7784a764845f6cce2fe38831cb93f0508d0051", size = 656317, upload-time = "2025-11-05T21:41:25.305Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8a/a4078f6e14932ac7edb171149c481de29969d96ddee3ece5dc4c26f9e0c3/rignore-0.7.6-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:2bdab1d31ec9b4fb1331980ee49ea051c0d7f7bb6baa28b3125ef03cdc48fdaf", size = 883057, upload-time = "2025-11-05T20:42:42.741Z" }, + { url = "https://files.pythonhosted.org/packages/f9/8f/f8daacd177db4bf7c2223bab41e630c52711f8af9ed279be2058d2fe4982/rignore-0.7.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:90f0a00ce0c866c275bf888271f1dc0d2140f29b82fcf33cdbda1e1a6af01010", size = 820150, upload-time = "2025-11-05T20:42:26.545Z" }, + { url = "https://files.pythonhosted.org/packages/36/31/b65b837e39c3f7064c426754714ac633b66b8c2290978af9d7f513e14aa9/rignore-0.7.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1ad295537041dc2ed4b540fb1a3906bd9ede6ccdad3fe79770cd89e04e3c73c", size = 897406, upload-time = "2025-11-05T20:40:53.854Z" }, + { url = "https://files.pythonhosted.org/packages/ca/58/1970ce006c427e202ac7c081435719a076c478f07b3a23f469227788dc23/rignore-0.7.6-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f782dbd3a65a5ac85adfff69e5c6b101285ef3f845c3a3cae56a54bebf9fe116", size = 874050, upload-time = "2025-11-05T20:41:08.922Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/eb45db9f90137329072a732273be0d383cb7d7f50ddc8e0bceea34c1dfdf/rignore-0.7.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65cece3b36e5b0826d946494734c0e6aaf5a0337e18ff55b071438efe13d559e", size = 1167835, upload-time = "2025-11-05T20:41:24.997Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f1/6f1d72ddca41a64eed569680587a1236633587cc9f78136477ae69e2c88a/rignore-0.7.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d7e4bb66c13cd7602dc8931822c02dfbbd5252015c750ac5d6152b186f0a8be0", size = 941945, upload-time = "2025-11-05T20:41:40.628Z" }, + { url = "https://files.pythonhosted.org/packages/48/6f/2f178af1c1a276a065f563ec1e11e7a9e23d4996fd0465516afce4b5c636/rignore-0.7.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:297e500c15766e196f68aaaa70e8b6db85fa23fdc075b880d8231fdfba738cd7", size = 959067, upload-time = "2025-11-05T20:42:11.09Z" }, + { url = "https://files.pythonhosted.org/packages/5b/db/423a81c4c1e173877c7f9b5767dcaf1ab50484a94f60a0b2ed78be3fa765/rignore-0.7.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a07084211a8d35e1a5b1d32b9661a5ed20669970b369df0cf77da3adea3405de", size = 984438, upload-time = "2025-11-05T20:41:55.443Z" }, + { url = "https://files.pythonhosted.org/packages/31/eb/c4f92cc3f2825d501d3c46a244a671eb737fc1bcf7b05a3ecd34abb3e0d7/rignore-0.7.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:181eb2a975a22256a1441a9d2f15eb1292839ea3f05606620bd9e1938302cf79", size = 1078365, upload-time = "2025-11-05T21:40:15.148Z" }, + { url = "https://files.pythonhosted.org/packages/26/09/99442f02794bd7441bfc8ed1c7319e890449b816a7493b2db0e30af39095/rignore-0.7.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:7bbcdc52b5bf9f054b34ce4af5269df5d863d9c2456243338bc193c28022bd7b", size = 1139066, upload-time = "2025-11-05T21:40:32.771Z" }, + { url = "https://files.pythonhosted.org/packages/2c/88/bcfc21e520bba975410e9419450f4b90a2ac8236b9a80fd8130e87d098af/rignore-0.7.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f2e027a6da21a7c8c0d87553c24ca5cc4364def18d146057862c23a96546238e", size = 1118036, upload-time = "2025-11-05T21:40:49.646Z" }, + { url = "https://files.pythonhosted.org/packages/e2/25/d37215e4562cda5c13312636393aea0bafe38d54d4e0517520a4cc0753ec/rignore-0.7.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee4a18b82cbbc648e4aac1510066682fe62beb5dc88e2c67c53a83954e541360", size = 1127550, upload-time = "2025-11-05T21:41:07.648Z" }, + { url = "https://files.pythonhosted.org/packages/dc/76/a264ab38bfa1620ec12a8ff1c07778da89e16d8c0f3450b0333020d3d6dc/rignore-0.7.6-cp313-cp313-win32.whl", hash = "sha256:a7d7148b6e5e95035d4390396895adc384d37ff4e06781a36fe573bba7c283e5", size = 646097, upload-time = "2025-11-05T21:41:53.201Z" }, + { url = "https://files.pythonhosted.org/packages/62/44/3c31b8983c29ea8832b6082ddb1d07b90379c2d993bd20fce4487b71b4f4/rignore-0.7.6-cp313-cp313-win_amd64.whl", hash = "sha256:b037c4b15a64dced08fc12310ee844ec2284c4c5c1ca77bc37d0a04f7bff386e", size = 726170, upload-time = "2025-11-05T21:41:38.131Z" }, + { url = "https://files.pythonhosted.org/packages/aa/41/e26a075cab83debe41a42661262f606166157df84e0e02e2d904d134c0d8/rignore-0.7.6-cp313-cp313-win_arm64.whl", hash = "sha256:e47443de9b12fe569889bdbe020abe0e0b667516ee2ab435443f6d0869bd2804", size = 656184, upload-time = "2025-11-05T21:41:27.396Z" }, + { url = "https://files.pythonhosted.org/packages/9a/b9/1f5bd82b87e5550cd843ceb3768b4a8ef274eb63f29333cf2f29644b3d75/rignore-0.7.6-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:8e41be9fa8f2f47239ded8920cc283699a052ac4c371f77f5ac017ebeed75732", size = 882632, upload-time = "2025-11-05T20:42:44.063Z" }, + { url = "https://files.pythonhosted.org/packages/e9/6b/07714a3efe4a8048864e8a5b7db311ba51b921e15268b17defaebf56d3db/rignore-0.7.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6dc1e171e52cefa6c20e60c05394a71165663b48bca6c7666dee4f778f2a7d90", size = 820760, upload-time = "2025-11-05T20:42:27.885Z" }, + { url = "https://files.pythonhosted.org/packages/ac/0f/348c829ea2d8d596e856371b14b9092f8a5dfbb62674ec9b3f67e4939a9d/rignore-0.7.6-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ce2268837c3600f82ab8db58f5834009dc638ee17103582960da668963bebc5", size = 899044, upload-time = "2025-11-05T20:40:55.336Z" }, + { url = "https://files.pythonhosted.org/packages/f0/30/2e1841a19b4dd23878d73edd5d82e998a83d5ed9570a89675f140ca8b2ad/rignore-0.7.6-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:690a3e1b54bfe77e89c4bacb13f046e642f8baadafc61d68f5a726f324a76ab6", size = 874144, upload-time = "2025-11-05T20:41:10.195Z" }, + { url = "https://files.pythonhosted.org/packages/c2/bf/0ce9beb2e5f64c30e3580bef09f5829236889f01511a125f98b83169b993/rignore-0.7.6-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09d12ac7a0b6210c07bcd145007117ebd8abe99c8eeb383e9e4673910c2754b2", size = 1168062, upload-time = "2025-11-05T20:41:26.511Z" }, + { url = "https://files.pythonhosted.org/packages/b9/8b/571c178414eb4014969865317da8a02ce4cf5241a41676ef91a59aab24de/rignore-0.7.6-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a2b2b74a8c60203b08452479b90e5ce3dbe96a916214bc9eb2e5af0b6a9beb0", size = 942542, upload-time = "2025-11-05T20:41:41.838Z" }, + { url = "https://files.pythonhosted.org/packages/19/62/7a3cf601d5a45137a7e2b89d10c05b5b86499190c4b7ca5c3c47d79ee519/rignore-0.7.6-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fc5a531ef02131e44359419a366bfac57f773ea58f5278c2cdd915f7d10ea94", size = 958739, upload-time = "2025-11-05T20:42:12.463Z" }, + { url = "https://files.pythonhosted.org/packages/5f/1f/4261f6a0d7caf2058a5cde2f5045f565ab91aa7badc972b57d19ce58b14e/rignore-0.7.6-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b7a1f77d9c4cd7e76229e252614d963442686bfe12c787a49f4fe481df49e7a9", size = 984138, upload-time = "2025-11-05T20:41:56.775Z" }, + { url = "https://files.pythonhosted.org/packages/2b/bf/628dfe19c75e8ce1f45f7c248f5148b17dfa89a817f8e3552ab74c3ae812/rignore-0.7.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ead81f728682ba72b5b1c3d5846b011d3e0174da978de87c61645f2ed36659a7", size = 1079299, upload-time = "2025-11-05T21:40:16.639Z" }, + { url = "https://files.pythonhosted.org/packages/af/a5/be29c50f5c0c25c637ed32db8758fdf5b901a99e08b608971cda8afb293b/rignore-0.7.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:12ffd50f520c22ffdabed8cd8bfb567d9ac165b2b854d3e679f4bcaef11a9441", size = 1139618, upload-time = "2025-11-05T21:40:34.507Z" }, + { url = "https://files.pythonhosted.org/packages/2a/40/3c46cd7ce4fa05c20b525fd60f599165e820af66e66f2c371cd50644558f/rignore-0.7.6-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:e5a16890fbe3c894f8ca34b0fcacc2c200398d4d46ae654e03bc9b3dbf2a0a72", size = 1117626, upload-time = "2025-11-05T21:40:51.494Z" }, + { url = "https://files.pythonhosted.org/packages/8c/b9/aea926f263b8a29a23c75c2e0d8447965eb1879d3feb53cfcf84db67ed58/rignore-0.7.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3abab3bf99e8a77488ef6c7c9a799fac22224c28fe9f25cc21aa7cc2b72bfc0b", size = 1128144, upload-time = "2025-11-05T21:41:09.169Z" }, + { url = "https://files.pythonhosted.org/packages/a4/f6/0d6242f8d0df7f2ecbe91679fefc1f75e7cd2072cb4f497abaab3f0f8523/rignore-0.7.6-cp314-cp314-win32.whl", hash = "sha256:eeef421c1782953c4375aa32f06ecae470c1285c6381eee2a30d2e02a5633001", size = 646385, upload-time = "2025-11-05T21:41:55.105Z" }, + { url = "https://files.pythonhosted.org/packages/d5/38/c0dcd7b10064f084343d6af26fe9414e46e9619c5f3224b5272e8e5d9956/rignore-0.7.6-cp314-cp314-win_amd64.whl", hash = "sha256:6aeed503b3b3d5af939b21d72a82521701a4bd3b89cd761da1e7dc78621af304", size = 725738, upload-time = "2025-11-05T21:41:39.736Z" }, + { url = "https://files.pythonhosted.org/packages/d9/7a/290f868296c1ece914d565757ab363b04730a728b544beb567ceb3b2d96f/rignore-0.7.6-cp314-cp314-win_arm64.whl", hash = "sha256:104f215b60b3c984c386c3e747d6ab4376d5656478694e22c7bd2f788ddd8304", size = 656008, upload-time = "2025-11-05T21:41:29.028Z" }, + { url = "https://files.pythonhosted.org/packages/ca/d2/3c74e3cd81fe8ea08a8dcd2d755c09ac2e8ad8fe409508904557b58383d3/rignore-0.7.6-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:bb24a5b947656dd94cb9e41c4bc8b23cec0c435b58be0d74a874f63c259549e8", size = 882835, upload-time = "2025-11-05T20:42:45.443Z" }, + { url = "https://files.pythonhosted.org/packages/77/61/a772a34b6b63154877433ac2d048364815b24c2dd308f76b212c408101a2/rignore-0.7.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5b1e33c9501cefe24b70a1eafd9821acfd0ebf0b35c3a379430a14df089993e3", size = 820301, upload-time = "2025-11-05T20:42:29.226Z" }, + { url = "https://files.pythonhosted.org/packages/71/30/054880b09c0b1b61d17eeb15279d8bf729c0ba52b36c3ada52fb827cbb3c/rignore-0.7.6-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bec3994665a44454df86deb762061e05cd4b61e3772f5b07d1882a8a0d2748d5", size = 897611, upload-time = "2025-11-05T20:40:56.475Z" }, + { url = "https://files.pythonhosted.org/packages/1e/40/b2d1c169f833d69931bf232600eaa3c7998ba4f9a402e43a822dad2ea9f2/rignore-0.7.6-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26cba2edfe3cff1dfa72bddf65d316ddebf182f011f2f61538705d6dbaf54986", size = 873875, upload-time = "2025-11-05T20:41:11.561Z" }, + { url = "https://files.pythonhosted.org/packages/55/59/ca5ae93d83a1a60e44b21d87deb48b177a8db1b85e82fc8a9abb24a8986d/rignore-0.7.6-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ffa86694fec604c613696cb91e43892aa22e1fec5f9870e48f111c603e5ec4e9", size = 1167245, upload-time = "2025-11-05T20:41:28.29Z" }, + { url = "https://files.pythonhosted.org/packages/a5/52/cf3dce392ba2af806cba265aad6bcd9c48bb2a6cb5eee448d3319f6e505b/rignore-0.7.6-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48efe2ed95aa8104145004afb15cdfa02bea5cdde8b0344afeb0434f0d989aa2", size = 941750, upload-time = "2025-11-05T20:41:43.111Z" }, + { url = "https://files.pythonhosted.org/packages/ec/be/3f344c6218d779395e785091d05396dfd8b625f6aafbe502746fcd880af2/rignore-0.7.6-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dcae43eb44b7f2457fef7cc87f103f9a0013017a6f4e62182c565e924948f21", size = 958896, upload-time = "2025-11-05T20:42:13.784Z" }, + { url = "https://files.pythonhosted.org/packages/c9/34/d3fa71938aed7d00dcad87f0f9bcb02ad66c85d6ffc83ba31078ce53646a/rignore-0.7.6-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2cd649a7091c0dad2f11ef65630d30c698d505cbe8660dd395268e7c099cc99f", size = 983992, upload-time = "2025-11-05T20:41:58.022Z" }, + { url = "https://files.pythonhosted.org/packages/24/a4/52a697158e9920705bdbd0748d59fa63e0f3233fb92e9df9a71afbead6ca/rignore-0.7.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42de84b0289d478d30ceb7ae59023f7b0527786a9a5b490830e080f0e4ea5aeb", size = 1078181, upload-time = "2025-11-05T21:40:18.151Z" }, + { url = "https://files.pythonhosted.org/packages/ac/65/aa76dbcdabf3787a6f0fd61b5cc8ed1e88580590556d6c0207960d2384bb/rignore-0.7.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:875a617e57b53b4acbc5a91de418233849711c02e29cc1f4f9febb2f928af013", size = 1139232, upload-time = "2025-11-05T21:40:35.966Z" }, + { url = "https://files.pythonhosted.org/packages/08/44/31b31a49b3233c6842acc1c0731aa1e7fb322a7170612acf30327f700b44/rignore-0.7.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8703998902771e96e49968105207719f22926e4431b108450f3f430b4e268b7c", size = 1117349, upload-time = "2025-11-05T21:40:53.013Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ae/1b199a2302c19c658cf74e5ee1427605234e8c91787cfba0015f2ace145b/rignore-0.7.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:602ef33f3e1b04c1e9a10a3c03f8bc3cef2d2383dcc250d309be42b49923cabc", size = 1127702, upload-time = "2025-11-05T21:41:10.881Z" }, + { url = "https://files.pythonhosted.org/packages/fc/d3/18210222b37e87e36357f7b300b7d98c6dd62b133771e71ae27acba83a4f/rignore-0.7.6-cp314-cp314t-win32.whl", hash = "sha256:c1d8f117f7da0a4a96a8daef3da75bc090e3792d30b8b12cfadc240c631353f9", size = 647033, upload-time = "2025-11-05T21:42:00.095Z" }, + { url = "https://files.pythonhosted.org/packages/3e/87/033eebfbee3ec7d92b3bb1717d8f68c88e6fc7de54537040f3b3a405726f/rignore-0.7.6-cp314-cp314t-win_amd64.whl", hash = "sha256:ca36e59408bec81de75d307c568c2d0d410fb880b1769be43611472c61e85c96", size = 725647, upload-time = "2025-11-05T21:41:44.449Z" }, + { url = "https://files.pythonhosted.org/packages/79/62/b88e5879512c55b8ee979c666ee6902adc4ed05007226de266410ae27965/rignore-0.7.6-cp314-cp314t-win_arm64.whl", hash = "sha256:b83adabeb3e8cf662cabe1931b83e165b88c526fa6af6b3aa90429686e474896", size = 656035, upload-time = "2025-11-05T21:41:31.13Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, + { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, + { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, + { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, + { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, + { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, + { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, + { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, + { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, + { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, + { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, + { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, + { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, + { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, + { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, + { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, + { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, + { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, + { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, + { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, +] + +[[package]] +name = "ruamel-yaml" +version = "0.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/3b/ebda527b56beb90cb7652cb1c7e4f91f48649fbcd8d2eb2fb6e77cd3329b/ruamel_yaml-0.19.1.tar.gz", hash = "sha256:53eb66cd27849eff968ebf8f0bf61f46cdac2da1d1f3576dd4ccee9b25c31993", size = 142709, upload-time = "2026-01-02T16:50:31.84Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/0c/51f6841f1d84f404f92463fc2b1ba0da357ca1e3db6b7fbda26956c3b82a/ruamel_yaml-0.19.1-py3-none-any.whl", hash = "sha256:27592957fedf6e0b62f281e96effd28043345e0e66001f97683aa9a40c667c93", size = 118102, upload-time = "2026-01-02T16:50:29.201Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/04/eab13a954e763b0606f460443fcbf6bb5a0faf06890ea3754ff16523dce5/ruff-0.15.2.tar.gz", hash = "sha256:14b965afee0969e68bb871eba625343b8673375f457af4abe98553e8bbb98342", size = 4558148, upload-time = "2026-02-19T22:32:20.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/70/3a4dc6d09b13cb3e695f28307e5d889b2e1a66b7af9c5e257e796695b0e6/ruff-0.15.2-py3-none-linux_armv6l.whl", hash = "sha256:120691a6fdae2f16d65435648160f5b81a9625288f75544dc40637436b5d3c0d", size = 10430565, upload-time = "2026-02-19T22:32:41.824Z" }, + { url = "https://files.pythonhosted.org/packages/71/0b/bb8457b56185ece1305c666dc895832946d24055be90692381c31d57466d/ruff-0.15.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a89056d831256099658b6bba4037ac6dd06f49d194199215befe2bb10457ea5e", size = 10820354, upload-time = "2026-02-19T22:32:07.366Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c1/e0532d7f9c9e0b14c46f61b14afd563298b8b83f337b6789ddd987e46121/ruff-0.15.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e36dee3a64be0ebd23c86ffa3aa3fd3ac9a712ff295e192243f814a830b6bd87", size = 10170767, upload-time = "2026-02-19T22:32:13.188Z" }, + { url = "https://files.pythonhosted.org/packages/47/e8/da1aa341d3af017a21c7a62fb5ec31d4e7ad0a93ab80e3a508316efbcb23/ruff-0.15.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9fb47b6d9764677f8c0a193c0943ce9a05d6763523f132325af8a858eadc2b9", size = 10529591, upload-time = "2026-02-19T22:32:02.547Z" }, + { url = "https://files.pythonhosted.org/packages/93/74/184fbf38e9f3510231fbc5e437e808f0b48c42d1df9434b208821efcd8d6/ruff-0.15.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f376990f9d0d6442ea9014b19621d8f2aaf2b8e39fdbfc79220b7f0c596c9b80", size = 10260771, upload-time = "2026-02-19T22:32:36.938Z" }, + { url = "https://files.pythonhosted.org/packages/05/ac/605c20b8e059a0bc4b42360414baa4892ff278cec1c91fff4be0dceedefd/ruff-0.15.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2dcc987551952d73cbf5c88d9fdee815618d497e4df86cd4c4824cc59d5dd75f", size = 11045791, upload-time = "2026-02-19T22:32:31.642Z" }, + { url = "https://files.pythonhosted.org/packages/fd/52/db6e419908f45a894924d410ac77d64bdd98ff86901d833364251bd08e22/ruff-0.15.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:42a47fd785cbe8c01b9ff45031af875d101b040ad8f4de7bbb716487c74c9a77", size = 11879271, upload-time = "2026-02-19T22:32:29.305Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d8/7992b18f2008bdc9231d0f10b16df7dda964dbf639e2b8b4c1b4e91b83af/ruff-0.15.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cbe9f49354866e575b4c6943856989f966421870e85cd2ac94dccb0a9dcb2fea", size = 11303707, upload-time = "2026-02-19T22:32:22.492Z" }, + { url = "https://files.pythonhosted.org/packages/d7/02/849b46184bcfdd4b64cde61752cc9a146c54759ed036edd11857e9b8443b/ruff-0.15.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7a672c82b5f9887576087d97be5ce439f04bbaf548ee987b92d3a7dede41d3a", size = 11149151, upload-time = "2026-02-19T22:32:44.234Z" }, + { url = "https://files.pythonhosted.org/packages/70/04/f5284e388bab60d1d3b99614a5a9aeb03e0f333847e2429bebd2aaa1feec/ruff-0.15.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:72ecc64f46f7019e2bcc3cdc05d4a7da958b629a5ab7033195e11a438403d956", size = 11091132, upload-time = "2026-02-19T22:32:24.691Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ae/88d844a21110e14d92cf73d57363fab59b727ebeabe78009b9ccb23500af/ruff-0.15.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:8dcf243b15b561c655c1ef2f2b0050e5d50db37fe90115507f6ff37d865dc8b4", size = 10504717, upload-time = "2026-02-19T22:32:26.75Z" }, + { url = "https://files.pythonhosted.org/packages/64/27/867076a6ada7f2b9c8292884ab44d08fd2ba71bd2b5364d4136f3cd537e1/ruff-0.15.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dab6941c862c05739774677c6273166d2510d254dac0695c0e3f5efa1b5585de", size = 10263122, upload-time = "2026-02-19T22:32:10.036Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ef/faf9321d550f8ebf0c6373696e70d1758e20ccdc3951ad7af00c0956be7c/ruff-0.15.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1b9164f57fc36058e9a6806eb92af185b0697c9fe4c7c52caa431c6554521e5c", size = 10735295, upload-time = "2026-02-19T22:32:39.227Z" }, + { url = "https://files.pythonhosted.org/packages/2f/55/e8089fec62e050ba84d71b70e7834b97709ca9b7aba10c1a0b196e493f97/ruff-0.15.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:80d24fcae24d42659db7e335b9e1531697a7102c19185b8dc4a028b952865fd8", size = 11241641, upload-time = "2026-02-19T22:32:34.617Z" }, + { url = "https://files.pythonhosted.org/packages/23/01/1c30526460f4d23222d0fabd5888868262fd0e2b71a00570ca26483cd993/ruff-0.15.2-py3-none-win32.whl", hash = "sha256:fd5ff9e5f519a7e1bd99cbe8daa324010a74f5e2ebc97c6242c08f26f3714f6f", size = 10507885, upload-time = "2026-02-19T22:32:15.635Z" }, + { url = "https://files.pythonhosted.org/packages/5c/10/3d18e3bbdf8fc50bbb4ac3cc45970aa5a9753c5cb51bf9ed9a3cd8b79fa3/ruff-0.15.2-py3-none-win_amd64.whl", hash = "sha256:d20014e3dfa400f3ff84830dfb5755ece2de45ab62ecea4af6b7262d0fb4f7c5", size = 11623725, upload-time = "2026-02-19T22:32:04.947Z" }, + { url = "https://files.pythonhosted.org/packages/6d/78/097c0798b1dab9f8affe73da9642bb4500e098cb27fd8dc9724816ac747b/ruff-0.15.2-py3-none-win_arm64.whl", hash = "sha256:cabddc5822acdc8f7b5527b36ceac55cc51eec7b1946e60181de8fe83ca8876e", size = 10941649, upload-time = "2026-02-19T22:32:18.108Z" }, +] + +[[package]] +name = "secretstorage" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "jeepney" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" }, +] + +[[package]] +name = "sentry-sdk" +version = "2.53.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d3/06/66c8b705179bc54087845f28fd1b72f83751b6e9a195628e2e9af9926505/sentry_sdk-2.53.0.tar.gz", hash = "sha256:6520ef2c4acd823f28efc55e43eb6ce2e6d9f954a95a3aa96b6fd14871e92b77", size = 412369, upload-time = "2026-02-16T11:11:14.743Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/d4/2fdf854bc3b9c7f55219678f812600a20a138af2dd847d99004994eada8f/sentry_sdk-2.53.0-py2.py3-none-any.whl", hash = "sha256:46e1ed8d84355ae54406c924f6b290c3d61f4048625989a723fd622aab838899", size = 437908, upload-time = "2026-02-16T11:11:13.227Z" }, +] + +[[package]] +name = "setuptools" +version = "82.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4f/db/cfac1baf10650ab4d1c111714410d2fbb77ac5a616db26775db562c8fab2/setuptools-82.0.1.tar.gz", hash = "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9", size = 1152316, upload-time = "2026-03-09T12:47:17.221Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb", size = 1006223, upload-time = "2026-03-09T12:47:15.026Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "smart-open" +version = "7.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c5/65/3ada667d32675399001bf022ad3d9f3989b57101351ebc71d6fbe2384634/smart_open-7.6.1.tar.gz", hash = "sha256:4347996e7ba21db7cd1e059632e0b30395407e4f6c660d2ddffc8f2a9ae5f990", size = 54754, upload-time = "2026-05-09T06:23:37.06Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/78/0f68b93564b8c6b6987a0696c582ba2591a381ab2f733a501909e949f241/smart_open-7.6.1-py3-none-any.whl", hash = "sha256:b4de6aebef023aca91cc9fb372052e1343ba3f152de215bd22391a663e3ddd21", size = 64845, upload-time = "2026-05-09T06:23:35.386Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "spacy" +version = "3.8.14" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "catalogue" }, + { name = "confection" }, + { name = "cymem" }, + { name = "jinja2" }, + { name = "murmurhash" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "preshed" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "setuptools" }, + { name = "spacy-legacy" }, + { name = "spacy-loggers" }, + { name = "srsly" }, + { name = "thinc" }, + { name = "tqdm" }, + { name = "typer" }, + { name = "wasabi" }, + { name = "weasel" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/78/e4f2ae19a791cae756cd0e801204953eaec4e9ab75a60ad39f671dbb8d5a/spacy-3.8.14-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:726f02c60a2c6b0029167370d22d51731172a053d29c7e2ea6190db6de3ab483", size = 6218335, upload-time = "2026-03-29T10:40:46.298Z" }, + { url = "https://files.pythonhosted.org/packages/06/df/178bbab47fa209c8baf2f1e609cbddc6b18a985200be1ceee22bd5b89beb/spacy-3.8.14-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e3ebe50b93f2d40e8ec3451255528bb622ccb12be39fd140bb87668ce8d1075b", size = 6033860, upload-time = "2026-03-29T10:40:47.861Z" }, + { url = "https://files.pythonhosted.org/packages/ff/e8/048d83b73b28686307bd9a60878a58de7b7b21b562ca4de8b5bd558031e9/spacy-3.8.14-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:daeb64b048f12c059997281aed53eb8776d26416dd313cf17ad6f63124b2b564", size = 32725099, upload-time = "2026-03-29T10:40:50.194Z" }, + { url = "https://files.pythonhosted.org/packages/8e/3f/1799af5f4ccc8eb7500e4a20ca301488134429dba08cda5be68ce6ab2992/spacy-3.8.14-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6d45715a24446f23b98ec3f09409a1d4111983d1d64613250ee38c3270e21853", size = 33205838, upload-time = "2026-03-29T10:40:53.029Z" }, + { url = "https://files.pythonhosted.org/packages/78/07/81ab9acd0ec64bfdd7339acfc4cf35f5fb74bbbb0b2be7e64d717c416bac/spacy-3.8.14-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1069a8be34940809f8462eb69f09a3f0ce59bf8b9cb82475f2a8e3580f50ece0", size = 32090380, upload-time = "2026-03-29T10:40:56.115Z" }, + { url = "https://files.pythonhosted.org/packages/74/a5/b081b5bd3cedb2634c23eb470b5e24c65c894c57646567f47627291c2b3f/spacy-3.8.14-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2dfa77aec7fdebac0455d8afd4ce1d92d6f868b03d507ed1976179a63db7b374", size = 32991946, upload-time = "2026-03-29T10:40:58.852Z" }, + { url = "https://files.pythonhosted.org/packages/5f/55/4371413a6dfc1fa837282a365498165f828c2f3fe018dfb35336acc869e0/spacy-3.8.14-cp312-cp312-win_amd64.whl", hash = "sha256:9def18c76a4472b326cb91a195623c9ca38a2b86999ad2df9e00b49ba8c63734", size = 14226946, upload-time = "2026-03-29T10:41:01.63Z" }, + { url = "https://files.pythonhosted.org/packages/f3/5e/12ac876017da6c1e6b72afcc3c8b309996227fd3aa15382cd3311aee21b8/spacy-3.8.14-cp312-cp312-win_arm64.whl", hash = "sha256:d6257133357e4801c9c5d011925af5439b0a015aacf3c16528aa0009982431c7", size = 13628765, upload-time = "2026-03-29T10:41:03.806Z" }, + { url = "https://files.pythonhosted.org/packages/1b/e5/822bbdfa459fee863ef2e9879a34b0ae5db7cd1e3eb76d32c766f19222e9/spacy-3.8.14-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2b4f60fa8b9641a5e93e7a96db0cdd106d05d61756bf1d0ddcd1705ad347909a", size = 6202114, upload-time = "2026-03-29T10:41:06.119Z" }, + { url = "https://files.pythonhosted.org/packages/7e/de/0e512154113e1f341567f2b9341835775e4180c180221e60faedaebb2f65/spacy-3.8.14-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0860c57220c633ccb20468bcd64bfb0d28908990c371a8857951d093a148dc8e", size = 6015458, upload-time = "2026-03-29T10:41:07.79Z" }, + { url = "https://files.pythonhosted.org/packages/0c/4f/29c7e56afc7db07348a9e0efe0243b5eef465d5dc3d56433f164378c3fa6/spacy-3.8.14-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c24620b7dba879c69cebc51ef3b1107d4d4e44a1e0d4baa439372887d00c3fd9", size = 32510659, upload-time = "2026-03-29T10:41:09.88Z" }, + { url = "https://files.pythonhosted.org/packages/1e/ce/cae678f664d5467016819253f5d6e52f8e68a12d8e799b651d73ec2a9a4b/spacy-3.8.14-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9699c1248d115d5825987c287a6f6acd66386ef3ebee7994ee67ba093e932c59", size = 32841057, upload-time = "2026-03-29T10:41:12.585Z" }, + { url = "https://files.pythonhosted.org/packages/04/d4/419868afd449bdd367df005932537eea66c71e97c899ba278f3124933f3c/spacy-3.8.14-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:042d799e342fdb6bb5b02a4213a95acc9116c40ed3c849bb0a8296fbe648ec22", size = 31763252, upload-time = "2026-03-29T10:41:15.569Z" }, + { url = "https://files.pythonhosted.org/packages/ec/53/df5c1fee45f200b749ba72eeb536fbb2c545fc56230324954263b2f3be00/spacy-3.8.14-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69b2264294097336e86832e8663f1ab3a7215621184863c96c082ab17ee11937", size = 32717872, upload-time = "2026-03-29T10:41:18.193Z" }, + { url = "https://files.pythonhosted.org/packages/12/c2/f1882ec2f5cc9c4e73cf2132997a03c397d7ceeb5ee7f7bb878b51a16365/spacy-3.8.14-cp313-cp313-win_amd64.whl", hash = "sha256:4b6d4f20e291a7c70e37de2f246622b44a0ce82efaa710c9801c6bd599e75177", size = 14220335, upload-time = "2026-03-29T10:41:20.89Z" }, +] + +[[package]] +name = "spacy-legacy" +version = "3.0.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d9/79/91f9d7cc8db5642acad830dcc4b49ba65a7790152832c4eceb305e46d681/spacy-legacy-3.0.12.tar.gz", hash = "sha256:b37d6e0c9b6e1d7ca1cf5bc7152ab64a4c4671f59c85adaf7a3fcb870357a774", size = 23806, upload-time = "2023-01-23T09:04:15.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/55/12e842c70ff8828e34e543a2c7176dac4da006ca6901c9e8b43efab8bc6b/spacy_legacy-3.0.12-py2.py3-none-any.whl", hash = "sha256:476e3bd0d05f8c339ed60f40986c07387c0a71479245d6d0f4298dbd52cda55f", size = 29971, upload-time = "2023-01-23T09:04:13.45Z" }, +] + +[[package]] +name = "spacy-loggers" +version = "1.0.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/67/3d/926db774c9c98acf66cb4ed7faf6c377746f3e00b84b700d0868b95d0712/spacy-loggers-1.0.5.tar.gz", hash = "sha256:d60b0bdbf915a60e516cc2e653baeff946f0cfc461b452d11a4d5458c6fe5f24", size = 20811, upload-time = "2023-09-11T12:26:52.323Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/78/d1a1a026ef3af911159398c939b1509d5c36fe524c7b644f34a5146c4e16/spacy_loggers-1.0.5-py3-none-any.whl", hash = "sha256:196284c9c446cc0cdb944005384270d775fdeaf4f494d8e269466cfa497ef645", size = 22343, upload-time = "2023-09-11T12:26:50.586Z" }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.47" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/4b/1e00561093fe2cd8eef09d406da003c8a118ff02d6548498c1ae677d68d9/sqlalchemy-2.0.47.tar.gz", hash = "sha256:e3e7feb57b267fe897e492b9721ae46d5c7de6f9e8dee58aacf105dc4e154f3d", size = 9886323, upload-time = "2026-02-24T16:34:27.947Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/88/74eb470223ff88ea6572a132c0b8de8c1d8ed7b843d3b44a8a3c77f31d39/sqlalchemy-2.0.47-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4fa91b19d6b9821c04cc8f7aa2476429cc8887b9687c762815aa629f5c0edec1", size = 2155687, upload-time = "2026-02-24T17:05:46.451Z" }, + { url = "https://files.pythonhosted.org/packages/ef/ba/1447d3d558971b036cb93b557595cb5dcdfe728f1c7ac4dec16505ef5756/sqlalchemy-2.0.47-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c5bbbd14eff577c8c79cbfe39a0771eecd20f430f3678533476f0087138f356", size = 3336978, upload-time = "2026-02-24T17:18:04.597Z" }, + { url = "https://files.pythonhosted.org/packages/8a/07/b47472d2ffd0776826f17ccf0b4d01b224c99fbd1904aeb103dffbb4b1cc/sqlalchemy-2.0.47-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5a6c555da8d4280a3c4c78c5b7a3f990cee2b2884e5f934f87a226191682ff7", size = 3349939, upload-time = "2026-02-24T17:27:18.937Z" }, + { url = "https://files.pythonhosted.org/packages/bb/c6/95fa32b79b57769da3e16f054cf658d90940317b5ca0ec20eac84aa19c4f/sqlalchemy-2.0.47-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ed48a1701d24dff3bb49a5bce94d6bc84cbe33d98af2aa2d3cdcce3dea1709ec", size = 3279648, upload-time = "2026-02-24T17:18:07.038Z" }, + { url = "https://files.pythonhosted.org/packages/bb/c8/3d07e7c73928dc59a0bed40961ca4e313e797bce650b088e8d5fdd3ad939/sqlalchemy-2.0.47-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4f3178c920ad98158f0b6309382194df04b14808fa6052ae07099fdde29d5602", size = 3314695, upload-time = "2026-02-24T17:27:20.93Z" }, + { url = "https://files.pythonhosted.org/packages/6b/d2/ed32b1611c1e19fdb028eee1adc5a9aa138c2952d09ae11f1670170f80ae/sqlalchemy-2.0.47-cp312-cp312-win32.whl", hash = "sha256:b9c11ac9934dd59ece9619fe42780a08abe2faab7b0543bb00d5eabea4f421b9", size = 2115502, upload-time = "2026-02-24T17:22:52.546Z" }, + { url = "https://files.pythonhosted.org/packages/fd/52/9de590356a4dd8e9ef5a881dbba64b2bbc4cbc71bf02bc68e775fb9b1899/sqlalchemy-2.0.47-cp312-cp312-win_amd64.whl", hash = "sha256:db43b72cf8274a99e089755c9c1e0b947159b71adbc2c83c3de2e38d5d607acb", size = 2142435, upload-time = "2026-02-24T17:22:54.268Z" }, + { url = "https://files.pythonhosted.org/packages/4a/e5/0af64ce7d8f60ec5328c10084e2f449e7912a9b8bdbefdcfb44454a25f49/sqlalchemy-2.0.47-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:456a135b790da5d3c6b53d0ef71ac7b7d280b7f41eb0c438986352bf03ca7143", size = 2152551, upload-time = "2026-02-24T17:05:47.675Z" }, + { url = "https://files.pythonhosted.org/packages/63/79/746b8d15f6940e2ac469ce22d7aa5b1124b1ab820bad9b046eb3000c88a6/sqlalchemy-2.0.47-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09a2f7698e44b3135433387da5d8846cf7cc7c10e5425af7c05fee609df978b6", size = 3278782, upload-time = "2026-02-24T17:18:10.012Z" }, + { url = "https://files.pythonhosted.org/packages/91/b1/bd793ddb34345d1ed43b13ab2d88c95d7d4eb2e28f5b5a99128b9cc2bca2/sqlalchemy-2.0.47-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0bbc72e6a177c78d724f9106aaddc0d26a2ada89c6332b5935414eccf04cbd5", size = 3295155, upload-time = "2026-02-24T17:27:22.827Z" }, + { url = "https://files.pythonhosted.org/packages/97/84/7213def33f94e5ca6f5718d259bc9f29de0363134648425aa218d4356b23/sqlalchemy-2.0.47-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:75460456b043b78b6006e41bdf5b86747ee42eafaf7fffa3b24a6e9a456a2092", size = 3226834, upload-time = "2026-02-24T17:18:11.465Z" }, + { url = "https://files.pythonhosted.org/packages/ef/06/456810204f4dc29b5f025b1b0a03b4bd6b600ebf3c1040aebd90a257fa33/sqlalchemy-2.0.47-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5d9adaa616c3bc7d80f9ded57cd84b51d6617cad6a5456621d858c9f23aaee01", size = 3265001, upload-time = "2026-02-24T17:27:24.813Z" }, + { url = "https://files.pythonhosted.org/packages/fb/20/df3920a4b2217dbd7390a5bd277c1902e0393f42baaf49f49b3c935e7328/sqlalchemy-2.0.47-cp313-cp313-win32.whl", hash = "sha256:76e09f974382a496a5ed985db9343628b1cb1ac911f27342e4cc46a8bac10476", size = 2113647, upload-time = "2026-02-24T17:22:55.747Z" }, + { url = "https://files.pythonhosted.org/packages/46/06/7873ddf69918efbfabd7211829f4bd8019739d0a719253112d305d3ba51d/sqlalchemy-2.0.47-cp313-cp313-win_amd64.whl", hash = "sha256:0664089b0bf6724a0bfb49a0cf4d4da24868a0a5c8e937cd7db356d5dcdf2c66", size = 2139425, upload-time = "2026-02-24T17:22:57.033Z" }, + { url = "https://files.pythonhosted.org/packages/54/fa/61ad9731370c90ac7ea5bf8f5eaa12c48bb4beec41c0fa0360becf4ac10d/sqlalchemy-2.0.47-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ed0c967c701ae13da98eb220f9ddab3044ab63504c1ba24ad6a59b26826ad003", size = 3558809, upload-time = "2026-02-24T17:12:15.232Z" }, + { url = "https://files.pythonhosted.org/packages/33/d5/221fac96f0529391fe374875633804c866f2b21a9c6d3a6ca57d9c12cfd7/sqlalchemy-2.0.47-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3537943a61fd25b241e976426a0c6814434b93cf9b09d39e8e78f3c9eb9a487", size = 3525480, upload-time = "2026-02-24T17:27:59.602Z" }, + { url = "https://files.pythonhosted.org/packages/ec/55/8247d53998c3673e4a8d1958eba75c6f5cc3b39082029d400bb1f2a911ae/sqlalchemy-2.0.47-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:57f7e336a64a0dba686c66392d46b9bc7af2c57d55ce6dc1697b4ef32b043ceb", size = 3466569, upload-time = "2026-02-24T17:12:16.94Z" }, + { url = "https://files.pythonhosted.org/packages/6b/b5/c1f0eea1bac6790845f71420a7fe2f2a0566203aa57543117d4af3b77d1c/sqlalchemy-2.0.47-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dff735a621858680217cb5142b779bad40ef7322ddbb7c12062190db6879772e", size = 3475770, upload-time = "2026-02-24T17:28:02.034Z" }, + { url = "https://files.pythonhosted.org/packages/c5/ed/2f43f92474ea0c43c204657dc47d9d002cd738b96ca2af8e6d29a9b5e42d/sqlalchemy-2.0.47-cp313-cp313t-win32.whl", hash = "sha256:3893dc096bb3cca9608ea3487372ffcea3ae9b162f40e4d3c51dd49db1d1b2dc", size = 2141300, upload-time = "2026-02-24T17:14:37.024Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a9/8b73f9f1695b6e92f7aaf1711135a1e3bbeb78bca9eded35cb79180d3c6d/sqlalchemy-2.0.47-cp313-cp313t-win_amd64.whl", hash = "sha256:b5103427466f4b3e61f04833ae01f9a914b1280a2a8bcde3a9d7ab11f3755b42", size = 2173053, upload-time = "2026-02-24T17:14:38.688Z" }, + { url = "https://files.pythonhosted.org/packages/c1/30/98243209aae58ed80e090ea988d5182244ca7ab3ff59e6d850c3dfc7651e/sqlalchemy-2.0.47-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b03010a5a5dfe71676bc83f2473ebe082478e32d77e6f082c8fe15a31c3b42a6", size = 2154355, upload-time = "2026-02-24T17:05:48.959Z" }, + { url = "https://files.pythonhosted.org/packages/ab/62/12ca6ea92055fe486d6558a2a4efe93e194ff597463849c01f88e5adb99d/sqlalchemy-2.0.47-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f8e3371aa9024520883a415a09cc20c33cfd3eeccf9e0f4f4c367f940b9cbd44", size = 3274486, upload-time = "2026-02-24T17:18:13.659Z" }, + { url = "https://files.pythonhosted.org/packages/97/88/7dfbdeaa8d42b1584e65d6cc713e9d33b6fa563e0d546d5cb87e545bb0e5/sqlalchemy-2.0.47-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9449f747e50d518c6e1b40cc379e48bfc796453c47b15e627ea901c201e48a6", size = 3279481, upload-time = "2026-02-24T17:27:26.491Z" }, + { url = "https://files.pythonhosted.org/packages/d0/b7/75e1c1970616a9dd64a8a6fd788248da2ddaf81c95f4875f2a1e8aee4128/sqlalchemy-2.0.47-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:21410f60d5cac1d6bfe360e05bd91b179be4fa0aa6eea6be46054971d277608f", size = 3224269, upload-time = "2026-02-24T17:18:15.078Z" }, + { url = "https://files.pythonhosted.org/packages/31/ac/eec1a13b891df9a8bc203334caf6e6aac60b02f61b018ef3b4124b8c4120/sqlalchemy-2.0.47-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:819841dd5bb4324c284c09e2874cf96fe6338bfb57a64548d9b81a4e39c9871f", size = 3246262, upload-time = "2026-02-24T17:27:27.986Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b0/661b0245b06421058610da39f8ceb34abcc90b49f90f256380968d761dbe/sqlalchemy-2.0.47-cp314-cp314-win32.whl", hash = "sha256:e255ee44821a7ef45649c43064cf94e74f81f61b4df70547304b97a351e9b7db", size = 2116528, upload-time = "2026-02-24T17:22:59.363Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ef/1035a90d899e61810791c052004958be622a2cf3eb3df71c3fe20778c5d0/sqlalchemy-2.0.47-cp314-cp314-win_amd64.whl", hash = "sha256:209467ff73ea1518fe1a5aaed9ba75bb9e33b2666e2553af9ccd13387bf192cb", size = 2142181, upload-time = "2026-02-24T17:23:01.001Z" }, + { url = "https://files.pythonhosted.org/packages/76/bb/17a1dd09cbba91258218ceb582225f14b5364d2683f9f5a274f72f2d764f/sqlalchemy-2.0.47-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e78fd9186946afaa287f8a1fe147ead06e5d566b08c0afcb601226e9c7322a64", size = 3563477, upload-time = "2026-02-24T17:12:18.46Z" }, + { url = "https://files.pythonhosted.org/packages/66/8f/1a03d24c40cc321ef2f2231f05420d140bb06a84f7047eaa7eaa21d230ba/sqlalchemy-2.0.47-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5740e2f31b5987ed9619d6912ae5b750c03637f2078850da3002934c9532f172", size = 3528568, upload-time = "2026-02-24T17:28:03.732Z" }, + { url = "https://files.pythonhosted.org/packages/fd/53/d56a213055d6b038a5384f0db5ece7343334aca230ff3f0fa1561106f22c/sqlalchemy-2.0.47-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb9ac00d03de93acb210e8ec7243fefe3e012515bf5fd2f0898c8dff38bc77a4", size = 3472284, upload-time = "2026-02-24T17:12:20.319Z" }, + { url = "https://files.pythonhosted.org/packages/ff/19/c235d81b9cfdd6130bf63143b7bade0dc4afa46c4b634d5d6b2a96bea233/sqlalchemy-2.0.47-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c72a0b9eb2672d70d112cb149fbaf172d466bc691014c496aaac594f1988e706", size = 3478410, upload-time = "2026-02-24T17:28:05.892Z" }, + { url = "https://files.pythonhosted.org/packages/0e/db/cafdeca5ecdaa3bb0811ba5449501da677ce0d83be8d05c5822da72d2e86/sqlalchemy-2.0.47-cp314-cp314t-win32.whl", hash = "sha256:c200db1128d72a71dc3c31c24b42eb9fd85b2b3e5a3c9ba1e751c11ac31250ff", size = 2147164, upload-time = "2026-02-24T17:14:40.783Z" }, + { url = "https://files.pythonhosted.org/packages/fc/5e/ff41a010e9e0f76418b02ad352060a4341bb15f0af66cedc924ab376c7c6/sqlalchemy-2.0.47-cp314-cp314t-win_amd64.whl", hash = "sha256:669837759b84e575407355dcff912835892058aea9b80bd1cb76d6a151cf37f7", size = 2182154, upload-time = "2026-02-24T17:14:43.205Z" }, + { url = "https://files.pythonhosted.org/packages/15/9f/7c378406b592fcf1fc157248607b495a40e3202ba4a6f1372a2ba6447717/sqlalchemy-2.0.47-py3-none-any.whl", hash = "sha256:e2647043599297a1ef10e720cf310846b7f31b6c841fee093d2b09d81215eb93", size = 1940159, upload-time = "2026-02-24T17:15:07.158Z" }, +] + +[[package]] +name = "sqlite-vec" +version = "0.1.6" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/ed/aabc328f29ee6814033d008ec43e44f2c595447d9cccd5f2aabe60df2933/sqlite_vec-0.1.6-py3-none-macosx_10_6_x86_64.whl", hash = "sha256:77491bcaa6d496f2acb5cc0d0ff0b8964434f141523c121e313f9a7d8088dee3", size = 164075, upload-time = "2024-11-20T16:40:29.847Z" }, + { url = "https://files.pythonhosted.org/packages/a7/57/05604e509a129b22e303758bfa062c19afb020557d5e19b008c64016704e/sqlite_vec-0.1.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fdca35f7ee3243668a055255d4dee4dea7eed5a06da8cad409f89facf4595361", size = 165242, upload-time = "2024-11-20T16:40:31.206Z" }, + { url = "https://files.pythonhosted.org/packages/f2/48/dbb2cc4e5bad88c89c7bb296e2d0a8df58aab9edc75853728c361eefc24f/sqlite_vec-0.1.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b0519d9cd96164cd2e08e8eed225197f9cd2f0be82cb04567692a0a4be02da3", size = 103704, upload-time = "2024-11-20T16:40:33.729Z" }, + { url = "https://files.pythonhosted.org/packages/80/76/97f33b1a2446f6ae55e59b33869bed4eafaf59b7f4c662c8d9491b6a714a/sqlite_vec-0.1.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux1_x86_64.whl", hash = "sha256:823b0493add80d7fe82ab0fe25df7c0703f4752941aee1c7b2b02cec9656cb24", size = 151556, upload-time = "2024-11-20T16:40:35.387Z" }, + { url = "https://files.pythonhosted.org/packages/6a/98/e8bc58b178266eae2fcf4c9c7a8303a8d41164d781b32d71097924a6bebe/sqlite_vec-0.1.6-py3-none-win_amd64.whl", hash = "sha256:c65bcfd90fa2f41f9000052bcb8bb75d38240b2dae49225389eca6c3136d3f0c", size = 281540, upload-time = "2024-11-20T16:40:37.296Z" }, +] + +[[package]] +name = "srsly" +version = "2.5.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "catalogue" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2b/db/f794f219a6c788b881252d2536a8c4a97d2bdaadc690391e1cb53d123d71/srsly-2.5.3.tar.gz", hash = "sha256:08f98dbecbff3a31466c4ae7c833131f59d3655a0ad8ac749e6e2c149e2b0680", size = 490881, upload-time = "2026-03-23T11:56:59.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/cc/e9f7fcec4cc92ad8bad6316c4241638b8cf7380382d4489d94ec6c436452/srsly-2.5.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:71e51c046ccbeefb86524c6b1e17574f579c6ac4dc8ea4a09437d3e8f88342d3", size = 658379, upload-time = "2026-03-23T11:55:59.85Z" }, + { url = "https://files.pythonhosted.org/packages/21/e4/fea4512e9785f58509b2cf67d993323848e583161b5fcfdc7dd9d7c1f3df/srsly-2.5.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f73c0db911552e94fe2016e1759d261d2f47926f68826664cada3723c87006a", size = 658513, upload-time = "2026-03-23T11:56:01.239Z" }, + { url = "https://files.pythonhosted.org/packages/20/b1/53591681b6ff2699a4f97b2d5552ba196eaa6a979b0873605f4c04b5f7ee/srsly-2.5.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c1ac27ae5f4bb9163c7d2c45fc8ec173aac3d92e32086d9472b326c5c6e570e", size = 1172265, upload-time = "2026-03-23T11:56:02.589Z" }, + { url = "https://files.pythonhosted.org/packages/4e/c9/741e29f534919a944a16da4184924b1d3404c4bf60716ab2b91be771d1e3/srsly-2.5.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:99026bcd9cbd3211cc36517400b04ca0fc5d3e412b14daf84ee6e65f67d9a2d8", size = 1180873, upload-time = "2026-03-23T11:56:03.944Z" }, + { url = "https://files.pythonhosted.org/packages/89/57/5554f786eccf78b2750d6ac63be126e1b67badec2cb409dd611cf6f8c52b/srsly-2.5.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:07d682679e639eb46ff7e6da4a92714f4d5ffe351d088ee66f221e9b1f8865bb", size = 1120437, upload-time = "2026-03-23T11:56:05.283Z" }, + { url = "https://files.pythonhosted.org/packages/eb/95/9b4f73b1be3692f86d72ccc131c8e50f26f824d5c8830a59390bcc5b60ef/srsly-2.5.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8e0542d85d6b55cf2934050d6ffcb1cd76c768dcf9572e7467002cf087bb366d", size = 1137376, upload-time = "2026-03-23T11:56:06.613Z" }, + { url = "https://files.pythonhosted.org/packages/5a/de/89ca640ca1953c4612279ce515d0af35658df3c06cdb324329bc91b4a7e1/srsly-2.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:598f1e494c18cacb978299d77125415a586417081959f8ec3f068b32d97f8933", size = 652459, upload-time = "2026-03-23T11:56:07.994Z" }, + { url = "https://files.pythonhosted.org/packages/6d/4f/7ab6d49e36d9cc72ee15746cabd116eb6f338be8a06c1882968ee9d6c7d7/srsly-2.5.3-cp312-cp312-win_arm64.whl", hash = "sha256:4b1b721cd3ad1a9b2343519aadc786a4d09d5c0666962d49852eb12d6ec3fe26", size = 638411, upload-time = "2026-03-23T11:56:09.31Z" }, + { url = "https://files.pythonhosted.org/packages/9d/5c/12901e3794f4158abc6da750725aad6c2afddb1e4227b300fe7c71f66957/srsly-2.5.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e67b6bbacbfadea5e100266d2797f2d4cec9883ea4dc84a5537673850036a8d8", size = 656750, upload-time = "2026-03-23T11:56:10.708Z" }, + { url = "https://files.pythonhosted.org/packages/04/61/181c26370995f96f56f1b64b801e3ca1e0d703fc36506ae28606d62369fb/srsly-2.5.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:348c231b4477d8fe86603131d0f166d2feac9c372704dfc4398be71cc5b6fb07", size = 656746, upload-time = "2026-03-23T11:56:12.28Z" }, + { url = "https://files.pythonhosted.org/packages/77/c6/35876c78889f8ffe11ed3521644e666c3aef20ea31527b70f47456cf35c2/srsly-2.5.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b0938c2978c91ae1ef9c1f2ba35abb86330e198fb23469e356eba311e02233ee", size = 1155762, upload-time = "2026-03-23T11:56:14.075Z" }, + { url = "https://files.pythonhosted.org/packages/3e/da/40b71ca9906c8eb8f8feb6ac11d33dad458c85a56e1de764b96d402168a0/srsly-2.5.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5f6a837954429ecbe6dcdd27390d2fb4c7d01a3f99c9ffcf9ce66b2a6dd1b738", size = 1161092, upload-time = "2026-03-23T11:56:15.778Z" }, + { url = "https://files.pythonhosted.org/packages/dc/14/c0dd30cc8b93ce8137ff4766f743c882440ce49195fffc5d50eaeef311a6/srsly-2.5.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3576c125c486ce2958c2047e8858fe3cfc9ea877adfa05203b0986f9badee355", size = 1109984, upload-time = "2026-03-23T11:56:17.056Z" }, + { url = "https://files.pythonhosted.org/packages/08/f3/34354f183d8faafc631585571224b54d1b4b67e796972c36519c074ca355/srsly-2.5.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5fb59c42922e095d1ea36085c55bc16e2adb06a7bfe57b24d381e0194ae699f2", size = 1128409, upload-time = "2026-03-23T11:56:18.761Z" }, + { url = "https://files.pythonhosted.org/packages/a4/d9/5531f8a19492060b4e76e4ab06aca6f096fb5128fe18cc813d1772daf653/srsly-2.5.3-cp313-cp313-win_amd64.whl", hash = "sha256:111805927f05f5db440aeeacb85ce43da0b19ce7b2a09567a9ef8d30f3cc4d83", size = 650820, upload-time = "2026-03-23T11:56:20.096Z" }, + { url = "https://files.pythonhosted.org/packages/8e/8a/62fb7a971eca29e12f03fb9ddacb058548c14d33e5b5675ff0f85839cc7b/srsly-2.5.3-cp313-cp313-win_arm64.whl", hash = "sha256:0f106b0a700ab56e4a7c431b0f1444009ab6cb332edc7bbf6811c2a43f4722cb", size = 637278, upload-time = "2026-03-23T11:56:21.439Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5b/e4ef43c2a381711230af98d4c94a5323df48d6a7899ee652e05bf889290e/srsly-2.5.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:39c13d552a9f9674a12cdcdc66b0c2f02f3430d0cd04c5f9cf598824c2bd3d65", size = 661294, upload-time = "2026-03-23T11:56:23.29Z" }, + { url = "https://files.pythonhosted.org/packages/92/2d/ebce7f3717e52cd0a01f4ec570f388f3b7098526794fcf1ad734e0b8f852/srsly-2.5.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:14c930767cc169611a2dc14e23bc7638cfb616d6f79029700ade033607343540", size = 660952, upload-time = "2026-03-23T11:56:24.908Z" }, + { url = "https://files.pythonhosted.org/packages/22/47/a8f3e9b214be2624c8e8a78d38ca7b1d4e26b92d57018412e4bfc4abe89a/srsly-2.5.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2f2d464f0d0237e32fb53f0ec6f05418652c550e772b50e9918e83a1577cba4d", size = 1154554, upload-time = "2026-03-23T11:56:26.608Z" }, + { url = "https://files.pythonhosted.org/packages/d6/71/2a89dc3180a51e633a87a079ca064225f4aaf46c7b2a5fc720e28f261d98/srsly-2.5.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d18933248a5bb0ad56a1bae6003a9a7f37daac2ecb0c5bcbfaaf081b317e1c84", size = 1155746, upload-time = "2026-03-23T11:56:28.102Z" }, + { url = "https://files.pythonhosted.org/packages/b8/36/72e5ce3153927ca404b6f5bf5280e6ff3399c11557df472b153945468e0a/srsly-2.5.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7ea5412ea229e571ac9738cbe14f845cc06c8e4e956afb5f42061ccd087ef31f", size = 1112374, upload-time = "2026-03-23T11:56:29.591Z" }, + { url = "https://files.pythonhosted.org/packages/04/b2/0895de109c28eca0d41a811ab7c076d4e4a505e8466f06bae22f5180a1dd/srsly-2.5.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8d3988970b4cf7d03bdd5b5169302ff84562dd2e1e0f84aeb34df3e5b5dc19bf", size = 1127732, upload-time = "2026-03-23T11:56:31.458Z" }, + { url = "https://files.pythonhosted.org/packages/c7/79/a37fa7759797fbdfe0a2e029ab13e78b1e81e191220d2bb8ff57d869aefb/srsly-2.5.3-cp314-cp314-win_amd64.whl", hash = "sha256:6a02d7dcc16126c8fae1c1c09b2072798a1dc482ab5f9c52b12c7114dac47325", size = 656467, upload-time = "2026-03-23T11:56:33.14Z" }, + { url = "https://files.pythonhosted.org/packages/d7/25/0dae019b3b90ad9037f91de4c390555cdaac9460a93ad62b02b03babdff5/srsly-2.5.3-cp314-cp314-win_arm64.whl", hash = "sha256:1c9129c4abe31903ff7996904a51afdd5428060de6c3d12af49a4da5e8df2821", size = 643040, upload-time = "2026-03-23T11:56:34.448Z" }, + { url = "https://files.pythonhosted.org/packages/3a/44/72dd5285b2e05435d98b0797f101d91d9b345d491ddc1fdb9bd09e27ccb8/srsly-2.5.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:29d5d01ba4c2e9c01f936e5e6d5babc4a47b38c9cbd6e1ec23f6d5a49df32605", size = 666200, upload-time = "2026-03-23T11:56:35.753Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ad/002c71b87fc3f648c9bf0ec47de0c3822bf2c95c8896a589dd03e7fd3977/srsly-2.5.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5c8df4039426d99f0148b5743542842ab96b82daded0b342555e15a639927757", size = 667409, upload-time = "2026-03-23T11:56:37.172Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/2cea3d5e80aeecfc4ece9e7e1783e7792cc3bad7ab85ab585882e1db4e38/srsly-2.5.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:06a43d63bde2e8cccadb953d7fff70b18196ca286b65dd2ad16006d65f3f8166", size = 1265941, upload-time = "2026-03-23T11:56:38.825Z" }, + { url = "https://files.pythonhosted.org/packages/aa/38/8a4d7e86dd0370a2e5af251b646000197bb5b7e0f9aa360c71bbfb253d0d/srsly-2.5.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:808cfafc047f0dec507a34c8fa8e4cda5722737fd33577df73452f52f7aca644", size = 1250693, upload-time = "2026-03-23T11:56:40.449Z" }, + { url = "https://files.pythonhosted.org/packages/99/05/340129de5ea7b237271b12f8a6962cfa7eb0c5a3056794626d348c5ae7c7/srsly-2.5.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:71d4cbe2b2a1335c76ed0acae2dc862163787d8b01a705e1949796907ed94ccd", size = 1242408, upload-time = "2026-03-23T11:56:41.8Z" }, + { url = "https://files.pythonhosted.org/packages/01/cb/d7fee7ab27c6aa2e3f865fb7b50ba18c81a4c763bba12bdf53df246441bc/srsly-2.5.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:565f69083d33cb329cfc74317da937fb3270c0f40fabc1b4488702d8074b4a3e", size = 1242749, upload-time = "2026-03-23T11:56:43.246Z" }, + { url = "https://files.pythonhosted.org/packages/d8/d1/9bad3a0f2fa7b72f4e0cf1d267b00513092d20ef538c47f72823ae4f7656/srsly-2.5.3-cp314-cp314t-win_amd64.whl", hash = "sha256:8ac016ffaeac35bc010992b71bf8afdd39d458f201c8138d84cf78778a936e6c", size = 673783, upload-time = "2026-03-23T11:56:44.875Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ae/57d1d7af907e20c077e113e0e4976f87b82c0a415403d99284a262229dd0/srsly-2.5.3-cp314-cp314t-win_arm64.whl", hash = "sha256:d822083fe26ec6728bd8c273ac121fc4ab3864a0fdf0cf0ff3efb188fcd209ed", size = 650229, upload-time = "2026-03-23T11:56:46.148Z" }, +] + +[[package]] +name = "sse-starlette" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "starlette" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/8d/00d280c03ffd39aaee0e86ec81e2d3b9253036a0f93f51d10503adef0e65/sse_starlette-3.2.0.tar.gz", hash = "sha256:8127594edfb51abe44eac9c49e59b0b01f1039d0c7461c6fd91d4e03b70da422", size = 27253, upload-time = "2026-01-17T13:11:05.62Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/7f/832f015020844a8b8f7a9cbc103dd76ba8e3875004c41e08440ea3a2b41a/sse_starlette-3.2.0-py3-none-any.whl", hash = "sha256:5876954bd51920fc2cd51baee47a080eb88a37b5b784e615abb0b283f801cdbf", size = 12763, upload-time = "2026-01-17T13:11:03.775Z" }, +] + +[[package]] +name = "starlette" +version = "0.52.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/68/79977123bb7be889ad680d79a40f339082c1978b5cfcf62c2d8d196873ac/starlette-0.52.1.tar.gz", hash = "sha256:834edd1b0a23167694292e94f597773bc3f89f362be6effee198165a35d62933", size = 2653702, upload-time = "2026-01-18T13:34:11.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272, upload-time = "2026-01-18T13:34:09.188Z" }, +] + +[[package]] +name = "sympy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, +] + +[[package]] +name = "thinc" +version = "8.3.13" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "blis" }, + { name = "catalogue" }, + { name = "confection" }, + { name = "cymem" }, + { name = "murmurhash" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "preshed" }, + { name = "pydantic" }, + { name = "setuptools" }, + { name = "srsly" }, + { name = "wasabi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/13/46/76df95f2c327f9a9cef30c1523bf285627897097163584dcf5f77b2ebce2/thinc-8.3.13.tar.gz", hash = "sha256:68e658549fc1eb3ff92aed5147fcbb9c15d6e9cc0e623b4d0998d16522ffb4f9", size = 194640, upload-time = "2026-03-23T07:22:36.41Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/af/f7c1ebfe92eb5d27d7f2f3da67a11e2eb57bc30ab1553279af6dc65b65a8/thinc-8.3.13-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:77a41f66285321d20aaedaea1e87d7cd48dca6d2427bed1867ec7cba7109fc8d", size = 821097, upload-time = "2026-03-23T07:21:56.698Z" }, + { url = "https://files.pythonhosted.org/packages/45/8f/69d7338575d98df85d0b54c0f5fc277dba72587fe9ab846ecdd12a998bcb/thinc-8.3.13-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3710d318b4e5460cf366a6f7b5ddbefb5d39dbd4cfa408222750fdc6c27c4411", size = 791932, upload-time = "2026-03-23T07:21:58.38Z" }, + { url = "https://files.pythonhosted.org/packages/4b/a5/21d010c81e81e1589e5ccb4950e521804d13726e541e87f644c51815673b/thinc-8.3.13-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5a08c87143a6d20177652dca1ec0dc815d88216d8fc62594a57e8bc45bf5ed49", size = 3854219, upload-time = "2026-03-23T07:21:59.819Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ff/6914bf370bd1d604d89e6dfb46b97d10cd9b00d42ff8c036283e92314a8c/thinc-8.3.13-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4b5ec9ff313819e7d8667794a3559463fa89ff45aaa73e3fd8d6273b1e0d7a7f", size = 3903307, upload-time = "2026-03-23T07:22:01.652Z" }, + { url = "https://files.pythonhosted.org/packages/f3/3d/5572b47fa155fb3388c071515b74024fa17a6efd1df9406da378f0aa84ef/thinc-8.3.13-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5c9a48f2bc1e04f138240ed5f9b815a9141a5de26accd0f08fa0137fcefed258", size = 4836882, upload-time = "2026-03-23T07:22:03.565Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f0/a8d77c7bac089697c6df302cc3c936a1ab36a4720deae889e6f1dbcbd0eb/thinc-8.3.13-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:79a29a44d76bd02f5ac0624268c6e42b3576ae472c791a8ae9c2d813ae789b59", size = 5033398, upload-time = "2026-03-23T07:22:05.045Z" }, + { url = "https://files.pythonhosted.org/packages/21/82/5651bb1f904d04220fc7670035ada921bf0638e2cff6444d67c12887a968/thinc-8.3.13-cp312-cp312-win_amd64.whl", hash = "sha256:ed1dc709ac4f2f03b710457889e4e02f05de51bc8456980c241d0b28798bc7cb", size = 1721248, upload-time = "2026-03-23T07:22:06.749Z" }, + { url = "https://files.pythonhosted.org/packages/94/8d/683703de021ffbe46833d722b70f49ffbbca8e5bd6876256977555d92d7d/thinc-8.3.13-cp312-cp312-win_arm64.whl", hash = "sha256:c6a049703a6011c8fe26ee41af7e70272145594140d82f79bb23de619c6a6525", size = 1645777, upload-time = "2026-03-23T07:22:08.104Z" }, + { url = "https://files.pythonhosted.org/packages/af/b9/7b46942176df459d1804a9e77b0976f7c56f3abf3ec7485d0e5f836a0382/thinc-8.3.13-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c2811dfd8d46d8b5d3b39051b23e64006b2994a5143b1978b436938018792af8", size = 817337, upload-time = "2026-03-23T07:22:09.538Z" }, + { url = "https://files.pythonhosted.org/packages/a7/79/53085a72cd8f4fc4e6e313d05ea5aa98e870684f4a0fb318a9875fc0a964/thinc-8.3.13-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5593e6300cb1ebe0c0e546e9c9fb49e7c2627a0aa688795cd4f995a8b820d2ec", size = 788120, upload-time = "2026-03-23T07:22:11.215Z" }, + { url = "https://files.pythonhosted.org/packages/9e/3e/d61b462b16da95ac6885f95bb395e672040ee594833e571a6edcffd234f5/thinc-8.3.13-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f697174d3fb474966ce50b430bbafa101a6d2f7ffb559dac4b5c59389ef72d22", size = 3844666, upload-time = "2026-03-23T07:22:12.67Z" }, + { url = "https://files.pythonhosted.org/packages/78/4c/898cc654bb123734c71ec5a425c02ca34439517d01ce1c95a6563295580e/thinc-8.3.13-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e9c7c5c104737b414c8c4ec578e67d78b6c859afe25cbc0684402e721415bd7f", size = 3890658, upload-time = "2026-03-23T07:22:14.668Z" }, + { url = "https://files.pythonhosted.org/packages/cd/56/1abdbf0a4ad628e8a05d6516fe0745969649d805367a3dccad8ee872981b/thinc-8.3.13-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7a99d0e242d1ccd23f9ae6bea7cd502f8626efa65c156b91d84581d0356696c3", size = 4819933, upload-time = "2026-03-23T07:22:16.85Z" }, + { url = "https://files.pythonhosted.org/packages/f1/22/b84dbdc6be5055bbdb2a7352e2c393f67e8593c137f1b83c82bf1e062b6e/thinc-8.3.13-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e676edd21a747afbe3e6b9f3fca8b962e36d146ded03b070cb0c28e2dfbe9499", size = 5018099, upload-time = "2026-03-23T07:22:18.356Z" }, + { url = "https://files.pythonhosted.org/packages/0f/a8/763cd7ba949334c9d2cddc92dadb68b344cb9546dc01b8d4a733dcaa16c1/thinc-8.3.13-cp313-cp313-win_amd64.whl", hash = "sha256:8ad40307f20e83f77af28ff5c6be0b86af7a8b251d1231c545508d2763157d8f", size = 1720309, upload-time = "2026-03-23T07:22:19.81Z" }, + { url = "https://files.pythonhosted.org/packages/f5/15/a11f7bb3cbc97dfecf32a90552f5a8f8a5c99316a99c6c17bdabf5baf256/thinc-8.3.13-cp313-cp313-win_arm64.whl", hash = "sha256:723949cab11d1925c15447928513a718276316cec6e0de28337cca0a62be0521", size = 1644606, upload-time = "2026-03-23T07:22:21.339Z" }, + { url = "https://files.pythonhosted.org/packages/80/40/f4937d113912c6d669ffe982356ab29dcb6c7fe3be926a15981dbbb6a91c/thinc-8.3.13-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7badb0be4825535e6362c19e8a41872b65409e9da46d3453a391b843a0720865", size = 817024, upload-time = "2026-03-23T07:22:23.005Z" }, + { url = "https://files.pythonhosted.org/packages/d2/00/4d4ed1a11ba2920b85a03a0683b16d97dc5beb2e78078dbf0e13e43bcea7/thinc-8.3.13-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:565300b7e13de799e5abff00d445f537e9256cf7da4dcb0d0f005fc16748a29e", size = 792096, upload-time = "2026-03-23T07:22:24.349Z" }, + { url = "https://files.pythonhosted.org/packages/44/5d/dc33d6932be8721af2ef76b4a3a6e8020648630eabae61fb916d2a861d1d/thinc-8.3.13-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c17cef1900a1aba7e1487493d16b8aa0a8633116f1b2a51c6649a4000697f17b", size = 3842215, upload-time = "2026-03-23T07:22:25.836Z" }, + { url = "https://files.pythonhosted.org/packages/af/bc/a6d37d8dadc2c5b524f51192413481160c42c9dd6105e8d5551531623225/thinc-8.3.13-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f4f26d1eec9b2a6a8f2e0298a5515d13eb06d70730d0d9e1040bb329e12bf3fb", size = 3849253, upload-time = "2026-03-23T07:22:27.845Z" }, + { url = "https://files.pythonhosted.org/packages/7a/59/ce9c7067f1dfe5985875927de9cf7a79f9dae3e69487fd650dfba558029d/thinc-8.3.13-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a61a31fd0ce3c2771cf4901ba6df70e774ffe32febf1024c5b43d63575cd58fe", size = 4831163, upload-time = "2026-03-23T07:22:29.395Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a8/f57819347fc4d8bef2204d15fcbb9d7dff2d6cdd5f83d5ed91456ddacc55/thinc-8.3.13-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ba8119daf84a12259ae4d251d36426417bafa0b34108890b4b7e2b50966bd990", size = 4986051, upload-time = "2026-03-23T07:22:30.933Z" }, + { url = "https://files.pythonhosted.org/packages/05/ef/a82214bb7c7c1e2d92b69e1a7654be90cfab180082c6108e45a98af2422c/thinc-8.3.13-cp314-cp314-win_amd64.whl", hash = "sha256:433e3826e018da489f1a8068e6de677f6eff3cc93991a599d90f12cd1bc26cdc", size = 1740382, upload-time = "2026-03-23T07:22:32.869Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ef/1648fda54e9689058335ff54f650a7a314db2a42e21af1b83949b2dc748e/thinc-8.3.13-cp314-cp314-win_arm64.whl", hash = "sha256:11754fada9ad5ba2e02d5f3f234f940e24015b82333db58372f4a6aedad9b43f", size = 1667687, upload-time = "2026-03-23T07:22:34.967Z" }, +] + +[[package]] +name = "tokenizers" +version = "0.22.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" }, + { url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" }, + { url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" }, + { url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" }, + { url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" }, + { url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" }, + { url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" }, + { url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" }, + { url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" }, + { url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, +] + +[[package]] +name = "typer" +version = "0.24.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/24/cb09efec5cc954f7f9b930bf8279447d24618bb6758d4f6adf2574c41780/typer-0.24.1.tar.gz", hash = "sha256:e39b4732d65fbdcde189ae76cf7cd48aeae72919dea1fdfc16593be016256b45", size = 118613, upload-time = "2026-02-21T16:54:40.609Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl", hash = "sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e", size = 56085, upload-time = "2026-02-21T16:54:41.616Z" }, +] + +[[package]] +name = "typer-slim" +version = "0.24.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/a7/e6aecc4b4eb59598829a3b5076a93aff291b4fdaa2ded25efc4e1f4d219c/typer_slim-0.24.0.tar.gz", hash = "sha256:f0ed36127183f52ae6ced2ecb2521789995992c521a46083bfcdbb652d22ad34", size = 4776, upload-time = "2026-02-16T22:08:51.2Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/24/5480c20380dfd18cf33d14784096dca45a24eae6102e91d49a718d3b6855/typer_slim-0.24.0-py3-none-any.whl", hash = "sha256:d5d7ee1ee2834d5020c7c616ed5e0d0f29b9a4b1dd283bdebae198ec09778d0e", size = 3394, upload-time = "2026-02-16T22:08:49.92Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "tzdata" +version = "2025.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, +] + +[[package]] +name = "tzlocal" +version = "5.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/2e/c14812d3d4d9cd1773c6be938f89e5735a1f11a9f184ac3639b93cef35d5/tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd", size = 30761, upload-time = "2025-03-05T21:17:41.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026, upload-time = "2025-03-05T21:17:39.857Z" }, +] + +[[package]] +name = "unidecode" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/7d/a8a765761bbc0c836e397a2e48d498305a865b70a8600fd7a942e85dcf63/Unidecode-1.4.0.tar.gz", hash = "sha256:ce35985008338b676573023acc382d62c264f307c8f7963733405add37ea2b23", size = 200149, upload-time = "2025-04-24T08:45:03.798Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/b7/559f59d57d18b44c6d1250d2eeaa676e028b9c527431f5d0736478a73ba1/Unidecode-1.4.0-py3-none-any.whl", hash = "sha256:c3c7606c27503ad8d501270406e345ddb480a7b5f38827eafe4fa82a137f0021", size = 235837, upload-time = "2025-04-24T08:45:01.609Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.41.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/32/ce/eeb58ae4ac36fe09e3842eb02e0eb676bf2c53ae062b98f1b2531673efdd/uvicorn-0.41.0.tar.gz", hash = "sha256:09d11cf7008da33113824ee5a1c6422d89fbc2ff476540d69a34c87fab8b571a", size = 82633, upload-time = "2026-02-16T23:07:24.1Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/e4/d04a086285c20886c0daad0e026f250869201013d18f81d9ff5eada73a88/uvicorn-0.41.0-py3-none-any.whl", hash = "sha256:29e35b1d2c36a04b9e180d4007ede3bcb32a85fbdfd6c6aeb3f26839de088187", size = 68783, upload-time = "2026-02-16T23:07:22.357Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" }, + { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" }, + { url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" }, + { url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" }, + { url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" }, + { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" }, + { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" }, + { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" }, + { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" }, + { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" }, + { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" }, + { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" }, + { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" }, + { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" }, + { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" }, + { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" }, + { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" }, + { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" }, +] + +[[package]] +name = "wasabi" +version = "1.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/f9/054e6e2f1071e963b5e746b48d1e3727470b2a490834d18ad92364929db3/wasabi-1.1.3.tar.gz", hash = "sha256:4bb3008f003809db0c3e28b4daf20906ea871a2bb43f9914197d540f4f2e0878", size = 30391, upload-time = "2024-05-31T16:56:18.99Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/7c/34330a89da55610daa5f245ddce5aab81244321101614751e7537f125133/wasabi-1.1.3-py3-none-any.whl", hash = "sha256:f76e16e8f7e79f8c4c8be49b4024ac725713ab10cd7f19350ad18a8e3f71728c", size = 27880, upload-time = "2024-05-31T16:56:16.699Z" }, +] + +[[package]] +name = "watchfiles" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" }, + { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" }, + { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" }, + { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" }, + { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" }, + { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" }, + { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" }, + { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" }, + { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" }, + { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" }, + { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" }, + { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" }, + { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" }, + { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" }, + { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" }, + { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" }, + { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" }, + { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" }, + { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" }, + { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" }, + { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" }, + { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" }, + { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" }, + { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" }, + { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" }, + { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" }, + { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" }, + { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" }, + { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" }, + { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" }, + { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, + { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, +] + +[[package]] +name = "wcwidth" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/a2/8e3becb46433538a38726c948d3399905a4c7cabd0df578ede5dc51f0ec2/wcwidth-0.6.0.tar.gz", hash = "sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159", size = 159684, upload-time = "2026-02-06T19:19:40.919Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" }, +] + +[[package]] +name = "weasel" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cloudpathlib" }, + { name = "confection" }, + { name = "httpx" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "smart-open" }, + { name = "srsly" }, + { name = "typer" }, + { name = "wasabi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/e5/e272bb9a045105a1fdf4b798d8086f5932a178f4d738f17a74f5c9e0ae9a/weasel-1.0.0.tar.gz", hash = "sha256:7b129b44c90cc543b760532974ca1e4eb30dad2aa2026f57bdce66354ae610fc", size = 38682, upload-time = "2026-03-20T08:10:25.266Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/07/57ebf7a6798b016c064bd0ca81b4c6a99daa4dc377b898bc7b41eb6b5af0/weasel-1.0.0-py3-none-any.whl", hash = "sha256:89518acee027f49d743126c3502d35e6dd14f5768be5c37c9af47c171b6005cc", size = 50713, upload-time = "2026-03-20T08:10:23.637Z" }, +] + +[[package]] +name = "websockets" +version = "16.0" +source = { registry = "https://pypi.org/simple" } +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" } +wheels = [ + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, +] + +[[package]] +name = "win32-setctime" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0", size = 4867, upload-time = "2024-12-07T15:28:28.314Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083, upload-time = "2024-12-07T15:28:26.465Z" }, +] + +[[package]] +name = "wrapt" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/9f/06263fcd8ad6c405f05a3905fd7a84dd3176eb5ad46e44bccc0cd16348bb/wrapt-2.2.1.tar.gz", hash = "sha256:6744f504375775d7609c82c8d3d94af1c9a6f05586984536905908ba905277b9", size = 127620, upload-time = "2026-05-22T14:49:43.056Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/0c/bfae7b9401583b6d05938cd16dedc43857d96da2f8a3d50d78cc515bf6ff/wrapt-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3ffad790d9d11d8ecf9f17c4bb671a5b4089e4d8b575c46c5129597f41f836b0", size = 81021, upload-time = "2026-05-22T14:48:00.313Z" }, + { url = "https://files.pythonhosted.org/packages/26/58/80f6a6599f933f4caecc1cb3ee88a04faf81e8b9bddbd6109c688dd63e0f/wrapt-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:628f5220c7a904d5fc78f7075c8d7871433eb6d035c94728a22fdf85f193d2a8", size = 81692, upload-time = "2026-05-22T14:48:01.49Z" }, + { url = "https://files.pythonhosted.org/packages/17/93/fb357cc7847c58a8ae790be718903afa81a28d23e642c843dc4129e8a0b2/wrapt-2.2.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:61acce4257a9883669703c525447c5b4c392edf0f987ae77ec32668440158f0e", size = 169364, upload-time = "2026-05-22T14:48:02.791Z" }, + { url = "https://files.pythonhosted.org/packages/aa/0b/76b601ee309a8bd556af0eecb184394c20b3c49aa9c8e085aa1ffacc2568/wrapt-2.2.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:727ab4244622cd6ad2390f322642090c877d2e83a608d2653a7643ae5368d926", size = 171079, upload-time = "2026-05-22T14:48:04.22Z" }, + { url = "https://files.pythonhosted.org/packages/cd/87/ee3f32d5658e3e26d3e0e457922b47a36dd3bfbdfee7f97bb3e802344a66/wrapt-2.2.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03df9ebed4c73ab93fa8c07e3d41d818dfca1852b15731a3de59457b27814624", size = 160205, upload-time = "2026-05-22T14:48:05.553Z" }, + { url = "https://files.pythonhosted.org/packages/b1/d0/ae2fd64277a67f5d7bffcf2d05eea1e476263fb2a072baf0b0129ab85984/wrapt-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0d9ff006f420b2ec8296aa56ade43ea7da3e997e85769f0aafc5e0661aacb710", size = 168922, upload-time = "2026-05-22T14:48:07.132Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f3/2d541a060c5bbafb9400bca4917e4d78bfd1f239f404782c86831a8f6b29/wrapt-2.2.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:844c858fc3bb7eacc0ba8efa904935d16aac6a4470948ad1e7e55c9f5a2a665f", size = 158388, upload-time = "2026-05-22T14:48:08.629Z" }, + { url = "https://files.pythonhosted.org/packages/1d/68/8d92c8800c57e93cb116ae9e9d6cbafc34fade5ee9f9107b6f203fb4dc35/wrapt-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87bacdaf225117a342a20d9c03438d701c02112f6e3f351ce9b7f32354f14797", size = 167682, upload-time = "2026-05-22T14:48:10.042Z" }, + { url = "https://files.pythonhosted.org/packages/30/72/83ea3790ea352439442349388e29ff07b76e0686265f9088bbb505d1608d/wrapt-2.2.1-cp312-cp312-win32.whl", hash = "sha256:2f8c90c8afde51969487be4e1343ae049b268854877d415c2510baf833775052", size = 77857, upload-time = "2026-05-22T14:48:11.782Z" }, + { url = "https://files.pythonhosted.org/packages/ef/cb/99450668dd3502d62a54a1c8aa56e44f34cb8c1261b381cfe2e7926c3b75/wrapt-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ce32763ac31ce94fe9aada947e479b1975012bff166da409b4b9e4e376cf7e5", size = 80825, upload-time = "2026-05-22T14:48:13.046Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3a/87512881be64e743f9ee4c66f4cbe8e884974bef2a5989af71f999653ac7/wrapt-2.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d1b4d0e0c2119587a31f5c029abd547e0c81d93b89d394566fe1588659eb579", size = 79087, upload-time = "2026-05-22T14:48:14.323Z" }, + { url = "https://files.pythonhosted.org/packages/88/d1/a1b08f8f4fac8cbb156fa51cf64ee2c7f7f74f9875ba3cf70b3c58368694/wrapt-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d2beb1c7cab10603aecdc42f8edd6ff013f9a32e4543474e38e6b77ce9975aeb", size = 80831, upload-time = "2026-05-22T14:48:15.598Z" }, + { url = "https://files.pythonhosted.org/packages/54/ce/57890814991446a845e09b3445ce8b694f27eb0577004f2c2a36a9772ed4/wrapt-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e0cb7e4dd71f4c32e5e84843cd3c4cd65dda034314004bbe1d7f99af2426ab80", size = 81375, upload-time = "2026-05-22T14:48:17.071Z" }, + { url = "https://files.pythonhosted.org/packages/38/65/08d7a6c76ac4493bdb668205ee9c1de1bd5daca61717c3e9aa49b4c01499/wrapt-2.2.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95821352042722cd9f1108874579a47989d0a7e12a37d87d2fc4af20fd99ab8a", size = 167417, upload-time = "2026-05-22T14:48:18.303Z" }, + { url = "https://files.pythonhosted.org/packages/62/ce/f1ccbee7a1bfe5cdc6b3da6bab4b45713d628b9294da32a39f563d648140/wrapt-2.2.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:abd621552ede77c4c69be7fac44ba911225b0c812b6ba604e5964cf98085b474", size = 166948, upload-time = "2026-05-22T14:48:19.768Z" }, + { url = "https://files.pythonhosted.org/packages/86/2a/f85d48d1cd4869aee6704028d257d740a47c1c467b457ce396b4b5b55d07/wrapt-2.2.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e3677c7146ce694874941ba82b57092cc4875445aadf29d72807351023105143", size = 158148, upload-time = "2026-05-22T14:48:21.96Z" }, + { url = "https://files.pythonhosted.org/packages/fe/5c/93939ad11d4a12358ab1aab219a2ef5efa5612e0db6b9fc65af8af1a891b/wrapt-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9a5934eaea872e17936b5f45501eba5ab0bce9a74122e172b663d7c28c459c4a", size = 165905, upload-time = "2026-05-22T14:48:23.373Z" }, + { url = "https://files.pythonhosted.org/packages/e0/22/b8c2aa89862ff58605934d7abf4b70e6a5a1c33df96656f49035ccdf1c8a/wrapt-2.2.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f5b9daf6b629fce418e0cc3dd0436eac045188fa35deadb7a7f3941d5b8203f9", size = 156712, upload-time = "2026-05-22T14:48:24.767Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/bf00a7b02239c12bb02ddcc3c0b971bfcc36e578c5a44f1ccfef5b458545/wrapt-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f53ac9f3ef573326d009ed809beff4efcac6451931c2b8132586da4b9e53ff31", size = 166560, upload-time = "2026-05-22T14:48:26.83Z" }, + { url = "https://files.pythonhosted.org/packages/fe/93/6390ca9c5b787683cef588d04f57c8d41b9a2323b5597a65f18638c90ef2/wrapt-2.2.1-cp313-cp313-win32.whl", hash = "sha256:1ffa9cfd4bdb581539951b14ae661ff20ed0c3599b3e911a131ee0ec5ac11337", size = 77817, upload-time = "2026-05-22T14:48:28.221Z" }, + { url = "https://files.pythonhosted.org/packages/97/73/ce10f0e71c0cfaa1a65faadb8efd4852028b3bb9ba28932b8889df769d38/wrapt-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:368eac1e20fd0bb03dd3cc42bf9887154c3861b60989389ccb5fac032617d215", size = 80736, upload-time = "2026-05-22T14:48:30.139Z" }, + { url = "https://files.pythonhosted.org/packages/c7/4c/89f4a6818fafbbd840330e4fa3873073e1bfc166133a64cac7f8fde7a5e3/wrapt-2.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:c754dafdf5aaf0b401b644a90a30046929a0dd1a536e0ff0ec959a59155d9c7f", size = 79099, upload-time = "2026-05-22T14:48:31.405Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f2/9a8741c46f8c208ac0a45b25ba170bcb4fb72a2781d5fb97dbd7b6be73cb/wrapt-2.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ed928d0fda15fc0adc8d13305c8b3c0f2fba5b0669950c9e6d019d9162a3b3e8", size = 82802, upload-time = "2026-05-22T14:48:33.307Z" }, + { url = "https://files.pythonhosted.org/packages/9c/0d/e9c855716a3705eef1416456bdf062b60620726fdc59428ff670fc3c60dc/wrapt-2.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fafb4e739e43544d12cb4abd1605fd4683b6ca6a9ad682b7fd8f4d21973eafa8", size = 83329, upload-time = "2026-05-22T14:48:34.593Z" }, + { url = "https://files.pythonhosted.org/packages/3b/d6/a88f1c13112b7831adac75cea65d8310e0d696d570c8961844c90a57b865/wrapt-2.2.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:74d6a0c31472fe5d814917266b9f46495d7c61ed890af08b468acea92fb89a8d", size = 202937, upload-time = "2026-05-22T14:48:35.859Z" }, + { url = "https://files.pythonhosted.org/packages/42/65/e29d54aef06a4d898a5b8a25589a0b3769bde454f922fad8f6f89fbfb650/wrapt-2.2.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab5be648d5a0b86b7438864f8df3c705a65cef35a2fd3e5561e3e203167e0f27", size = 209997, upload-time = "2026-05-22T14:48:38.153Z" }, + { url = "https://files.pythonhosted.org/packages/2a/91/e4454263516cf0e12640912fbca9a83654e424f0a6ddb79f5cd7ce14bf33/wrapt-2.2.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9d8f204c8e3a8bf9ece17e0a83d137fd807440977f8a5e762d59306795011440", size = 194856, upload-time = "2026-05-22T14:48:39.69Z" }, + { url = "https://files.pythonhosted.org/packages/de/d0/fe0ee202286afdf4a7f77dd29f195703145764d572aec209c5086e57d924/wrapt-2.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d047f6498c973874ba08ac3f97c69a2c4b2211c8de6f4c205f75cb1c9522596e", size = 205654, upload-time = "2026-05-22T14:48:43.456Z" }, + { url = "https://files.pythonhosted.org/packages/23/b6/87d860dfc6460c246af70b1fd5c8b76df77571b42a493459423ded94fd7d/wrapt-2.2.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:7a4fdb9326aab4a5a477a1640e5ad786a8495901009d7e7b038371edd23a9d2b", size = 192206, upload-time = "2026-05-22T14:48:44.858Z" }, + { url = "https://files.pythonhosted.org/packages/df/46/3eea8cde077d985f239a38c0257087b8064fd9ee9b1a99e282d2c86da4ef/wrapt-2.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c8cc5094b08abeae52da9c73c8a32003623be691a5193df2f4e3eac3d557c394", size = 198428, upload-time = "2026-05-22T14:48:46.319Z" }, + { url = "https://files.pythonhosted.org/packages/18/dc/b927ee9c7fc67adc3a5658f246a0d275425eb840ba36e7b702e70f18bde8/wrapt-2.2.1-cp313-cp313t-win32.whl", hash = "sha256:9907a4402ab6db12b7077a0ea5d7a4d028ecb22c8eee2b53527080d347cd1562", size = 79448, upload-time = "2026-05-22T14:48:47.901Z" }, + { url = "https://files.pythonhosted.org/packages/ec/b3/fd30b473fe498c70e6b9a5f328b8d3fbaf1b8c3c481465f59724bba8eb70/wrapt-2.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:5590d63f5243251641cf543009b4c9314a79d0598fdb8a8e4cfc918494536c53", size = 83021, upload-time = "2026-05-22T14:48:49.201Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f3/96c39153a8737a6e9aa85adef254ac4195bea3f2d24efc60472ccc3c9e2e/wrapt-2.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:c318a64b53d97b841d7b5e637517e50a27be64bc695128422953d4b21710954e", size = 80295, upload-time = "2026-05-22T14:48:50.479Z" }, + { url = "https://files.pythonhosted.org/packages/0a/a3/11d7f34ebbf3231bc907a3e6d5ee051b14d034c1bc7b65a97d5cc00516df/wrapt-2.2.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6f56a647e4eaf5f0ca40330fb070f566bdf9f7b0db89a1af20d71c28dcd7a0ab", size = 80879, upload-time = "2026-05-22T14:48:51.802Z" }, + { url = "https://files.pythonhosted.org/packages/13/3c/b74cfd984cef560b900fb1a727af20352d89e1f06bf2e1114dd3f00f5f5a/wrapt-2.2.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:64b7deeda4b70408e382328d8bbe52a256fe9bc63ae3db86d804608367e5422c", size = 81462, upload-time = "2026-05-22T14:48:53.18Z" }, + { url = "https://files.pythonhosted.org/packages/15/a3/7c8f704b8dc07dfe0a5d01c2edbfd88317aa8e5e3fa7c743eb7a085ae767/wrapt-2.2.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b9cf53ba90717db2e292401de290776c498d4bbfb0d4a559ca2895db8b9dcb5c", size = 167251, upload-time = "2026-05-22T14:48:54.562Z" }, + { url = "https://files.pythonhosted.org/packages/80/85/a34d1888d97247da6c2ff6118c3a721c73ed8cc4dd198c00208bb73b6f80/wrapt-2.2.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cf3638274ab9d9b724c9baa0b4c04e132cd6faefb78b4dd3dd1a02a4bdaad41e", size = 166316, upload-time = "2026-05-22T14:48:56.065Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d7/72ffaeb01eebc704afe3fb99e840480f4bda45f0fa66e3381b6a39251c8f/wrapt-2.2.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aed9658797d0b45d6c49adcfc6b41f66e6f2d0c6de3ec79e16cf4b1855df240f", size = 157952, upload-time = "2026-05-22T14:48:57.924Z" }, + { url = "https://files.pythonhosted.org/packages/24/5b/36f5d6b024e4edfdd90b140742d11ebcf7836daf5c9daf326c55c24db412/wrapt-2.2.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1d676ee388bc42a04d56dd7deb5605244dac2e35cc2fadbb43c9fa25bbd93508", size = 166130, upload-time = "2026-05-22T14:48:59.384Z" }, + { url = "https://files.pythonhosted.org/packages/81/06/9296d9e97bfdef5483dfcc859d57b095b257144b2bc5300ab521e06f4bc7/wrapt-2.2.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e395f7bc31851ef9b612050368cb446e9bc14cd7454b025018980349caf25ae5", size = 156604, upload-time = "2026-05-22T14:49:00.921Z" }, + { url = "https://files.pythonhosted.org/packages/53/37/16953929ed6776175720e58fc966e779926d8d71e2c7b2273230590ca71f/wrapt-2.2.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5f1845c2a8cc1180ccccfa45785dd06f562730d19ef75be180334254012b6283", size = 166007, upload-time = "2026-05-22T14:49:02.332Z" }, + { url = "https://files.pythonhosted.org/packages/b9/73/20ee58c0612dae7c31131a7095345812ed2c7b389019e175f68cde34e5b4/wrapt-2.2.1-cp314-cp314-win32.whl", hash = "sha256:436addbc4bb4fc0a88c702577f51195d7d73683a7f3e0e5b253d8404d7847243", size = 78327, upload-time = "2026-05-22T14:49:03.722Z" }, + { url = "https://files.pythonhosted.org/packages/22/b3/ef7c3295d02e0448a71c639a36a057f46d524d057c9486291a7a3039e65c/wrapt-2.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:50972a1d974ea07725a7f6b1cec5f8759008afd030a0024843ebe7d52de47f2b", size = 81144, upload-time = "2026-05-22T14:49:05.093Z" }, + { url = "https://files.pythonhosted.org/packages/ac/dc/7bdf336953f99f4ceb0a584bb8870e42c8f26f93ea10c87834dad62f1668/wrapt-2.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:1c9934ea5d92957e3cd0adbc0845539dccfd62710ebe16195a8c66c53954db36", size = 79569, upload-time = "2026-05-22T14:49:06.413Z" }, + { url = "https://files.pythonhosted.org/packages/6a/6d/6dfae80150ff1919c356d1dd528f049bcdfaae29b4d284bc957e022caef4/wrapt-2.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:17de18fc12cea55b8a9587314cb830573e37fb33b247a7515696350863714188", size = 82892, upload-time = "2026-05-22T14:49:07.925Z" }, + { url = "https://files.pythonhosted.org/packages/82/7b/4e34766a7d7804ffce9e71befe47e9b3225dc350c49c94493c4ab39fd3a5/wrapt-2.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a9dec1aca52dddde7df94818310fa2fe79739c8f385b2014c4cb1035f5508199", size = 83333, upload-time = "2026-05-22T14:49:09.257Z" }, + { url = "https://files.pythonhosted.org/packages/9d/57/0b34db3e8de44ccfece62d7b337abd1631dd810f5adc5f3db571727836b5/wrapt-2.2.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:69f2e9244542cb34dd59c7f073445b9e54ad9f3fce8d93606c368a1b499fc413", size = 202899, upload-time = "2026-05-22T14:49:10.572Z" }, + { url = "https://files.pythonhosted.org/packages/e5/45/ac0c459f154b99d92789a6cba7ca727185b83513b986f8ec7fe2aacddcbf/wrapt-2.2.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d83966dc7f4f45e8b97b5933685ac2e6e67fc0e19246ea314bceb9a8970c956", size = 209986, upload-time = "2026-05-22T14:49:12.229Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e4/77e37ff33ad018fa81ade52c25fa327b80b56f81d734279a63614fcb4cbc/wrapt-2.2.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:78b0aa6bfb7be8deed0ab23e7aa028cc5210c29bc2d32a04d52b50e517a7307e", size = 194893, upload-time = "2026-05-22T14:49:14.139Z" }, + { url = "https://files.pythonhosted.org/packages/dd/9d/7ea651d1ab032fc5fa222fbec91d0f8a1397f6ae04ebb93fa7219aa921d7/wrapt-2.2.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:05d5cb74d1b232ec8cfa130a8f900708699ff2491d97b8f85a4cdc5996294b85", size = 205636, upload-time = "2026-05-22T14:49:15.714Z" }, + { url = "https://files.pythonhosted.org/packages/09/af/8e88031a701275b9085c54e64bc88c0b1cd55c77eadd400691c371cd76c4/wrapt-2.2.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f6518b94edb9150452e9aba08027d4cc293433753ec1fbefb4629a21cbc74181", size = 192267, upload-time = "2026-05-22T14:49:17.283Z" }, + { url = "https://files.pythonhosted.org/packages/bf/a8/e657ca876b06710194f243d81c4b0896ade646e244bdbec2d87c8c56a8bd/wrapt-2.2.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ed55af48b3eb28f43228ca2306788892bcb629eb2b5c4876e2a3659872c2f17a", size = 198378, upload-time = "2026-05-22T14:49:18.785Z" }, + { url = "https://files.pythonhosted.org/packages/c8/59/822efe4ea722a3961331bfa35b7d90937790d2c20f0616de1997ccc3aebd/wrapt-2.2.1-cp314-cp314t-win32.whl", hash = "sha256:2e08688ab16525897da6589d56d0aebaf417bbe91c2d8e3b96203b1efa596e85", size = 80226, upload-time = "2026-05-22T14:49:20.264Z" }, + { url = "https://files.pythonhosted.org/packages/ab/31/2a7dc5f6abb2fca0b6e1610e120419f603650aceb4f1d3ac4cae0354e162/wrapt-2.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:fd0135d34387f5fd087d9be368ea77ea89cf2451dc1cd1c622d35021bcb3ab50", size = 83835, upload-time = "2026-05-22T14:49:21.634Z" }, + { url = "https://files.pythonhosted.org/packages/9f/c0/782b86e28d1ceebeb74cccea12d2cd3d2ba0bd68e3dec20b1bc5873f6127/wrapt-2.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:f70db64e8266d7c45d3b735f2e08eeb434b5e03da9a479ae42b2e2e486a21a00", size = 80722, upload-time = "2026-05-22T14:49:23.59Z" }, + { url = "https://files.pythonhosted.org/packages/53/46/29ac9daf11a86c22a8c38cd9236c62928ccae83f7ceb06bd3b0467cf9d05/wrapt-2.2.1-py3-none-any.whl", hash = "sha256:3aafea2975caef8ca49400640dde02cc7426e798f24870ed01f490bc3cffd32f", size = 61000, upload-time = "2026-05-22T14:49:41.593Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, +]