diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 54eb32342..88c07f60d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -133,10 +133,36 @@ jobs: compat: true soft: true march: x86-64-v2 + # macos_floor is the OS this asset actually runs on, and every bundled + # dylib is checked against it. `uv sync` resolves wheels for the runner, + # so numpy/scipy arrive as macosx_14_0 and 14.0 is the honest floor here. - os: macos-latest asset_name: lilbee-macos-arm64 backend: metal - # Intel Mac dropped: macos-13 hosted runners stay queued; pip wheel covers them. + macos_floor: "14.0" + # Intel Mac: built natively on macos-15-intel, GitHub's Intel image + # (Nuitka can't cross-compile arch from the arm64 runner). Soft because + # the Intel image can queue longer than the arm64 pool -- this asset must + # never block the release fan-out. The install below pins wheel resolution + # to the x86_64 target rather than the runner, so the deps come in at their + # oldest macOS build (pyarrow's 12.0 is the binding one) and the AVX + # baseline reaches every Ivy Bridge / Haswell Mac, which cap at macOS 12. + - os: macos-15-intel + asset_name: lilbee-macos-x86_64 + backend: cpu + soft: true + macos_floor: "12.0" + # Pre-Haswell-safe Intel Mac build: same self-contained model as the + # linux/windows compat cells (stock lancedb swapped for +compat, launcher + # held to x86-64-v2). Covers the AVX-but-no-AVX2 tail (e.g. 2013 Mac Pro) + # and guards against an AVX2-baselined stock lancedb SIGILL. + - os: macos-15-intel + asset_name: lilbee-compat-macos-x86_64 + backend: cpu + compat: true + soft: true + march: x86-64-v2 + macos_floor: "12.0" - os: windows-latest asset_name: lilbee-windows-x86_64.exe backend: vulkan @@ -236,8 +262,36 @@ jobs: run: echo "NUITKA_CACHE_DIR=${GITHUB_WORKSPACE}/nuitka-cache" >> "$GITHUB_ENV" - name: Install dependencies + if: matrix.os != 'macos-15-intel' run: uv sync --extra release && uv pip install 'nuitka[onefile]>=4.0,<5' pip + # Intel Mac: `uv sync` can't run here -- stock lancedb 0.33.0 ships no + # macosx_x86_64 wheel. Resolve it from the +compat index (PEP 440 drop-in), + # then install the rest of [release] fresh from PyPI. CMAKE_ARGS + deployment + # target hold any throwaway llama-cpp-python sdist build at the cpu/x86_64 + # baseline; the dedicated step below builds the llama-cpp-python that actually + # ships. The cell stays soft so an install failure never blocks the fan-out. + # + # --python-platform resolves wheels for the x86_64 target instead of the + # runner. Without it uv picks the newest wheel the macOS 15 image accepts + # (numpy/scipy macosx_14_0) and bundles a binary that dies on macOS 12-13, + # even though both ship older-built wheels that run just as well here. + - name: Install dependencies (Intel Mac) + if: matrix.os == 'macos-15-intel' + shell: bash + env: + MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macos_floor }} + run: | + set -euxo pipefail + eval "$(BACKEND=cpu TARGET_ARCH=x86_64 tools/wheel-build/cmake_args.sh)" + export CMAKE_ARGS + uv venv + uv pip install --python-platform x86_64-apple-darwin \ + --extra-index-url https://lilbee.sh/compat/ \ + lancedb==0.33.0+compat + uv pip install --python-platform x86_64-apple-darwin \ + '.[release]' 'nuitka[onefile]>=4.0,<5' pip + - name: Verify extras installed in build venv shell: bash # --no-sync is load-bearing: re-syncing reinstalls python-multipart and shadows the shim. @@ -255,6 +309,16 @@ jobs: check fi + # Nuitka bundles these dylibs, and dyld enforces each one's minos separately + # from the launcher's, so one dep built on a newer runner silently ships a + # binary that dies at load. Fail here rather than on the user's Mac. + - name: Verify macOS floor + if: runner.os == 'macOS' + shell: bash + env: + MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macos_floor }} + run: bash tools/wheel-build/check_macos_floor.sh + # compat cell only: swap the synced stock lancedb for the pre-Haswell build. # --no-deps leaves the rest of the venv intact, so nothing leaves this cell. # Pinned to the latest stable lancedb for Python (0.33), served by the fork as @@ -278,13 +342,19 @@ jobs: # engine cache separate from bare-runner builds. cache-env: ${{ matrix.container || matrix.os }} env: - MACOSX_DEPLOYMENT_TARGET: "11.0" + MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macos_floor }} - name: Build executable shell: bash env: ASSET_NAME: ${{ matrix.asset_name }} PRODUCT_VERSION: ${{ env.LILBEE_NUITKA_VERSION }} + # Pin Nuitka's clang output to this asset's floor. Unset, clang defaults the + # deployment target to the build host (macOS 15), which would stamp the + # launcher minos=15 and refuse to launch below it. This only pins the + # launcher; the bundled dylibs carry their own minos, which is what the + # "Verify macOS floor" step above checks. No-op off macOS. + MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macos_floor }} # compat cell only: hold Nuitka's C output to the pre-Haswell baseline so # the launcher layer (not just lancedb) runs on old silicon. Empty elsewhere. CFLAGS: ${{ matrix.march && format('-march={0}', matrix.march) || '' }} diff --git a/.gitignore b/.gitignore index 7086321a5..437a01498 100644 --- a/.gitignore +++ b/.gitignore @@ -30,7 +30,7 @@ CLAUDE.md docs/plans/ demo.cast docs/superpowers/ -.kreuzberg/ +.xberg/ .claude/ .dolt/ .doltcfg/ diff --git a/AGENTS.md b/AGENTS.md index 61c1f941e..f131afe4b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -140,7 +140,7 @@ CLI also accepts `--model` / `-m` for chat model, `--data-dir` / `-d`, `--ocr-ti - Type checking: `mypy` with strict settings - **No em dashes (—)** — use periods or commas instead - **No divider comments** (`# ------`) for grouping — use modules or classes instead -- **Don't scatter functions between constants.** Within a module (or a topical block in one), keep the module-level constants contiguous and the functions/classes contiguous. A `def` wedged into the middle of a run of `FOO = ...` / `BAR = ...` lines breaks the eye's scan. A helper that formats or validates a small group of constants may follow that group directly (the constants, then their one helper), but don't interleave: `A = ...`, `def f(): ...`, `B = ...`, `def g(): ...`, `C = ...` is wrong; group it as `A = ...`, `B = ...`, `C = ...`, `def f(): ...`, `def g(): ...`. Order by concern (all the "skip" constants, then the skip helpers; all the "doc table" constants, then the doc-table dataclass), not by when each was added. +- **Canonical module layout — group by kind, don't scatter.** A module reads top-to-bottom in this order: module docstring → `from __future__` → imports → module-level constants & data tables → enums → exception classes → data containers (`dataclass` / `TypedDict` / `NamedTuple` / `BaseModel`) → classes → functions. Within each band, order by concern, not by when each symbol was added. The reader should never have to scan past a `class` or `def` to find the next constant, nor past a constant to find the next class. A constant assignment (`FOO = ...`, `BAR: T = ...`) appearing AFTER the first `def`/`class` in a module is the scatter smell — hoist it into the constants band at the top. A `def` wedged into a run of `FOO = ...` / `BAR = ...` lines is the same smell from the other side. The one allowed exception: a single small helper that exists only to build or validate one adjacent group of constants may sit directly after that group (the constants, then their one helper) — but never interleaved (`A = ...`, `def f(): ...`, `B = ...` is always wrong). When in doubt, constants and data tables go at the top, contiguous; everything callable goes below, contiguous. - **Annotate `isinstance` guards** with a comment explaining why the check is needed (e.g. untyped frontmatter, untyped SDK response). NEVER use `isinstance(self.app, LilbeeApp)` style host-narrowing in production: if a screen / widget needs LilbeeApp-specific access, declare `app: LilbeeApp # type: ignore[assignment]` at the class scope (mypy narrows; runtime is unchanged). Tests host via `tests/_lilbee_app_test_host.LilbeeAppHost` (a LilbeeApp subclass with `_test_skip_auto_init = True`), never `App[None]` / `class _PlainApp(App)`. - Use literal unicode chars (`★`) not escapes (`\u2605`); extract to named constants - **No filterwarnings** without explicit user approval; fix warnings at the source @@ -190,7 +190,7 @@ Default: **every import lives at module top**, ordered stdlib, third-party, loca **Permitted reasons for a function-local import:** 1. **Circular import.** Module A's top-level already imports module B, and module B needs a symbol from A. Put A's import of B inside the one function in B that needs it, with a comment `# circular: B -> A via `. -2. **Heavy third-party lib.** Module-top import takes **>50 ms measured by `python -X importtime`** or loads native libraries. Known-heavy libs in this project: `litellm` (provider SDK fanout), `lancedb` (arrow + datafusion), `kreuzberg` (OCR stack), `gguf` (numpy), `sentence_transformers`, `spacy`, `crawl4ai`, `textual` when imported outside the TUI screen modules. (The local inference engine is the out-of-process `llama-server`, reached over HTTP, so it adds no heavy Python import.) Use importtime before declaring a lib heavy. +2. **Heavy third-party lib.** Module-top import takes **>50 ms measured by `python -X importtime`** or loads native libraries. Known-heavy libs in this project: `litellm` (provider SDK fanout), `lancedb` (arrow + datafusion), `xberg` (OCR stack), `gguf` (numpy), `sentence_transformers`, `spacy`, `crawl4ai`, `textual` when imported outside the TUI screen modules. (The local inference engine is the out-of-process `llama-server`, reached over HTTP, so it adds no heavy Python import.) Use importtime before declaring a lib heavy. 3. **CLI startup path.** The import lives inside a Typer command body so `lilbee --help` stays fast. Treat this as a sub-case of (2): the CLI loader stays lean so unused subcommands don't pay for their dependencies. **Never lazy-import the following:** @@ -390,6 +390,7 @@ work; the greps below remain the full set a reviewer still runs by eye. - `grep -rn "App\[None\]\|class _PlainApp\b\|class _BareApp\b" tests/` — test host that bypasses LilbeeApp. Migrate to LilbeeAppHost. - `grep -cE "isinstance\(.*widget, (Input|Checkbox|Select|TextArea)\)" src/` followed by "if more than one site" — type-keyed widget routing. Replace with a `dict[type, Callable]`. - `grep -rnE "^\s*global \w+" src/` — module-level mutable globals. Encapsulate on a class. +- `for f in $(git diff --name-only main -- 'src/**/*.py'); do awk 'NR>1 && /^[A-Z_]+ *[:=]/ && seen {print FILENAME": "$0} /^(def |class )/{seen=1}' "$f"; done` — module-level constant assigned AFTER the first `def`/`class` (the scatter smell). Hoist it into the constants band at the top. See the canonical-module-layout rule in Code Style. - `grep -rnE "def \w+\(.*\) -> .*:\s*$" src/ | xargs -I{} awk 'def_lines>20'` (proxy: scan modules >700 LOC) — functions over ~20 lines need a split into named sub-helpers. - `grep -rn "from lilbee\.\(modelhub\|providers\|data\|retrieval\|wiki\|crawler\)" src/lilbee/catalog src/lilbee/core` — upward imports from foundation layers. catalog and core never import from anything above them. - `grep -rn "from lilbee\.\(retrieval\|wiki\|crawler\|cli\|server\)" src/lilbee/data src/lilbee/modelhub src/lilbee/providers` — Layer-2 modules importing Layer-3+. Invert via callback or move the helper down. diff --git a/README.md b/README.md index 6628cb5ec..990d5af19 100644 --- a/README.md +++ b/README.md @@ -229,9 +229,9 @@ Or crawl a whole site, not just one page. With recursive crawling on, lilbee fol lilbee splits indexing by what's being read: -- **Prose and structured documents** (PDFs, Office files, ebooks, HTML, 90+ formats) go through [Kreuzberg] with heading-aware chunking, so each chunk keeps its section context. +- **Prose and structured documents** (PDFs, Office files, ebooks, HTML, 90+ formats) go through [Xberg] with heading-aware chunking, so each chunk keeps its section context. - **Code** goes through [tree-sitter]'s AST-aware splitter across [150+ languages](https://github.com/Goldziher/tree-sitter-language-pack), so chunks map to functions, classes, and modules instead of arbitrary line ranges. -- **Scanned PDFs and photos** go through OCR: Tesseract for plain text, or a local / remote vision model that keeps tables and layout as markdown. +- **Scanned PDFs and photos** go through OCR in 100+ languages: Tesseract for plain text (set `LILBEE_OCR_LANGUAGE`, e.g. `eng+deu`), or a local / remote vision model that keeps tables and layout as markdown. Retrieval returns things that make sense on their own, not fragments cut through an argument or a function signature. @@ -434,7 +434,7 @@ Pre-2013 Intel or pre-Zen AMD CPUs lack [AVX2](https://en.wikipedia.org/wiki/Adv | **Scoop** | `scoop install lilbee-compat` | | **Flatpak** | `flatpak install lilbee io.github.tobocop2.lilbee.compat` | | **Snap** | `curl -LO https://github.com/tobocop2/lilbee/releases/latest/download/lilbee-compat-linux-x86_64.snap && sudo snap install ./lilbee-compat-linux-x86_64.snap --dangerous --classic` | -| **Binary** | [`lilbee-compat-linux-x86_64`](https://github.com/tobocop2/lilbee/releases/latest) or [`lilbee-compat-windows-x86_64.exe`](https://github.com/tobocop2/lilbee/releases/latest) | +| **Binary** | [`lilbee-compat-linux-x86_64`](https://github.com/tobocop2/lilbee/releases/latest), [`lilbee-compat-windows-x86_64.exe`](https://github.com/tobocop2/lilbee/releases/latest), or [`lilbee-compat-macos-x86_64`](https://github.com/tobocop2/lilbee/releases/latest) (pre-AVX2 Intel Macs, e.g. the 2013 Mac Pro) | Same `lilbee` command after install. The crash is from [lancedb](https://lancedb.github.io/lancedb/)'s AVX2-compiled wheels; this build swaps in a [lancedb fork](https://github.com/tobocop2/lance) that picks instructions at runtime. A 👍 or comment on the upstream [lance PR](https://github.com/lance-format/lance/pull/6630) helps it land. @@ -527,7 +527,7 @@ Pull a chat and embedding model first; all recipes pin the server to `127.0.0.1: ## Supported formats -Text extraction powered by [Kreuzberg], code chunking by [tree-sitter]. Structured formats (XML, JSON, CSV) get embedding-friendly preprocessing. This list is not exhaustive; Kreuzberg supports additional formats beyond what's listed here. +Document extraction powered by [Xberg], code chunking by [tree-sitter]. lilbee handles every format Xberg can extract (100+) and tracks its list directly, so support grows as Xberg adds formats. The table below covers the common ones.
Format table @@ -535,15 +535,17 @@ Text extraction powered by [Kreuzberg], code chunking by [tree-sitter]. Structur | Format | Extensions | Requires | | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | PDF | `.pdf` | none | -| Scanned PDF | `.pdf` (no extractable text) | [Tesseract](https://github.com/tesseract-ocr/tesseract) (auto, plain text), or a GGUF vision model via the native mtmd backend (recommended, preserves tables, headings, and layout as markdown) | -| Office | `.docx`, `.xlsx`, `.pptx` | none | -| eBook | `.epub` | none | -| Images (OCR) | `.png`, `.jpg`, `.jpeg`, `.tiff`, `.bmp`, `.webp` | [Tesseract](https://github.com/tesseract-ocr/tesseract) | -| Data | `.csv`, `.tsv` | none | -| Structured | `.xml`, `.json`, `.jsonl`, `.yaml`, `.yml` | none | +| Scanned PDF | `.pdf` (no extractable text) | [Tesseract](https://github.com/tesseract-ocr/tesseract) (auto, plain text, 100+ languages via `LILBEE_OCR_LANGUAGE`), or a GGUF vision model via the native mtmd backend (recommended, preserves tables, headings, and layout as markdown) | +| Office | `.docx`, `.xlsx`, `.pptx`, `.doc`, `.xls`, `.ppt`, `.odt`, `.ods`, `.rtf` | none | +| eBook | `.epub`, `.fb2` | none | +| Images (OCR) | `.png`, `.jpg`, `.jpeg`, `.tiff`, `.bmp`, `.webp`, `.gif`, `.heic`, `.avif`, `.jp2`/`.j2k`/`.jpx` (JPEG-2000) | [Tesseract](https://github.com/tesseract-ocr/tesseract) or a vision model | +| Text/markup | `.md`, `.txt`, `.html`, `.rst`, `.org`, `.tex`, `.typst` | none | +| Data | `.csv`, `.tsv`, `.json`, `.jsonl`, `.xml`, `.yaml`, `.toml` | none | +| Email | `.eml`, `.msg` | none | +| Archives | `.zip`, `.tar`, `.gz`, `.7z`, `.pst` (combined contents extracted) | none | | Code | `.py`, `.js`, `.ts`, `.go`, `.rs`, `.java` and [150+ more](https://github.com/Goldziher/tree-sitter-language-pack) via tree-sitter (AST-aware chunking) | none | -See the [usage guide](docs/usage.md#ocr) for OCR setup and [model benchmarks](docs/benchmarks/vision-ocr.md). +Plus notebooks, bibliographies, iWork, and audio/video, among others. See the [usage guide](docs/usage.md#ocr) for OCR setup and [model benchmarks](docs/benchmarks/vision-ocr.md).
@@ -574,7 +576,7 @@ See the [Semantic chunking section of the usage guide](docs/usage.md#semantic-ch lilbee stands on a stack of established open-source projects, all bundled into one install: -- [Kreuzberg] parses 90+ document formats with heading-aware chunking. +- [Xberg] parses 90+ document formats with heading-aware chunking. - [llama.cpp] is the local model runtime: lilbee bundles its `llama-server` and starts it for you, so every chat, embedding, vision, and reranker call goes through it. [llama-swap] keeps a server per role resident together behind one endpoint, and [gguf-parser] estimates each model's memory footprint so lilbee loads what fits. Without llama.cpp there is no lilbee. - [Hugging Face Hub] (via [huggingface_hub]) hosts the model catalog and handles every download. Search, browse, and pull all route through it. - [LanceDB] is the embedded vector store. @@ -609,7 +611,7 @@ lilbee is built and maintained by one person. If it is useful to you, you can ch MIT. See [LICENSE](LICENSE). -[Kreuzberg]: https://github.com/kreuzberg-dev/kreuzberg +[Xberg]: https://github.com/xberg-io/xberg [LanceDB]: https://lancedb.com [llama.cpp]: https://github.com/ggml-org/llama.cpp [llama-swap]: https://github.com/mostlygeek/llama-swap diff --git a/docs/architecture.md b/docs/architecture.md index fce977469..059216892 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -73,13 +73,13 @@ flowchart LR Documents are chunked, embedded, and stored as vectors for later retrieval. - **File discovery.** Recursive walk of `documents/` with SHA-256 hash-based change detection so only modified files are re-indexed. Each source row also stores the file's size and mtime, so an unchanged file is skipped on a stat alone; the hash runs only when the stat pair drifts. Stores created before these columns re-hash once and backfill. -- **Markdown.** Heading-aware chunking via kreuzberg's `chunker_type="markdown"` with `prepend_heading_context=True`. Splits at heading boundaries and prepends the full hierarchy path (e.g., `# Setup > ## Install`) so each chunk's section context travels with it. Inspired by Anthropic's Contextual Retrieval (2024), which showed adding context to chunks reduces retrieval failures by 49%. +- **Markdown.** Heading-aware chunking via xberg's `chunker_type="markdown"` with `prepend_heading_context=True`. Splits at heading boundaries and prepends the full hierarchy path (e.g., `# Setup > ## Install`) so each chunk's section context travels with it. Inspired by Anthropic's Contextual Retrieval (2024), which showed adding context to chunks reduces retrieval failures by 49%. - **Code.** tree-sitter AST splitting via tree-sitter-language-pack for 150+ languages, with symbol name, type, and line range in chunk headers. -- **PDF.** kreuzberg text extraction with an OCR fallback chain (text extraction → Tesseract OCR → GGUF vision model on `llama-server`). PDF page rasterization is delegated to kreuzberg's `PdfPageIterator`. +- **PDF.** xberg text extraction with an OCR fallback chain (text extraction → Tesseract OCR → GGUF vision model on `llama-server`). PDF page rasterization is delegated to xberg's `PdfPageIterator`. - **Vision OCR.** When `LILBEE_VISION_MODEL` is set, scanned PDFs and images are transcribed by a GGUF vision model served by `llama-server` with an `--mmproj` projector. The pipeline streams an SSE heartbeat during long scans and preserves tables, headings, and multi-column layout as structured markdown. Falls back to Tesseract when no vision model is configured. -- **Structured files.** kreuzberg handles XML, JSON, JSONL, YAML, and CSV natively. Language detection for code-shaped content is delegated to tree-sitter-language-pack's `detect_language()`. +- **Structured files.** xberg handles XML, JSON, JSONL, YAML, and CSV natively. Language detection for code-shaped content is delegated to tree-sitter-language-pack's `detect_language()`. - **Web pages.** crawl4ai fetches HTML with JavaScript rendering via Playwright, converts to markdown, and saves to `documents/_web/` for indexing. Recursive crawls emit live progress, respect per-domain rate limits, and retry on HTTP 429/503 with jitter. SSRF protection blocks internal networks by default. -- **Chunking strategy.** Fixed-size chunking (default, token-aware) for reliability on procedural and reference docs. Opt-in semantic chunking (`LILBEE_SEMANTIC_CHUNKING=true`) splits at topic boundaries via kreuzberg's ONNX embedding model; better on prose-heavy corpora at the cost of roughly 9x more downstream embedding calls. +- **Chunking strategy.** Fixed-size chunking (default, token-aware) for reliability on procedural and reference docs. Opt-in semantic chunking (`LILBEE_SEMANTIC_CHUNKING=true`) splits at topic boundaries via xberg's ONNX embedding model; better on prose-heavy corpora at the cost of roughly 9x more downstream embedding calls. - **Embedding.** Provider-agnostic: native GGUF on the local `llama-server` engine by default, or any backend reachable via the SDK protocol when `pip install lilbee[litellm]` is available. - **Asymmetric query/document embedding.** Instruction-tuned embedders (Qwen3-Embedding, e5/gte `*-instruct`) only reach their retrieval scores when the query carries a task instruction (`Instruct: ...\nQuery: `) embedded differently from documents; base e5 uses `query:`/`passage:` prefixes. lilbee detects the family from the configured embedder ref and applies the right prefixes automatically (`retrieval/embedding_profiles.py`): the query path uses `embed_query`/`embed_query_batch`, the document path keeps `embed`/`embed_batch`. Symmetric models (bge-m3, nomic) and unrecognized models get no prefix, so behavior is unchanged for them. There is no instruction config: a wrong template silently underperforms, so support for a new family is a curated map entry, not a user knob. - **Concept extraction (opt-in).** With `pip install lilbee[graph]`, spaCy noun phrases are extracted per chunk, a co-occurrence graph is built with PPMI weights, and Leiden clustering assigns concepts to communities. @@ -641,7 +641,7 @@ flowchart TD SM -->|No prefix| PROBE[BM25 Confidence Probe] PROBE --> CONF{Confident AND separated?} - CONF -->|Yes| DUAL[Dual-Arm Retrieval] + CONF -->|Yes| DUAL[Hybrid Retrieval] CONF -->|No| EXPAND[LLM Query Expansion] EXPAND --> GEXP[+ Graph Expansion] @@ -677,19 +677,20 @@ flowchart TD #### History Condensation **On by default** (`LILBEE_HISTORY_REWRITE`). A follow-up question is condensed into a standalone retrieval query using the recent chat history (one small LLM call, skipped when there is no history). Retrieval sees only the query text: without this, "what about his brother?" is embedded and BM25-matched with its pronouns. The user's original wording still reaches the answering prompt. -#### Hybrid Search (Dual-Arm Reciprocal-Rank Fusion) -**Always on.** Retrieves the BM25 arm (LanceDB FTS) and the vector arm independently, each fetched exactly `top_k` deep, then fuses by reciprocal rank into one canonical [0, 1] score: +#### Hybrid Search (Weighted Reciprocal-Rank Fusion) +**Always on.** Retrieves a vector arm and a BM25 chunk arm (LanceDB FTS) independently, each fetched exactly `top_k` deep, then fuses by weighted reciprocal rank into one [0, 1] score. An optional third BM25 arm over document titles joins when `LILBEE_TITLE_SEARCH` is on: ``` -score = (rank_weight(vector_rank) + rank_weight(bm25_rank)) / 2, rank_weight(r) = 61 / (60 + r) +score = Σ_arm weight_arm × rank_weight(rank_arm) / Σ_arm weight_arm, rank_weight(r) = 61 / (60 + r) ``` -A row ranked first by both arms scores 1.0; a row one arm never retrieved contributes 0 for that arm, so an arm's top hit still scores 0.5 and stays visible next to rows deep in the other arm. The fused ordering is final: no diversity selection runs on the hybrid path. +The vector arm weighs 1.0, the lexical arm `LILBEE_LEXICAL_FUSION_WEIGHT`, the title arm `LILBEE_TITLE_SEARCH_WEIGHT`. A row ranked first by every arm scores 1.0; a row only one arm retrieved scores that arm's share of the total weight, so a peaked single-arm hit stays visible next to rows deep in the other arms. The fused ordering is final: no diversity selection runs on the hybrid path. -- **Why rank fusion and not score fusion**: a convex combination of normalized raw scores (`alpha × vector_similarity + (1 − alpha) × normalized_bm25`) was tried here and regressed graded precision about 20% against RRF, at every blend weight. Cosine similarities sit in a high narrow band, giving every dense neighbor a score floor that crowds out lexically-certain rows; ranks are scale-free, so neither arm's score distribution can drown the other. (On the rank-vs-score question, see Bruch et al. 2024, "[An Analysis of Fusion Functions for Hybrid Retrieval](https://dl.acm.org/doi/10.1145/3596512)".) Arm depth matters as much as the formula: a row one arm is certain about scores a fixed 0.5, while rows both arms rank mid-pool accumulate two contributions, so deep candidate pools flood the fused top-k with both-arm mediocrity; arms therefore stay `top_k` deep. +- **Adaptive fusion** (`LILBEE_ADAPTIVE_FUSION`, on by default): the lexical arm's weight is scaled per query by how peaked the vector ranking is, so a confident dense arm downweights lexical and a flat one keeps it. `LILBEE_LEXICAL_FUSION_WEIGHT` is the ceiling the rule scales down from. Scores still normalize against that configured budget (a constant), not the per-query adapted weight, so scores from the separate sub-searches expansion merges stay on one comparable scale. +- **Why rank fusion and not score fusion**: a convex combination of normalized raw scores (`alpha × vector_similarity + (1 − alpha) × normalized_bm25`) was tried here and regressed graded precision about 20% against RRF, at every blend weight. Cosine similarities sit in a high narrow band, giving every dense neighbor a score floor that crowds out lexically-certain rows; ranks are scale-free, so neither arm's score distribution can drown the other. (On the rank-vs-score question, see Bruch et al. 2024, "[An Analysis of Fusion Functions for Hybrid Retrieval](https://dl.acm.org/doi/10.1145/3596512)".) Arm depth matters as much as the formula: rows both arms rank mid-pool accumulate two contributions, so deep candidate pools flood the fused top-k with both-arm mediocrity; arms therefore stay `top_k` deep. - **Why no MMR on this path**: for lexical queries the relevant passages are often mutually similar (they quote the same identifiers), which is exactly what diversity selection penalizes; running MMR over the fused pool measurably traded relevant lexical hits for diverse off-topic neighbors. MMR still runs on the vector-only fallback path. - **Canonical score**: every search path sets `SearchChunk.score`, and every downstream stage (sorting, filtering, greedy set cover, concept boost, reranker blending) compares only that field. `distance`, `bm25_score`, and the legacy `relevance_score` remain as provenance. -- **Abstention**: the canonical score is [0, 1] with fixed meaning (0.5 = top of one arm), so `min_relevance_score` is a usable floor: when every retrieved chunk falls below it, ask refuses instead of feeding noise as context. Raw RRF sums (~0.016-0.033 total range) made any threshold meaningless. +- **Abstention**: `min_relevance_score` gates on the fused [0, 1] score. The score normalizes against the configured weight budget (a constant), so an arm's top hit scores a stable share of it and the threshold is a usable floor: when every retrieved chunk falls below it, ask refuses instead of feeding noise as context. Raw RRF sums (~0.016-0.033 total range) made any threshold meaningless. - **max_distance** applies to rows whose *only* signal is a far vector match; a row the BM25 arm also matched keeps its standing regardless of distance, since dropping it would re-bury exactly the identifier hits fusion exists to preserve. - **When it helps**: queries with specific terms, function names, error messages, exact phrases, document identifiers. diff --git a/docs/benchmarks/vision-ocr.md b/docs/benchmarks/vision-ocr.md index 2b88d7d33..774cf3cc6 100644 --- a/docs/benchmarks/vision-ocr.md +++ b/docs/benchmarks/vision-ocr.md @@ -34,7 +34,7 @@ Extract ALL text from this page as clean markdown. Preserve table structure. - **LightOnOCR-2 is the clear winner:** fastest, smallest, and produces the best-structured output with clean markdown formatting - **DeepSeek-OCR is the accuracy runner-up** with very clean text extraction but no markdown formatting - **Model size does not correlate with quality:** the 1B model beat the 8B model in both speed and accuracy -- **Vision OCR is only useful for scanned/image PDFs** — for text-based PDFs, Kreuzberg's text extraction is faster and more accurate +- **Vision OCR is only useful for scanned/image PDFs** — for text-based PDFs, Xberg's text extraction is faster and more accurate ## Limitations diff --git a/docs/usage.md b/docs/usage.md index 1a5fb3f69..ab0900de7 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -814,10 +814,17 @@ something feels off. | `LILBEE_RERANK_BLEND` | `true` | Blend reranker scores with retrieval fusion; off = the reranker's own ordering stands | | `LILBEE_HYDE` | `false` | Enable Hypothetical Document Embeddings: an LLM drafts a hypothetical answer, that's embedded, and results are merged with the original query's. Adds ~500 ms per query; helps on vague questions | | `LILBEE_HYDE_WEIGHT` | `0.7` | How much to trust HyDE results relative to the direct query (0.0-1.0) | +| `LILBEE_TITLE_SEARCH` | `false` | Match queries against document titles as a third hybrid-search arm, so a query naming a document by title surfaces its chunks. Documents indexed before this feature existed have no stored title; run `lilbee rebuild` to make them title-searchable | +| `LILBEE_TITLE_SEARCH_WEIGHT` | `0.5` | Title arm weight in rank fusion (1.0 = equal voice with the vector and text arms) | +| `LILBEE_LEXICAL_FUSION_WEIGHT` | `1.0` | BM25 arm weight in rank fusion (1.0 = equal to the vector arm; lower it when a strong embedder should dominate a corpus where the lexical arm adds noise) | +| `LILBEE_ADAPTIVE_FUSION` | `true` | Scale the BM25 arm's weight per query by how confident the vector arm is (a peaked dense ranking downweights lexical, a flat one keeps it) instead of using a fixed `LILBEE_LEXICAL_FUSION_WEIGHT`. That weight becomes the ceiling the adaptive rule scales down from. Set to `false` to pin the fixed weight | +| `LILBEE_ADAPTIVE_FUSION_MARGIN` | `0.15` | Vector-similarity margin (top hit minus the field) at which adaptive fusion fully silences the BM25 arm; smaller values downweight lexical more aggressively. `0` disables adaptation, leaving the lexical arm at its full fixed weight | +| `LILBEE_FILTER_STRUCTURAL_CHUNKS` | `false` | Drop tables-of-contents and classification-banner cover/title pages from search results. Off by default: an evaluation A/B found the filter net-negative, since its cover-page heuristic (which fires only on classified-document banners) also caught short banner-carrying body pages. When on, a query-matched or top-ranked page is never dropped. Re-validate per corpus before turning it on | | `LILBEE_ADAPTIVE_THRESHOLD` | `false` | Widen the distance threshold step by step when too few results pass, on the vector-only fallback path (no effect on the default hybrid search) | | `LILBEE_ADAPTIVE_THRESHOLD_STEP` | `0.2` | How much to widen per step when adaptive threshold triggers | | `LILBEE_TEMPORAL_FILTERING` | `true` | When the query contains temporal cues ("recent", "last week"), filter results by document date and sort by recency | | `LILBEE_MAX_CONTEXT_SOURCES` | `8` | Max chunks included in the LLM's RAG context. Raise for more coverage, lower for shorter prompts | +| `LILBEE_NEIGHBOR_EXPANSION` | `0` | Adjacent chunks pulled in on each side of every retrieved passage and merged into one contiguous excerpt, so a hit that lands mid-argument regains its surrounding text. `0` disables | | `LILBEE_EXPANSION_SHORT_QUERY_TOKENS` | `2` | Queries this short (in tokens) skip LLM query expansion | | `LILBEE_ANN_INDEX_THRESHOLD` | `50000` | Chunk count above which sync builds the ANN vector index | | `LILBEE_MAX_REASONING_CHARS` | `64000` | Cap on a reasoning model's thinking output per answer | @@ -837,6 +844,8 @@ effect on already-indexed material. | `LILBEE_CHUNK_OVERLAP` | `100` | Overlap tokens between adjacent chunks | | `LILBEE_MAX_EMBED_CHARS` | `2000` | Max characters per chunk passed to the embedder | | `LILBEE_SEMANTIC_CHUNKING` | `false` | Experimental topic-aware chunking. See [Semantic chunking](#semantic-chunking) | +| `LILBEE_TABLE_EXTRACTION` | `false` | Recognize table structure in PDFs and index each table as its own chunk, with long tables split so the header row repeats | +| `LILBEE_LAYOUT_DETECTION` | `false` | Layout-aware PDF extraction: text follows the detected reading order and running headers/footers are stripped | | `LILBEE_TOPIC_THRESHOLD` | `0.75` | Cosine boundary threshold for semantic chunking (lower = more splits) | | `LILBEE_EMBEDDING_DIM` | `768` | Embedding dimensionality. Must match the embedding model | | `LILBEE_EMBED_REPLICAS` | `1` | Embedding servers to run in parallel, one per spare GPU, for large-scale ingest. Capped at runtime by the GPUs with room after the chat model is placed | @@ -1200,7 +1209,7 @@ chunk is more likely to contain the full section that matches rather than the first half of it plus unrelated setup. **Trade-off:** Enabling semantic chunking triggers a one-time download of -kreuzberg's ONNX embedding model (separate from the chunk-to-vector embedder) +xberg's ONNX embedding model (separate from the chunk-to-vector embedder) and runs roughly 9x more downstream embedding calls during indexing. Indexing takes longer; retrieval latency is unchanged. @@ -1245,6 +1254,7 @@ vision model is configured, it takes precedence. | | Tesseract | Vision model | |---|---|---| | **Output** | Plain text | Structured markdown (tables, headings) | +| **Languages** | 100+ via `LILBEE_OCR_LANGUAGE` (e.g. `eng+deu`) | Many scripts (model-dependent) | | **Retrieval quality** | Fragments lose context | Chunks preserve semantic boundaries | | **Install** | System package (`brew`/`apt`) | Native GGUF via the built-in mtmd backend, or any vision model reachable via the SDK backend (`pip install --pre 'lilbee[litellm]'` / `uv tool install --prerelease=allow 'lilbee[litellm]'`) | | **Best for** | Simple text-only scans | Tables, multi-column layouts, formatted docs | @@ -1261,6 +1271,19 @@ brew install tesseract # macOS sudo apt install tesseract-ocr # Ubuntu/Debian ``` +By default Tesseract reads English (`eng`). To OCR other languages, set the +language codes; xberg downloads the Tesseract data on first use. Join multiple +languages with `+` (Tesseract's own convention), for example `eng+deu` for +English and German. The setting is the same value everywhere: + +```bash +export LILBEE_OCR_LANGUAGE=eng+deu # environment +lilbee set ocr_language eng+deu # CLI / persisted to config.toml +``` + +You can also set it in the `/settings` screen in the TUI, or over the HTTP and +MCP settings APIs. Commas work in place of `+` if you prefer (`eng,deu`). + ### Vision models lilbee runs vision OCR in one of two ways: diff --git a/pyproject.toml b/pyproject.toml index a88b6ee61..cd50d0f40 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,7 @@ classifiers = [ dependencies = [ "lancedb", - "kreuzberg>=4.9.1,<5", + "xberg>=1.0.0rc30", "filelock", "tree-sitter-language-pack>=1.8.0,<2.0", "typer>=0.12", @@ -56,7 +56,14 @@ dependencies = [ "huggingface_hub>=1.11.0", "psutil>=5.9", "pydantic-settings>=2.14.2", - "numpy", + # numpy <2.5: 2.5 requires Python 3.12, above lilbee's 3.11 floor. Uncapped, + # 3.12+ resolves a numpy 3.11 users never get, and its stubs use 3.12-only + # syntax mypy rejects at the 3.11 target. 2.4.x covers cp311 through cp314. + "numpy<2.5", + # Entity schema patterns are LLM-authored, so their matches need a wall-clock + # bound; regex takes a per-match timeout where the stdlib re module cannot. + # Already present transitively via tiktoken. + "regex", ] [project.optional-dependencies] diff --git a/site/index.html b/site/index.html index 15a4f1652..cde60960c 100644 --- a/site/index.html +++ b/site/index.html @@ -621,6 +621,18 @@

install

+
+ +
+ $ + curl -L https://github.com/tobocop2/lilbee/releases/latest/download/lilbee-macos-x86_64 -o lilbee && chmod +x lilbee && xattr -d com.apple.quarantine ./lilbee + +
+
+
Windows x86_64 @@ -669,6 +681,17 @@

install

+
+ +
+ $ + curl -L https://github.com/tobocop2/lilbee/releases/latest/download/lilbee-compat-macos-x86_64 -o lilbee && chmod +x lilbee && xattr -d com.apple.quarantine ./lilbee + +
+
Windows x86_64 @@ -683,7 +706,7 @@

install

@@ -787,7 +810,7 @@

crawl websites for offline search

scans & OCR

-

Old scans and photos go through OCR or a local vision model and come out as searchable markdown, layout intact.

+

Old scans and photos go through OCR in 100+ languages, or a local vision model, and come out as searchable markdown, layout intact.

remembers what you tell it

@@ -846,7 +869,7 @@

go deeper

built on

- Kreuzberg · + Xberg · llama.cpp · llama-swap · gguf-parser · diff --git a/src/lilbee/api.py b/src/lilbee/api.py index 366bbc5a7..116f98478 100644 --- a/src/lilbee/api.py +++ b/src/lilbee/api.py @@ -35,6 +35,7 @@ from lilbee.app.ingest import copy_files from lilbee.app.services import build_services, services_scope from lilbee.core.config import Config, cfg, config_scope +from lilbee.core.system import canonical_data_root from lilbee.data.store import LOCAL_OWNER, MemoryKind, MemoryRow if TYPE_CHECKING: @@ -81,7 +82,7 @@ def __init__( if config is not None: self._config = config elif documents_dir is not None: - root = Path(documents_dir).resolve() + root = canonical_data_root(documents_dir) self._config = cfg.model_copy( update={ "data_root": root, diff --git a/src/lilbee/app/services.py b/src/lilbee/app/services.py index 5a28a00ce..aaacb2852 100644 --- a/src/lilbee/app/services.py +++ b/src/lilbee/app/services.py @@ -135,11 +135,11 @@ class _ServicesState: """The cached process-global singleton plus the per-task scoped override. ``singleton`` is set on first ``get_services()`` call. Concurrency - contract: lilbee runs the asyncio loop on a single worker thread + - Textual's main thread; ``get_services()`` is idempotent (the early-out - covers re-entry from a background thread), and the Services dataclass is - logically immutable post-construction, so concurrent reads are safe - without a lock. Tests that need a custom container call + contract: creation is serialized by ``_singleton_create_lock`` (several + worker threads can first-touch services at once, and a duplicate build + would collide in xberg's process-global backend registry), and the + Services dataclass is logically immutable post-construction, so concurrent + reads are safe without a lock. Tests that need a custom container call ``set_services(make_mock_services(...))``; ``peek_services()`` is the read-only inspector for cleanup fixtures. @@ -169,12 +169,20 @@ def build_services( ``get_services()`` calls this with the process-global cfg; the library API calls it per instance. Service modules are imported inside the function to - keep CLI startup fast (they transitively pull in lancedb / kreuzberg). Pass + keep CLI startup fast (they transitively pull in lancedb / xberg). Pass *provider* to reuse a caller-supplied one; otherwise it is built from *config* via the provider factory. Pass *registry* to reuse one already built (get_services builds it for embedding-dim reconciliation). Embedding-dim reconciliation is a global-cfg concern owned by :func:`get_services`, not done here. + + Side effect: registers *provider* as xberg's process-global OCR, embedding, and + tokenizer backend (:func:`sync_vision_ocr_backend` / :func:`sync_embedding_backend` + / :func:`sync_tokenizer_backend`) so scanned-page OCR, semantic-chunk boundary + detection, and token-budgeted chunk sizing route through it. xberg's registry is a + single global slot, so the most recently built container wins; this is why + registration lives here (every container, singleton or per-instance library, binds + its own provider) rather than only in :func:`get_services`. """ from lilbee.catalog.hf_client import HfClient from lilbee.data.store import Store @@ -190,6 +198,14 @@ def build_services( from lilbee.runtime.ingest_lock import IngestLockRegistry provider = provider or create_provider(config) + # Register this provider as xberg's OCR + embedding + tokenizer backend so + # scanned-page OCR, semantic-chunk boundary detection, and token-budgeted chunk + # sizing route through it. Bound here rather than in get_services so the library + # API's per-instance containers register too; each backend reads the live cfg and + # re-binds whenever the provider is rebuilt. + sync_vision_ocr_backend(provider) + sync_embedding_backend(provider) + sync_tokenizer_backend(provider) registry = registry or ModelRegistry(config.models_dir) store = Store(config) embedder = Embedder(config, provider) @@ -223,6 +239,18 @@ def build_services( ) +# Serializes first-touch singleton creation: several worker threads (e.g. +# concurrent downloads) can call get_services() before the singleton exists, +# and a duplicate build re-registers xberg's process-global backends mid-flight, +# raising 'already registered' in the losing thread. +_singleton_create_lock = threading.Lock() + +# Makes each xberg registry re-bind (unregister + register) atomic, so +# concurrent binds serialize and the most recently built provider wins instead +# of colliding on 'already registered'. +_backend_bind_lock = threading.Lock() + + def get_services() -> Services: """Return the active container: a scoped override if set, else the cached singleton. @@ -237,26 +265,30 @@ def get_services() -> Services: if _state.singleton is not None: return _state.singleton - from lilbee.app.settings import reconcile_embedding_dim - from lilbee.core.config import cfg - from lilbee.modelhub.registry import ModelRegistry - - registry = ModelRegistry(cfg.models_dir) - # Pin the store width to the embedder before Store(); pass the registry so - # resolution doesn't re-enter this half-built get_services. - reconcile_embedding_dim(registry) - _state.singleton = build_services(cfg, registry=registry) - # Eager start is the default: pay the spawn cost per role server at TUI mount - # so the first user action lands on a warm fleet. Roles whose model is unset - # are skipped, so a setup with only chat + embed never spawns rerank or - # vision. Set ``cfg.worker_pool_eager_start = false`` for headless scripts - # where mount time matters more than first-call latency. - if cfg.worker_pool_eager_start: - from contextlib import suppress - - with suppress(Exception): - _state.singleton.provider.warm_up_pool() - return _state.singleton + with _singleton_create_lock: + if _state.singleton is not None: + return _state.singleton + + from lilbee.app.settings import reconcile_embedding_dim + from lilbee.core.config import cfg + from lilbee.modelhub.registry import ModelRegistry + + registry = ModelRegistry(cfg.models_dir) + # Pin the store width to the embedder before Store(); pass the registry so + # resolution doesn't re-enter this half-built get_services. + reconcile_embedding_dim(registry) + _state.singleton = build_services(cfg, registry=registry) + # Eager start is the default: pay the spawn cost per role server at TUI mount + # so the first user action lands on a warm fleet. Roles whose model is unset + # are skipped, so a setup with only chat + embed never spawns rerank or + # vision. Set ``cfg.worker_pool_eager_start = false`` for headless scripts + # where mount time matters more than first-call latency. + if cfg.worker_pool_eager_start: + from contextlib import suppress + + with suppress(Exception): + _state.singleton.provider.warm_up_pool() + return _state.singleton @contextmanager @@ -274,6 +306,94 @@ def services_scope(services: Services) -> Iterator[None]: _state.override.reset(token) +def sync_vision_ocr_backend(provider: LLMProvider) -> None: + """Register or unregister lilbee's vision model as xberg's OCR backend. + + Driven by ``cfg.vision_model``: registered while a model is set, removed when + cleared. The backend reads ``cfg.vision_model`` live, so a model swap needs no + re-registration, but it captures ``provider.vision_ocr``, so it must re-bind + whenever the provider is rebuilt (``reset_services``) -- otherwise the global + xberg registry keeps routing OCR to the shut-down provider. + """ + from xberg import list_ocr_backends, register_ocr_backend, unregister_ocr_backend + + from lilbee.core.config import cfg + from lilbee.data.ingest.types import OcrBackendName + from lilbee.data.ingest.vision_ocr_backend import VisionOcrBackend + + with _backend_bind_lock: + registered = OcrBackendName.LILBEE_VISION in list_ocr_backends() + if cfg.vision_model: + # Re-register so the backend always binds to the current provider. + if registered: + unregister_ocr_backend(OcrBackendName.LILBEE_VISION) + register_ocr_backend( + VisionOcrBackend(ocr_fn=provider.vision_ocr, model_ref_fn=lambda: cfg.vision_model) + ) + elif registered: + unregister_ocr_backend(OcrBackendName.LILBEE_VISION) + + +def sync_embedding_backend(provider: LLMProvider) -> None: + """Register lilbee's embedder as xberg's plugin embedding backend. + + The semantic chunker uses it to detect topic boundaries with the same model + that vectorizes chunks, instead of xberg's bundled ONNX preset. The backend + reads ``cfg.embedding_dim`` live and routes through ``provider.embed`` (which + resolves the embedding model per call), so a model swap needs no + re-registration -- but it captures ``provider.embed``, so it must re-bind + whenever the provider is rebuilt (``reset_services``). + """ + from xberg import ( + list_embedding_backends, + register_embedding_backend, + unregister_embedding_backend, + ) + + from lilbee.core.config import cfg + from lilbee.data.embedding_backend import LilbeeEmbeddingBackend + from lilbee.data.ingest.types import EmbeddingBackendName + + with _backend_bind_lock: + if EmbeddingBackendName.LILBEE in list_embedding_backends(): + unregister_embedding_backend(EmbeddingBackendName.LILBEE) + register_embedding_backend( + LilbeeEmbeddingBackend(embed_fn=provider.embed, dim_fn=lambda: cfg.embedding_dim) + ) + + +def sync_tokenizer_backend(provider: LLMProvider) -> None: + """Register lilbee's embedder tokenizer as xberg's chunk-sizing backend. + + Driven by ``cfg.token_sizing``: registered while it is on, removed when off. + Gated rather than always-on because xberg validates a tokenizer backend at + registration by probing ``count_tokens``, so registering only when the feature + is on keeps that probe (and its embedder round-trip) off the default path. The + backend captures ``provider.count_tokens``, so it must re-bind whenever the + provider is rebuilt (``reset_services``) -- otherwise xberg's registry keeps + counting with the shut-down provider. + """ + from xberg import ( + list_tokenizer_backends, + register_tokenizer_backend, + unregister_tokenizer_backend, + ) + + from lilbee.core.config import cfg + from lilbee.data.ingest.types import TokenizerBackendName + from lilbee.data.tokenizer_backend import LilbeeTokenizerBackend + + with _backend_bind_lock: + registered = TokenizerBackendName.LILBEE in list_tokenizer_backends() + if cfg.token_sizing: + # Re-register so the backend always binds to the current provider. + if registered: + unregister_tokenizer_backend(TokenizerBackendName.LILBEE) + register_tokenizer_backend(LilbeeTokenizerBackend(count_fn=provider.count_tokens)) + elif registered: + unregister_tokenizer_backend(TokenizerBackendName.LILBEE) + + def set_services(services: Services | None) -> None: """Replace the cached Services singleton (for testing).""" _state.singleton = services diff --git a/src/lilbee/app/settings.py b/src/lilbee/app/settings.py index e47c2f20b..4030c7499 100644 --- a/src/lilbee/app/settings.py +++ b/src/lilbee/app/settings.py @@ -277,6 +277,12 @@ def _reload_changed_roles(changed_keys: set[str]) -> None: changed_role_fields = changed_keys & MODEL_ROLE_FIELDS for field in changed_role_fields: services.reload_role(MODEL_FIELD_TO_ROLE[field]) + if "vision_model" in changed_role_fields: + # Register/unregister lilbee's xberg OCR backend on any vision-model + # change (REST/MCP/TUI/CLI all funnel here), not just the REST route. + from lilbee.app.services import sync_vision_ocr_backend + + sync_vision_ocr_backend(services.provider) role_agnostic = (changed_keys & LOAD_AFFECTING_KEYS) - MODEL_ROLE_FIELDS if role_agnostic: services.provider.drop_loaded_models_async() @@ -318,6 +324,15 @@ def _invalidate_caches(changed_keys: set[str]) -> None: if changed_keys & LOAD_AFFECTING_KEYS: # heavy: app.services pulls the provider stack + lancedb (~70 ms) _reload_changed_roles(changed_keys) + if "token_sizing" in changed_keys: + # Register/unregister lilbee's xberg tokenizer backend when token_sizing is + # toggled (via any settings path), so chunk sizing picks up the change + # without waiting for a services rebuild. + from lilbee.app.services import peek_services, sync_tokenizer_backend + + services = peek_services() + if services is not None: + sync_tokenizer_backend(services.provider) if changed_keys & PROVIDER_API_KEYS: # heavy: sdk_llm_provider pulls litellm fanout (~145 ms) from lilbee.providers.sdk_llm_provider import inject_provider_keys diff --git a/src/lilbee/app/settings_map.py b/src/lilbee/app/settings_map.py index eaba796e6..5a9978d94 100644 --- a/src/lilbee/app/settings_map.py +++ b/src/lilbee/app/settings_map.py @@ -161,6 +161,27 @@ def get_default(key: str) -> object: group=SettingGroup.INGEST, help_text="Topic-boundary similarity threshold, 0.0-1.0, used when semantic chunking is on", ), + "token_sizing": SettingDef( + bool, + nullable=False, + group=SettingGroup.INGEST, + help_text="Size chunks by real embedder tokens, not chars (changes invalidate the index)", + ), + "table_extraction": SettingDef( + bool, + nullable=False, + group=SettingGroup.INGEST, + help_text="Index each extracted table as its own chunk (changes invalidate the index)", + ), + "layout_detection": SettingDef( + bool, + nullable=False, + group=SettingGroup.INGEST, + help_text=( + "Layout-aware PDF extraction: reading order plus header/footer " + "stripping (changes invalidate the index)" + ), + ), "embedding_model": SettingDef( str, nullable=False, @@ -782,6 +803,12 @@ def get_default(key: str) -> object: group=SettingGroup.INGEST, help_text="Per-page Tesseract timeout in seconds (used when no vision model is set)", ), + "ocr_language": SettingDef( + list, + nullable=False, + group=SettingGroup.INGEST, + help_text="Tesseract OCR languages when no vision model is set; '+'-join, e.g. eng+deu", + ), "worker_pool_eager_start": SettingDef( bool, nullable=False, @@ -857,6 +884,42 @@ def get_default(key: str) -> object: group=SettingGroup.RETRIEVAL, help_text="Candidate-pool multiplier over top_k before reranking", ), + "title_search": SettingDef( + bool, + nullable=False, + group=SettingGroup.RETRIEVAL, + help_text="Match queries against document titles as a third hybrid-search arm", + ), + "title_search_weight": SettingDef( + float, + nullable=False, + group=SettingGroup.RETRIEVAL, + help_text="Title arm weight in rank fusion (1.0 = equal voice with the other arms)", + ), + "lexical_fusion_weight": SettingDef( + float, + nullable=False, + group=SettingGroup.RETRIEVAL, + help_text="BM25 arm weight in fusion (1.0 = equal to vector; lower to favor dense)", + ), + "adaptive_fusion": SettingDef( + bool, + nullable=False, + group=SettingGroup.RETRIEVAL, + help_text="Scale the BM25 weight per query by vector-arm confidence, not a fixed value", + ), + "adaptive_fusion_margin": SettingDef( + float, + nullable=False, + group=SettingGroup.RETRIEVAL, + help_text="Vector-similarity margin at which adaptive fusion fully silences the BM25 arm", + ), + "filter_structural_chunks": SettingDef( + bool, + nullable=False, + group=SettingGroup.RETRIEVAL, + help_text="Drop tables-of-contents and classification-banner cover pages from results", + ), "history_rewrite": SettingDef( bool, nullable=False, @@ -903,6 +966,12 @@ def get_default(key: str) -> object: group=SettingGroup.RETRIEVAL, help_text="Maximum unique sources contributing chunks to a single answer", ), + "neighbor_expansion": SettingDef( + int, + nullable=False, + group=SettingGroup.RETRIEVAL, + help_text="Adjacent chunks merged into each retrieved passage per side (0 = off)", + ), "diversity_max_per_source": SettingDef( int, nullable=False, diff --git a/src/lilbee/cli/app.py b/src/lilbee/cli/app.py index 2fb7d5170..920524738 100644 --- a/src/lilbee/cli/app.py +++ b/src/lilbee/cli/app.py @@ -65,7 +65,14 @@ def _apply_data_root(root: Path) -> None: Exporting the env var keeps spawn-context worker subprocesses on the same data root after their fresh ``import lilbee``. + + Canonicalized here because this bypasses ``Config._resolve_defaults`` and + derives its own children, so a symlinked or relative ``--data-dir`` would + otherwise key a different lock than another process on the same directory. """ + from lilbee.core.system import canonical_data_root + + root = canonical_data_root(root) cfg.data_root = root cfg.documents_dir = root / "documents" cfg.data_dir = root / "data" diff --git a/src/lilbee/cli/commands/ingest_sync.py b/src/lilbee/cli/commands/ingest_sync.py index e827106bf..b85564044 100644 --- a/src/lilbee/cli/commands/ingest_sync.py +++ b/src/lilbee/cli/commands/ingest_sync.py @@ -400,6 +400,7 @@ def index( apply_overrides(data_dir=data_dir, use_global=use_global) store = get_services().store store.ensure_fts_index() + store.ensure_scalar_indexes() built = store.ensure_vector_index(force=True) if cfg.json_mode: json_output({"command": "index", "vector_index": built}) diff --git a/src/lilbee/cli/tui/__init__.py b/src/lilbee/cli/tui/__init__.py index 5a702d125..60d3d13fa 100644 --- a/src/lilbee/cli/tui/__init__.py +++ b/src/lilbee/cli/tui/__init__.py @@ -45,14 +45,14 @@ def _redirect_native_stderr_to(log_path: Path) -> _StderrRedirect | None: Textual's Linux/macOS driver writes its alternate-screen ANSI to ``sys.__stderr__`` (see ``textual.drivers.linux_driver.LinuxDriver.write``). Native deps - like kreuzberg's vendored tesseract write directly to fd 2 and leak + like xberg's vendored tesseract write directly to fd 2 and leak onto the same buffer, e.g. "Detected N diacritics", which corrupts the TUI. Strategy: dup the original fd 2 to a saved fd, repoint ``sys.__stderr__`` and ``sys.stderr`` at that saved fd so Textual keeps drawing to the real terminal, then dup2 fd 2 itself to the - log file so any fd-2 writer (kreuzberg, tesseract, poppler) lands + log file so any fd-2 writer (xberg, tesseract, poppler) lands in ``tui.log`` instead of on top of the screen. """ try: diff --git a/src/lilbee/cli/tui/screens/catalog.py b/src/lilbee/cli/tui/screens/catalog.py index fb8a7641d..91955741c 100644 --- a/src/lilbee/cli/tui/screens/catalog.py +++ b/src/lilbee/cli/tui/screens/catalog.py @@ -407,7 +407,7 @@ def compose(self) -> ComposeResult: with Horizontal(id="catalog-body"): with ( Container(id="catalog-tabs-wrap"), - TabbedContent(initial=TAB_CHAT, id="catalog-tabs"), + TabbedContent(id="catalog-tabs"), ): with TabPane(msg.CATALOG_TAB_DISCOVER, id=TAB_DISCOVER): yield DiscoverRails(id="discover-rails") @@ -443,13 +443,10 @@ def compose(self) -> ComposeResult: def on_mount(self) -> None: self._fetch_installed_names() - # Force Chat as the initial active tab. `TabbedContent(initial=...)` - # doesn't take effect when panes are added via `with TabPane(...)` - # (Textual resolves initial at construction time but the panes mount - # after), so we set active explicitly via call_after_refresh so the - # TabActivated cascade has already settled before our setter runs. - # Chat is the most common landing destination; users opt into - # Discover via keyboard shortcut. + # `TabbedContent(initial=...)` is unusable under `with TabPane(...)`: the + # strip composes empty so the id never resolves, and it still arms + # `Tabs._on_mount`'s unguarded forced-active set, which raises "No Tab + # with id" when the tab children mount late. Activate Chat here instead. self.call_after_refresh(self._activate_initial_tab) self.add_class("-grid-view") diff --git a/src/lilbee/core/config/model.py b/src/lilbee/core/config/model.py index a12647cfa..d26316fc5 100644 --- a/src/lilbee/core/config/model.py +++ b/src/lilbee/core/config/model.py @@ -95,10 +95,11 @@ class Config(BaseSettings): max_embed_chars: int = Field(default=2000, ge=1) top_k: int = ConfigField(default=12, ge=1, writable=True) max_distance: float = ConfigField(default=0.75, ge=0.0, writable=True) - # Abstention floor against the canonical [0, 1] relevance score - # (0.0 = no filtering). When every retrieved chunk falls below it, ask - # refuses instead of feeding noise as context. On the fused reciprocal-rank - # scale an arm's top hit scores 0.5, so useful floors start around 0.4. + # Abstention floor against the [0, 1] fused relevance score (0.0 = no + # filtering). When every retrieved chunk falls below it, ask refuses instead + # of feeding noise as context. The fused score normalizes against the + # configured weight budget (a constant), so an arm's top hit scores a stable + # share of it; useful floors start around 0.4. Tune against your own corpus. min_relevance_score: float = ConfigField(default=0.0, ge=0.0, writable=True) adaptive_threshold: bool = ConfigField(default=False, writable=True) rag_system_prompt: str = ConfigField( @@ -136,6 +137,10 @@ class Config(BaseSettings): # Tesseract fallback wall-clock timeout per file, seconds. 0 = no cap. tesseract_timeout: float = ConfigField(default=60.0, ge=0.0, writable=True) + # Tesseract OCR language codes for the scanned-document fallback (used when no + # vision model is set), e.g. ["eng"] or ["eng", "deu"]. Set via env as + # LILBEE_OCR_LANGUAGE="eng+deu". xberg requires a non-empty list. + ocr_language: list[str] = ConfigField(default_factory=lambda: ["eng"], writable=True) # Opt-in typed entity extraction (an entities table for exact counting # and cross-referencing). Fully automatic: sync induces a schema from the # corpus and extracts across the index; new files extract at ingest. Off @@ -144,6 +149,24 @@ class Config(BaseSettings): entity_extraction: bool = ConfigField(default=False, writable=True) semantic_chunking: bool = ConfigField(default=False, writable=True) topic_threshold: float = ConfigField(default=0.75, ge=0.0, le=1.0, writable=True) + # Size chunk budgets (chunk_size/chunk_overlap) in real tokens from the + # embedder's own tokenizer via xberg's registered tokenizer backend, instead of + # the chars-per-token heuristic. Off by default: turning it on re-partitions + # chunks, so a library must be reindexed. Applies to the plain and heading + # chunkers; the semantic chunker sizes by characters and ignores it. + token_sizing: bool = ConfigField(default=False, writable=True, reindex=True) + # Index each extracted table as its own chunk: xberg recognizes table + # structure during extraction and the markdown serialization is embedded + # alongside the prose chunks, so tabular data is retrievable as a unit. + # Off by default: recognition costs extraction time, and toggling changes + # what gets indexed, so a library must be reindexed. + table_extraction: bool = ConfigField(default=False, writable=True, reindex=True) + # Layout-aware PDF extraction: xberg's layout detection orders page text + # by detected reading order (multi-column PDFs stop interleaving) and the + # running header/footer bands are stripped. Off by default: detection runs + # an extra model pass per page, and toggling changes extracted text, so a + # library must be reindexed. + layout_detection: bool = ConfigField(default=False, writable=True, reindex=True) server_host: str = "127.0.0.1" server_port: int = Field(default=0, ge=0, le=65535) cors_origins: list[str] = Field(default_factory=list) @@ -194,6 +217,42 @@ class Config(BaseSettings): # fusion arms stay exactly top_k deep. candidate_multiplier: int = ConfigField(default=3, ge=1, writable=True) + # Third lexical arm in hybrid search: BM25 over document titles, fused with + # the vector and chunk arms so a query naming a document by title surfaces + # its chunks. Off by default until the eval harness measures it. + title_search: bool = ConfigField(default=False, writable=True) + + # Title arm weight relative to a full arm in rank fusion (1.0 = equal voice + # with the vector and chunk arms). + title_search_weight: float = ConfigField(default=0.5, ge=0.0, le=1.0, writable=True) + + # Lexical (BM25) arm weight relative to the vector arm in rank fusion. + # 1.0 gives the two arms equal voice; lowering it lets + # a strong dense embedder dominate on corpora where the lexical arm adds + # noise rather than signal. The right value is corpus-dependent and set by + # the retrieval benchmark, not guessed here. + lexical_fusion_weight: float = ConfigField(default=1.0, ge=0.0, le=1.0, writable=True) + + # Adaptive fusion: scale the BM25 arm per query by vector-arm confidence + # instead of a fixed lexical_fusion_weight (a peaked dense ranking downweights + # lexical, a flat one keeps it). On by default. lexical_fusion_weight is the + # ceiling the rule scales down from. Set adaptive_fusion=false to pin the + # fixed weight. + adaptive_fusion: bool = ConfigField(default=True, writable=True) + + # Vector-similarity margin at which the lexical arm is fully silenced; smaller + # = more aggressive downweighting. 0 disables adaptation entirely (the lexical + # arm keeps its full fixed weight). + adaptive_fusion_margin: float = ConfigField(default=0.15, ge=0.0, le=2.0, writable=True) + + # Drop tables-of-contents and classification-banner cover/title pages from + # search results. OFF by default: an evaluation A/B on a government-document + # corpus found the filter net-negative, because its cover-page heuristic also + # fires on short banner-carrying body pages. When on, a query-matched or + # top-ranked page is never dropped (searcher.search), so removal is limited to + # structural chunks the query did not hit. Re-validate per corpus before use. + filter_structural_chunks: bool = ConfigField(default=False, writable=True) + # Chunk count at/above which sync builds an approximate (ANN) vector index # so search stays fast at millions of vectors. Below this, search uses exact # flat scan (faster and exact for small vaults). 0 disables the ANN index. @@ -243,6 +302,15 @@ class Config(BaseSettings): # Chunks included in LLM context after adaptive selection. max_context_sources: int = ConfigField(default=8, ge=1, writable=True) + # Adjacent chunks pulled from the same source on each side of every + # selected chunk and merged into one contiguous passage, so a hit that + # lands mid-argument regains the text before and after it. 0 disables. + # Capped: it is a small chunk radius (useful values are single digits), and + # the merged text is token-budget-bounded anyway, so a large value only + # inflates per-query fetch cost -- and a misread as a token count (e.g. + # 50000) would build a megabyte-long IN-predicate per source. + neighbor_expansion: int = ConfigField(default=0, ge=0, le=100, writable=True) + # HyDE (Gao et al. 2022): hypothetical-answer embedding search. +~500ms. hyde: bool = ConfigField(default=False, writable=True) @@ -760,6 +828,24 @@ def _parse_enable_ocr(cls, v: Any) -> bool | None: pass # fall through to bool() coercion below for unrecognised strings return bool(v) + @field_validator("ocr_language", mode="before") + @classmethod + def _parse_ocr_language(cls, v: Any) -> list[str]: + """Accept a list or a ``+``/comma/newline-separated string; never empty. + + Tesseract joins languages with ``+`` (e.g. ``eng+deu``), so that is the + canonical user-facing form. Commas are also accepted. Newlines are + accepted because ``app.settings`` joins list values with ``\\n`` when it + persists them to config.toml; without splitting on it a multi-language + value would reload as one malformed token. Blank input falls back to + English, since xberg errors on an empty list. + """ + if isinstance(v, str): + v = v.replace("+", ",").replace("\n", ",").split(",") + items = v or [] + langs = [s.strip() for s in items if isinstance(s, str) and s.strip()] + return langs or ["eng"] + @field_validator("flash_attention", mode="before") @classmethod def _parse_flash_attention(cls, v: Any) -> bool | None: @@ -973,11 +1059,20 @@ def _parse_ent_types(cls, v: Any) -> frozenset[str]: @model_validator(mode="before") @classmethod def _resolve_defaults(cls, data: Any) -> Any: - from lilbee.core.system import canonical_models_dir, default_data_dir, find_local_root + from lilbee.core.system import ( + canonical_data_root, + canonical_models_dir, + default_data_dir, + find_local_root, + ) if not isinstance(data, dict): return data + # An empty LILBEE_DATA_ROOT (delivered as "") must fall through to default + # resolution like an unset one, not become Path(".") = the process cwd. + if isinstance(data.get("data_root"), str) and not data["data_root"].strip(): + data["data_root"] = None if data.get("data_root") in (None, _UNSET_PATH): data_env = os.environ.get("LILBEE_DATA", "").strip() if data_env: @@ -985,7 +1080,11 @@ def _resolve_defaults(cls, data: Any) -> Any: else: local = find_local_root() data["data_root"] = local if local is not None else default_data_dir() - root = data["data_root"] + # Every child path below derives from this, and the server lock keys on + # those, so canonicalizing here is what makes one directory key one lock. + # Also coerces a raw string (LILBEE_DATA_ROOT) to Path. + root = canonical_data_root(data["data_root"]) + data["data_root"] = root if data.get("documents_dir") in (None, _UNSET_PATH): data["documents_dir"] = root / "documents" if data.get("data_dir") in (None, _UNSET_PATH): @@ -1006,15 +1105,19 @@ def settings_customise_sources( dotenv_settings: Any, file_secret_settings: Any, ) -> tuple[Any, ...]: - from lilbee.core.system import default_data_dir, find_local_root + from lilbee.core.system import canonical_data_root, default_data_dir, find_local_root - data_env = os.environ.get("LILBEE_DATA", "") + # .strip() to match _resolve_defaults; a padded value would otherwise + # send the root and its config.toml to different directories. + data_env = os.environ.get("LILBEE_DATA", "").strip() if data_env: toml_dir = Path(data_env) else: local = find_local_root() toml_dir = local if local else default_data_dir() - toml_path = toml_dir / "config.toml" + # Same call as the root itself, so this looks where the root resolves to; + # a "~/lilbee" value would otherwise search a literal ./~ and find nothing. + toml_path = canonical_data_root(toml_dir) / "config.toml" plain_env = _PlainEnvSource(settings_cls, env_prefix="LILBEE_", env_ignore_empty=True) sources: list[Any] = [init_settings, plain_env] diff --git a/src/lilbee/core/system.py b/src/lilbee/core/system.py index 1a22d3583..a54ebdb86 100644 --- a/src/lilbee/core/system.py +++ b/src/lilbee/core/system.py @@ -66,6 +66,21 @@ def find_local_root(start: Path | None = None) -> Path | None: return None +def canonical_data_root(root: Path | str) -> Path: + """Resolve a data root to one canonical path. + + Session file, port file, and write lock all derive from the data root, so + two spellings of one directory key two locks. Symlinks, relative paths, a + leading ``~``, and macOS ``/var`` vs ``/private/var`` each produce a pair. + A root that does not exist yet resolves to where it will be created. + + Uses ``os.path`` rather than ``Path.expanduser().resolve()``: ``resolve`` + rebuilds via ``type(self)``, which raises for a ``PosixPath`` that exists + on Windows (``Path()`` picks its flavour from ``os.name``, which tests patch). + """ + return Path(os.path.realpath(os.path.expanduser(os.fspath(root)))) + + def canonical_models_dir() -> Path: """Return the shared models directory (always in the platform default, never per-project). Multiple lilbee instances share this directory so models are downloaded once. diff --git a/src/lilbee/data/chunk.py b/src/lilbee/data/chunk.py index f797cf382..79945e9da 100644 --- a/src/lilbee/data/chunk.py +++ b/src/lilbee/data/chunk.py @@ -2,21 +2,20 @@ from __future__ import annotations -import os +from dataclasses import replace from typing import TYPE_CHECKING from lilbee.core.config import active_config if TYPE_CHECKING: - from kreuzberg import ChunkingConfig + from xberg import ChunkingConfig, ChunkSizing, EmbeddingConfig, TableChunkingMode +# Char->token ratio for English. Sole owner: engine_params, embedder, +# code_chunker and history_window import it rather than restating it. CHARS_PER_TOKEN = 4 _SEMANTIC_CHUNKER = "semantic" _MARKDOWN_CHUNKER = "markdown" -# Kreuzberg silently falls back to a non-semantic path when embedding is None. -_SEMANTIC_EMBEDDING_PRESET = "fast" -_DISABLE_PROGRESS_ENV = "HF_HUB_DISABLE_PROGRESS_BARS" def _char_budget() -> tuple[int, int]: @@ -27,35 +26,86 @@ def _char_budget() -> tuple[int, int]: return max_chars, max_overlap -def _show_download_progress() -> bool: - """Honor lilbee's global progress-bar suppression (set for quiet/JSON modes). +def _size_params() -> tuple[int, int, ChunkSizing | None]: + """Return (max, overlap, sizing) for the plain and heading chunkers. - lilbee defaults ``HF_HUB_DISABLE_PROGRESS_BARS`` on in ``__init__``; mirroring - it here keeps the embedding-model download silent instead of hardcoding a bar - that would corrupt JSON output. + With ``cfg.token_sizing`` on, the budget is a raw token count and ``sizing`` + routes to lilbee's registered tokenizer backend, so ``chunk_size`` is a real + token ceiling. Otherwise the character heuristic with no sizing (xberg's default + character sizer). The semantic chunker does not use this -- it sizes by + characters and ignores ChunkSizing.""" + config = active_config() + if config.token_sizing: + from xberg import ChunkSizing + + from lilbee.data.ingest.types import TokenizerBackendName + + overlap = min(config.chunk_overlap, config.chunk_size // 2) + return ( + config.chunk_size, + overlap, + ChunkSizing(type="tokenizer", model=TokenizerBackendName.LILBEE), + ) + max_chars, max_overlap = _char_budget() + return max_chars, max_overlap, None + + +def _semantic_embedding_config() -> EmbeddingConfig: + """EmbeddingConfig for semantic chunking. Boundary-detection embeddings route to + lilbee's embedder, registered as xberg's plugin backend in + ``app.services.sync_embedding_backend``, so the model that vectorizes chunks for + retrieval is the one that decides where they split.""" + from xberg import EmbeddingConfig, EmbeddingModelType + + # Lazy: importing ingest.types at module scope cycles back through chunk.py. + from lilbee.data.ingest.types import EmbeddingBackendName + + model = EmbeddingModelType.plugin(EmbeddingBackendName.LILBEE) + return EmbeddingConfig(model=model) + + +def _table_chunking() -> TableChunkingMode | None: + """Header-repeating table splits when table extraction is on, else None for + xberg's default. + + REPEAT_HEADER carries the header row into every piece of a long table, so + no chunk holds headerless rows. """ - return os.environ.get(_DISABLE_PROGRESS_ENV, "0").lower() not in ("1", "true") + config = active_config() + if not config.table_extraction: + return None + from xberg import TableChunkingMode + return TableChunkingMode.REPEAT_HEADER -def build_chunking_config(*, use_semantic: bool = True) -> ChunkingConfig: - """Build a kreuzberg ChunkingConfig from the current cfg.""" - from kreuzberg import ChunkingConfig, EmbeddingConfig, EmbeddingModelType - max_chars, max_overlap = _char_budget() +def build_chunking_config(*, use_semantic: bool = True) -> ChunkingConfig: + """Build an xberg ChunkingConfig from the current cfg.""" + from xberg import ChunkingConfig config = active_config() if use_semantic and config.semantic_chunking: - return ChunkingConfig( + # The semantic chunker sizes by characters and ignores ChunkSizing, so it + # stays on the character budget regardless of cfg.token_sizing. + max_chars, max_overlap = _char_budget() + chunking = ChunkingConfig( chunker_type=_SEMANTIC_CHUNKER, - embedding=EmbeddingConfig( - model=EmbeddingModelType.preset(_SEMANTIC_EMBEDDING_PRESET), - show_download_progress=_show_download_progress(), - ), + embedding=_semantic_embedding_config(), topic_threshold=config.topic_threshold, - max_chars=max_chars, - max_overlap=max_overlap, + max_characters=max_chars, + overlap=max_overlap, ) - return ChunkingConfig(max_chars=max_chars, max_overlap=max_overlap) + else: + max_size, max_overlap, sizing = _size_params() + chunking = ChunkingConfig( + max_characters=max_size, + overlap=max_overlap, + sizing=sizing, + ) + # table_chunking has no "unset" value on xberg's frozen ChunkingConfig, so the + # field is left at its default rather than overwritten with None. + mode = _table_chunking() + return chunking if mode is None else replace(chunking, table_chunking=mode) def chunk_text( @@ -69,21 +119,24 @@ def chunk_text( if not text or not text.strip(): return [] - from kreuzberg import ChunkingConfig, ExtractionConfig, extract_bytes_sync + from xberg import ChunkingConfig, ExtractionConfig + + from lilbee.data.xberg_extract import extract_document if heading_context: - max_chars, max_overlap = _char_budget() + max_size, max_overlap, sizing = _size_params() chunking = ChunkingConfig( - max_chars=max_chars, - max_overlap=max_overlap, + max_characters=max_size, + overlap=max_overlap, + sizing=sizing, chunker_type=_MARKDOWN_CHUNKER, - prepend_heading_context=True, # type: ignore[call-arg] + prepend_heading_context=True, ) else: chunking = build_chunking_config(use_semantic=use_semantic) config = ExtractionConfig(chunking=chunking) - result = extract_bytes_sync(text.encode("utf-8"), mime_type, config=config) - if result.chunks: - return [c.content for c in result.chunks] + doc = extract_document(text.encode("utf-8"), mime_type, config=config) + if doc.chunks: + return [c.content for c in doc.chunks] return [] diff --git a/src/lilbee/data/code_chunker.py b/src/lilbee/data/code_chunker.py index 91a391ebd..2b92dfbf6 100644 --- a/src/lilbee/data/code_chunker.py +++ b/src/lilbee/data/code_chunker.py @@ -20,7 +20,7 @@ ) from lilbee.core.config import cfg -from lilbee.data.chunk import chunk_text +from lilbee.data.chunk import CHARS_PER_TOKEN, chunk_text log = logging.getLogger(__name__) @@ -176,7 +176,11 @@ def chunk_code(file_path: Path, source_name: str | None = None) -> list[CodeChun structure=True, symbols=True, docstrings=True, - chunk_max_size=cfg.chunk_size, + # tree-sitter documents chunk_max_size in bytes; cfg.chunk_size is + # a token budget, so it converts exactly like the text path's char + # budget. Without this, code split ~4x smaller than prose and the + # parser and fallback paths disagreed for the same file. + chunk_max_size=cfg.chunk_size * CHARS_PER_TOKEN, ) result = process(source_text, config) # type: ignore[arg-type] # tslp 1.8.0 typing bug, see init() above except Exception: diff --git a/src/lilbee/data/embedding_backend.py b/src/lilbee/data/embedding_backend.py new file mode 100644 index 000000000..ff7f1f7ab --- /dev/null +++ b/src/lilbee/data/embedding_backend.py @@ -0,0 +1,50 @@ +"""lilbee's embedder exposed as a xberg plugin embedding backend. + +xberg's semantic chunker needs embeddings to detect topic boundaries. Registering +this backend routes those embeddings to lilbee's own embedder (whatever model the +fleet serves), so the model that vectorizes chunks for retrieval is the same one +that decides where chunks split. Without it, xberg falls back to its bundled ONNX +preset, a different model. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from lilbee.data.ingest.types import EmbeddingBackendName + +if TYPE_CHECKING: + from collections.abc import Callable + + +class LilbeeEmbeddingBackend: + """Routes xberg's boundary-detection embeddings to lilbee's embedder. + + The functional path is injected: ``embed_fn`` is the provider's batch embed + (read live, so an embedding-model swap needs no re-registration) and + ``dim_fn`` reports the current vector width. xberg calls ``initialize`` once + at registration, then ``dimensions`` and ``embed`` while chunking. Methods are + sync; xberg invokes them on its own worker threads. + """ + + def __init__( + self, + *, + embed_fn: Callable[[list[str]], list[list[float]]], + dim_fn: Callable[[], int], + ) -> None: + self._embed_fn = embed_fn + self._dim_fn = dim_fn + + def name(self) -> str: + return EmbeddingBackendName.LILBEE + + def initialize(self) -> None: ... + + def shutdown(self) -> None: ... + + def dimensions(self) -> int: + return self._dim_fn() + + def embed(self, texts: list[str]) -> list[list[float]]: + return self._embed_fn(texts) diff --git a/src/lilbee/data/export.py b/src/lilbee/data/export.py index 3ad1e080c..3754ee815 100644 --- a/src/lilbee/data/export.py +++ b/src/lilbee/data/export.py @@ -10,7 +10,8 @@ from typing import TYPE_CHECKING, cast from lilbee.data.ingest.extract import chunk_and_embed_pages -from lilbee.data.store import ChunkWrite, PageTextRecord, SourceType +from lilbee.data.ingest.title import derive_title +from lilbee.data.store import ChunkWrite, PageTextRecord, SourceMeta, SourceType from lilbee.runtime.progress import DetailedProgressCallback, noop_callback if TYPE_CHECKING: @@ -94,9 +95,27 @@ def build_page_dataset(store: Store, source: str | None = None) -> pa.Table: extra = _reconstructed_arrow(store, sorted(names - captured), table.schema) if extra.num_rows: table = pa.concat_tables([table, extra]) + table = _with_source_metadata(store, table) return table.sort_by([("source", "ascending"), ("page", "ascending")]) +def _with_source_metadata(store: Store, table: pa.Table) -> pa.Table: + """Denormalize each source's extraction metadata onto its page rows. + + The dataset is per page while title/authors/created_at live per source, so + without this an export/import cycle drops them and the import can only fall + back to the filename stem. Absent metadata stays null. + """ + import pyarrow as pa + + by_name: dict[str, dict] = {s["filename"]: dict(s) for s in store.get_sources()} + sources = table.column("source").to_pylist() + for column in SourceMeta._fields: + values = [by_name.get(name, {}).get(column) for name in sources] + table = table.append_column(column, pa.array(values, pa.string())) + return table + + def _reconstructed_arrow(store: Store, sources: list[str], schema: pa.Schema) -> pa.Table: """Chunk-reconstructed pages for *sources* as an Arrow table in *schema*.""" import pyarrow as pa @@ -200,6 +219,37 @@ def load_page_dataset(path: Path, fmt: DatasetFormat) -> list[PageTextRecord]: return deserialize_dataset(path.read_bytes(), fmt) +def _page_text_row(row: PageTextRecord) -> dict: + """Project a dataset row down to the ``_page_texts`` columns. + + A dataset carries the source's metadata denormalized on every page row; the + page-texts table has no such columns, so they are dropped before the write. + """ + return { + "source": row["source"], + "page": row["page"], + "text": row["text"], + "content_type": row["content_type"], + } + + +def _source_meta_from_rows(rows: list[PageTextRecord], name: str) -> SourceMeta: + """Recover a source's extraction metadata from its dataset rows. + + The values are identical on every page row, so the first carries them. A + dataset exported before the metadata columns existed has none, in which case + the title falls back to the cleaned filename stem. + """ + first: dict = dict(rows[0]) if rows else {} + stored = first.get("title") + title = stored.strip() if isinstance(stored, str) and stored.strip() else derive_title(name) + return SourceMeta( + title=title, + authors=first.get("authors") or "", + created_at=first.get("created_at") or "", + ) + + async def import_dataset( store: Store, rows: list[PageTextRecord], @@ -225,6 +275,13 @@ async def import_dataset( content_type = source_rows[0]["content_type"] or "text" page_texts = [(r["page"], r["text"]) for r in source_rows] chunks = await chunk_and_embed_pages(page_texts, name, content_type, on_progress) + # Datasets exported with the metadata columns round-trip the extracted + # title/authors/created_at; older ones carry none, so fall back to the + # stem-derived title that keeps imported chunks visible to the title arm. + meta = _source_meta_from_rows(source_rows, name) + title = meta.title + for chunk in chunks: + chunk["title"] = title or None # One locked transaction (cleanup + chunks + page texts + source row) so a # failure can't leave the source with its old rows deleted and no new ones; # the embedding-dim check inside runs before the cleanup delete. @@ -236,8 +293,9 @@ async def import_dataset( file_hash="", records=cast(list[dict], chunks), needs_cleanup=True, - page_texts=[dict(r) for r in source_rows], + page_texts=[_page_text_row(r) for r in source_rows], source_type=SourceType.IMPORTED, + meta=meta, ) ], ) diff --git a/src/lilbee/data/ingest/adaptive.py b/src/lilbee/data/ingest/adaptive.py index f46c3c43e..2873ee662 100644 --- a/src/lilbee/data/ingest/adaptive.py +++ b/src/lilbee/data/ingest/adaptive.py @@ -235,6 +235,10 @@ def decide( is falling -- the Universal Scalability Law's retrograde region); (3) otherwise hill-climb toward the knee. Always clamped to ``[permit_min, permit_max]``. + The veto blocks climbing only. A hill-climb step that goes *down* -- the reversal + on clearly falling throughput -- still runs under soft pressure, since backing off + is exactly what soft pressure should permit. + The latency veto is the leading knee indicator (TCP Vegas / Netflix Gradient2): residence time ``W`` is estimated by Little's Law as ``permits / throughput``; once ``W`` inflates past its observed minimum (the unloaded baseline) by the profile's @@ -271,13 +275,18 @@ def out(new_permits: int, new_direction: int, new_cool_down: int) -> ControllerS if gpu_saturated and delta is not None and delta < 0: return out(clamp(permits - profile.step(permits)), -1, cool_down) # USL retrograde - if _increase_vetoed( + vetoed = _increase_vetoed( profile, signals, cool_down=cool_down, gpu_saturated=gpu_saturated, w_est=w_est, w_min=w_min - ): + ) + climbed, direction = _hill_climb(profile, permits, state.direction, delta, new_ewma, clamp) + if vetoed and climbed > permits: + # The veto is against climbing, not against retreating: the hill-climb's + # step down on falling throughput is the controller's only graceful + # decrease, and suppressing it too would pin the limit past the knee + # under sustained soft pressure until a critical threshold forced a 50% + # cut -- the oscillation the dead band and slew limit exist to avoid. return out(permits, state.direction, cool_down) - - permits, direction = _hill_climb(profile, permits, state.direction, delta, new_ewma, clamp) - return out(permits, direction, cool_down) + return out(climbed, direction, cool_down) class ResizableGate: @@ -287,6 +296,15 @@ class ResizableGate: wakes blocked acquirers, shrinking lowers the ceiling and lets the surplus drain as active holders release. The limit never drops below one, so a shrink can never deadlock a run. + + Kept hand-rolled rather than adopting ``anyio.CapacityLimiter``, whose + settable ``total_tokens`` covers the same resizing need, for two reasons. + The caller treats this as a drop-in for ``asyncio.Semaphore`` (the ingest + permit is typed as either), and the ingest path is otherwise plain + asyncio. And CapacityLimiter is a per-borrower token model: it raises if + one task takes a second token, whereas this is a plain counting gate. + Revisit if a third resizing need appears, rather than growing a second + hand-rolled primitive. """ def __init__(self, limit: int) -> None: @@ -325,8 +343,10 @@ class AdaptiveController: """Drives a :class:`ResizableGate`'s limit from live signals until cancelled. ``sample(throughput)`` returns the current :class:`Signals`; ``completed()`` is a - monotonic count of finished documents, from which per-interval throughput is - derived. Both are injected so the controller runs in tests with no clock or GPU. + monotonic count of finished work units, from which per-interval throughput is + derived. The production wiring counts OCR pages, not documents (a per-document + count would bias the controller toward files of a given size). Both are injected + so the controller runs in tests with no clock or GPU. """ def __init__( diff --git a/src/lilbee/data/ingest/discovery.py b/src/lilbee/data/ingest/discovery.py index e189dae44..47edb9792 100644 --- a/src/lilbee/data/ingest/discovery.py +++ b/src/lilbee/data/ingest/discovery.py @@ -5,24 +5,50 @@ import hashlib import logging import os +from functools import cache from pathlib import Path from lilbee.core.config import active_config from lilbee.core.security import validate_path_within from lilbee.core.system import is_ignored_dir from lilbee.data.code_chunker import is_code_file -from lilbee.data.ingest.types import DOCUMENT_EXTENSION_MAP +from lilbee.data.ingest.types import IMAGE_CONTENT_TYPE, PDF_CONTENT_TYPE log = logging.getLogger(__name__) +_PDF_MIME = "application/pdf" + + +def _content_type_for(ext: str, mime: str) -> str: + """content_type for a xberg format: PDFs and images grouped, others keyed by extension.""" + if mime == _PDF_MIME: + return PDF_CONTENT_TYPE + if mime.startswith("image/"): + return IMAGE_CONTENT_TYPE + return ext.lstrip(".") + + +@cache +def supported_extension_map() -> dict[str, str]: + """Extension -> content_type for every format xberg can extract. + + Built from ``xberg.list_supported_formats()`` so lilbee covers the full set + without a hand-maintained list. Source-code files are routed separately (their + extensions are absent here), so ``classify_file`` falls through to the code path. + """ + from xberg import list_supported_formats + + out: dict[str, str] = {} + for fmt in list_supported_formats(): + ext = (fmt.extension if fmt.extension.startswith(".") else f".{fmt.extension}").lower() + out[ext] = _content_type_for(ext, fmt.mime_type) + return out + def file_hash(path: Path) -> str: """Compute SHA-256 hex digest of a file.""" - h = hashlib.sha256() with open(path, "rb") as f: - for block in iter(lambda: f.read(8192), b""): - h.update(block) - return h.hexdigest() + return hashlib.file_digest(f, "sha256").hexdigest() def _relative_name(path: Path) -> str: @@ -31,8 +57,12 @@ def _relative_name(path: Path) -> str: def classify_file(path: Path) -> str | None: - """Classify file by extension. Returns content_type or None if unsupported.""" - doc_type = DOCUMENT_EXTENSION_MAP.get(path.suffix.lower()) + """Classify a file by extension: a xberg content_type, "code", or None. + + xberg-extractable formats win; source code (not in xberg's set) routes + to the code chunker; anything else is unsupported. + """ + doc_type = supported_extension_map().get(path.suffix.lower()) if doc_type is not None: return doc_type if is_code_file(path): diff --git a/src/lilbee/data/ingest/extract.py b/src/lilbee/data/ingest/extract.py index 0aea58d4f..c405d7f54 100644 --- a/src/lilbee/data/ingest/extract.py +++ b/src/lilbee/data/ingest/extract.py @@ -1,39 +1,32 @@ -"""Document extraction: kreuzberg config, OCR fallback, markdown/document chunking.""" +"""Document extraction: one xberg pass that natively extracts text and OCRs +scanned pages/images through the registered backend; chunk + embed the result.""" from __future__ import annotations -import asyncio import contextvars import logging -from collections.abc import Awaitable, Callable, Generator, Sequence +import time +from collections.abc import Generator, Sequence from contextlib import contextmanager -from io import BytesIO from pathlib import Path -from typing import TYPE_CHECKING, Any - -from PIL import Image, ImageSequence - -if TYPE_CHECKING: - from kreuzberg import ExtractionConfig, ExtractionResult +from typing import TYPE_CHECKING, Any, Protocol from lilbee.app.services import get_services from lilbee.core.config import active_config from lilbee.data.chunk import build_chunking_config, chunk_text -from lilbee.data.ingest.discovery import file_hash -from lilbee.data.ingest.ocr_cache import load_ocr_pages, ocr_cache_key, store_ocr_pages from lilbee.data.ingest.offload import to_ingest_thread +from lilbee.data.ingest.title import derive_title, source_meta_from_extraction +from lilbee.data.ingest.trace import ExtractionTrace, trace_extraction, trace_log from lilbee.data.ingest.types import ( IMAGE_CONTENT_TYPE, MARKDOWN_OUTPUT, - MIN_MEANINGFUL_CHARS, PDF_CONTENT_TYPE, - TESSERACT_BACKEND, ChunkRecord, ExtractMode, + OcrBackendName, ) -from lilbee.data.store import ChunkType, PageTextRecord -from lilbee.runtime.cancellation import TaskCancelledError -from lilbee.runtime.cpu import cpu_quota +from lilbee.data.ingest.vision_ocr_backend import backend_options_for, ocr_request +from lilbee.data.store import ChunkType, PageTextRecord, SourceMeta from lilbee.runtime.progress import ( DetailedProgressCallback, EventType, @@ -41,19 +34,37 @@ noop_callback, ) +if TYPE_CHECKING: + from xberg import ( + ExtractedDocument, + ExtractionConfig, + LayoutDetectionConfig, + OcrConfig, + PdfConfig, + ) + log = logging.getLogger(__name__) -def _has_meaningful_text(result: ExtractionResult) -> bool: - """True when extraction yielded real text; content is primary, chunks the fallback.""" - if len(result.content.strip()) > MIN_MEANINGFUL_CHARS: - return True - return sum(len(c.content.strip()) for c in result.chunks or []) > MIN_MEANINGFUL_CHARS +class _ExtractedTable(Protocol): + """The table fields lilbee indexes as dedicated chunks. + + Structural, so it is satisfied by both xberg's public ``Table`` and the native + type that ``ExtractedDocument.tables`` actually yields. + """ + + @property + def markdown(self) -> str: ... + + @property + def page_number(self) -> int: ... def content_type_to_mode(content_type: str) -> ExtractMode: - """Map a content_type to the extraction mode.""" - return ExtractMode.PAGINATED if content_type == PDF_CONTENT_TYPE else ExtractMode.MARKDOWN + """Map a content_type to the extraction mode (paginated for PDFs and images).""" + if content_type in (PDF_CONTENT_TYPE, IMAGE_CONTENT_TYPE): + return ExtractMode.PAGINATED + return ExtractMode.MARKDOWN def _page_text_record(source: str, page: int, text: str, content_type: str) -> PageTextRecord: @@ -61,37 +72,6 @@ def _page_text_record(source: str, page: int, text: str, content_type: str) -> P return PageTextRecord(source=source, page=page, text=text, content_type=content_type) -def extraction_config(mode: ExtractMode) -> ExtractionConfig: - """Build ExtractionConfig for the given extraction mode.""" - from kreuzberg import ConcurrencyConfig, ExtractionConfig, OcrConfig, PageConfig - - chunking = build_chunking_config() - pages = PageConfig(extract_pages=True, insert_page_markers=False) - ocr = OcrConfig(backend=TESSERACT_BACKEND) - # Bound kreuzberg's internal pool to the same CPU budget as the - # pipeline semaphore so the two stop competing for cores. - concurrency = ConcurrencyConfig(max_threads=cpu_quota()) - builders: dict[ExtractMode, Callable[[], ExtractionConfig]] = { - ExtractMode.MARKDOWN: lambda: ExtractionConfig( - chunking=chunking, - output_format=MARKDOWN_OUTPUT, - concurrency=concurrency, - ), - ExtractMode.PAGINATED: lambda: ExtractionConfig( - chunking=chunking, - pages=pages, - concurrency=concurrency, - ), - ExtractMode.PAGINATED_OCR: lambda: ExtractionConfig( - chunking=chunking, - pages=pages, - ocr=ocr, - concurrency=concurrency, - ), - } - return builders[mode]() - - _ocr_enable_override: contextvars.ContextVar[bool | None] = contextvars.ContextVar( "lilbee_ocr_enable_override", default=None ) @@ -104,9 +84,7 @@ def _effective_enable_ocr() -> bool | None: """``cfg.enable_ocr`` unless a per-request OCR override is active. The override is a ContextVar, not a global cfg mutation, so concurrent - ingests on the shared HTTP daemon each see their own setting. The override - also propagates into ``to_ingest_thread`` workers (it copies the calling - task's context), which is how the timeout reaches the image-OCR call. + ingests on the shared HTTP daemon each see their own setting. """ override = _ocr_enable_override.get() return active_config().enable_ocr if override is None else override @@ -125,8 +103,8 @@ def ocr_override( """Scope per-request OCR settings without mutating the global cfg. A ``None`` argument leaves that setting at its cfg default. Each override is - isolated to the entering context (and any ``to_ingest_thread`` work it - spawns), so overlapping ingests never clobber one another's OCR config. + isolated to the entering context, so overlapping ingests never clobber one + another's OCR config. """ tokens: list[tuple[contextvars.ContextVar[Any], contextvars.Token[Any]]] = [] try: @@ -140,243 +118,119 @@ def ocr_override( var.reset(token) -def _should_run_ocr() -> bool: - """Decide whether to attempt vision-based OCR on scanned PDFs. +def _ocr_config(ocr_token: str | None) -> OcrConfig: + """Pick the OCR backend for this extraction. - Uses the effective OCR setting (``cfg.enable_ocr`` or a per-request - override) and ``cfg.vision_model``: - True = force on (requires ``cfg.vision_model`` to be set for a real - vision run; otherwise the caller falls back to Tesseract). - False = force off. - None = auto-detect: run vision OCR when ``cfg.vision_model`` is set. + Mirrors the prior fallback policy: OCR off when ``enable_ocr`` is False; lilbee's + vision backend when a vision model is configured; otherwise xberg's tesseract. + xberg auto-OCRs only the pages that lack a text layer. """ - enable_ocr = _effective_enable_ocr() - if enable_ocr is True: - return True - if enable_ocr is False: - return False - return bool(active_config().vision_model) - + from xberg import OcrConfig -def _record_page_texts( - page_texts: Sequence[tuple[int, str]], - source_name: str, - content_type: str, - page_texts_out: list[PageTextRecord] | None, -) -> None: - """Append OCR page texts to the export accumulator when one is supplied.""" - if page_texts_out is None: - return - page_texts_out.extend( - _page_text_record(source_name, page, text, content_type) for page, text in page_texts - ) - - -async def _vision_ocr_cached( - path: Path, - source_name: str, - content_type: str, - *, - ocr_fn: Callable[[], Awaitable[list[tuple[int, str]]]], - on_progress: DetailedProgressCallback, - page_texts_out: list[PageTextRecord] | None = None, -) -> list[ChunkRecord]: - """Cache-wrapped vision OCR: reuse stored pages, else run *ocr_fn*, then chunk + embed. - - Pool routing amortises the multi-second vision-Llama load across files. - *ocr_fn* returns the OCR'd pages as ``(page_number, text)`` tuples (a PDF page - loop or a single image). Output is cached by file content + model, so a retry - after a downstream failure (chunk/embed/store) reuses the pages, not re-OCR-ing. - """ - # The per-page timeout bounds completeness: a page that exhausts the budget - # yields empty text. Key on it so raising the timeout re-OCRs the file rather - # than serving the earlier, partially-empty cached result for the same content. config = active_config() - key = ocr_cache_key( - file_hash(path), - backend="vision", - model=config.vision_model, - extra=f"{config.vision_ocr_max_tokens}:{_effective_ocr_timeout()}", - ) - cached = load_ocr_pages(key) - if cached is not None: - _record_page_texts(cached, source_name, content_type, page_texts_out) - return await chunk_and_embed_pages(cached, source_name, content_type, on_progress) - try: - pages = await ocr_fn() - except (asyncio.CancelledError, TaskCancelledError): - # A user cancel (SIGINT / TUI cancel) raised cooperatively through the - # per-page on_progress callback must abort the file, not be logged as an - # OCR failure and swallowed. - raise - except Exception: - # A vision-backend failure (dead replica, exhausted failover, transport - # error) is a per-file ingest FAILURE, not a "document had no text". - # Returning [] would classify the file as empty and skip-mark it under - # its current hash, silently dropping it from search until - # retry_skipped; raising routes it through the pipeline's failed path - # with the real reason. - log.warning("OCR via vision backend failed for %s.", source_name, exc_info=True) - raise - store_ocr_pages(key, pages) - _record_page_texts(pages, source_name, content_type, page_texts_out) - return await chunk_and_embed_pages(pages, source_name, content_type, on_progress) - - -async def _vision_ocr_fallback( - path: Path, - source_name: str, - content_type: str, - *, - on_progress: DetailedProgressCallback, - quiet: bool, - page_texts_out: list[PageTextRecord] | None = None, -) -> list[ChunkRecord]: - """Vision OCR a scanned PDF: rasterize + OCR each page through the worker pool.""" - - async def _ocr() -> list[tuple[int, str]]: - pages = await to_ingest_thread( - get_services().provider.pdf_ocr, - path, - backend="vision", - model=active_config().vision_model, - per_page_timeout_s=_effective_ocr_timeout(), - quiet=quiet, - on_progress=on_progress, + if _effective_enable_ocr() is False: + return OcrConfig(enabled=False) + if config.vision_model: + options = backend_options_for(ocr_token) if ocr_token else None + return OcrConfig( + backend=OcrBackendName.LILBEE_VISION, + backend_options=options, ) - return [(p.page, p.text) for p in pages] + # xberg requires a non-empty language list (4.x defaulted to English; + # 5.x errors on an empty one). cfg.ocr_language is validated non-empty. + return OcrConfig(backend=OcrBackendName.TESSERACT, language=list(config.ocr_language)) - return await _vision_ocr_cached( - path, - source_name, - content_type, - ocr_fn=_ocr, - on_progress=on_progress, - page_texts_out=page_texts_out, - ) +def _ocr_force_requested() -> bool: + """Whether LILBEE_OCR_FORCE opts every page into vision OCR. -async def _vision_image_ocr( - path: Path, - source_name: str, - content_type: str, - *, - on_progress: DetailedProgressCallback, - page_texts_out: list[PageTextRecord] | None = None, -) -> list[ChunkRecord]: - """Vision OCR an image through the worker pool, one page per frame. + Born-digital PDFs carry a clean text layer that xberg extracts natively and, + since rc25, short-circuits past the OCR backend entirely -- so a re-OCR run + never touches the vision model. This lever sets ExtractionConfig.force_ocr, + which overrides that short-circuit and OCRs every page. A targeted-reingest + lever, not a default: normal runs keep native-first extraction.""" + import os - A multi-frame TIFF/GIF yields one OCR page per frame instead of silently - dropping every frame after the first. - """ - - async def _ocr() -> list[tuple[int, str]]: - page_pngs = await to_ingest_thread(_image_page_pngs, path) - pages: list[tuple[int, str]] = [] - for page_num, png in enumerate(page_pngs, start=1): - text = await to_ingest_thread(_ocr_image_png, png) - pages.append((page_num, text)) - return pages - - return await _vision_ocr_cached( - path, - source_name, - content_type, - ocr_fn=_ocr, - on_progress=on_progress, - page_texts_out=page_texts_out, - ) + return os.environ.get("LILBEE_OCR_FORCE", "").strip().lower() in {"1", "true", "yes"} -def _ocr_image_png(png: bytes) -> str: - """OCR one rendered image page through the vision server.""" - return get_services().provider.vision_ocr( - png, active_config().vision_model, timeout=_effective_ocr_timeout() - ) +# Fraction of page height treated as the running header/footer band when +# layout detection is on. Conservative: strips only the outermost 5%. +_TOP_MARGIN_FRACTION = 0.05 +_BOTTOM_MARGIN_FRACTION = 0.05 -def _image_page_pngs(path: Path) -> list[bytes]: - """Each frame of a (possibly multi-frame) image, re-encoded as PNG. +def _pdf_options() -> PdfConfig | None: + """PdfConfig for the enabled opt-in ingest features, or None when all are off. - A single-frame image yields one entry; a multipage TIFF/GIF yields one per - frame so every page reaches the projector instead of just the first. - """ - pages: list[bytes] = [] - with Image.open(path) as img: - for frame in ImageSequence.Iterator(img): - buf = BytesIO() - frame.convert("RGB").save(buf, format="PNG") - pages.append(buf.getvalue()) - return pages - - -def _run_tesseract_sync(path: Path) -> Any: - """Run kreuzberg Tesseract OCR with the worker's stderr redirected to /dev/null. - - Tesseract writes "Line cannot be recognized!!", "Image too small to - scale!!", and "Detected N diacritics" directly to fd 2 from inside libc. - Without the redirect those lines flood the TUI log file (1 000+ entries - per scanned PDF) and can bleed into the TUI itself. We hold the - suppression for just the duration of the extraction call so other - threads' stderr writes still go through. + Table extraction turns on structure recognition; layout detection orders + text by detected reading order and strips the header/footer margin bands. """ - from kreuzberg import extract_file_sync + config = active_config() + if not (config.table_extraction or config.layout_detection): + return None + from xberg import PdfConfig + + kwargs: dict[str, Any] = {} + if config.table_extraction: + kwargs["extract_tables"] = True + if config.layout_detection: + kwargs.update( + reading_order=True, + top_margin_fraction=_TOP_MARGIN_FRACTION, + bottom_margin_fraction=_BOTTOM_MARGIN_FRACTION, + ) + return PdfConfig(**kwargs) - from lilbee.core.system import stderr_suppressed - with stderr_suppressed(): - return extract_file_sync(str(path), config=extraction_config(ExtractMode.PAGINATED_OCR)) +def _layout_config() -> LayoutDetectionConfig | None: + """LayoutDetectionConfig when layout detection is on, else None for xberg's default.""" + if not active_config().layout_detection: + return None + from xberg import LayoutDetectionConfig + return LayoutDetectionConfig() -async def _tesseract_ocr_fallback( - path: Path, - source_name: str, - content_type: str, - *, - on_progress: DetailedProgressCallback, - page_texts_out: list[PageTextRecord] | None = None, -) -> list[ChunkRecord]: - """Tesseract OCR via ``asyncio.to_thread`` (no model load = no pool). - ``cfg.tesseract_timeout`` caps the whole-document extract; 0 means - unlimited. Failures (including timeout) log a warning and return an - empty list so the caller can skip the file. OCR output is cached by file - content so a downstream failure doesn't force a re-OCR on retry. - """ - key = ocr_cache_key(file_hash(path), backend=TESSERACT_BACKEND, model=TESSERACT_BACKEND) - page_texts = load_ocr_pages(key) - if page_texts is None: - tesseract_timeout = active_config().tesseract_timeout - coro = to_ingest_thread(_run_tesseract_sync, path) - try: - if tesseract_timeout > 0: - result = await asyncio.wait_for(coro, timeout=tesseract_timeout) - else: - result = await coro - except TimeoutError: - log.warning( - "Tesseract OCR exceeded %.0fs timeout on %s; skipping.", - tesseract_timeout, - source_name, - ) - return [] - except Exception: - log.warning("OCR via tesseract backend failed for %s.", source_name, exc_info=True) - return [] - - by_page: dict[int, list[str]] = {} - for chunk in result.chunks or []: - page = int(chunk.metadata.get("first_page") or 1) - by_page.setdefault(page, []).append(chunk.content) - page_texts = [(page, "\n".join(by_page[page])) for page in sorted(by_page)] - store_ocr_pages(key, page_texts) - _record_page_texts(page_texts, source_name, content_type, page_texts_out) - return await chunk_and_embed_pages(page_texts, source_name, content_type, on_progress) +def extraction_config(mode: ExtractMode, *, ocr_token: str | None = None) -> ExtractionConfig: + """Build ExtractionConfig for the given extraction mode.""" + from xberg import ExtractionConfig, PageConfig + + # Files are extracted one per call, so xberg parallelizes OCR across the + # pages of each document internally; cross-file concurrency is the pipeline's + # semaphore. (max_concurrent_extractions only bounds multi-file batch calls, + # which lilbee never makes, so it is intentionally not set here.) + chunking = build_chunking_config() + ocr = _ocr_config(ocr_token) + # force_ocr defeats xberg's text-layer short-circuit so every page reaches the + # vision backend; scoped to the vision path, since it is a GPU re-OCR lever. + force_ocr = _ocr_force_requested() and ocr.backend == OcrBackendName.LILBEE_VISION + if mode is ExtractMode.PAGINATED: + paginated = ExtractionConfig( + chunking=chunking, + pages=PageConfig(extract_pages=True, insert_page_markers=False), + ocr=ocr, + force_ocr=force_ocr, + pdf_options=_pdf_options(), + ) + # Set only when on: the keys are absent rather than None, leaving xberg's + # defaults in place when layout detection is off. + layout = _layout_config() + if layout is not None: + paginated["layout"] = layout + paginated["use_layout_for_markdown"] = True + return paginated + return ExtractionConfig( + chunking=chunking, + output_format=MARKDOWN_OUTPUT, + ocr=ocr, + force_ocr=force_ocr, + ) def _chunk_pages(page_texts: Sequence[tuple[int, str]]) -> list[tuple[int, str]]: - """Chunk each OCR page's text. Semantic chunking is off: a single OCR page - rarely spans multiple topics, so the semantic round-trip is not worth it.""" + """Chunk each page's text. Semantic chunking is off: a single page rarely spans + multiple topics, so the semantic round-trip is not worth it.""" return [ (page_num, chunk) for page_num, text in page_texts @@ -390,11 +244,11 @@ async def chunk_and_embed_pages( content_type: str, on_progress: DetailedProgressCallback, ) -> list[ChunkRecord]: - """Chunk per-page text and embed every chunk. Shared by OCR ingest and import.""" + """Chunk per-page text and embed every chunk. Used by the dataset import path.""" if not page_texts: return [] - # chunk_text runs kreuzberg's synchronous extractor; offload it so a long OCR + # chunk_text runs xberg's synchronous extractor; offload it so a long # document does not stall sibling files sharing this event loop. all_chunks = await to_ingest_thread(_chunk_pages, page_texts) if not all_chunks: @@ -421,29 +275,43 @@ async def chunk_and_embed_pages( def _capture_result_page_texts( - result: ExtractionResult, + doc: ExtractedDocument, source_name: str, content_type: str, page_texts_out: list[PageTextRecord] | None, ) -> None: - """Append a normal extraction's page texts to the export accumulator. + """Append an extraction's page texts to the export accumulator. - Paginated PDFs yield one row per ``result.pages`` entry; other documents - have no page split, so the full ``result.content`` is recorded as page 0. + Paginated documents yield one row per ``doc.pages`` entry; others have no + page split, so the full ``doc.content`` is recorded as page 0. """ if page_texts_out is None: return - if result.pages: + if doc.pages: page_texts_out.extend( - _page_text_record(source_name, page["page_number"], page["content"], content_type) - for page in result.pages + _page_text_record(source_name, page.page_number, page.content, content_type) + for page in doc.pages ) - elif result.content.strip(): - page_texts_out.append(_page_text_record(source_name, 0, result.content, content_type)) + elif doc.content.strip(): + page_texts_out.append(_page_text_record(source_name, 0, doc.content, content_type)) + + +def _document_tables(doc: ExtractedDocument) -> list[_ExtractedTable]: + """The result tables to index as dedicated chunks, when table extraction is on. + + Each table becomes its own chunk carrying xberg's markdown serialization and + page metadata. The table's flattened text also stays inside the content + chunks: stripping it there would tear holes in reading-order prose and the + page-text export, so both are indexed deliberately. The dedicated table + chunk adds the structured serialization for targeted retrieval. + """ + if not active_config().table_extraction: + return [] + return [t for t in (doc.tables or []) if t.markdown and t.markdown.strip()] def _warn_empty_ocr(source_name: str, media: str) -> None: - """Warn that OCR yielded no text and point to the vision-model remedy.""" + """Warn that extraction yielded no text and point to the vision-model remedy.""" log.warning( "Skipped %s: text extraction produced no usable text. " "For better results on %s, configure a vision model " @@ -453,94 +321,6 @@ def _warn_empty_ocr(source_name: str, media: str) -> None: ) -async def _handle_scanned_pdf_fallback( - path: Path, - source_name: str, - content_type: str, - result: ExtractionResult, - *, - quiet: bool, - on_progress: DetailedProgressCallback, - page_texts_out: list[PageTextRecord] | None = None, -) -> list[ChunkRecord]: - """Route a scanned PDF through the configured OCR backend. - - Vision OCR (pool-routed) runs when ``_should_run_ocr()`` is True - and a vision model is configured; otherwise the file falls back to - inline Tesseract. Both paths return chunk records; an empty list - means OCR found no usable text and the caller should skip the - file. - """ - del result # Both backends re-extract; the kreuzberg result is not reused. - if _effective_enable_ocr() is False: - # OCR explicitly disabled: skip entirely (vision and Tesseract) rather - # than paying the full Tesseract cost the config says is turned off. - log.info("OCR disabled; skipping scanned-PDF OCR for %s", source_name) - return [] - use_ocr = _should_run_ocr() - vision_model = active_config().vision_model - if use_ocr and vision_model: - log.info( - "Scanned PDF: using vision OCR for %s (model=%s)", - source_name, - vision_model, - ) - return await _vision_ocr_fallback( - path, - source_name, - content_type, - on_progress=on_progress, - quiet=quiet, - page_texts_out=page_texts_out, - ) - - log.info("Scanned PDF: falling back to Tesseract OCR for %s", source_name) - chunks = await _tesseract_ocr_fallback( - path, - source_name, - content_type, - on_progress=on_progress, - page_texts_out=page_texts_out, - ) - if not chunks: - _warn_empty_ocr(source_name, "scanned PDFs") - return chunks - - -async def _handle_image( - path: Path, - source_name: str, - content_type: str, - *, - on_progress: DetailedProgressCallback, - page_texts_out: list[PageTextRecord] | None = None, -) -> list[ChunkRecord]: - """OCR an image: vision OCR when a vision model is configured, else Tesseract. - - An image has no text layer to extract first, so it routes straight to OCR -- - the same downstream call a PDF page hits after it is rasterized to an image. - """ - if _effective_enable_ocr() is False: - # OCR explicitly disabled: an image has no text layer, so skip it rather - # than paying the full Tesseract cost the config says is turned off. - log.info("OCR disabled; skipping image OCR for %s", source_name) - return [] - vision_model = active_config().vision_model - if _should_run_ocr() and vision_model: - log.info("Image: using vision OCR for %s (model=%s)", source_name, vision_model) - return await _vision_image_ocr( - path, source_name, content_type, on_progress=on_progress, page_texts_out=page_texts_out - ) - - log.info("Image: falling back to Tesseract OCR for %s", source_name) - chunks = await _tesseract_ocr_fallback( - path, source_name, content_type, on_progress=on_progress, page_texts_out=page_texts_out - ) - if not chunks: - _warn_empty_ocr(source_name, "images") - return chunks - - async def ingest_document( path: Path, source_name: str, @@ -549,71 +329,135 @@ async def ingest_document( quiet: bool = False, on_progress: DetailedProgressCallback = noop_callback, page_texts_out: list[PageTextRecord] | None = None, -) -> list[ChunkRecord]: - """Extract and chunk a document, embed, return records. - - Vision OCR is controlled by ``cfg.enable_ocr`` (see ``_should_run_ocr``). - When ``page_texts_out`` is given, per-page text is appended for export. +) -> tuple[list[ChunkRecord], SourceMeta]: + """Extract, chunk, and embed a document in a single xberg pass, with its metadata. + + xberg extracts native text and, where a page has none, OCRs it through the + registered backend (lilbee's vision model, or tesseract). Per-page OCR progress + is streamed as a running count via ``ocr_request``. ``quiet`` is accepted for + pipeline call compatibility. The returned metadata carries the document's + extraction title/authors/date and is derived even when extraction yields nothing. """ - # An image carries no text layer; route it straight to OCR (vision or Tesseract) - # instead of a no-op kreuzberg markdown extract that yields nothing for a scan. - if content_type == IMAGE_CONTENT_TYPE: - return await _handle_image( - path, source_name, content_type, on_progress=on_progress, page_texts_out=page_texts_out - ) + del quiet + from lilbee.data.xberg_extract import aextract_document - from kreuzberg import extract_file_sync + page_seen = 0 - config = extraction_config(content_type_to_mode(content_type)) - result = await to_ingest_thread(extract_file_sync, str(path), config=config) + def _tick() -> None: + nonlocal page_seen + page_seen += 1 + on_progress( + EventType.EXTRACT, + ExtractEvent(file=source_name, page=page_seen, total_pages=0), + ) - if content_type == PDF_CONTENT_TYPE and not _has_meaningful_text(result): - return await _handle_scanned_pdf_fallback( - path, - source_name, - content_type, - result, - quiet=quiet, - on_progress=on_progress, - page_texts_out=page_texts_out, + trace_log.debug("extract-start source=%r type=%s", source_name, content_type) + started = time.perf_counter() + with ocr_request(on_page=_tick, timeout=_effective_ocr_timeout()) as token: + config = extraction_config(content_type_to_mode(content_type), ocr_token=token) + # xberg's extract is async; awaiting it keeps the OCR page loop off this thread. + doc = await aextract_document(path.read_bytes(), filename=path.name, config=config) + elapsed = time.perf_counter() - started + + # One trace line per xberg extraction (filename, timing, counts, OCR pages), + # plus a dedicated vision line for scanned files -- the diagnostics an xberg + # author needs. Emitted for empty results too: a slow file that yields nothing + # is exactly the case worth surfacing. + trace_extraction( + ExtractionTrace( + source=source_name, + content_type=content_type, + elapsed_s=elapsed, + page_count=len(doc.pages or []) or len(doc.chunks or []), + chunk_count=len(doc.chunks or []), + ocr_pages=page_seen, + vision_configured=bool(active_config().vision_model), ) + ) - if not result.chunks: - return [] + # Derived before the empty-result return: a scan's metadata (title, authors) + # survives even when the extraction yields no chunks, so the source row and + # the title stamped on its chunks stay populated. + meta = source_meta_from_extraction(doc.metadata, source_name) - _capture_result_page_texts(result, source_name, content_type, page_texts_out) + tables = _document_tables(doc) + if not doc.chunks and not tables: + if content_type in (PDF_CONTENT_TYPE, IMAGE_CONTENT_TYPE): + _warn_empty_ocr(source_name, "scanned documents") + return [], meta - # Fire one EXTRACT event per file so subscribers (chat /add, /sync, - # CLI Rich progress) can show "extracted N pages" before the embed - # phase starts; otherwise a 44MB PDF sits at file-level 0% for - # minutes. get_page_count is the canonical PDF page count; for - # non-paginated formats we fall back to the chunk count. - page_count = result.get_page_count() or len(result.chunks) + _capture_result_page_texts(doc, source_name, content_type, page_texts_out) + + # One EXTRACT event per file so subscribers (chat /add, /sync, CLI Rich + # progress) show "extracted N pages" before the embed phase; result.pages is + # the canonical page list, falling back to the chunk count for non-paginated docs. + page_count = len(doc.pages or []) or len(doc.chunks or []) on_progress( EventType.EXTRACT, ExtractEvent(file=source_name, page=page_count, total_pages=page_count), ) - texts = [chunk.content for chunk in result.chunks] + # Content chunks and table serializations share one embed batch; the vector + # list is split back apart below by position. + texts = [chunk.content for chunk in doc.chunks or []] + table_texts = [table.markdown for table in tables] vectors = await to_ingest_thread( - get_services().embedder.embed_batch, texts, source=source_name, on_progress=on_progress + get_services().embedder.embed_batch, + texts + table_texts, + source=source_name, + on_progress=on_progress, ) - - return [ + records = [ ChunkRecord( source=source_name, content_type=content_type, chunk_type=ChunkType.RAW, - page_start=chunk.metadata.get("first_page") or 0, - page_end=chunk.metadata.get("last_page") or 0, + page_start=chunk.metadata.first_page or 0, + page_end=chunk.metadata.last_page or 0, line_start=0, line_end=0, chunk=text, - chunk_index=chunk.metadata.get("chunk_index", idx), + chunk_index=chunk.metadata.chunk_index, vector=vec, ) - for idx, (chunk, text, vec) in enumerate(zip(result.chunks, texts, vectors, strict=True)) + for chunk, text, vec in zip(doc.chunks or [], texts, vectors[: len(texts)], strict=True) ] + # Table chunk indices continue after the content chunks so a source's + # (source, chunk_index) pairs stay unique. + records.extend( + ChunkRecord( + source=source_name, + content_type=content_type, + chunk_type=ChunkType.TABLE, + page_start=table.page_number, + page_end=table.page_number, + line_start=0, + line_end=0, + chunk=text, + chunk_index=len(texts) + i, + vector=vec, + ) + for i, (table, text, vec) in enumerate( + zip(tables, table_texts, vectors[len(texts) :], strict=True) + ) + ) + return records, meta + + +def _markdown_h1(text: str) -> str | None: + """The document's leading ``# Heading``, the best title a note carries. + + Only a top-level ATX heading counts; ``##`` and deeper are sections, not the + document title. None when the note opens without one. + """ + for line in text.splitlines(): + stripped = line.strip() + if not stripped: + continue + if stripped.startswith("# "): + return stripped[2:].strip() or None + return None + return None async def ingest_markdown( @@ -621,23 +465,26 @@ async def ingest_markdown( source_name: str, on_progress: DetailedProgressCallback = noop_callback, page_texts_out: list[PageTextRecord] | None = None, -) -> list[ChunkRecord]: +) -> tuple[list[ChunkRecord], SourceMeta]: """Chunk a markdown file with heading context prepended to each chunk. + Each chunk gets the heading hierarchy path (e.g. "# Setup > ## Install") prepended for better retrieval context. When ``page_texts_out`` is given, - the full text is appended as page 0 for export. + the full text is appended as page 0 for export. The returned metadata's + title is the note's leading ``# Heading`` when it has one, else the stem. """ raw_text = await to_ingest_thread(path.read_text, encoding="utf-8", errors="replace") + meta = SourceMeta(title=derive_title(source_name, _markdown_h1(raw_text))) if not raw_text.strip(): - return [] + return [], meta - # chunk_text runs kreuzberg's synchronous extractor; offload it so a large + # chunk_text runs xberg's synchronous extractor; offload it so a large # markdown doc does not stall sibling files sharing this event loop. texts = await to_ingest_thread( chunk_text, raw_text, mime_type="text/markdown", heading_context=True ) if not texts: - return [] + return [], meta if page_texts_out is not None: page_texts_out.append(_page_text_record(source_name, 0, raw_text, "text")) @@ -645,7 +492,7 @@ async def ingest_markdown( vectors = await to_ingest_thread( get_services().embedder.embed_batch, texts, source=source_name, on_progress=on_progress ) - return [ + records = [ ChunkRecord( source=source_name, content_type="text", @@ -660,3 +507,4 @@ async def ingest_markdown( ) for idx, (t, vec) in enumerate(zip(texts, vectors, strict=True)) ] + return records, meta diff --git a/src/lilbee/data/ingest/ocr_cache.py b/src/lilbee/data/ingest/ocr_cache.py deleted file mode 100644 index d74fe3657..000000000 --- a/src/lilbee/data/ingest/ocr_cache.py +++ /dev/null @@ -1,90 +0,0 @@ -"""Content-addressed cache for per-page OCR text. - -OCR is the most expensive ingest step (a 595-page scanned manual runs ~18 min). -Its per-page output is cached keyed on the file's content hash plus the OCR -backend and model, so a failure in a downstream step (chunk, embed, or the -LanceDB write) no longer discards the whole OCR pass: the retry reads the cached -pages and goes straight to chunk + embed. -""" - -from __future__ import annotations - -import hashlib -import json -import logging -from pathlib import Path - -from lilbee.core.config import active_config - -log = logging.getLogger(__name__) - -# A cached page is serialized as a [page_number, text] pair. -_PAGE_ENTRY_LEN = 2 - -_CACHE_DIRNAME = "ocr_cache" -# Bump when the entry format or what counts as "the same OCR" changes, so old -# entries miss instead of being misread. -_CACHE_VERSION = "1" -_KEY_SEP = "\0" - - -def _cache_dir() -> Path: - return active_config().data_dir / _CACHE_DIRNAME - - -def ocr_cache_key(file_hash: str, *, backend: str, model: str, extra: str = "") -> str: - """Stable cache key for one file's OCR output under a backend/model/config tuple. - - ``file_hash`` ties the entry to exact file content (an edit changes the hash - and so misses); ``backend`` / ``model`` / ``extra`` capture what would change - the OCR text for the same bytes. - """ - payload = _KEY_SEP.join((_CACHE_VERSION, file_hash, backend, model, extra)) - return hashlib.sha256(payload.encode("utf-8")).hexdigest() - - -def load_ocr_pages(key: str) -> list[tuple[int, str]] | None: - """Return cached ``(page, text)`` pairs for *key*, or ``None`` on miss. - - A missing, unreadable, or malformed entry returns ``None`` so the caller - re-runs OCR rather than trusting partial data. - """ - path = _cache_dir() / f"{key}.json" - if not path.exists(): - return None - try: - raw = json.loads(path.read_text(encoding="utf-8")) - except (OSError, json.JSONDecodeError): - log.debug("OCR cache read failed for %s", key, exc_info=True) - return None - if not isinstance(raw, list): - return None - pages: list[tuple[int, str]] = [] - for entry in raw: - if ( - isinstance(entry, list) - and len(entry) == _PAGE_ENTRY_LEN - and isinstance(entry[0], int) - and isinstance(entry[1], str) - ): - pages.append((entry[0], entry[1])) - return pages - - -def store_ocr_pages(key: str, pages: list[tuple[int, str]]) -> None: - """Persist ``(page, text)`` pairs for *key*. - - Best-effort: a write failure is logged and swallowed (a cache miss next time - is harmless). Empty results are not cached so a failed OCR run is retried. - """ - if not pages: - return - cache_dir = _cache_dir() - try: - cache_dir.mkdir(parents=True, exist_ok=True) - path = cache_dir / f"{key}.json" - tmp = path.with_suffix(".json.tmp") - tmp.write_text(json.dumps([[page, text] for page, text in pages]), encoding="utf-8") - tmp.replace(path) - except OSError: - log.debug("OCR cache write failed for %s", key, exc_info=True) diff --git a/src/lilbee/data/ingest/offload.py b/src/lilbee/data/ingest/offload.py index b4bd1fb3e..75322441f 100644 --- a/src/lilbee/data/ingest/offload.py +++ b/src/lilbee/data/ingest/offload.py @@ -25,8 +25,15 @@ _MAX_WORKERS_ENV = "LILBEE_INGEST_MAX_WORKERS" -def _max_workers() -> int: - """Concurrent slots for ingest offload work; honors ``LILBEE_INGEST_MAX_WORKERS``. +@functools.cache +def max_workers() -> int: + """The ingest pool's worker count -- its hard concurrency ceiling. + + The adaptive-concurrency controller uses this as the upper bound on in-flight + documents, since each one needs a pool thread to run its blocking extraction. + Resolved once and cached: the pool is built with this same value on first use, + so re-reading ``LILBEE_INGEST_MAX_WORKERS`` per call would let the reported + ceiling drift away from the pool that actually exists. Extraction rasterizes PDFs and drives OCR on this pool. The default caps at ``min(32, cpu_count + 4)``: a worker-count sweep on forced-OCR multi-page PDFs @@ -55,19 +62,10 @@ def _max_workers() -> int: return min(32, (os.cpu_count() or 4) + 4) -def max_workers() -> int: - """The ingest pool's worker count -- its hard concurrency ceiling. - - The adaptive-concurrency controller uses this as the upper bound on in-flight - documents, since each one needs a pool thread to run its blocking extraction. - """ - return _max_workers() - - @functools.cache def _ingest_executor() -> ThreadPoolExecutor: """The shared ingest pool, created on first use (cache makes it a singleton).""" - return ThreadPoolExecutor(max_workers=_max_workers(), thread_name_prefix="lilbee-ingest") + return ThreadPoolExecutor(max_workers=max_workers(), thread_name_prefix="lilbee-ingest") async def to_ingest_thread(fn: Callable[_P, _R], /, *args: _P.args, **kwargs: _P.kwargs) -> _R: diff --git a/src/lilbee/data/ingest/pipeline.py b/src/lilbee/data/ingest/pipeline.py index 1173cb6ee..cdb04dcd3 100644 --- a/src/lilbee/data/ingest/pipeline.py +++ b/src/lilbee/data/ingest/pipeline.py @@ -41,6 +41,8 @@ write_skip_markers, write_skip_reasons, ) +from lilbee.data.ingest.title import derive_title +from lilbee.data.ingest.trace import configure_from_env as configure_trace_from_env from lilbee.data.ingest.types import ( ChunkRecord, FileChangePlan, @@ -53,6 +55,7 @@ ChunkWrite, ConceptRecords, PageTextRecord, + SourceMeta, SourceRecord, SourceStat, SourceStatBackfill, @@ -141,11 +144,11 @@ async def _build_entity_records(records: list[ChunkRecord], source_name: str) -> nlp = None if any(t.kind is ExtractorKind.SPACY for t in schema.types): from lilbee.retrieval.concepts import concepts_available - from lilbee.retrieval.concepts.nlp import _ensure_spacy_model + from lilbee.retrieval.concepts.nlp import load_spacy_pipeline if concepts_available(): try: - nlp = _ensure_spacy_model() + nlp = load_spacy_pipeline() except ImportError: log.warning("spaCy model unavailable; spacy-kind entity types skipped") provider = None @@ -198,22 +201,27 @@ async def _produce_records( quiet: bool = False, on_progress: DetailedProgressCallback = noop_callback, page_texts_out: list[PageTextRecord] | None = None, -) -> list[ChunkRecord]: - """Extract, chunk, and embed a single file into store-ready records. +) -> tuple[list[ChunkRecord], SourceMeta]: + """Extract, chunk, and embed a single file into (records, source metadata). The LanceDB write is deferred: records are returned to the caller and written in a batched flush (see :func:`_flush_writes`), so bulk ingest pays one write-lock acquisition per batch instead of one per file. The per-page text dataset rows land in ``page_texts_out`` and are written by the same flush. + The returned metadata (extraction-provided when available, stem-derived title + otherwise) stamps every record's ``title`` and updates the source row. """ records: list[ChunkRecord] page_texts: list[PageTextRecord] = page_texts_out if page_texts_out is not None else [] if content_type == "code": records = await to_ingest_thread(ingest_code_sync, path, source_name, on_progress) + meta = SourceMeta(title=derive_title(source_name)) elif path.suffix.lower() == ".md": - records = await ingest_markdown(path, source_name, on_progress, page_texts_out=page_texts) + records, meta = await ingest_markdown( + path, source_name, on_progress, page_texts_out=page_texts + ) else: - records = await ingest_document( + records, meta = await ingest_document( path, source_name, content_type, @@ -222,7 +230,11 @@ async def _produce_records( page_texts_out=page_texts, ) - return records + for record in records: + # NULL (not "") for an absent title, so chunk rows match the migration + # and the _sources table, which both persist absence as NULL. + record["title"] = meta.title or None + return records, meta def _disk_stat(path: Path) -> SourceStat | None: @@ -524,6 +536,7 @@ async def sync( if files_to_process or removed: _store.ensure_fts_index() + _store.ensure_scalar_indexes() _store.ensure_vector_index() _store.optimize_sources() await _rebuild_concept_clusters() @@ -653,6 +666,9 @@ async def ingest_batch( ingesting new ones so the two operations are atomic per file. When *cancel* is set, pending files raise CancelledError before starting. """ + # Honor LILBEE_INGEST_TRACE once per batch: it raises the trace loggers above + # the default WARNING so per-file extraction lines actually surface. + configure_trace_from_env() # Throughput is measured in OCR pages, not documents: a document's cost scales # with its page count (a 500-page scan is 500x a memo), so pages are the unbiased # unit of GPU-feeding work for the adaptive controller to hill-climb on. @@ -680,7 +696,7 @@ async def _process_one(entry: FileToProcess, file_index: int) -> _IngestResult: # transaction as the new write (see _flush_writes), so cleanup is # carried on the result rather than run eagerly here. page_texts: list[PageTextRecord] = [] - records = await _produce_records( + records, meta = await _produce_records( entry.path, name, entry.content_type, @@ -707,6 +723,7 @@ async def _process_one(entry: FileToProcess, file_index: int) -> _IngestResult: stat=entry.stat, concept_records=concept_records, entity_rows=entity_rows, + meta=meta, ) except (asyncio.CancelledError, TaskCancelledError) as exc: # TaskCancelledError is the TUI's cooperative cancel signal raised @@ -1031,6 +1048,7 @@ def _flush_batch(buffer: list[_IngestResult]) -> None: needs_cleanup=r.needs_cleanup, stat=r.stat, page_texts=cast(list[dict], r.page_texts or []), + meta=r.meta, ) for r in buffer ] diff --git a/src/lilbee/data/ingest/title.py b/src/lilbee/data/ingest/title.py new file mode 100644 index 000000000..2498fe483 --- /dev/null +++ b/src/lilbee/data/ingest/title.py @@ -0,0 +1,68 @@ +"""Document title and source-level metadata derivation for ingest.""" + +from __future__ import annotations + +import re +from pathlib import PurePath +from typing import Protocol + +from lilbee.data.store import SourceMeta + +# Filename-stem separators flattened to spaces when no extracted title exists. +_STEM_SEPARATOR_RE = re.compile(r"[_\-\s]+") + + +class ExtractionMetadata(Protocol): + """The extraction-metadata fields lilbee folds into a source row. + + Structural, so it is satisfied by both xberg's public ``Metadata`` and the + native type its extractor actually hands back. The fields are ``object`` + because xberg annotates but does not enforce them: ``authors`` is declared + ``list[str] | None`` yet arrives as a bare ``str`` for a PDF ``/Author``, and + a non-string ``title`` is accepted. Callers narrow before using them. + """ + + @property + def title(self) -> object: ... + + @property + def authors(self) -> object: ... + + @property + def created_at(self) -> object: ... + + +def derive_title(source_name: str, metadata_title: object = None) -> str: + """Human-readable document title: the extracted title, else the cleaned filename stem. + + The stem cleanup flattens underscore/hyphen separators to spaces so BM25 + tokenizes ``survey_214.pdf`` into the same terms a query would use. + """ + if isinstance(metadata_title, str) and metadata_title.strip(): + return metadata_title.strip() + return _STEM_SEPARATOR_RE.sub(" ", PurePath(source_name).stem).strip() + + +def source_meta_from_extraction( + metadata: ExtractionMetadata | None, source_name: str +) -> SourceMeta: + """Fold xberg extraction metadata into a :class:`SourceMeta`. + + The title falls back to the filename stem; authors and creation date stay + empty (persisted NULL) when the extractor reports none. xberg annotates these + fields but does not enforce them: a PDF ``/Author`` arrives as a bare ``str`` + where ``list[str]`` is declared, so a string is treated as one author rather + than split into its characters, and non-string entries are coerced. + """ + raw_authors = metadata.authors if metadata is not None else None + if isinstance(raw_authors, str): + authors: list[str] = [raw_authors] + elif isinstance(raw_authors, (list, tuple)): + authors = [str(a) for a in raw_authors if a] + else: + authors = [] + return SourceMeta( + title=derive_title(source_name, metadata.title if metadata is not None else None), + authors=", ".join(a for a in authors if a), + created_at=str((metadata.created_at if metadata is not None else None) or ""), + ) diff --git a/src/lilbee/data/ingest/trace.py b/src/lilbee/data/ingest/trace.py new file mode 100644 index 000000000..bce1e4fcf --- /dev/null +++ b/src/lilbee/data/ingest/trace.py @@ -0,0 +1,87 @@ +"""Structured per-file extraction tracing, for sharing ingest diagnostics. + +Every xberg extraction emits one machine-parseable line on the ``lilbee.ingest.trace`` +logger: filename, wall-clock, chunk and page counts, and how many pages fell through +to OCR. Files that needed the vision model also emit a line on ``lilbee.ingest.vision`` +so ``grep vision`` yields exactly the set of scanned files. Enable with +``LILBEE_INGEST_TRACE=1`` (sets both loggers to DEBUG). +""" + +from __future__ import annotations + +import logging +import os +from dataclasses import dataclass +from pathlib import Path + +trace_log = logging.getLogger("lilbee.ingest.trace") +vision_log = logging.getLogger("lilbee.ingest.vision") + +_TRACE_ENV = "LILBEE_INGEST_TRACE" + + +@dataclass(frozen=True) +class ExtractionTrace: + """One xberg extraction's measured outcome.""" + + source: str + content_type: str + elapsed_s: float + page_count: int + chunk_count: int + ocr_pages: int + vision_configured: bool + + @property + def used_vision(self) -> bool: + """A page fell through to OCR and the OCR backend is the vision model.""" + return self.ocr_pages > 0 and self.vision_configured + + def as_line(self) -> str: + """A stable key=value line, easy to grep, diff, and hand to the xberg author.""" + return ( + f"extract source={self.source!r} type={self.content_type} " + f"elapsed_ms={self.elapsed_s * 1000:.0f} pages={self.page_count} " + f"chunks={self.chunk_count} ocr_pages={self.ocr_pages} " + f"vision={'yes' if self.used_vision else 'no'}" + ) + + +def configure_from_env() -> None: + """Enable trace/vision logging per LILBEE_INGEST_TRACE; mirror to a file when + LILBEE_INGEST_TRACE_FILE is set. + + The file handler exists because host apps own the root handlers: the TUI + logs WARNING+ to its file, so INFO trace lines vanish there even with the + loggers enabled. A dedicated handler on these two loggers makes the trace + destination independent of whichever front-end is running.""" + if os.environ.get("LILBEE_INGEST_TRACE", "").strip().lower() not in {"1", "true", "yes"}: + return + trace_log.setLevel(logging.DEBUG) + vision_log.setLevel(logging.INFO) + target = os.environ.get("LILBEE_INGEST_TRACE_FILE", "").strip() + if not target: + return + resolved = str(Path(target).absolute()) + for logger in (trace_log, vision_log): + if any( + isinstance(h, logging.FileHandler) and h.baseFilename == resolved + for h in logger.handlers + ): + continue + handler = logging.FileHandler(resolved) + handler.setLevel(logging.DEBUG) + handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(name)s: %(message)s")) + logger.addHandler(handler) + + +def trace_extraction(trace: ExtractionTrace) -> None: + """Log one extraction's outcome, plus a dedicated line if it needed vision.""" + trace_log.info("%s", trace.as_line()) + if trace.used_vision: + vision_log.info( + "vision-ocr source=%r ocr_pages=%d elapsed_ms=%.0f", + trace.source, + trace.ocr_pages, + trace.elapsed_s * 1000, + ) diff --git a/src/lilbee/data/ingest/types.py b/src/lilbee/data/ingest/types.py index 111269790..5651b37f9 100644 --- a/src/lilbee/data/ingest/types.py +++ b/src/lilbee/data/ingest/types.py @@ -5,7 +5,7 @@ from dataclasses import dataclass from enum import StrEnum from pathlib import Path -from typing import NamedTuple, TypedDict +from typing import NamedTuple, NotRequired, TypedDict from pydantic import BaseModel @@ -13,10 +13,19 @@ ChunkType, ConceptRecords, PageTextRecord, + SourceMeta, SourceStat, SourceStatBackfill, ) +# PDF and image content types route to paginated extraction; every other format +# routes to markdown extraction. content_type is derived per-file in +# discovery.classify_file (PDFs and images grouped; others keyed by extension). +PDF_CONTENT_TYPE = "pdf" +IMAGE_CONTENT_TYPE = "image" +MARKDOWN_OUTPUT = "markdown" +MARKDOWN_MIME = "text/markdown" + class FileToProcess(NamedTuple): """A file queued for ingestion with its metadata.""" @@ -39,24 +48,35 @@ class FileChangePlan(NamedTuple): stat_backfills: list[SourceStatBackfill] -# Minimum total chars for extracted text to be considered meaningful. -# 50 chars ≈ 12 words: if a PDF yields less, it's almost certainly a scanned -# document with no embedded text layer. Text PDFs with even just a title page -# easily exceed this threshold; blank/scan-only PDFs yield 0 chars. -MIN_MEANINGFUL_CHARS = 50 +class OcrBackendName(StrEnum): + """OCR backends lilbee selects in OcrConfig: xberg's tesseract or lilbee's vision plugin.""" -PDF_CONTENT_TYPE = "pdf" -IMAGE_CONTENT_TYPE = "image" -MARKDOWN_OUTPUT = "markdown" -TESSERACT_BACKEND = "tesseract" + TESSERACT = "tesseract" + LILBEE_VISION = "lilbee-vision" + + +class EmbeddingBackendName(StrEnum): + """Embedding backends registered with xberg. lilbee registers its own embedder + as a plugin so the semantic chunker detects boundaries with the same model that + vectorizes chunks.""" + + LILBEE = "lilbee" + + +class TokenizerBackendName(StrEnum): + """Tokenizer backends registered with xberg. lilbee registers its embedder's + tokenizer so ChunkSizing counts chunk budgets in the same tokens the embedder + consumes, instead of a chars-per-token heuristic. Separate registry from the + embedding backend, so sharing the ``lilbee`` name is fine.""" + + LILBEE = "lilbee" class ExtractMode(StrEnum): - """Extraction topology: pagination / OCR / output format.""" + """Extraction topology: paginated (PDFs/images) vs markdown output (text formats).""" MARKDOWN = "markdown" PAGINATED = "paginated" - PAGINATED_OCR = "paginated_ocr" class ChunkRecord(TypedDict): @@ -72,6 +92,9 @@ class ChunkRecord(TypedDict): chunk: str chunk_index: int vector: list[float] + # Stamped once per document by the pipeline (see _produce_records); None + # when the title is empty, so chunk rows persist NULL like the _sources table. + title: NotRequired[str | None] class SyncResult(BaseModel): @@ -124,7 +147,8 @@ class _IngestResult: travels with the records so the flush can delete the source's old chunks in the same transaction. ``page_texts`` carries the per-page text dataset rows and ``concept_records`` the file's concept-table rows, and ``entity_rows`` - the file's typed-entity rows, all written by the same flush. + the file's typed-entity rows, all written by the same flush. ``meta`` + carries the document's extraction-time metadata for the source row. """ name: str @@ -138,50 +162,4 @@ class _IngestResult: stat: SourceStat | None = None concept_records: ConceptRecords | None = None entity_rows: list[dict] | None = None - - -# Extension → content_type string for document formats handled by kreuzberg. -# Container formats (.zip/.tar/.7z/.pst) are deliberately absent: kreuzberg can -# extract them, but one file fans out to many inner documents, which the -# source/citation model can't express yet. -DOCUMENT_EXTENSION_MAP: dict[str, str] = { - **{ext: "text" for ext in (".md", ".txt", ".html", ".htm", ".rst", ".yaml", ".yml")}, - ".pdf": PDF_CONTENT_TYPE, - **{ - ext: ext.lstrip(".") - for ext in ( - ".docx", - ".xlsx", - ".pptx", - ".doc", - ".xls", - ".ppt", - ".rtf", - ".odt", - ".ods", - ".eml", - ".msg", - ) - }, - ".epub": "epub", - **{ - ext: IMAGE_CONTENT_TYPE - for ext in ( - ".png", - ".jpg", - ".jpeg", - ".tiff", - ".tif", - ".bmp", - ".webp", - ".gif", - ".jp2", - ".j2k", - ".j2c", - ".jpx", - ) - }, - **{ext: "data" for ext in (".csv", ".tsv", ".dbf")}, - ".xml": "xml", - **{ext: "json" for ext in (".json", ".jsonl")}, -} + meta: SourceMeta | None = None diff --git a/src/lilbee/data/ingest/vision_ocr_backend.py b/src/lilbee/data/ingest/vision_ocr_backend.py new file mode 100644 index 000000000..e849c7275 --- /dev/null +++ b/src/lilbee/data/ingest/vision_ocr_backend.py @@ -0,0 +1,197 @@ +"""lilbee's vision model exposed as a xberg custom OCR backend.""" + +from __future__ import annotations + +import json +import threading +import uuid +from contextlib import contextmanager +from dataclasses import dataclass +from importlib.metadata import PackageNotFoundError, version +from pathlib import Path +from typing import TYPE_CHECKING, Protocol + +from lilbee.data.ingest.types import MARKDOWN_MIME, OcrBackendName +from lilbee.vision import resolve_ocr_prompt + +if TYPE_CHECKING: + from collections.abc import Callable, Generator + + # The OcrBackend Protocol types the callback config as the public + # xberg.OcrConfig (kreuzberg-u4r); a backend typed against the native + # xberg._xberg.OcrConfig no longer satisfies it. The runtime object still + # arrives with backend_options as a JSON string (handled in _OcrConfigView). + from xberg import ExtractedDocument, OcrBackendType, OcrConfig + +# Token key inside OcrConfig.backend_options JSON. xberg does not propagate +# contextvars into process_image (xberg-4w9), so per-request state travels as +# a token on the config and is resolved through the registry below. +_REQUEST_TOKEN_KEY = "req" # noqa: S105 # JSON key name, not a secret + + +@dataclass(frozen=True) +class OcrRequestContext: + """Per-extraction state the backend needs but xberg won't carry for it.""" + + on_page: Callable[[], None] | None = None + timeout: float = 0.0 + + +class _OcrRequestRegistry: + """Token-keyed request contexts; lock-guarded (process_image runs on xberg threads).""" + + def __init__(self) -> None: + self._lock = threading.Lock() + self._by_token: dict[str, OcrRequestContext] = {} + + def register(self, ctx: OcrRequestContext) -> str: + token = uuid.uuid4().hex + with self._lock: + self._by_token[token] = ctx + return token + + def get(self, token: str | None) -> OcrRequestContext | None: + if token is None: + return None + with self._lock: + return self._by_token.get(token) + + def unregister(self, token: str) -> None: + with self._lock: + self._by_token.pop(token, None) + + +ocr_requests = _OcrRequestRegistry() + + +@contextmanager +def ocr_request( + *, on_page: Callable[[], None] | None = None, timeout: float = 0.0 +) -> Generator[str, None, None]: + """Register a per-extraction context and yield its token for OcrConfig.backend_options.""" + token = ocr_requests.register(OcrRequestContext(on_page=on_page, timeout=timeout)) + try: + yield token + finally: + ocr_requests.unregister(token) + + +def backend_options_for(token: str) -> dict[str, str]: + """Carry a request token in OcrConfig.backend_options for process_image to read.""" + return {_REQUEST_TOKEN_KEY: token} + + +class _OcrConfigView: + """Typed reader over the xberg OcrConfig object passed to process_image. + + Typed against the public ``xberg.OcrConfig`` from the OcrBackend Protocol, whose + fields are read directly as attributes. The native round-trip hands + ``backend_options`` back as a JSON string rather than the dict lilbee put in, so + ``request_token`` accepts both shapes. + """ + + def __init__(self, config: OcrConfig) -> None: + self._config = config + + @property + def vlm_prompt(self) -> str | None: + return self._config.vlm_prompt + + @property + def request_token(self) -> str | None: + # The native round-trip hands backend_options back as a JSON STRING, not + # the dict lilbee put in (alef serializes the map). Accept both shapes, + # or the token -- and with it on_page progress and the OCR timeout -- + # silently vanishes for every OCR call. + options = self._config.backend_options + if isinstance(options, str): + try: + options = json.loads(options) + except json.JSONDecodeError: + return None + token = options.get(_REQUEST_TOKEN_KEY) if isinstance(options, dict) else None + return token if isinstance(token, str) else None + + +class _OcrFn(Protocol): + # Positional-only so the provider's vision_ocr (named png_bytes) matches structurally. + def __call__( + self, image_bytes: bytes, model: str, prompt: str, /, *, timeout: float + ) -> str: ... + + +def _lilbee_version() -> str: + try: + return version("lilbee") + except PackageNotFoundError: + return "0" + + +class VisionOcrBackend: + """Routes xberg OCR calls to lilbee's vision model through the injected + ``ocr_fn`` (single-image OCR) and ``model_ref_fn`` (the active vision model).""" + + def __init__(self, *, ocr_fn: _OcrFn, model_ref_fn: Callable[[], str]) -> None: + self._ocr_fn = ocr_fn + self._model_ref_fn = model_ref_fn + + def name(self) -> str: + return OcrBackendName.LILBEE_VISION + + def version(self) -> str: + return _lilbee_version() + + def supported_languages(self) -> list[str]: + return [] + + def supports_language(self, lang: str) -> bool: + return True + + def initialize(self) -> None: ... + + def shutdown(self) -> None: ... + + def backend_type(self) -> OcrBackendType: + from xberg import OcrBackendType + + return OcrBackendType.CUSTOM + + def supports_table_detection(self) -> bool: + return False + + def supports_document_processing(self) -> bool: + return False + + def emits_structured_markdown(self) -> bool: + # Keep xberg's default: the pre-migration differential was validated with + # layout reconstruction on. The vision model does emit markdown, so flipping + # this to True (skip reconstruction) is a valid optimization, but it changes + # real OCR output and must be validated on the live integration first. + return False + + def process_image(self, image_bytes: bytes, config: OcrConfig) -> ExtractedDocument: + # xberg passes the OcrConfig as a native object and expects a native + # ExtractedDocument back (alef typed trait callbacks); read the request + # token and prompt override off the config, return the OCR text as markdown. + from xberg import ExtractedDocument + + view = _OcrConfigView(config) + model = self._model_ref_fn() + prompt = view.vlm_prompt or resolve_ocr_prompt(model) + ctx = ocr_requests.get(view.request_token) + text = self._ocr_fn(image_bytes, model, prompt, timeout=ctx.timeout if ctx else 0.0) + if ctx is not None and ctx.on_page is not None: + ctx.on_page() + return ExtractedDocument(content=text, mime_type=MARKDOWN_MIME) + + def process_image_file(self, path: str, config: OcrConfig) -> ExtractedDocument: + # OCR an image file by reading its bytes and delegating to process_image + # (mirrors xberg's default OcrBackend::process_image_file). + return self.process_image(Path(path).read_bytes(), config) + + def process_document(self, _path: str, _config: OcrConfig) -> ExtractedDocument: + # Image-only backend: xberg only calls this when supports_document_processing() + # is True, which it never is here, so document-level OCR is unreachable. + raise NotImplementedError( + "Document-level OCR is not supported by the lilbee vision backend" + ) diff --git a/src/lilbee/data/store/__init__.py b/src/lilbee/data/store/__init__.py index ab2b4f3fb..83a42f05f 100644 --- a/src/lilbee/data/store/__init__.py +++ b/src/lilbee/data/store/__init__.py @@ -28,6 +28,7 @@ RemoveResult, SearchChunk, SearchScope, + SourceMeta, SourceRecord, SourceStat, SourceStatBackfill, @@ -53,6 +54,7 @@ "RemoveResult", "SearchChunk", "SearchScope", + "SourceMeta", "SourceRecord", "SourceStat", "SourceStatBackfill", diff --git a/src/lilbee/data/store/core.py b/src/lilbee/data/store/core.py index 3bf452922..434f36a67 100644 --- a/src/lilbee/data/store/core.py +++ b/src/lilbee/data/store/core.py @@ -4,14 +4,13 @@ import logging import math -from collections.abc import Callable +from collections.abc import Callable, Sequence from contextlib import AbstractContextManager from datetime import UTC, datetime from pathlib import Path from typing import TYPE_CHECKING import pyarrow as pa -import pyarrow.compute as pc from lilbee.core.config import ( CHUNK_CONCEPTS_TABLE, @@ -28,10 +27,12 @@ from lilbee.core.security import validate_path_within from lilbee.runtime.lock import LOCK_TIMEOUT, write_lock -from .fusion import fuse_arms, normalized_bm25, vector_similarity +from .fusion import adaptive_weight_scale, fuse_arms, normalized_bm25, vector_similarity from .lance_helpers import ( + _CHUNK_COLUMN, _chunk_type_predicate, _has_fts_index, + _has_scalar_index, _has_vector_index, _safe_delete_unlocked, _sources_search_filter, @@ -64,6 +65,7 @@ PageTextRecord, RemoveResult, SearchChunk, + SourceMeta, SourceRecord, SourceStat, SourceStatBackfill, @@ -105,6 +107,41 @@ def _drop_unsupported_far_rows( _MAX_THRESHOLD = 1.0 _MAX_FILTER_ITERATIONS = 20 # safety cap to prevent runaway loops + +def _is_fts_position_overflow(exc: Exception) -> bool: + """True when *exc* is LanceDB's positional-FTS list-encoding overflow. + + A positional index (built by an intermediate dev commit) raises e.g. + "Max offset N exceeds length of values M" on optimize(); a positionless + rebuild is the remediation. Matched on message because LanceDB raises it + as a generic error type. + """ + msg = str(exc).lower() + return "offset" in msg and "exceeds" in msg + + +def _lexical_rows( + table: lancedb.table.Table, + query_text: str, + limit: int, + chunk_type: ChunkType | None, + column: str = _CHUNK_COLUMN, +) -> list[SearchChunk]: + """BM25 rows for *query_text* over a single FTS *column*. + + ``MatchQuery`` pins the column and matches plain terms, so an unpinned search + cannot widen to the title index and a quoted span cannot reach LanceDB as a + phrase (which the positionless index rejects). This is the one place FTS + queries are built; every arm goes through it. + """ + from lancedb.query import MatchQuery + + query = table.search(MatchQuery(query_text, column), query_type="fts").limit(limit) + if chunk_type: + query = query.where(_chunk_type_predicate(chunk_type)) + return [SearchChunk(**r) for r in query.to_list()] + + # Vector ANN index. IVF_PQ compresses vectors so search scales to millions; # refine_factor re-ranks the PQ candidates against full vectors to recover recall. _VECTOR_METRIC = "cosine" @@ -118,6 +155,14 @@ def _drop_unsupported_far_rows( # in place with the SOURCE_STAT_UNKNOWN sentinel. _SOURCE_STAT_COLUMNS = ("size_bytes", "mtime_ns", "stat_captured_ns") +# Extraction-metadata columns of ``_sources``; nullable strings, so legacy +# tables migrate in place with NULL (meaning "extractor reported nothing"). +_SOURCE_META_COLUMNS = ("title", "authors", "created_at") + +# Document-title column of the chunks table; nullable so pre-title rows and +# writers that carry no title (wiki pages) read as NULL. +_TITLE_COLUMN = "title" + # (table, source column) pairs deleted when a source's rows are replaced. The # concept nodes/edges tables carry no source column (corpus-level aggregates), # so only the per-chunk concept mapping is source-scoped. @@ -137,6 +182,12 @@ def _drop_unsupported_far_rows( # bounds the decoded-text working set while the scan stays columnar. _TERM_SCAN_BATCH_ROWS = 20_000 +# The title arm collapses each matched document to one row, so it over-fetches +# to gather enough distinct documents before deduping. Bounded so a title that +# hits a huge document can't scan the whole corpus. +_TITLE_FETCH_FACTOR = 20 +_TITLE_MIN_FETCH = 200 + def _ann_nprobes(row_count: int) -> int: """Partitions to probe: a fixed fraction of the IVF partition count (~sqrt(N)), floored.""" @@ -174,6 +225,10 @@ class Store: def __init__(self, config: Config) -> None: self._config = config self._fts_ready: bool = False + # Scalar indexes (source/chunk_type) are built at ingest, but a store + # served without a fresh ingest never ran that path, so search builds + # them lazily once. This guards the one-shot per process. + self._scalar_ready: bool = False self._db: lancedb.DBConnection | None = None # Cache of {filename: ingested_at} rebuilt only when sources # mutate; callers (temporal filter) hit it per-query. @@ -218,10 +273,25 @@ def _chunks_schema(self) -> pa.Schema: pa.field("line_end", pa.int32()), pa.field("chunk", pa.utf8()), pa.field("chunk_index", pa.int32()), + pa.field(_TITLE_COLUMN, pa.utf8()), pa.field("vector", pa.list_(pa.float32(), self._config.embedding_dim)), ] ) + def _chunks_table(self) -> lancedb.table.Table: + """Open/create the chunks table, adding the title column to pre-title tables.""" + table = ensure_table(self.get_db(), CHUNKS_TABLE, self._chunks_schema()) + if _TITLE_COLUMN not in table.schema.names: + table.add_columns({_TITLE_COLUMN: "CAST(NULL AS STRING)"}) + # Titles are stamped only at ingest, so existing rows stay NULL and + # the title arm cannot see them. Point the user at a rebuild. + log.warning( + "Added the title column to an existing store. Documents indexed " + "before this upgrade have no title, so the title-search arm will " + "not match them until you run `lilbee rebuild`." + ) + return table + def get_meta(self) -> StoreMeta | None: """Return the persisted store metadata row, or ``None`` if unset.""" table = self.open_table(META_TABLE) @@ -410,15 +480,131 @@ def ensure_fts_index(self) -> None: return try: if _has_fts_index(table): - table.optimize() - log.debug("FTS index optimized on '%s'", CHUNKS_TABLE) + # An existing index serves queries regardless of how the + # best-effort optimize() below turns out, so hybrid is ready + # either way. + self._fts_ready = True + try: + # One optimize folds new rows into every index on the + # table, the title index included. + table.optimize() + log.debug("FTS index optimized on '%s'", CHUNKS_TABLE) + except Exception as exc: + if _is_fts_position_overflow(exc): + # A pre-fix positional index overflows LanceDB's list + # encoding on every optimize(). Rebuild it positionless + # once so index maintenance can complete again. + self._rebuild_fts_positionless(table) + else: + log.warning( + "FTS optimize() failed; the existing index still " + "serves hybrid search", + exc_info=True, + ) else: - table.create_fts_index("chunk", replace=False) + # Positionless: with_position=True overflows LanceDB's list + # encoding on optimize() for a large corpus. Positions only + # serve exact-phrase queries, which lilbee never issues (FTS + # queries match plain terms), so the index never needs them. + table.create_fts_index(_CHUNK_COLUMN, replace=False, with_position=False) + self._fts_ready = True log.debug("FTS index created on '%s'", CHUNKS_TABLE) - self._fts_ready = True + # Only the opt-in title arm needs the title index; without this a + # second FTS index is built (and optimized) on every store even + # though nothing queries it. + if self._config.title_search: + self._ensure_title_fts_unlocked(table) except Exception: log.debug("FTS index ensure failed (empty table?)", exc_info=True) + def _ensure_title_fts_unlocked(self, table: lancedb.table.Table) -> None: + """Create the title FTS index when the column exists. Caller holds ``write_lock()``. + + Failure never blocks the chunk index: the title arm feature-detects the + index per query, so a store without it simply searches without titles. + """ + if _TITLE_COLUMN not in table.schema.names or _has_fts_index(table, _TITLE_COLUMN): + return + try: + # Positionless for the same reason as the chunk index. + table.create_fts_index(_TITLE_COLUMN, replace=False, with_position=False) + log.debug("Title FTS index created on '%s'", CHUNKS_TABLE) + except Exception: + # Only reached with title_search enabled, so a silent failure means + # the user's opted-in title arm quietly does nothing. Warn, don't hide. + log.warning( + "Title FTS index creation failed; the title-search arm will " + "contribute nothing until it can be built", + exc_info=True, + ) + + def _rebuild_fts_positionless(self, table: lancedb.table.Table) -> None: + """Replace positional FTS indexes with positionless ones. Caller holds the lock. + + The one-shot remediation for a store whose index was built + ``with_position=True`` and now overflows on every ``optimize()``. The + title index is rebuilt too when the title arm is enabled. + """ + try: + table.create_fts_index(_CHUNK_COLUMN, replace=True, with_position=False) + if self._config.title_search and _TITLE_COLUMN in table.schema.names: + table.create_fts_index(_TITLE_COLUMN, replace=True, with_position=False) + log.warning("Rebuilt the FTS index positionless after a positional-index overflow") + except Exception: + log.warning( + "Positionless FTS rebuild failed; the existing index still serves", + exc_info=True, + ) + + def ensure_scalar_indexes(self) -> None: + """Build scalar indexes on the columns lilbee filters by. + + ``source`` and ``chunk_type`` predicates run as prefilters (LanceDB's + default), but without an index each is a full-table scan. A BTree on the + high-cardinality ``source`` (known-item lookup and per-source fetches) + and a Bitmap on the low-cardinality ``chunk_type`` (raw/wiki/table scope) + turn those into indexed lookups. Missing columns and empty tables are + skipped; ``optimize()`` folds later rows into every index, this one too. + """ + with self._write_lock(): + self._ensure_scalar_index_on( + CHUNKS_TABLE, (("source", "BTREE"), ("chunk_type", "BITMAP")) + ) + # The concept-boost path filters chunk_concepts by chunk_source once + # per search result (see ConceptGraph._chunk_concepts_from), so index + # it too or every boosted query full-scans the table. + self._ensure_scalar_index_on(CHUNK_CONCEPTS_TABLE, (("chunk_source", "BTREE"),)) + self._scalar_ready = True + + def _ensure_scalar_index_on( + self, table_name: str, columns: tuple[tuple[str, str], ...] + ) -> None: + """Build the given (column, index_type) scalar indexes on *table_name*. + + Caller holds ``write_lock()``. Each column gets its own try so one + failure does not skip the rest; a failure on a populated table warns + (the prefilter speedup is silently lost) while an empty table's is debug. + """ + table = self.open_table(table_name) + if table is None: + return + names = table.schema.names + fail_level = logging.WARNING if table.count_rows() > 0 else logging.DEBUG + for column, index_type in columns: + if column not in names or _has_scalar_index(table, column): + continue + try: + table.create_scalar_index(column, index_type=index_type, replace=False) + log.debug("Scalar (%s) index created on '%s.%s'", index_type, table_name, column) + except Exception: + log.log( + fail_level, + "Scalar index create failed on '%s.%s'", + table_name, + column, + exc_info=True, + ) + def ensure_vector_index(self, *, force: bool = False) -> bool: """Build or refresh the ANN vector index when the corpus is large enough. @@ -470,11 +656,11 @@ def add_chunks(self, records: list[dict]) -> int: embedding_dim = self._config.embedding_dim self._ensure_embedding_compat() self._fts_ready = False + self._scalar_ready = False if not records: return 0 _check_vector_dims(records, embedding_dim) - db = self.get_db() - table = ensure_table(db, CHUNKS_TABLE, self._chunks_schema()) + table = self._chunks_table() table.add(records) if self.get_meta() is None: self._write_meta_unlocked( @@ -487,7 +673,8 @@ def bm25_probe( ) -> list[SearchChunk]: """Quick BM25-only search for confidence checking. Returns up to top_k results. - When *chunk_type* is set, only chunks of that type ("raw" or "wiki") are returned. + When *chunk_type* is set, only matching chunks are returned; a "raw" + filter also covers table chunks. """ table = self.open_table(CHUNKS_TABLE) if table is None: @@ -497,11 +684,7 @@ def bm25_probe( if not self._fts_ready: return [] try: - query = table.search(query_text, query_type="fts") - if chunk_type: - query = query.where(_chunk_type_predicate(chunk_type)) - rows = query.limit(top_k).to_list() - results = [SearchChunk(**r) for r in rows] + results = _lexical_rows(table, query_text, top_k, chunk_type) norms = normalized_bm25([r.bm25_score or 0.0 for r in results]) return [ r.model_copy(update={"score": norm}) for r, norm in zip(results, norms, strict=True) @@ -522,7 +705,8 @@ def search( Results with distance > max_distance are filtered out (vector-only path). Pass max_distance=0 to disable filtering. - When *chunk_type* is set, only chunks of that type ("raw" or "wiki") are returned. + When *chunk_type* is set, only matching chunks are returned; a "raw" + filter also covers table chunks. Raises ``EmbeddingModelMismatchError`` if the persisted ``_meta`` row was written under a different embedding model than the current ``cfg``. @@ -538,6 +722,11 @@ def search( self.canonicalize_meta_if_legacy() self._ensure_embedding_compat() + if not self._scalar_ready: + # A serve-only store never ran ingest, where scalar indexes are + # built; without them the source/chunk_type prefilters full-scan. + self.ensure_scalar_indexes() + if query_text and not self._fts_ready: self.ensure_fts_index() @@ -600,11 +789,50 @@ def _fts_arm( limit: int, chunk_type: ChunkType | None, ) -> list[SearchChunk]: - """BM25-arm candidates.""" - query = table.search(query_text, query_type="fts").limit(limit) - if chunk_type: - query = query.where(_chunk_type_predicate(chunk_type)) - return [SearchChunk(**r) for r in query.to_list()] + """BM25-arm candidates over the chunk text.""" + return _lexical_rows(table, query_text, limit, chunk_type) + + def _title_arm( + self, + table: lancedb.table.Table, + query_text: str, + limit: int, + chunk_type: ChunkType | None, + ) -> list[SearchChunk]: + """One BM25 row per document whose title matches, in title-relevance order. + + Every chunk of a document carries the same title, so all of its chunks + tie on BM25 and a plain ``limit`` would return an arbitrary tie-ordered + subset of a single document. Instead this over-fetches, collapses each + source to one deterministic representative (its first chunk), and returns + the top *limit* documents ordered by title score -- so "a query naming a + document by title surfaces its chunks" holds as one stable row per doc. + + Empty when the store predates the title column or its FTS index (old + indexes keep working) and empty on any query-time failure: the optional + title arm must never take down the healthy chunk arm, so its failure + degrades to no-titles, mirroring ``bm25_probe``. + """ + if not _has_fts_index(table, _TITLE_COLUMN): + return [] + try: + rows = _lexical_rows( + table, + query_text, + max(limit * _TITLE_FETCH_FACTOR, _TITLE_MIN_FETCH), + chunk_type, + column=_TITLE_COLUMN, + ) + except Exception: + log.debug("Title arm search failed; contributing no title rows", exc_info=True) + return [] + best: dict[str, SearchChunk] = {} + for row in rows: + seen = best.get(row.source) + if seen is None or row.chunk_index < seen.chunk_index: + best[row.source] = row + ordered = sorted(best.values(), key=lambda r: (-(r.bm25_score or 0.0), r.source)) + return ordered[:limit] def _hybrid_search( self, @@ -615,23 +843,53 @@ def _hybrid_search( max_distance: float, chunk_type: ChunkType | None = None, ) -> list[SearchChunk]: - """Dual-arm retrieval fused by reciprocal rank; the fused ordering is final. + """Multi-arm retrieval fused by weighted reciprocal rank; the fused ordering is final. + + A vector arm and a chunk-BM25 arm always run; a title-BM25 arm joins + when ``cfg.title_search`` is on. Each row's fused score is the + weight-normalized sum of its arm contributions: the vector arm has + weight 1.0, the lexical arm ``cfg.lexical_fusion_weight`` (scaled per + query when ``cfg.adaptive_fusion`` is on), the title arm + ``cfg.title_search_weight``. So a row a single peaked arm is certain + about scores that arm's share of the total weight, not a fixed 0.5. Each arm fetches exactly ``top_k`` rows. Deeper pools measurably hurt - rank fusion: rows a single arm is certain about score a fixed 0.5, - while rows both arms rank mid-pool accumulate two contributions, so - widening the arms floods the fused top-k with both-arm mediocrity and - buries single-arm certainty (lexical identifier hits above all). - - No diversity selection runs here either. For lexical queries the - relevant passages are often mutually similar (they quote the same - identifiers), which is exactly what MMR penalizes: selecting the - fused pool through MMR measurably traded relevant lexical hits for - diverse off-topic neighbors on graded evaluation. + rank fusion by flooding the fused top-k with both-arm mediocrity and + burying single-arm certainty (lexical identifier hits above all). No + MMR runs here: lexical passages are often mutually similar, which MMR + penalizes, trading relevant hits for off-topic neighbors. + + Title rows carry ``bm25_score``, so a title match counts as lexical + support for the distance exemption like any other lexical hit. """ + title_rows: list[SearchChunk] = [] + if self._config.title_search: + title_rows = self._title_arm(table, query_text, top_k, chunk_type) + vector_rows = self._vector_arm(table, query_vector, top_k, chunk_type) + base_lexical_weight = self._config.lexical_fusion_weight + base_title_weight = self._config.title_search_weight + lexical_weight = base_lexical_weight + title_weight = base_title_weight + if self._config.adaptive_fusion: + # Gate the lexical arms per query by how peaked the vector ranking is. + # The title arm is lexical too, so the same factor scales it; leaving + # it at full weight would re-admit the signal this just silenced. + scale = adaptive_weight_scale(vector_rows, self._config.adaptive_fusion_margin) + lexical_weight = base_lexical_weight * scale + title_weight = base_title_weight * scale + # Normalize against the configured weight budget, not this query's adapted + # weights or whether its title arm happened to return rows, so scores stay + # comparable across the sub-searches Searcher merges (query + variants). + weight_total = ( + 1.0 + base_lexical_weight + (base_title_weight if self._config.title_search else 0.0) + ) fused = fuse_arms( - self._vector_arm(table, query_vector, top_k, chunk_type), + vector_rows, self._fts_arm(table, query_text, top_k, chunk_type), + title_rows, + lexical_weight=lexical_weight, + title_weight=title_weight, + weight_total=weight_total, ) fused = _drop_unsupported_far_rows(fused, max_distance) return fused[:top_k] @@ -841,24 +1099,40 @@ def count_chunks(self) -> int: return table.count_rows() if table is not None else 0 def get_chunks_by_source(self, source: str) -> list[SearchChunk]: - """Return every chunk whose ``source`` equals *source*.""" + """Return every chunk whose ``source`` equals *source*. + + The database does the filtering, so only the matching rows are read. + A query failure raises rather than falling back to a whole-table scan: + a document's chunks are a bounded read, and the scan that would rescue + it costs the entire index, vectors included, in memory. + """ table = self.open_table(CHUNKS_TABLE) if table is None: return [] escaped = escape_sql_string(source) - try: - rows = table.search().where(f"source = '{escaped}'").limit(None).to_list() - except Exception: - # FTS-enabled tables return a query builder that cannot - # handle .where() on arbitrary columns; fall through to a - # pyarrow.compute filter on the Arrow table so the source - # match runs in C++ without materializing non-matching rows. - log.debug("get_chunks_by_source search() failed, using Arrow fallback", exc_info=True) - arrow_tbl = table.to_arrow() - filtered = arrow_tbl.filter(pc.equal(arrow_tbl["source"], source)) - rows = filtered.to_pylist() + rows = table.search().where(f"source = '{escaped}'").limit(None).to_list() return [SearchChunk(**r) for r in rows] + def get_chunks_by_indices(self, source: str, indices: Sequence[int]) -> list[SearchChunk]: + """Return *source*'s chunks whose ``chunk_index`` is in *indices*. + + Rows come back in ``chunk_index`` order; indices past either end of + the document are simply absent from the result. Filtering happens in + the database for the same reason as :meth:`get_chunks_by_source`: + neighbor expansion runs once per hit source per query, so a + whole-table rescue would spike memory on the hottest path there is. + """ + if not indices: + return [] + table = self.open_table(CHUNKS_TABLE) + if table is None: + return [] + escaped = escape_sql_string(source) + wanted = ", ".join(str(int(i)) for i in indices) + predicate = f"source = '{escaped}' AND chunk_index IN ({wanted})" + rows = table.search().where(predicate).limit(None).to_list() + return sorted((SearchChunk(**r) for r in rows), key=lambda c: c.chunk_index) + def _delete_by_sources_unlocked(self, sources: list[str]) -> None: """Delete the sources' chunks, page texts, and chunk-concept rows. @@ -964,8 +1238,14 @@ def _source_row( chunk_count: int, source_type: str, stat: SourceStat | None, + meta: SourceMeta | None = None, ) -> dict: - """Build one ``_sources`` row, defaulting absent stat to the unknown sentinel.""" + """Build one ``_sources`` row, defaulting absent stat to the unknown sentinel. + + Absent extraction metadata persists as NULL, matching rows written + before the metadata columns existed. + """ + meta = meta or SourceMeta() return { "filename": filename, "file_hash": file_hash, @@ -975,14 +1255,19 @@ def _source_row( "size_bytes": stat.size_bytes if stat else SOURCE_STAT_UNKNOWN, "mtime_ns": stat.mtime_ns if stat else SOURCE_STAT_UNKNOWN, "stat_captured_ns": stat.captured_ns if stat else SOURCE_STAT_UNKNOWN, + "title": meta.title or None, + "authors": meta.authors or None, + "created_at": meta.created_at or None, } def _sources_table(self) -> lancedb.table.Table: - """Open/create ``_sources``, adding the stat columns to pre-stat tables.""" + """Open/create ``_sources``, adding the stat and metadata columns to older tables.""" table = ensure_table(self.get_db(), SOURCES_TABLE, _sources_schema()) - missing = [name for name in _SOURCE_STAT_COLUMNS if name not in table.schema.names] + defaults = {name: f"CAST({SOURCE_STAT_UNKNOWN} AS BIGINT)" for name in _SOURCE_STAT_COLUMNS} + defaults |= {name: "CAST(NULL AS STRING)" for name in _SOURCE_META_COLUMNS} + missing = {name: sql for name, sql in defaults.items() if name not in table.schema.names} if missing: - table.add_columns({name: f"CAST({SOURCE_STAT_UNKNOWN} AS BIGINT)" for name in missing}) + table.add_columns(missing) return table def _replace_source_rows_unlocked(self, rows: list[dict]) -> None: @@ -1007,9 +1292,10 @@ def upsert_source( chunk_count: int, source_type: SourceType = SourceType.DOCUMENT, stat: SourceStat | None = None, + meta: SourceMeta | None = None, ) -> None: """Add or update a source tracking record.""" - row = self._source_row(filename, file_hash, chunk_count, source_type, stat) + row = self._source_row(filename, file_hash, chunk_count, source_type, stat, meta) with self._write_lock(): self._replace_source_rows_unlocked([row]) self._invalidate_source_cache() @@ -1062,12 +1348,13 @@ def write_chunks_batch(self, items: list[ChunkWrite]) -> int: embedding_dim = self._config.embedding_dim self._ensure_embedding_compat() self._fts_ready = False + self._scalar_ready = False all_records = [rec for it in items for rec in it.records] _check_vector_dims(all_records, embedding_dim) db = self.get_db() self._cleanup_batch_unlocked(items) self._add_page_texts_unlocked(db, items) - self._add_chunk_records_unlocked(db, all_records, embedding_model, embedding_dim) + self._add_chunk_records_unlocked(all_records, embedding_model, embedding_dim) self._replace_source_rows_unlocked(self._batch_source_rows(items)) self._invalidate_source_cache() return len(all_records) @@ -1086,7 +1373,6 @@ def _add_page_texts_unlocked(self, db: lancedb.DBConnection, items: list[ChunkWr def _add_chunk_records_unlocked( self, - db: lancedb.DBConnection, all_records: list[dict], embedding_model: str, embedding_dim: int, @@ -1094,14 +1380,16 @@ def _add_chunk_records_unlocked( """Add the batch's chunk rows, writing meta on first use. Caller holds ``write_lock()``.""" if not all_records: return - ensure_table(db, CHUNKS_TABLE, self._chunks_schema()).add(all_records) + self._chunks_table().add(all_records) if self.get_meta() is None: self._write_meta_unlocked(embedding_model=embedding_model, embedding_dim=embedding_dim) def _batch_source_rows(self, items: list[ChunkWrite]) -> list[dict]: """One ``_sources`` row per batched document.""" return [ - self._source_row(it.source, it.file_hash, len(it.records), it.source_type, it.stat) + self._source_row( + it.source, it.file_hash, len(it.records), it.source_type, it.stat, it.meta + ) for it in items ] @@ -1418,6 +1706,7 @@ def close(self) -> None: """Release the database connection and reset state.""" self._db = None self._fts_ready = False + self._scalar_ready = False def drop_all(self) -> None: """Drop every table except ``_memories`` -- used by rebuild. @@ -1428,6 +1717,7 @@ def drop_all(self) -> None: """ with self._write_lock(): self._fts_ready = False + self._scalar_ready = False db = self.get_db() for name in _table_names(db): if name == MEMORIES_TABLE: diff --git a/src/lilbee/data/store/fusion.py b/src/lilbee/data/store/fusion.py index 157784f47..743631f70 100644 --- a/src/lilbee/data/store/fusion.py +++ b/src/lilbee/data/store/fusion.py @@ -1,12 +1,22 @@ -"""Reciprocal-rank fusion of the vector and BM25 retrieval arms. +"""Reciprocal-rank fusion of the vector, BM25, and optional title arms. Each arm contributes ``(K + 1) / (K + rank)`` for the rows it retrieved (rank is 1-based, K = 60, the standard RRF constant); a row's canonical -score is the mean of its two arm contributions, so it lives in [0, 1] and -a row ranked first by both arms scores exactly 1. Rows seen by only one -arm score half of that arm's contribution, which still places an arm's -top hit above every row deep in the other arm: the property that keeps a -lexical-only identifier match visible next to dense neighbors. +score is the weight-normalized sum of its arm contributions, so it lives +in [0, 1] and a row ranked first by every arm scores exactly 1. Rows seen +by only one arm score that arm's share of the total weight, which still +places an arm's top hit above every row deep in the other arms: the +property that keeps a lexical-only identifier match visible next to dense +neighbors. The vector arm weighs 1; the chunk-BM25 arm weighs +``lexical_weight`` (1.0 = equal voice, lower lets a strong dense arm +dominate); the optional title arm weighs ``title_weight``. Weights rescale +the shares without leaving the canonical range. + +Scores normalize against the configured weight budget, not the per-query +adapted ``lexical_weight`` or whether a given query's title arm returned rows +(see ``fuse_arms``'s *weight_total*). That fixed denominator keeps scores on +one scale across queries and across the sub-searches a caller merges, so +``min_relevance_score`` stays a usable floor. Rank fusion is deliberate. A convex combination of normalized raw scores (``alpha * vector_similarity + (1 - alpha) * normalized_bm25``) was tried @@ -22,17 +32,56 @@ from __future__ import annotations +from statistics import fmean + from .types import SearchChunk # Standard RRF smoothing constant (Cormack, Clarke & Buettcher 2009). _RRF_K = 60 +# Adaptive fusion needs a top hit plus at least one field row to measure a margin. +_MIN_ROWS_FOR_MARGIN = 2 + def vector_similarity(distance: float) -> float: """Cosine distance to canonical [0, 1] similarity (distance spans [0, 2]).""" return max(0.0, min(1.0, 1.0 - distance)) +def adaptive_weight_scale(vector_rows: list[SearchChunk], margin_scale: float) -> float: + """A [0, 1] factor to shrink the lexical arms by when the vector arm is + confident about this query. + + A *peaked* vector ranking -- a top hit standing well clear of the field -- + means the dense embedder already located the answer and the lexical arms + mostly add term-match noise. A *flat* ranking means dense is unsure and + BM25's exact-term matching is worth trusting. The confidence signal is the + margin between the top similarity and the mean of the rest, divided by + *margin_scale*: at or above that margin the factor is 0 (arms silenced), at + zero margin it is 1 (arms kept), scaling linearly between. Returns 1.0 when + there is nothing to measure (fewer than two scored rows) or when + *margin_scale* <= 0 (adaptation off). + """ + if margin_scale <= 0: + return 1.0 + sims = sorted( + (vector_similarity(r.distance) for r in vector_rows if r.distance is not None), + reverse=True, + ) + if len(sims) < _MIN_ROWS_FOR_MARGIN: + return 1.0 + margin = max(0.0, sims[0] - fmean(sims[1:])) + confidence = min(1.0, margin / margin_scale) + return 1.0 - confidence + + +def adaptive_lexical_weight( + vector_rows: list[SearchChunk], base_weight: float, margin_scale: float +) -> float: + """The lexical arm's *base_weight* scaled by :func:`adaptive_weight_scale`.""" + return base_weight * adaptive_weight_scale(vector_rows, margin_scale) + + def normalized_bm25(scores: list[float]) -> list[float]: """Scale raw BM25 scores against the list maximum, into (0, 1]. @@ -55,28 +104,64 @@ def _rank_weight(rank: int) -> float: return (_RRF_K + 1) / (_RRF_K + rank) -def fuse_arms( - vector_rows: list[SearchChunk], - fts_rows: list[SearchChunk], -) -> list[SearchChunk]: - """Merge the two arms into one list scored by reciprocal rank. +def _merge_arm( + merged: dict[tuple[str, int], SearchChunk], + rows: list[SearchChunk], + share: float, +) -> None: + """Fold one arm's ranked rows into *merged*, each contributing *share* of its rank weight. - Both arms weigh equally. Rows found by both arms carry both provenance - fields (``distance`` from the vector arm, ``bm25_score`` from the FTS - arm). The result is sorted by ``score`` descending and deduplicated on - ``(source, chunk_index)``. + A lexical row (title or chunk FTS) carries ``bm25_score``; when the row was + already seen, that provenance is kept from whichever lexical arm set it + first, so the lexical-support exemption applies either way. """ - merged: dict[tuple[str, int], SearchChunk] = {} - for rank, row in enumerate(vector_rows, start=1): - merged[_key(row)] = row.model_copy(update={"score": _rank_weight(rank) / 2}) - for rank, row in enumerate(fts_rows, start=1): + for rank, row in enumerate(rows, start=1): key = _key(row) - lexical = _rank_weight(rank) / 2 + contribution = _rank_weight(rank) * share seen = merged.get(key) if seen is None: - merged[key] = row.model_copy(update={"score": lexical}) + merged[key] = row.model_copy(update={"score": contribution}) else: - merged[key] = seen.model_copy( - update={"score": (seen.score or 0.0) + lexical, "bm25_score": row.bm25_score} - ) + update: dict[str, object] = {"score": (seen.score or 0.0) + contribution} + if seen.bm25_score is None and row.bm25_score is not None: + update["bm25_score"] = row.bm25_score + merged[key] = seen.model_copy(update=update) + + +def fuse_arms( + vector_rows: list[SearchChunk], + fts_rows: list[SearchChunk], + title_rows: list[SearchChunk] | None = None, + *, + lexical_weight: float = 1.0, + title_weight: float = 1.0, + weight_total: float | None = None, +) -> list[SearchChunk]: + """Merge the arms into one list scored by reciprocal rank. + + The vector arm weighs 1; the chunk-FTS (lexical) arm weighs *lexical_weight* + relative to it (1.0 = equal voice, lower lets a strong dense arm dominate); + a non-empty *title_rows* arm joins at *title_weight*. Rows found by several + arms carry every provenance field (``distance`` from the vector arm, + ``bm25_score`` from the FTS arms). The result is sorted by ``score`` + descending and deduplicated on ``(source, chunk_index)``. + + *weight_total* is the constant this call normalizes scores against. Pass the + configured weight budget so scores from separate sub-searches -- whose + adaptive *lexical_weight* and per-query title-arm presence differ -- stay on + one comparable scale when a caller merges them. It is a uniform divisor, so + the ranking within this call is unchanged. When omitted it defaults to the + arms present in this call (the standalone behavior). + """ + if weight_total is None: + weight_total = 1.0 + lexical_weight + (title_weight if title_rows else 0.0) + merged: dict[tuple[str, int], SearchChunk] = {} + _merge_arm(merged, vector_rows, 1.0 / weight_total) + # A zero-weight arm contributes nothing, so skip it rather than folding in + # zero-score rows that would still carry lexical provenance (and its + # downstream distance/structural exemptions) on no real support. + if lexical_weight > 0: + _merge_arm(merged, fts_rows, lexical_weight / weight_total) + if title_rows and title_weight > 0: + _merge_arm(merged, title_rows, title_weight / weight_total) return sorted(merged.values(), key=lambda r: r.score or 0.0, reverse=True) diff --git a/src/lilbee/data/store/lance_helpers.py b/src/lilbee/data/store/lance_helpers.py index 6565f3ac1..8ae411589 100644 --- a/src/lilbee/data/store/lance_helpers.py +++ b/src/lilbee/data/store/lance_helpers.py @@ -20,6 +20,11 @@ log = logging.getLogger(__name__) +# The chunks table's body-text column: FTS-indexed and searched by the lexical +# arm, named here so the store's query and index code shares one spelling with +# its sibling _TITLE_COLUMN. +_CHUNK_COLUMN = "chunk" + def install_lancedb_thread_error_suppressor() -> None: """Install a ``threading.excepthook`` that swallows lancedb shutdown noise. @@ -120,25 +125,40 @@ def _chunk_type_predicate(chunk_type: ChunkType | str) -> str: Rows written before ``chunk_type`` was populated land as NULL. They are semantically raw, so a ``'raw'`` filter still includes them; a - ``'wiki'`` filter excludes them. + ``'wiki'`` filter excludes them. Table chunks are document content, + so the ``'raw'`` scope covers them too; filter on ``'table'`` to + isolate them. """ escaped = escape_sql_string(chunk_type) if chunk_type == ChunkType.RAW: - return f"(chunk_type = '{escaped}' OR chunk_type IS NULL)" + return f"(chunk_type IN ('{escaped}', '{ChunkType.TABLE}') OR chunk_type IS NULL)" return f"chunk_type = '{escaped}'" -def _has_fts_index(table: lancedb.table.Table) -> bool: - """Return True when an FTS index on the chunk column already exists.""" +def _has_fts_index(table: lancedb.table.Table, column: str = _CHUNK_COLUMN) -> bool: + """Return True when an FTS index on *column* already exists.""" try: for idx in table.list_indices(): - if idx.index_type == "FTS" and "chunk" in idx.columns: + if idx.index_type == "FTS" and column in idx.columns: return True except Exception: return False return False +def _has_scalar_index(table: lancedb.table.Table, column: str) -> bool: + """Return True when a scalar index on *column* already exists. + + lilbee only builds scalar indexes on ``source`` and ``chunk_type``, and + never an FTS or vector index on those columns, so any index touching the + column is the scalar one. + """ + try: + return any(column in idx.columns for idx in table.list_indices()) + except Exception: + return False + + def _has_vector_index(table: lancedb.table.Table) -> bool: """Return True when an ANN index on the vector column already exists. diff --git a/src/lilbee/data/store/ranking.py b/src/lilbee/data/store/ranking.py index 5904269c1..f409680f3 100644 --- a/src/lilbee/data/store/ranking.py +++ b/src/lilbee/data/store/ranking.py @@ -4,7 +4,7 @@ import numpy as np -from lilbee.core.config import cfg +from lilbee.core.config import active_config from .types import SearchChunk @@ -33,7 +33,7 @@ def mmr_rerank( ``mmr_lambda`` controls the relevance/diversity tradeoff: 0.0 = maximum diversity, 1.0 = pure relevance. - Defaults to ``cfg.mmr_lambda`` (0.5). + Defaults to the active config's ``mmr_lambda`` (0.5). Complexity: O(top_k · N · D) time, O(N · D) space for N candidates of dimension D. Each outer iteration updates a running max-redundancy @@ -44,7 +44,10 @@ def mmr_rerank( since sub-float32 differences are below retrieval signal. """ if mmr_lambda is None: - mmr_lambda = cfg.mmr_lambda + # active_config(), not the process-global cfg: under the library API + # a config_scope binding is the caller's config, and every sibling in + # the data path resolves through the scope. + mmr_lambda = active_config().mmr_lambda if len(results) <= top_k: return results @@ -64,9 +67,10 @@ def mmr_rerank( available = np.ones(n, dtype=bool) selected: list[SearchChunk] = [] - for picks in range(top_k): - redundancy_term = max_redundancy if picks > 0 else np.zeros(n, dtype=np.float32) - score = mmr_lambda * relevance - (1.0 - mmr_lambda) * redundancy_term + for _ in range(top_k): + # max_redundancy starts at zeros, so the first pass is pure relevance + # without needing a special case for it. + score = mmr_lambda * relevance - (1.0 - mmr_lambda) * max_redundancy # Mask already-picked candidates so argmax skips them. score = np.where(available, score, -np.inf) best = int(np.argmax(score)) diff --git a/src/lilbee/data/store/schema.py b/src/lilbee/data/store/schema.py index 21bd15324..60265911c 100644 --- a/src/lilbee/data/store/schema.py +++ b/src/lilbee/data/store/schema.py @@ -38,6 +38,9 @@ def _sources_schema() -> pa.Schema: pa.field("size_bytes", pa.int64()), pa.field("mtime_ns", pa.int64()), pa.field("stat_captured_ns", pa.int64()), + pa.field("title", pa.utf8()), + pa.field("authors", pa.utf8()), + pa.field("created_at", pa.utf8()), ] ) diff --git a/src/lilbee/data/store/types.py b/src/lilbee/data/store/types.py index f7f0cf51a..444fe7517 100644 --- a/src/lilbee/data/store/types.py +++ b/src/lilbee/data/store/types.py @@ -45,6 +45,19 @@ class SourceType(StrEnum): IMPORTED = "imported" +class SourceMeta(NamedTuple): + """Document-level metadata captured at extraction time. + + ``title`` is always derivable (extraction metadata or the cleaned filename + stem); ``authors`` and ``created_at`` are only present when the extractor + reports them. Empty strings persist as NULL so old and new rows read alike. + """ + + title: str = "" + authors: str = "" + created_at: str = "" + + class ChunkWrite(NamedTuple): """One document's chunks plus its source-table update, for a batched write. @@ -62,16 +75,20 @@ class ChunkWrite(NamedTuple): stat: SourceStat | None = None page_texts: list[dict] | None = None source_type: SourceType = SourceType.DOCUMENT + meta: SourceMeta | None = None class ChunkType(StrEnum): """Values for the ``chunk_type`` column. - Everything ingests as ``RAW`` except wiki pages written by the wiki - producer; callers filter with ``Store.search(chunk_type=...)``. + Documents ingest as ``RAW``, extracted tables as ``TABLE`` (when table + extraction is on), and wiki pages written by the wiki producer as + ``WIKI``. Callers filter with ``Store.search(chunk_type=...)``; a ``RAW`` + filter also covers table chunks, since both are document content. """ RAW = "raw" + TABLE = "table" WIKI = "wiki" @@ -152,6 +169,10 @@ def _coerce_none_chunk_type(cls, v: str | None) -> str: """LanceDB rows from before the chunk_type column was added return None.""" return v if v is not None else ChunkType.RAW + # Document title at ingest time; None on rows written before the column + # existed (or by writers that carry no title, e.g. wiki pages). + title: str | None = None + page_start: int page_end: int line_start: int @@ -192,6 +213,11 @@ class SourceRecord(TypedDict): size_bytes: NotRequired[int] mtime_ns: NotRequired[int] stat_captured_ns: NotRequired[int] + # Extraction-time document metadata; absent or None on rows written + # before the columns existed, and None when the extractor reported none. + title: NotRequired[str | None] + authors: NotRequired[str | None] + created_at: NotRequired[str | None] # Sentinel for the stat columns on rows written before they existed (or for @@ -237,12 +263,21 @@ class SourceStatBackfill(NamedTuple): class PageTextRecord(TypedDict): - """One row of the per-page text dataset, matching ``_page_texts``.""" + """One row of the per-page text dataset, matching ``_page_texts``. + + The export dataset additionally carries the source's extraction metadata + (denormalized onto every page row) so an export/import cycle preserves it; + these are absent on rows read from the ``_page_texts`` table and on datasets + exported before the columns existed. + """ source: str page: int text: str content_type: str + title: NotRequired[str | None] + authors: NotRequired[str | None] + created_at: NotRequired[str | None] class CitationRecord(TypedDict): diff --git a/src/lilbee/data/tokenizer_backend.py b/src/lilbee/data/tokenizer_backend.py new file mode 100644 index 000000000..9694eb5ba --- /dev/null +++ b/src/lilbee/data/tokenizer_backend.py @@ -0,0 +1,67 @@ +"""lilbee's embedder tokenizer exposed as a xberg plugin tokenizer backend. + +xberg's chunk sizer can budget chunks in tokens, but only with tokenizers it loads +itself (a HuggingFace id or a local ``tokenizer.json``). lilbee's embedder is a +GGUF model served by llama-server, whose vocab is not published that way. +Registering this backend lets ChunkSizing count chunk budgets with the exact +tokenizer the embedder consumes, so ``chunk_size`` is a real token ceiling instead +of a chars-per-token guess (which mis-sizes token-dense text by 2-4x). +""" + +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING + +from lilbee.data.ingest.types import TokenizerBackendName + +if TYPE_CHECKING: + from collections.abc import Callable + +log = logging.getLogger(__name__) + +# Conservative (over-counting) chars-per-token used only when the exact count is +# unavailable. Over-counting keeps the fallback on the safe side of the budget -- +# it splits a touch early rather than emitting an over-length chunk. Mirrors the +# embed client's own estimate (providers.fleet.client._EMBED_EST_CHARS_PER_TOKEN). +_FALLBACK_CHARS_PER_TOKEN = 3 + + +def _estimate_tokens(text: str) -> int: + """Conservative token estimate from character length (ceiling division).""" + return max(1, -(-len(text) // _FALLBACK_CHARS_PER_TOKEN)) + + +class LilbeeTokenizerBackend: + """Counts chunk-sizing tokens with lilbee's embedder tokenizer. + + ``count_fn`` is the provider's exact token count (read live, so an + embedding-model swap needs no re-registration). xberg calls ``count_tokens`` + synchronously inside the chunk sizer's boundary search -- many times per chunk, + on its own worker threads -- and requires a non-zero count for non-empty text + both at registration (a probe) and at runtime. So any failure of the exact + count degrades to the character estimate instead of raising: the embedder may be + unloaded when this runs (the registration probe, or chunking outside an ingest), + and a raise would fail registration or abort extraction while a zero would make + every span look like it fits the budget. + """ + + def __init__(self, *, count_fn: Callable[[str], int]) -> None: + self._count_fn = count_fn + + def name(self) -> str: + return TokenizerBackendName.LILBEE + + def initialize(self) -> None: ... + + def shutdown(self) -> None: ... + + def count_tokens(self, text: str) -> int: + if not text: + return 0 + try: + count = self._count_fn(text) + except Exception: # embedder unreachable/erroring: degrade, never crash chunking + log.debug("exact token count failed; using character estimate", exc_info=True) + return _estimate_tokens(text) + return count if count > 0 else _estimate_tokens(text) diff --git a/src/lilbee/data/xberg_extract.py b/src/lilbee/data/xberg_extract.py new file mode 100644 index 000000000..26d171d43 --- /dev/null +++ b/src/lilbee/data/xberg_extract.py @@ -0,0 +1,75 @@ +"""Bridge to xberg's async-only ``extract`` for lilbee's call sites. + +xberg 1.x exposes a single ``extract(ExtractInput, config) -> ExtractionResult`` +coroutine whose ``results`` hold one ``ExtractedDocument`` per input. lilbee +extracts one in-memory document at a time, from both async code (await directly) +and synchronous code (driven to completion here). +""" + +from __future__ import annotations + +import asyncio +from concurrent.futures import ThreadPoolExecutor +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from collections.abc import Coroutine + + from xberg import ExtractedDocument, ExtractInput, ExtractionConfig, ExtractionResult + + +def _input(data: bytes, mime_type: str | None, filename: str | None) -> ExtractInput: + from xberg import ExtractInput, ExtractInputKind + + return ExtractInput( + kind=ExtractInputKind.BYTES, bytes=data, mime_type=mime_type, filename=filename + ) + + +def _first(result: ExtractionResult) -> ExtractedDocument: + """Return the single extracted document, or raise on an extraction error.""" + if result.results: + return result.results[0] + if result.errors: + raise RuntimeError(str(result.errors[0])) + raise RuntimeError("xberg extraction returned no document") + + +async def aextract_document( + data: bytes, + mime_type: str | None = None, + *, + filename: str | None = None, + config: ExtractionConfig, +) -> ExtractedDocument: + """Extract one in-memory document. For callers already on the event loop.""" + from xberg import extract + + return _first(await extract(_input(data, mime_type, filename), config)) + + +def extract_document( + data: bytes, + mime_type: str | None = None, + *, + filename: str | None = None, + config: ExtractionConfig, +) -> ExtractedDocument: + """Extract one in-memory document from synchronous code. + + Drives xberg's coroutine to completion. When no event loop is running on this + thread (the common case: a plain sync caller or one of lilbee's offloaded + worker threads) ``asyncio.run`` is used directly; if a loop is already running + here, the coroutine is driven on a fresh worker thread so it never re-enters + that loop. + """ + return _run(aextract_document(data, mime_type, filename=filename, config=config)) + + +def _run(coro: Coroutine[None, None, ExtractedDocument]) -> ExtractedDocument: + try: + asyncio.get_running_loop() + except RuntimeError: + return asyncio.run(coro) + with ThreadPoolExecutor(max_workers=1) as pool: + return pool.submit(asyncio.run, coro).result() diff --git a/src/lilbee/mcp_server.py b/src/lilbee/mcp_server.py index 41761b8d4..328e7e955 100644 --- a/src/lilbee/mcp_server.py +++ b/src/lilbee/mcp_server.py @@ -51,7 +51,7 @@ from lilbee.core.config import cfg from lilbee.core.config.enums import CrawlRenderMode from lilbee.core.settings import overlay_persisted_settings -from lilbee.core.system import LOCAL_ROOT_DIRNAME +from lilbee.core.system import LOCAL_ROOT_DIRNAME, canonical_data_root from lilbee.crawler import crawler_available, is_url, require_valid_crawl_url from lilbee.crawler.task import get_task, start_crawl from lilbee.data.store import ( @@ -408,7 +408,9 @@ def init(path: str = "") -> dict[str, Any]: "init is unavailable on the HTTP server: it is bound to one vault and " "shared by every connected client. Start a separate server for another vault." ) - base = Path(path) if path else Path.cwd() + # Canonical so this vault keys the same lock paths a CLI or server process + # would derive for the same directory. + base = canonical_data_root(path) if path else canonical_data_root(Path.cwd()) root = base / LOCAL_ROOT_DIRNAME created = False diff --git a/src/lilbee/providers/base.py b/src/lilbee/providers/base.py index 02c31fff7..a00b699c3 100644 --- a/src/lilbee/providers/base.py +++ b/src/lilbee/providers/base.py @@ -11,9 +11,8 @@ from pydantic import BaseModel if TYPE_CHECKING: - from lilbee.providers.roles import OcrBackend, WorkerRole + from lilbee.providers.roles import WorkerRole from lilbee.providers.warm_progress import WarmProgress - from lilbee.vision import PageText T_co = TypeVar("T_co", covariant=True) @@ -260,6 +259,15 @@ def embed(self, texts: list[str]) -> list[list[float]]: """Embed a batch of texts, return list of vectors.""" ... + def count_tokens(self, text: str) -> int: + """Exact token count of *text* under the embedding model's tokenizer. + + Raise ``NotImplementedError`` when the backend has no local tokenizer (cloud + SDK backends); token-budgeted chunk sizing then falls back to a character + estimate. + """ + ... + @overload def chat( self, @@ -343,23 +351,6 @@ def vision_ocr( """OCR one page image; ``timeout`` seconds, ``None``/``0`` = no cap.""" ... - def pdf_ocr( - self, - path: Path, - *, - backend: OcrBackend, - model: str = "", - per_page_timeout_s: float | None = None, - quiet: bool = True, - on_progress: Callable[..., None] | None = None, - ) -> list[PageText]: - """OCR every page of a PDF, returning per-page text in input order. - - Backends that cannot OCR scanned PDFs locally raise - :class:`NotImplementedError`; ingest callers catch and log this. - """ - ... - def vision_slot_capacity(self) -> int | None: """Fitted concurrent-OCR slots if the vision fleet is running, else None. diff --git a/src/lilbee/providers/fleet/binary.py b/src/lilbee/providers/fleet/binary.py index b2ebb8d1b..8abfe26c0 100644 --- a/src/lilbee/providers/fleet/binary.py +++ b/src/lilbee/providers/fleet/binary.py @@ -6,6 +6,7 @@ from enum import StrEnum from pathlib import Path +from lilbee.core.config import cfg from lilbee.providers.base import ProviderError, ProviderErrorKind _INSTALL_HINT = ( @@ -48,14 +49,11 @@ def resolve_engine_tool(tool: EngineTool) -> Path: (an explicit setting beats the bundled wheel); the other tools resolve from the wheel, then ``PATH``. """ - if tool is EngineTool.LLAMA_SERVER: - from lilbee.core.config import cfg - - if cfg.llama_server_path: - configured = Path(cfg.llama_server_path) - if not configured.is_file(): - raise ProviderError(f"LILBEE_LLAMA_SERVER_PATH is not a file: {configured}") - return configured + if tool is EngineTool.LLAMA_SERVER and cfg.llama_server_path: + configured = Path(cfg.llama_server_path) + if not configured.is_file(): + raise ProviderError(f"LILBEE_LLAMA_SERVER_PATH is not a file: {configured}") + return configured bundled = _bundled_tool(tool) if bundled is not None: diff --git a/src/lilbee/providers/fleet/client.py b/src/lilbee/providers/fleet/client.py index e532c1aee..c4ad47b2c 100644 --- a/src/lilbee/providers/fleet/client.py +++ b/src/lilbee/providers/fleet/client.py @@ -1007,6 +1007,10 @@ def _detokenize(self, tokens: list[int]) -> str: _raise_for_status(resp) return str(resp.json()["content"]) + def count_tokens(self, text: str) -> int: + """Exact token count from this server's tokenizer (embed/rerank servers).""" + return len(self._tokenize(text)) + @contextlib.contextmanager def _abortable(self, resp: httpx.Response) -> Generator[None]: """Expose *resp* to ``abort_streams`` for the duration of its read loop.""" diff --git a/src/lilbee/providers/fleet/gpu_select.py b/src/lilbee/providers/fleet/gpu_select.py index a7c88314d..7599f684f 100644 --- a/src/lilbee/providers/fleet/gpu_select.py +++ b/src/lilbee/providers/fleet/gpu_select.py @@ -20,6 +20,7 @@ from dataclasses import dataclass from enum import IntEnum, StrEnum +from lilbee.core.config import cfg from lilbee.providers.fleet.vulkan_icd_discovery import ( iter_vulkan_manifest_paths, ) @@ -551,7 +552,6 @@ def disable_conflicting_vulkan_icds() -> str | None: from disk (registry on Windows, XDG on Linux); enumerating via ``vkCreateInstance`` would pre-load every vendor's ICD before the disable lands. """ - from lilbee.core.config import cfg if not _platform_supports_icd_pin(): return None diff --git a/src/lilbee/providers/fleet/planning.py b/src/lilbee/providers/fleet/planning.py index ae26a5b66..f66abf2a7 100644 --- a/src/lilbee/providers/fleet/planning.py +++ b/src/lilbee/providers/fleet/planning.py @@ -11,6 +11,7 @@ from pathlib import Path from typing import TYPE_CHECKING +from lilbee.core.config import cfg from lilbee.core.config.enums import KvCacheType from lilbee.core.system import is_network_path from lilbee.providers import model_cache @@ -213,7 +214,6 @@ def _resolve_vision_slots( ) -> int: """Largest OCR batching slot count (<= ``cfg.vision_ocr_concurrency``) that fits the memory budget; 1 when the ceiling is 1 or nothing larger fits.""" - from lilbee.core.config import cfg ceiling = max(1, cfg.vision_ocr_concurrency) if ceiling == 1: @@ -299,7 +299,6 @@ def _role_ctx(role: WorkerRole, model_path: Path, meta: dict[str, str] | None) - falls back to the single-GPU dynamic chat-ctx picker. A tensor-split chat is sized against its per-device headroom instead (see :func:`fit_split_ctx`). """ - from lilbee.core.config import cfg from lilbee.providers.engine_params import ( resolve_chat_ctx, resolve_embed_ctx, @@ -322,7 +321,6 @@ def _role_ctx(role: WorkerRole, model_path: Path, meta: dict[str, str] | None) - def _rerank_mode_for(meta: dict[str, str] | None) -> RerankMode: """Resolve the RERANK serving mode from cfg + the reranker GGUF arch.""" - from lilbee.core.config import cfg arch = meta.get("architecture") if meta else None return resolve_rerank_mode(cfg.reranker_type, arch) @@ -362,7 +360,6 @@ def _role_gpu_layers(role: WorkerRole) -> int: def _flash_enabled() -> bool: """Flash attention is on unless ``cfg.flash_attention`` is explicitly ``False``.""" - from lilbee.core.config import cfg return cfg.flash_attention is not False @@ -379,7 +376,6 @@ def _role_flash(role: WorkerRole) -> bool: def _role_kv_cache_type(role: WorkerRole) -> KvCacheType: """Chat honors ``cfg.kv_cache_type``; embed/rerank/vision run f16 KV.""" - from lilbee.core.config import cfg return cfg.kv_cache_type if role is WorkerRole.CHAT else KvCacheType.F16 @@ -391,9 +387,6 @@ def _replica_count(role: WorkerRole, device_count: int) -> int: def _cache_type_flag() -> str | None: """KV cache type for chat, or ``None`` to leave llama-server's f16 default.""" - from lilbee.core.config import cfg - from lilbee.core.config.enums import KvCacheType - if cfg.kv_cache_type is KvCacheType.F16: return None return cfg.kv_cache_type.value @@ -479,7 +472,6 @@ def _chat_serve_budget_footprint(footprint: int) -> int: its context. Small models are unaffected -- they fit the serve budget with KV room to spare. """ - from lilbee.core.config import cfg return int(footprint * (USABLE_VRAM_FRACTION / cfg.gpu_memory_fraction)) @@ -493,7 +485,6 @@ def _placement_estimate_ctx(role: WorkerRole, model_path: Path, meta: dict[str, single-card placement) nor the full trained ceiling (which over-reserves). A model that cannot hold weights + this floor on one card is tensor-split. """ - from lilbee.core.config import cfg from lilbee.providers.engine_params import chat_ctx_ceiling if role is WorkerRole.CHAT: @@ -511,7 +502,6 @@ def _placement_estimate_slots(role: WorkerRole, meta: dict[str, str] | None) -> A tensor-split chat serves a single full-context sequence, so the placement total is the per-sequence ceiling, not ``ceiling x _CHAT_SLOTS`` (KV no launch allocates). """ - from lilbee.core.config import cfg if role is WorkerRole.CHAT: return _SPLIT_CHAT_SLOTS @@ -842,7 +832,6 @@ def _launch_for( model_path = resolve_model_path(model_ref) weights_bytes = _weights_bytes(model_path) meta = read_gguf_metadata(model_path) - from lilbee.core.config import cfg chosen = tuple(by_index[i] for i in plan.devices) is_chat = plan.role is WorkerRole.CHAT @@ -1076,7 +1065,6 @@ def clear(self) -> None: def capture_plan_probe() -> None: """Snapshot devices and memory for planning; call only on a clean box.""" - from lilbee.core.config import cfg from lilbee.providers.fleet.cuda_runtime import apply_cuda_runtime_env from lilbee.providers.fleet.gpu_env import apply_fleet_gpu_env @@ -1105,8 +1093,6 @@ def _plan_devices(binary: Path) -> list[FleetDevice]: def _plan_available_memory() -> int: """Usable VRAM for ctx/slot sizing: the snapshot, else the live read.""" - from lilbee.core.config import cfg - probe = _plan_probe_store.get() if probe is not None: return probe.available_vram @@ -1275,7 +1261,6 @@ def plan_launches( devices: list[FleetDevice], ) -> FleetPlan: """Plan placement for *roles* (``None`` = all configured) and build their launches.""" - from lilbee.core.config import cfg unified_budget = _unified_memory_budget(devices) inputs, model_refs, reservation, skipped_not_installed = _server_model_inputs( diff --git a/src/lilbee/providers/fleet/provider.py b/src/lilbee/providers/fleet/provider.py index dd305d414..71301da04 100644 --- a/src/lilbee/providers/fleet/provider.py +++ b/src/lilbee/providers/fleet/provider.py @@ -18,7 +18,7 @@ import sys import threading import time -from concurrent.futures import FIRST_COMPLETED, Future, ThreadPoolExecutor, wait +from concurrent.futures import ThreadPoolExecutor from contextlib import contextmanager from importlib.metadata import version as _pkg_version from pathlib import Path @@ -60,8 +60,6 @@ ChatStreamItem, ChatToolResult, ClosableIterator, - OcrBackend, - PageText, ) # User-facing name for this engine in error messages. @@ -302,7 +300,7 @@ class _VisionReplica(NamedTuple): class _PageBudgetExhausted(Exception): # noqa: N818 - internal control flow, not an error API - """A PDF page's document-wide OCR budget ran out before its slot came up.""" + """A page's document-wide OCR budget ran out before its slot came up.""" class _VisionDispatcher: @@ -411,7 +409,6 @@ def _vision_call( a ``ProviderError`` so the page-level OCR caller can fail just that page. Callers hold a dispatcher slot, so queue time isn't billed against the timeout. """ - from lilbee.core.config import cfg options = {"max_tokens": cfg.vision_ocr_max_tokens} if timeout and timeout > 0: @@ -471,44 +468,6 @@ def _attempt(client: LlamaServerClient) -> str: ) -def _ocr_pdf_page( - idx: int, - png: bytes, - *, - pool: list[_VisionReplica], - ocr_prompt: str, - deadline: float | None, - page_path: Path, -) -> tuple[int, str]: - """OCR one rasterized page on a replica with a free slot; empty text on failure. - - The document-deadline clock is read only once a slot is held, so queue time - isn't billed against the page's share; an exhausted budget skips the page - rather than running it un-timed. - """ - from lilbee.vision import build_vision_messages - - messages = build_vision_messages(ocr_prompt, png) - try: - return idx, _ocr_dispatch(pool, messages, deadline) - except _PageBudgetExhausted: - log.warning( - "Vision OCR budget exhausted before page %d of %s; skipping.", - idx + 1, - page_path.name, - ) - return idx, "" - except ProviderError: - # One failed/timed-out page yields empty text; siblings continue. - log.warning( - "Vision OCR failed for page %d of %s; skipping that page.", - idx + 1, - page_path.name, - exc_info=True, - ) - return idx, "" - - def _pdf_drain_budget(total_pages: int, per_page_timeout_s: float | None) -> float | None: """Total OCR wall-clock budget = pages*per_page + load grace, or None for no cap. @@ -670,7 +629,6 @@ def _ensure_fleet(self) -> bool: # while this warm-up/reload thread was in flight; do not spawn a # llama-swap no live provider would ever reap. return False - from lilbee.core.config import cfg # A dead owner's surviving llama-swap holds VRAM; reap it before the # snapshot so the cards are actually free for this fleet (and the @@ -971,8 +929,6 @@ def _shutdown_swap(self, *, latch: bool = True) -> None: # and the sweep stops every llama-swap this process spawned (keyed # on the per-group config paths), not just tracked handles. if not swaps: - from lilbee.core.config import cfg - sweep_owned(cfg.data_dir) def _detach_swap(self) -> None: @@ -1082,7 +1038,6 @@ def chat( the server parses native tool calls, so tool support needs no per-family parser here. """ - from lilbee.core.config import cfg from lilbee.providers.engine_params import chat_options_to_kwargs self._require_configured_model(model, str(cfg.chat_model), WorkerRole.CHAT) @@ -1113,7 +1068,6 @@ def chat_with_tools( model: str | None = None, ) -> ChatToolResult: """Route a tool-enabled chat turn to the least-busy chat server.""" - from lilbee.core.config import cfg from lilbee.providers.engine_params import chat_options_to_kwargs self._require_configured_model(model, str(cfg.chat_model), WorkerRole.CHAT) @@ -1157,10 +1111,20 @@ def embed(self, texts: list[str]) -> list[list[float]]: clients = self._require_clients(WorkerRole.EMBED) return _call_with_failover(clients, lambda client: client.embed(texts)) + def count_tokens(self, text: str) -> int: + """Exact token count of *text* under the embedding model's tokenizer. + + Routes to the embed server's ``/tokenize`` so chunk sizing counts the same + tokens the embedder will consume. Raises ``ProviderError`` when no embed + server is configured; callers on the chunk-sizing path degrade to an + estimate rather than propagate it. + """ + clients = self._require_clients(WorkerRole.EMBED) + return _call_with_failover(clients, lambda client: client.count_tokens(text)) + def vision_ocr( self, png_bytes: bytes, model: str, prompt: str = "", *, timeout: float | None = None ) -> str: - from lilbee.core.config import cfg from lilbee.vision import build_vision_messages, resolve_ocr_prompt self._require_configured_model(model, str(cfg.vision_model), WorkerRole.VISION) @@ -1207,98 +1171,9 @@ def _vision_pool(self) -> list[_VisionReplica]: fallback_slots = max(1, cfg.vision_ocr_concurrency) return [_VisionReplica(client, fallback_slots) for client in clients] - def pdf_ocr( - self, - path: Path, - *, - backend: OcrBackend, - model: str = "", - per_page_timeout_s: float | None = None, - quiet: bool = True, - on_progress: Callable[..., None] | None = None, - ) -> list[PageText]: - """OCR each rasterized PDF page through the vision server. - - ``backend`` is ``Literal["vision"]`` (tesseract is run inline by the - ingest caller, never here). ``per_page_timeout_s`` caps each page's - request; ``quiet`` is accepted for protocol parity (the server emits no - Rich progress to suppress). Pages are numbered 1-based to match - ``PageText`` / ``ExtractEvent`` everywhere else in lilbee. - """ - from lilbee.core.config import cfg - from lilbee.runtime.progress import EventType, ExtractEvent - from lilbee.vision import ( - PageText, - pdf_page_count, - rasterize_pdf, - resolve_ocr_prompt, - ) - - del quiet # protocol parity; no server-side Rich progress to suppress. - self._require_configured_model(model, str(cfg.vision_model), WorkerRole.VISION) - replicas = self._vision_pool() - # The model is fixed for the whole document, so resolve its prompt once. - ocr_prompt = resolve_ocr_prompt(model or str(cfg.vision_model)) - log.debug("OCR prompt for %s -> %r", model or cfg.vision_model, ocr_prompt) - total = pdf_page_count(path) - # One document-wide deadline (pages*per_page + load grace), not a per-page - # cap: each page gets whatever budget remains, so a slow page borrows from - # fast ones and the cold first-inference is covered, matching in-process OCR. - budget = _pdf_drain_budget(total, per_page_timeout_s) - deadline = (time.monotonic() + budget) if budget is not None else None - - _ocr = functools.partial( - _ocr_pdf_page, - pool=replicas, - ocr_prompt=ocr_prompt, - deadline=deadline, - page_path=path, - ) - - # OCR pages concurrently (a single-page decode underuses the GPU). The - # window is the fleet's total batching slots so one large document can - # saturate every replica; a bounded sliding window keeps that many pages - # in flight without rasterizing the whole PDF into memory, and results - # are reassembled in page order. - concurrency = max(1, sum(replica.slots for replica in replicas)) - raster = rasterize_pdf(path) - results: dict[int, str] = {} - with ThreadPoolExecutor(max_workers=concurrency) as pool: - pending: set[Future[tuple[int, str]]] = set() - - def _submit_next() -> bool: - page = next(raster, None) - if page is None: - return False - idx, png_bytes = page - pending.add(pool.submit(_ocr, idx, bytes(png_bytes))) - return True - - for _ in range(concurrency): - if not _submit_next(): - break - while pending: - completed, pending = wait(pending, return_when=FIRST_COMPLETED) - for done in completed: - page_idx, text = done.result() - results[page_idx] = text - if on_progress is not None: - on_progress( - EventType.EXTRACT, - ExtractEvent(file=path.name, page=page_idx + 1, total_pages=total), - ) - _submit_next() - pages = [PageText(idx + 1, results[idx]) for idx in sorted(results)] - unrecovered = sum(1 for page in pages if not page.text) - if unrecovered: - log.warning( - "Vision OCR produced no text for %d of %d pages of %s " - "(timed out or server still busy); those pages are blank in the index.", - unrecovered, - total, - path.name, - ) - return pages + # PDF/image OCR now runs inside xberg via the registered lilbee-vision + # backend (see data.ingest.vision_ocr_backend); this provider only exposes + # single-image vision_ocr, which that backend calls. def rerank(self, query: str, candidates: list[str]) -> list[float]: clients = self._require_clients(WorkerRole.RERANK) @@ -1774,8 +1649,6 @@ def _reload_pass(self, force: frozenset[WorkerRole] = frozenset()) -> None: so a racing shutdown/build can't interleave with the restart and leak a live llama-swap holding GPU memory. """ - from lilbee.core.config import cfg - with self._build_lock: with self._lock: if self._shut_down: diff --git a/src/lilbee/providers/fleet/replicas.py b/src/lilbee/providers/fleet/replicas.py index 6dfa46dcc..b7273b379 100644 --- a/src/lilbee/providers/fleet/replicas.py +++ b/src/lilbee/providers/fleet/replicas.py @@ -11,6 +11,7 @@ import functools import logging +from lilbee.core.config import cfg from lilbee.providers.roles import ROLE_REGISTRY, WorkerRole log = logging.getLogger(__name__) @@ -26,7 +27,6 @@ def resolve_replica_count(role: WorkerRole, device_count: int) -> int: 0 means one replica per GPU (falling to one when GPU-less). Other roles run one instance. Capping to residual VRAM happens in placement. """ - from lilbee.core.config import cfg knob = ROLE_REGISTRY[role].replica_knob if knob is None: diff --git a/src/lilbee/providers/fleet/swap_manager.py b/src/lilbee/providers/fleet/swap_manager.py index a3b482f7b..4cd76d912 100644 --- a/src/lilbee/providers/fleet/swap_manager.py +++ b/src/lilbee/providers/fleet/swap_manager.py @@ -244,6 +244,7 @@ def _write_state(self, *, detached: bool = False, launches: list[dict] | None = _STATE_KEY_DETACHED: detached, _STATE_KEY_LAUNCHES: launches or [], } + self._state_path.parent.mkdir(parents=True, exist_ok=True) tmp_path = self._state_path.with_name( f"{_STATE_TMP_PREFIX}{self._state_path.name}{_STATE_TMP_SUFFIX}" ) diff --git a/src/lilbee/providers/fleet/vram.py b/src/lilbee/providers/fleet/vram.py index 50c3eb830..b9bd0cb59 100644 --- a/src/lilbee/providers/fleet/vram.py +++ b/src/lilbee/providers/fleet/vram.py @@ -1,7 +1,6 @@ """gguf-parser-backed, UMA-aware memory estimation for one llama-server instance. -See docs/architecture.md (VRAM estimation) for why this replaced the hand-rolled -weights + KV-cache math. +See docs/architecture.md (VRAM estimation). """ from __future__ import annotations diff --git a/src/lilbee/providers/routing_provider.py b/src/lilbee/providers/routing_provider.py index 92e13b789..168850545 100644 --- a/src/lilbee/providers/routing_provider.py +++ b/src/lilbee/providers/routing_provider.py @@ -21,9 +21,8 @@ ) from lilbee.providers.litellm_sdk import LitellmSdkBackend from lilbee.providers.model_ref import ProviderModelRef, parse_model_ref, routes_to_native_gguf -from lilbee.providers.roles import ROLE_REGISTRY, OcrBackend, WorkerRole +from lilbee.providers.roles import ROLE_REGISTRY, WorkerRole from lilbee.providers.sdk_llm_provider import SdkLLMProvider -from lilbee.vision import PageText if TYPE_CHECKING: from lilbee.providers.warm_progress import WarmProgress @@ -83,6 +82,10 @@ def embed(self, texts: list[str]) -> list[list[float]]: ref = parse_model_ref(cfg.embedding_model) return self._pick_backend(ref).embed(texts) + def count_tokens(self, text: str) -> int: + ref = parse_model_ref(cfg.embedding_model) + return self._pick_backend(ref).count_tokens(text) + @overload def chat( self, @@ -173,33 +176,6 @@ def vision_ocr( ref = parse_model_ref(model) return self._pick_backend(ref).vision_ocr(png_bytes, model, prompt, timeout=timeout) - def pdf_ocr( - self, - path: Path, - *, - backend: OcrBackend, - model: str = "", - per_page_timeout_s: float | None = None, - quiet: bool = True, - on_progress: Callable[..., None] | None = None, - ) -> list[PageText]: - """Dispatch by ``model``'s ref prefix, same rules as :meth:`vision_ocr`. - - Hosted refs reach :class:`SdkLLMProvider`, which raises - ``NotImplementedError`` for PDF OCR; native refs reach the - local llama-server engine. ``model`` is empty when the caller wants - the configured ``cfg.vision_model`` to drive the dispatch. - """ - ref = parse_model_ref(model or cfg.vision_model) - return self._pick_backend(ref).pdf_ocr( - path, - backend=backend, - model=model, - per_page_timeout_s=per_page_timeout_s, - quiet=quiet, - on_progress=on_progress, - ) - def vision_slot_capacity(self) -> int | None: """Delegate to the local fleet, but never build it just to size the fan-out.""" return self._local.vision_slot_capacity() if self._local is not None else None diff --git a/src/lilbee/providers/sdk_backend.py b/src/lilbee/providers/sdk_backend.py index 05149b7e2..0b1eb15c2 100644 --- a/src/lilbee/providers/sdk_backend.py +++ b/src/lilbee/providers/sdk_backend.py @@ -18,6 +18,7 @@ # Display name for the active backend the SDK is talking to. The # adapter's own identity is exposed separately via provider_name. +from lilbee.core.config import cfg from lilbee.providers.backend_names import BackendName from lilbee.providers.local_servers import detect_local_server @@ -56,7 +57,6 @@ def get_provider_api_key(provider: str) -> str | None: :data:`PROVIDER_API_KEY_FIELD`. Reads only the lilbee config field; use :func:`provider_has_key` to also honor the SDK's own env var. """ - from lilbee.core.config import cfg field = PROVIDER_API_KEY_FIELD.get(provider.lower()) if field is None: diff --git a/src/lilbee/providers/sdk_llm_provider.py b/src/lilbee/providers/sdk_llm_provider.py index c834efac1..941310a2d 100644 --- a/src/lilbee/providers/sdk_llm_provider.py +++ b/src/lilbee/providers/sdk_llm_provider.py @@ -36,7 +36,6 @@ from lilbee.providers.local_servers import LOCAL_SERVER_KEYS from lilbee.providers.local_servers.config_urls import base_url_for, configured_local_servers from lilbee.providers.model_ref import ProviderModelRef, parse_model_ref, translate_options -from lilbee.providers.roles import OcrBackend from lilbee.providers.sdk_backend import ( PROVIDER_KEYS, CompletionRequest, @@ -44,7 +43,6 @@ LlmSdkBackend, RerankRequest, ) -from lilbee.vision import PageText log = logging.getLogger(__name__) @@ -125,6 +123,11 @@ def embed(self, texts: list[str]) -> list[list[float]]: ) from exc return result.vectors + def count_tokens(self, text: str) -> int: + """Cloud SDK backends expose no local tokenizer, so chunk sizing falls back + to a character estimate (see LilbeeTokenizerBackend).""" + raise NotImplementedError("SDK backends have no local tokenizer for chunk sizing") + @overload def chat( self, @@ -301,23 +304,6 @@ def vision_ocr( ) return result.text - def pdf_ocr( - self, - path: Path, - *, - backend: OcrBackend, - model: str = "", - per_page_timeout_s: float | None = None, - quiet: bool = True, - on_progress: Callable[..., None] | None = None, - ) -> list[PageText]: - """SDK backend cannot rasterise PDFs locally; ingest callers fall back.""" - del path, backend, model, per_page_timeout_s, quiet, on_progress - raise NotImplementedError( - "Hosted models do not support scanned-PDF OCR. " - "Set LILBEE_VISION_MODEL to a local GGUF vision model to enable it." - ) - def vision_slot_capacity(self) -> int | None: """Hosted backends have no local OCR slots; the caller estimates.""" return None diff --git a/src/lilbee/retrieval/clustering.py b/src/lilbee/retrieval/clustering.py index 04efa3845..1142d66c6 100644 --- a/src/lilbee/retrieval/clustering.py +++ b/src/lilbee/retrieval/clustering.py @@ -73,18 +73,27 @@ def _select_backend(config: Config, store: Store) -> SourceClusterer: class Clusterer: - """Wiki synthesis clusterer facade with backend selection.""" + """Wiki synthesis clusterer facade with live backend selection. + + The backend is selected on each access rather than pinned at + construction: the services container caches this facade for the process + lifetime, and a concept graph built later in the same process must be + picked up without a restart. Selection is cheap -- backend construction + just captures config and store, and availability is a config flag plus + a table-existence check. + """ def __init__(self, config: Config, store: Store) -> None: - self._backend: SourceClusterer = _select_backend(config, store) + self._config = config + self._store = store @property def backend(self) -> SourceClusterer: - """Return the underlying backend (useful for tests and introspection).""" - return self._backend + """Select and return the backend for the store's current state.""" + return _select_backend(self._config, self._store) def available(self) -> bool: - return self._backend.available() + return self.backend.available() def get_clusters(self, min_sources: int = 3) -> list[SourceCluster]: - return self._backend.get_clusters(min_sources=min_sources) + return self.backend.get_clusters(min_sources=min_sources) diff --git a/src/lilbee/retrieval/clustering_embedding/helpers.py b/src/lilbee/retrieval/clustering_embedding/helpers.py index 1a229d7a8..1bcac244c 100644 --- a/src/lilbee/retrieval/clustering_embedding/helpers.py +++ b/src/lilbee/retrieval/clustering_embedding/helpers.py @@ -20,12 +20,17 @@ from lilbee.core.config import CHUNKS_TABLE from lilbee.data.store import Store from lilbee.retrieval.clustering import SourceCluster -from lilbee.retrieval.clustering_embedding.types import ChunkRecord +from lilbee.retrieval.clustering_embedding.types import ClusterChunk # Block size for the similarity kernel. With N=10000 and D=768 this caps # peak float32 memory at block * N * 4 bytes ~= 40 MB. _BLOCK_SIZE = 1024 +# Rows boxed into Python objects per batch when scanning the chunks table. +# Caps the transient working set at ~25 MB (1024 rows x 768 dims), whatever +# the corpus size. +_SCAN_BATCH_ROWS = 1024 + # Label Propagation hard iteration cap. Convergence is typically reached # in well under 10 passes on real corpora. _MAX_LPA_ITERATIONS = 30 @@ -33,10 +38,10 @@ # Minimum non-zero L2 norm for a row vector to be kept. _MIN_VECTOR_NORM = 1e-12 -# Source-membership thresholds. A source joins a chunk community when it -# contributes at least `min(_MIN_SOURCE_CHUNKS, ceil(total * _MIN_SOURCE_FRACTION))` -# of its chunks. The stricter (smaller) side wins, so a single stray chunk -# from a long document never pulls the whole source into an unrelated cluster. +# A source joins a chunk community on at least +# `min(_MIN_SOURCE_CHUNKS, ceil(total * _MIN_SOURCE_FRACTION))` of its chunks. +# min() is the lenient cutoff: it caps the requirement at _MIN_SOURCE_CHUNKS so +# a long document needs a foothold, not a proportional chunk count. _MIN_SOURCE_CHUNKS = 3 _MIN_SOURCE_FRACTION = 0.2 @@ -52,29 +57,6 @@ _MIN_K = 5 _MAX_K = 20 -# Minimum token length for TF-IDF labeling. Shorter tokens are mostly -# articles, prepositions, and single letters: noise that inflates term -# counts without adding topic signal. Three characters keeps useful -# acronyms (api, xml, sql). -_MIN_TF_TOKEN_LEN = 3 - - -def _tokenize_for_tf(text: str) -> list[str]: - """Lowercase alphanumeric tokens for TF-IDF scoring. - - Deliberately has NO stopword list: common words like "the" or "and" - get an IDF near zero (they appear in almost every chunk) so TF-IDF - filters them automatically. A hand-curated English stoplist would - add maintenance burden and break on non-English corpora for no - additional quality. - """ - result: list[str] = [] - for raw in text.lower().split(): - word = "".join(ch for ch in raw if ch.isalnum()) - if len(word) >= _MIN_TF_TOKEN_LEN: - result.append(word) - return result - def auto_k(n: int) -> int: """Pick a neighborhood size from corpus size via ``clamp(log2(N)+2)``.""" @@ -86,7 +68,7 @@ def auto_k(n: int) -> int: def _parse_chunk_row( row: dict[str, object], -) -> tuple[ChunkRecord, list[float] | tuple[float, ...]] | None: +) -> tuple[ClusterChunk, list[float] | tuple[float, ...]] | None: """Extract a chunk record + vector from a raw Arrow row, or None on invalid.""" vector = row.get("vector") if not isinstance(vector, (list, tuple)): @@ -98,44 +80,50 @@ def _parse_chunk_row( chunk_text = raw_text if isinstance(raw_text, str) else "" raw_index = row.get("chunk_index") chunk_index = raw_index if isinstance(raw_index, int) else 0 - record = ChunkRecord( - source=source, - chunk_index=chunk_index, - text=chunk_text, - tokens=_tokenize_for_tf(chunk_text), - ) + # tokens derive from text in ClusterChunk.__post_init__. + record = ClusterChunk(source=source, chunk_index=chunk_index, text=chunk_text) return record, vector def _load_chunk_records( store: Store, -) -> tuple[list[ChunkRecord], np.ndarray]: +) -> tuple[list[ClusterChunk], np.ndarray]: """Scan the chunks table once and return records plus a float32 matrix. Rows with an unparseable vector are skipped. Records are sorted by ``(source, chunk_index)`` so downstream cluster IDs are stable regardless of LanceDB's row return order. Records are tokenized once - here so TF-IDF labeling does not re-tokenize. The vector matrix is - preallocated and populated via numpy row-assignment, which pushes - the Python-level float cast into numpy's C loop and avoids building - a transient ``list[list[float]]``. + here so TF-IDF labeling does not re-tokenize. + + The scan consumes the table in bounded row batches (``_SCAN_BATCH_ROWS``): + each batch's rows are parsed and its vectors immediately compacted into a + float32 block, so the boxed Python working set (row dicts plus vector + float lists) is one batch, not the whole table. Only the compact matrix + and the text records scale with corpus size. """ table = store.open_table(CHUNKS_TABLE) if table is None: return [], np.zeros((0, 0), dtype=np.float32) - parsed = [pair for pair in map(_parse_chunk_row, table.to_arrow().to_pylist()) if pair] - if not parsed: + records: list[ClusterChunk] = [] + blocks: list[np.ndarray] = [] + for batch in table.to_arrow().to_batches(max_chunksize=_SCAN_BATCH_ROWS): + batch_vectors: list[list[float] | tuple[float, ...]] = [] + for row in batch.to_pylist(): + pair = _parse_chunk_row(row) + if pair is None: + continue + record, vector = pair + records.append(record) + batch_vectors.append(vector) + if batch_vectors: + blocks.append(np.asarray(batch_vectors, dtype=np.float32)) + if not records: return [], np.zeros((0, 0), dtype=np.float32) - parsed.sort(key=lambda pair: (pair[0].source, pair[0].chunk_index)) - dim = len(parsed[0][1]) - matrix = np.empty((len(parsed), dim), dtype=np.float32) - records: list[ChunkRecord] = [] - for row_idx, (record, vector) in enumerate(parsed): - records.append(record) - matrix[row_idx] = vector - return records, matrix + matrix = np.vstack(blocks) + order = sorted(range(len(records)), key=lambda i: (records[i].source, records[i].chunk_index)) + return [records[i] for i in order], matrix[order] def normalize_rows(matrix: np.ndarray) -> tuple[np.ndarray, np.ndarray]: @@ -231,7 +219,7 @@ def communities_by_label(labels: list[int]) -> dict[int, list[int]]: return communities -def _source_totals(records: list[ChunkRecord]) -> dict[str, int]: +def _source_totals(records: list[ClusterChunk]) -> dict[str, int]: """Return the total chunk count per source across the whole corpus.""" totals: dict[str, int] = {} for record in records: @@ -241,7 +229,7 @@ def _source_totals(records: list[ChunkRecord]) -> dict[str, int]: def _filter_sources( member_indices: list[int], - records: list[ChunkRecord], + records: list[ClusterChunk], source_totals: dict[str, int], ) -> frozenset[str]: """Apply the source-membership threshold to a community's members.""" @@ -259,7 +247,7 @@ def _filter_sources( return frozenset(kept) -def _corpus_document_frequency(records: list[ChunkRecord]) -> dict[str, int]: +def _corpus_document_frequency(records: list[ClusterChunk]) -> dict[str, int]: """Compute document frequency (chunk count containing term) for every term.""" df: dict[str, int] = {} for record in records: @@ -270,7 +258,7 @@ def _corpus_document_frequency(records: list[ChunkRecord]) -> dict[str, int]: def _label_community( member_indices: list[int], - records: list[ChunkRecord], + records: list[ClusterChunk], df: dict[str, int], total_chunks: int, fallback: str, @@ -305,7 +293,7 @@ def _label_community( def _build_clusters( communities: dict[int, list[int]], - records: list[ChunkRecord], + records: list[ClusterChunk], source_totals: dict[str, int], df: dict[str, int], min_sources: int, diff --git a/src/lilbee/retrieval/clustering_embedding/types.py b/src/lilbee/retrieval/clustering_embedding/types.py index b7f943d75..e4e357605 100644 --- a/src/lilbee/retrieval/clustering_embedding/types.py +++ b/src/lilbee/retrieval/clustering_embedding/types.py @@ -4,12 +4,54 @@ from dataclasses import dataclass, field +# Minimum token length for TF-IDF labeling. Shorter tokens are mostly +# articles, prepositions, and single letters: noise that inflates term +# counts without adding topic signal. Three characters keeps useful +# acronyms (api, xml, sql). +_MIN_TF_TOKEN_LEN = 3 -@dataclass -class ChunkRecord: - """Lightweight view of one chunk row used by the clusterer.""" + +def _tokenize_for_tf(text: str) -> list[str]: + """Lowercase alphanumeric tokens for TF-IDF scoring. + + Deliberately has NO stopword list: common words like "the" or "and" + get an IDF near zero (they appear in almost every chunk) so TF-IDF + filters them automatically. A hand-curated English stoplist would + add maintenance burden and break on non-English corpora for no + additional quality. + + Lives beside :class:`ClusterChunk` because the record derives its + tokens from it; importing it from the helpers module would be circular. + """ + result: list[str] = [] + for raw in text.lower().split(): + word = "".join(ch for ch in raw if ch.isalnum()) + if len(word) >= _MIN_TF_TOKEN_LEN: + result.append(word) + return result + + +@dataclass(slots=True) +class ClusterChunk: + """Lightweight view of one chunk row used by the clusterer. + + Named for the clusterer rather than ``ChunkRecord`` so it cannot be + confused with :class:`lilbee.data.ingest.types.ChunkRecord`, an + unrelated TypedDict with a different field set used across ingest. + + ``tokens`` is a derived cache of ``_tokenize_for_tf(text)``. It is + computed here rather than at each construction site: TF-IDF labeling + silently degrades to fallback labels when a record carries no tokens, + so the invariant is structural instead of caller discipline. ``slots`` + keeps the per-record footprint down, since one record is materialized + per corpus chunk. + """ source: str chunk_index: int text: str tokens: list[str] = field(default_factory=list) + + def __post_init__(self) -> None: + if not self.tokens: + self.tokens = _tokenize_for_tf(self.text) diff --git a/src/lilbee/retrieval/concepts/__init__.py b/src/lilbee/retrieval/concepts/__init__.py index 26ec00439..00b58775b 100644 --- a/src/lilbee/retrieval/concepts/__init__.py +++ b/src/lilbee/retrieval/concepts/__init__.py @@ -6,7 +6,10 @@ concept overlap and expand queries via graph traversal. Requires optional ``graph`` extra: ``pip install lilbee[graph]``. -When dependencies are missing, all public functions degrade gracefully. +When dependencies are missing everything here degrades gracefully -- empty +results, ``concepts_available()`` False -- except ``load_spacy_pipeline``, +which raises ``ImportError`` by design so the caller can surface the model +download command. """ from __future__ import annotations diff --git a/src/lilbee/retrieval/concepts/community.py b/src/lilbee/retrieval/concepts/community.py index 9ad391251..a4a81c00e 100644 --- a/src/lilbee/retrieval/concepts/community.py +++ b/src/lilbee/retrieval/concepts/community.py @@ -28,10 +28,10 @@ def _compute_pmi( total_chunks: int, ) -> dict[tuple[str, str], float]: """Compute PPMI (Positive PMI) weights for concept co-occurrence pairs. - PPMI = max(0, log2(P(a,b) / (P(a) * P(b)))). Based on Church & Hanks 1990, "Word Association Norms, Mutual Information, - and Lexicography." Negative values are clamped to zero to discard - anti-correlated pairs. + and Lexicography." Pairs with non-positive PMI (co-occurring at or below + chance) are dropped entirely: a stored 0.0 would later be floored to a + positive Leiden weight, turning anti-correlation into attraction. """ pmi: dict[tuple[str, str], float] = {} for (a, b), count in cooccurrences.items(): @@ -40,12 +40,18 @@ def _compute_pmi( if p_a == 0 or p_b == 0: continue p_ab = count / total_chunks - pmi[(a, b)] = max(0.0, math.log2(p_ab / (p_a * p_b))) + value = math.log2(p_ab / (p_a * p_b)) + if value > 0: + pmi[(a, b)] = value return pmi class _LeidenResult(NamedTuple): - """Leiden output: node -> community id, and node -> weighted degree.""" + """Leiden output: node -> community id, and node -> incident-edge count. + + ``degrees`` is an unweighted count of edges incident to each node (edge + weights are not summed), which is what label ranking consumes. + """ partition: dict[str, int] degrees: dict[str, int] diff --git a/src/lilbee/retrieval/concepts/graph.py b/src/lilbee/retrieval/concepts/graph.py index 1ec13956e..d8c6331c2 100644 --- a/src/lilbee/retrieval/concepts/graph.py +++ b/src/lilbee/retrieval/concepts/graph.py @@ -171,16 +171,25 @@ def write_concept_records(self, records: ConceptRecords) -> None: cc_tbl.add(records.chunk_concepts) def boost_results(self, results: list[Any], query_concepts: list[str]) -> list[Any]: - """Boost search results whose chunks overlap with query concepts.""" + """Boost search results whose chunks overlap with query concepts. + + One batched chunk_concepts query serves the whole result set, grouped + back per chunk in Python -- the same batching get_related_concepts + uses per depth level. The table has no scalar index, so a per-result + predicate would be one full table scan per result. + """ if not query_concepts or not results: return results table = self._store.open_table(CHUNK_CONCEPTS_TABLE) if table is None: return results query_set = set(query_concepts) + concepts_by_chunk = self._chunk_concepts_batch( + table, {(r.source, r.chunk_index) for r in results} + ) boosted: list[Any] = [] for r in results: - chunk_concepts = set(self._chunk_concepts_from(table, r.source, r.chunk_index)) + chunk_concepts = concepts_by_chunk.get((r.source, r.chunk_index), set()) overlap = len(query_set & chunk_concepts) if overlap > 0: boost = (overlap / len(query_set)) * self._config.concept_boost_weight @@ -199,25 +208,45 @@ def get_chunk_concepts(self, source: str, chunk_index: int) -> list[str]: table = self._store.open_table(CHUNK_CONCEPTS_TABLE) if table is None: return [] - return self._chunk_concepts_from(table, source, chunk_index) + escaped = escape_sql_string(source) + try: + rows = ( + table.search() + .where(f"chunk_source = '{escaped}' AND chunk_index = {int(chunk_index)}") + .to_list() + ) + except Exception: + log.debug("get_chunk_concepts query failed for %r", source, exc_info=True) + return [] + return [r["concept"] for r in rows] @staticmethod - def _chunk_concepts_from(table: Any, source: str, chunk_index: int) -> list[str]: - """Query one chunk's concepts from an already-open table. - - Split out so a batch caller (``boost_results``) opens the table once - instead of re-opening it per result (an N+1 LanceDB access). + def _chunk_concepts_batch( + table: Any, chunks: set[tuple[str, int]] + ) -> dict[tuple[str, int], set[str]]: + """Fetch many chunks' concepts in one query, keyed by (source, index). + + The predicate is the cross product of the distinct sources and + indexes -- a cheap superset -- and rows are filtered back to the + exact requested pairs in Python. """ - escaped = escape_sql_string(source) + sources = ", ".join(f"'{escape_sql_string(s)}'" for s in sorted({s for s, _ in chunks})) + indexes = ", ".join(str(int(i)) for i in sorted({i for _, i in chunks})) try: rows = ( table.search() - .where(f"chunk_source = '{escaped}' AND chunk_index = {chunk_index}") + .where(f"chunk_source IN ({sources}) AND chunk_index IN ({indexes})") .to_list() ) - return [r["concept"] for r in rows] except Exception: - return [] + log.debug("chunk concepts batch query failed", exc_info=True) + return {} + concepts_by_chunk: dict[tuple[str, int], set[str]] = {} + for row in rows: + key = (row["chunk_source"], row["chunk_index"]) + if key in chunks: + concepts_by_chunk.setdefault(key, set()).add(row["concept"]) + return concepts_by_chunk def expand_query(self, query: str) -> list[str]: """Expand a query with related concepts from the graph.""" @@ -314,8 +343,9 @@ def _corpus_pmi_inputs( chunk_concepts is the ground-truth concept<->chunk map: it is source-scoped (re-ingesting a source replaces its rows) and its schema is stable, so PMI computed from it stays correct across re-ingests and version upgrades. The - edge table is append-only and its ``weight`` is a per-file co-occurrence - count, not corpus PMI, so it is not a safe source for these corpus counts. + edge table accrues per-file appends between rebuilds and those weights are + per-file co-occurrence counts, not corpus PMI, so it is not a safe source + for these corpus counts. Concepts are de-duplicated per chunk, so a concept (or pair) counts once per distinct chunk it appears in -- the document frequency PMI is defined on. @@ -346,11 +376,19 @@ def rebuild_clusters(self) -> None: co-occurrence and concept counts (see :meth:`_corpus_pmi_inputs`) rather than per file; summing per-file PMI would inflate pairs that recur across many small files. + + Both the nodes and the edges tables are replaced with the freshly + computed corpus graph. Per-file writes only ever append edges, so + without this rewrite the edges table grows monotonically across syncs + and expand_query keeps serving edges for concepts that left the corpus. """ cooccurrences, concept_counts, total_chunks = self._corpus_pmi_inputs() if total_chunks == 0 or not cooccurrences: return pmi_weights = _compute_pmi(cooccurrences, concept_counts, total_chunks) + if not pmi_weights: + # Every pair co-occurred at or below chance: no edge set to cluster. + return edge_rows = [{"source": a, "target": b, "weight": w} for (a, b), w in pmi_weights.items()] partition, degree_map = _leiden_partition(edge_rows) @@ -364,12 +402,15 @@ def rebuild_clusters(self) -> None: for node, cluster_id in partition.items() ] - # Delete the old nodes and add the new ones under one lock so a reader - # never sees the nodes table emptied while get_graph() still reports it - # present (which would blank top_communities / cluster labels). + # Delete the old rows and add the new ones under one lock per table so + # a reader never sees a table emptied while get_graph() still reports + # it present (which would blank top_communities / cluster labels). self._store.clear_and_add( CONCEPT_NODES_TABLE, _concept_nodes_schema(), node_records, "concept IS NOT NULL" ) + self._store.clear_and_add( + CONCEPT_EDGES_TABLE, _concept_edges_schema(), edge_rows, "source IS NOT NULL" + ) self.compact_tables() def compact_tables(self) -> None: diff --git a/src/lilbee/retrieval/concepts/nlp.py b/src/lilbee/retrieval/concepts/nlp.py index e71d2c3a2..16a9c93f4 100644 --- a/src/lilbee/retrieval/concepts/nlp.py +++ b/src/lilbee/retrieval/concepts/nlp.py @@ -11,7 +11,14 @@ def concepts_available() -> bool: - """Check if concept graph dependencies (spacy, graspologic) are installed.""" + """Check if concept graph dependencies (spacy, graspologic) are installed. + + Deliberately checks the *packages* only, not whether the ``en_core_web_sm`` + model is downloaded: a missing model is a fixable user situation, so + :func:`load_spacy_pipeline` raises with the download command rather than + being silently reported unavailable here. Callers therefore still handle + ``ImportError`` from the load even when this returns True. + """ try: import graspologic_native # noqa: F401 import spacy # noqa: F401 @@ -35,10 +42,12 @@ def _ensure_spacy_model() -> Any: def load_spacy_pipeline() -> Any: - """Public wrapper around the shared spaCy NER + noun-chunk pipeline. + """Public entry point for the shared spaCy NER + noun-chunk pipeline. - Raises ``ImportError`` if spaCy or the ``en_core_web_sm`` model cannot - be installed. + Raises ``ImportError`` if spaCy or the ``en_core_web_sm`` model is not + installed; the message carries the download command. This is the seam + other packages import -- ``_ensure_spacy_model`` stays private to this + module and its own package. """ return _ensure_spacy_model() diff --git a/src/lilbee/retrieval/embedder.py b/src/lilbee/retrieval/embedder.py index 0046a2ff5..a2dad6a61 100644 --- a/src/lilbee/retrieval/embedder.py +++ b/src/lilbee/retrieval/embedder.py @@ -67,7 +67,6 @@ class Embedder: def __init__(self, config: Config, provider: LLMProvider) -> None: self._config = config self._provider = provider - self.last_batch_truncated = 0 self._truncated_total = 0 self._truncated_lock = threading.Lock() @@ -92,9 +91,13 @@ def truncated_total(self) -> int: with self._truncated_lock: return self._truncated_total - def truncate(self, text: str) -> str: - """Truncate text to the embed char budget, counting any truncation.""" - budget = self.embed_char_budget + def truncate(self, text: str, reserved: int = 0) -> str: + """Truncate text to the embed char budget, counting any truncation. + + *reserved* is charged against the budget so an instruction prefix + prepended afterwards still leaves the sent text inside the guard. + """ + budget = max(1, self.embed_char_budget - reserved) if len(text) <= budget: return text log.debug("Truncating chunk from %d to %d chars for embedding", len(text), budget) @@ -116,8 +119,23 @@ def validate_vector(self, vector: list[float]) -> None: raise ValueError(f"Embedding contains invalid value at index {i}: {vector[i]}") def validate_model(self) -> bool: - """Check if the configured embedding model is available. No side effects.""" - return self.embedding_available() + """Availability gate for the startup and ingest paths: warns when it fails. + + Same probe as :meth:`embedding_available`, but this is the entry-point + check whose whole job is to not fail silently -- a missing model here + means every chunk of the run ahead will fail to embed, and the operator + needs to hear that once, up front, rather than as a per-file error much + later. Callers that can genuinely degrade (search falling back to + keyword) ask :meth:`embedding_available` and stay quiet. + """ + if self.embedding_available(): + return True + log.warning( + "Embedding model %r is not available; embedding will fail. " + "Pull it or set a different embedding_model.", + self._config.embedding_model, + ) + return False def embedding_available(self) -> bool: """Return True if the embedding model can be resolved. @@ -176,16 +194,17 @@ def _embed_with( Fires ``embed`` progress events per batch when *on_progress* is provided. """ if not texts: - self.last_batch_truncated = 0 return [] - truncated_before = self.truncated_total total_chunks = len(texts) max_batch_chars = self.batch_char_budget vectors: list[list[float]] = [] batch: list[str] = [] batch_chars = 0 for text in texts: - truncated = prefix + self.truncate(text) + # The prefix is charged against the budget: it is part of what the + # embed model receives, so counting only the text would ship + # budget+len(prefix) chars and lose the tail the clamp protects. + truncated = prefix + self.truncate(text, reserved=len(prefix)) chunk_len = len(truncated) if batch and batch_chars + chunk_len > max_batch_chars: vectors.extend(self._provider.embed(batch)) @@ -205,5 +224,4 @@ def _embed_with( ) for vec in vectors: self.validate_vector(vec) - self.last_batch_truncated = self.truncated_total - truncated_before return vectors diff --git a/src/lilbee/retrieval/embedding_profiles.py b/src/lilbee/retrieval/embedding_profiles.py index 914fe31ca..a107a50f7 100644 --- a/src/lilbee/retrieval/embedding_profiles.py +++ b/src/lilbee/retrieval/embedding_profiles.py @@ -24,8 +24,17 @@ class EmbeddingProfile: ), ) _E5 = EmbeddingProfile(query_instruction="query: ", doc_prefix="passage: ") +_SYMMETRIC = EmbeddingProfile() _FAMILY_PROFILES: tuple[tuple[str, EmbeddingProfile], ...] = ( ("qwen3-embedding", _INSTRUCT), + # "instructor" contains "instruct" but is a different dialect: the Instructor + # family (hkunlp/instructor-*, a t5encoder the engine can load) prefixes a + # "Represent the ... for retrieval:" instruction on the document as well as + # the query. Handing it the Instruct/Query query prefix with no doc prefix + # would be the asymmetric-in-the-wrong-dialect case this module promises not + # to produce, so it takes the symmetric fallback until the real prefixes are + # wired. + ("instructor", _SYMMETRIC), ("instruct", _INSTRUCT), # any instruction-tuned embedder: Instruct/Query, no doc prefix ("multilingual-e5", _E5), ("e5-", _E5), diff --git a/src/lilbee/retrieval/entities/extractor.py b/src/lilbee/retrieval/entities/extractor.py index 00149c2a4..28dea9e14 100644 --- a/src/lilbee/retrieval/entities/extractor.py +++ b/src/lilbee/retrieval/entities/extractor.py @@ -1,8 +1,8 @@ """Two-phase typed entity extraction. Phase 1 (cheap): an LLM reads a stratified sample of chunks and proposes the -corpus-specific type schema, persisted as an editable artifact before any -expensive pass runs. +corpus-specific type schema, persisted as machine state inside the index +before any expensive pass runs (see :mod:`schema`; nothing to review or edit). Phase 2 (scales with corpus): each type is found by the cheapest extractor that can serve it: compiled regex for identifier-shaped types, spaCy labels @@ -16,8 +16,11 @@ import logging import re from collections.abc import Mapping +from dataclasses import dataclass, field from typing import TYPE_CHECKING, Any +import regex + from lilbee.retrieval.entities.schema import ( EntitySchema, EntityType, @@ -43,6 +46,23 @@ _CONFIDENCE = {ExtractorKind.REGEX: 1.0, ExtractorKind.SPACY: 0.8, ExtractorKind.LLM: 0.6} + +@dataclass +class ExtractionStats: + """Mutable state :func:`extract_entities` carries across its calls. + + Lets the full pass tell a clean zero-entity result from batches the + provider failed outright -- the latter must not count as a completed + pass, or the schema gets marked applied with rows silently missing. + ``timed_out_types`` carries a runaway pattern's name across batches so + it is abandoned once per pass instead of per chunk. + """ + + llm_batches: int = 0 + llm_batches_failed: int = 0 + timed_out_types: set[str] = field(default_factory=set) + + INDUCTION_PROMPT = ( "You are designing an entity-extraction schema for a document collection. " "Below are sample passages. Propose the 3-8 entity TYPES most useful for " @@ -69,22 +89,22 @@ def _first_json_object(text: str) -> dict | None: - """The first balanced JSON object in *text*, or None.""" + """The first JSON object in *text*, or None. + + ``raw_decode`` from each ``{`` position lets the stdlib own the parsing + state; a hand-rolled brace counter miscounts braces inside string + literals (a regex pattern or entity text containing ``}``). + """ + decoder = json.JSONDecoder() start = text.find("{") - if start < 0: - return None - depth = 0 - for i, ch in enumerate(text[start:], start): - if ch == "{": - depth += 1 - elif ch == "}": - depth -= 1 - if depth == 0: - try: - parsed = json.loads(text[start : i + 1]) - except json.JSONDecodeError: - return None - return parsed if isinstance(parsed, dict) else None + while start >= 0: + try: + parsed, _ = decoder.raw_decode(text, start) + except json.JSONDecodeError: + start = text.find("{", start + 1) + continue + # A value starting at "{" can only decode to a dict. + return parsed if isinstance(parsed, dict) else None return None @@ -97,6 +117,28 @@ def normalize_value(text: str) -> str: return value +def _pattern_is_usable(entity_type: EntityType) -> bool: + """Whether an induced type's pattern can actually drive its extractor. + + Only llm kinds legitimately leave the pattern blank (they carry a + description instead). An empty regex compiles but then matches at every + position of every chunk in the corpus, and an empty spaCy label names + nothing. + """ + if entity_type.kind is ExtractorKind.LLM: + return True + if not entity_type.pattern: + log.warning("Dropping induced type %s: empty pattern", entity_type.name) + return False + if entity_type.kind is ExtractorKind.REGEX: + try: + _compile_pattern(entity_type.pattern) + except regex.error: + log.warning("Dropping induced type %s: bad regex", entity_type.name) + return False + return True + + def induce_schema(sample_texts: list[str], provider: LLMProvider) -> EntitySchema | None: """Phase 1: propose a schema from sampled chunk texts. None on failure.""" if not sample_texts: @@ -128,12 +170,8 @@ def induce_schema(sample_texts: list[str], provider: LLMProvider) -> EntitySchem except Exception: log.warning("Dropping invalid induced type: %r", raw) continue - if entity_type.kind is ExtractorKind.REGEX: - try: - re.compile(entity_type.pattern) - except re.error: - log.warning("Dropping induced type %s: bad regex", entity_type.name) - continue + if not _pattern_is_usable(entity_type): + continue # Small models sometimes propose several names for one extractor # (three types sharing a regex triple the table with identical rows); # the first name wins. @@ -151,9 +189,25 @@ def induce_schema(sample_texts: list[str], provider: LLMProvider) -> EntitySchem # never defeats the match. _IDENTIFIER_TOKEN_RE = re.compile(r"[0-9A-Za-z][0-9A-Za-z-]*") +# Wall-clock budget for one pattern against one chunk. Schema patterns are +# authored by a small local model, so a pathological one (nested quantifiers) +# is a realistic input; without a bound it backtracks until sync gives up. +# A second is orders of magnitude above what a sane identifier pattern costs. +_PATTERN_MATCH_TIMEOUT_SECONDS = 1.0 + -def _extract_regex(entity_type: EntityType, text: str) -> list[str]: - """Regex mentions, treating anchored patterns as per-token full matches. +def _compile_pattern(pattern: str) -> regex.Pattern[str]: + """Compile a schema pattern on the timeout-capable engine. + + ``regex`` accepts everything ``re`` does and, unlike the stdlib module, + takes a per-match ``timeout``, which is the only way to bound an + LLM-authored pattern running over the whole corpus. + """ + return regex.compile(pattern) + + +def _extract_regex(entity_type: EntityType, text: str) -> list[str] | None: + """Regex mentions, or ``None`` when the match blew its time budget. Schema authors (and the induction model) tend to write ^...$ patterns that describe an identifier's whole shape. Applied to running text those @@ -166,19 +220,24 @@ def _extract_regex(entity_type: EntityType, text: str) -> list[str]: # Only a single fully-anchored pattern converts; an alternation of # anchored branches (^A$|^B$) would be mangled by stripping the outer # pair, so it falls through to finditer unchanged. - if ( + anchored = ( pattern.startswith("^") and pattern.endswith("$") and "^" not in inner_text and "$" not in inner_text - ): - inner = re.compile(inner_text) - return [ - token.group(0) - for token in _IDENTIFIER_TOKEN_RE.finditer(text) - if inner.fullmatch(token.group(0)) - ] - return [m.group(0) for m in re.finditer(pattern, text)] + ) + try: + if anchored: + inner = _compile_pattern(inner_text) + return [ + token.group(0) + for token in _IDENTIFIER_TOKEN_RE.finditer(text) + if inner.fullmatch(token.group(0), timeout=_PATTERN_MATCH_TIMEOUT_SECONDS) + ] + compiled = _compile_pattern(pattern) + return [m.group(0) for m in compiled.finditer(text, timeout=_PATTERN_MATCH_TIMEOUT_SECONDS)] + except TimeoutError: + return None def _extract_spacy(types: list[EntityType], text: str, nlp: Any) -> list[tuple[EntityType, str]]: @@ -196,7 +255,14 @@ def _extract_llm_batch( types: list[EntityType], texts: list[str], provider: LLMProvider, -) -> list[list[tuple[EntityType, str]]]: +) -> list[list[tuple[EntityType, str]]] | None: + """One LLM extraction call over a batch of texts. + + Returns ``None`` when the provider call itself fails (model down or + unloaded), so the caller can tell a failed batch from a batch with no + entities. A response that parses to nothing usable is an empty result, + not a failure. + """ by_name = {t.name: t for t in types} type_lines = "\n".join(f"- {t.name}: {t.description or t.name}" for t in types) passages = "\n".join(f"[{i}] {t[:800]}" for i, t in enumerate(texts)) @@ -210,7 +276,7 @@ def _extract_llm_batch( ) except Exception: log.warning("LLM entity extraction failed for a batch", exc_info=True) - return empty + return None payload = _first_json_object(strip_reasoning(response.text)) if payload is None: return empty @@ -232,12 +298,39 @@ def _extract_llm_batch( return results +def _regex_findings( + regex_types: list[EntityType], text: str, timed_out: set[str] +) -> list[tuple[EntityType, str]]: + """Regex-kind findings for one chunk, skipping types that blew their budget. + + A type whose pattern times out is added to *timed_out* so the caller + stops attempting it; the ban lasts as long as that set does. + """ + found: list[tuple[EntityType, str]] = [] + for entity_type in regex_types: + if entity_type.name in timed_out: + continue + matches = _extract_regex(entity_type, text) + if matches is None: + timed_out.add(entity_type.name) + log.warning( + "Entity type %s: its pattern exceeded the %.0fs match budget; " + "skipping that type. Its regex is likely pathological.", + entity_type.name, + _PATTERN_MATCH_TIMEOUT_SECONDS, + ) + continue + found.extend((entity_type, m) for m in matches) + return found + + def extract_entities( chunks: list[Mapping[str, Any]], schema: EntitySchema, *, provider: LLMProvider | None = None, nlp: Any = None, + stats: ExtractionStats | None = None, ) -> list[dict]: """Phase 2 over ingest-shaped chunk records; returns entities-table rows. @@ -245,17 +338,21 @@ def extract_entities( ``page_start``. Extractor kinds degrade independently: regex always runs, spaCy kinds are skipped without a loaded model, LLM kinds without a provider, so a partial toolchain yields partial extraction, never failure. + Pass ``stats`` to observe how many LLM batches ran and how many the + provider failed; a failed batch contributes no rows either way. """ regex_types = [t for t in schema.types if t.kind is ExtractorKind.REGEX] spacy_types = [t for t in schema.types if t.kind is ExtractorKind.SPACY] llm_types = [t for t in schema.types if t.kind is ExtractorKind.LLM] + # A pattern that blows its budget is abandoned rather than retried on + # every remaining chunk; with stats the ban holds for the whole pass. + timed_out = stats.timed_out_types if stats is not None else set() + per_chunk: list[list[tuple[EntityType, str]]] = [] for record in chunks: text = record["chunk"] - found: list[tuple[EntityType, str]] = [] - for entity_type in regex_types: - found.extend((entity_type, m) for m in _extract_regex(entity_type, text)) + found = _regex_findings(regex_types, text, timed_out) if spacy_types and nlp is not None: found.extend(_extract_spacy(spacy_types, text, nlp)) per_chunk.append(found) @@ -264,6 +361,12 @@ def extract_entities( for start in range(0, len(chunks), LLM_EXTRACTION_BATCH): batch = chunks[start : start + LLM_EXTRACTION_BATCH] batch_found = _extract_llm_batch(llm_types, [r["chunk"] for r in batch], provider) + if stats is not None: + stats.llm_batches += 1 + if batch_found is None: + stats.llm_batches_failed += 1 + if batch_found is None: + continue for offset, found in enumerate(batch_found): per_chunk[start + offset].extend(found) diff --git a/src/lilbee/retrieval/entities/lifecycle.py b/src/lilbee/retrieval/entities/lifecycle.py index b1e72e93f..7b2949002 100644 --- a/src/lilbee/retrieval/entities/lifecycle.py +++ b/src/lilbee/retrieval/entities/lifecycle.py @@ -28,6 +28,7 @@ from lilbee.core.config import CHUNKS_TABLE, ENTITIES_TABLE, active_config from lilbee.retrieval.entities.extractor import ( INDUCTION_SAMPLE_SIZE, + ExtractionStats, extract_entities, induce_schema, ) @@ -45,7 +46,8 @@ log = logging.getLogger(__name__) -# Chunks per extraction batch during a full pass; bounds the working set. +# Chunks per extraction batch during a full pass; bounds the boxed Python +# rows in flight (the scan itself is projected and stays columnar). _BACKFILL_BATCH = 2000 # Corpus growth that makes the induced taxonomy worth revisiting, as a @@ -171,14 +173,22 @@ def _sample_chunks(store: Store, limit: int) -> list[str]: total = table.count_rows() if total == 0: return [] - step = max(1, total // limit) + # Ceiling division: flooring makes the stride too small, so the sample is + # the first `limit` contiguous rows and the table's tail -- the most + # recently ingested documents, exactly what re-induction exists to catch -- + # is never reached. + step = max(1, -(-total // limit)) texts: list[str] = [] - arrow = table.to_arrow().select(["chunk"]) + # Projection is pushed into LanceDB: a bare to_arrow() would drag every + # column, embedding vectors included, into memory before selecting. + arrow = table.search().select(["chunk"]).limit(None).to_arrow() index = 0 for batch in arrow.to_batches(max_chunksize=_BACKFILL_BATCH): for text in batch.column("chunk").to_pylist(): - if index % step == 0 and len(texts) < limit: + if index % step == 0: texts.append(text) + if len(texts) >= limit: + return texts index += 1 return texts @@ -186,7 +196,10 @@ def _sample_chunks(store: Store, limit: int) -> list[str]: def _full_pass(store: Store, schema: EntitySchema, cancel: threading.Event | None) -> bool: """Extract entities across every stored chunk. True when it completed. - Clears previously extracted rows first so the pass is idempotent. + Clears previously extracted rows first so the pass is idempotent. A pass + with failed LLM batches (chat model down or erroring) does NOT count as + completed: rows are missing for those chunks, and marking the schema + applied would make every later sync return early and never retry. """ table = store.open_table(CHUNKS_TABLE) if table is None: @@ -197,10 +210,10 @@ def _full_pass(store: Store, schema: EntitySchema, cancel: threading.Event | Non from lilbee.retrieval.concepts import concepts_available if concepts_available(): - from lilbee.retrieval.concepts.nlp import _ensure_spacy_model + from lilbee.retrieval.concepts.nlp import load_spacy_pipeline try: - nlp = _ensure_spacy_model() + nlp = load_spacy_pipeline() except ImportError: log.warning("spaCy model unavailable; skipping spaCy entity types") provider = None @@ -209,14 +222,23 @@ def _full_pass(store: Store, schema: EntitySchema, cancel: threading.Event | Non provider = get_services().provider written = 0 + stats = ExtractionStats() columns = ["chunk", "source", "chunk_index", "page_start"] - arrow = table.to_arrow().select(columns) + # Projection pushed into LanceDB; see _sample_chunks. + arrow = table.search().select(columns).limit(None).to_arrow() for batch in arrow.to_batches(max_chunksize=_BACKFILL_BATCH): if cancel is not None and cancel.is_set(): log.info("Entity extraction cancelled; the next sync restarts the pass") return False records = batch.to_pylist() - rows = extract_entities(records, schema, provider=provider, nlp=nlp) + rows = extract_entities(records, schema, provider=provider, nlp=nlp, stats=stats) written += store.add_entities(rows) + if stats.llm_batches_failed: + log.warning( + "Entity extraction pass had %d of %d LLM batches fail; the next sync redoes the pass", + stats.llm_batches_failed, + stats.llm_batches, + ) + return False log.info("Entity extraction complete: %d rows across the index", written) return True diff --git a/src/lilbee/retrieval/entities/schema.py b/src/lilbee/retrieval/entities/schema.py index fd63d6b95..14c800f8c 100644 --- a/src/lilbee/retrieval/entities/schema.py +++ b/src/lilbee/retrieval/entities/schema.py @@ -137,7 +137,7 @@ def save_schema(schema: EntitySchema, store: Store, *, applied: bool, source_cou store.save_entity_schema(payload, applied=applied, source_count=source_count) -def _entities_schema(dim_unused: int | None = None) -> pa.Schema: +def _entities_schema() -> pa.Schema: return pa.schema( [ pa.field("entity", pa.utf8()), diff --git a/src/lilbee/retrieval/query/compaction.py b/src/lilbee/retrieval/query/compaction.py index 79c961acd..f39512b80 100644 --- a/src/lilbee/retrieval/query/compaction.py +++ b/src/lilbee/retrieval/query/compaction.py @@ -14,7 +14,6 @@ from lilbee.retrieval.query.history_window import ( chars_for_tokens, - estimate_text_tokens, estimate_tokens, windowed_history, ) @@ -28,7 +27,7 @@ "Condense the conversation below into brief factual notes that let an " "assistant carry it on. Keep names, numbers, decisions, and anything left " "unresolved; drop pleasantries. Under {words} words. Return ONLY the notes.\n\n" - "{previous}Conversation:\n{transcript}" + "Conversation:\n{transcript}" ) # Ceiling on a summary's tokens; ctx/8 governs below it. A tighter ceiling @@ -103,9 +102,9 @@ class CompactionResult: summary: str condensed: int - """Turns folded into the notes.""" + """Messages folded into the notes (not exchanges: a user+assistant pair is two).""" stranded: int - """Turns dropped with no notes. Non-zero means the conversation lost detail + """Messages dropped with no notes. Non-zero means the conversation lost detail outright, which the UI must say plainly rather than let the model appear to have forgotten for no reason.""" @@ -116,7 +115,7 @@ class CompactionPlan: batches: list[list[ChatMessage]] stranded: int - """Turns dropped with no notes because the backlog exceeded MAX_COMPACT_CALLS. + """Messages dropped with no notes because the backlog exceeded MAX_COMPACT_CALLS. Deliberately no ``condensed`` counterpart: a plan cannot know what will be condensed, only what it will try. Whether a batch lands depends on the model @@ -194,9 +193,24 @@ def foldable(history: list[ChatMessage]) -> list[ChatMessage]: No ``keep`` parameter: COMPACT_KEEP_RECENT is the policy, and a knob nobody turns is just a second place for it to disagree with itself. + + The boundary is aligned forward to a user message so the kept window opens + a turn. A history can be odd-length (an interrupted turn persists a user + message with no reply), and cutting a fixed count would then leave the + window starting on an assistant reply, so the assembled prompt would run + user, assistant, assistant -- the non-alternating shape the summary pair + and windowed_history both take care to avoid. """ keep = COMPACT_KEEP_RECENT - return history[:-keep] if len(history) > keep else [] + if len(history) <= keep: + return [] + cut = len(history) - keep + for i in range(cut, len(history)): + if history[i]["role"] == "user": + return history[:i] + # No user message in the tail at all: keep the plain boundary rather than + # folding the entire history away. + return history[:cut] def summary_cap(ctx_target: int) -> int: @@ -204,9 +218,7 @@ def summary_cap(ctx_target: int) -> int: return max(_SUMMARY_MIN_TOKENS, min(COMPACT_MAX_TOKENS, ctx_target // _SUMMARY_CTX_FRACTION)) -def batch_overflow( - dropped: list[ChatMessage], previous_summary: str, *, ctx_target: int -) -> list[list[ChatMessage]]: +def batch_overflow(dropped: list[ChatMessage], *, ctx_target: int) -> list[list[ChatMessage]]: """Split *dropped* into batches whose summarize prompt each fit *ctx_target*. Compaction usually nibbles a pair at a time, but switching models does not: @@ -226,12 +238,7 @@ def batch_overflow( room = max( _MIN_BATCH_TOKENS, int( - ( - ctx_target - - summary_cap(ctx_target) - - estimate_text_tokens(previous_summary) - - _PROMPT_OVERHEAD_TOKENS - ) + (ctx_target - summary_cap(ctx_target) - _PROMPT_OVERHEAD_TOKENS) * _ESTIMATE_SAFETY_FRACTION ), ) @@ -256,9 +263,7 @@ def batch_overflow( return batches -def plan_compaction( - dropped: list[ChatMessage], previous_summary: str, *, ctx_target: int -) -> CompactionPlan: +def plan_compaction(dropped: list[ChatMessage], *, ctx_target: int) -> CompactionPlan: """Decide what one compaction condenses, keeping its cost bounded. Beyond MAX_COMPACT_CALLS batches the oldest turns are stranded rather than @@ -267,7 +272,7 @@ def plan_compaction( trade. The most recent slice is the part still worth remembering, and the caller reports the stranded count instead of pretending it survived. """ - batches = batch_overflow(dropped, previous_summary, ctx_target=ctx_target) + batches = batch_overflow(dropped, ctx_target=ctx_target) if len(batches) <= MAX_COMPACT_CALLS: return CompactionPlan(batches=batches, stranded=0) return CompactionPlan( diff --git a/src/lilbee/retrieval/query/dedup.py b/src/lilbee/retrieval/query/dedup.py index a1159a400..9f5314d99 100644 --- a/src/lilbee/retrieval/query/dedup.py +++ b/src/lilbee/retrieval/query/dedup.py @@ -105,11 +105,11 @@ def filter_results( ) -> list[SearchChunk]: """Drop results below min_relevance_score or above max_distance. - ``min_relevance_score`` gates on the canonical [0, 1] score, which is what - makes an abstention threshold possible. ``max_distance`` additionally drops - rows whose only signal is a far vector match (a row with lexical support - keeps its standing regardless of distance). Pass max_distance=0 to disable - distance filtering. + ``min_relevance_score`` gates on the [0, 1] fused score, which normalizes + against the configured weight budget (a constant), so the threshold means the + same thing across queries. ``max_distance`` additionally drops rows whose only + signal is a far vector match (a row with lexical support keeps its standing + regardless of distance). Pass max_distance=0 to disable distance filtering. """ if max_distance <= 0 and min_relevance_score <= 0: return results diff --git a/src/lilbee/retrieval/query/expansion.py b/src/lilbee/retrieval/query/expansion.py index ba03126b3..ac795a665 100644 --- a/src/lilbee/retrieval/query/expansion.py +++ b/src/lilbee/retrieval/query/expansion.py @@ -1,4 +1,4 @@ -"""Constants for LLM-driven query expansion and history condensation.""" +"""Constants for LLM-driven query expansion, HyDE, and history condensation.""" from __future__ import annotations @@ -10,6 +10,11 @@ EXPANSION_MAX_TOKENS = 200 +# HyDE writes one hypothetical answer passage to embed, which is a different +# shape of output from a list of query variants. Same number today, but tuning +# either one must not move the other. +HYDE_MAX_TOKENS = 200 + CONDENSE_PROMPT = ( "Rewrite the follow-up question as one standalone search query, resolving " "pronouns and references using the conversation. Return ONLY the rewritten " @@ -19,6 +24,7 @@ CONDENSE_MAX_TOKENS = 120 -# History turns included in the condensation prompt; older turns rarely change -# what a follow-up refers to and only add latency. +# Trailing history messages included in the condensation prompt (one +# ChatMessage each, so 6 is the last 3 user/assistant exchanges); older +# messages rarely change what a follow-up refers to and only add latency. CONDENSE_HISTORY_TURNS = 6 diff --git a/src/lilbee/retrieval/query/formatting.py b/src/lilbee/retrieval/query/formatting.py index 4f6e58686..6693404be 100644 --- a/src/lilbee/retrieval/query/formatting.py +++ b/src/lilbee/retrieval/query/formatting.py @@ -22,24 +22,28 @@ # Ranges wider than this are page spans or line numbers, not citation lists. _MAX_CITE_RANGE = 32 -# An LLM-generated citation block: a "Sources:"/"References:"/... heading line -# followed by a list (bullets, arrows, "[1]" or "1." numbering). Requiring the -# list keeps an answer that legitimately discusses such a heading in prose -# (e.g. "References:\n\nIt lists 40 works.") from being clipped. +# Heading shapes that open a model-authored citation block. Anchored to the +# start of the text as well as to a preceding newline, so an answer that is +# nothing but a fabricated block (heading at position 0) is still stripped. +_CITE_HEADING = ( + r"(?:\n{1,3}|\A)(?:#+\s*)?(?:(?:Key\s+)?Sources|References|Bibliography|Citations)\s*:?\s*" +) +# One list line: a bullet, arrow, "[1]" or "1." marker and the rest of its line. +_CITE_LIST_LINE = r"[ \t]*(?:[-*•→\[]|\d+[.)])[^\n]*" + +# A heading line followed by a list. The list is required so prose discussing +# such a heading is not clipped; the match ends with the list, not end-of-text, +# so an answer resuming after its citations keeps the continuation. _LLM_CITATION_BLOCK_RE = re.compile( - r"\n{1,3}(?:#+\s*)?(?:(?:Key\s+)?Sources|References|Bibliography|Citations)\s*:?\s*\n" - r"\s*(?:[-*•→\[]|\d+[.)]).*", - re.IGNORECASE | re.DOTALL, + _CITE_HEADING + r"\n\s*" + _CITE_LIST_LINE + r"(?:\n" + _CITE_LIST_LINE + r")*", + re.IGNORECASE, ) # A citation-style heading at the very end of the text, nothing after it yet. # Mid-stream this is ambiguous (the next line decides list vs prose), so it is # held back rather than shown; at end of stream it is a dangling artifact of a # citation block the model never finished, and is dropped either way. -_TRAILING_HEADING_RE = re.compile( - r"\n{1,3}(?:#+\s*)?(?:(?:Key\s+)?Sources|References|Bibliography|Citations)\s*:?\s*$", - re.IGNORECASE, -) +_TRAILING_HEADING_RE = re.compile(_CITE_HEADING + r"$", re.IGNORECASE) def display_source_path(source: str) -> str: @@ -50,8 +54,10 @@ def display_source_path(source: str) -> str: user's filesystem and substitute ``~`` for the home directory so the path is unambiguous without being noisy. - Falls back to the raw source string if the file no longer exists on disk - (e.g. the user moved the documents directory since ingestion). + Falls back to the raw source string only if resolution itself fails (an + exotic OSError such as a symlink loop). A missing file is not a failure: + ``resolve(strict=False)`` still returns the absolute path to where the + file would be, so a moved documents directory renders its old location. """ candidate = cfg.documents_dir / source try: @@ -94,13 +100,8 @@ def _source_file_url(source: str) -> str | None: def _source_locator(result: SearchChunk) -> str: """The ``, page N`` / ``, lines A-B`` suffix for a source line, or ''.""" - if result.content_type == "pdf" and (result.page_start or result.page_end): - ps, pe = result.page_start, result.page_end - return f", page {ps}" if ps == pe else f", pages {ps}-{pe}" - if result.content_type == "code" and (result.line_start or result.line_end): - ls, le = result.line_start, result.line_end - return f", line {ls}" if ls == le else f", lines {ls}-{le}" - return "" + location = _location_suffix(result) + return f", {location}" if location else "" def _format_citation(citation: CitationRecord) -> str: @@ -118,11 +119,17 @@ def _format_citation(citation: CitationRecord) -> str: def _location_suffix(result: SearchChunk) -> str: - """The page or line span of a chunk, or empty when neither applies.""" - if result.content_type == "pdf": + """The page or line span of a chunk, or empty when neither applies. + + Zero means "no location": PDF chunks whose page metadata was missing are + stored with page 0, so a locator is only rendered when at least one end + of the span is set. Sole owner of the rule -- ``_source_locator`` adds + the leading separator and nothing else. + """ + if result.content_type == "pdf" and (result.page_start or result.page_end): ps, pe = result.page_start, result.page_end return f"page {ps}" if ps == pe else f"pages {ps}-{pe}" - if result.content_type == "code": + if result.content_type == "code" and (result.line_start or result.line_end): ls, le = result.line_start, result.line_end return f"line {ls}" if ls == le else f"lines {ls}-{le}" return "" @@ -257,11 +264,25 @@ def cited_subset(answer: str, sources: list[SearchChunk]) -> list[SearchChunk]: continue name = Path(source.source).name.lower() stem = Path(source.source).stem - if name in lowered or (_identifier_shaped(stem) and stem.lower() in lowered): + if _mentions(lowered, name) or ( + _identifier_shaped(stem) and _mentions(lowered, stem.lower()) + ): picked.add(i) return [uniq[i] for i in sorted(picked)] +def _mentions(answer_lower: str, needle: str) -> bool: + """Whether *answer_lower* names *needle* as a whole token. + + Plain containment marks a source cited whenever its name embeds in a + longer one ("log-1" inside "catalog-10", "notes.md" inside + "footnotes.md"), which inflates the grounding signal. Filename + characters are what must not abut the match; surrounding punctuation + and whitespace still count as a mention. + """ + return re.search(rf"(? str: """The prefix of *text* safe to show: a model-authored citation block (heading plus list) is removed, and a bare trailing citation heading is diff --git a/src/lilbee/retrieval/query/history_window.py b/src/lilbee/retrieval/query/history_window.py index b0c26deeb..c5fe2bbad 100644 --- a/src/lilbee/retrieval/query/history_window.py +++ b/src/lilbee/retrieval/query/history_window.py @@ -5,27 +5,24 @@ from collections.abc import Callable from typing import TYPE_CHECKING +from lilbee.data.chunk import CHARS_PER_TOKEN + if TYPE_CHECKING: from lilbee.retrieval.query.searcher import ChatMessage -# Conservative char->token estimator. Matches OpenAI's "4 chars ~= 1 token" -# rule of thumb for English; under-counts non-ASCII slightly but the -# budget already leaves headroom for that. -_CHARS_PER_TOKEN = 4 - def estimate_text_tokens(text: str) -> int: """Cheap char/4 token estimate for a string.""" - return max(1, len(text) // _CHARS_PER_TOKEN) + return max(1, len(text) // CHARS_PER_TOKEN) def chars_for_tokens(tokens: int) -> int: """Rough char budget for a token budget: the inverse of estimate_text_tokens. - Keeps the chars-per-token ratio owned by this module, so callers that need to - clip text to a token budget do not import the constant behind its back. + The chars-per-token ratio itself is owned by :mod:`lilbee.data.chunk`, the + one place it is defined; this is the token->char direction of it. """ - return tokens * _CHARS_PER_TOKEN + return tokens * CHARS_PER_TOKEN def estimate_tokens(message: ChatMessage) -> int: @@ -45,6 +42,11 @@ def windowed_history( message; never strands an orphan assistant reply with no preceding user turn for the model to anchor to. The newest pair is always kept even if it exceeds the budget on its own (caller decides what to do then). + + A non-positive *max_tokens* disables windowing and returns everything, + rather than windowing hardest. No production caller can reach it today + (the context target has a floor), but a caller deriving a budget that + goes non-positive gets the full history, not an empty one. """ if max_tokens <= 0 or not messages: return list(messages) diff --git a/src/lilbee/retrieval/query/intent.py b/src/lilbee/retrieval/query/intent.py index 1c4c45bef..833ecc882 100644 --- a/src/lilbee/retrieval/query/intent.py +++ b/src/lilbee/retrieval/query/intent.py @@ -121,11 +121,23 @@ def matches_reference(ref: str, filename: str) -> bool: continue if token == ref_token: return True - if token.isdigit() and ref_token.isdigit() and int(token) == int(ref_token): + if _same_number(token, ref_token): return True return False +def _same_number(token: str, ref_token: str) -> bool: + """Whether two tokens are the same number ignoring leading zeros. + + Compares zero-stripped decimal strings rather than calling ``int``: + ``str.isdigit()`` is True for Unicode digits like the superscript two, + which ``int`` rejects, and the reference pattern matches those. + """ + if not (token.isdecimal() and ref_token.isdecimal()): + return False + return token.lstrip("0") == ref_token.lstrip("0") + + def title_candidates(question: str, lang: QueryLanguage | None = None) -> list[str]: """Document-title candidates from known-item question shapes. @@ -250,7 +262,10 @@ def parse_llm_aggregate(text: str) -> AggregateQuery | None: data = json.loads(match.group(0)) except json.JSONDecodeError: return None - kind = _LLM_KINDS.get(data.get("kind", "")) + raw_kind = data.get("kind", "") + # A non-string kind (list, dict) is malformed, not a crash: an unhashable + # value would raise TypeError inside dict.get. + kind = _LLM_KINDS.get(raw_kind) if isinstance(raw_kind, str) else None if kind is None: return None term = str(data.get("term", "") or "").strip() diff --git a/src/lilbee/retrieval/query/memory.py b/src/lilbee/retrieval/query/memory.py index fe888f5e0..a076b5f61 100644 --- a/src/lilbee/retrieval/query/memory.py +++ b/src/lilbee/retrieval/query/memory.py @@ -25,16 +25,19 @@ def format_memory_block( ) -> str: """Render preferences (always) then facts (by relevance) within *token_budget*. - Preferences claim the budget first; facts fill the remainder. Returns an empty - string when nothing fits. + Preferences claim the budget first; facts fill the remainder. The budget + covers the whole rendered block, framing included, so the header and footer + are charged up front. An entry too large for the remaining room is skipped + rather than ending the fill, so one oversized preference cannot strand + every fact behind it. Returns an empty string when nothing fits. """ - used = estimate_text_tokens(MEMORY_BLOCK_HEADER) + used = estimate_text_tokens(MEMORY_BLOCK_HEADER) + estimate_text_tokens(MEMORY_BLOCK_FOOTER) lines: list[str] = [] for memory in [*preferences, *facts]: line = f"- {memory.text}" cost = estimate_text_tokens(line) if used + cost > token_budget: - break + continue lines.append(line) used += cost if not lines: diff --git a/src/lilbee/retrieval/query/memory_extract.py b/src/lilbee/retrieval/query/memory_extract.py index bfe675d9d..0ea284b90 100644 --- a/src/lilbee/retrieval/query/memory_extract.py +++ b/src/lilbee/retrieval/query/memory_extract.py @@ -11,7 +11,6 @@ import json import logging -import re from collections.abc import Callable from dataclasses import dataclass @@ -39,7 +38,26 @@ _EXTRACT_USER_TEMPLATE = "User said:\n{question}\n\nAssistant replied:\n{answer}" -_JSON_ARRAY_RE = re.compile(r"\[.*\]", re.DOTALL) + +def _first_json_array(text: str) -> list | None: + """The first JSON array in *text*, or None. + + ``raw_decode`` from each ``[`` lets the stdlib find where the array + actually ends. A greedy ``\\[.*\\]`` span runs to the LAST bracket in the + reply, so a conforming array followed by prose containing any ``]`` (a + footnote marker, a second fence) fails to parse and drops every memory. + """ + decoder = json.JSONDecoder() + start = text.find("[") + while start >= 0: + try: + parsed, _ = decoder.raw_decode(text, start) + except json.JSONDecodeError: + start = text.find("[", start + 1) + continue + # A value starting at "[" can only decode to a list. + return parsed if isinstance(parsed, list) else None + return None @dataclass(frozen=True) @@ -78,14 +96,8 @@ def parse_extraction(raw: str) -> list[ExtractedMemory]: code fence), keeps only objects with a usable-length ``text``, and decodes the kind. Any parse failure yields an empty list. """ - match = _JSON_ARRAY_RE.search(raw) - if match is None: - return [] - try: - # The regex captures a bracketed span, so a successful parse is always - # a list (a malformed span raises and is caught below). - items = json.loads(match.group(0)) - except json.JSONDecodeError: + items = _first_json_array(raw) + if items is None: return [] memories: list[ExtractedMemory] = [] diff --git a/src/lilbee/retrieval/query/neighbors.py b/src/lilbee/retrieval/query/neighbors.py new file mode 100644 index 000000000..d456a083c --- /dev/null +++ b/src/lilbee/retrieval/query/neighbors.py @@ -0,0 +1,191 @@ +"""Neighbor-window expansion: widen retrieved passages with adjacent chunks. + +A hit that lands in the middle of an argument loses the sentences before and +after it. After context selection, each selected chunk pulls up to N adjacent +chunks per side from its own source and merges them into one contiguous +passage, deduplicating the overlap text adjacent chunks share from chunking. +The widened passage keeps the original chunk's score and citation identity; +only its text and page/line span change. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from collections.abc import Callable + + from lilbee.data.store import SearchChunk, Store + + +def _overlap_chars(left: str, right: str) -> int: + """Length of the longest suffix of *left* that is a prefix of *right*. + + Adjacent chunks carry the chunker's overlap verbatim (both cuts come from + the same document text), so the longest match IS the shared region. A + fully contained text matches whole, which is what makes merging an + already-widened passage idempotent instead of duplicating its neighbors. + + Longest length first, so the first match wins. A prefix-function scan is + the better complexity on paper but measured slower here on every input + shape tried: each ``endswith`` rejects on its first differing character in + C, while the linear version pays per-character interpreter overhead. + """ + for k in range(min(len(left), len(right)), 0, -1): + if left.endswith(right[:k]): + return k + return 0 + + +def merge_adjacent_texts(texts: list[str]) -> str: + """Concatenate adjacent chunk texts, deduplicating their shared overlap. + + Adjacent texts with no detectable overlap (a chunk_overlap=0 build) join + with a newline seam rather than gluing two words together. + """ + merged = texts[0] + for text in texts[1:]: + k = _overlap_chars(merged, text) + tail = text[k:] + if not tail: + continue + merged = merged + tail if k else f"{merged}\n{tail}" + return merged + + +def expand_neighbors( + results: list[SearchChunk], + store: Store, + radius: int, + budget: int, + cost: Callable[[str], int], +) -> list[SearchChunk]: + """Widen each result with up to *radius* adjacent same-source chunks. + + Results are processed in rank order and only spend *budget*, the tokens + left over after the originals were fitted: a widened passage whose extra + cost does not fit sheds its farthest neighbors first and falls back to + the original text, so expansion is always trimmed before any original + chunk. An index that is itself selected, or already claimed by a + higher-ranked expansion, is never pulled again, so no passage text is + duplicated (a document routed whole expands to nothing). + """ + if budget <= 0: + # Nothing can be spent, so skip the per-source store fetches entirely: + # on a tight window every widen attempt would fail against a zero + # budget after paying for the reads. + return results + centers: dict[str, set[int]] = {} + for r in results: + centers.setdefault(r.source, set()).add(r.chunk_index) + rows = _fetch_neighbor_rows(store, centers, radius) + if not rows: + return results + claimed = {source: set(indices) for source, indices in centers.items()} + remaining = budget + expanded: list[SearchChunk] = [] + for r in results: + widened, spent = _widen(r, rows, claimed[r.source], radius, remaining, cost) + remaining -= spent + expanded.append(widened) + return expanded + + +def _fetch_neighbor_rows( + store: Store, centers: dict[str, set[int]], radius: int +) -> dict[tuple[str, int], SearchChunk]: + """Every candidate neighbor row, fetched with one store call per source. + + The centers are fetched alongside their neighbors so the caller can tell + whether the document was re-ingested since the search snapshot; indices past + the end of a document are simply absent from the reply. + """ + rows: dict[tuple[str, int], SearchChunk] = {} + for source, owned in centers.items(): + wanted = sorted( + { + index + for center in owned + for index in range(center - radius, center + radius + 1) + if index >= 0 + } + ) + for row in store.get_chunks_by_indices(source, wanted): + rows[(source, row.chunk_index)] = row + return rows + + +def _neighbor_run( + result: SearchChunk, + rows: dict[tuple[str, int], SearchChunk], + claimed: set[int], + step: int, + radius: int, +) -> list[int]: + """Contiguous free neighbor indices on one side of the center, nearest first.""" + indices: list[int] = [] + for offset in range(1, radius + 1): + index = result.chunk_index + step * offset + if index in claimed or (result.source, index) not in rows: + break + indices.append(index) + return indices + + +def _widen( + result: SearchChunk, + rows: dict[tuple[str, int], SearchChunk], + claimed: set[int], + radius: int, + remaining: int, + cost: Callable[[str], int], +) -> tuple[SearchChunk, int]: + """One result widened within *remaining* tokens: (chunk, tokens spent). + + Neighbors extend from the center until a missing, selected, or already + claimed index stops each side. While the widened text over-spends, the + farthest neighbor is shed first (a tie sheds the trailing side, keeping + the text that leads up to the hit); shedding everything keeps the + original chunk untouched. + """ + center = result.chunk_index + current = rows.get((result.source, center)) + if current is not None and current.chunk != result.chunk: + # The document was re-ingested between the search and this fetch (reads + # take no lock and the store's read-consistency window is seconds), so + # the neighbor rows belong to a different chunking of the file. Splicing + # them would invent text and a page span that existed in no version. + return result, 0 + left = _neighbor_run(result, rows, claimed, -1, radius) + right = _neighbor_run(result, rows, claimed, +1, radius) + while left or right: + span = sorted([*left, center, *right]) + texts = [ + result.chunk if index == center else rows[(result.source, index)].chunk + for index in span + ] + merged = merge_adjacent_texts(texts) + extra = cost(merged) - cost(result.chunk) + if extra <= remaining: + claimed.update(index for index in span if index != center) + neighbors = [rows[(result.source, index)] for index in span if index != center] + return _widened_copy(result, merged, neighbors), extra + if right and (not left or right[-1] - center >= center - left[-1]): + right.pop() + else: + left.pop() + return result, 0 + + +def _widened_copy(result: SearchChunk, merged: str, neighbors: list[SearchChunk]) -> SearchChunk: + """The result with widened text and a truthfully recomputed page/line span.""" + spans = [result, *neighbors] + return result.model_copy( + update={ + "chunk": merged, + "page_start": min(s.page_start for s in spans), + "page_end": max(s.page_end for s in spans), + "line_start": min(s.line_start for s in spans), + "line_end": max(s.line_end for s in spans), + } + ) diff --git a/src/lilbee/retrieval/query/searcher.py b/src/lilbee/retrieval/query/searcher.py index 1d293f31c..679e9aa20 100644 --- a/src/lilbee/retrieval/query/searcher.py +++ b/src/lilbee/retrieval/query/searcher.py @@ -52,6 +52,7 @@ CONDENSE_PROMPT, EXPANSION_MAX_TOKENS, EXPANSION_PROMPT, + HYDE_MAX_TOKENS, ) from lilbee.retrieval.query.formatting import ( CONTEXT_TEMPLATE, @@ -75,6 +76,8 @@ title_candidates, ) from lilbee.retrieval.query.memory import format_memory_block +from lilbee.retrieval.query.neighbors import expand_neighbors +from lilbee.retrieval.query.structural import is_structural_chunk from lilbee.retrieval.query.tokenize import _idf_weights, _tokenize from lilbee.retrieval.reasoning import ( StreamToken, @@ -225,10 +228,16 @@ class StructuredQuery(NamedTuple): class RagContext(NamedTuple): - """Grounded context for one turn: the chunks and the prompt built on them.""" + """Grounded context for one turn: the chunks and the prompt built on them. + + ``base_results`` is the pre-widen selected set. An overflow retry refits from + it, not from ``results`` (whose neighbor text is baked in and can no longer + be shed), so a tighter fit drops expansion before it drops an original chunk. + """ results: list[SearchChunk] messages: list[ChatMessage] + base_results: list[SearchChunk] | None = None class Searcher: @@ -423,7 +432,7 @@ def _hyde_search( response = self._provider.chat( [{"role": "user", "content": self._config.hyde_prompt.format(question=question)}], stream=False, - options={"num_predict": EXPANSION_MAX_TOKENS}, + options={"num_predict": HYDE_MAX_TOKENS}, ) # Reasoning models front-load deliberation; embedding it instead # of the passage would search for the model's thought process. @@ -565,8 +574,9 @@ def search( """Embed question and search with expansion, HyDE, and concept boost. Returns up to top_k*2 candidates for downstream filtering. - When *chunk_type* is set (``"raw"`` or ``"wiki"``), only chunks of - that type are returned. An explicit ``chunk_type`` always wins + When *chunk_type* is set (``"raw"`` or ``"wiki"``), only matching + chunks are returned (``"raw"`` also covers table chunks). An + explicit ``chunk_type`` always wins over the ``wiki:``/``raw:`` prefix shortcut in *question* so the user-facing scope choice has the final say. @@ -625,6 +635,19 @@ def search( # HTTP, and MCP copies of a bare distance cutoff dropped both-arm # rows the fusion layer deliberately keeps past max_distance. results = filter_results(results, self._config.max_distance) + # Drop tables-of-contents and cover pages that only the vector arm + # surfaced: they dilute context precision without answering a question. + # Filtered from the top_k*2 candidate buffer so enough real passages + # remain for the downstream trim. + if self._config.filter_structural_chunks: + # A lexical (BM25 or title) hit or the top-ranked row is content the + # answer may need, whatever its shape, so it is never dropped; only + # structural chunks the lexical arms did not support are removed. + results = [ + r + for i, r in enumerate(results) + if r.bm25_score is not None or i == 0 or not is_structural_chunk(r.chunk) + ] return results[: top_k * 2] def _condense_question(self, question: str, history: list[ChatMessage]) -> str: @@ -679,14 +702,14 @@ def summarize_history( ``on_batch`` hears ``(batch, total)`` before each model call, for progress UI. """ ctx_target = self._config.chat_n_ctx_target - plan = plan_compaction(messages, previous_summary, ctx_target=ctx_target) + plan = plan_compaction(messages, ctx_target=ctx_target) notes: list[str] = [] condensed = 0 stranded = plan.stranded for index, batch in enumerate(plan.batches): if on_batch is not None: on_batch(index + 1, len(plan.batches)) - note = self._summarize_batch(batch, "") + note = self._summarize_batch(batch) if note: notes.append(note) condensed += len(batch) @@ -697,26 +720,29 @@ def summarize_history( merged = merge_notes(previous_summary, notes) cap = summary_cap(ctx_target) if estimate_text_tokens(merged) > cap: - merged = self._summarize_batch([{"role": "user", "content": merged}], "") or merged + merged = self._summarize_batch([{"role": "user", "content": merged}]) or merged return CompactionResult( summary=merged or previous_summary, condensed=condensed, stranded=stranded ) - def _summarize_batch(self, batch: list[ChatMessage], previous_summary: str) -> str: - """Fold one batch of dropped turns into *previous_summary*. + def _summarize_batch(self, batch: list[ChatMessage]) -> str: + """Fold one batch of dropped turns into notes. + + Each batch is summarized on its own, with no carried-forward notes in + the prompt: summarize_history merges the per-batch notes instead, which + keeps summary depth at one rather than re-summarizing the summary once + per batch. An overflowing batch splits in half and each half folds on its own: batch sizing is estimate-based, and the cost of an estimate miss here is stranded turns, not a slow call. Depth is log2 of the batch. - Returns *previous_summary* unchanged on any other failure: older notes - beat dropping the turns on the floor. + Returns "" on any other failure, so the caller counts the batch as + stranded rather than reporting turns it has no notes for. """ transcript = "\n".join(f"{m['role']}: {m['content']}" for m in batch) - previous = f"Earlier notes:\n{previous_summary}\n\n" if previous_summary.strip() else "" prompt = COMPACT_PROMPT.format( words=summary_word_budget(self._config.chat_n_ctx_target), - previous=previous, transcript=transcript, ) try: @@ -743,23 +769,23 @@ def _summarize_batch(self, batch: list[ChatMessage], previous_summary: str) -> s reasoning = split_reasoning(response.text).reasoning.strip() if reasoning: return reasoning - log.warning("History compaction returned nothing; keeping the previous summary") + log.warning("History compaction returned nothing for this batch") except ProviderError as exc: # A single message too big for the window cannot split; it falls # through to the warning below. if exc.kind is ProviderErrorKind.CONTEXT_OVERFLOW and len(batch) > 1: mid = len(batch) // 2 - first = self._summarize_batch(batch[:mid], previous_summary) - second = self._summarize_batch(batch[mid:], "") + first = self._summarize_batch(batch[:mid]) + second = self._summarize_batch(batch[mid:]) merged = "\n".join(part for part in (first, second) if part.strip()) if merged.strip(): return merged - log.warning("History compaction failed; keeping the previous summary", exc_info=True) + log.warning("History compaction failed for this batch", exc_info=True) except Exception: # warning, not debug: the user is told turns were dropped, so the # reason must be in the log by default. - log.warning("History compaction failed; keeping the previous summary", exc_info=True) - return previous_summary + log.warning("History compaction failed for this batch", exc_info=True) + return "" def _known_item_results(self, question: str) -> list[SearchChunk]: """Resolve a document named in *question* to its own chunks. @@ -860,8 +886,9 @@ def build_rag_context( ) -> RagContext | None: """Build RAG context from search results. - ``chunk_type`` restricts the pool to ``"raw"`` or ``"wiki"`` rows; - ``None`` (default) searches the mixed pool. + ``chunk_type`` restricts the pool to ``"raw"`` (which covers table + chunks too) or ``"wiki"`` rows; ``None`` (default) searches the + mixed pool. """ retrieval_query = question if history and self._config.history_rewrite: @@ -916,33 +943,31 @@ def _finalize_context( retrieved set tighter without re-running retrieval or condensation. """ system = self._system_with_memory(self._config.rag_system_prompt, question) - results = self._fit_context_budget(results, system, question, history, scale) + base_results = list(results) + budget = self._context_budget(system, question, history, scale) + results, used = self._fit_to_budget(results, budget) + results = self._widen_with_neighbors(results, max(0, budget - used)) context = build_context(results) prompt = CONTEXT_TEMPLATE.format(context=context, question=question) messages: list[ChatMessage] = [{"role": "system", "content": system}] if history: messages.extend(history) messages.append({"role": "user", "content": prompt}) - return RagContext(results, messages) + return RagContext(results, messages, base_results) @staticmethod def _budget_tokens(text: str) -> int: """Conservative token cost for budgeting (see _BUDGET_CHARS_PER_TOKEN).""" return max(1, len(text) // _BUDGET_CHARS_PER_TOKEN) - def _fit_context_budget( + def _context_budget( self, - results: list[SearchChunk], system: str, question: str, history: list[ChatMessage] | None, scale: float = 1.0, - ) -> list[SearchChunk]: - """Drop the lowest-ranked sources until the assembled prompt fits num_ctx. - - ``max_context_sources`` caps by count; this caps by tokens so a - retrieval-heavy query degrades gracefully instead of erroring with - CONTEXT_OVERFLOW. The top-ranked source is always kept. + ) -> int: + """Token budget left for source passages after the fixed prompt parts. The ceiling is the engine's ACTUAL per-slot window when known: the configured value is a target the dynamic picker aims for, and the @@ -962,7 +987,21 @@ def _fit_context_budget( + sum(self._budget_tokens(m["content"]) for m in history or []) + _CONTEXT_TEMPLATE_TOKENS ) - budget = int((prompt_token_budget(ctx) - non_source) * scale) + return int((prompt_token_budget(ctx) - non_source) * scale) + + def _fit_to_budget( + self, results: list[SearchChunk], budget: int + ) -> tuple[list[SearchChunk], int]: + """Fit *results* into *budget*: the kept sources and the tokens they cost. + + ``max_context_sources`` caps by count; this caps by tokens so a + retrieval-heavy query degrades gracefully instead of erroring with + CONTEXT_OVERFLOW. The top-ranked source is always kept. + + Returning the spent total lets the caller derive the leftover for + neighbor expansion instead of re-deriving the same per-chunk cost, so + the two stages cannot drift apart on the accounting. + """ kept: list[SearchChunk] = [] used = 0 for r in results: @@ -977,7 +1016,21 @@ def _fit_context_budget( len(kept), len(results), ) - return kept + return kept, used + + def _widen_with_neighbors(self, results: list[SearchChunk], leftover: int) -> list[SearchChunk]: + """Widen each fitted passage with adjacent same-source chunks. + + Spends only *leftover*, the budget the fit did not use, so a tight + window sheds expansion first and never drops an original chunk for a + neighbor. Widening keeps each passage's citation number and identity; + its text and page/line span do change, so the sources block shows the + widened range. + """ + radius = self._config.neighbor_expansion + if radius <= 0 or leftover <= 0: + return results + return expand_neighbors(results, self._store, radius, leftover, self._budget_tokens) def _system_with_memory(self, base_prompt: str, question: str) -> str: """Append the local-owner memory block to *base_prompt* when memory is enabled.""" @@ -1258,7 +1311,7 @@ def ask_raw( rag = self.build_rag_context(question, top_k=top_k, history=history, chunk_type=chunk_type) if rag is None: return AskResult(answer=GROUNDED_REFUSAL, sources=[]) - results, messages = rag + results, messages = rag.results, rag.messages opts = options if options is not None else self._config.generation_options() try: result = self._provider.chat( @@ -1267,13 +1320,18 @@ def ask_raw( except ProviderError as exc: if exc.kind is not ProviderErrorKind.CONTEXT_OVERFLOW or not results: raise - # The budget estimator is a heuristic; when the engine still - # reports overflow, refit the same retrieved set tighter and - # retry once instead of hard-failing the question. + # The budget estimator is a heuristic; when the engine still reports + # overflow, refit tighter and retry once. Refit from the pre-widen + # set so the tighter budget sheds neighbor expansion before it drops + # an original chunk, not the reverse. log.warning("Context overflow despite budgeting; retrying with a tighter fit") - results, messages = self._finalize_context( - results, question, history, scale=_OVERFLOW_RETRY_SCALE + retry = self._finalize_context( + rag.base_results if rag.base_results is not None else results, + question, + history, + scale=_OVERFLOW_RETRY_SCALE, ) + results, messages = retry.results, retry.messages result = self._provider.chat( self._messages_for_provider(messages), options=opts or None ) @@ -1347,10 +1405,10 @@ def ask_stream( if rag is None: yield StreamToken(content=GROUNDED_REFUSAL, is_reasoning=False) return - results, messages = rag + results, messages = rag.results, rag.messages # No overflow retry here: a stream cannot be rebuilt once tokens have - # been yielded, so the conservative budget in _fit_context_budget is - # the streaming path's protection. + # been yielded, so the conservative budget the context fit already + # applied is the streaming path's protection. provider_messages = self._messages_for_provider(messages) opts = options if options is not None else self._config.generation_options() events = stream_chat_with_cap( diff --git a/src/lilbee/retrieval/query/structural.py b/src/lilbee/retrieval/query/structural.py new file mode 100644 index 000000000..ed59da2db --- /dev/null +++ b/src/lilbee/retrieval/query/structural.py @@ -0,0 +1,67 @@ +"""Detect document-structure chunks that dilute retrieval precision. + +Flags two classes: tables of contents (generic), and classification-banner +cover/title pages. Both carry a document's title and section words but never +*answer* a substantive question. The detector is deliberately conservative: +it only flags chunks that are unambiguously structural, so a false negative +(some noise slips through) is preferred to a false positive (dropping real +content). +""" + +from __future__ import annotations + +import re + +# A TOC line ends in dot leaders followed by a page number: "Geographic Trends ....... 9". +_TOC_LINE = re.compile(r"\.{3,}\s*\d{1,4}\s*$") + +# Classification banners head cover/title pages and running headers alike, so the +# banner alone is not enough -- it is combined with low prose density below. +_CLASSIFICATION = re.compile(r"\b(UNCLASSIFIED|CONFIDENTIAL|SECRET|FOR OFFICIAL USE ONLY|FOUO)\b") + +# A chunk needs at least this many non-empty lines before the TOC ratio means anything. +_MIN_TOC_LINES = 3 +# And at least this many dot-leader lines, so a page with one stray "... 42" is not a TOC. +_MIN_TOC_HITS = 3 +_TOC_RATIO = 0.30 + +# Cover/title-page gates: a title page is very short with essentially no prose. +# Deliberately tight -- looser gates fire on short banner-carrying body pages and +# drop content the answer needs, so a real body page's word count or its first +# full sentence must take it out of scope. +_COVER_MAX_WORDS = 60 +_COVER_MAX_SENTENCES = 1 +_COVER_CAPS_RATIO = 0.30 + + +def _is_toc(nonempty: list[str]) -> bool: + """A table of contents: several dot-leader-to-page-number lines.""" + if len(nonempty) < _MIN_TOC_LINES: + return False + hits = sum(1 for line in nonempty if _TOC_LINE.search(line)) + return hits >= _MIN_TOC_HITS and hits / len(nonempty) >= _TOC_RATIO + + +def _is_cover_page(text: str) -> bool: + """A cover/title page: short, almost no sentences, shouting-case dominated, + and carrying a classification banner. Real prose has many sentence stops and + fails the sentence gate, so it is never flagged.""" + if not _CLASSIFICATION.search(text): + return False + words = text.split() + if not words or len(words) > _COVER_MAX_WORDS: + return False + sentences = text.count(".") + text.count("!") + text.count("?") + if sentences > _COVER_MAX_SENTENCES: + return False + caps = sum(1 for w in words if len(w) > 1 and w.isupper()) + return caps / len(words) >= _COVER_CAPS_RATIO + + +def is_structural_chunk(text: str) -> bool: + """True when *text* is a table of contents or a cover/title page -- a + document-structure chunk that should not compete as an answer passage.""" + if not text or not text.strip(): + return False + nonempty = [line for line in text.splitlines() if line.strip()] + return _is_toc(nonempty) or _is_cover_page(text) diff --git a/src/lilbee/retrieval/reasoning.py b/src/lilbee/retrieval/reasoning.py index c86cdec51..3a565b3d8 100644 --- a/src/lilbee/retrieval/reasoning.py +++ b/src/lilbee/retrieval/reasoning.py @@ -183,9 +183,10 @@ def effective_reasoning_cap() -> int: A per-model ``ModelDefaults.max_reasoning_chars`` value (including ``0`` for "this model is allowed to think forever") beats the global - ``cfg.max_reasoning_chars`` setting. Only ``None`` falls through to - the global, so a per-model 0 means the user explicitly opted that - model out of the cap. + ``cfg.max_reasoning_chars`` setting. A missing (``None``) or negative + per-model value falls through to the global: ModelDefaults is an + unvalidated dataclass, so a negative is treated as "unset" rather than + trusted as a cap. """ defaults = cfg.model_defaults override = defaults.max_reasoning_chars if defaults is not None else None @@ -207,33 +208,56 @@ def stream_chat_with_cap( in the first pass. If reasoning exceeds *cap_chars*, the upstream iterator is closed, a single ``CapNotice`` is yielded, and the continuation stream starts (same messages plus a user message asking - the model to answer directly). Continuation tokens stream as - ``StreamToken(is_reasoning=False)``. + the model to answer directly). + + The continuation is parsed too: a chat template can force-open a + ```` block whatever the nudge asks, and that reasoning must not + reach the visible answer. Its cap is disabled (the cap already fired + once, and re-capping would cut the answer off), and every continuation + token is reported as final-answer text, matching the async HTTP path. + + A run that reasoned but never produced final-answer text closes with + ``REASONING_EXHAUSTED_NOTICE``, so the CLI/TUI/library path ends with an + explanation rather than silence -- the same close the HTTP path makes. """ cap_fired = False + reasoned = False + answered = False def _on_cap() -> None: - nonlocal cap_fired + nonlocal cap_fired, reasoned cap_fired = True + reasoned = True + + def _on_reasoning(_chars: int) -> None: + nonlocal reasoned + reasoned = True first_stream = provider.chat(messages, stream=True, options=options or None, model=model) - yield from filter_reasoning( + for token in filter_reasoning( _text_only(first_stream), show=show_reasoning, cap_chars=cap_chars, on_cap=_on_cap, - ) - if not cap_fired: - return - yield CapNotice(cap_chars=cap_chars) - nudged = [*messages, {"role": "user", "content": CAP_CONTINUATION_PROMPT}] - second_stream = provider.chat(nudged, stream=True, options=options or None, model=model) - try: - for chunk in _text_only(second_stream): - if chunk: - yield StreamToken(content=chunk, is_reasoning=False) - finally: - _close_iterator(second_stream) + on_progress=_on_reasoning, + ): + answered = answered or not token.is_reasoning + yield token + if cap_fired: + yield CapNotice(cap_chars=cap_chars) + nudged = [*messages, {"role": "user", "content": CAP_CONTINUATION_PROMPT}] + second_stream = provider.chat(nudged, stream=True, options=options or None, model=model) + try: + for token in filter_reasoning( + _text_only(second_stream), show=show_reasoning, cap_chars=0 + ): + if token.content: + answered = True + yield StreamToken(content=token.content, is_reasoning=False) + finally: + _close_iterator(second_stream) + if reasoned and not answered: + yield StreamToken(content=REASONING_EXHAUSTED_NOTICE, is_reasoning=False) def _text_only(stream: Iterator[Any]) -> Iterator[str]: diff --git a/src/lilbee/retrieval/reranker.py b/src/lilbee/retrieval/reranker.py index 15aa5df27..4317d62dc 100644 --- a/src/lilbee/retrieval/reranker.py +++ b/src/lilbee/retrieval/reranker.py @@ -121,14 +121,27 @@ def rerank( def _score_candidates(query: str, to_rerank: list[SearchChunk]) -> list[float] | None: - """Call the active provider's rerank; return None on error after logging.""" + """Call the active provider's rerank; return None on error after logging. + + A provider that returns the wrong number of scores is contained here + like any other failure: the scores cannot be paired with the candidates, + so the pass is skipped and retrieval order stands. + """ # circular: services -> reranker via Searcher; deferred so test-time - # monkeypatching of ``lilbee.services.get_services`` stays effective. + # monkeypatching of ``lilbee.app.services.get_services`` stays effective. from lilbee.app.services import get_services try: provider = get_services().provider - return provider.rerank(query, [c.chunk for c in to_rerank]) + scores = provider.rerank(query, [c.chunk for c in to_rerank]) except Exception as exc: log.warning("Reranker failed; skipping rerank pass: %s", exc, exc_info=True) return None + if len(scores) != len(to_rerank): + log.warning( + "Reranker returned %d scores for %d candidates; skipping rerank pass", + len(scores), + len(to_rerank), + ) + return None + return scores diff --git a/src/lilbee/runtime/progress/types.py b/src/lilbee/runtime/progress/types.py index 05f24da17..65e879beb 100644 --- a/src/lilbee/runtime/progress/types.py +++ b/src/lilbee/runtime/progress/types.py @@ -86,10 +86,10 @@ class BatchProgressEvent(BaseModel): class ExtractEvent(BaseModel): """Emitted with page-level extraction progress. - Vision PDF OCR fires one event per page (``page < total_pages``); - plain (non-OCR) extraction fires once per file with - ``page == total_pages`` so subscribers see "extracted N pages" - before the embed phase ticks. + OCR fires one event per page as xberg processes it, as a running count + with ``total_pages == 0`` (the total is unknown mid-extraction). Extraction + then fires once per file with ``page == total_pages`` so subscribers see + "extracted N pages" before the embed phase ticks. """ file: str diff --git a/src/lilbee/server/handlers/models.py b/src/lilbee/server/handlers/models.py index 4c63c55de..8cf4cf31f 100644 --- a/src/lilbee/server/handlers/models.py +++ b/src/lilbee/server/handlers/models.py @@ -280,6 +280,8 @@ async def set_embedding_model(model: str) -> SetModelResponse: async def set_vision_model(model: str) -> SetModelResponse: """Switch vision OCR model. Empty string unsets it (vision OCR disabled).""" normalized = _require_model_for_task(model, ModelTask.VISION, allow_empty=True) + # The OCR-backend (un)registration is handled by apply_settings_update's role + # reload fan-out inside _set_model, covering REST/MCP/TUI/CLI uniformly. return await _set_model("vision_model", normalized) diff --git a/src/lilbee/server/handlers/rag.py b/src/lilbee/server/handlers/rag.py index 470b2c315..25614bbad 100644 --- a/src/lilbee/server/handlers/rag.py +++ b/src/lilbee/server/handlers/rag.py @@ -509,7 +509,7 @@ async def chat( return AskResponse( answer=GROUNDED_REFUSAL, sources=[], cited_sources=[], compaction=compaction ) - sources, messages = rag + sources, messages = rag.results, rag.messages req = _build_canonical_request(messages, options) response = await asyncio.to_thread(dispatch_chat, req) text = _join_text_blocks(response.content) @@ -631,7 +631,7 @@ async def _stream_chat_response( yield frame if ctx is None: return - sources, messages = ctx + sources, messages = ctx.results, ctx.messages req = _build_canonical_request(messages, options) answer_parts: list[str] = [] @@ -852,7 +852,7 @@ def _resolve_stream_context( return _StreamResolution([], None, [frame]) if rag is None: return _StreamResolution([], None, [sse_error("No relevant documents found.")]) - results, messages = rag + results, messages = rag.results, rag.messages return _StreamResolution(results, messages, []) diff --git a/src/lilbee/skills/lilbee_mcp/SKILL.md b/src/lilbee/skills/lilbee_mcp/SKILL.md index 995e91bed..931b5cc06 100644 --- a/src/lilbee/skills/lilbee_mcp/SKILL.md +++ b/src/lilbee/skills/lilbee_mcp/SKILL.md @@ -197,7 +197,7 @@ them back. ### 5. Your first answer feels thin -- self-tune and retry When the user asks a broad question against a dense pile of reference -docs (godot class XMLs, an API reference, kreuzberg-style docstrings) +docs (godot class XMLs, an API reference, xberg-style docstrings) and your first `lilbee_search` returns only one or two relevant hits where you'd expect a family, the retrieval defaults are too narrow for the shape of what's indexed. Self-tune in-place rather than handing the diff --git a/src/lilbee/vision.py b/src/lilbee/vision.py index bc0e35a11..b0f2e4c27 100644 --- a/src/lilbee/vision.py +++ b/src/lilbee/vision.py @@ -1,35 +1,10 @@ -"""Helpers for PDF rasterisation and vision-model OCR. +"""Vision-model OCR helpers: prompt resolution and OpenAI-compatible image messages. -Multi-page vision OCR runs through ``FleetProvider.pdf_ocr``, which rasterises -each page and sends it to the vision server; this module hosts the small helpers -(page count, rasterisation, prompt + chat-message construction, and the shared -:class:`PageText` / :class:`PdfOcrChunk` types) that the provider and its callers -share. +PDF rasterisation and the page loop now live inside xberg (the registered +lilbee-vision OCR backend); this module only builds the single-image request the +provider's ``vision_ocr`` sends to the vision server. """ -import logging -from collections.abc import Iterator -from pathlib import Path -from typing import NamedTuple - -log = logging.getLogger(__name__) - - -class PageText(NamedTuple): - """Extracted text for a single PDF page.""" - - page: int - text: str - - -class PdfOcrChunk(NamedTuple): - """One streaming PDF-OCR worker frame: page index, total pages, page text.""" - - page: int - total: int - text: str - - OCR_PROMPT = ( "Extract ALL text from this page as clean markdown. " "Preserve table structure using markdown table syntax. " @@ -53,25 +28,6 @@ def resolve_ocr_prompt(model_ref: str) -> str: return OCR_PROMPT -_RASTER_DPI = 150 - - -def pdf_page_count(path: Path) -> int: - """Return the number of pages in a PDF without rasterizing.""" - from kreuzberg import PdfPageIterator # lazy: heavy dependency - - it = PdfPageIterator(str(path), dpi=_RASTER_DPI) - return len(it) - - -def rasterize_pdf(path: Path) -> Iterator[tuple[int, bytes]]: - """Yield (0-based index, PNG bytes) for each page of a PDF.""" - from kreuzberg import PdfPageIterator # lazy: heavy dependency - - with PdfPageIterator(str(path), dpi=_RASTER_DPI) as pages: - yield from pages - - def _png_to_data_url(png_bytes: bytes) -> str: """Convert raw PNG bytes to a base64 data URL for OpenAI-compatible messages.""" import base64 diff --git a/src/lilbee/wiki/citation.py b/src/lilbee/wiki/citation.py index 2ef63b9ce..e64159e10 100644 --- a/src/lilbee/wiki/citation.py +++ b/src/lilbee/wiki/citation.py @@ -10,9 +10,7 @@ from lilbee.wiki.grammar import ( CITATION_BLOCK_COMMENT, CITATION_BLOCK_SEP, - CITE_RE, FOOTNOTE_RE, - INFERENCE_RE, ) @@ -80,29 +78,24 @@ def verify_citation(citation: CitationRecord, source_text: str) -> CitationStatu by comparing ``citation.source_hash`` against the current file hash and checking file presence. """ + from xberg import verify_excerpt + if not citation["excerpt"]: return CitationStatus.EXCERPT_MISSING - if _normalize(citation["excerpt"]) in _normalize(source_text): + if verify_excerpt(citation["excerpt"], source_text): return CitationStatus.VALID return CitationStatus.EXCERPT_MISSING def find_unmarked_claims(markdown: str) -> list[str]: - """Find statements that are neither cited ``[^srcN]`` nor marked ``[*inference*]``. - Scans non-empty, non-metadata lines in the body (before the citation block). - Returns the text of each unmarked line. + """Find body statements that are neither cited ``[^srcN]`` nor marked ``[*inference*]``. + + Delegates to xberg's footnote/citation API over the body (frontmatter and the + citation block stripped). """ - body = extract_body(markdown) - lines = body.splitlines() - unmarked: list[str] = [] - for line in lines: - stripped = line.strip() - if not _is_content_line(stripped): - continue - if CITE_RE.search(stripped) or INFERENCE_RE.search(stripped): - continue - unmarked.append(stripped) - return unmarked + from xberg import find_unmarked_claims as _find_unmarked_claims + + return _find_unmarked_claims(extract_body(markdown)) def strip_citation_block(markdown: str) -> str: @@ -154,15 +147,6 @@ def _strip_frontmatter(markdown: str) -> str: return markdown -def _is_content_line(stripped: str) -> bool: - """Return True if a line contains a substantive claim (not heading/blank/marker).""" - if not stripped: - return False - if stripped.startswith("#"): - return False - return stripped != CITATION_BLOCK_SEP - - def _format_source_ref(rec: CitationRecord) -> str: """Format a CitationRecord into a human-readable footnote reference.""" ref = rec["source_filename"] @@ -180,8 +164,3 @@ def _format_source_ref(rec: CitationRecord) -> str: if rec["excerpt"]: ref += f', excerpt: "{rec["excerpt"]}"' return ref - - -def _normalize(text: str) -> str: - """Normalize whitespace for fuzzy excerpt matching.""" - return " ".join(text.split()).lower() diff --git a/src/lilbee/wiki/citations.py b/src/lilbee/wiki/citations.py index 579582e15..421a048a3 100644 --- a/src/lilbee/wiki/citations.py +++ b/src/lilbee/wiki/citations.py @@ -152,8 +152,10 @@ def verify_citations( config: Config, ) -> list[CitationRecord]: """Filter citation records, keeping only those whose excerpts are in the chunks.""" + from xberg import verify_excerpt + wiki_prefix = config.wiki_dir + "/" - all_chunk_text = normalize_whitespace(" ".join(c.chunk for c in chunks)) + all_chunk_text = " ".join(c.chunk for c in chunks) verified: list[CitationRecord] = [] for rec in citation_records: if rec["source_filename"].startswith(wiki_prefix): @@ -162,7 +164,7 @@ def verify_citations( if rec["claim_type"] == "inference" or not rec["excerpt"]: verified.append(rec) continue - if normalize_whitespace(rec["excerpt"]) in all_chunk_text: + if verify_excerpt(rec["excerpt"], all_chunk_text): verified.append(rec) else: log.debug("Citation %s excerpt not found in %s, dropping", rec["citation_key"], label) @@ -172,10 +174,9 @@ def verify_citations( def render_provenance(config: Config, chunks: list[SearchChunk]) -> str: """Render the provenance block: chunk references + extraction method. - Routes through ``yaml.safe_dump`` rather than hand-rolled string - formatting so a chunk source containing a quote, backslash, - colon, or newline does not produce invalid YAML that - ``parse_frontmatter`` would silently drop on read. + Uses ``yaml.safe_dump`` so a chunk source containing a quote, backslash, + colon, or newline cannot produce invalid YAML that ``parse_frontmatter`` + would silently drop on read. """ block = { "provenance": { diff --git a/src/lilbee/wiki/page.py b/src/lilbee/wiki/page.py index 9392690ff..e1b9a4ff7 100644 --- a/src/lilbee/wiki/page.py +++ b/src/lilbee/wiki/page.py @@ -149,9 +149,8 @@ def build_frontmatter( the generator and the extraction method from config, so a bad page is auditable without re-running the pipeline. """ - # JSON-serialize the list: a JSON array is valid YAML flow syntax and escapes - # quotes/backslashes/unicode, so a filename like ``a"b\c.txt`` can't corrupt - # the frontmatter (the hand-rolled per-name quoting did). + # A JSON array is valid YAML flow syntax and escapes quotes/backslashes/ + # unicode, so a filename like ``a"b\c.txt`` cannot corrupt the frontmatter. sources_yaml = json.dumps(sorted(source_names)) hash_line = f"leaf_hash: {leaf_hash}\n" if leaf_hash else "" provenance_block = render_provenance(config, chunks) if chunks is not None else "" diff --git a/tests/conftest.py b/tests/conftest.py index 3015f9103..50663003b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -30,10 +30,15 @@ from lilbee.catalog import CatalogModel from lilbee.catalog.refs import format_native_gguf_ref from lilbee.core.config import cfg +from lilbee.data import xberg_extract as _xberg_extract from lilbee.data.ingest import file_hash from lilbee.data.store import CitationRecord from lilbee.modelhub.registry import ModelManifest, ModelRegistry +# Pristine extraction entry points, captured before any test can patch them. +_PRISTINE_EXTRACT_DOCUMENT = _xberg_extract.extract_document +_PRISTINE_AEXTRACT_DOCUMENT = _xberg_extract.aextract_document + FIXTURES_DIR = Path(__file__).parent / "fixtures" @@ -226,6 +231,23 @@ def overlay_reads_config_toml(monkeypatch): monkeypatch.delenv("LILBEE_SKIP_TOML_CONFIG", raising=False) +@pytest.fixture(autouse=True) +def _reset_xberg_extract_globals(): + """Start every test with the real extraction functions. + + ``extract_document`` (sync) resolves ``aextract_document`` via module-global + lookup, so a ``mock.patch`` of ``lilbee.data.xberg_extract.aextract_document`` + intercepts both the async and the sync path -- and the sync path is what + ``chunk_text`` uses. The ingest/handler suites patch that global heavily; under + the parallel run a patch occasionally stays active into an unrelated test, and + ``chunk_text`` then silently returns mock content ('Some extracted text.'), + a nondeterministic cross-test failure. Resetting the globals before each test + makes that leak impossible regardless of how a patch escaped. (bb-ql1) + """ + _xberg_extract.extract_document = _PRISTINE_EXTRACT_DOCUMENT + _xberg_extract.aextract_document = _PRISTINE_AEXTRACT_DOCUMENT + + @pytest.fixture(autouse=True) def _no_leaked_task_workers(): """Fail the test that leaves a live task-bar worker thread behind. @@ -585,3 +607,26 @@ def install_fake_model(hf_repo: str, gguf_filename: str, task: str) -> str: ), ) return format_native_gguf_ref(hf_repo, gguf_filename) + + +def make_pdf(*, pages: int = 1, title: str | None = None, author: str | None = None) -> bytes: + """A born-digital PDF with a real text layer, for extraction tests. + + reportlab always writes a ``/Title``, defaulting to "untitled", so a PDF built + without an explicit ``title`` still carries one rather than reporting none. + """ + import io + + from reportlab.pdfgen import canvas + + buf = io.BytesIO() + c = canvas.Canvas(buf) + if title is not None: + c.setTitle(title) + if author is not None: + c.setAuthor(author) + for i in range(pages): + c.drawString(72, 720, f"Page {i + 1} with a perfectly clean native text layer.") + c.showPage() + c.save() + return buf.getvalue() diff --git a/tests/integration/test_pdf_integration.py b/tests/integration/test_pdf_integration.py index ce05e7a74..205a4d60d 100644 --- a/tests/integration/test_pdf_integration.py +++ b/tests/integration/test_pdf_integration.py @@ -3,7 +3,7 @@ Uses a rasterized PDF fixture (no selectable text) to exercise the full extraction pipeline including Tesseract OCR and vision model fallbacks. -Requires: kreuzberg, Tesseract (for OCR tests), a vision model (for vision tests). +Requires: xberg, Tesseract (for OCR tests), a vision model (for vision tests). Skipped automatically when dependencies are not available. Run with: @@ -132,11 +132,20 @@ class TestTesseractOcrFallback: ) async def test_tesseract_extracts_text(self): """Tesseract OCR produces non-empty text from the scanned PDF fixture.""" - from kreuzberg import ExtractionConfig, OcrConfig, extract_file + from xberg import ExtractionConfig, OcrConfig - config = ExtractionConfig(ocr=OcrConfig()) - result = await extract_file(str(SCANNED_PDF), config=config) - assert len(result.content.strip()) > 0, "Tesseract produced empty text" + from lilbee.data.xberg_extract import aextract_document + + config = ExtractionConfig( + ocr=OcrConfig(backend="tesseract", language=["eng"]), force_ocr=True + ) + doc = await aextract_document( + SCANNED_PDF.read_bytes(), + mime_type="application/pdf", + filename=SCANNED_PDF.name, + config=config, + ) + assert len(doc.content.strip()) > 0, "Tesseract produced empty text" @pytest.mark.skipif( not shutil.which("tesseract"), @@ -144,11 +153,20 @@ async def test_tesseract_extracts_text(self): ) async def test_tesseract_extracts_known_phrases(self): """Tesseract OCR captures key phrases from the scanned document.""" - from kreuzberg import ExtractionConfig, OcrConfig, extract_file + from xberg import ExtractionConfig, OcrConfig + + from lilbee.data.xberg_extract import aextract_document - config = ExtractionConfig(ocr=OcrConfig()) - result = await extract_file(str(SCANNED_PDF), config=config) - text_lower = result.content.lower() + config = ExtractionConfig( + ocr=OcrConfig(backend="tesseract", language=["eng"]), force_ocr=True + ) + doc = await aextract_document( + SCANNED_PDF.read_bytes(), + mime_type="application/pdf", + filename=SCANNED_PDF.name, + config=config, + ) + text_lower = doc.content.lower() # At least some of the rendered text should be recognized recognized = any( phrase in text_lower @@ -171,41 +189,44 @@ def _vision_model_available() -> bool: class TestVisionOcrFallback: - """Verify vision model OCR fallback on scanned PDFs.""" + """Vision OCR through the registered lilbee-vision xberg backend.""" + + async def _vision_extract(self) -> str: + from xberg import ExtractionConfig, OcrConfig + + from lilbee.app.services import get_services, sync_vision_ocr_backend + from lilbee.data.ingest.types import OcrBackendName + from lilbee.data.xberg_extract import aextract_document + + sync_vision_ocr_backend(get_services().provider) + config = ExtractionConfig( + ocr=OcrConfig(backend=OcrBackendName.LILBEE_VISION), force_ocr=True + ) + doc = await aextract_document( + SCANNED_PDF.read_bytes(), + mime_type="application/pdf", + filename=SCANNED_PDF.name, + config=config, + ) + return doc.content @pytest.mark.skipif( not _vision_model_available(), - reason="No vision-capable chat model available locally", + reason="No vision-capable model available locally", ) async def test_vision_extracts_text(self): - """Vision model OCR produces non-empty text from the scanned PDF fixture.""" - from lilbee.app.services import get_services - - page_texts = get_services().provider.pdf_ocr( - SCANNED_PDF, - backend="vision", - model=cfg.chat_model, - per_page_timeout_s=cfg.ocr_timeout, - ) - all_text = " ".join(text for _, text in page_texts) - assert len(all_text.strip()) > 0, "Vision OCR produced empty text" + """Vision OCR via the registered backend produces non-empty text.""" + content = await self._vision_extract() + assert len(content.strip()) > 0, "Vision OCR produced empty text" @pytest.mark.skipif( not _vision_model_available(), - reason="No vision-capable chat model available locally", + reason="No vision-capable model available locally", ) async def test_vision_extracts_known_phrases(self): - """Vision model OCR captures key phrases from the scanned document.""" - from lilbee.app.services import get_services - - page_texts = get_services().provider.pdf_ocr( - SCANNED_PDF, - backend="vision", - model=cfg.chat_model, - per_page_timeout_s=cfg.ocr_timeout, - ) - all_text = " ".join(text for _, text in page_texts).lower() + """Vision OCR via the registered backend captures key phrases.""" + text_lower = (await self._vision_extract()).lower() recognized = any( - phrase in all_text for phrase in ["oil", "maintenance", "filter", "quarts", "engine"] + phrase in text_lower for phrase in ["oil", "maintenance", "filter", "quarts", "engine"] ) - assert recognized, f"No expected phrases found in vision output: {all_text[:200]}" + assert recognized, f"No expected phrases found in vision output: {text_lower[:200]}" diff --git a/tests/integration/test_xet_progress_granularity.py b/tests/integration/test_xet_progress_granularity.py new file mode 100644 index 000000000..4d516f3c0 --- /dev/null +++ b/tests/integration/test_xet_progress_granularity.py @@ -0,0 +1,297 @@ +"""Bounded-prefix QA matrix: real-time download-progress granularity, xet vs HTTP. + +WHY: lilbee sets ``HF_HUB_DISABLE_XET=1`` (src/lilbee/__init__.py) because the xet +transfer layer historically reported download progress in 3-4 coarse jumps (HF issue +#4058), making bars look stuck on large files. Forcing the HTTP path restores smooth +per-chunk updates. This harness empirically measures whether hf-xet's transfer layer +now drives our progress callback (catalog/download_progress.py) at chunk granularity, +i.e. whether the bypass can be dropped. + +It NEVER downloads the whole file. Each transfer path runs in a fresh subprocess +(``HF_HUB_DISABLE_XET`` is read at import time, so it must be set before ``import +lilbee``). The child streams progress samples; the parent kills it once a bounded byte +prefix has been sampled. Total bandwidth per run is ~2x ``PREFIX_BUDGET_BYTES``. + +Manual run (prints the matrix + verdict): + uv run python tests/integration/test_xet_progress_granularity.py +Pytest (slow): + uv run pytest tests/integration/test_xet_progress_granularity.py -v -m slow +""" + +from __future__ import annotations + +import contextlib +import os +import signal +import statistics +import subprocess +import sys +import tempfile +import time +from dataclasses import dataclass, field +from pathlib import Path + +import pytest + +pytestmark = pytest.mark.slow + +# A xet-backed GGUF (get_hf_file_metadata reports XetFileData for this file). +# ~1.0 GB total; we only sample the prefix, so the full size never downloads. +TEST_REPO = "unsloth/Qwen3-4B-GGUF" +TEST_FILE = "Qwen3-4B-UD-IQ1_S.gguf" + +# Stop each download once this many bytes have been observed. Large enough to +# reveal cadence on the xet path, small enough to stay cheap. +PREFIX_BUDGET_BYTES = 400 * 1024 * 1024 +# Hard wall-clock cap per path, so a stalled transfer can't hang the run. +WALL_TIMEOUT_S = 600.0 + +# Thresholds for "real-time" over the sampled prefix. The HTTP path (200KB chunks, +# see _shrink_hf_download_chunk_size) is the known-good baseline and easily clears +# these; the xet path is what we're judging. +MIN_UPDATES = 20 +MAX_JUMP_PCT = 10.0 + +_SAMPLE_PREFIX = "S\t" + + +@dataclass +class Metrics: + path: str + samples: list[tuple[int, int, float]] = field(default_factory=list) # (bytes, total, ts) + error: str | None = None + + @property + def n_updates(self) -> int: + """Distinct forward progress updates (a sample whose byte count grew).""" + count = 0 + prev = -1 + for done, _total, _ts in self.samples: + if done > prev: + count += 1 + prev = done + return count + + @property + def total_size(self) -> int: + for _done, total, _ts in self.samples: + if total > 0: + return total + return 0 + + @property + def max_jump_pct(self) -> float: + """Largest single-update byte jump as a percent of the full file size.""" + total = self.total_size + if total <= 0 or len(self.samples) < 2: + return 100.0 + prev = self.samples[0][0] + worst = 0 + for done, _total, _ts in self.samples[1:]: + worst = max(worst, done - prev) + prev = done + return worst * 100.0 / total + + @property + def gaps(self) -> list[float]: + return [b[2] - a[2] for a, b in zip(self.samples, self.samples[1:], strict=False)] + + @property + def max_gap_s(self) -> float: + gaps = self.gaps + return max(gaps) if gaps else 0.0 + + @property + def median_gap_s(self) -> float: + gaps = self.gaps + return statistics.median(gaps) if gaps else 0.0 + + @property + def monotonic(self) -> bool: + prev = -1 + for done, _total, _ts in self.samples: + if done < prev: + return False + prev = done + return True + + @property + def is_smooth(self) -> bool: + return ( + self.error is None + and self.n_updates >= MIN_UPDATES + and self.max_jump_pct <= MAX_JUMP_PCT + and self.monotonic + ) + + +def _child_download(models_dir: str) -> int: + """Child entry point: download into ``models_dir``, stream progress, abort at budget. + + Runs in a fresh process so the parent's ``HF_HUB_DISABLE_XET`` env is honored. + """ + import lilbee # noqa: F401 # sets HF env from the inherited environment + from lilbee.catalog import CatalogModel, download_model + from lilbee.core.config.model import cfg + from lilbee.runtime.cancellation import TaskCancelledError + + cfg.models_dir = Path(models_dir) + cfg.models_dir.mkdir(parents=True, exist_ok=True) + + entry = CatalogModel( + hf_repo=TEST_REPO, + gguf_filename=TEST_FILE, + size_gb=1.0, + min_ram_gb=2.0, + description="xet granularity probe", + featured=False, + downloads=0, + task="chat", + ) + + def on_progress(downloaded: int, total: int) -> None: + sys.stdout.write(f"{_SAMPLE_PREFIX}{downloaded}\t{total}\t{time.monotonic()}\n") + sys.stdout.flush() + if downloaded >= PREFIX_BUDGET_BYTES: + # Best-effort clean stop (download.py re-raises TaskCancelledError). + # The parent also hard-kills at budget in case xet swallows this. + raise TaskCancelledError + + try: + download_model(entry, on_progress=on_progress) + except TaskCancelledError: + pass + except Exception as exc: + sys.stdout.write(f"ERR\t{type(exc).__name__}: {exc}\n") + sys.stdout.flush() + return 1 + return 0 + + +def _measure(path: str, base_dir: Path) -> Metrics: + """Run one transfer path in a subprocess, killing it once the prefix is sampled.""" + assert path in ("xet", "http") + models_dir = base_dir / "models" + hf_home = base_dir / "hf_home" + models_dir.mkdir(parents=True, exist_ok=True) + hf_home.mkdir(parents=True, exist_ok=True) + + env = os.environ.copy() + env["HF_HUB_DISABLE_XET"] = "0" if path == "xet" else "1" + env["HF_HOME"] = str(hf_home) # isolates the xet chunk-cache for a cold download + env["HF_HUB_DISABLE_PROGRESS_BARS"] = "1" + + proc = subprocess.Popen( + [sys.executable, __file__, "--child", str(models_dir)], + env=env, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + text=True, + bufsize=1, + start_new_session=True, + ) + metrics = Metrics(path=path) + deadline = time.monotonic() + WALL_TIMEOUT_S + try: + assert proc.stdout is not None + for line in proc.stdout: + if time.monotonic() > deadline: + metrics.error = metrics.error or "wall-clock timeout" + break + if line.startswith("ERR\t"): + metrics.error = line[4:].strip() + break + if not line.startswith(_SAMPLE_PREFIX): + continue + try: + done_s, total_s, ts_s = line[len(_SAMPLE_PREFIX) :].split("\t") + metrics.samples.append((int(done_s), int(total_s), float(ts_s))) + except ValueError: + continue + if metrics.samples[-1][0] >= PREFIX_BUDGET_BYTES: + break + finally: + _kill_group(proc) + return metrics + + +def _kill_group(proc: subprocess.Popen[str]) -> None: + if proc.poll() is None: + try: + os.killpg(os.getpgid(proc.pid), signal.SIGKILL) + except (ProcessLookupError, PermissionError): + proc.kill() + with contextlib.suppress(subprocess.TimeoutExpired): + proc.wait(timeout=10) + + +def run_matrix(base_dir: Path) -> tuple[Metrics, Metrics]: + """Measure both transfer paths under isolated, cold caches.""" + http = _measure("http", base_dir / "http") + xet = _measure("xet", base_dir / "xet") + return http, xet + + +def _row(m: Metrics) -> str: + if m.error and not m.samples: + return f" {m.path:<5} | ERROR: {m.error}" + verdict = "PASS" if m.is_smooth else "FAIL" + return ( + f" {m.path:<5} | {verdict} | updates={m.n_updates:>5} " + f"max_jump={m.max_jump_pct:6.2f}% max_gap={m.max_gap_s:6.2f}s " + f"median_gap={m.median_gap_s:6.3f}s monotonic={m.monotonic}" + ) + + +def format_matrix(http: Metrics, xet: Metrics) -> str: + total_mb = (http.total_size or xet.total_size) / (1024 * 1024) + lines = [ + "", + "Real-time download-progress QA matrix", + f" model: {TEST_REPO}/{TEST_FILE} (~{total_mb:.0f} MB total, " + f"sampled prefix ~{PREFIX_BUDGET_BYTES // (1024 * 1024)} MB)", + f" bar: >= {MIN_UPDATES} updates, max single jump <= {MAX_JUMP_PCT:.0f}% of file", + _row(http), + _row(xet), + ] + if xet.error is None and xet.samples: + if xet.is_smooth: + lines.append(" VERDICT: xet now delivers real-time progress.") + lines.append( + " -> RECOMMEND dropping HF_HUB_DISABLE_XET=1 (re-check the chunk shrink)." + ) + else: + lines.append(" VERDICT: xet progress is still coarse; keep the HTTP bypass.") + lines.append("") + return "\n".join(lines) + + +def test_progress_granularity_matrix(tmp_path: Path) -> None: + """Run the xet-vs-HTTP matrix; guard the HTTP baseline, report the xet verdict.""" + http, xet = run_matrix(tmp_path) + print(format_matrix(http, xet)) + + if http.error and not http.samples: + pytest.skip(f"HTTP baseline unavailable (network?): {http.error}") + + # The HTTP path is lilbee's shipped default: it must stay smooth (regression guard). + assert http.is_smooth, f"HTTP baseline regressed: {_row(http)}" + + # The xet path is under investigation and disabled by default. We only require + # that it produces progress at all; smoothness is reported, not asserted. + if xet.error and not xet.samples: + pytest.skip(f"xet path unavailable (network?): {xet.error}") + assert xet.n_updates >= 1, "xet path produced no progress samples" + + +def _main() -> int: + with tempfile.TemporaryDirectory(prefix="lilbee-xet-qa-") as tmp: + http, xet = run_matrix(Path(tmp)) + print(format_matrix(http, xet)) + return 0 + + +if __name__ == "__main__": + if len(sys.argv) >= 3 and sys.argv[1] == "--child": + sys.exit(_child_download(sys.argv[2])) + sys.exit(_main()) diff --git a/tests/server/test_chat_compaction.py b/tests/server/test_chat_compaction.py index 0f7c636f6..97e48723f 100644 --- a/tests/server/test_chat_compaction.py +++ b/tests/server/test_chat_compaction.py @@ -305,9 +305,11 @@ async def test_other_requests_are_served_while_retrieval_runs( released = False def _slow_context_signalling(question, top_k=0, history=None, chunk_type=None): + from lilbee.retrieval.query.searcher import RagContext + loop.call_soon_threadsafe(entered.set) time.sleep(0.3) # stands in for embed + search + rerank - return ([], [{"role": "user", "content": "q"}]) + return RagContext([], [{"role": "user", "content": "q"}]) mock_svc.searcher.build_rag_context.side_effect = _slow_context_signalling loop = asyncio.get_running_loop() diff --git a/tests/server/test_chat_stream_reasoning.py b/tests/server/test_chat_stream_reasoning.py index 42f85fa2f..bd05224f1 100644 --- a/tests/server/test_chat_stream_reasoning.py +++ b/tests/server/test_chat_stream_reasoning.py @@ -3,7 +3,6 @@ from __future__ import annotations from collections.abc import AsyncIterator -from typing import Any import pytest @@ -41,8 +40,10 @@ async def _gen(): return _gen() -def _rag_return() -> tuple[list[Any], list[dict[str, str]]]: - return ( +def _rag_return(): + from lilbee.retrieval.query.searcher import RagContext + + return RagContext( [], [ {"role": "system", "content": "ctx"}, diff --git a/tests/server/test_handlers.py b/tests/server/test_handlers.py index a3c8d29f1..a57bb88f1 100644 --- a/tests/server/test_handlers.py +++ b/tests/server/test_handlers.py @@ -4,10 +4,10 @@ import contextlib from pathlib import Path from unittest import mock -from unittest.mock import Mock import pytest from litestar.testing import AsyncTestClient +from xberg import Metadata from lilbee.app.services import set_services from lilbee.core.config import cfg @@ -75,21 +75,27 @@ def reset_ingest_locks(): get_services().ingest_lock_registry.reset() -def _make_kreuzberg_result(text: str = "Some extracted text. " * 20, num_chunks: int = 1): +def _make_xberg_result(text: str = "Some extracted text. " * 20, num_chunks: int = 1): chunks = [] for i in range(num_chunks): chunk_text = text[i * len(text) // num_chunks : (i + 1) * len(text) // num_chunks] chunk = mock.MagicMock() chunk.content = chunk_text - chunk.metadata = {"chunk_index": i} + chunk.metadata = mock.MagicMock(chunk_index=i, first_page=None, last_page=None) chunks.append(chunk) result = mock.MagicMock() result.chunks = chunks result.content = text + result.pages = [] + result.metadata = Metadata() return result -@mock.patch("kreuzberg.extract_file_sync", new_callable=Mock, return_value=_make_kreuzberg_result()) +@mock.patch( + "lilbee.data.xberg_extract.aextract_document", + new_callable=mock.AsyncMock, + return_value=_make_xberg_result(), +) class TestAddEndpoint: async def test_add_single_file(self, mock_extract_file, isolated_env, tmp_path): """POST /api/add with a valid file streams SSE events and adds it.""" @@ -348,9 +354,9 @@ async def test_hundreds_of_paths_accepted(self, isolated_env, tmp_path): paths = [f"/fake/file_{i}.txt" for i in range(285)] with mock.patch( - "kreuzberg.extract_file_sync", - new_callable=Mock, - return_value=_make_kreuzberg_result(), + "lilbee.data.xberg_extract.aextract_document", + new_callable=mock.AsyncMock, + return_value=_make_xberg_result(), ): async with AsyncTestClient(create_app()) as client: resp = await client.post("/api/add", json={"paths": paths}, headers=_auth_headers()) @@ -618,9 +624,9 @@ async def test_partial_contention_partitions_paths(self, isolated_env, tmp_path) assert lock is not None try: with mock.patch( - "kreuzberg.extract_file_sync", - new_callable=Mock, - return_value=_make_kreuzberg_result(), + "lilbee.data.xberg_extract.aextract_document", + new_callable=mock.AsyncMock, + return_value=_make_xberg_result(), ): events = await self._collect(add_files_stream([str(held), str(free)])) finally: @@ -648,9 +654,9 @@ async def test_concurrent_different_sources_run_in_parallel(self, isolated_env, async def _run(path: Path): text = "" with mock.patch( - "kreuzberg.extract_file_sync", - new_callable=Mock, - return_value=_make_kreuzberg_result(), + "lilbee.data.xberg_extract.aextract_document", + new_callable=mock.AsyncMock, + return_value=_make_xberg_result(), ): async for frame in add_files_stream([str(path)]): text += frame @@ -733,9 +739,9 @@ async def test_new_file_triggers_cleanup(self, isolated_env, tmp_path, mock_svc) store.get_sources.return_value = [] with mock.patch( - "kreuzberg.extract_file_sync", - new_callable=Mock, - return_value=_make_kreuzberg_result(), + "lilbee.data.xberg_extract.aextract_document", + new_callable=mock.AsyncMock, + return_value=_make_xberg_result(), ): await sync(quiet=True) @@ -757,9 +763,9 @@ async def test_retry_after_orphaned_chunks_cleans_up(self, isolated_env, tmp_pat store.get_sources.return_value = [] with mock.patch( - "kreuzberg.extract_file_sync", - new_callable=Mock, - return_value=_make_kreuzberg_result(), + "lilbee.data.xberg_extract.aextract_document", + new_callable=mock.AsyncMock, + return_value=_make_xberg_result(), ): await sync(quiet=True) diff --git a/tests/server/test_rag_uses_chat_dispatch.py b/tests/server/test_rag_uses_chat_dispatch.py index 6b90d6b63..c3740aa95 100644 --- a/tests/server/test_rag_uses_chat_dispatch.py +++ b/tests/server/test_rag_uses_chat_dispatch.py @@ -12,6 +12,7 @@ from lilbee.app.services import set_services from lilbee.core.config import cfg from lilbee.providers.base import ChatResult, FinishReason +from lilbee.retrieval.query.searcher import RagContext from lilbee.server import auth as _auth_mod from lilbee.server.chat_dispatch.canonical import ( CanonicalChatRequest, @@ -53,7 +54,7 @@ def services_with_chat_dispatch(): services = make_mock_services(provider=provider) services.registry.list_installed = MagicMock(return_value=[_installed_manifest(cfg.chat_model)]) services.searcher.build_rag_context = MagicMock( - return_value=( + return_value=RagContext( [], [ {"role": "system", "content": "ctx"}, diff --git a/tests/test_api.py b/tests/test_api.py index 9249ee719..eb5322d09 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -59,6 +59,16 @@ def test_create_with_documents_dir(self, tmp_path): assert bee.config.data_dir.exists() assert "myproject" in str(bee.config.data_root) + def test_documents_dir_tilde_expands_instead_of_literal_dir(self, tmp_path, monkeypatch): + """A "~/vault" root must reach the home directory, not a literal ./~ tree.""" + from lilbee import Lilbee + + # POSIX expanduser reads HOME; the Windows one reads USERPROFILE. + monkeypatch.setenv("HOME", str(tmp_path)) + monkeypatch.setenv("USERPROFILE", str(tmp_path)) + bee = Lilbee("~/tilde_vault") + assert bee.config.data_root == (tmp_path / "tilde_vault").resolve() + def test_create_with_config(self, tmp_path): from lilbee import Lilbee diff --git a/tests/test_chunker.py b/tests/test_chunker.py index 7d217512e..710ca0d26 100644 --- a/tests/test_chunker.py +++ b/tests/test_chunker.py @@ -7,6 +7,7 @@ import tempfile from dataclasses import dataclass, field from pathlib import Path +from types import SimpleNamespace import pytest @@ -107,7 +108,7 @@ def test_use_semantic_false_bypasses_semantic(self, monkeypatch): class TestBuildChunkingConfig: def test_semantic_enabled_uses_semantic_chunker_with_embedding(self, monkeypatch): - """Semantic path requires an EmbeddingConfig or kreuzberg silently falls back.""" + """Semantic path requires an EmbeddingConfig or xberg silently falls back.""" from lilbee.core.config import cfg from lilbee.data.chunk import build_chunking_config @@ -119,14 +120,19 @@ def test_semantic_enabled_uses_semantic_chunker_with_embedding(self, monkeypatch assert result.embedding is not None def test_semantic_respects_max_chars_when_embedding_present(self, monkeypatch): - """With an embedding attached kreuzberg honors max_chars on the semantic path.""" + """With an embedding attached xberg honors max_characters on the semantic path.""" from lilbee.core.config import cfg from lilbee.data.chunk import CHARS_PER_TOKEN, build_chunking_config monkeypatch.setattr(cfg, "semantic_chunking", True) monkeypatch.setattr(cfg, "chunk_size", 512) result = build_chunking_config() - assert result.max_chars == 512 * CHARS_PER_TOKEN + assert result.max_characters == 512 * CHARS_PER_TOKEN + assert result.embedding is not None + # Boundary detection routes to lilbee's embedder via xberg's plugin backend + # (registered in app.services.sync_embedding_backend), not the ONNX preset. + assert result.embedding.model.type == "plugin" + assert str(result.embedding.model) == '{"type":"plugin","name":"lilbee"}' def test_char_budget_when_disabled(self, monkeypatch): from lilbee.core.config import cfg @@ -137,8 +143,8 @@ def test_char_budget_when_disabled(self, monkeypatch): monkeypatch.setattr(cfg, "chunk_overlap", 100) result = build_chunking_config() assert result.chunker_type == "text" - assert result.max_chars == 512 * CHARS_PER_TOKEN - assert result.max_overlap == 100 * CHARS_PER_TOKEN + assert result.max_characters == 512 * CHARS_PER_TOKEN + assert result.overlap == 100 * CHARS_PER_TOKEN assert result.embedding is None def test_disabled_does_not_attach_embedding(self, monkeypatch): @@ -150,28 +156,6 @@ def test_disabled_does_not_attach_embedding(self, monkeypatch): result = build_chunking_config() assert result.embedding is None - def test_download_progress_off_when_globally_suppressed(self, monkeypatch): - """quiet/JSON modes suppress HF progress bars; the embedding config mirrors that.""" - from lilbee.data.chunk import _show_download_progress - - monkeypatch.setenv("HF_HUB_DISABLE_PROGRESS_BARS", "1") - assert _show_download_progress() is False - - def test_download_progress_on_when_not_suppressed(self, monkeypatch): - from lilbee.data.chunk import _show_download_progress - - monkeypatch.setenv("HF_HUB_DISABLE_PROGRESS_BARS", "0") - assert _show_download_progress() is True - - def test_download_progress_default_off_when_unset(self, monkeypatch): - """lilbee defaults the env var on at import, so the bar stays off by default.""" - from lilbee.data.chunk import _show_download_progress - - monkeypatch.delenv("HF_HUB_DISABLE_PROGRESS_BARS", raising=False) - # Unset env reads as "not disabled" -> progress allowed; lilbee's __init__ - # sets it to "1" in real runs, so this documents the bare-helper contract. - assert _show_download_progress() is True - def test_heading_path_shares_char_budget(self, monkeypatch): """The heading-aware path uses the same token->char budget as the default path.""" from lilbee.core.config import cfg @@ -184,6 +168,88 @@ def test_heading_path_shares_char_budget(self, monkeypatch): assert max_overlap == 40 * CHARS_PER_TOKEN +_TOKENIZER_SIZING = '{"type":"tokenizer","model":"lilbee"}' + + +class TestTokenSizing: + """cfg.token_sizing routes the plain and heading chunkers through lilbee's + registered tokenizer backend, so chunk_size is a real token budget.""" + + def test_size_params_off_is_char_budget_without_sizing(self, monkeypatch): + from lilbee.core.config import cfg + from lilbee.data.chunk import CHARS_PER_TOKEN, _size_params + + monkeypatch.setattr(cfg, "token_sizing", False) + monkeypatch.setattr(cfg, "chunk_size", 512) + monkeypatch.setattr(cfg, "chunk_overlap", 100) + max_size, overlap, sizing = _size_params() + assert (max_size, overlap) == (512 * CHARS_PER_TOKEN, 100 * CHARS_PER_TOKEN) + assert sizing is None + + def test_size_params_on_is_raw_token_budget_with_sizing(self, monkeypatch): + from lilbee.core.config import cfg + from lilbee.data.chunk import _size_params + + monkeypatch.setattr(cfg, "token_sizing", True) + monkeypatch.setattr(cfg, "chunk_size", 512) + monkeypatch.setattr(cfg, "chunk_overlap", 100) + max_size, overlap, sizing = _size_params() + assert (max_size, overlap) == (512, 100) + assert str(sizing) == _TOKENIZER_SIZING + + def test_overlap_capped_at_half_the_token_budget(self, monkeypatch): + from lilbee.core.config import cfg + from lilbee.data.chunk import _size_params + + monkeypatch.setattr(cfg, "token_sizing", True) + monkeypatch.setattr(cfg, "chunk_size", 100) + monkeypatch.setattr(cfg, "chunk_overlap", 90) + _max_size, overlap, _sizing = _size_params() + assert overlap == 50 + + def test_default_chunker_attaches_tokenizer_sizing(self, monkeypatch): + from lilbee.core.config import cfg + from lilbee.data.chunk import build_chunking_config + + monkeypatch.setattr(cfg, "semantic_chunking", False) + monkeypatch.setattr(cfg, "token_sizing", True) + monkeypatch.setattr(cfg, "chunk_size", 512) + result = build_chunking_config() + assert result.max_characters == 512 + assert str(result.sizing) == _TOKENIZER_SIZING + + def test_semantic_chunker_ignores_token_sizing(self, monkeypatch): + """The semantic chunker sizes by characters, so it keeps the char budget even + with token_sizing on (xberg's ChunkSizing has no effect on that path).""" + from lilbee.core.config import cfg + from lilbee.data.chunk import CHARS_PER_TOKEN, build_chunking_config + + monkeypatch.setattr(cfg, "semantic_chunking", True) + monkeypatch.setattr(cfg, "token_sizing", True) + monkeypatch.setattr(cfg, "chunk_size", 512) + result = build_chunking_config() + assert result.chunker_type == "semantic" + assert result.max_characters == 512 * CHARS_PER_TOKEN + + def test_heading_path_attaches_tokenizer_sizing(self, monkeypatch): + from lilbee.core.config import cfg + from lilbee.data.chunk import chunk_text + + monkeypatch.setattr(cfg, "token_sizing", True) + captured = {} + + def fake_extract_document(data, mime_type, *, config): + chunking = config["chunking"] + captured["sizing"] = str(chunking.sizing) + captured["chunker_type"] = chunking.chunker_type + return SimpleNamespace(chunks=[]) + + monkeypatch.setattr("lilbee.data.xberg_extract.extract_document", fake_extract_document) + chunk_text("# Heading\n\nbody text", mime_type="text/markdown", heading_context=True) + assert captured["sizing"] == _TOKENIZER_SIZING + assert captured["chunker_type"] == "markdown" + + class TestMarkdownChunking: def test_splits_on_headings(self): md = ( @@ -332,7 +398,10 @@ def test_ensure_language_false_triggers_fallback(self): try: with patch("lilbee.data.code_chunker._ensure_language", return_value=False): chunks = chunk_code(path) - assert isinstance(chunks, list) + # Truthiness, not isinstance: a regression returning [] would drop + # the file's content from the index and still be a list. + assert chunks + assert "x = 1" in "\n".join(c.chunk for c in chunks) finally: path.unlink() @@ -355,7 +424,8 @@ def test_process_exception_triggers_fallback(self): patch("lilbee.data.code_chunker.process", side_effect=RuntimeError("parse fail")), ): chunks = chunk_code(path) - assert isinstance(chunks, list) + assert chunks + assert "x = 1" in "\n".join(c.chunk for c in chunks) finally: path.unlink() @@ -377,8 +447,8 @@ def test_empty_chunks_triggers_fallback(self): patch("lilbee.data.code_chunker.process", return_value=_FakeResult([])), ): chunks = chunk_code(path) - assert isinstance(chunks, list) assert chunks # fell back to non-empty text chunks + assert "x = 1" in "\n".join(c.chunk for c in chunks) finally: path.unlink() @@ -462,6 +532,36 @@ def test_chunk_code_emits_chunks_from_result(self): assert first.line_end == 2 assert first.chunk_index == 0 + def test_chunk_code_budget_is_bytes_not_tokens(self): + """tree-sitter's chunk_max_size is documented in BYTES while + cfg.chunk_size is a token budget, so the parser path must convert the + same way the text path does -- otherwise code files split ~4x smaller + than prose, and the parser and fallback paths disagree for one file.""" + from unittest.mock import patch + + from lilbee.core.config import cfg + from lilbee.data.chunk import CHARS_PER_TOKEN + from lilbee.data.code_chunker import chunk_code + + result = _FakeResult([_FakeTSChunk("x = 1\n", start_line=0, end_line=1, symbols=[])]) + with tempfile.NamedTemporaryFile(suffix=".py", mode="w", delete=False) as f: + f.write("x = 1\n") + f.flush() + path = Path(f.name) + + try: + with ( + patch("lilbee.data.code_chunker._ensure_language", return_value=True), + patch("lilbee.data.code_chunker.process", return_value=result) as proc, + patch("lilbee.data.code_chunker.ProcessConfig") as config_cls, + ): + chunk_code(path, source_name="m.py") + finally: + path.unlink() + + proc.assert_called_once() + assert config_cls.call_args.kwargs["chunk_max_size"] == cfg.chunk_size * CHARS_PER_TOKEN + def test_chunk_header_omits_symbols_and_never_says_none(self): """A symbol-free (anonymous) chunk omits the symbol segment entirely rather than rendering the literal string 'None' (bb-ziks.62).""" @@ -593,7 +693,7 @@ def test_real_parser_line_range_spans_no_trailing_newline_file(self): class TestHeadingContextNoDuplicate: def test_heading_context_no_duplicate(self): - """kreuzberg >= 4.8.5 should not duplicate headings with prepend_heading_context.""" + """xberg >= 4.8.5 should not duplicate headings with prepend_heading_context.""" md = "# Title\n\n" + "Word " * 500 + "\n\n## Section\n\n" + "More " * 500 chunks = chunk_text(md, mime_type="text/markdown", heading_context=True) for c in chunks: @@ -611,5 +711,5 @@ def test_returns_empty_when_no_chunks(self): mock_result = MagicMock() mock_result.chunks = [] - with patch("kreuzberg.extract_bytes_sync", return_value=mock_result): + with patch("lilbee.data.xberg_extract.extract_document", return_value=mock_result): assert chunk_text("some text") == [] diff --git a/tests/test_citation.py b/tests/test_citation.py index bb9131bb4..0f5bc3884 100644 --- a/tests/test_citation.py +++ b/tests/test_citation.py @@ -201,9 +201,12 @@ def test_whitespace_normalized_for_matching(self): rec = _citation_record(excerpt="gradual\n typing") assert verify_citation(rec, "supports gradual typing here") == CitationStatus.VALID - def test_case_insensitive_matching(self): + def test_case_sensitive_matching(self): + # Verification is case-sensitive (xberg.verify_excerpt, adopted via bb-548): + # a case mismatch fails, an exact-case (whitespace-normalized) match passes. rec = _citation_record(excerpt="Gradual Typing") - assert verify_citation(rec, "gradual typing module") == CitationStatus.VALID + assert verify_citation(rec, "gradual typing module") == CitationStatus.EXCERPT_MISSING + assert verify_citation(rec, "uses Gradual Typing here") == CitationStatus.VALID class TestFindUnmarkedClaims: diff --git a/tests/test_cli.py b/tests/test_cli.py index 1fb491bee..43e1a850d 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -3105,13 +3105,15 @@ def test_status_with_pages(self, mock_svc, isolated_env): cfg.wiki = True cfg.wiki_dir = "wiki" (isolated_env / "wiki" / "summaries").mkdir(parents=True) - (isolated_env / "wiki" / "summaries" / "a.md").write_text("content") + # An unmarked claim (a sentence with no citation) -> lint flags a warning. + (isolated_env / "wiki" / "summaries" / "a.md").write_text("Python is a typed language.\n") (isolated_env / "wiki" / "drafts").mkdir(parents=True) (isolated_env / "wiki" / "drafts" / "b.md").write_text("content") mock_svc.store.get_citations_for_wiki.return_value = [] result = runner.invoke(app, ["wiki", "status"]) assert result.exit_code == 0 assert "1" in result.output # summaries count + assert "warning(s)" in result.output # the unmarked claim is linted def test_status_json_output(self, mock_svc, isolated_env): cfg.wiki = True diff --git a/tests/test_clustering.py b/tests/test_clustering.py index 87f8f7b2a..bea6a8857 100644 --- a/tests/test_clustering.py +++ b/tests/test_clustering.py @@ -16,14 +16,13 @@ ) from lilbee.retrieval.clustering_embedding import EmbeddingClusterer from lilbee.retrieval.clustering_embedding.helpers import ( - _tokenize_for_tf, auto_k, communities_by_label, label_propagation, mutual_knn, normalize_rows, ) -from lilbee.retrieval.clustering_embedding.types import ChunkRecord +from lilbee.retrieval.clustering_embedding.types import ClusterChunk, _tokenize_for_tf @pytest.fixture(autouse=True) @@ -35,13 +34,8 @@ def isolated_cfg(): setattr(cfg, name, getattr(snapshot, name)) -def _record(source: str, chunk_index: int = 0, text: str = "") -> ChunkRecord: - return ChunkRecord( - source=source, - chunk_index=chunk_index, - text=text, - tokens=_tokenize_for_tf(text), - ) +def _record(source: str, chunk_index: int = 0, text: str = "") -> ClusterChunk: + return ClusterChunk(source=source, chunk_index=chunk_index, text=text) def _row( @@ -58,12 +52,18 @@ def _row( } -def _mock_table(rows: list[dict[str, object]]) -> MagicMock: +def _mock_table(*row_batches: list[dict[str, object]]) -> MagicMock: + """A chunks table serving *row_batches* through the batched Arrow scan.""" table = MagicMock() arrow = MagicMock() - arrow.to_pylist.return_value = rows + batches = [] + for rows in row_batches: + batch = MagicMock() + batch.to_pylist.return_value = rows + batches.append(batch) + arrow.to_batches.return_value = batches table.to_arrow.return_value = arrow - table.count_rows.return_value = len(rows) + table.count_rows.return_value = sum(len(rows) for rows in row_batches) return table @@ -220,13 +220,41 @@ def test_groups_by_label(self): assert communities[2] == [5] -class TestChunkRecord: +class TestTokenizeForTf: + def test_lowercases_and_strips_non_alphanumerics(self): + assert _tokenize_for_tf("Python, TYPING; rules!") == ["python", "typing", "rules"] + + def test_drops_tokens_below_the_minimum_length(self): + """Short tokens are articles and single letters: noise for TF-IDF. + Three characters keeps useful acronyms.""" + assert _tokenize_for_tf("a an the sql api") == ["the", "sql", "api"] + + def test_keeps_common_words_for_idf_to_handle(self): + """No stopword list by design: a term in nearly every chunk gets an + IDF at or below zero and is filtered by the scoring, not a list.""" + assert "and" in _tokenize_for_tf("indexing and retrieval") + + +class TestClusterChunk: def test_holds_cached_tokens(self): record = _record("doc.md", 0, "python typing") assert record.source == "doc.md" assert record.chunk_index == 0 assert record.tokens == ["python", "typing"] + def test_tokens_derive_from_text_without_the_caller_supplying_them(self): + """The invariant is structural, not caller discipline: TF-IDF labeling + silently falls back to the cluster id when a record carries no tokens, + so a construction site that forgets to tokenize must not be possible.""" + record = ClusterChunk(source="doc.md", chunk_index=0, text="python typing rules") + assert record.tokens == ["python", "typing", "rules"] + + def test_explicit_tokens_are_respected(self): + record = ClusterChunk( + source="doc.md", chunk_index=0, text="ignored text", tokens=["preset"] + ) + assert record.tokens == ["preset"] + class TestEmbeddingClustererAvailable: def test_available_when_table_has_rows(self): @@ -305,6 +333,34 @@ def test_drops_zero_norm_rows(self): store = _store_with_rows(rows) assert EmbeddingClusterer(cfg, store).get_clusters() == [] + def test_scans_in_bounded_row_batches(self): + """The chunks table is consumed batch by batch, never materialized as + one full to_pylist -- peak Python-object memory must stay bounded by + the batch size regardless of corpus size.""" + from lilbee.retrieval.clustering_embedding.helpers import ( + _SCAN_BATCH_ROWS, + _load_chunk_records, + ) + + first = [_row("b.md", [0.0, 1.0], chunk="beta", chunk_index=1)] + second = [ + _row("a.md", [1.0, 0.0], chunk="alpha", chunk_index=0), + {"source": None, "vector": [9.0, 9.0], "chunk": "bad", "chunk_index": 0}, + ] + table = _mock_table(first, second) + store = MagicMock() + store.open_table.return_value = table + + records, matrix = _load_chunk_records(store) + table.to_arrow.return_value.to_batches.assert_called_once_with( + max_chunksize=_SCAN_BATCH_ROWS + ) + # Records from both batches, invalid row skipped, sorted with the + # matrix rows kept aligned across the batch boundary. + assert [(r.source, r.chunk_index) for r in records] == [("a.md", 0), ("b.md", 1)] + assert matrix.tolist() == [[1.0, 0.0], [0.0, 1.0]] + assert matrix.dtype == np.float32 + class TestEmbeddingClustererGetClusters: def test_empty_store_returns_no_clusters(self): @@ -516,11 +572,28 @@ def test_concepts_choice_falls_back_when_unavailable( cfg.wiki_clusterer = ClustererBackend.CONCEPTS monkeypatch.setattr(ConceptGraphClusterer, "available", lambda self: False) + clusterer = Clusterer(cfg, MagicMock()) with caplog.at_level(logging.WARNING, logger="lilbee.retrieval.clustering"): - clusterer = Clusterer(cfg, MagicMock()) - assert isinstance(clusterer.backend, EmbeddingClusterer) + backend = clusterer.backend + assert isinstance(backend, EmbeddingClusterer) assert any("falling back" in rec.message.lower() for rec in caplog.records) + def test_backend_selection_is_live_not_pinned_at_construction( + self, monkeypatch: pytest.MonkeyPatch + ): + """The services container caches the facade for the process lifetime, + so a concept graph built later in the same process must be picked up + without a restart.""" + from lilbee.retrieval.concepts import ConceptGraphClusterer + + cfg.wiki_clusterer = ClustererBackend.CONCEPTS + graph_built = {"value": False} + monkeypatch.setattr(ConceptGraphClusterer, "available", lambda self: graph_built["value"]) + clusterer = Clusterer(cfg, MagicMock()) + assert isinstance(clusterer.backend, EmbeddingClusterer) + graph_built["value"] = True + assert isinstance(clusterer.backend, ConceptGraphClusterer) + def test_facade_forwards_available_and_get_clusters(self, monkeypatch: pytest.MonkeyPatch): # Swap _select_backend out for a stub backend so the test only # exercises the forwarding logic without poking private attrs. diff --git a/tests/test_compaction.py b/tests/test_compaction.py index 9db44b6d2..6ab3f1191 100644 --- a/tests/test_compaction.py +++ b/tests/test_compaction.py @@ -3,9 +3,11 @@ from __future__ import annotations from lilbee.retrieval.query.compaction import ( + COMPACT_KEEP_RECENT, COMPACT_MAX_TOKENS, SUMMARY_REQUEST, batch_overflow, + foldable, overflow, prompt_history, summary_cap, @@ -89,7 +91,7 @@ def test_batches_each_fit_the_current_model_window() -> None: """ dropped = _msgs(200) # ~20k tokens, i.e. a large conversation ctx = 2048 - batches = batch_overflow(dropped, "", ctx_target=ctx) + batches = batch_overflow(dropped, ctx_target=ctx) assert len(batches) > 1, "a 20k backlog must not be summarized in one 2k call" # `estimate < ctx` is the invariant that shipped a live failure: a batch # estimated at 1728 tokens reached a 2048-token server as ~2666 real tokens @@ -108,25 +110,17 @@ def test_batches_each_fit_the_current_model_window() -> None: assert sum(len(b) for b in batches) == len(dropped) -def test_batches_leave_room_for_the_previous_summary() -> None: - """The previous notes ride in every batch prompt, so they must be budgeted.""" - dropped = _msgs(60) - lean = batch_overflow(dropped, "", ctx_target=4096) - fat = batch_overflow(dropped, "s" * 6000, ctx_target=4096) - assert len(fat) >= len(lean), "a long previous summary must shrink the batches" - - def test_a_single_oversized_turn_is_clipped_rather_than_sent_to_fail() -> None: """One turn bigger than the window would fail every call and lose the lot.""" huge = [{"role": "user", "content": "y" * 200_000}] - batches = batch_overflow(huge, "", ctx_target=2048) + batches = batch_overflow(huge, ctx_target=2048) assert len(batches) == 1 assert estimate_tokens(batches[0][0]) < 2048 assert batches[0][0]["content"].endswith("[…clipped]") def test_no_overflow_yields_no_batches() -> None: - assert batch_overflow([], "", ctx_target=2048) == [] + assert batch_overflow([], ctx_target=2048) == [] def test_an_oversized_turn_flushes_the_batch_being_built() -> None: @@ -137,7 +131,7 @@ def test_an_oversized_turn_flushes_the_batch_being_built() -> None: {"role": "user", "content": "y" * 200_000}, # bigger than any batch {"role": "assistant", "content": "small three"}, ] - batches = batch_overflow(dropped, "", ctx_target=2048) + batches = batch_overflow(dropped, ctx_target=2048) # the two small turns batch together, the giant one stands alone (clipped) assert [len(b) for b in batches] == [2, 1, 1] assert batches[0][0]["content"] == "small one" @@ -172,3 +166,42 @@ def test_a_summary_that_cannot_fit_is_dropped_not_stacked() -> None: without = prompt_history(history, "", max_tokens=budget) assert all(m["content"] != SUMMARY_REQUEST for m in with_summary), "the summary was dropped" assert with_summary == without, "and the prompt is exactly what it would be without one" + + +class TestFoldableAlignment: + """The fold boundary must leave the kept window opening on a user turn.""" + + def test_odd_history_keeps_alternation_intact(self): + """An interrupted turn persists an unpaired user message, so a history + can be odd-length. Cutting a fixed count then leaves the kept window + starting on an assistant reply, and the assembled prompt runs + user, assistant, assistant -- the shape strict chat templates reject.""" + history = [ + {"role": "user", "content": "u1"}, + {"role": "assistant", "content": "a1"}, + {"role": "user", "content": "u2"}, + {"role": "assistant", "content": "a2"}, + {"role": "user", "content": "u3"}, + ] + dropped = foldable(history) + kept = history[len(dropped) :] + assert kept, "folding must not consume the whole history" + assert kept[0]["role"] == "user" + + def test_even_history_is_unchanged(self): + history = _msgs(10) + dropped = foldable(history) + assert len(dropped) == len(history) - COMPACT_KEEP_RECENT + assert history[len(dropped)]["role"] == "user" + + def test_short_history_folds_nothing(self): + assert foldable(_msgs(COMPACT_KEEP_RECENT)) == [] + + def test_a_tail_with_no_user_turn_keeps_the_plain_boundary(self): + """Nothing to align to: scanning past the end and folding everything + away would throw the whole conversation out to fix its shape.""" + history = [ + {"role": "assistant", "content": f"a{i}"} for i in range(COMPACT_KEEP_RECENT + 4) + ] + dropped = foldable(history) + assert len(dropped) == len(history) - COMPACT_KEEP_RECENT diff --git a/tests/test_concepts.py b/tests/test_concepts.py index 276aa7243..88f5fd90b 100644 --- a/tests/test_concepts.py +++ b/tests/test_concepts.py @@ -465,7 +465,13 @@ def test_batched_write_rebuilds_same_clusters_as_per_file(self, mock_leiden, moc per_file = [ ConceptGraph(cfg, MagicMock()).build_concept_records([(name, 0)], [concepts]) - for name, concepts in (("a.md", ["python", "rust"]), ("b.md", ["python", "go"])) + for name, concepts in ( + ("a.md", ["python", "rust"]), + ("b.md", ["python", "go"]), + # A chunk without python keeps the pairs above chance (PPMI > 0); + # a concept in every chunk has zero PMI with everything. + ("c.md", ["notes"]), + ) ] mock_leiden.side_effect = lambda edge_rows: ( {c: 0 for row in edge_rows for c in (row["source"], row["target"])}, @@ -489,6 +495,11 @@ def _rebuild(write_units: list[ConceptRecords], lancedb_dir) -> list[dict]: assert len(batched_rows) > 0 +def _cc_row(source: str, chunk_index: int, concept: str) -> dict: + """A chunk_concepts table row as the batched boost query returns it.""" + return {"chunk_source": source, "chunk_index": chunk_index, "concept": concept} + + class TestBoostResults: def test_boost_scoreless_row_is_untouched(self, cg, mock_svc): """A hand-built row without the canonical score has nothing to boost @@ -497,7 +508,7 @@ def test_boost_scoreless_row_is_untouched(self, cg, mock_svc): results = [_make_result(distance=0.5, chunk_index=0)] mock_table = MagicMock() mock_table.search.return_value.where.return_value.to_list.return_value = [ - {"concept": "python"}, + _cc_row("test.pdf", 0, "python"), ] mock_svc.store.open_table.return_value = mock_table boosted = cg.boost_results(results, ["python"]) @@ -512,7 +523,8 @@ def test_boost_canonical_score_bounded(self, cg, mock_svc): ] mock_table = MagicMock() mock_table.search.return_value.where.return_value.to_list.return_value = [ - {"concept": "python"}, + _cc_row("test.pdf", 0, "python"), + _cc_row("test.pdf", 1, "python"), ] mock_svc.store.open_table.return_value = mock_table boosted = cg.boost_results(results, ["python"]) @@ -523,7 +535,7 @@ def test_boost_results_no_overlap(self, cg, mock_svc): results = [_make_result(distance=0.5, chunk_index=0)] mock_table = MagicMock() mock_table.search.return_value.where.return_value.to_list.return_value = [ - {"concept": "java"}, + _cc_row("test.pdf", 0, "java"), ] mock_svc.store.open_table.return_value = mock_table boosted = cg.boost_results(results, ["python"]) @@ -536,16 +548,57 @@ def test_boost_results_returns_unchanged_when_table_missing(self, cg, mock_svc): boosted = cg.boost_results(results, ["python"]) assert boosted == results - def test_boost_results_opens_table_once_for_many_results(self, cg, mock_svc): - """The chunk_concepts table is opened once per call, not once per result (N+1).""" - results = [_make_result(distance=0.5, chunk_index=i) for i in range(4)] + def test_boost_results_single_query_for_many_results(self, cg, mock_svc): + """The whole result set is served by ONE batched chunk_concepts query + (chunk_source IN ... AND chunk_index IN ...), not one query per result: + the table has no scalar index, so each per-result predicate was a full + table scan.""" + results = [ + _make_result(source="a.md", chunk_index=0, score=0.5), + _make_result(source="a.md", chunk_index=3, score=0.5), + _make_result(source="b.md", chunk_index=1, score=0.5), + ] mock_table = MagicMock() mock_table.search.return_value.where.return_value.to_list.return_value = [ - {"concept": "python"} + _cc_row("a.md", 0, "python"), ] mock_svc.store.open_table.return_value = mock_table - cg.boost_results(results, ["python"]) + boosted = cg.boost_results(results, ["python"]) mock_svc.store.open_table.assert_called_once() + assert mock_table.search.call_count == 1 + where_arg = mock_table.search.return_value.where.call_args.args[0] + assert "chunk_source IN (" in where_arg + assert "'a.md'" in where_arg + assert "'b.md'" in where_arg + assert "chunk_index IN (" in where_arg + assert boosted[0].score > 0.5 + + def test_boost_ignores_cross_product_rows_for_unrequested_chunks(self, cg, mock_svc): + """The batched predicate is a source x index cross product; rows for + pairs that were not requested must not leak concepts into any result.""" + results = [ + _make_result(source="a.md", chunk_index=0, score=0.5), + _make_result(source="b.md", chunk_index=1, score=0.5), + ] + mock_table = MagicMock() + # (a.md, 1) matches the cross product but is not a requested chunk. + mock_table.search.return_value.where.return_value.to_list.return_value = [ + _cc_row("a.md", 1, "python"), + ] + mock_svc.store.open_table.return_value = mock_table + boosted = cg.boost_results(results, ["python"]) + assert boosted[0].score == 0.5 + assert boosted[1].score == 0.5 + + def test_boost_results_unchanged_when_batch_query_fails(self, cg, mock_svc): + results = [_make_result(chunk_index=0, score=0.5)] + mock_table = MagicMock() + mock_table.search.return_value.where.return_value.to_list.side_effect = RuntimeError( + "query failed" + ) + mock_svc.store.open_table.return_value = mock_table + boosted = cg.boost_results(results, ["python"]) + assert boosted[0].score == 0.5 def test_boost_results_empty_query_concepts(self, cg): results = [_make_result()] @@ -562,8 +615,8 @@ def test_boost_raises_score_and_leaves_distance_alone(self, cg, mock_svc): results = [_make_result(distance=0.1, chunk_index=0, score=0.4)] mock_table = MagicMock() mock_table.search.return_value.where.return_value.to_list.return_value = [ - {"concept": "python"}, - {"concept": "ml"}, + _cc_row("test.pdf", 0, "python"), + _cc_row("test.pdf", 0, "ml"), ] mock_svc.store.open_table.return_value = mock_table boosted = cg.boost_results(results, ["python", "ml"]) @@ -796,10 +849,60 @@ def test_rebuild_recomputes_corpus_pmi(self, mock_leiden, cg, mock_svc): cg.rebuild_clusters() mock_leiden.assert_called_once_with([{"source": "ml", "target": "python", "weight": 1.0}]) # Nodes are replaced atomically (delete+add under one lock), not a bare add. - mock_svc.store.clear_and_add.assert_called_once() - node_records = mock_svc.store.clear_and_add.call_args.args[2] + from lilbee.core.config import CONCEPT_NODES_TABLE + + rewrites = {call.args[0]: call for call in mock_svc.store.clear_and_add.call_args_list} + node_records = rewrites[CONCEPT_NODES_TABLE].args[2] assert {r["concept"] for r in node_records} == {"python", "ml"} + @patch("lilbee.retrieval.concepts.graph._leiden_partition") + def test_rebuild_rewrites_edges_from_corpus(self, mock_leiden, cg, mock_svc): + """rebuild_clusters replaces the edges table with the corpus PPMI edge + set. Per-file writes only ever append, so without this rewrite the + table grows monotonically and expand_query serves edges for concepts + that left the corpus long ago.""" + from lilbee.core.config import CONCEPT_EDGES_TABLE + + _cc, side = self._chunk_concepts( + { + "chunk_source": ["d.md"] * 6, + "chunk_index": [0, 0, 1, 1, 2, 3], + "concept": ["python", "ml", "python", "ml", "rust", "rust"], + } + ) + mock_svc.store.open_table.side_effect = side + mock_leiden.return_value = ({"python": 0, "ml": 0}, {"python": 1, "ml": 1}) + + cg.rebuild_clusters() + rewrites = {call.args[0]: call for call in mock_svc.store.clear_and_add.call_args_list} + assert CONCEPT_EDGES_TABLE in rewrites + edge_rows = rewrites[CONCEPT_EDGES_TABLE].args[2] + assert edge_rows == [{"source": "ml", "target": "python", "weight": 1.0}] + + @patch("lilbee.retrieval.concepts.graph._leiden_partition") + def test_reingest_then_rebuild_leaves_no_stale_or_duplicate_edges(self, mock_leiden, mock_svc): + """End-state: re-ingesting a file appends its edges again, and the + following rebuild collapses the table back to the corpus edge set.""" + from lilbee.core.config import CONCEPT_EDGES_TABLE + from lilbee.data.store import Store + + mock_leiden.side_effect = lambda edge_rows: ( + {c: 0 for row in edge_rows for c in (row["source"], row["target"])}, + {c: 1 for row in edge_rows for c in (row["source"], row["target"])}, + ) + store = Store(cfg) + graph = ConceptGraph(cfg, store) + chunks = [("a.md", 0), ("b.md", 0)] + concepts = [["python", "rust"], ["notes"]] + graph.write_concept_records(graph.build_concept_records(chunks, concepts)) + graph.rebuild_clusters() + # A sync re-ingests a.md: its full co-occurrence edge set is appended again. + graph.write_concept_records(graph.build_concept_records(chunks[:1], concepts[:1])) + graph.rebuild_clusters() + + rows = store.open_table(CONCEPT_EDGES_TABLE).search().limit(None).to_list() + assert [(r["source"], r["target"]) for r in rows] == [("python", "rust")] + @patch("lilbee.retrieval.concepts.graph._leiden_partition") def test_rebuild_counts_cooccurrence_per_distinct_chunk(self, mock_leiden, cg, mock_svc): """A pair counts once per distinct chunk it shares; a duplicate concept row @@ -819,6 +922,24 @@ def test_rebuild_counts_cooccurrence_per_distinct_chunk(self, mock_leiden, cg, m # python & ml in 2 of 4 chunks, co-occur in 2 -> PPMI 1.0 (not 3 from the dup). mock_leiden.assert_called_once_with([{"source": "ml", "target": "python", "weight": 1.0}]) + @patch("lilbee.retrieval.concepts.graph._leiden_partition") + def test_rebuild_skips_when_all_pairs_at_or_below_chance(self, mock_leiden, cg, mock_svc): + """When PPMI drops every pair (all co-occur at or below chance), there + is no edge set to cluster and Leiden must not run on an empty graph.""" + # python co-occurs with ml in both chunks; with only 2 chunks each pair + # sits exactly at chance (PMI 0) and is dropped. + _cc, side = self._chunk_concepts( + { + "chunk_source": ["d.md"] * 4, + "chunk_index": [0, 0, 1, 1], + "concept": ["python", "ml", "python", "ml"], + } + ) + mock_svc.store.open_table.side_effect = side + cg.rebuild_clusters() + mock_leiden.assert_not_called() + mock_svc.store.clear_and_add.assert_not_called() + @patch("lilbee.retrieval.concepts.graph._leiden_partition") def test_rebuild_skips_when_no_cooccurrence(self, mock_leiden, cg, mock_svc): """Chunks that each carry a single concept produce no pairs, so there is @@ -835,7 +956,11 @@ def test_rebuild_skips_when_no_cooccurrence(self, mock_leiden, cg, mock_svc): def test_rebuild_compacts_concept_tables(self, mock_leiden, cg, mock_svc): """rebuild_clusters ends with optimize() on the concept tables.""" cc_tbl, side = self._chunk_concepts( - {"chunk_source": ["d.md", "d.md"], "chunk_index": [0, 0], "concept": ["a", "b"]} + { + "chunk_source": ["d.md"] * 3, + "chunk_index": [0, 0, 1], + "concept": ["a", "b", "c"], + } ) mock_svc.store.open_table.side_effect = side mock_leiden.return_value = ({"a": 0}, {"a": 1}) @@ -895,17 +1020,34 @@ def test_basic_ppmi(self): # PPMI: all values >= 0 assert pmi[("a", "b")] >= 0.0 - def test_ppmi_clamps_negative(self): - """Anti-correlated pairs should get PPMI = 0.""" + def test_ppmi_discards_anticorrelated_pairs(self): + """Anti-correlated pairs are dropped, not stored with weight 0. + + A stored 0.0 would later be floored to a positive Leiden weight, + actively pulling pairs that co-occur less than chance into the + same community. + """ from collections import Counter from lilbee.retrieval.concepts.community import _compute_pmi - # a and b rarely co-occur but each appear often -> negative PMI -> clamped to 0 + # a and b rarely co-occur but each appear often -> negative PMI -> dropped cooccurrences = Counter({("a", "b"): 1}) concept_counts = Counter({"a": 9, "b": 9}) pmi = _compute_pmi(cooccurrences, concept_counts, 10) - assert pmi[("a", "b")] == 0.0 + assert ("a", "b") not in pmi + + def test_ppmi_discards_independent_pairs(self): + """Pairs co-occurring exactly at chance (PMI == 0) carry no signal.""" + from collections import Counter + + from lilbee.retrieval.concepts.community import _compute_pmi + + # P(a,b) = 0.25 = P(a) * P(b) -> PMI exactly 0 -> dropped + cooccurrences = Counter({("a", "b"): 1}) + concept_counts = Counter({"a": 2, "b": 2}) + pmi = _compute_pmi(cooccurrences, concept_counts, 4) + assert ("a", "b") not in pmi def test_ppmi_skips_zero_count_concepts(self): """Concepts with zero count are skipped (avoid division by zero).""" diff --git a/tests/test_config.py b/tests/test_config.py index 7c391100c..9fa0f1816 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -104,6 +104,86 @@ def test_lilbee_data_overrides_paths(self, tmp_path): assert c.data_dir == tmp_path / "data" assert c.lancedb_dir == tmp_path / "data" / "lancedb" + def test_data_root_expands_user_home(self): + """A ~ in LILBEE_DATA_ROOT expands: systemd/.env deliver a literal '~' + that would otherwise create a './~' tree and split a path-keyed lock.""" + env = _clean_env() + env.pop("LILBEE_DATA", None) + env["LILBEE_SKIP_TOML_CONFIG"] = "1" + env["LILBEE_DATA_ROOT"] = "~/lilbee_expanduser_probe" + with mock.patch.dict(os.environ, env, clear=True): + c = Config() + assert c.data_root == Path.home() / "lilbee_expanduser_probe" + assert c.lancedb_dir == Path.home() / "lilbee_expanduser_probe" / "data" / "lancedb" + + def test_symlinked_data_root_keys_the_same_paths_as_its_target(self, tmp_path): + """Two spellings of one directory derive one set of lock paths.""" + real = tmp_path / "real_root" + real.mkdir() + link = tmp_path / "link_root" + link.symlink_to(real) + + with mock.patch.dict(os.environ, {"LILBEE_DATA": str(real)}): + direct = Config() + with mock.patch.dict(os.environ, {"LILBEE_DATA": str(link)}): + through_link = Config() + + assert through_link.data_root == direct.data_root + assert through_link.data_dir == direct.data_dir + assert through_link.lancedb_dir == direct.lancedb_dir + + def test_padded_data_env_finds_the_same_dir_for_root_and_config( + self, tmp_path, overlay_reads_config_toml + ): + """A padded LILBEE_DATA sends the root and its config.toml to one dir.""" + (tmp_path / "config.toml").write_text("top_k = 7\n", encoding="utf-8") + with mock.patch.dict(os.environ, {"LILBEE_DATA": f" {tmp_path} "}): + c = Config() + assert c.data_root == tmp_path + assert c.top_k == 7 + + def test_relative_data_root_resolves_absolute(self, tmp_path, monkeypatch): + """A relative root must not re-key on the process working directory.""" + (tmp_path / "kb").mkdir() + monkeypatch.chdir(tmp_path) + with mock.patch.dict(os.environ, {"LILBEE_DATA": "kb"}): + c = Config() + assert c.data_root.is_absolute() + assert c.data_root == (tmp_path / "kb").resolve() + + def test_empty_data_root_falls_back_to_default_not_cwd(self): + """An empty LILBEE_DATA_ROOT must resolve to the platform default, not + the process cwd (which would make the data dir move with the launcher).""" + env = _clean_env() + env.pop("LILBEE_DATA", None) + env["LILBEE_SKIP_TOML_CONFIG"] = "1" + env["LILBEE_DATA_ROOT"] = "" + with mock.patch.dict(os.environ, env, clear=True): + c = Config() + assert c.data_root != Path() + assert c.data_root != Path.cwd() + assert str(c.data_root).endswith("lilbee") + # A raw blank string (direct construction / a string env source) hits + # the same fall-through instead of resolving to Path(".") = cwd. + c2 = Config(data_root=" ") + assert c2.data_root != Path() + assert c2.data_root != Path.cwd() + assert str(c2.data_root).endswith("lilbee") + + def test_unresolvable_home_still_loads_config(self): + """An unresolvable ~ still yields a usable root instead of raising. + + os.path.expanduser returns an unknown ~user unchanged; Path.expanduser + raises. + """ + from lilbee.core.system import canonical_data_root + + root = canonical_data_root("~nosuchuser_lilbee_probe/lilbee") + assert root.is_absolute() + assert str(root).endswith("lilbee") + # The normal case still expands to the real home. + assert canonical_data_root("~/lilbee") == Path.home() / "lilbee" + def test_local_server_urls_from_env(self, tmp_path): env = _clean_env(tmp_path) env["LILBEE_OLLAMA_BASE_URL"] = "http://box:11434" @@ -145,6 +225,40 @@ def test_chat_model_override_native_hf_ref(self): c = Config() assert c.chat_model == ref + +class TestOcrLanguage: + def test_defaults_to_english(self, tmp_path): + with mock.patch.dict(os.environ, _clean_env(tmp_path), clear=True): + assert Config().ocr_language == ["eng"] + + def test_env_plus_separated(self, tmp_path): + env = _clean_env(tmp_path) | {"LILBEE_OCR_LANGUAGE": "eng+deu"} + with mock.patch.dict(os.environ, env, clear=True): + assert Config().ocr_language == ["eng", "deu"] + + def test_env_comma_separated(self, tmp_path): + env = _clean_env(tmp_path) | {"LILBEE_OCR_LANGUAGE": "deu, fra"} + with mock.patch.dict(os.environ, env, clear=True): + assert Config().ocr_language == ["deu", "fra"] + + def test_direct_list(self): + assert Config(ocr_language=["spa"]).ocr_language == ["spa"] + + def test_persisted_newline_form_round_trips(self): + """app.settings joins list values with '\\n' before writing config.toml; + the validator must split on it so a multi-language value reloads intact.""" + persisted = "\n".join(["eng", "deu"]) + assert Config(ocr_language=persisted).ocr_language == ["eng", "deu"] + + def test_empty_list_falls_back_to_english(self): + assert Config(ocr_language=[]).ocr_language == ["eng"] + + def test_blank_string_falls_back_to_english(self): + assert Config(ocr_language="").ocr_language == ["eng"] + + def test_blank_entries_are_dropped(self): + assert Config(ocr_language=["", "eng", " "]).ocr_language == ["eng"] + def test_chat_mode_defaults_to_search_when_none_or_empty(self): """The validator coerces None / "" to 'search' so old configs round-trip.""" from lilbee.core.config.model import Config as ConfigCls @@ -185,6 +299,24 @@ def test_normalize_model_tag_blank_chat_rejected(self): with pytest.raises(ValidationError, match="embedding_model must not be blank"): cfg.embedding_model = "\t" + def test_fusion_config_fields_enforce_their_bounds(self): + """The new fusion/expansion knobs reject out-of-range values, so a bad + config surfaces at assignment instead of silently mis-weighting fusion.""" + from pydantic import ValidationError + + for field, bad in [ + ("lexical_fusion_weight", 1.5), + ("lexical_fusion_weight", -0.1), + ("adaptive_fusion_margin", 2.5), + ("adaptive_fusion_margin", -0.1), + ("title_search_weight", 1.5), + ("title_search_weight", -0.1), + ("neighbor_expansion", -1), + ("neighbor_expansion", 101), # upper bound guards against a token-count misread + ]: + with pytest.raises(ValidationError): + setattr(cfg, field, bad) + def test_embedding_dim_override(self): with mock.patch.dict(os.environ, {"LILBEE_EMBEDDING_DIM": "1024"}): c = Config() @@ -747,6 +879,17 @@ def test_env_overrides_toml(self, tmp_path) -> None: with mock.patch.dict(os.environ, env, clear=True): assert Config().semantic_chunking is True + def test_data_root_env_var_is_coerced_to_path(self, tmp_path) -> None: + """LILBEE_DATA_ROOT sets the data_root field directly as a string; + deriving the child paths from it must not raise on str / str.""" + env = _clean_env() + env["LILBEE_DATA_ROOT"] = str(tmp_path) + with mock.patch.dict(os.environ, env, clear=True): + c = Config() + assert c.data_root == tmp_path + assert c.documents_dir == tmp_path / "documents" + assert c.data_dir == tmp_path / "data" + class TestTopicThresholdConfig: def test_default_is_0_75(self, tmp_path) -> None: diff --git a/tests/test_dynamic_settings_eviction.py b/tests/test_dynamic_settings_eviction.py index 6d0dc60fe..77f7f0754 100644 --- a/tests/test_dynamic_settings_eviction.py +++ b/tests/test_dynamic_settings_eviction.py @@ -37,6 +37,11 @@ def reload_role(self, role: object, *, wait: bool = False) -> None: def drop_loaded_models_async(self) -> None: self.dropped += 1 + def vision_ocr( + self, image_bytes: bytes, model: str, prompt: str = "", *, timeout: float | None = None + ) -> str: + return "" + def role_ready(self, role: object) -> bool: # The bottom TaskBar polls chat readiness on every screen; report ready so # this settings-eviction host isn't treated as mid-warm. @@ -281,12 +286,18 @@ def test_reconcile_embedding_dim_leaves_dim_when_unreadable_or_matching(monkeypa assert cfg.embedding_dim == 768 # already matches -> untouched -def test_chat_and_vision_model_change_reloads_those_roles(): +def test_chat_and_vision_model_change_reloads_those_roles(monkeypatch): """A chat_model / vision_model swap reloads that role's server so the next request uses the new model. The fleet serves the configured model per role and rejects per-call overrides, so the reload is required (not optional).""" from lilbee.providers.roles import WorkerRole + # The vision-model reload also (un)registers the OCR backend; keep that global + # xberg state out of this unit test. + monkeypatch.setattr("xberg.list_ocr_backends", list) + monkeypatch.setattr("xberg.register_ocr_backend", lambda backend: None) + monkeypatch.setattr("xberg.unregister_ocr_backend", lambda name: None) + provider = _install_recording_provider() try: apply_settings_update({"chat_model": "Qwen/Qwen3-0.6B-GGUF"}) diff --git a/tests/test_embed_truncation_surfacing.py b/tests/test_embed_truncation_surfacing.py index e5f5f7b87..06492e5bd 100644 --- a/tests/test_embed_truncation_surfacing.py +++ b/tests/test_embed_truncation_surfacing.py @@ -45,16 +45,23 @@ def test_effective_limit_at_least_chunk_budget(self, embedder): class TestTruncationIsSurfaced: - """Over-budget input is counted and the count reaches the caller.""" + """Over-budget input is counted and the count reaches the caller. - def test_embed_batch_reports_truncated_count(self, embedder): + The count the pipeline surfaces is the delta of the lock-protected + cumulative counter across the run, which is the only reading that stays + correct while several ingest threads embed through the shared Embedder. + """ + + def test_embed_batch_counts_truncations(self, embedder): over = "x" * (embedder.embed_char_budget + 500) + before = embedder.truncated_total embedder.embed_batch(["short", over, over]) - assert embedder.last_batch_truncated == 2 + assert embedder.truncated_total - before == 2 - def test_no_truncation_reports_zero(self, embedder): + def test_no_truncation_counts_nothing(self, embedder): + before = embedder.truncated_total embedder.embed_batch(["short", "also short"]) - assert embedder.last_batch_truncated == 0 + assert embedder.truncated_total - before == 0 class TestSyncResultCarriesTruncation: diff --git a/tests/test_embedder.py b/tests/test_embedder.py index c28e57e1f..7a3fd3a1b 100644 --- a/tests/test_embedder.py +++ b/tests/test_embedder.py @@ -1,5 +1,6 @@ """Tests for the embedding wrapper (mocked -- no live server needed).""" +import logging from unittest import mock from unittest.mock import MagicMock @@ -53,6 +54,21 @@ def test_truncates_long_input(self, embedder, mock_provider): call_args = mock_provider.embed.call_args[0][0] assert len(call_args[0]) == embedder.embed_char_budget + def test_instruction_prefix_counts_against_the_char_budget(self, embedder, mock_provider): + """The budget guards the embed model's context, so the prefix has to + fit inside it: prepending after truncation ships budget+len(prefix) + chars and the engine silently drops the tail the clamp exists to save.""" + from lilbee.retrieval.embedding_profiles import EmbeddingProfile + + profile = EmbeddingProfile(query_instruction="Instruct: do the thing\nQuery: ") + mock_provider.embed.return_value = [[0.0] * 768] + long_text = "a" * (embedder.embed_char_budget + 1000) + with mock.patch.object(embedder, "_profile", return_value=profile): + embedder.embed_query(long_text) + sent = mock_provider.embed.call_args[0][0][0] + assert sent.startswith(profile.query_instruction) + assert len(sent) == embedder.embed_char_budget + class TestEmbedBatch: def test_returns_multiple_vectors(self, embedder, mock_provider): @@ -141,6 +157,21 @@ def test_validate_returns_false_on_provider_error(self, embedder, mock_provider) mock_provider.list_models.side_effect = RuntimeError("no connection") assert embedder.validate_model() is False + def test_validate_warns_when_the_model_is_missing(self, embedder, mock_provider, caplog): + """Every production caller invokes this as a bare statement, so a gate + that only returned a bool passed silently and the run failed per-file + much later. The warning is what makes it a gate.""" + mock_provider.list_models.return_value = [] + with caplog.at_level(logging.WARNING, logger="lilbee.retrieval.embedder"): + embedder.validate_model() + assert cfg.embedding_model in caplog.text + + def test_validate_stays_quiet_when_the_model_is_there(self, embedder, mock_provider, caplog): + mock_provider.list_models.return_value = [cfg.embedding_model] + with caplog.at_level(logging.WARNING, logger="lilbee.retrieval.embedder"): + embedder.validate_model() + assert not caplog.text + def test_embedding_available_true(self, embedder, mock_provider): mock_provider.list_models.return_value = [cfg.embedding_model] assert embedder.embedding_available() is True diff --git a/tests/test_embedding_profiles.py b/tests/test_embedding_profiles.py index 09ef0d3a3..a8caa371b 100644 --- a/tests/test_embedding_profiles.py +++ b/tests/test_embedding_profiles.py @@ -26,6 +26,18 @@ def test_instruct_embedders_use_instruct_query(ref) -> None: assert p.doc_prefix == "" +@pytest.mark.parametrize( + "ref", + ["hkunlp/instructor-large-GGUF/i.gguf", "hkunlp/instructor-xl-GGUF/i.gguf"], +) +def test_instructor_family_stays_symmetric(ref) -> None: + """The Instructor family's name contains "instruct" but it wants a document + instruction too, in a different dialect. Giving it the Instruct/Query query + prefix and no document prefix is asymmetric prompting in the wrong format -- + the silently-wrong case the symmetric fallback exists to avoid.""" + assert resolve_embedding_profile(ref) == EmbeddingProfile() + + @pytest.mark.parametrize( "ref", ["intfloat/e5-large-v2-GGUF/e.gguf", "intfloat/multilingual-e5-large-GGUF/m.gguf"], diff --git a/tests/test_entities_extractor.py b/tests/test_entities_extractor.py index 817274c52..c2ed1fa39 100644 --- a/tests/test_entities_extractor.py +++ b/tests/test_entities_extractor.py @@ -2,6 +2,7 @@ import json from types import SimpleNamespace +from unittest import mock from unittest.mock import MagicMock import pytest @@ -179,6 +180,43 @@ def test_duplicate_extractors_collapse_to_the_first_name(self): assert schema is not None assert [t.name for t in schema.types] == ["title", "person", "vessel"] + def test_drops_empty_pattern_for_pattern_bearing_kinds(self): + """An empty pattern compiles fine but makes finditer yield a + zero-width match at every position of every chunk in the corpus. + The induction prompt tells the model to leave the pattern empty for + llm kinds, so a confused model emitting regex+'' is realistic.""" + provider = MagicMock() + provider.chat.return_value = _text_result( + json.dumps( + { + "types": [ + {"name": "empty_regex", "kind": "regex", "pattern": ""}, + {"name": "empty_spacy", "kind": "spacy", "pattern": ""}, + {"name": "part_number", "kind": "regex", "pattern": r"PX\d{4}"}, + ] + } + ) + ) + schema = induce_schema(["text"], provider) + assert schema is not None + assert [t.name for t in schema.types] == ["part_number"] + + def test_llm_kind_may_keep_an_empty_pattern(self): + """llm kinds carry a description instead, exactly as the prompt asks.""" + provider = MagicMock() + provider.chat.return_value = _text_result( + json.dumps( + { + "types": [ + {"name": "vessel", "kind": "llm", "pattern": "", "description": "ships"} + ] + } + ) + ) + schema = induce_schema(["text"], provider) + assert schema is not None + assert [t.name for t in schema.types] == ["vessel"] + def test_drops_bad_regex_keeps_rest(self): provider = MagicMock() provider.chat.return_value = _text_result( @@ -220,6 +258,24 @@ def test_unbalanced_is_none(self): assert _first_json_object("{unclosed") is None + def test_braces_inside_string_values_do_not_derail_the_scan(self): + """A regex pattern or entity text containing a brace is legal JSON; + a naive brace counter truncates mid-string and loses the object.""" + from lilbee.retrieval.entities.extractor import _first_json_object + + text = '{"types": [{"name": "x", "pattern": "\\\\}"}]}' + parsed = _first_json_object(text) + assert parsed == {"types": [{"name": "x", "pattern": "\\}"}]} + + assert _first_json_object('{"0": [{"type": "note", "text": "a } b"}]}') == { + "0": [{"type": "note", "text": "a } b"}] + } + + def test_object_after_stray_brace_in_prose_is_found(self): + from lilbee.retrieval.entities.extractor import _first_json_object + + assert _first_json_object('opening { thoughts... {"kind": "x"}') == {"kind": "x"} + class TestInduceSchemaEdges: def test_non_dict_type_entry_dropped(self): @@ -262,6 +318,52 @@ def test_regex_kind(self): } assert all(r["confidence"] == 1.0 for r in rows) + def test_runaway_pattern_is_abandoned_and_named(self, caplog): + """An induced pattern that blows its match budget must not stall the + pass: the type is dropped with a warning naming it, and the other + types still extract.""" + import lilbee.retrieval.entities.extractor as extractor_mod + + runaway = EntityType(name="runaway", kind=ExtractorKind.REGEX, pattern=r"(a+)+b") + real_compile = extractor_mod._compile_pattern + + def fake_compile(pattern): + if pattern == r"(a+)+b": + stub = MagicMock() + stub.finditer.side_effect = TimeoutError("regex timed out") + return stub + return real_compile(pattern) + + with ( + mock.patch.object(extractor_mod, "_compile_pattern", fake_compile), + caplog.at_level("WARNING"), + ): + rows = extract_entities( + [_chunk("parts PX4471 aaaaaaaaaa")], + EntitySchema(types=[runaway, PART]), + ) + assert {r["normalized_value"] for r in rows} == {"px4471"} + assert any("runaway" in rec.message for rec in caplog.records) + + def test_runaway_pattern_is_not_retried_across_batches(self): + """With stats carried across a full pass, a pattern that timed out + once is skipped for the rest of the pass rather than costing the + budget again on every chunk.""" + import lilbee.retrieval.entities.extractor as extractor_mod + from lilbee.retrieval.entities.extractor import ExtractionStats + + runaway = EntityType(name="runaway", kind=ExtractorKind.REGEX, pattern=r"(a+)+b") + stub = MagicMock() + stub.finditer.side_effect = TimeoutError("regex timed out") + + stats = ExtractionStats() + with mock.patch.object(extractor_mod, "_compile_pattern", lambda pattern: stub): + extract_entities([_chunk("aaaa")], EntitySchema(types=[runaway]), stats=stats) + extract_entities([_chunk("aaaa")], EntitySchema(types=[runaway]), stats=stats) + assert stats.timed_out_types == {"runaway"} + # Two chunks, but the pattern was only ever attempted once. + assert stub.finditer.call_count == 1 + def test_anchored_pattern_matches_identifiers_inline(self): """The OCR-corpus failure: an induced pattern anchored with ^...$ matches only a token that IS the identifier, so identifiers inline in @@ -341,6 +443,40 @@ def test_llm_failure_degrades_to_empty(self): ) assert rows == [] + def test_llm_provider_failure_is_reported_through_stats(self): + """A caller that passes stats can tell a clean empty result from a + provider failure -- the full pass must not mark the schema applied + when batches failed.""" + from lilbee.retrieval.entities.extractor import ExtractionStats + + provider = MagicMock() + provider.chat.side_effect = RuntimeError("down") + stats = ExtractionStats() + rows = extract_entities( + [_chunk("the Meridian docked")], + EntitySchema(types=[VESSEL]), + provider=provider, + stats=stats, + ) + assert rows == [] + assert stats.llm_batches == 1 + assert stats.llm_batches_failed == 1 + + def test_successful_llm_batches_count_zero_failures(self): + from lilbee.retrieval.entities.extractor import ExtractionStats + + provider = MagicMock() + provider.chat.return_value = _text_result(json.dumps({"0": []})) + stats = ExtractionStats() + extract_entities( + [_chunk("no ships here")], + EntitySchema(types=[VESSEL]), + provider=provider, + stats=stats, + ) + assert stats.llm_batches == 1 + assert stats.llm_batches_failed == 0 + def test_llm_response_edge_shapes_are_ignored(self): provider = MagicMock() provider.chat.return_value = _text_result( diff --git a/tests/test_entities_ingest.py b/tests/test_entities_ingest.py index b13408e9d..d445fd58e 100644 --- a/tests/test_entities_ingest.py +++ b/tests/test_entities_ingest.py @@ -112,7 +112,7 @@ def test_spacy_types_load_the_model_when_available(self, mock_svc): fake_nlp = mock.MagicMock(return_value=mock.MagicMock(ents=[])) with ( mock.patch("lilbee.retrieval.concepts.concepts_available", return_value=True), - mock.patch("lilbee.retrieval.concepts.nlp._ensure_spacy_model", return_value=fake_nlp), + mock.patch("lilbee.retrieval.concepts.nlp.load_spacy_pipeline", return_value=fake_nlp), ): rows = asyncio.run(_build_entity_records(_records(), "a.txt")) assert rows is not None @@ -124,7 +124,7 @@ def test_spacy_model_import_error_degrades(self, mock_svc, caplog): with ( mock.patch("lilbee.retrieval.concepts.concepts_available", return_value=True), mock.patch( - "lilbee.retrieval.concepts.nlp._ensure_spacy_model", + "lilbee.retrieval.concepts.nlp.load_spacy_pipeline", side_effect=ImportError("no model"), ), caplog.at_level("WARNING"), diff --git a/tests/test_entities_lifecycle.py b/tests/test_entities_lifecycle.py index d50a4175d..56405eee3 100644 --- a/tests/test_entities_lifecycle.py +++ b/tests/test_entities_lifecycle.py @@ -210,7 +210,7 @@ def test_spacy_and_llm_kinds_wire_their_tools(self, isolated): fake_nlp = mock.MagicMock(return_value=mock.MagicMock(ents=[])) with ( mock.patch("lilbee.retrieval.concepts.concepts_available", return_value=True), - mock.patch("lilbee.retrieval.concepts.nlp._ensure_spacy_model", return_value=fake_nlp), + mock.patch("lilbee.retrieval.concepts.nlp.load_spacy_pipeline", return_value=fake_nlp), ): ensure_entities() fake_nlp.assert_called() @@ -234,7 +234,7 @@ def test_spacy_model_import_error_degrades(self, isolated, caplog): with ( mock.patch("lilbee.retrieval.concepts.concepts_available", return_value=True), mock.patch( - "lilbee.retrieval.concepts.nlp._ensure_spacy_model", + "lilbee.retrieval.concepts.nlp.load_spacy_pipeline", side_effect=ImportError("no model"), ), caplog.at_level("WARNING"), @@ -243,6 +243,45 @@ def test_spacy_model_import_error_degrades(self, isolated, caplog): assert store.entity_value_counts("part_number") == (1, 1) # regex still ran assert any("spaCy model unavailable" in r.message for r in caplog.records) + def test_provider_failure_during_full_pass_leaves_schema_unapplied(self, isolated, caplog): + """A pass whose LLM batches fail (chat model down) must not mark the + schema applied: applied=True with zero rows would make every later + sync return early and nothing would ever retry the extraction.""" + store, services = isolated + _index_chunks(store, ["the vessel Meridian docked"]) + save_schema( + EntitySchema(types=[EntityType(name="vessel", kind=ExtractorKind.LLM)]), + store, + applied=False, + source_count=1, + ) + services.provider.chat.side_effect = RuntimeError("model unloaded") + with caplog.at_level("WARNING"): + ensure_entities() + assert not _applied(store) + assert any("LLM batch" in r.message for r in caplog.records) + + def test_llm_batch_failure_still_writes_other_kind_rows(self, isolated): + """Extractor kinds degrade independently: regex rows land even when + the LLM batches fail, and the pass is simply redone next sync.""" + store, services = isolated + _index_chunks(store, ["part PX4471 aboard the Meridian"]) + save_schema( + EntitySchema( + types=[ + EntityType(name="part_number", kind=ExtractorKind.REGEX, pattern=r"PX\d{4}"), + EntityType(name="vessel", kind=ExtractorKind.LLM), + ] + ), + store, + applied=False, + source_count=1, + ) + services.provider.chat.side_effect = RuntimeError("model unloaded") + ensure_entities() + assert store.entity_value_counts("part_number") == (1, 1) + assert not _applied(store) + def test_empty_chunks_table_skips_pass(self, isolated): store, _services = isolated save_schema(_part_schema(), store, applied=False, source_count=1) @@ -259,6 +298,93 @@ def test_emptied_chunks_table_defers_induction(self, isolated): services.provider.chat.assert_not_called() +class TestChunkScanProjection: + """The lifecycle's chunk scans must push column projection into LanceDB; + a bare to_arrow() drags the whole table, embedding vectors included, + into memory before selecting.""" + + @staticmethod + def _projecting_table(batches): + table = mock.MagicMock() + query = table.search.return_value.select.return_value.limit.return_value + query.to_arrow.return_value.to_batches.return_value = batches + return table + + def test_sample_chunks_projects_only_the_chunk_column(self): + from lilbee.retrieval.entities.lifecycle import _sample_chunks + + batch = mock.MagicMock() + batch.column.return_value.to_pylist.return_value = ["a", "b", "c"] + table = self._projecting_table([batch]) + table.count_rows.return_value = 3 + store = mock.MagicMock() + store.open_table.return_value = table + + # 3 rows into a 2-sample: stride 2 takes the first and the last, + # spreading across the table rather than taking the head. + texts = _sample_chunks(store, 2) + assert texts == ["a", "c"] + table.search.return_value.select.assert_called_once_with(["chunk"]) + table.search.return_value.select.return_value.limit.assert_called_once_with(None) + table.to_arrow.assert_not_called() + + def test_sample_spans_the_whole_table_not_just_the_head(self): + """Floor division makes the stride too small, so the sample is the + first `limit` contiguous rows and the table's tail -- the most + recently ingested documents, i.e. exactly the new family that + re-induction exists to catch -- can never be sampled.""" + from lilbee.retrieval.entities.lifecycle import _sample_chunks + + total = 79 + batch = mock.MagicMock() + batch.column.return_value.to_pylist.return_value = [f"chunk-{i}" for i in range(total)] + table = self._projecting_table([batch]) + table.count_rows.return_value = total + store = mock.MagicMock() + store.open_table.return_value = table + + texts = _sample_chunks(store, 40) + sampled = [int(t.split("-")[1]) for t in texts] + assert len(sampled) <= 40 + # Floor division gives stride 1 here, sampling rows 0-39 and leaving + # the last half of the table unreachable; an even spread reaches it. + assert max(sampled) >= total - 10 + + def test_sample_chunks_stops_reading_once_the_sample_is_full(self): + from lilbee.retrieval.entities.lifecycle import _sample_chunks + + first = mock.MagicMock() + first.column.return_value.to_pylist.return_value = ["a", "b", "c"] + second = mock.MagicMock() + table = self._projecting_table([first, second]) + table.count_rows.return_value = 4 + store = mock.MagicMock() + store.open_table.return_value = table + + # step = 4 // 2 = 2 -> indexes 0 and 2, both in the first batch. + texts = _sample_chunks(store, 2) + assert texts == ["a", "c"] + second.column.assert_not_called() + + def test_full_pass_projects_the_needed_columns(self): + from lilbee.retrieval.entities.lifecycle import _full_pass + + batch = mock.MagicMock() + batch.to_pylist.return_value = [ + {"chunk": "part PX4471", "source": "a.txt", "chunk_index": 0, "page_start": 1} + ] + table = self._projecting_table([batch]) + store = mock.MagicMock() + store.open_table.return_value = table + store.add_entities.return_value = 1 + + assert _full_pass(store, _part_schema(), None) is True + table.search.return_value.select.assert_called_once_with( + ["chunk", "source", "chunk_index", "page_start"] + ) + table.to_arrow.assert_not_called() + + class TestSchemaEvolution: """The taxonomy keeps up with the corpus without anyone touching it: a library that grows a new kind of document gains the types to answer about diff --git a/tests/test_export.py b/tests/test_export.py index 2b9259540..4be5f1bbb 100644 --- a/tests/test_export.py +++ b/tests/test_export.py @@ -263,3 +263,47 @@ def counting_batch(items): assert legacy == [] # none of the old per-op unlocked writes were used # The atomic write still landed the source as IMPORTED. assert store.get_sources()[0]["source_type"] == SourceType.IMPORTED + + async def test_import_stamps_stem_title(self, services): + # A dataset exported before the metadata columns existed carries none, so + # the stem-derived title keeps imported chunks visible to the title arm. + store = services + await import_dataset(store, [_page("field_notes.pdf", 1, "page body")]) + chunks = store.get_chunks_by_source("field_notes.pdf") + assert chunks and all(c.title == "field notes" for c in chunks) + assert store.get_sources()[0]["title"] == "field notes" + + async def test_import_restores_extraction_metadata_from_the_dataset(self, services): + """A dataset carrying the metadata columns restores the real extracted + title/authors/created_at instead of downgrading to the filename stem.""" + store = services + row = { + **_page("report_2021.pdf", 1, "page body"), + "title": "Annual Report", + "authors": "Ada, Grace", + "created_at": "2021-05-01", + } + await import_dataset(store, [row]) + chunks = store.get_chunks_by_source("report_2021.pdf") + assert chunks and all(c.title == "Annual Report" for c in chunks) + source = store.get_sources()[0] + assert source["title"] == "Annual Report" + assert source["authors"] == "Ada, Grace" + assert source["created_at"] == "2021-05-01" + + async def test_export_round_trips_the_metadata_back_out(self, services): + """The exported dataset carries each source's metadata on its page rows, + so an export/import cycle preserves it instead of losing it.""" + store = services + row = { + **_page("report_2021.pdf", 1, "page body"), + "title": "Annual Report", + "authors": "Ada, Grace", + "created_at": "2021-05-01", + } + await import_dataset(store, [row]) + exported = build_page_dataset(store).to_pylist() + assert exported + assert all(r["title"] == "Annual Report" for r in exported) + assert all(r["authors"] == "Ada, Grace" for r in exported) + assert all(r["created_at"] == "2021-05-01" for r in exported) diff --git a/tests/test_fleet_client.py b/tests/test_fleet_client.py index ee62b1018..dc0e80b9e 100644 --- a/tests/test_fleet_client.py +++ b/tests/test_fleet_client.py @@ -55,6 +55,17 @@ def _client(handler=_handler) -> LlamaServerClient: return client +def test_count_tokens_returns_server_token_count() -> None: + """count_tokens posts to the server's /tokenize and reports the token count.""" + + def handler(request: httpx.Request) -> httpx.Response: + if request.url.path == "/upstream/test-model/tokenize": + return httpx.Response(200, json={"tokens": [1, 2, 3, 4, 5]}) + return httpx.Response(404) + + assert _client(handler).count_tokens("hello world") == 5 + + def test_rerank_scores_pairs_via_rank_pooling() -> None: seen: dict[str, list] = {} diff --git a/tests/test_fleet_provider.py b/tests/test_fleet_provider.py index 85ad8cc76..98f93cf95 100644 --- a/tests/test_fleet_provider.py +++ b/tests/test_fleet_provider.py @@ -220,6 +220,22 @@ def test_embed_routes_to_server_when_present() -> None: assert p.embed(["a"]) == [[0.1]] +def test_count_tokens_routes_to_embed_server() -> None: + client = _fake_client() + client.count_tokens.return_value = 9 + p = _provider_with_clients({WorkerRole.EMBED: [client]}) + assert p.count_tokens("hello") == 9 + client.count_tokens.assert_called_once_with("hello") + + +def test_count_tokens_without_server_raises() -> None: + from lilbee.providers.base import ProviderError + + p = _provider_with_clients({}) + with pytest.raises(ProviderError): + p.count_tokens("hello") + + def test_embed_routes_to_least_busy_replica() -> None: # Data-parallel replicas: a request goes to the idlest replica in the pool. busy, idle = _fake_client(5), _fake_client(1) @@ -761,71 +777,6 @@ def test_dispatch_vision_marks_second_replica_unhealthy_on_retry_failure() -> No dead_b.mark_unhealthy.assert_called_once() -def test_ocr_pdf_page_skips_when_budget_exhausted(monkeypatch) -> None: - # A page that waited out the document deadline while queued is skipped (empty - # text), not run un-timed -- a 0 timeout would disable the per-page cap. - monkeypatch.setattr("lilbee.vision.build_vision_messages", lambda *_a, **_k: []) - called: list[object] = [] - monkeypatch.setattr(prov_mod, "_vision_call", lambda *a, **_k: called.append(a) or "ocr") - idx, text = prov_mod._ocr_pdf_page( - 3, - b"\x89PNG", - pool=[prov_mod._VisionReplica(_fake_client(), 2)], - ocr_prompt="describe", - deadline=time.monotonic() - 1.0, # already past - page_path=Path("doc.pdf"), - ) - assert (idx, text) == (3, "") - assert called == [] # _vision_call never ran - - -def test_ocr_pdf_page_retries_busy_then_keeps_text(monkeypatch) -> None: - # A 429 on a PDF page is retried like the image path, so the page text lands - # instead of the page being skipped to empty. - from lilbee.providers.base import ProviderError, ProviderErrorKind - - monkeypatch.setattr("lilbee.providers.fleet.client.time.sleep", lambda _s: None) - monkeypatch.setattr("lilbee.vision.build_vision_messages", lambda *_a, **_k: []) - busy = ProviderError("busy", provider="llama-server", kind=ProviderErrorKind.RATE_LIMIT) - client = _fake_client() - client.chat.side_effect = [busy, busy, "page text"] - idx, text = prov_mod._ocr_pdf_page( - 0, - b"\x89PNG", - pool=[prov_mod._VisionReplica(client, 2)], - ocr_prompt="describe", - deadline=None, - page_path=Path("doc.pdf"), - ) - assert (idx, text) == (0, "page text") - assert client.chat.call_count == 3 - - -def test_ocr_pdf_page_retries_past_attempt_cap_until_deadline(monkeypatch) -> None: - # The per-page path is deadline-bound too: a deep queue that 429s past the - # attempt cap still lands the page while the document deadline holds (bb-z34). - from lilbee.providers.base import ProviderError, ProviderErrorKind - - monkeypatch.setattr("lilbee.providers.fleet.client.time.sleep", lambda _s: None) - monkeypatch.setattr("lilbee.providers.fleet.client.time.monotonic", lambda: 0.0) - monkeypatch.setattr("lilbee.providers.fleet.provider.time.monotonic", lambda: 0.0) - monkeypatch.setattr("lilbee.vision.build_vision_messages", lambda *_a, **_k: []) - busy = ProviderError("busy", provider="llama-server", kind=ProviderErrorKind.RATE_LIMIT) - client = _fake_client() - # A positive remaining budget routes through the bounded (chat_bounded) path. - client.chat_bounded.side_effect = [busy] * (prov_mod._VISION_BUSY_RETRIES + 5) + ["page text"] - idx, text = prov_mod._ocr_pdf_page( - 0, - b"\x89PNG", - pool=[prov_mod._VisionReplica(client, 2)], - ocr_prompt="describe", - deadline=100.0, - page_path=Path("doc.pdf"), - ) - assert (idx, text) == (0, "page text") - assert client.chat_bounded.call_count == prov_mod._VISION_BUSY_RETRIES + 6 - - def test_chat_streams_from_server() -> None: client = _fake_client(0) client.chat_stream_items.return_value = iter(["a", "b"]) @@ -984,139 +935,6 @@ def test_supports_tools_tolerates_unstattable_path(monkeypatch, _clear_tools_cac assert FleetProvider().supports_tools("org/repo/chat.gguf") is True -def test_pdf_ocr_ocrs_each_page_over_vision_server(monkeypatch) -> None: - from lilbee.runtime.progress import EventType - from lilbee.vision import PageText - - client = _fake_client(0) - client.chat.side_effect = ["page one", "page two"] - p = _provider_with_clients({WorkerRole.VISION: [client]}) - monkeypatch.setattr(cfg, "vision_model", "") # empty model arg -> configured - monkeypatch.setattr(cfg, "vision_ocr_concurrency", 1) # sequential: side_effect by call order - monkeypatch.setattr("lilbee.vision.pdf_page_count", lambda _p: 2) - monkeypatch.setattr( - "lilbee.vision.rasterize_pdf", lambda _p: iter([(0, b"png0"), (1, b"png1")]) - ) - events: list[tuple] = [] - result = p.pdf_ocr( - Path("doc.pdf"), - backend="vision", # type: ignore[arg-type] - on_progress=lambda etype, evt: events.append((etype, evt.page, evt.total_pages)), - ) - assert result == [PageText(1, "page one"), PageText(2, "page two")] - assert events == [(EventType.EXTRACT, 1, 2), (EventType.EXTRACT, 2, 2)] - - -def test_pdf_ocr_tallies_unrecovered_pages(monkeypatch, caplog) -> None: - # A page skipped to empty text (timeout, persistently busy fleet) must surface - # in one end-of-run tally, not vanish silently as a blank index entry. - import logging - - from lilbee.providers.base import ProviderError - from lilbee.vision import PageText - - client = _fake_client(0) - client.chat.side_effect = [ProviderError("boom", provider="llama-server"), "page two"] - p = _provider_with_clients({WorkerRole.VISION: [client]}) - monkeypatch.setattr(cfg, "vision_model", "") - monkeypatch.setattr(cfg, "vision_ocr_concurrency", 1) - monkeypatch.setattr("lilbee.vision.pdf_page_count", lambda _p: 2) - monkeypatch.setattr( - "lilbee.vision.rasterize_pdf", lambda _p: iter([(0, b"png0"), (1, b"png1")]) - ) - with caplog.at_level(logging.WARNING, logger="lilbee.providers.fleet.provider"): - result = p.pdf_ocr(Path("doc.pdf"), backend="vision") # type: ignore[arg-type] - assert result == [PageText(1, ""), PageText(2, "page two")] - assert "no text for 1 of 2 pages" in caplog.text - - -def test_pdf_ocr_runs_pages_concurrently_and_preserves_order(monkeypatch) -> None: - # OCR fans pages across the vision server's batching slots; results must still - # come back in page order, and more than one page must be in flight at once. - import threading - import time as _time - - monkeypatch.setattr(cfg, "vision_model", "") - monkeypatch.setattr(cfg, "vision_ocr_concurrency", 4) # fallback pool: 4 slots - n = 8 - monkeypatch.setattr("lilbee.vision.pdf_page_count", lambda _p: n) - monkeypatch.setattr( - "lilbee.vision.rasterize_pdf", lambda _p: iter([(i, f"png{i}".encode()) for i in range(n)]) - ) - lock = threading.Lock() - inflight = {"now": 0, "max": 0} - - def _vision(_client, _messages, _timeout): - with lock: - inflight["now"] += 1 - inflight["max"] = max(inflight["max"], inflight["now"]) - _time.sleep(0.02) - with lock: - inflight["now"] -= 1 - return "ocr" - - monkeypatch.setattr(prov_mod, "_vision_call", _vision) - p = _provider_with_clients({WorkerRole.VISION: [_fake_client(0)]}) - result = p.pdf_ocr(Path("doc.pdf"), backend="vision") # type: ignore[arg-type] - assert [pt.page for pt in result] == list(range(1, n + 1)) # reassembled in order - assert inflight["max"] >= 2 # pages ran concurrently, not one at a time - - -def test_pdf_drain_budget_totals_pages_plus_load_grace(monkeypatch) -> None: - """Budget is one document-wide pool: pages*per_page + load grace, else uncapped.""" - monkeypatch.setattr(cfg, "vision_load_budget_s", 300.0) - assert prov_mod._pdf_drain_budget(2, 120.0) == 540.0 - assert prov_mod._pdf_drain_budget(5, None) is None - assert prov_mod._pdf_drain_budget(5, 0.0) is None - - -def test_pdf_ocr_spends_one_document_budget_across_pages(monkeypatch) -> None: - """Each page gets the remaining doc budget, not a fixed per-page cap.""" - from lilbee.vision import PageText - - p = _provider_with_clients({WorkerRole.VISION: [_fake_client(0)]}) - monkeypatch.setattr(cfg, "vision_model", "") - monkeypatch.setattr(cfg, "vision_load_budget_s", 300.0) - monkeypatch.setattr("lilbee.vision.pdf_page_count", lambda _p: 2) - monkeypatch.setattr( - "lilbee.vision.rasterize_pdf", lambda _p: iter([(0, b"png0"), (1, b"png1")]) - ) - seen: list[float | None] = [] - - def _capture(_client, _messages, timeout): - seen.append(timeout) - return "ocr" - - monkeypatch.setattr(prov_mod, "_vision_call", _capture) - result = p.pdf_ocr(Path("doc.pdf"), backend="vision", per_page_timeout_s=120.0) # type: ignore[arg-type] - assert result == [PageText(1, "ocr"), PageText(2, "ocr")] - # Budget is 2*120 + 300 = 540; pages run concurrently, so each draws nearly - # the full remaining budget (far above any 120 cap), in either capture order. - assert seen[0] == pytest.approx(540.0, abs=1.0) - assert seen[1] == pytest.approx(540.0, abs=1.0) - assert all(t is not None and t > 120.0 for t in seen) - - -def test_pdf_ocr_without_per_page_timeout_runs_uncapped(monkeypatch) -> None: - """No per-page cap means an uncapped (None) budget on every page.""" - p = _provider_with_clients({WorkerRole.VISION: [_fake_client(0)]}) - monkeypatch.setattr(cfg, "vision_model", "") - monkeypatch.setattr("lilbee.vision.pdf_page_count", lambda _p: 1) - monkeypatch.setattr("lilbee.vision.rasterize_pdf", lambda _p: iter([(0, b"png0")])) - seen: list[float | None] = [] - monkeypatch.setattr(prov_mod, "_vision_call", lambda *a: seen.append(a[2]) or "ocr") - p.pdf_ocr(Path("doc.pdf"), backend="vision", per_page_timeout_s=None) # type: ignore[arg-type] - assert seen == [None] - - -def test_pdf_ocr_without_server_raises() -> None: - from lilbee.providers.base import ProviderError - - p = _provider_with_clients({}) - with pytest.raises(ProviderError, match="No vision model server is running"): - p.pdf_ocr(Path("doc.pdf"), backend="vision") # type: ignore[arg-type] - - # --- llama-swap lifecycle ---------------------------------------------------- @@ -2265,27 +2083,6 @@ def test_vision_ocr_fails_over_to_healthy_replica(self, monkeypatch) -> None: assert dead.healthy is False assert alive.calls == 1 - def test_vision_pdf_page_fails_over_to_healthy_replica(self, monkeypatch) -> None: - """The per-page PDF OCR path fails a dead replica over too, so a page is - OCR'd by a live replica rather than skipped to empty text (bb-7jg1.5).""" - import httpx as _httpx - - from lilbee.providers.fleet import provider as prov - - dead = _FakeReplica(fail=_httpx.ConnectError("refused")) - alive = _FakeReplica(in_flight=5) - idx, text = prov._ocr_pdf_page( - 0, - b"\x89PNG", - pool=[prov._VisionReplica(dead, 2), prov._VisionReplica(alive, 2)], - ocr_prompt="read it", - deadline=None, - page_path=Path("doc.pdf"), - ) - assert (idx, text) == (0, "ocr text") - assert dead.healthy is False - assert alive.calls == 1 - def test_successful_call_restores_an_unhealthy_replica(self) -> None: only = _FakeReplica() only.mark_unhealthy() @@ -2399,30 +2196,6 @@ def test_bounded_call_passes_the_deadline_to_chat_bounded(self) -> None: assert prov_mod._vision_call(client, [{"role": "user", "content": "x"}], 9.0) == "text" assert client.chat_bounded.call_args.kwargs["deadline_s"] == 9.0 - def test_pdf_ocr_one_timed_out_page_does_not_abort_siblings(self, monkeypatch) -> None: - from lilbee.providers.base import ProviderError - from lilbee.vision import PageText - - monkeypatch.setattr(cfg, "vision_model", "") - monkeypatch.setattr(cfg, "vision_ocr_concurrency", 1) - monkeypatch.setattr("lilbee.vision.pdf_page_count", lambda _p: 2) - monkeypatch.setattr( - "lilbee.vision.rasterize_pdf", lambda _p: iter([(0, b"png0"), (1, b"png1")]) - ) - - failed_once: list[bool] = [] - - def _vision(_client, _messages, _timeout) -> str: - if not failed_once: - failed_once.append(True) - raise ProviderError("Vision OCR timed out after 1s.", provider="llama-server") - return "page two" - - monkeypatch.setattr(prov_mod, "_vision_call", _vision) - p = _provider_with_clients({WorkerRole.VISION: [_fake_client(0)]}) - result = p.pdf_ocr(Path("doc.pdf"), backend="vision") # type: ignore[arg-type] - assert result == [PageText(1, ""), PageText(2, "page two")] - class TestReloadSingleFlight: def test_concurrent_reload_calls_dispatch_one_reload(self, monkeypatch) -> None: diff --git a/tests/test_formats.py b/tests/test_formats.py index d0fcd97f5..723d8204f 100644 --- a/tests/test_formats.py +++ b/tests/test_formats.py @@ -1,16 +1,16 @@ """Real-file format tests: full sync() pipeline with actual files on disk. -All document formats go through kreuzberg. Code files still use tree-sitter. -Embeddings are mocked (no live LLM server needed). kreuzberg is mocked for document -extraction since we're testing the pipeline, not kreuzberg itself. +All document formats go through xberg. Code files still use tree-sitter. +Embeddings are mocked (no live LLM server needed). xberg is mocked for document +extraction since we're testing the pipeline, not xberg itself. """ from __future__ import annotations from unittest import mock -from unittest.mock import Mock import pytest +from xberg import Metadata from lilbee.app.services import set_services from lilbee.core.config import cfg @@ -50,36 +50,40 @@ def mock_svc(): set_services(None) -def _make_kreuzberg_result(text="Extracted content. " * 10, num_chunks=1): - """Build a mock kreuzberg ExtractionResult.""" +def _make_xberg_result(text="Extracted content. " * 10, num_chunks=1): + """Build a mock xberg ExtractionResult.""" chunks = [] for i in range(num_chunks): chunk_text = text[i * len(text) // num_chunks : (i + 1) * len(text) // num_chunks] chunk = mock.MagicMock() chunk.content = chunk_text - chunk.metadata = { - "byte_start": 0, - "byte_end": len(chunk_text), - "chunk_index": i, - "total_chunks": num_chunks, - "token_count": None, - } + chunk.metadata = mock.MagicMock( + byte_start=0, + byte_end=len(chunk_text), + chunk_index=i, + total_chunks=num_chunks, + token_count=None, + first_page=None, + last_page=None, + ) chunks.append(chunk) result = mock.MagicMock() result.chunks = chunks result.content = text + result.pages = [] + result.metadata = Metadata() return result # --------------------------------------------------------------------------- -# Document formats (all go through kreuzberg) +# Document formats (all go through xberg) # --------------------------------------------------------------------------- @mock.patch( - "kreuzberg.extract_file_sync", - new_callable=Mock, - return_value=_make_kreuzberg_result(), + "lilbee.data.xberg_extract.aextract_document", + new_callable=mock.AsyncMock, + return_value=_make_xberg_result(), ) class TestSyncDocx: async def test_docx_discovered_and_ingested(self, mock_extract_file, isolated_env): @@ -91,9 +95,9 @@ async def test_docx_discovered_and_ingested(self, mock_extract_file, isolated_en @mock.patch( - "kreuzberg.extract_file_sync", - new_callable=Mock, - return_value=_make_kreuzberg_result(), + "lilbee.data.xberg_extract.aextract_document", + new_callable=mock.AsyncMock, + return_value=_make_xberg_result(), ) class TestSyncXlsx: async def test_xlsx_discovered_and_ingested(self, mock_extract_file, isolated_env): @@ -105,9 +109,9 @@ async def test_xlsx_discovered_and_ingested(self, mock_extract_file, isolated_en @mock.patch( - "kreuzberg.extract_file_sync", - new_callable=Mock, - return_value=_make_kreuzberg_result(), + "lilbee.data.xberg_extract.aextract_document", + new_callable=mock.AsyncMock, + return_value=_make_xberg_result(), ) class TestSyncPptx: async def test_pptx_discovered_and_ingested(self, mock_extract_file, isolated_env): @@ -124,9 +128,9 @@ async def test_pptx_discovered_and_ingested(self, mock_extract_file, isolated_en @mock.patch( - "kreuzberg.extract_file_sync", - new_callable=Mock, - return_value=_make_kreuzberg_result(), + "lilbee.data.xberg_extract.aextract_document", + new_callable=mock.AsyncMock, + return_value=_make_xberg_result(), ) class TestSyncEpub: async def test_epub_discovered_and_ingested(self, mock_extract_file, isolated_env): @@ -143,9 +147,9 @@ async def test_epub_discovered_and_ingested(self, mock_extract_file, isolated_en @mock.patch( - "kreuzberg.extract_file_sync", - new_callable=Mock, - return_value=_make_kreuzberg_result(), + "lilbee.data.xberg_extract.aextract_document", + new_callable=mock.AsyncMock, + return_value=_make_xberg_result(), ) class TestSyncImage: async def test_image_discovered_and_ingested(self, mock_extract_file, isolated_env): @@ -222,9 +226,9 @@ async def test_code_file_syncs(self, isolated_env, filename, fixture): @mock.patch( - "kreuzberg.extract_file_sync", - new_callable=Mock, - return_value=_make_kreuzberg_result(), + "lilbee.data.xberg_extract.aextract_document", + new_callable=mock.AsyncMock, + return_value=_make_xberg_result(), ) class TestSyncCsvTsv: async def test_csv_discovered_and_ingested(self, mock_extract_file, isolated_env): diff --git a/tests/test_ingest.py b/tests/test_ingest.py index b605d8633..125106c3d 100644 --- a/tests/test_ingest.py +++ b/tests/test_ingest.py @@ -4,12 +4,14 @@ import sys from pathlib import Path from unittest import mock -from unittest.mock import MagicMock, Mock +from unittest.mock import MagicMock import pytest +from xberg import Metadata import lilbee.app.services as svc_mod from lilbee.core.config import cfg +from tests.conftest import make_pdf @pytest.fixture(autouse=True) @@ -132,49 +134,62 @@ def _real_ingest_result(name, *, file_hash, page_text="page one of a.pdf", stat= ) -def _make_kreuzberg_result( +def _make_xberg_result( text="Some extracted text. " * 20, num_chunks=1, has_pages=False, document=None, + tables=None, + metadata=None, ): - """Build a mock kreuzberg ExtractionResult.""" + """Build a mock xberg ExtractionResult.""" chunks = [] for i in range(num_chunks): chunk_text = text[i * len(text) // num_chunks : (i + 1) * len(text) // num_chunks] - metadata = { - "byte_start": 0, - "byte_end": len(chunk_text), - "chunk_index": i, - "total_chunks": num_chunks, - "token_count": None, - } - if has_pages: - metadata["first_page"] = i + 1 - metadata["last_page"] = i + 1 chunk = mock.MagicMock() chunk.content = chunk_text - chunk.metadata = metadata + chunk.metadata = mock.MagicMock( + byte_start=0, + byte_end=len(chunk_text), + chunk_index=i, + total_chunks=num_chunks, + token_count=None, + first_page=(i + 1) if has_pages else None, + last_page=(i + 1) if has_pages else None, + ) chunks.append(chunk) result = mock.MagicMock() result.chunks = chunks result.content = text result.document = document + result.tables = tables if tables is not None else [] + result.metadata = metadata if metadata is not None else Metadata() result.pages = ( - [{"page_number": i + 1, "content": chunks[i].content} for i in range(num_chunks)] + [mock.MagicMock(page_number=i + 1, content=chunks[i].content) for i in range(num_chunks)] if has_pages else [] ) return result +def _make_table(markdown="| h1 | h2 |\n|---|---|\n| a | b |", page_number=1): + """Build a mock xberg Table carrying its markdown serialization.""" + table = mock.MagicMock() + table.markdown = markdown + table.page_number = page_number + return table + + def _make_empty_result(): - """Build a mock kreuzberg ExtractionResult with no chunks.""" + """Build a mock xberg ExtractionResult with no chunks.""" result = mock.MagicMock() result.chunks = [] result.content = "" result.document = None + result.tables = [] + result.pages = [] + result.metadata = Metadata() return result @@ -189,7 +204,11 @@ def test_reconcile_missing_flags_only_silent_drops(): assert missing == ["d.pdf"] -@mock.patch("kreuzberg.extract_file_sync", new_callable=Mock, return_value=_make_kreuzberg_result()) +@mock.patch( + "lilbee.data.xberg_extract.aextract_document", + new_callable=mock.AsyncMock, + return_value=_make_xberg_result(), +) class TestSync: async def test_empty_documents_dir(self, mock_extract_file, isolated_env): from lilbee.data.ingest import SyncResult, sync @@ -345,7 +364,7 @@ async def test_unchanged_file_skipped(self, mock_extract_file, isolated_env): assert result.added == [] async def test_unsupported_extension_skipped(self, mock_extract_file, isolated_env): - (isolated_env / "data.zip").write_bytes(b"binary data") + (isolated_env / "data.exe").write_bytes(b"binary data") from lilbee.data.ingest import sync assert (await sync()).added == [] @@ -441,7 +460,7 @@ async def test_midstream_flush_when_chunk_threshold_crossed( def test_flush_writes_marks_files_failed_on_write_error(self, _mock_extract_file): # A failed batch write moves every buffered file to ``failed`` and out of # added/updated, since none of its chunks persisted; the buffer still clears. - # ``_mock_extract_file`` is the class-level kreuzberg patch, unused here. + # ``_mock_extract_file`` is the class-level xberg patch, unused here. from lilbee.app.services import get_services from lilbee.data.ingest import pipeline from lilbee.data.ingest.types import _IngestResult @@ -746,7 +765,11 @@ async def _fail(path, name, ct, **kwargs): assert "qflaky.txt" not in result.updated -@mock.patch("kreuzberg.extract_file_sync", new_callable=Mock, return_value=_make_kreuzberg_result()) +@mock.patch( + "lilbee.data.xberg_extract.aextract_document", + new_callable=mock.AsyncMock, + return_value=_make_xberg_result(), +) class TestSyncCancellation: """Tests for cancel support and atomic per-file delete in sync.""" @@ -885,17 +908,45 @@ async def test_cancel_preserves_old_chunks_for_modified_file( mock_svc.store.write_chunks_batch.assert_not_called() +class TestOcrForceRequested: + """cfg-independent LILBEE_OCR_FORCE lever that OCRs every page, text layer or not.""" + + def test_false_when_env_unset(self, monkeypatch): + from lilbee.data.ingest.extract import _ocr_force_requested + + monkeypatch.delenv("LILBEE_OCR_FORCE", raising=False) + assert _ocr_force_requested() is False + + @pytest.mark.parametrize("value", ["1", "true", "YES"]) + def test_true_when_env_set(self, monkeypatch, value): + from lilbee.data.ingest.extract import _ocr_force_requested + + monkeypatch.setenv("LILBEE_OCR_FORCE", value) + assert _ocr_force_requested() is True + + @pytest.mark.parametrize("value", ["0", "false", "no", "", " "]) + def test_false_for_non_truthy_values(self, monkeypatch, value): + from lilbee.data.ingest.extract import _ocr_force_requested + + monkeypatch.setenv("LILBEE_OCR_FORCE", value) + assert _ocr_force_requested() is False + + class TestIngestHelpers: """Cover edge cases in ingest_document and ingest_code_sync.""" - @mock.patch("kreuzberg.extract_file_sync", new_callable=Mock, return_value=_make_empty_result()) + @mock.patch( + "lilbee.data.xberg_extract.aextract_document", + new_callable=mock.AsyncMock, + return_value=_make_empty_result(), + ) async def testingest_document_empty_chunks(self, mock_extract_file, isolated_env): """Document that produces no chunks returns empty list.""" from lilbee.data.ingest import ingest_document f = isolated_env / "empty.txt" f.write_text(" ") - result = await ingest_document(f, "empty.txt", "text") + result, _ = await ingest_document(f, "empty.txt", "text") assert result == [] async def test_ingest_code_empty_chunks(self, isolated_env): @@ -942,10 +993,10 @@ class _FakeResult: assert "# File: pkg/mod.py" in joined assert str(f) not in joined # absolute path must never leak into content - @mock.patch("kreuzberg.extract_file_sync", new_callable=Mock) + @mock.patch("lilbee.data.xberg_extract.aextract_document", new_callable=mock.AsyncMock) async def testingest_document_pdf_with_pages(self, mock_kf, isolated_env): """PDF document returns records with page metadata.""" - mock_kf.return_value = _make_kreuzberg_result( + mock_kf.return_value = _make_xberg_result( text="Page 1 content. " * 10 + "Page 2 content. " * 10, num_chunks=2, has_pages=True, @@ -954,7 +1005,7 @@ async def testingest_document_pdf_with_pages(self, mock_kf, isolated_env): f = isolated_env / "test.pdf" f.write_bytes(b"fake") - result = await ingest_document(f, "test.pdf", "pdf") + result, _ = await ingest_document(f, "test.pdf", "pdf") assert len(result) == 2 assert result[0]["page_start"] == 1 assert result[1]["page_start"] == 2 @@ -962,7 +1013,9 @@ async def testingest_document_pdf_with_pages(self, mock_kf, isolated_env): class TestCancellation: @mock.patch( - "kreuzberg.extract_file_sync", new_callable=Mock, return_value=_make_kreuzberg_result() + "lilbee.data.xberg_extract.aextract_document", + new_callable=mock.AsyncMock, + return_value=_make_xberg_result(), ) async def test_cancelled_error_propagates(self, mock_extract_file, isolated_env): """CancelledError in _process_one is re-raised, not swallowed.""" @@ -983,7 +1036,9 @@ async def _cancel(*args, **kwargs): await ingest_batch([entry], added, {}, {}, {}, quiet=True) @mock.patch( - "kreuzberg.extract_file_sync", new_callable=Mock, return_value=_make_kreuzberg_result() + "lilbee.data.xberg_extract.aextract_document", + new_callable=mock.AsyncMock, + return_value=_make_xberg_result(), ) async def test_task_cancelled_error_does_not_orphan_siblings( self, mock_extract_file, isolated_env @@ -1040,10 +1095,12 @@ class TestSkipMarkerLifecycle: until the file changes or retry_skipped / force_rebuild clears the marker.""" @staticmethod - def _zero_chunks(*_args, **_kwargs) -> list: + def _zero_chunks(*_args, **_kwargs): # Simulate "OCR found no usable text": no records produced, so the file # is recorded as skipped. - return [] + from lilbee.data.store import SourceMeta + + return [], SourceMeta() async def test_failed_file_is_skipped_on_next_sync(self, isolated_env, mock_svc): from lilbee.data.ingest import sync @@ -1090,13 +1147,21 @@ class TestZeroChunkPageTextPersistence: @staticmethod async def _pages_no_chunks( - path, source_name, content_type, *, quiet=False, on_progress=None, page_texts_out=None + path, + source_name, + content_type, + *, + quiet=False, + on_progress=None, + page_texts_out=None, ): + from lilbee.data.store import SourceMeta + if page_texts_out is not None: page_texts_out.append( {"source": source_name, "page": 1, "text": " ", "content_type": "pdf"} ) - return [] + return [], SourceMeta() async def test_pages_and_source_row_persist_and_replan_stops(self, isolated_env, mock_svc): from lilbee.data.ingest import sync @@ -1379,7 +1444,9 @@ def _spy_plan(*args, **kwargs): monkeypatch.setattr(pipeline, "_plan_file_changes", _spy_plan) with mock.patch( - "kreuzberg.extract_file_sync", new_callable=Mock, return_value=_make_kreuzberg_result() + "lilbee.data.xberg_extract.aextract_document", + new_callable=mock.AsyncMock, + return_value=_make_xberg_result(), ): await sync(quiet=True) assert plan_threads @@ -1394,7 +1461,9 @@ async def test_sync_backfills_stats_via_store(self, isolated_env, mock_svc): mock_svc.store.upsert_source("legacy.txt", file_hash(f), 1, source_type="document") with mock.patch( - "kreuzberg.extract_file_sync", new_callable=Mock, return_value=_make_kreuzberg_result() + "lilbee.data.xberg_extract.aextract_document", + new_callable=mock.AsyncMock, + return_value=_make_xberg_result(), ): result = await sync(quiet=True) assert result.unchanged == 1 @@ -1478,14 +1547,14 @@ class TestClassifyFile: "filename, expected", [ ("doc.pdf", "pdf"), - ("f.md", "text"), - ("f.txt", "text"), - ("f.html", "text"), - ("f.rst", "text"), + ("f.md", "md"), + ("f.txt", "txt"), + ("f.html", "html"), + ("f.rst", "rst"), ("f.py", "code"), ("f.js", "code"), ("f.go", "code"), - ("f.zip", None), + ("f.zip", "zip"), ("f.exe", None), ], ) @@ -1901,8 +1970,8 @@ class TestClassifyNewFormats: ("scan.tif", "image"), ("img.bmp", "image"), ("img.webp", "image"), - ("data.csv", "data"), - ("data.tsv", "data"), + ("data.csv", "csv"), + ("data.tsv", "tsv"), ], ) def test_classify(self, filename, expected): @@ -1969,7 +2038,7 @@ def test_paginated_has_page_config(self): from lilbee.data.ingest import ExtractMode, extraction_config config = extraction_config(ExtractMode.PAGINATED) - assert config.pages is not None + assert config.get("pages") is not None def test_paginated_no_markdown_output(self): from lilbee.data.ingest import ExtractMode, extraction_config @@ -1981,27 +2050,39 @@ def test_markdown_has_no_page_config(self): from lilbee.data.ingest import ExtractMode, extraction_config config = extraction_config(ExtractMode.MARKDOWN) - assert config.pages is None + assert config.get("pages") is None def test_markdown_has_chunking(self): from lilbee.data.ingest import ExtractMode, extraction_config config = extraction_config(ExtractMode.MARKDOWN) - assert config.chunking is not None + assert config.get("chunking") is not None def test_markdown_sets_output_format(self): from lilbee.data.ingest import ExtractMode, extraction_config config = extraction_config(ExtractMode.MARKDOWN) - assert config.output_format == "markdown" + assert config["output_format"] == "markdown" + + def test_paginated_has_tesseract_ocr_backend(self): + from lilbee.data.ingest import ExtractMode, extraction_config - def test_paginated_ocr_has_tesseract(self): + config = extraction_config(ExtractMode.PAGINATED) + assert config.get("pages") is not None + assert config.get("ocr") is not None + # No vision model configured -> xberg's tesseract backend OCRs scanned pages. + assert config["ocr"].backend == "tesseract" + # xberg 5.x errors on image OCR with an empty language list; lilbee must + # set an explicit default to preserve 4.x behavior (regression guard). + assert config["ocr"].language == ["eng"] + + def test_tesseract_ocr_language_from_config(self, monkeypatch): + from lilbee.core.config import cfg from lilbee.data.ingest import ExtractMode, extraction_config - config = extraction_config(ExtractMode.PAGINATED_OCR) - assert config.pages is not None - assert config.ocr is not None - assert config.ocr.backend == "tesseract" + monkeypatch.setattr(cfg, "ocr_language", ["deu", "fra"]) + config = extraction_config(ExtractMode.PAGINATED) + assert config["ocr"].language == ["deu", "fra"] @pytest.mark.parametrize( "content_type, expected_mode_name", @@ -2012,7 +2093,7 @@ def test_paginated_ocr_has_tesseract(self): ("xlsx", "MARKDOWN"), ("pptx", "MARKDOWN"), ("epub", "MARKDOWN"), - ("image", "MARKDOWN"), + ("image", "PAGINATED"), ("code", "MARKDOWN"), ], ) @@ -2030,12 +2111,192 @@ def test_topic_threshold_propagates_to_every_mode(self, monkeypatch): monkeypatch.setattr(cfg, "topic_threshold", 0.42) for mode in ExtractMode: config = extraction_config(mode) - assert config.chunking.chunker_type == "semantic" - assert config.chunking.topic_threshold == pytest.approx(0.42, abs=1e-5) + assert config["chunking"].chunker_type == "semantic" + assert config["chunking"].topic_threshold == pytest.approx(0.42, abs=1e-5) + + def test_table_extraction_off_sets_no_pdf_options(self): + """Default config leaves pdf_options unset: exact pre-feature behavior.""" + from lilbee.data.ingest import ExtractMode, extraction_config + + config = extraction_config(ExtractMode.PAGINATED) + assert config.get("pdf_options") is None + + def test_table_extraction_sets_pdf_options_and_table_chunking(self, monkeypatch): + """The flag turns on xberg table recognition and header-repeating splits.""" + from xberg import TableChunkingMode + + from lilbee.core.config import cfg + from lilbee.data.ingest import ExtractMode, extraction_config + + monkeypatch.setattr(cfg, "table_extraction", True) + config = extraction_config(ExtractMode.PAGINATED) + assert config["pdf_options"].extract_tables is True + assert config["chunking"].table_chunking == TableChunkingMode.REPEAT_HEADER + + def test_table_extraction_markdown_mode_skips_pdf_options(self, monkeypatch): + """Non-paginated formats have no PdfConfig but still chunk tables with headers.""" + from xberg import TableChunkingMode + + from lilbee.core.config import cfg + from lilbee.data.ingest import ExtractMode, extraction_config + + monkeypatch.setattr(cfg, "table_extraction", True) + config = extraction_config(ExtractMode.MARKDOWN) + assert config.get("pdf_options") is None + assert config["chunking"].table_chunking == TableChunkingMode.REPEAT_HEADER + + def test_layout_detection_off_sets_no_layout(self): + """Default config leaves layout detection unset: exact pre-feature behavior.""" + from lilbee.data.ingest import ExtractMode, extraction_config + + config = extraction_config(ExtractMode.PAGINATED) + assert config.get("layout") is None + assert config.get("pdf_options") is None + + def test_layout_detection_sets_layout_and_reading_order(self, monkeypatch): + """The flag enables layout detection, reading order, and margin stripping.""" + from lilbee.core.config import cfg + from lilbee.data.ingest import ExtractMode, extraction_config + + monkeypatch.setattr(cfg, "layout_detection", True) + config = extraction_config(ExtractMode.PAGINATED) + assert config["layout"] is not None + assert config["use_layout_for_markdown"] is True + pdf = config["pdf_options"] + assert pdf.reading_order is True + assert pdf.top_margin_fraction == pytest.approx(0.05) + assert pdf.bottom_margin_fraction == pytest.approx(0.05) + # extract_tables stays at xberg's default (True); table INDEXING is + # gated separately by cfg.table_extraction in _document_tables. + assert pdf.extract_tables is True + + def test_layout_detection_markdown_mode_unchanged(self, monkeypatch): + """Layout detection is visual-format work; non-paginated formats skip it.""" + from lilbee.core.config import cfg + from lilbee.data.ingest import ExtractMode, extraction_config + + monkeypatch.setattr(cfg, "layout_detection", True) + config = extraction_config(ExtractMode.MARKDOWN) + assert config.get("layout") is None + assert config.get("pdf_options") is None + + def test_layout_and_tables_combine_in_one_pdf_config(self, monkeypatch): + """Both flags land in the same PdfConfig.""" + from lilbee.core.config import cfg + from lilbee.data.ingest import ExtractMode, extraction_config + + monkeypatch.setattr(cfg, "table_extraction", True) + monkeypatch.setattr(cfg, "layout_detection", True) + config = extraction_config(ExtractMode.PAGINATED) + pdf = config["pdf_options"] + assert pdf.extract_tables is True + assert pdf.reading_order is True + + +class TestTableChunks: + """Table indexing in ingest_document, driven by cfg.table_extraction.""" + @mock.patch("lilbee.data.xberg_extract.aextract_document", new_callable=mock.AsyncMock) + async def test_config_off_ignores_tables(self, mock_kf, isolated_env): + """With the flag off, result tables are not indexed: current behavior.""" + mock_kf.return_value = _make_xberg_result(tables=[_make_table()]) + from lilbee.data.ingest import ingest_document + from lilbee.data.store import ChunkType + + f = isolated_env / "test.pdf" + f.write_bytes(b"fake") + records, _ = await ingest_document(f, "test.pdf", "pdf") + assert len(records) == 1 + assert all(r["chunk_type"] == ChunkType.RAW for r in records) + + @mock.patch("lilbee.data.xberg_extract.aextract_document", new_callable=mock.AsyncMock) + async def test_tables_indexed_as_table_chunks(self, mock_kf, isolated_env, monkeypatch): + """Each table becomes its own chunk: markdown text, page span, TABLE type.""" + from lilbee.core.config import cfg + + monkeypatch.setattr(cfg, "table_extraction", True) + tables = [ + _make_table(markdown="| a | b |\n|---|---|\n| 1 | 2 |", page_number=2), + _make_table(markdown="| x |\n|---|\n| y |", page_number=5), + ] + mock_kf.return_value = _make_xberg_result(tables=tables) + from lilbee.data.ingest import ingest_document + from lilbee.data.store import ChunkType + + f = isolated_env / "test.pdf" + f.write_bytes(b"fake") + records, _ = await ingest_document(f, "test.pdf", "pdf") + assert len(records) == 3 + table_records = [r for r in records if r["chunk_type"] == ChunkType.TABLE] + assert len(table_records) == 2 + assert table_records[0]["chunk"] == tables[0].markdown + assert table_records[0]["page_start"] == 2 + assert table_records[0]["page_end"] == 2 + assert table_records[1]["page_start"] == 5 + # Indices continue after the content chunks so (source, chunk_index) stays unique. + assert [r["chunk_index"] for r in records] == [0, 1, 2] + assert all(r["vector"] for r in table_records) + assert all(r["content_type"] == "pdf" for r in table_records) + + @mock.patch("lilbee.data.xberg_extract.aextract_document", new_callable=mock.AsyncMock) + async def test_spanning_cell_markdown_passes_through_verbatim( + self, mock_kf, isolated_env, monkeypatch + ): + """xberg's serialization of spanning cells is indexed exactly as produced.""" + from lilbee.core.config import cfg + + monkeypatch.setattr(cfg, "table_extraction", True) + spanned = "| h1 | h1 | h2 |\n|---|---|---|\n| merged | merged | v |" + mock_kf.return_value = _make_xberg_result(tables=[_make_table(markdown=spanned)]) + from lilbee.data.ingest import ingest_document + from lilbee.data.store import ChunkType + + f = isolated_env / "test.pdf" + f.write_bytes(b"fake") + records, _ = await ingest_document(f, "test.pdf", "pdf") + assert records[-1]["chunk_type"] == ChunkType.TABLE + assert records[-1]["chunk"] == spanned + + @mock.patch("lilbee.data.xberg_extract.aextract_document", new_callable=mock.AsyncMock) + async def test_blank_table_markdown_skipped(self, mock_kf, isolated_env, monkeypatch): + """A table with no usable serialization contributes no chunk.""" + from lilbee.core.config import cfg + + monkeypatch.setattr(cfg, "table_extraction", True) + mock_kf.return_value = _make_xberg_result(tables=[_make_table(markdown=" ")]) + from lilbee.data.ingest import ingest_document + from lilbee.data.store import ChunkType -class TestClassifyKreuzbergParityFormats: - """Formats kreuzberg extracts that the map must not silently drop.""" + f = isolated_env / "test.pdf" + f.write_bytes(b"fake") + records, _ = await ingest_document(f, "test.pdf", "pdf") + assert len(records) == 1 + assert records[0]["chunk_type"] == ChunkType.RAW + + @mock.patch("lilbee.data.xberg_extract.aextract_document", new_callable=mock.AsyncMock) + async def test_tables_without_text_chunks_still_indexed( + self, mock_kf, isolated_env, monkeypatch + ): + """A document whose only content is tables is not dropped as empty.""" + from lilbee.core.config import cfg + + monkeypatch.setattr(cfg, "table_extraction", True) + doc = _make_empty_result() + doc.tables = [_make_table(page_number=1)] + mock_kf.return_value = doc + from lilbee.data.ingest import ingest_document + from lilbee.data.store import ChunkType + + f = isolated_env / "test.pdf" + f.write_bytes(b"fake") + records, _ = await ingest_document(f, "test.pdf", "pdf") + assert len(records) == 1 + assert records[0]["chunk_type"] == ChunkType.TABLE + assert records[0]["chunk_index"] == 0 + + +class TestClassifyXbergParityFormats: + """Formats xberg extracts that the map must not silently drop.""" @pytest.mark.parametrize( "filename, expected", @@ -2045,7 +2306,7 @@ class TestClassifyKreuzbergParityFormats: ("scan.j2k", "image"), ("scan.j2c", "image"), ("scan.jpx", "image"), - ("page.htm", "text"), + ("page.htm", "htm"), ("memo.doc", "doc"), ("deck.ppt", "ppt"), ("ledger.xls", "xls"), @@ -2054,11 +2315,10 @@ class TestClassifyKreuzbergParityFormats: ("ledger.ods", "ods"), ("mail.eml", "eml"), ("mail.msg", "msg"), - ("table.dbf", "data"), - # Containers stay unsupported on purpose: one file fans out to many - # inner documents, which the source/citation model can't express yet. - ("archive.pst", None), - ("archive.7z", None), + ("table.dbf", "dbf"), + # Containers are supported too (xberg extracts their combined text). + ("archive.pst", "pst"), + ("archive.7z", "7z"), ], ) def test_classify(self, filename, expected): @@ -2073,10 +2333,10 @@ class TestClassifyStructuredFormats: [ ("data.xml", "xml"), ("data.json", "json"), - ("data.jsonl", "json"), - ("config.yaml", "text"), - ("config.yml", "text"), - ("data.csv", "data"), + ("data.jsonl", "jsonl"), + ("config.yaml", "yaml"), + ("config.yml", "yml"), + ("data.csv", "csv"), ], ) def test_classify(self, filename, expected): @@ -2085,7 +2345,11 @@ def test_classify(self, filename, expected): assert classify_file(Path(filename)) == expected -@mock.patch("kreuzberg.extract_file_sync", new_callable=Mock, return_value=_make_kreuzberg_result()) +@mock.patch( + "lilbee.data.xberg_extract.aextract_document", + new_callable=mock.AsyncMock, + return_value=_make_xberg_result(), +) class TestSyncStructuredFormats: async def test_xml_file_ingested( self, @@ -2132,436 +2396,6 @@ async def test_csv_file_ingested( assert "data.csv" in result.added -class TestHasMeaningfulText: - def test_empty_content_and_chunks_returns_false(self): - from lilbee.data.ingest.extract import _has_meaningful_text - - result = mock.MagicMock(content="", chunks=[]) - assert _has_meaningful_text(result) is False - - def test_short_text_returns_false(self): - from lilbee.data.ingest.extract import _has_meaningful_text - - chunk = mock.MagicMock() - chunk.content = "short" - result = mock.MagicMock(content="short", chunks=[chunk]) - assert _has_meaningful_text(result) is False - - def test_meaningful_chunks_returns_true(self): - from lilbee.data.ingest.extract import _has_meaningful_text - - chunk = mock.MagicMock() - chunk.content = "A" * 100 - result = mock.MagicMock(content="", chunks=[chunk]) - assert _has_meaningful_text(result) is True - - def test_content_rich_empty_chunks_returns_true(self): - """A text PDF whose extractor populated content but no chunks must - take the text path, not vision OCR (bb-4u58).""" - from lilbee.data.ingest.extract import _has_meaningful_text - - result = mock.MagicMock(content="A" * 100, chunks=[]) - assert _has_meaningful_text(result) is True - - -def _vision_pages(*pairs): - """Build PageText-like NamedTuples for stubbing provider.pdf_ocr.""" - from lilbee.vision import PageText - - return [PageText(page, text) for page, text in pairs] - - -class TestVisionFallback: - """OCR fallback dispatches to ``provider.pdf_ocr`` (the persistent worker).""" - - @mock.patch("kreuzberg.extract_file_sync", new_callable=Mock) - async def test_vision_fallback_called_for_empty_pdf(self, mock_kf, isolated_env, mock_svc): - cfg.vision_model = "org/Test-Vision-GGUF/test-vision-Q4_K_M.gguf" - cfg.ocr_timeout = 45.0 - cfg.enable_ocr = True - mock_kf.return_value = _make_empty_result() - mock_svc.provider.pdf_ocr.return_value = _vision_pages((1, "Vision extracted text. " * 10)) - - f = isolated_env / "scanned.pdf" - f.write_bytes(b"fake pdf") - - from lilbee.data.ingest import ingest_document - - result = await ingest_document(f, "scanned.pdf", "pdf", quiet=True) - mock_svc.provider.pdf_ocr.assert_called_once_with( - f, - backend="vision", - model="org/Test-Vision-GGUF/test-vision-Q4_K_M.gguf", - per_page_timeout_s=45.0, - quiet=True, - on_progress=mock.ANY, - ) - assert len(result) > 0 - assert result[0]["content_type"] == "pdf" - assert result[0]["page_start"] == 1 - - @mock.patch("kreuzberg.extract_file_sync", new_callable=Mock) - async def test_vision_fallback_quiet_false_by_default(self, mock_kf, isolated_env, mock_svc): - cfg.vision_model = "org/Test-Vision-GGUF/test-vision-Q4_K_M.gguf" - cfg.ocr_timeout = 120.0 - cfg.enable_ocr = True - mock_kf.return_value = _make_empty_result() - mock_svc.provider.pdf_ocr.return_value = _vision_pages((1, "Vision extracted text. " * 10)) - - f = isolated_env / "scanned.pdf" - f.write_bytes(b"fake pdf") - - from lilbee.data.ingest import ingest_document - - await ingest_document(f, "scanned.pdf", "pdf") - mock_svc.provider.pdf_ocr.assert_called_once_with( - f, - backend="vision", - model="org/Test-Vision-GGUF/test-vision-Q4_K_M.gguf", - per_page_timeout_s=120.0, - quiet=False, - on_progress=mock.ANY, - ) - - @mock.patch("kreuzberg.extract_file_sync", new_callable=Mock) - async def test_ocr_disabled_skips_both_backends(self, mock_kf, isolated_env, mock_svc): - """``cfg.enable_ocr=False`` disables OCR entirely (bb-ziks.20): neither the - vision pool nor the Tesseract fallback runs. Only the initial text-layer - extract is attempted; finding none, the file is skipped without paying the - Tesseract cost the config says is turned off.""" - mock_kf.return_value = _make_empty_result() - cfg.enable_ocr = False - f = isolated_env / "scanned.pdf" - f.write_bytes(b"fake pdf") - - from lilbee.data.ingest import ingest_document - - with mock.patch("lilbee.data.ingest.extract._run_tesseract_sync") as mock_tess: - result = await ingest_document(f, "scanned.pdf", "pdf") - mock_svc.provider.pdf_ocr.assert_not_called() - mock_tess.assert_not_called() - # Only the initial text-layer extract ran; no Tesseract re-extract. - assert mock_kf.call_count == 1 - assert result == [] - - @mock.patch("kreuzberg.extract_file_sync", new_callable=Mock) - async def test_vision_fallback_not_called_for_non_pdf(self, mock_kf, isolated_env, mock_svc): - mock_kf.return_value = _make_empty_result() - cfg.enable_ocr = True - f = isolated_env / "doc.txt" - f.write_text("") - - from lilbee.data.ingest import ingest_document - - result = await ingest_document(f, "doc.txt", "text") - mock_svc.provider.pdf_ocr.assert_not_called() - assert result == [] - - @mock.patch("kreuzberg.extract_file_sync", new_callable=Mock) - async def test_vision_fallback_empty_vision_text_returns_empty( - self, mock_kf, isolated_env, mock_svc - ): - cfg.enable_ocr = True - cfg.vision_model = "org/Test-Vision-GGUF/test-vision-Q4_K_M.gguf" - mock_kf.return_value = _make_empty_result() - mock_svc.provider.pdf_ocr.return_value = [] - - f = isolated_env / "blank.pdf" - f.write_bytes(b"fake pdf") - - from lilbee.data.ingest import ingest_document - - result = await ingest_document(f, "blank.pdf", "pdf") - assert result == [] - - @mock.patch("kreuzberg.extract_file_sync", new_callable=Mock) - async def test_no_vision_fallback_when_text_meaningful(self, mock_kf, isolated_env, mock_svc): - mock_kf.return_value = _make_kreuzberg_result( - text="Meaningful PDF content. " * 20, num_chunks=1, has_pages=True - ) - cfg.enable_ocr = True - f = isolated_env / "good.pdf" - f.write_bytes(b"fake pdf") - - from lilbee.data.ingest import ingest_document - - result = await ingest_document(f, "good.pdf", "pdf") - mock_svc.provider.pdf_ocr.assert_not_called() - assert len(result) > 0 - - @mock.patch("kreuzberg.extract_file_sync", new_callable=Mock) - async def test_vision_fallback_no_chunks_returns_empty(self, mock_kf, isolated_env, mock_svc): - cfg.enable_ocr = True - cfg.vision_model = "org/Test-Vision-GGUF/test-vision-Q4_K_M.gguf" - mock_kf.return_value = _make_empty_result() - mock_svc.provider.pdf_ocr.return_value = _vision_pages((1, "Some text")) - - f = isolated_env / "nochunks.pdf" - f.write_bytes(b"fake pdf") - - with mock.patch("lilbee.data.ingest.extract.chunk_text", return_value=[]): - from lilbee.data.ingest import ingest_document - - result = await ingest_document(f, "nochunks.pdf", "pdf") - assert result == [] - - @mock.patch("kreuzberg.extract_file_sync", new_callable=Mock) - async def test_vision_fallback_bypasses_semantic_chunking( - self, mock_kf, isolated_env, mock_svc - ): - """Per-page OCR text gets ``chunk_text(use_semantic=False)`` to skip the - embedding round-trip per single-topic page.""" - cfg.enable_ocr = True - cfg.vision_model = "org/Test-Vision-GGUF/test-vision-Q4_K_M.gguf" - mock_kf.return_value = _make_empty_result() - mock_svc.provider.pdf_ocr.return_value = _vision_pages( - (1, "page one text"), (2, "page two text") - ) - - f = isolated_env / "bypass.pdf" - f.write_bytes(b"fake pdf") - - with mock.patch( - "lilbee.data.ingest.extract.chunk_text", return_value=["chunk"] - ) as mock_chunk: - from lilbee.data.ingest import ingest_document - - await ingest_document(f, "bypass.pdf", "pdf") - - assert mock_chunk.call_count == 2, "chunk_text called once per OCR page" - for call in mock_chunk.call_args_list: - assert call.kwargs.get("use_semantic") is False - - @mock.patch("kreuzberg.extract_file_sync", new_callable=Mock) - async def test_vision_cancel_propagates_not_swallowed(self, mock_kf, isolated_env, mock_svc): - """A user cancel raised through the per-page on_progress (TaskCancelledError) - must abort the file, not be logged as an OCR failure and swallowed as [].""" - from lilbee.runtime.cancellation import TaskCancelledError - - cfg.enable_ocr = True - cfg.vision_model = "org/Test-Vision-GGUF/test-vision-Q4_K_M.gguf" - mock_kf.return_value = _make_empty_result() - mock_svc.provider.pdf_ocr.side_effect = TaskCancelledError - - f = isolated_env / "cancel.pdf" - f.write_bytes(b"fake pdf") - - from lilbee.data.ingest import ingest_document - - with pytest.raises(TaskCancelledError): - await ingest_document(f, "cancel.pdf", "pdf", quiet=True) - - -def _write_png(path: Path) -> None: - """Write a tiny valid PNG so the image OCR path can open + re-encode it.""" - from PIL import Image - - Image.new("RGB", (8, 8), color=(255, 255, 255)).save(path, format="PNG") - - -class TestImageOcr: - """bb-657: image content_type routes through OCR (vision when configured, else Tesseract).""" - - async def test_image_routed_to_vision_ocr(self, isolated_env, mock_svc): - cfg.vision_model = "org/Test-Vision-GGUF/test-vision-Q4_K_M.gguf" - cfg.enable_ocr = True - cfg.ocr_timeout = 30.0 - mock_svc.provider.vision_ocr.return_value = "Scanned image text. " * 20 - - f = isolated_env / "scan.png" - _write_png(f) - - from lilbee.data.ingest import ingest_document - - result = await ingest_document(f, "scan.png", "image") - # the single-image path is used, not the PDF page loop - mock_svc.provider.vision_ocr.assert_called_once() - mock_svc.provider.pdf_ocr.assert_not_called() - assert len(result) > 0 - assert result[0]["content_type"] == "image" - assert result[0]["page_start"] == 1 - - async def test_image_skips_kreuzberg_markdown_extract(self, isolated_env, mock_svc): - # The pre-fix bug: an image went through a markdown extract that yields no - # text. The image branch must not call kreuzberg.extract_file_sync at all. - cfg.vision_model = "org/Test-Vision-GGUF/test-vision-Q4_K_M.gguf" - cfg.enable_ocr = True - mock_svc.provider.vision_ocr.return_value = "text " * 20 - f = isolated_env / "scan.png" - _write_png(f) - - from lilbee.data.ingest import ingest_document - - with mock.patch("kreuzberg.extract_file_sync", new_callable=Mock) as mock_kf: - await ingest_document(f, "scan.png", "image") - mock_kf.assert_not_called() - - async def test_image_falls_back_to_tesseract_without_vision_model(self, isolated_env, mock_svc): - cfg.vision_model = "" - cfg.enable_ocr = None # auto: no vision model -> Tesseract - f = isolated_env / "scan.png" - _write_png(f) - - from lilbee.data.ingest import ingest_document - - with mock.patch("lilbee.data.ingest.extract._run_tesseract_sync") as mock_tess: - mock_tess.return_value = _make_kreuzberg_result("Tesseract image text. " * 20) - result = await ingest_document(f, "scan.png", "image") - mock_svc.provider.vision_ocr.assert_not_called() - assert len(result) > 0 - assert result[0]["content_type"] == "image" - - async def test_image_vision_ocr_failure_fails_file(self, isolated_env, mock_svc): - """A vision-backend error is a per-file failure, not an empty document. - - Swallowing it into [] would skip-mark the file under its current hash - and silently drop it from search until retry_skipped. - """ - cfg.vision_model = "org/Test-Vision-GGUF/test-vision-Q4_K_M.gguf" - cfg.enable_ocr = True - mock_svc.provider.vision_ocr.side_effect = RuntimeError("vision down") - f = isolated_env / "scan.png" - _write_png(f) - - from lilbee.data.ingest import ingest_document - - with pytest.raises(RuntimeError, match="vision down"): - await ingest_document(f, "scan.png", "image") - - async def test_image_ocr_disabled_skips_both_backends(self, isolated_env, mock_svc): - """enable_ocr=False disables OCR entirely for images (bb-ziks.20): neither - the vision pool nor the Tesseract fallback runs.""" - cfg.enable_ocr = False - cfg.vision_model = "org/Test-Vision-GGUF/test-vision-Q4_K_M.gguf" - f = isolated_env / "scan.png" - _write_png(f) - - from lilbee.data.ingest import ingest_document - - with mock.patch("lilbee.data.ingest.extract._run_tesseract_sync") as mock_tess: - result = await ingest_document(f, "scan.png", "image") - assert result == [] - mock_svc.provider.vision_ocr.assert_not_called() - mock_tess.assert_not_called() - - async def test_vision_ocr_cache_key_includes_timeout(self, isolated_env, mock_svc, monkeypatch): - """The vision OCR cache key carries the per-page timeout so raising it - re-OCRs rather than serving an earlier partially-empty result for the same - content (bb-ziks.39). Also exercises the cache-hit reuse path.""" - from lilbee.data.ingest import extract - from lilbee.runtime.progress import noop_callback - - cfg.enable_ocr = True - cfg.vision_model = "org/Test-Vision-GGUF/test-vision-Q4_K_M.gguf" - cfg.ocr_timeout = 77.0 - captured: dict[str, str] = {} - - def fake_key(file_h, *, backend, model, extra): - captured["extra"] = extra - return "cache-key" - - monkeypatch.setattr(extract, "ocr_cache_key", fake_key) - monkeypatch.setattr(extract, "load_ocr_pages", lambda key: [(1, "cached page text")]) - f = isolated_env / "scan.png" - _write_png(f) - - result = await extract._vision_image_ocr(f, "scan.png", "image", on_progress=noop_callback) - assert "77" in captured["extra"] - assert result # cache hit produced chunks from the cached page, no re-OCR - mock_svc.provider.vision_ocr.assert_not_called() - - async def test_image_tesseract_empty_skips_file(self, isolated_env, mock_svc): - cfg.vision_model = "" - cfg.enable_ocr = None - f = isolated_env / "scan.png" - _write_png(f) - - from lilbee.data.ingest import ingest_document - - with mock.patch("lilbee.data.ingest.extract._run_tesseract_sync") as mock_tess: - mock_tess.return_value = _make_empty_result() - assert await ingest_document(f, "scan.png", "image") == [] - - async def test_multipage_image_ocrs_every_frame_end_to_end(self, isolated_env, mock_svc): - """A multi-frame TIFF routed to vision OCR yields one OCR call and one page - record per frame (bb-7jg1.10), not just the first frame.""" - cfg.enable_ocr = True - cfg.vision_model = "org/Test-Vision-GGUF/test-vision-Q4_K_M.gguf" - mock_svc.provider.vision_ocr.return_value = "frame text " * 5 - from PIL import Image - - f = isolated_env / "multi.tiff" - frames = [Image.new("RGB", (4, 4), color=c) for c in [(1, 2, 3), (4, 5, 6), (7, 8, 9)]] - frames[0].save(f, format="TIFF", save_all=True, append_images=frames[1:]) - - from lilbee.data.ingest import ingest_document - - records = await ingest_document(f, "multi.tiff", "image") - assert mock_svc.provider.vision_ocr.call_count == 3 - assert sorted({r["page_start"] for r in records}) == [1, 2, 3] - - def test_image_page_pngs_single_frame_reencodes(self, isolated_env): - from lilbee.data.ingest.extract import _image_page_pngs - - f = isolated_env / "x.bmp" - from PIL import Image - - Image.new("RGB", (4, 4), color=(1, 2, 3)).save(f, format="BMP") - pages = _image_page_pngs(f) - assert len(pages) == 1 - assert pages[0].startswith(b"\x89PNG\r\n") - - def test_image_page_pngs_multipage_tiff_yields_all_frames(self, isolated_env): - """A multi-frame TIFF produces one PNG page per frame; the prior code - dropped every frame after the first (bb-7jg1.10).""" - from lilbee.data.ingest.extract import _image_page_pngs - - f = isolated_env / "multi.tiff" - from PIL import Image - - frame0 = Image.new("RGB", (4, 4), color=(1, 2, 3)) - frame1 = Image.new("RGB", (4, 4), color=(4, 5, 6)) - frame2 = Image.new("RGB", (4, 4), color=(7, 8, 9)) - frame0.save(f, format="TIFF", save_all=True, append_images=[frame1, frame2]) - pages = _image_page_pngs(f) - assert len(pages) == 3 - assert all(p.startswith(b"\x89PNG\r\n") for p in pages) - - -class TestShouldRunOcrAutoDetect: - def test_auto_detect_vision_model_set(self, isolated_env): - """When enable_ocr is None, _should_run_ocr reflects cfg.vision_model.""" - cfg.enable_ocr = None - cfg.vision_model = "noctrex/LightOnOCR-2-1B-GGUF/lightonocr-Q4_K_M.gguf" - from lilbee.data.ingest.extract import _should_run_ocr - - assert _should_run_ocr() is True - - def test_auto_detect_vision_model_empty(self, isolated_env): - """When enable_ocr is None and cfg.vision_model is empty, returns False.""" - cfg.enable_ocr = None - cfg.vision_model = "" - from lilbee.data.ingest.extract import _should_run_ocr - - assert _should_run_ocr() is False - - def test_force_on_with_empty_vision_model(self, isolated_env): - """enable_ocr=True still returns True; caller may fall back to Tesseract.""" - cfg.enable_ocr = True - cfg.vision_model = "" - from lilbee.data.ingest.extract import _should_run_ocr - - assert _should_run_ocr() is True - - def test_force_off(self, isolated_env): - """enable_ocr=False always returns False regardless of vision_model.""" - cfg.enable_ocr = False - cfg.vision_model = "noctrex/LightOnOCR-2-1B-GGUF/lightonocr-Q4_K_M.gguf" - from lilbee.data.ingest.extract import _should_run_ocr - - assert _should_run_ocr() is False - - class TestOcrOverrideContextVar: """Per-request OCR overrides use a ContextVar, never a global cfg mutation.""" @@ -2597,14 +2431,6 @@ def test_none_arguments_keep_cfg_defaults(self, isolated_env): with ocr_override(enable_ocr=None, ocr_timeout=None): assert _effective_enable_ocr() is True - def test_should_run_ocr_honors_override(self, isolated_env): - from lilbee.data.ingest.extract import _should_run_ocr, ocr_override - - cfg.enable_ocr = True - cfg.vision_model = "" - with ocr_override(enable_ocr=False): - assert _should_run_ocr() is False - def test_overrides_isolated_across_contexts(self, isolated_env): # Two copied contexts must each see only their own override; this is the # concurrency guarantee that a global cfg mutation could not give. @@ -2648,157 +2474,6 @@ async def test_override_propagates_into_to_thread_worker(self, isolated_env): assert await asyncio.to_thread(_effective_ocr_timeout) == 5.0 -class TestOcrFallbackBackendDispatch: - """``_handle_scanned_pdf_fallback`` routes to the right backend on the pool.""" - - @mock.patch("kreuzberg.extract_file_sync", new_callable=Mock) - async def test_vision_backend_when_ocr_enabled_and_model_set( - self, mock_kf, isolated_env, mock_svc - ): - cfg.enable_ocr = True - cfg.vision_model = "org/Test-Vision-GGUF/test-vision-Q4_K_M.gguf" - cfg.ocr_timeout = 60.0 - mock_kf.return_value = _make_empty_result() - mock_svc.provider.pdf_ocr.return_value = _vision_pages((1, "Vision text. " * 10)) - - f = isolated_env / "scanned.pdf" - f.write_bytes(b"fake pdf") - - from lilbee.data.ingest import ingest_document - - result = await ingest_document(f, "scanned.pdf", "pdf") - assert mock_svc.provider.pdf_ocr.call_args.kwargs["backend"] == "vision" - assert mock_svc.provider.pdf_ocr.call_args.kwargs["per_page_timeout_s"] == 60.0 - assert len(result) > 0 - - @mock.patch("kreuzberg.extract_file_sync", new_callable=Mock) - async def test_tesseract_backend_when_no_vision_model(self, mock_kf, isolated_env, mock_svc): - """Without a vision model the scanned PDF runs Tesseract inline (no pool).""" - cfg.enable_ocr = True - cfg.vision_model = "" - cfg.tesseract_timeout = 30.0 - # First call: pre-extract that flags the PDF as scanned. Second - # call: the inline Tesseract fallback that produces real chunks. - mock_kf.side_effect = [ - _make_empty_result(), - _make_kreuzberg_result(text="Tesseract OCR text. " * 10, num_chunks=1, has_pages=True), - ] - - f = isolated_env / "scanned.pdf" - f.write_bytes(b"fake pdf") - - from lilbee.data.ingest import ingest_document - - result = await ingest_document(f, "scanned.pdf", "pdf") - # Pool pdf_ocr is not used for tesseract. - mock_svc.provider.pdf_ocr.assert_not_called() - assert mock_kf.call_count == 2 - assert len(result) > 0 - - @mock.patch("kreuzberg.extract_file_sync", new_callable=Mock) - async def test_tesseract_returning_empty_logs_skip( - self, mock_kf, isolated_env, mock_svc, caplog - ): - """Tesseract returning no pages produces a user-facing skip warning.""" - cfg.enable_ocr = None # auto: no vision model -> Tesseract fallback runs - cfg.vision_model = "" - # Both pre-extract and tesseract fallback return empty. - mock_kf.side_effect = [_make_empty_result(), _make_empty_result()] - - f = isolated_env / "blank.pdf" - f.write_bytes(b"fake pdf") - - from lilbee.data.ingest import ingest_document - - with caplog.at_level("WARNING", logger="lilbee.data.ingest.extract"): - result = await ingest_document(f, "blank.pdf", "pdf") - assert result == [] - assert "Skipped blank.pdf" in caplog.text - - @mock.patch("kreuzberg.extract_file_sync", new_callable=Mock) - async def test_pool_exception_fails_file(self, mock_kf, isolated_env, mock_svc): - """A pdf_ocr backend error is logged and raised as a per-file failure.""" - cfg.enable_ocr = True - cfg.vision_model = "org/Test-Vision-GGUF/test-vision-Q4_K_M.gguf" - mock_kf.return_value = _make_empty_result() - mock_svc.provider.pdf_ocr.side_effect = RuntimeError("pool died") - - f = isolated_env / "broken.pdf" - f.write_bytes(b"fake pdf") - - from lilbee.data.ingest import ingest_document - - with pytest.raises(RuntimeError, match="pool died"): - await ingest_document(f, "broken.pdf", "pdf", quiet=True) - - @mock.patch("kreuzberg.extract_file_sync", new_callable=Mock) - async def test_tesseract_no_timeout_runs_uncapped(self, mock_kf, isolated_env, mock_svc): - """``cfg.tesseract_timeout == 0`` skips ``asyncio.wait_for`` and runs uncapped.""" - cfg.enable_ocr = None # auto: no vision model -> Tesseract fallback runs - cfg.vision_model = "" - cfg.tesseract_timeout = 0 - mock_kf.side_effect = [ - _make_empty_result(), - _make_kreuzberg_result(text="Tesseract OCR text. " * 10, num_chunks=1, has_pages=True), - ] - - f = isolated_env / "scanned.pdf" - f.write_bytes(b"fake pdf") - - from lilbee.data.ingest import ingest_document - - result = await ingest_document(f, "scanned.pdf", "pdf") - assert mock_kf.call_count == 2 - assert len(result) > 0 - - @mock.patch("kreuzberg.extract_file_sync", new_callable=Mock) - async def test_tesseract_timeout_logs_and_returns_empty( - self, mock_kf, isolated_env, mock_svc, caplog - ): - """Tesseract exceeding the wall-clock cap logs a warning and skips the file.""" - cfg.enable_ocr = None # auto: no vision model -> Tesseract fallback runs - cfg.vision_model = "" - cfg.tesseract_timeout = 30.0 - # Pre-extract returns empty; tesseract fallback's wait_for fires. - mock_kf.side_effect = [_make_empty_result(), _make_kreuzberg_result()] - - async def _instant_timeout(coro, *, timeout): - coro.close() - raise TimeoutError - - with mock.patch("asyncio.wait_for", side_effect=_instant_timeout): - f = isolated_env / "scanned.pdf" - f.write_bytes(b"fake pdf") - - from lilbee.data.ingest import ingest_document - - with caplog.at_level("WARNING", logger="lilbee.data.ingest.extract"): - result = await ingest_document(f, "scanned.pdf", "pdf") - assert result == [] - assert "Tesseract OCR exceeded" in caplog.text - - @mock.patch("kreuzberg.extract_file_sync", new_callable=Mock) - async def test_tesseract_extract_exception_logs_and_returns_empty( - self, mock_kf, isolated_env, mock_svc, caplog - ): - """A kreuzberg backend crash logs a warning and returns no chunks.""" - cfg.enable_ocr = None # auto: no vision model -> Tesseract fallback runs - cfg.vision_model = "" - cfg.tesseract_timeout = 30.0 - # Pre-extract OK; tesseract fallback raises. - mock_kf.side_effect = [_make_empty_result(), RuntimeError("tesseract binary missing")] - - f = isolated_env / "scanned.pdf" - f.write_bytes(b"fake pdf") - - from lilbee.data.ingest import ingest_document - - with caplog.at_level("WARNING", logger="lilbee.data.ingest.extract"): - result = await ingest_document(f, "scanned.pdf", "pdf") - assert result == [] - assert "OCR via tesseract backend failed" in caplog.text - - class TestPhaseProgressCallback: """The non-quiet Rich bar advances once per file, so a single multi-page file would freeze at "0/1" through its whole OCR + embed phase. The phase-progress @@ -2859,7 +2534,7 @@ async def test_empty_markdown_returns_empty(self, isolated_env): md = isolated_env / "empty.md" md.write_text(" ") - result = await ingest_markdown(md, "empty.md") + result, _ = await ingest_markdown(md, "empty.md") assert result == [] async def test_no_chunks_returns_empty(self, isolated_env): @@ -2868,7 +2543,7 @@ async def test_no_chunks_returns_empty(self, isolated_env): md = isolated_env / "blank.md" md.write_text("some text") with mock.patch("lilbee.data.ingest.extract.chunk_text", return_value=[]): - result = await ingest_markdown(md, "blank.md") + result, _ = await ingest_markdown(md, "blank.md") assert result == [] async def test_frontmatter_only_produces_chunks(self, isolated_env): @@ -2876,16 +2551,16 @@ async def test_frontmatter_only_produces_chunks(self, isolated_env): md = isolated_env / "fm_only.md" md.write_text("---\ntitle: Just Frontmatter\ntags: [test]\n---\n") - result = await ingest_markdown(md, "fm_only.md") + result, _ = await ingest_markdown(md, "fm_only.md") assert len(result) > 0, "Frontmatter content should be indexed" class TestPageTextAccumulator: """`page_texts_out` captures clean per-page text for the export dataset.""" - @mock.patch("kreuzberg.extract_file_sync", new_callable=Mock) + @mock.patch("lilbee.data.xberg_extract.aextract_document", new_callable=mock.AsyncMock) async def test_pdf_pages_captured(self, mock_kf, isolated_env): - mock_kf.return_value = _make_kreuzberg_result(num_chunks=2, has_pages=True) + mock_kf.return_value = _make_xberg_result(num_chunks=2, has_pages=True) from lilbee.data.ingest import ingest_document f = isolated_env / "test.pdf" @@ -2895,9 +2570,9 @@ async def test_pdf_pages_captured(self, mock_kf, isolated_env): assert [p["page"] for p in pages] == [1, 2] assert all(p["content_type"] == "pdf" for p in pages) - @mock.patch("kreuzberg.extract_file_sync", new_callable=Mock) + @mock.patch("lilbee.data.xberg_extract.aextract_document", new_callable=mock.AsyncMock) async def test_non_paginated_doc_captured_as_page_zero(self, mock_kf, isolated_env): - mock_kf.return_value = _make_kreuzberg_result(text="Plain body. " * 10, has_pages=False) + mock_kf.return_value = _make_xberg_result(text="Plain body. " * 10, has_pages=False) from lilbee.data.ingest import ingest_document f = isolated_env / "note.txt" @@ -2920,15 +2595,14 @@ async def test_markdown_captured_as_page_zero(self, isolated_env): assert pages[0]["page"] == 0 assert "markdown body" in pages[0]["text"] - @mock.patch("kreuzberg.extract_file_sync", new_callable=Mock) - async def test_ocr_fallback_captured(self, mock_kf, isolated_env, mock_svc): + @mock.patch("lilbee.data.xberg_extract.aextract_document", new_callable=mock.AsyncMock) + async def test_ocr_pages_captured(self, mock_kf, isolated_env, mock_svc): + # xberg OCRs scanned pages in-pass; the OCR'd text arrives in result.pages. cfg.enable_ocr = True cfg.vision_model = "" - cfg.tesseract_timeout = 30.0 - mock_kf.side_effect = [ - _make_empty_result(), - _make_kreuzberg_result(text="OCR page text. " * 10, num_chunks=2, has_pages=True), - ] + mock_kf.return_value = _make_xberg_result( + text="OCR page text. " * 10, num_chunks=2, has_pages=True + ) from lilbee.data.ingest import ingest_document f = isolated_env / "scanned.pdf" @@ -2941,26 +2615,28 @@ async def test_ocr_fallback_captured(self, mock_kf, isolated_env, mock_svc): class TestIngestDocumentEdgeCases: async def test_empty_extraction_returns_empty(self, isolated_env): - """Structured formats now go through kreuzberg: empty result yields no chunks.""" + """Structured formats now go through xberg: empty result yields no chunks.""" from lilbee.data.ingest import ingest_document - empty_result = mock.MagicMock(chunks=[]) - mock_extract = Mock(return_value=empty_result) - with mock.patch("kreuzberg.extract_file_sync", mock_extract): - result = await ingest_document(isolated_env / "e.xml", "e.xml", "xml") + (isolated_env / "e.xml").write_bytes(b"") # ingest_document reads the file bytes + empty_result = mock.MagicMock(chunks=[], metadata=Metadata()) + mock_extract = mock.AsyncMock(return_value=empty_result) + with mock.patch("lilbee.data.xberg_extract.aextract_document", mock_extract): + result, _ = await ingest_document(isolated_env / "e.xml", "e.xml", "xml") assert result == [] async def test_no_chunks_returns_empty(self, isolated_env): from lilbee.data.ingest import ingest_document - no_chunks_result = mock.MagicMock(chunks=[]) - mock_extract = Mock(return_value=no_chunks_result) - with mock.patch("kreuzberg.extract_file_sync", mock_extract): - result = await ingest_document(isolated_env / "s.xml", "s.xml", "xml") + (isolated_env / "s.xml").write_bytes(b"") # ingest_document reads the file bytes + no_chunks_result = mock.MagicMock(chunks=[], metadata=Metadata()) + mock_extract = mock.AsyncMock(return_value=no_chunks_result) + with mock.patch("lilbee.data.xberg_extract.aextract_document", mock_extract): + result, _ = await ingest_document(isolated_env / "s.xml", "s.xml", "xml") assert result == [] -class TestChunkViaKreuzberg: +class TestChunkViaXberg: def test_empty_returns_empty(self): from lilbee.data.chunk import chunk_text @@ -2980,7 +2656,9 @@ def _mock_concepts_available(self): yield @mock.patch( - "kreuzberg.extract_file_sync", new_callable=Mock, return_value=_make_kreuzberg_result() + "lilbee.data.xberg_extract.aextract_document", + new_callable=mock.AsyncMock, + return_value=_make_xberg_result(), ) async def test_concept_extraction_called_during_ingest( self, mock_extract_file, isolated_env, mock_svc @@ -2997,7 +2675,9 @@ async def test_concept_extraction_called_during_ingest( mock_svc.concepts.extract_concepts_batch.assert_called() @mock.patch( - "kreuzberg.extract_file_sync", new_callable=Mock, return_value=_make_kreuzberg_result() + "lilbee.data.xberg_extract.aextract_document", + new_callable=mock.AsyncMock, + return_value=_make_xberg_result(), ) async def test_concept_disabled_skips_extraction( self, mock_extract_file, isolated_env, mock_svc @@ -3012,7 +2692,9 @@ async def test_concept_disabled_skips_extraction( mock_svc.concepts.extract_concepts_batch.assert_not_called() @mock.patch( - "kreuzberg.extract_file_sync", new_callable=Mock, return_value=_make_kreuzberg_result() + "lilbee.data.xberg_extract.aextract_document", + new_callable=mock.AsyncMock, + return_value=_make_xberg_result(), ) async def test_concept_failure_does_not_break_ingest( self, mock_extract_file, isolated_env, mock_svc @@ -3029,7 +2711,9 @@ async def test_concept_failure_does_not_break_ingest( assert "concept_test2.txt" in result.added @mock.patch( - "kreuzberg.extract_file_sync", new_callable=Mock, return_value=_make_kreuzberg_result() + "lilbee.data.xberg_extract.aextract_document", + new_callable=mock.AsyncMock, + return_value=_make_xberg_result(), ) async def test_concept_write_failure_does_not_fail_files( self, mock_extract_file, isolated_env, mock_svc @@ -3051,7 +2735,9 @@ async def test_concept_write_failure_does_not_fail_files( assert result.failed == [] @mock.patch( - "kreuzberg.extract_file_sync", new_callable=Mock, return_value=_make_kreuzberg_result() + "lilbee.data.xberg_extract.aextract_document", + new_callable=mock.AsyncMock, + return_value=_make_xberg_result(), ) async def test_cluster_rebuild_called_after_sync( self, mock_extract_file, isolated_env, mock_svc @@ -3068,7 +2754,9 @@ async def test_cluster_rebuild_called_after_sync( mock_svc.concepts.rebuild_clusters.assert_called() @mock.patch( - "kreuzberg.extract_file_sync", new_callable=Mock, return_value=_make_kreuzberg_result() + "lilbee.data.xberg_extract.aextract_document", + new_callable=mock.AsyncMock, + return_value=_make_xberg_result(), ) async def test_cluster_rebuild_failure_does_not_break_sync( self, mock_extract_file, isolated_env, mock_svc @@ -3086,7 +2774,9 @@ async def test_cluster_rebuild_failure_does_not_break_sync( assert "rebuild_test.txt" in result.added @mock.patch( - "kreuzberg.extract_file_sync", new_callable=Mock, return_value=_make_kreuzberg_result() + "lilbee.data.xberg_extract.aextract_document", + new_callable=mock.AsyncMock, + return_value=_make_xberg_result(), ) async def test_graph_none_skips_indexing(self, mock_extract_file, isolated_env, mock_svc): """When get_graph() returns None, concept indexing is skipped gracefully.""" @@ -3100,7 +2790,9 @@ async def test_graph_none_skips_indexing(self, mock_extract_file, isolated_env, assert "graph_none_test.txt" in result.added @mock.patch( - "kreuzberg.extract_file_sync", new_callable=Mock, return_value=_make_kreuzberg_result() + "lilbee.data.xberg_extract.aextract_document", + new_callable=mock.AsyncMock, + return_value=_make_xberg_result(), ) async def test_concepts_unavailable_skips_rebuild( self, mock_extract_file, isolated_env, mock_svc @@ -3117,7 +2809,9 @@ async def test_concepts_unavailable_skips_rebuild( mock_svc.concepts.rebuild_clusters.assert_not_called() @mock.patch( - "kreuzberg.extract_file_sync", new_callable=Mock, return_value=_make_kreuzberg_result() + "lilbee.data.xberg_extract.aextract_document", + new_callable=mock.AsyncMock, + return_value=_make_xberg_result(), ) async def test_concepts_unavailable_skips_indexing( self, mock_extract_file, isolated_env, mock_svc @@ -3184,3 +2878,292 @@ def test_no_marker_when_nothing_removed(self, isolated_env, mock_svc): mock_svc.store.remove_documents.return_value = RemoveResult(removed=[], not_found=["gone"]) remove_documents_durably(["gone"]) assert load_skip_markers(cfg.data_root) == {} + + +class TestOcrConfigSelection: + """extraction_config picks the OCR backend from enable_ocr + vision_model.""" + + def test_disabled_when_enable_ocr_false(self, isolated_env): + from lilbee.data.ingest import ExtractMode, extraction_config + + cfg.enable_ocr = False + config = extraction_config(ExtractMode.PAGINATED) + assert config["ocr"].enabled is False + + def test_vision_backend_with_token_when_model_set(self, isolated_env): + from lilbee.data.ingest import ExtractMode, extraction_config + + cfg.enable_ocr = None + cfg.vision_model = "org/Test-Vision-GGUF/test-vision-Q4_K_M.gguf" + config = extraction_config(ExtractMode.PAGINATED, ocr_token="tok-123") + assert config["ocr"].backend == "lilbee-vision" + assert config["ocr"].backend_options["req"] == "tok-123" + + def test_force_ocr_off_by_default(self, isolated_env, monkeypatch): + from lilbee.data.ingest import ExtractMode, extraction_config + + monkeypatch.delenv("LILBEE_OCR_FORCE", raising=False) + cfg.vision_model = "org/Test-Vision-GGUF/test-vision-Q4_K_M.gguf" + config = extraction_config(ExtractMode.PAGINATED) + assert config.get("force_ocr") is False + + @pytest.mark.parametrize("mode_name", ["PAGINATED", "MARKDOWN"]) + def test_force_ocr_set_when_env_and_vision_model(self, isolated_env, monkeypatch, mode_name): + from lilbee.data.ingest import ExtractMode, extraction_config + + monkeypatch.setenv("LILBEE_OCR_FORCE", "1") + cfg.vision_model = "org/Test-Vision-GGUF/test-vision-Q4_K_M.gguf" + config = extraction_config(getattr(ExtractMode, mode_name)) + assert config["force_ocr"] is True + + def test_force_ocr_ignored_without_vision_model(self, isolated_env, monkeypatch): + # Tesseract path: force is a vision/GPU re-OCR lever, so it stays off here. + from lilbee.data.ingest import ExtractMode, extraction_config + + monkeypatch.setenv("LILBEE_OCR_FORCE", "1") + cfg.vision_model = "" + config = extraction_config(ExtractMode.PAGINATED) + assert config["ocr"].backend == "tesseract" + assert config.get("force_ocr") is False + + def test_force_ocr_ignored_when_ocr_disabled(self, isolated_env, monkeypatch): + from lilbee.data.ingest import ExtractMode, extraction_config + + monkeypatch.setenv("LILBEE_OCR_FORCE", "1") + cfg.enable_ocr = False + cfg.vision_model = "org/Test-Vision-GGUF/test-vision-Q4_K_M.gguf" + config = extraction_config(ExtractMode.PAGINATED) + assert config["ocr"].enabled is False + assert config.get("force_ocr") is False + + +class _CountingVisionBackend: + """Minimal xberg custom OCR backend registered as 'lilbee-vision' that records + how many pages were sent to OCR. Used to observe whether force_ocr defeats + xberg's text-layer short-circuit.""" + + def __init__(self) -> None: + self.calls = 0 + + def name(self): + from lilbee.data.ingest.types import OcrBackendName + + return OcrBackendName.LILBEE_VISION + + def version(self): + return "0" + + def supported_languages(self): + return [] + + def supports_language(self, _lang): + return True + + def initialize(self): ... + + def shutdown(self): ... + + def backend_type(self): + from xberg import OcrBackendType + + return OcrBackendType.CUSTOM + + def supports_table_detection(self): + return False + + def supports_document_processing(self): + return False + + def emits_structured_markdown(self): + return False + + def process_image(self, _image_bytes, _config): + from xberg import ExtractedDocument + + from lilbee.data.ingest.types import MARKDOWN_MIME + + self.calls += 1 + return ExtractedDocument(content="OCR-TEXT", mime_type=MARKDOWN_MIME) + + def process_image_file(self, _path, config): + return self.process_image(b"", config) + + def process_document(self, _path, _config): + raise NotImplementedError + + +class TestForceOcrRoutesToBackend: + """Behavioral contract against real xberg: LILBEE_OCR_FORCE must OCR every page + of a born-digital PDF through the registered vision backend, defeating xberg's + text-layer short-circuit. This is the guard that a future xberg bump can't + silently disable forced re-OCR (the way rc25's short-circuit did).""" + + @staticmethod + def _extract(pdf: bytes, config) -> tuple[int, str]: + """Extract under a fake 'lilbee-vision' backend; return (ocr_calls, content).""" + from xberg import register_ocr_backend, unregister_ocr_backend + + from lilbee.data.ingest.types import OcrBackendName + from lilbee.data.xberg_extract import extract_document + + backend = _CountingVisionBackend() + register_ocr_backend(backend) + try: + doc = extract_document(pdf, "application/pdf", filename="born.pdf", config=config) + return backend.calls, doc.content or "" + finally: + unregister_ocr_backend(OcrBackendName.LILBEE_VISION) + + def test_native_text_skips_ocr_without_force(self, isolated_env, monkeypatch): + from lilbee.data.ingest import ExtractMode, extraction_config + + monkeypatch.delenv("LILBEE_OCR_FORCE", raising=False) + cfg.vision_model = "org/Test-Vision-GGUF/test-vision-Q4_K_M.gguf" + config = extraction_config(ExtractMode.PAGINATED) + calls, content = self._extract(make_pdf(pages=2), config) + assert calls == 0 + assert "clean native text layer" in content + + def test_force_ocrs_every_page(self, isolated_env, monkeypatch): + from lilbee.data.ingest import ExtractMode, extraction_config + + monkeypatch.setenv("LILBEE_OCR_FORCE", "1") + cfg.vision_model = "org/Test-Vision-GGUF/test-vision-Q4_K_M.gguf" + config = extraction_config(ExtractMode.PAGINATED) + calls, content = self._extract(make_pdf(pages=2), config) + assert calls == 2 # one OCR call per page, text layer notwithstanding + assert "OCR-TEXT" in content + assert "clean native text layer" not in content + + +class TestChunkAndEmbedPagesEmpty: + async def test_empty_page_texts_returns_empty(self): + from lilbee.data.ingest.extract import chunk_and_embed_pages + from lilbee.runtime.progress import noop_callback + + assert await chunk_and_embed_pages([], "s", "pdf", noop_callback) == [] + + async def test_whitespace_pages_yield_no_chunks(self, mock_svc): + from lilbee.data.ingest.extract import chunk_and_embed_pages + from lilbee.runtime.progress import noop_callback + + assert await chunk_and_embed_pages([(1, " ")], "s", "pdf", noop_callback) == [] + + +class TestIngestDocumentOcrPath: + @mock.patch("lilbee.data.xberg_extract.aextract_document", new_callable=mock.AsyncMock) + async def test_empty_pdf_warns_no_usable_text(self, mock_kf, isolated_env, mock_svc, caplog): + mock_kf.return_value = mock.MagicMock(chunks=[], metadata=Metadata()) + from lilbee.data.ingest import ingest_document + + f = isolated_env / "scan.pdf" + f.write_bytes(b"x") + result, _ = await ingest_document(f, "scan.pdf", "pdf") + assert result == [] + assert "no usable text" in caplog.text + + @mock.patch("lilbee.data.xberg_extract.aextract_document", new_callable=mock.AsyncMock) + async def test_streams_per_page_progress_ticks(self, mock_kf, isolated_env, mock_svc): + cfg.vision_model = "org/Test-Vision-GGUF/test-vision-Q4_K_M.gguf" + result_obj = _make_xberg_result(num_chunks=1, has_pages=True) + + async def fake_extract(data, *, filename=None, config): + # Simulate xberg calling the registered backend once per scanned page. + from lilbee.data.ingest.vision_ocr_backend import ocr_requests + + token = config["ocr"].backend_options["req"] + ctx = ocr_requests.get(token) + ctx.on_page() + ctx.on_page() + return result_obj + + mock_kf.side_effect = fake_extract + from lilbee.data.ingest import ingest_document + + seen: list[int] = [] + + def on_prog(_et, ev): + seen.append(ev.page) + + f = isolated_env / "scan.pdf" + f.write_bytes(b"x") + await ingest_document(f, "scan.pdf", "pdf", on_progress=on_prog) + assert 1 in seen and 2 in seen # two per-page running-count ticks + + +class TestTitleStamping: + """Every produced record carries the document title; the source row its metadata. + + Guards the title path end to end: xberg's typed Metadata is folded into + SourceMeta, stamped on every chunk row, and written to the source row. This is + the test that catches titles silently breaking, which would poison title_search. + """ + + @mock.patch("lilbee.data.xberg_extract.aextract_document", new_callable=mock.AsyncMock) + async def test_extracted_metadata_flows_to_records_and_source_row( + self, mock_kf, isolated_env, mock_svc + ): + mock_kf.return_value = _make_xberg_result( + metadata=Metadata( + title="Extracted Title", + authors=["Ada", "Grace"], + created_at="2020-01-01", + ) + ) + (isolated_env / "report_2021.txt").write_text("body text") + from lilbee.data.ingest import sync + + await sync(quiet=True) + items = mock_svc.store.write_chunks_batch.call_args.args[0] + item = next(it for it in items if it.source == "report_2021.txt") + assert item.meta.title == "Extracted Title" + assert item.meta.authors == "Ada, Grace" + assert item.meta.created_at == "2020-01-01" + assert all(r["title"] == "Extracted Title" for r in item.records) + + @mock.patch("lilbee.data.xberg_extract.aextract_document", new_callable=mock.AsyncMock) + async def test_bare_string_author_is_one_author_not_characters( + self, mock_kf, isolated_env, mock_svc + ): + # xberg annotates authors as list[str] | None but does not enforce it: a PDF + # /Author field arrives as a bare str. Joining it naively yields "A, d, a". + mock_kf.return_value = _make_xberg_result(metadata=Metadata(authors="Ada Lovelace")) + (isolated_env / "paper.txt").write_text("body text") + from lilbee.data.ingest import sync + + await sync(quiet=True) + items = mock_svc.store.write_chunks_batch.call_args.args[0] + item = next(it for it in items if it.source == "paper.txt") + assert item.meta.authors == "Ada Lovelace" + + @mock.patch("lilbee.data.xberg_extract.aextract_document", new_callable=mock.AsyncMock) + async def test_missing_metadata_falls_back_to_stem(self, mock_kf, isolated_env, mock_svc): + mock_kf.return_value = _make_xberg_result() + (isolated_env / "annual_wildlife_survey.txt").write_text("body text") + from lilbee.data.ingest import sync + + await sync(quiet=True) + items = mock_svc.store.write_chunks_batch.call_args.args[0] + item = next(it for it in items if it.source == "annual_wildlife_survey.txt") + assert item.meta.title == "annual wildlife survey" + assert item.meta.authors == "" + assert all(r["title"] == "annual wildlife survey" for r in item.records) + + async def test_markdown_title_uses_the_h1_heading(self, isolated_env, mock_svc): + (isolated_env / "meeting_notes.md").write_text("# Project Kickoff\n\nSome content here.") + from lilbee.data.ingest import sync + + await sync(quiet=True) + items = mock_svc.store.write_chunks_batch.call_args.args[0] + item = next(it for it in items if it.source == "meeting_notes.md") + assert item.meta.title == "Project Kickoff" + assert all(r["title"] == "Project Kickoff" for r in item.records) + + async def test_markdown_without_h1_falls_back_to_stem(self, isolated_env, mock_svc): + (isolated_env / "meeting_notes.md").write_text("Some content, no heading.") + from lilbee.data.ingest import sync + + await sync(quiet=True) + items = mock_svc.store.write_chunks_batch.call_args.args[0] + item = next(it for it in items if it.source == "meeting_notes.md") + assert item.meta.title == "meeting notes" diff --git a/tests/test_ingest_adaptive.py b/tests/test_ingest_adaptive.py index e5e36384f..010a82116 100644 --- a/tests/test_ingest_adaptive.py +++ b/tests/test_ingest_adaptive.py @@ -146,9 +146,27 @@ def test_saturated_and_falling_steps_back() -> None: def test_latency_gradient_vetoes_increase() -> None: # An established low baseline (w_min) plus an inflated current residence time - # (permits high, throughput low) vetoes the climb before throughput rolls over. - out = decide(CONSERVATIVE, _state(20, ewma=100, direction=1, w_min=0.1), _signals(10), MIN, MAX) - assert out.permits == 20 # held: w_est 20/73 ~ 0.27 > 0.1 * 1.5 + # vetoes the climb before throughput rolls over: throughput is still rising + # here, so without the veto this tick would step up. + out = decide( + CONSERVATIVE, _state(60, ewma=100, direction=1, w_min=0.1), _signals(500), MIN, MAX + ) + assert out.permits == 60 # held: w_est 60/220 ~ 0.27 > 0.1 * 1.5 + + +@pytest.mark.parametrize( + ("field", "value"), + [("cpu_pct", 92.0), ("ram_free_frac", 0.15), ("gpu_temp_c", 82.0)], +) +def test_soft_pressure_still_allows_the_retreat(field: str, value: float) -> None: + """The veto is against climbing, not against backing off. Suppressing the + hill-climb's step down would leave the controller riding sustained soft + pressure past the knee until a critical threshold forced a 50% cut.""" + out = decide( + CONSERVATIVE, _state(10, ewma=100, direction=1), _signals(0, **{field: value}), MIN, MAX + ) + assert out.permits == 9 + assert out.direction == -1 def test_residence_baseline_is_tracked() -> None: diff --git a/tests/test_ingest_offload.py b/tests/test_ingest_offload.py index 503a1a6c9..2837582f6 100644 --- a/tests/test_ingest_offload.py +++ b/tests/test_ingest_offload.py @@ -9,41 +9,63 @@ from __future__ import annotations +from collections.abc import Iterator + import pytest from lilbee.data.ingest import offload @pytest.fixture(autouse=True) -def _clear_env(monkeypatch: pytest.MonkeyPatch) -> None: +def _clear_env(monkeypatch: pytest.MonkeyPatch) -> Iterator[None]: monkeypatch.delenv("LILBEE_INGEST_MAX_WORKERS", raising=False) + offload.max_workers.cache_clear() + yield + offload.max_workers.cache_clear() def test_default_caps_at_32_on_a_big_box(monkeypatch: pytest.MonkeyPatch) -> None: # OCR ingest is GPU-bound; a worker sweep showed throughput flat/declining past # ~32, so the default caps there instead of scaling to the vCPU count. monkeypatch.setattr(offload.os, "cpu_count", lambda: 128) - assert offload._max_workers() == 32 + assert offload.max_workers() == 32 def test_default_keeps_headroom_on_a_small_box(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(offload.os, "cpu_count", lambda: 8) - assert offload._max_workers() == 12 + assert offload.max_workers() == 12 def test_default_falls_back_when_cpu_count_is_unknown(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(offload.os, "cpu_count", lambda: None) - assert offload._max_workers() == 8 + assert offload.max_workers() == 8 def test_env_override_wins_over_the_capped_default(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(offload.os, "cpu_count", lambda: 8) monkeypatch.setenv("LILBEE_INGEST_MAX_WORKERS", "200") - assert offload._max_workers() == 200 + assert offload.max_workers() == 200 @pytest.mark.parametrize("bad", ["nan", "0", "-4", " "]) def test_invalid_env_override_is_ignored(monkeypatch: pytest.MonkeyPatch, bad: str) -> None: monkeypatch.setattr(offload.os, "cpu_count", lambda: 8) monkeypatch.setenv("LILBEE_INGEST_MAX_WORKERS", bad) - assert offload._max_workers() == 12 + assert offload.max_workers() == 12 + + +def test_the_ceiling_matches_the_pool_that_actually_exists( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """The controller sizes its permit ceiling from max_workers() while the pool + is already running. Re-reading the env per call would report a ceiling the + live pool does not have -- promising threads that are not there.""" + monkeypatch.setattr(offload.os, "cpu_count", lambda: 8) + offload._ingest_executor.cache_clear() + try: + pool = offload._ingest_executor() + monkeypatch.setenv("LILBEE_INGEST_MAX_WORKERS", "200") + assert offload.max_workers() == pool._max_workers + finally: + pool.shutdown(wait=False) + offload._ingest_executor.cache_clear() diff --git a/tests/test_ingest_title.py b/tests/test_ingest_title.py new file mode 100644 index 000000000..32618720a --- /dev/null +++ b/tests/test_ingest_title.py @@ -0,0 +1,112 @@ +"""Tests for document title and source-metadata derivation at ingest. + +The metadata cases build real ``xberg.Metadata`` rather than mocks: a mock answers +any attribute, so it would stay green through a renamed or dropped xberg field and +hide exactly the drift these tests exist to catch. +""" + +from xberg import Metadata + +from lilbee.data.ingest.title import derive_title, source_meta_from_extraction +from tests.conftest import make_pdf + + +class TestDeriveTitle: + def test_metadata_title_wins(self): + assert derive_title("q3_report.pdf", "Quarterly Revenue Report") == ( + "Quarterly Revenue Report" + ) + + def test_metadata_title_is_stripped(self): + assert derive_title("a.pdf", " Padded Title \n") == "Padded Title" + + def test_blank_metadata_falls_back_to_stem(self): + assert derive_title("survey_214.pdf", " ") == "survey 214" + + def test_none_metadata_falls_back_to_stem(self): + assert derive_title("annual-wildlife-survey.pdf") == "annual wildlife survey" + + def test_stem_keeps_meaningful_dots(self): + assert derive_title("spec-v3.0.pdf") == "spec v3.0" + + def test_subdirectory_source_uses_basename_stem(self): + assert derive_title("filings/2024/notice_of_appeal.pdf") == "notice of appeal" + + def test_mixed_separators_collapse(self): + assert derive_title("a_b-c d.txt") == "a b c d" + + +class TestSourceMetaFromExtraction: + def test_full_metadata(self): + meta = source_meta_from_extraction( + Metadata(title="The Title", authors=["Ada", "Grace"], created_at="2021-05-01"), + "x.pdf", + ) + assert meta.title == "The Title" + assert meta.authors == "Ada, Grace" + assert meta.created_at == "2021-05-01" + + def test_empty_metadata_derives_stem_title(self): + meta = source_meta_from_extraction(Metadata(), "field_notes.pdf") + assert meta.title == "field notes" + assert meta.authors == "" + assert meta.created_at == "" + + def test_absent_metadata_derives_stem_title(self): + # xberg reports no metadata at all for some inputs. + meta = source_meta_from_extraction(None, "field_notes.pdf") + assert meta.title == "field notes" + assert meta.authors == "" + assert meta.created_at == "" + + def test_falsy_authors_are_dropped(self): + meta = source_meta_from_extraction(Metadata(authors=["Ada", "", None]), "x.pdf") + assert meta.authors == "Ada" + + def test_none_values_tolerated(self): + meta = source_meta_from_extraction( + Metadata(title=None, authors=None, created_at=None), "notes.md" + ) + assert meta.title == "notes" + assert meta.authors == "" + assert meta.created_at == "" + + def test_string_authors_is_one_author_not_split_into_characters(self): + # xberg declares authors as list[str] | None but does not enforce it: a raw + # PDF /Author field arrives as a plain string and must not become "J, o, h, n". + meta = source_meta_from_extraction(Metadata(authors="John Doe"), "x.pdf") + assert meta.authors == "John Doe" + + def test_non_string_author_entries_are_coerced_not_raised(self): + meta = source_meta_from_extraction(Metadata(authors=["Ada", 42]), "x.pdf") + assert meta.authors == "Ada, 42" + + def test_non_string_title_falls_back_to_stem(self): + # xberg accepts a non-string title; it must not raise, just fall back. + meta = source_meta_from_extraction(Metadata(title=123), "annual_report.pdf") + assert meta.title == "annual report" + + +class TestRealPdfTitleExtraction: + """End-to-end against real xberg, no mocks: a born-digital PDF's embedded title + and author reach SourceMeta. Every other test here mocks the extractor, so this + is the only one that would catch xberg ceasing to populate metadata.title -- + the silent failure that would report a meaningless title_search benchmark arm. + """ + + async def test_embedded_pdf_title_and_author_reach_source_meta(self): + from xberg import ExtractInput, ExtractInputKind, ExtractionConfig, PageConfig, extract + + pdf = make_pdf(title="Quarterly Revenue Report", author="Ada Lovelace") + result = await extract( + ExtractInput( + kind=ExtractInputKind.BYTES, + bytes=pdf, + mime_type="application/pdf", + filename="q3_report.pdf", + ), + ExtractionConfig(pages=PageConfig(extract_pages=True, insert_page_markers=False)), + ) + meta = source_meta_from_extraction(result.results[0].metadata, "q3_report.pdf") + assert meta.title == "Quarterly Revenue Report" + assert meta.authors == "Ada Lovelace" diff --git a/tests/test_ingest_trace.py b/tests/test_ingest_trace.py new file mode 100644 index 000000000..225d64875 --- /dev/null +++ b/tests/test_ingest_trace.py @@ -0,0 +1,111 @@ +"""The extraction trace: one parseable line per file, a vision line when OCR fired.""" + +from __future__ import annotations + +import logging + +import pytest + +from lilbee.data.ingest.trace import ( + ExtractionTrace, + configure_from_env, + trace_extraction, + trace_log, + vision_log, +) + + +def _trace(**kw: object) -> ExtractionTrace: + base = { + "source": "docs/report.pdf", + "content_type": "application/pdf", + "elapsed_s": 1.234, + "page_count": 10, + "chunk_count": 42, + "ocr_pages": 0, + "vision_configured": True, + } + base.update(kw) + return ExtractionTrace(**base) # type: ignore[arg-type] + + +def test_line_carries_filename_timing_and_counts() -> None: + line = _trace().as_line() + assert "source='docs/report.pdf'" in line + assert "elapsed_ms=1234" in line + assert "pages=10" in line + assert "chunks=42" in line + assert "ocr_pages=0" in line + assert "vision=no" in line + + +def test_used_vision_requires_ocr_pages_and_a_configured_model() -> None: + assert _trace(ocr_pages=5, vision_configured=True).used_vision is True + assert _trace(ocr_pages=5, vision_configured=False).used_vision is False # tesseract + assert _trace(ocr_pages=0, vision_configured=True).used_vision is False # all native + + +def test_native_only_file_emits_no_vision_line(caplog: pytest.LogCaptureFixture) -> None: + caplog.set_level(logging.INFO, logger="lilbee.ingest.vision") + caplog.set_level(logging.INFO, logger="lilbee.ingest.trace") + trace_extraction(_trace(ocr_pages=0)) + assert any(r.name == "lilbee.ingest.trace" for r in caplog.records) + assert not any(r.name == "lilbee.ingest.vision" for r in caplog.records) + + +def test_scanned_file_emits_a_dedicated_vision_line(caplog: pytest.LogCaptureFixture) -> None: + caplog.set_level(logging.INFO, logger="lilbee.ingest.vision") + trace_extraction(_trace(ocr_pages=7, vision_configured=True)) + vision_records = [r for r in caplog.records if r.name == "lilbee.ingest.vision"] + assert len(vision_records) == 1 + assert "ocr_pages=7" in vision_records[0].getMessage() + assert "docs/report.pdf" in vision_records[0].getMessage() + + +def test_env_flag_enables_both_loggers(monkeypatch: pytest.MonkeyPatch) -> None: + trace_log.setLevel(logging.WARNING) + vision_log.setLevel(logging.WARNING) + monkeypatch.setenv("LILBEE_INGEST_TRACE", "1") + configure_from_env() + assert trace_log.level == logging.DEBUG # includes the extract-start debug line + assert vision_log.level == logging.INFO # vision lines are INFO, and emit + + +def test_trace_file_receives_records_when_host_handlers_filter( + monkeypatch: pytest.MonkeyPatch, tmp_path: object +) -> None: + # The TUI's root file handler is WARNING+; the dedicated trace file must + # still receive every INFO line or the extraction report starves. + from pathlib import Path + + target = Path(str(tmp_path)) / "trace.log" + monkeypatch.setenv("LILBEE_INGEST_TRACE", "1") + monkeypatch.setenv("LILBEE_INGEST_TRACE_FILE", str(target)) + configure_from_env() + try: + trace_extraction(_trace(ocr_pages=3, vision_configured=True)) + for handler in [*trace_log.handlers, *vision_log.handlers]: + handler.flush() + text = target.read_text() + assert "extract source=" in text and "vision-ocr source=" in text + configure_from_env() # idempotent: same file re-configured adds no dup handler + resolved = str(target.absolute()) + dups = [h for h in trace_log.handlers if getattr(h, "baseFilename", "") == resolved] + assert len(dups) == 1 + finally: + for logger in (trace_log, vision_log): + for handler in list(logger.handlers): + logger.removeHandler(handler) + handler.close() + + +def test_trace_surfaces_under_a_warning_root(monkeypatch: pytest.MonkeyPatch) -> None: + # The real failure mode: root at WARNING (lilbee's default) silently drops + # INFO trace lines. The env flag must lift the named loggers so records emit. + logging.getLogger().setLevel(logging.WARNING) + trace_log.setLevel(logging.WARNING) + vision_log.setLevel(logging.WARNING) + monkeypatch.setenv("LILBEE_INGEST_TRACE", "1") + configure_from_env() + assert trace_log.isEnabledFor(logging.INFO) + assert vision_log.isEnabledFor(logging.INFO) diff --git a/tests/test_intent.py b/tests/test_intent.py index 1aa5da9f7..0b1df3f2d 100644 --- a/tests/test_intent.py +++ b/tests/test_intent.py @@ -125,6 +125,8 @@ def test_json_embedded_in_prose(self): '{"kind": "distinct_type"}', '{"kind": "type_association", "noun": "flights"}', '{"kind": 3}', + '{"kind": ["term_mentions"], "term": "war"}', + '{"kind": {"name": "term_mentions"}, "term": "war"}', "", ], ) @@ -156,6 +158,20 @@ def test_filename_ref_matches_whole_name(self): assert matches_reference("survey_report.pdf", "survey_report.pdf") assert matches_reference("survey_report.pdf", "archive/survey_report.pdf") + def test_unicode_digit_ref_does_not_crash(self): + """str.isdigit() is True for Unicode No characters like the + superscript two, but int() rejects them. The reference pattern + matches \\w so such a ref is reachable; it must decline to match + rather than raise and kill the turn.""" + assert not matches_reference("²", "report-2-final.pdf") + assert not matches_reference("2", "report-²-final.pdf") + + def test_zero_padding_equivalence_survives_the_unicode_guard(self): + """The guard must not cost the zero-padded numeric matching it wraps.""" + assert matches_reference("0482", "ARC-REC-482.pdf") + assert matches_reference("482", "ARC-REC-00482.pdf") + assert not matches_reference("482", "ARC-REC-483.pdf") + class TestParseAggregate: def test_non_count_question_is_none(self): diff --git a/tests/test_memory_extract.py b/tests/test_memory_extract.py index 4a8d29956..3b8aa2cf7 100644 --- a/tests/test_memory_extract.py +++ b/tests/test_memory_extract.py @@ -29,6 +29,23 @@ def test_extracts_array_wrapped_in_prose(self): raw = 'Here is what I found:\n[{"text": "uses a Crown Victoria", "kind": "fact"}]\nDone.' assert parse_extraction(raw) == [ExtractedMemory("uses a Crown Victoria", MemoryKind.FACT)] + def test_trailing_bracket_in_prose_does_not_swallow_the_array(self): + """A conforming array followed by prose containing any ']' must still + parse: a greedy first-'[' to last-']' span covers the prose too and + silently discards every memory.""" + raw = ( + 'Here is what I found:\n[{"text": "uses a Crown Victoria", "kind": "fact"}]\n' + "See the manual [1] for details." + ) + assert parse_extraction(raw) == [ExtractedMemory("uses a Crown Victoria", MemoryKind.FACT)] + + def test_first_array_wins_when_a_second_follows(self): + raw = ( + '[{"text": "the user prefers rust", "kind": "fact"}]\n' + 'Ignore this one: [{"text": "not a memory", "kind": "fact"}]' + ) + assert parse_extraction(raw) == [ExtractedMemory("the user prefers rust", MemoryKind.FACT)] + def test_empty_array_yields_nothing(self): assert parse_extraction("[]") == [] diff --git a/tests/test_memory_recall.py b/tests/test_memory_recall.py index 312d61f0b..08de5c421 100644 --- a/tests/test_memory_recall.py +++ b/tests/test_memory_recall.py @@ -55,8 +55,12 @@ def test_preferences_before_facts_with_framing(self): def test_budget_drops_overflow_facts(self): prefs = [_mem("p" * 80, MemoryKind.PREFERENCE)] facts = [_mem("f" * 80)] - # Budget fits exactly the header + the preference line, so the fact overflows. - budget = estimate_text_tokens(MEMORY_BLOCK_HEADER) + estimate_text_tokens("- " + "p" * 80) + # Budget fits exactly the framing + the preference line, so the fact overflows. + budget = ( + estimate_text_tokens(MEMORY_BLOCK_HEADER) + + estimate_text_tokens(MEMORY_BLOCK_FOOTER) + + estimate_text_tokens("- " + "p" * 80) + ) block = format_memory_block(prefs, facts, budget) assert "p" * 80 in block assert "f" * 80 not in block @@ -64,6 +68,24 @@ def test_budget_drops_overflow_facts(self): def test_tiny_budget_returns_empty(self): assert format_memory_block([_mem("anything")], [], 1) == "" + def test_rendered_block_stays_within_the_budget(self): + """The footer is appended unconditionally, so it has to be budgeted: + otherwise the rendered block overruns the budget it was given.""" + prefs = [_mem("p" * 80, MemoryKind.PREFERENCE)] + budget = estimate_text_tokens(MEMORY_BLOCK_HEADER) + estimate_text_tokens("- " + "p" * 80) + block = format_memory_block(prefs, [], budget) + assert estimate_text_tokens(block) <= budget + + def test_one_oversized_preference_does_not_block_every_fact(self): + """'Preferences claim the budget first; facts fill the remainder' -- + a single preference too large to fit must be skipped, not end the + fill and strand every fact behind it.""" + prefs = [_mem("p" * 4000, MemoryKind.PREFERENCE)] + facts = [_mem("uses rust")] + budget = estimate_text_tokens(MEMORY_BLOCK_HEADER) + 200 + block = format_memory_block(prefs, facts, budget) + assert "uses rust" in block + class TestSearcherMemoryBlock: def test_disabled_returns_empty_and_no_store_calls(self): diff --git a/tests/test_neighbors.py b/tests/test_neighbors.py new file mode 100644 index 000000000..1613b4a9d --- /dev/null +++ b/tests/test_neighbors.py @@ -0,0 +1,197 @@ +"""Tests for neighbor-window expansion (pure logic, mocked store).""" + +from unittest.mock import MagicMock + +from lilbee.data.store import SearchChunk +from lilbee.retrieval.query.neighbors import expand_neighbors, merge_adjacent_texts + + +def _cost(text: str) -> int: + """Mirror Searcher._budget_tokens (3 chars per token, floor 1).""" + return max(1, len(text) // 3) + + +def _chunk( + source="doc.pdf", + index=0, + text="text", + content_type="pdf", + page=1, + line_start=0, + line_end=0, + score=0.9, +) -> SearchChunk: + return SearchChunk( + source=source, + content_type=content_type, + page_start=page, + page_end=page, + line_start=line_start, + line_end=line_end, + chunk=text, + chunk_index=index, + vector=[0.1], + score=score, + ) + + +def _store_with(rows: list[SearchChunk]) -> MagicMock: + """A store mock serving *rows* filtered by the requested source and indices.""" + store = MagicMock() + store.get_chunks_by_indices.side_effect = lambda source, indices: [ + r for r in rows if r.source == source and r.chunk_index in set(indices) + ] + return store + + +class TestMergeAdjacentTexts: + def test_dedups_the_shared_overlap(self): + merged = merge_adjacent_texts(["alpha beta gamma", "gamma delta"]) + assert merged == "alpha beta gamma delta" + + def test_no_overlap_joins_with_a_newline_seam(self): + assert merge_adjacent_texts(["alpha", "delta"]) == "alpha\ndelta" + + def test_fully_contained_text_adds_nothing(self): + assert merge_adjacent_texts(["alpha beta", "beta"]) == "alpha beta" + + def test_an_empty_side_has_no_overlap(self): + # An empty chunk shares nothing, so the seam is a plain join rather than + # a scan over a zero-length string. + assert merge_adjacent_texts(["", "delta"]) == "\ndelta" + assert merge_adjacent_texts(["alpha", ""]) == "alpha" + + def test_control_bytes_in_the_text_do_not_confuse_the_overlap(self): + # Extracted document text can carry stray control bytes. The shared + # region here is the trailing "\0a\0"; a scan that joined the two sides + # around a NUL sentinel would match past it and swallow right's tail. + assert merge_adjacent_texts(["Xa\0a\0", "\0a\0a"]) == "Xa\0a\0a" + + def test_merging_an_already_merged_passage_is_idempotent(self): + texts = ["one two", "two three", "three four"] + merged = merge_adjacent_texts(texts) + assert merged == "one two three four" + # Re-merging with either neighbor changes nothing: the neighbor's text + # is fully contained, so a second expansion pass cannot duplicate it. + assert merge_adjacent_texts(["one two", merged]) == merged + assert merge_adjacent_texts([merged, "three four"]) == merged + + +class TestExpandNeighbors: + def test_widens_both_sides_and_recomputes_the_page_span(self): + center = _chunk(index=2, text="gamma delta", page=3) + rows = [ + _chunk(index=1, text="beta gamma", page=2), + _chunk(index=3, text="delta epsilon", page=4), + ] + store = _store_with(rows) + out = expand_neighbors([center], store, radius=1, budget=1000, cost=_cost) + assert len(out) == 1 + assert out[0].chunk == "beta gamma delta epsilon" + assert (out[0].page_start, out[0].page_end) == (2, 4) + assert out[0].score == center.score + assert out[0].chunk_index == center.chunk_index + store.get_chunks_by_indices.assert_called_once_with("doc.pdf", [1, 2, 3]) + + def test_recomputes_the_line_span_for_code(self): + center = _chunk(content_type="code", index=1, text="mid", line_start=10, line_end=20) + rows = [ + _chunk(content_type="code", index=0, text="top", line_start=1, line_end=9), + _chunk(content_type="code", index=2, text="tail", line_start=21, line_end=30), + ] + out = expand_neighbors([center], _store_with(rows), radius=1, budget=1000, cost=_cost) + assert (out[0].line_start, out[0].line_end) == (1, 30) + + def test_single_chunk_document_stays_untouched(self): + center = _chunk(index=0, text="whole doc") + store = _store_with([]) + out = expand_neighbors([center], store, radius=2, budget=1000, cost=_cost) + assert out == [center] + + def test_selected_neighbors_are_never_pulled_twice(self): + # Chunks 2 and 3 are both selected: each widens only away from the + # other, so no passage text is duplicated. + a = _chunk(index=2, text="two three") + b = _chunk(index=3, text="three four") + rows = [ + _chunk(index=1, text="one two"), + _chunk(index=4, text="four five"), + ] + store = _store_with(rows) + out = expand_neighbors([a, b], store, radius=1, budget=1000, cost=_cost) + assert out[0].chunk == "one two three" + assert out[1].chunk == "three four five" + store.get_chunks_by_indices.assert_called_once_with("doc.pdf", [1, 2, 3, 4]) + + def test_higher_ranked_expansion_claims_the_shared_neighbor(self): + first = _chunk(index=2, text="bb") + second = _chunk(index=4, text="dd") + rows = [ + _chunk(index=1, text="aa"), + _chunk(index=3, text="cc"), + _chunk(index=5, text="ee"), + ] + out = expand_neighbors([first, second], _store_with(rows), 1, 1000, _cost) + # Rank order wins: chunk 2 claims index 3, so chunk 4 widens right only. + assert out[0].chunk == "aa\nbb\ncc" + assert out[1].chunk == "dd\nee" + + def test_budget_sheds_farthest_neighbors_first_trailing_on_ties(self): + center = _chunk(index=5, text="e" * 30) + rows = [_chunk(index=i, text=c * 30) for i, c in ((3, "c"), (4, "d"), (6, "f"), (7, "g"))] + # Full widening costs 41 extra tokens; 30 fits only after shedding the + # trailing index 7 (the tie), then the leading index 3 (the farthest). + out = expand_neighbors([center], _store_with(rows), radius=2, budget=30, cost=_cost) + assert out[0].chunk == "\n".join(["d" * 30, "e" * 30, "f" * 30]) + + def test_zero_budget_keeps_every_original(self): + center = _chunk(index=2, text="core") + rows = [_chunk(index=1, text="left"), _chunk(index=3, text="right")] + store = _store_with(rows) + out = expand_neighbors([center], store, radius=1, budget=0, cost=_cost) + assert out == [center] + # A zero budget can buy nothing, so it must not pay for the store reads. + store.get_chunks_by_indices.assert_not_called() + + def test_reingested_document_is_not_spliced_across_versions(self): + """If the file is re-ingested between the search and the neighbor fetch, + the rows describe a different chunking. Splicing them would invent text + and a page span that existed in no version, so widening is skipped.""" + center = _chunk(index=2, text="original center text") + rows = [ + _chunk(index=1, text="new left"), + # Same (source, chunk_index), different text: the file was re-chunked. + _chunk(index=2, text="re-ingested center text"), + _chunk(index=3, text="new right"), + ] + out = expand_neighbors([center], _store_with(rows), radius=1, budget=1000, cost=_cost) + assert out == [center] + + def test_unchanged_center_still_widens(self): + """The version guard must not block the normal path: a center whose + stored row still matches widens as before.""" + center = _chunk(index=2, text="beta gamma") + rows = [ + _chunk(index=1, text="alpha beta"), + _chunk(index=2, text="beta gamma"), + _chunk(index=3, text="gamma delta"), + ] + out = expand_neighbors([center], _store_with(rows), radius=1, budget=1000, cost=_cost) + assert out[0].chunk != "beta gamma" + + def test_whole_document_selection_expands_nothing(self): + # Every index is a selected passage already, so only the off-the-end + # probe runs and nothing changes. + selected = [_chunk(index=i, text=f"part {i}") for i in range(3)] + store = _store_with([]) + out = expand_neighbors(selected, store, radius=1, budget=1000, cost=_cost) + assert out == selected + store.get_chunks_by_indices.assert_called_once_with("doc.pdf", [0, 1, 2, 3]) + + def test_window_stops_at_a_gap_in_stored_indices(self): + # Index 3 is missing from the store: index 2 must not leapfrog it, + # or the merged passage would splice non-contiguous text. + center = _chunk(index=4, text="dd") + rows = [_chunk(index=2, text="bb"), _chunk(index=5, text="ee")] + out = expand_neighbors([center], _store_with(rows), radius=2, budget=1000, cost=_cost) + assert out[0].chunk == "dd\nee" diff --git a/tests/test_ocr_cache.py b/tests/test_ocr_cache.py deleted file mode 100644 index ea9b82018..000000000 --- a/tests/test_ocr_cache.py +++ /dev/null @@ -1,125 +0,0 @@ -"""OCR output is cached by file content + backend/model so a downstream failure -doesn't discard an expensive OCR pass on retry.""" - -from __future__ import annotations - -from pathlib import Path -from types import SimpleNamespace - -import pytest - -from lilbee.core.config import cfg -from lilbee.data.ingest import extract, ocr_cache -from lilbee.vision import PageText - - -@pytest.fixture -def cache_dir(tmp_path: Path, monkeypatch) -> Path: - data_dir = tmp_path / "data" - monkeypatch.setattr(cfg, "data_dir", data_dir) - return data_dir / "ocr_cache" - - -def test_key_is_stable_and_varies_by_component() -> None: - base = ocr_cache.ocr_cache_key("hash1", backend="vision", model="m") - assert base == ocr_cache.ocr_cache_key("hash1", backend="vision", model="m") - assert base != ocr_cache.ocr_cache_key("hash2", backend="vision", model="m") - assert base != ocr_cache.ocr_cache_key("hash1", backend="tesseract", model="m") - assert base != ocr_cache.ocr_cache_key("hash1", backend="vision", model="other") - assert base != ocr_cache.ocr_cache_key("hash1", backend="vision", model="m", extra="2048") - - -def test_store_then_load_round_trips(cache_dir: Path) -> None: - pages = [(1, "page one"), (2, "page two")] - key = ocr_cache.ocr_cache_key("h", backend="vision", model="m") - ocr_cache.store_ocr_pages(key, pages) - assert (cache_dir / f"{key}.json").exists() - assert ocr_cache.load_ocr_pages(key) == pages - - -def test_load_returns_none_on_miss(cache_dir: Path) -> None: - assert ocr_cache.load_ocr_pages("nope") is None - - -def test_load_returns_none_on_corrupt_entry(cache_dir: Path) -> None: - cache_dir.mkdir(parents=True) - (cache_dir / "bad.json").write_text("{not json", encoding="utf-8") - assert ocr_cache.load_ocr_pages("bad") is None - - -def test_empty_pages_are_not_cached(cache_dir: Path) -> None: - key = ocr_cache.ocr_cache_key("h", backend="tesseract", model="tesseract") - ocr_cache.store_ocr_pages(key, []) - assert ocr_cache.load_ocr_pages(key) is None - - -def test_load_returns_none_when_json_is_not_a_list(cache_dir: Path) -> None: - # Valid JSON of the wrong shape (a dict, not a list of pairs) is still a miss. - cache_dir.mkdir(parents=True) - (cache_dir / "wrong.json").write_text('{"page": 1}', encoding="utf-8") - assert ocr_cache.load_ocr_pages("wrong") is None - - -def test_store_swallows_write_error(tmp_path: Path, monkeypatch) -> None: - # data_dir points at a regular file, so creating the cache dir under it raises - # OSError; store must log and swallow rather than break ingestion. - data_file = tmp_path / "data" - data_file.write_text("not a directory", encoding="utf-8") - monkeypatch.setattr(cfg, "data_dir", data_file) - key = ocr_cache.ocr_cache_key("h", backend="vision", model="m") - ocr_cache.store_ocr_pages(key, [(1, "page")]) # must not raise - - -@pytest.mark.asyncio -async def test_vision_fallback_uses_cache_and_skips_ocr(tmp_path: Path, monkeypatch) -> None: - # A cache hit must short-circuit the expensive pdf_ocr call and feed the - # cached pages straight into chunk + embed. - pdf = tmp_path / "scan.pdf" - pdf.write_bytes(b"%PDF-1.4 fake") - monkeypatch.setattr(extract, "load_ocr_pages", lambda _k: [(1, "cached page")]) - - def _boom(*_a, **_k): - raise AssertionError("pdf_ocr must not run on a cache hit") - - monkeypatch.setattr( - extract, "get_services", lambda: SimpleNamespace(provider=SimpleNamespace(pdf_ocr=_boom)) - ) - - seen: dict[str, object] = {} - - async def _capture(page_texts, *_a, **_k): - seen["pages"] = list(page_texts) - return [] - - monkeypatch.setattr(extract, "chunk_and_embed_pages", _capture) - out = await extract._vision_ocr_fallback( - pdf, "scan.pdf", "pdf", on_progress=lambda *_a, **_k: None, quiet=True - ) - assert out == [] - assert seen["pages"] == [(1, "cached page")] - - -@pytest.mark.asyncio -async def test_vision_fallback_stores_fresh_ocr(tmp_path: Path, monkeypatch) -> None: - # On a miss, OCR runs and its per-page output is cached for the next retry. - pdf = tmp_path / "scan.pdf" - pdf.write_bytes(b"%PDF-1.4 fake") - monkeypatch.setattr(extract, "load_ocr_pages", lambda _k: None) - monkeypatch.setattr( - extract, - "get_services", - lambda: SimpleNamespace( - provider=SimpleNamespace(pdf_ocr=lambda *_a, **_k: [PageText(1, "fresh")]) - ), - ) - stored: dict[str, object] = {} - monkeypatch.setattr(extract, "store_ocr_pages", lambda _k, pages: stored.update(pages=pages)) - - async def _noop(_page_texts, *_a, **_k): - return [] - - monkeypatch.setattr(extract, "chunk_and_embed_pages", _noop) - await extract._vision_ocr_fallback( - pdf, "scan.pdf", "pdf", on_progress=lambda *_a, **_k: None, quiet=True - ) - assert stored["pages"] == [(1, "fresh")] diff --git a/tests/test_providers.py b/tests/test_providers.py index 5e18de242..c8a2cf6de 100644 --- a/tests/test_providers.py +++ b/tests/test_providers.py @@ -436,6 +436,28 @@ def test_routes_embed_to_local_engine_for_local_ref(self) -> None: result = rp.embed(["test"]) assert result == [[0.3, 0.4]] + def test_routes_count_tokens_to_local_engine_for_local_ref(self) -> None: + rp = self._make_provider() + mock_llama = mock.MagicMock() + mock_llama.count_tokens.return_value = 11 + rp._local = mock_llama + + cfg.embedding_model = ( + "nomic-ai/nomic-embed-text-v1.5-GGUF/nomic-embed-text-v1.5.Q4_K_M.gguf" + ) + assert rp.count_tokens("some text") == 11 + mock_llama.count_tokens.assert_called_once_with("some text") + + def test_routes_count_tokens_to_sdk_for_remote_ref(self) -> None: + rp = self._make_provider() + mock_sdk = mock.MagicMock() + mock_sdk.count_tokens.side_effect = NotImplementedError + rp._sdk_provider = mock_sdk + + cfg.embedding_model = "ollama/nomic-embed-text:latest" + with pytest.raises(NotImplementedError): + rp.count_tokens("some text") + def test_local_ref_never_falls_through_to_litellm(self) -> None: """Local HF refs stay on the local engine even when litellm is installed. @@ -3088,6 +3110,17 @@ def test_v1_models_sends_auth_header(self) -> None: assert headers.get("Authorization") == "Bearer sk-secret" +def test_sdk_provider_count_tokens_not_implemented() -> None: + """Cloud SDK backends have no local tokenizer, so count_tokens raises and chunk + sizing degrades to the character estimate.""" + from lilbee.providers.litellm_sdk import LitellmSdkBackend + from lilbee.providers.sdk_llm_provider import SdkLLMProvider + + provider = SdkLLMProvider(LitellmSdkBackend()) + with pytest.raises(NotImplementedError): + provider.count_tokens("hello") + + class TestSdkLLMProviderVisionOcr: """``SdkLLMProvider.vision_ocr`` translates to a multipart chat call.""" @@ -3594,65 +3627,6 @@ def test_rerank_with_empty_model_raises_provider_error(self) -> None: rp.rerank("q", ["a", "b"]) -class TestRoutingProviderPdfOcr: - """``RoutingProvider.pdf_ocr`` dispatches by ref prefix, like ``vision_ocr``.""" - - def test_native_ref_routes_to_local_engine(self) -> None: - from lilbee.providers.routing_provider import RoutingProvider - - rp = RoutingProvider() - mock_native = mock.MagicMock() - mock_native.pdf_ocr.return_value = ["p1", "p2"] - rp._local = mock_native - progress = mock.MagicMock() - native_ref = "org/Test-Vision-GGUF/test-vision-Q4_K_M.gguf" - - result = rp.pdf_ocr( - Path("/x.pdf"), - backend="vision", - model=native_ref, - per_page_timeout_s=12.5, - quiet=False, - on_progress=progress, - ) - - assert result == ["p1", "p2"] - mock_native.pdf_ocr.assert_called_once_with( - Path("/x.pdf"), - backend="vision", - model=native_ref, - per_page_timeout_s=12.5, - quiet=False, - on_progress=progress, - ) - - def test_hosted_ref_routes_to_sdk_which_raises(self) -> None: - """A hosted ``cfg.vision_model`` reaches the SDK side, which raises.""" - from lilbee.providers.routing_provider import RoutingProvider - - rp = RoutingProvider() - mock_sdk = mock.MagicMock() - mock_sdk.pdf_ocr.side_effect = NotImplementedError("hosted PDF OCR not supported") - rp._sdk_provider = mock_sdk - cfg.vision_model = "" - - with pytest.raises(NotImplementedError): - rp.pdf_ocr(Path("/x.pdf"), backend="vision", model="openai/gpt-4-vision") - mock_sdk.pdf_ocr.assert_called_once() - - -class TestSdkLLMProviderPdfOcr: - """``SdkLLMProvider.pdf_ocr`` cannot rasterise PDFs and must raise.""" - - def test_raises_not_implemented_with_user_facing_message(self) -> None: - from lilbee.providers.litellm_sdk import LitellmSdkBackend - from lilbee.providers.sdk_llm_provider import SdkLLMProvider - - provider = SdkLLMProvider(LitellmSdkBackend()) - with pytest.raises(NotImplementedError, match="LILBEE_VISION_MODEL"): - provider.pdf_ocr(Path("/scan.pdf"), backend="vision") - - class TestChatWithToolsRouting: def test_base_default_raises(self) -> None: from lilbee.providers.base import LLMProvider, ProviderError diff --git a/tests/test_query.py b/tests/test_query.py index b1a576f6f..49d2b706b 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -27,6 +27,8 @@ unique_sources, ) from lilbee.retrieval.query.searcher import ( + _CONTEXT_TEMPLATE_TOKENS, + _PER_SOURCE_TOKENS, EMPTY_LIBRARY, GROUNDED_REFUSAL, SEARCH_NEEDS_EMBEDDER, @@ -134,6 +136,17 @@ def _make_result( ) +def _fit( + results: list[SearchChunk], + system: str = "sys", + question: str = "q", + history: list | None = None, +) -> list[SearchChunk]: + """Sources kept by the budget fit, derived exactly as _finalize_context does.""" + searcher = get_services().searcher + return searcher._fit_to_budget(results, searcher._context_budget(system, question, history))[0] + + class TestDisplaySourcePath: """source citations render absolute paths with ~ expansion.""" @@ -163,10 +176,16 @@ def test_falls_back_to_raw_on_resolve_failure(self, tmp_path, monkeypatch): from lilbee.retrieval.query import display_source_path cfg.documents_dir = tmp_path / "docs" + target = cfg.documents_dir / "anything.md" + + # Raise only for the path under test; an unconditional patch also hits + # the config validator and blows up in fixture teardown. + real_resolve = _Path.resolve - # Force resolve() to raise so the fallback path runs. def _raise(self, strict=False): - raise OSError("simulated") + if self == target: + raise OSError("simulated") + return real_resolve(self, strict=strict) monkeypatch.setattr(_Path, "resolve", _raise) assert display_source_path("anything.md") == "anything.md" @@ -436,6 +455,29 @@ def test_header_plain_for_text(self): ctx = build_context(results) assert "[1] (notes.md)\nhello" in ctx + def test_header_omits_page_zero(self): + """PDF chunks whose page metadata was missing are stored with page 0. + The Sources block already suppresses that locator, so the prompt + header must too, or the model cites a page the user's list denies.""" + results = [ + _make_result( + source="scan.pdf", content_type="pdf", chunk="body", page_start=0, page_end=0 + ) + ] + ctx = build_context(results) + assert "[1] (scan.pdf)\nbody" in ctx + assert "page 0" not in ctx + + def test_header_omits_line_zero_for_code(self): + results = [ + _make_result( + source="app.py", content_type="code", chunk="x = 1", line_start=0, line_end=0 + ) + ] + ctx = build_context(results) + assert "[1] (app.py)\nx = 1" in ctx + assert "line 0" not in ctx + class TestCitedIndexExtraction: def test_single_brackets(self): @@ -501,6 +543,59 @@ def test_far_vector_only_rows_filtered_at_search(self, mock_svc): results = get_services().searcher.search("q") assert [r.source for r in results] == ["a.md"] + _TOC_CHUNK = "A. Summary ......... 1\nB. Intro ......... 3\nC. Trends ......... 9\n" + + def test_structural_chunk_the_query_missed_is_dropped(self, mock_svc): + """With the filter on, a lower-ranked TOC the query did not hit is + dropped, while the real answer passage survives.""" + cfg.filter_structural_chunks = True + real = _make_result(source="body.pdf", distance=0.1, chunk="Real answer prose.") + toc = _make_result(source="toc.pdf", distance=0.4, chunk=self._TOC_CHUNK) + mock_svc.store.search.return_value = [real, toc] + results = get_services().searcher.search("q") + assert [r.source for r in results] == ["body.pdf"] + + def test_structural_filter_runs_before_the_buffer_slice_so_real_passages_backfill( + self, mock_svc + ): + """The filter drops structural chunks from the candidate list before the + top_k*2 slice, so a real passage past the window backfills into it. If the + filter ran after the slice, the deeper real passage would be lost.""" + cfg.top_k = 1 # window is top_k*2 = 2 + cfg.filter_structural_chunks = True + real0 = _make_result(source="a.pdf", distance=0.1, chunk="Answer prose one.") + toc1 = _make_result(source="toc1.pdf", distance=0.2, chunk=self._TOC_CHUNK) + toc2 = _make_result(source="toc2.pdf", distance=0.3, chunk=self._TOC_CHUNK) + real3 = _make_result(source="b.pdf", distance=0.4, chunk="Answer prose two.") + mock_svc.store.search.return_value = [real0, toc1, toc2, real3] + sources = [r.source for r in get_services().searcher.search("q")] + assert "b.pdf" in sources # backfilled from past the pre-filter window + assert "toc1.pdf" not in sources and "toc2.pdf" not in sources + + def test_structural_filter_keeps_query_matched_page(self, mock_svc): + """A page the query lexically hit is never dropped, whatever its shape: + it is content the answer may need.""" + cfg.filter_structural_chunks = True + real = _make_result(source="body.pdf", distance=0.1, chunk="Real answer prose.") + hit = _make_result(source="toc.pdf", distance=0.4, bm25_score=9.0, chunk=self._TOC_CHUNK) + mock_svc.store.search.return_value = [real, hit] + assert "toc.pdf" in [r.source for r in get_services().searcher.search("q")] + + def test_structural_filter_keeps_top_hit(self, mock_svc): + """The top-ranked result is never dropped, whatever its shape.""" + cfg.filter_structural_chunks = True + toc = _make_result(source="toc.pdf", distance=0.05, chunk=self._TOC_CHUNK) + mock_svc.store.search.return_value = [toc] + assert [r.source for r in get_services().searcher.search("q")] == ["toc.pdf"] + + def test_structural_filter_off_by_default_keeps_toc(self, mock_svc): + """Off by default: the A/B found it net-negative on the eval corpus.""" + assert cfg.filter_structural_chunks is False + toc = _make_result(source="toc.pdf", distance=0.4, chunk=self._TOC_CHUNK) + real = _make_result(source="body.pdf", distance=0.1, chunk="Real answer prose.") + mock_svc.store.search.return_value = [real, toc] + assert "toc.pdf" in [r.source for r in get_services().searcher.search("q")] + def test_far_row_with_lexical_support_survives_search(self, mock_svc): """A both-arm row keeps its standing past max_distance: dropping it on vector distance alone would re-bury exactly the identifier hits @@ -827,7 +922,7 @@ def _restore_ctx(self): def test_trims_lowest_ranked_sources_to_fit(self, mock_svc): cfg.num_ctx = 1400 results = [_make_result(source=f"{i}.pdf", chunk="x" * 300) for i in range(5)] - kept = get_services().searcher._fit_context_budget(results, "sys", "q", None) + kept = _fit(results) assert 0 < len(kept) < len(results) assert kept == results[: len(kept)] # keeps the top-ranked prefix @@ -845,7 +940,7 @@ def test_fitted_context_survives_the_provider_that_enforces_the_window(self, moc system, question = "sys " * 40, "q " * 20 results = [_make_result(source=f"{i}.pdf", chunk="x" * 900) for i in range(5)] - kept = get_services().searcher._fit_context_budget(results, system, question, None) + kept = _fit(results, system, question) searcher = get_services().searcher assembled = ( @@ -862,12 +957,12 @@ def test_fitted_context_survives_the_provider_that_enforces_the_window(self, moc def test_keeps_all_when_budget_ample(self, mock_svc): cfg.num_ctx = 100_000 results = [_make_result(source=f"{i}.pdf", chunk="short") for i in range(5)] - assert get_services().searcher._fit_context_budget(results, "sys", "q", None) == results + assert _fit(results) == results def test_keeps_top_source_even_if_alone_over_budget(self, mock_svc): cfg.num_ctx = 1 results = [_make_result(source="big.pdf", chunk="x" * 9000), _make_result(source="b.pdf")] - kept = get_services().searcher._fit_context_budget(results, "sys", "q", None) + kept = _fit(results) assert len(kept) == 1 def test_history_counts_against_the_budget(self, mock_svc): @@ -877,18 +972,145 @@ def test_history_counts_against_the_budget(self, mock_svc): cfg.num_ctx = 3000 results = [_make_result(source=f"{i}.pdf", chunk="x" * 1200) for i in range(5)] history = [{"role": "user", "content": "h" * 3000}] - no_hist = get_services().searcher._fit_context_budget(results, "sys", "q", None) - with_hist = get_services().searcher._fit_context_budget(results, "sys", "q", history) + no_hist = _fit(results) + with_hist = _fit(results, history=history) assert len(with_hist) < len(no_hist) def test_logs_when_trimming(self, mock_svc, caplog): cfg.num_ctx = 1400 results = [_make_result(source=f"{i}.pdf", chunk="x" * 300) for i in range(5)] with caplog.at_level("INFO"): - get_services().searcher._fit_context_budget(results, "sys", "q", None) + _fit(results) assert "to fit the model context window" in caplog.text +class TestNeighborExpansion: + """Selected chunks widen with adjacent same-source chunks at answer time.""" + + def test_off_by_default_never_touches_neighbors(self, mock_svc): + mock_svc.store.search.return_value = [_make_result(chunk="core text")] + mock_svc.provider.chat.return_value = _text_result("answer") + result = get_services().searcher.ask_raw("q") + assert result.sources[0].chunk == "core text" + mock_svc.store.get_chunks_by_indices.assert_not_called() + + def test_widens_the_passage_and_the_prompt(self, mock_svc): + cfg.neighbor_expansion = 1 + mock_svc.store.search.return_value = [_make_result(chunk="core text", chunk_index=2)] + mock_svc.store.get_chunks_by_indices.return_value = [ + _make_result(chunk="before core", chunk_index=1), + _make_result(chunk="text after", chunk_index=3), + ] + mock_svc.provider.chat.return_value = _text_result("answer") + result = get_services().searcher.ask_raw("q") + assert result.sources[0].chunk == "before core text after" + assert result.sources[0].chunk_index == 2 + prompt = mock_svc.provider.chat.call_args[0][0][-1]["content"] + assert "before core text after" in prompt + + def test_widening_keeps_citation_numbering(self, mock_svc): + cfg.neighbor_expansion = 1 + mock_svc.store.search.return_value = [ + _make_result(source="a.pdf", chunk="alpha", chunk_index=5, distance=0.1), + _make_result(source="b.pdf", chunk="beta", chunk_index=0, distance=0.2), + ] + mock_svc.store.get_chunks_by_indices.return_value = [] + mock_svc.provider.chat.return_value = _text_result("From [1] and [2].") + answer = get_services().searcher.ask("q") + assert "1. [a.pdf](file://" in answer + assert "2. [b.pdf](file://" in answer + + def test_sources_block_shows_the_widened_page_span(self, mock_svc): + cfg.neighbor_expansion = 1 + mock_svc.store.search.return_value = [ + _make_result(chunk="core", chunk_index=2, page_start=3, page_end=3) + ] + mock_svc.store.get_chunks_by_indices.return_value = [ + _make_result(chunk="prev", chunk_index=1, page_start=2, page_end=2), + _make_result(chunk="next", chunk_index=3, page_start=4, page_end=4), + ] + mock_svc.provider.chat.return_value = _text_result("answer") + answer = get_services().searcher.ask("q") + assert "pages 2-4" in answer + + def test_tight_budget_sheds_expansion_never_the_original(self, mock_svc): + cfg.neighbor_expansion = 1 + cfg.num_ctx = 1200 + mock_svc.store.search.return_value = [_make_result(chunk="x" * 300, chunk_index=2)] + mock_svc.store.get_chunks_by_indices.return_value = [ + _make_result(chunk="y" * 300, chunk_index=1), + _make_result(chunk="z" * 300, chunk_index=3), + ] + mock_svc.provider.chat.return_value = _text_result("answer") + result = get_services().searcher.ask_raw("q") + assert result.sources[0].chunk == "x" * 300 + + def test_overflow_retry_sheds_expansion_not_an_original(self, mock_svc): + """On the context-overflow retry the tighter refit must start from the + pre-widen originals, so it sheds a chunk's neighbor filler before it + drops a whole lower-ranked original chunk.""" + from lilbee.providers.base import ProviderError, ProviderErrorKind + + cfg.neighbor_expansion = 1 + cfg.num_ctx = 4096 + mock_svc.provider.served_chat_ctx.return_value = None + a = _make_result(source="a.pdf", chunk="a" * 300, chunk_index=2, distance=0.1) + b = _make_result(source="b.pdf", chunk="b" * 300, chunk_index=0, distance=0.2) + mock_svc.store.search.return_value = [a, b] + + def neighbors(source, indices): + if source == "a.pdf": + return [ + _make_result(source="a.pdf", chunk="n" * 3000, chunk_index=1), + _make_result(source="a.pdf", chunk="m" * 3000, chunk_index=3), + ] + return [] + + mock_svc.store.get_chunks_by_indices.side_effect = neighbors + mock_svc.provider.chat.side_effect = [ + ProviderError("overflow", kind=ProviderErrorKind.CONTEXT_OVERFLOW), + _text_result("fits now"), + ] + result = get_services().searcher.ask_raw("q") + assert mock_svc.provider.chat.call_count == 2 + # The lower-ranked original b.pdf must survive; only a.pdf's expansion sheds. + assert {r.source for r in result.sources} == {"a.pdf", "b.pdf"} + + def test_widened_prompt_still_fits_the_provider_ceiling(self, mock_svc): + """Widening runs after the budget fit, so it must budget against the same + provider ceiling: the assembled widened prompt must not exceed + prompt_token_budget, and expansion must actually happen (non-vacuous).""" + from lilbee.providers.base import prompt_token_budget + + ctx = 4096 + cfg.neighbor_expansion = 1 + cfg.num_ctx = ctx + mock_svc.provider.served_chat_ctx.return_value = None + mock_svc.store.get_chunks_by_indices.return_value = [ + _make_result(chunk="b" * 3000, chunk_index=1, page_start=2, page_end=2), + _make_result(chunk="a" * 3000, chunk_index=3, page_start=4, page_end=4), + ] + system, question = "sys " * 40, "q " * 20 + base = [_make_result(chunk="c" * 1500, chunk_index=2, page_start=3, page_end=3)] + + searcher = get_services().searcher + budget = searcher._context_budget(system, question, None, 1.0) + fitted, used = searcher._fit_to_budget(base, budget) + widened = searcher._widen_with_neighbors(fitted, max(0, budget - used)) + + # Non-vacuous: at least one neighbor was actually merged in. + assert widened[0].chunk != "c" * 1500 + assembled = ( + searcher._budget_tokens(system) + + searcher._budget_tokens(question) + + sum(searcher._budget_tokens(r.chunk) + _PER_SOURCE_TOKENS for r in widened) + + _CONTEXT_TEMPLATE_TOKENS + ) + assert assembled <= prompt_token_budget(ctx), ( + f"widened prompt {assembled} tokens exceeds provider ceiling {prompt_token_budget(ctx)}" + ) + + class TestAskRaw: def test_returns_structured_result(self, mock_svc): mock_svc.store.search.return_value = [_make_result(chunk="oil is 5 quarts")] @@ -1514,6 +1736,17 @@ def test_passage_is_embedded_as_a_query(self, mock_svc): get_services().searcher._hyde_search("explain X", top_k=5) mock_svc.embedder.embed_query.assert_called_once_with("hypothetical passage") + def test_spends_its_own_token_budget(self, mock_svc): + """A generated answer passage and a list of query variants are not the + same length of output, so they must not share one cap: retuning either + one through a shared constant silently retunes the other.""" + from lilbee.retrieval.query.expansion import HYDE_MAX_TOKENS + + mock_svc.provider.chat.return_value = _text_result("hypothetical passage") + mock_svc.store.search.return_value = [] + get_services().searcher._hyde_search("explain X", top_k=5) + assert mock_svc.provider.chat.call_args.kwargs["options"]["num_predict"] == HYDE_MAX_TOKENS + def test_returns_empty_on_error(self, mock_svc): mock_svc.provider.chat.side_effect = RuntimeError("fail") assert get_services().searcher._hyde_search("test", top_k=5) == [] @@ -2052,7 +2285,7 @@ def test_named_document_bypasses_similarity_search(self, mock_svc): ] rag = get_services().searcher.build_rag_context("summarize survey_report.pdf") assert rag is not None - results, _ = rag + results = rag.results assert [r.chunk for r in results] == ["first", "second"] assert all(r.score == 1.0 for r in results) mock_svc.store.search.assert_not_called() @@ -2099,7 +2332,7 @@ def test_routed_document_fits_the_served_context_window(self, mock_svc): cfg.num_ctx = None rag = get_services().searcher.build_rag_context("summarize survey_report.pdf") assert rag is not None - results, messages = rag + results, messages = rag.results, rag.messages prompt_tokens = sum(len(m["content"]) // 4 for m in messages) assert prompt_tokens <= 8192 # Document order preserved after trimming: the head survives. @@ -2119,7 +2352,7 @@ def test_dense_text_budgets_conservatively(self, mock_svc): cfg.num_ctx = None rag = get_services().searcher.build_rag_context("summarize survey_report.pdf") assert rag is not None - _, messages = rag + messages = rag.messages # At 2.56 chars/token the real prompt cost must still fit the window. real_tokens = sum(len(m["content"]) / 2.56 for m in messages) assert real_tokens <= 24576 * 1.05 @@ -2159,7 +2392,7 @@ def test_budget_falls_back_to_config_when_served_ctx_unknown(self, mock_svc): finally: cfg.num_ctx = None assert rag is not None - _, messages = rag + messages = rag.messages assert sum(len(m["content"]) // 4 for m in messages) <= 4096 def test_docket_reference_resolves_by_content_concentration(self, mock_svc): @@ -2430,7 +2663,7 @@ def test_follow_up_is_rewritten_for_retrieval(self, mock_svc): finally: cfg.query_expansion_count = 3 assert rag is not None - _, messages = rag + messages = rag.messages assert mock_svc.store.search.call_args[1]["query_text"] == rewritten assert "and when was it written?" in messages[-1]["content"] assert rewritten not in messages[-1]["content"] @@ -2834,7 +3067,7 @@ def test_build_rag_context_default_is_mixed_pool(self, mock_svc): mock_svc.store.search.return_value = [wiki_chunk, raw_chunk] result = get_services().searcher.build_rag_context("question") assert result is not None - chunks, _ = result + chunks = result.results assert len(chunks) == 2 def test_build_rag_context_forwards_chunk_type_to_store(self, mock_svc): @@ -3246,6 +3479,30 @@ def test_prose_word_stem_does_not_false_positive(self): answer = "The witness notes that the repair happened in March." assert cited_subset(answer, sources) == [] + def test_stem_embedded_in_a_longer_identifier_is_not_cited(self): + """Substring containment marks 'log-1' cited by an answer that only + discusses 'catalog-10'. cited_sources is the grounding signal JSON + consumers read, so a false positive inflates grounding.""" + from lilbee.retrieval.query.formatting import cited_subset + + sources = [_make_result(source="log-1.md", chunk_index=0)] + answer = "The catalog-10.pdf entry lists the part." + assert cited_subset(answer, sources) == [] + + def test_name_embedded_in_a_longer_filename_is_not_cited(self): + from lilbee.retrieval.query.formatting import cited_subset + + sources = [_make_result(source="notes.md", chunk_index=0)] + answer = "See footnotes.md for the caveats." + assert cited_subset(answer, sources) == [] + + def test_name_mention_still_counts_next_to_punctuation(self): + from lilbee.retrieval.query.formatting import cited_subset + + sources = [_make_result(source="log-1.md", chunk_index=0)] + for answer in ("As log-1.md shows,", "(log-1.md)", "see log-1.md."): + assert len(cited_subset(answer, sources)) == 1, answer + def test_marker_and_name_citations_combine(self): from lilbee.retrieval.query.formatting import cited_subset @@ -3295,6 +3552,22 @@ def test_preserves_heading_followed_by_prose(self): text = "The paper has three parts.\n\nReferences:\nIt lists 40 works." assert strip_llm_citations(text) == text + def test_removes_a_block_that_starts_the_answer(self): + """An answer that is nothing but a fabricated citation block must not + stream through: lilbee stacks its authoritative list underneath, which + is the double-list shape the filter exists to prevent.""" + text = "Sources:\n- fake.pdf\n- other.pdf" + assert strip_llm_citations(text) == "" + + def test_removes_a_bare_heading_that_starts_the_answer(self): + assert strip_llm_citations("Sources:") == "" + + def test_keeps_prose_that_follows_a_mid_answer_block(self): + """The block is removed, not everything after it: a model that emits a + citation list and then keeps answering must not lose the continuation.""" + text = "Answer.\n\nSources:\n- a.pdf\n\nAdditionally, more prose here." + assert strip_llm_citations(text) == "Answer.\n\nAdditionally, more prose here." + def test_removes_dangling_heading(self): text = "The answer is 42.\n\nSources:\n" assert strip_llm_citations(text) == "The answer is 42." @@ -3375,7 +3648,7 @@ def test_filters_high_distance_results(self, mock_svc): mock_svc.store.search.return_value = [close, far] result = get_services().searcher.build_rag_context("question") assert result is not None - results, _ = result + results = result.results sources = [r.source for r in results] assert "close.pdf" in sources assert "far.pdf" not in sources diff --git a/tests/test_reasoning.py b/tests/test_reasoning.py index c14073efb..d3517e9cb 100644 --- a/tests/test_reasoning.py +++ b/tests/test_reasoning.py @@ -8,6 +8,7 @@ from lilbee.providers.model_defaults import ModelDefaults from lilbee.retrieval.reasoning import ( CAP_CONTINUATION_PROMPT, + REASONING_EXHAUSTED_NOTICE, CapNotice, StreamToken, effective_reasoning_cap, @@ -327,6 +328,62 @@ def test_no_cap_fire_yields_only_stream_tokens(self): ) assert "the answer" in response + def test_reasoning_only_run_closes_with_the_exhausted_notice(self): + """A model that spends the whole generation inside leaves the + sync path with zero visible tokens: the answer would be just the Sources + block with nothing explaining why. The HTTP path already closes such a + run with the notice; this is the same close.""" + provider = self._make_provider(["thinking forever"]) + events = list( + stream_chat_with_cap( + provider, + [{"role": "user", "content": "hi"}], + options=None, + model="test-model", + show_reasoning=False, + cap_chars=0, + ) + ) + answer = "".join( + e.content for e in events if isinstance(e, StreamToken) and not e.is_reasoning + ) + assert answer == REASONING_EXHAUSTED_NOTICE + + def test_an_answered_run_does_not_get_the_notice(self): + provider = self._make_provider(["brief", "the answer"]) + events = list( + stream_chat_with_cap( + provider, + [{"role": "user", "content": "hi"}], + options=None, + model="test-model", + show_reasoning=False, + cap_chars=0, + ) + ) + answer = "".join( + e.content for e in events if isinstance(e, StreamToken) and not e.is_reasoning + ) + assert answer == "the answer" + + def test_a_silent_continuation_still_closes_with_the_notice(self): + long_think = "" + ("x " * 400) + "" + provider = self._make_provider([long_think], [""]) + events = list( + stream_chat_with_cap( + provider, + [{"role": "user", "content": "explain X"}], + options=None, + model="test-model", + show_reasoning=False, + cap_chars=512, + ) + ) + answer = "".join( + e.content for e in events if isinstance(e, StreamToken) and not e.is_reasoning + ) + assert answer == REASONING_EXHAUSTED_NOTICE + def test_cap_fire_emits_notice_then_continuation_tokens(self): long_think = "" + ("x " * 400) + "not reached" provider = self._make_provider([long_think], ["final ", "answer."]) @@ -349,6 +406,76 @@ def test_cap_fire_emits_notice_then_continuation_tokens(self): assert "final " in response and "answer." in response assert provider.chat.call_count == 2 + def test_continuation_reasoning_is_filtered_out(self): + """The cap fires because the model over-thinks, and R1/Qwen3-style + templates can force-open a block in the continuation whatever + the nudge says. That reasoning must not stream into the visible + answer -- the HTTP path parses the continuation the same way.""" + long_think = "" + ("x " * 400) + "" + provider = self._make_provider( + [long_think], ["still ", "musing", "final answer."] + ) + events = list( + stream_chat_with_cap( + provider, + [{"role": "user", "content": "q"}], + options=None, + model="test-model", + show_reasoning=False, + cap_chars=512, + ) + ) + response = "".join( + e.content for e in events if isinstance(e, StreamToken) and not e.is_reasoning + ) + assert "final answer." in response + assert "musing" not in response + assert "" not in response + + def test_continuation_reasoning_stays_marked_as_answer_text(self): + """With show_reasoning on, the continuation's reasoning is still + surfaced (nothing is dropped silently), but every continuation token + is final-answer text, matching the HTTP path.""" + long_think = "" + ("x " * 400) + "" + provider = self._make_provider([long_think], ["hmm", "answer."]) + events = list( + stream_chat_with_cap( + provider, + [{"role": "user", "content": "q"}], + options=None, + model="test-model", + show_reasoning=True, + cap_chars=512, + ) + ) + after_notice = events[events.index(next(e for e in events if isinstance(e, CapNotice))) :] + continuation = [e for e in after_notice if isinstance(e, StreamToken)] + assert continuation, "continuation produced no tokens" + assert all(not e.is_reasoning for e in continuation) + assert "answer." in "".join(e.content for e in continuation) + + def test_continuation_is_not_re_capped(self): + """The cap already fired once; the continuation must run to + completion rather than being cut off by a second cap.""" + long_think = "" + ("x " * 400) + "" + continuation = ["" + ("y " * 400) + "", "the real answer"] + provider = self._make_provider([long_think], continuation) + events = list( + stream_chat_with_cap( + provider, + [{"role": "user", "content": "q"}], + options=None, + model="test-model", + show_reasoning=False, + cap_chars=512, + ) + ) + assert len([e for e in events if isinstance(e, CapNotice)]) == 1 + response = "".join( + e.content for e in events if isinstance(e, StreamToken) and not e.is_reasoning + ) + assert "the real answer" in response + def test_cap_fire_closes_the_first_stream_through_text_only(self): # On cap-fire the first stream is closed via _text_only's # forwarded close, or its HTTP connection / in_flight slot leaks until GC. diff --git a/tests/test_reranker.py b/tests/test_reranker.py index 97d7abce7..22dfb18e0 100644 --- a/tests/test_reranker.py +++ b/tests/test_reranker.py @@ -190,6 +190,27 @@ def explode(query: str, cands: list[str]) -> list[float]: assert [c.chunk for c in out] == ["A", "B"] assert all(r.rerank_score is None for r in out) + @pytest.mark.parametrize("returned", [[0.9], [0.9, 0.5, 0.1], []]) + def test_score_count_mismatch_skips_the_pass(self, reranker, returned, caplog): + """A provider returning the wrong number of scores is provider + misbehavior, which the pass is meant to contain: skip reranking and + keep the retrieval order rather than killing the whole turn.""" + cfg.reranker_model = _RERANKER_MODEL + results = [_chunk("a.md", "A"), _chunk("b.md", "B")] + with _patch_provider(lambda query, cands: returned), caplog.at_level("WARNING"): + out = reranker.rerank("test", results) + assert [c.chunk for c in out] == ["A", "B"] + assert all(r.rerank_score is None for r in out) + assert any("score" in rec.message for rec in caplog.records) + + def test_score_count_mismatch_skips_the_pass_without_blending(self, reranker): + cfg.reranker_model = _RERANKER_MODEL + cfg.rerank_blend = False + results = [_chunk("a.md", "A"), _chunk("b.md", "B")] + with _patch_provider(lambda query, cands: [0.9]): + out = reranker.rerank("test", results) + assert [c.chunk for c in out] == ["A", "B"] + def test_stamps_rerank_score_on_candidates_only(self, reranker): cfg.reranker_model = _RERANKER_MODEL results = [ diff --git a/tests/test_server_handlers.py b/tests/test_server_handlers.py index 8641d9768..5234f3a25 100644 --- a/tests/test_server_handlers.py +++ b/tests/test_server_handlers.py @@ -13,6 +13,7 @@ from lilbee.data.ingest import SyncResult from lilbee.data.store import SearchChunk from lilbee.providers.base import ProviderError, ProviderErrorKind +from lilbee.retrieval.query.searcher import RagContext from lilbee.runtime.progress import SseErrorCode from lilbee.server import handlers from lilbee.server.handlers import ( @@ -42,9 +43,11 @@ def _rag_return(chunks: list[SearchChunk] | None = None): """Build a mock build_rag_context return value.""" + from lilbee.retrieval.query.searcher import RagContext + results = chunks or [_SAMPLE_CHUNK] messages = [{"role": "system", "content": "test"}, {"role": "user", "content": "q"}] - return results, messages + return RagContext(results, messages) @pytest.fixture(autouse=True) @@ -498,7 +501,7 @@ async def test_sources_event_falls_back_to_full_set_when_uncited(self, mock_svc) retrieved set, mirroring Searcher.ask_stream's ``used if used else results``.""" a = _SAMPLE_CHUNK.model_copy(update={"source": "a.md", "chunk_index": 0}) b = _SAMPLE_CHUNK.model_copy(update={"source": "b.md", "chunk_index": 1}) - mock_svc.searcher.build_rag_context.return_value = ([a, b], []) + mock_svc.searcher.build_rag_context.return_value = RagContext([a, b], []) mock_svc.provider.chat.return_value = iter(["an answer with no citation markers"]) events = [e async for e in handlers.ask_stream("question")] sources_event = next(e for e in events if e and e.startswith("event: sources")) @@ -523,7 +526,7 @@ async def test_sources_event_carries_cited_subset(self, mock_svc): matching the non-stream cited_sources contract, not the full retrieved list.""" cited = _SAMPLE_CHUNK.model_copy(update={"source": "cited.md", "chunk_index": 0}) other = _SAMPLE_CHUNK.model_copy(update={"source": "other.md", "chunk_index": 1}) - mock_svc.searcher.build_rag_context.return_value = ([cited, other], []) + mock_svc.searcher.build_rag_context.return_value = RagContext([cited, other], []) mock_svc.provider.chat.return_value = iter(["see [1] for details"]) events = [e async for e in handlers.ask_stream("question")] sources_event = next(e for e in events if e and e.startswith("event: sources")) @@ -534,7 +537,7 @@ async def test_model_sources_block_suppressed_in_grounded_stream(self, mock_svc) """A model that appends its own Sources block must not double up with the authoritative SOURCES event: no token frame carries the model's list.""" cited = _SAMPLE_CHUNK.model_copy(update={"source": "real.md", "chunk_index": 0}) - mock_svc.searcher.build_rag_context.return_value = ([cited], []) + mock_svc.searcher.build_rag_context.return_value = RagContext([cited], []) mock_svc.provider.chat.return_value = iter( ["Grounded answer [1].", "\n\nSources:\n- invented.md"] ) @@ -555,7 +558,7 @@ async def test_grounded_stream_releases_held_back_final_line(self, mock_svc): """The citation filter holds the last line until the stream ends; the flushed tail must still be emitted as a token before SOURCES.""" cited = _SAMPLE_CHUNK.model_copy(update={"source": "real.md", "chunk_index": 0}) - mock_svc.searcher.build_rag_context.return_value = ([cited], []) + mock_svc.searcher.build_rag_context.return_value = RagContext([cited], []) mock_svc.provider.chat.return_value = iter(["First line [1].\n", "Held final line."]) events = [e async for e in handlers.ask_stream("question")] token_text = "".join( @@ -570,7 +573,7 @@ async def test_grounded_stream_forwards_reasoning_tokens(self, mock_svc, monkeyp channel and stays out of the answer (and the citation filter).""" monkeypatch.setattr(cfg, "show_reasoning", True) cited = _SAMPLE_CHUNK.model_copy(update={"source": "real.md", "chunk_index": 0}) - mock_svc.searcher.build_rag_context.return_value = ([cited], []) + mock_svc.searcher.build_rag_context.return_value = RagContext([cited], []) mock_svc.provider.chat.return_value = iter(["pondering", "answer [1]"]) events = [e async for e in handlers.ask_stream("question")] reasoning_text = "".join( @@ -1028,7 +1031,7 @@ async def test_sources_event_carries_cited_subset(self, mock_svc, monkeypatch): matching the non-stream cited_sources contract, not the full retrieved list.""" cited = _SAMPLE_CHUNK.model_copy(update={"source": "cited.md", "chunk_index": 0}) other = _SAMPLE_CHUNK.model_copy(update={"source": "other.md", "chunk_index": 1}) - mock_svc.searcher.build_rag_context.return_value = ([cited, other], []) + mock_svc.searcher.build_rag_context.return_value = RagContext([cited, other], []) monkeypatch.setattr( _rag_h, "dispatch_chat_stream", lambda req: _canonical_text_stream(["see [1] here"]) ) @@ -1042,7 +1045,7 @@ async def test_sources_event_falls_back_to_full_set_when_uncited(self, mock_svc, retrieved set, mirroring Searcher.ask_stream's ``used if used else results``.""" a = _SAMPLE_CHUNK.model_copy(update={"source": "a.md", "chunk_index": 0}) b = _SAMPLE_CHUNK.model_copy(update={"source": "b.md", "chunk_index": 1}) - mock_svc.searcher.build_rag_context.return_value = ([a, b], []) + mock_svc.searcher.build_rag_context.return_value = RagContext([a, b], []) monkeypatch.setattr( _rag_h, "dispatch_chat_stream", lambda req: _canonical_text_stream(["no markers here"]) ) @@ -1055,7 +1058,7 @@ async def test_model_sources_block_suppressed_in_grounded_stream(self, mock_svc, """A model Sources block in chat streaming is dropped so it doesn't double up with the authoritative SOURCES event.""" cited = _SAMPLE_CHUNK.model_copy(update={"source": "real.md", "chunk_index": 0}) - mock_svc.searcher.build_rag_context.return_value = ([cited], []) + mock_svc.searcher.build_rag_context.return_value = RagContext([cited], []) monkeypatch.setattr( _rag_h, "dispatch_chat_stream", @@ -1077,7 +1080,7 @@ async def test_model_sources_block_suppressed_in_grounded_stream(self, mock_svc, async def test_chat_stream_releases_held_back_final_line(self, mock_svc, monkeypatch): """The chat SSE path also flushes the filter's held-back tail as a token.""" cited = _SAMPLE_CHUNK.model_copy(update={"source": "real.md", "chunk_index": 0}) - mock_svc.searcher.build_rag_context.return_value = ([cited], []) + mock_svc.searcher.build_rag_context.return_value = RagContext([cited], []) monkeypatch.setattr( _rag_h, "dispatch_chat_stream", diff --git a/tests/test_services.py b/tests/test_services.py index e9a897c5f..3749ea5b4 100644 --- a/tests/test_services.py +++ b/tests/test_services.py @@ -18,6 +18,194 @@ def isolated_cfg(): setattr(cfg, name, getattr(snapshot, name)) +class TestSyncVisionOcrBackend: + def _patch_xberg(self, monkeypatch, *, listed): + reg = MagicMock() + unreg = MagicMock() + monkeypatch.setattr("xberg.list_ocr_backends", lambda: listed) + monkeypatch.setattr("xberg.register_ocr_backend", reg) + monkeypatch.setattr("xberg.unregister_ocr_backend", unreg) + return reg, unreg + + def test_registers_when_model_set_and_absent(self, monkeypatch): + from lilbee.app.services import sync_vision_ocr_backend + + monkeypatch.setattr(cfg, "vision_model", "vendor/glm-ocr") + reg, unreg = self._patch_xberg(monkeypatch, listed=["tesseract"]) + sync_vision_ocr_backend(MagicMock()) + reg.assert_called_once() + unreg.assert_not_called() + + def test_rebinds_to_current_provider_when_already_registered(self, monkeypatch): + """A rebuilt provider must replace the stale binding: unregister then re-register. + + ``reset_services`` shuts the old provider down; if sync left the prior + registration in place, xberg would keep routing OCR to the dead provider. + """ + from lilbee.app.services import sync_vision_ocr_backend + + monkeypatch.setattr(cfg, "vision_model", "vendor/glm-ocr") + reg, unreg = self._patch_xberg(monkeypatch, listed=["lilbee-vision"]) + sync_vision_ocr_backend(MagicMock()) + unreg.assert_called_once_with("lilbee-vision") + reg.assert_called_once() + + def test_unregisters_when_model_cleared(self, monkeypatch): + from lilbee.app.services import sync_vision_ocr_backend + + monkeypatch.setattr(cfg, "vision_model", "") + reg, unreg = self._patch_xberg(monkeypatch, listed=["lilbee-vision"]) + sync_vision_ocr_backend(MagicMock()) + unreg.assert_called_once_with("lilbee-vision") + reg.assert_not_called() + + def test_noop_when_no_model_and_absent(self, monkeypatch): + from lilbee.app.services import sync_vision_ocr_backend + + monkeypatch.setattr(cfg, "vision_model", "") + reg, unreg = self._patch_xberg(monkeypatch, listed=["tesseract"]) + sync_vision_ocr_backend(MagicMock()) + reg.assert_not_called() + unreg.assert_not_called() + + def test_settings_role_reload_syncs_vision_backend(self, monkeypatch): + """A vision_model change via any settings path (REST/MCP/TUI/CLI) registers it.""" + from lilbee.app.services import set_services + from lilbee.app.settings import _reload_changed_roles + from tests.conftest import make_mock_services + + set_services(make_mock_services()) + try: + monkeypatch.setattr(cfg, "vision_model", "org/V-GGUF/v-Q4_K_M.gguf") + reg, _unreg = self._patch_xberg(monkeypatch, listed=["tesseract"]) + _reload_changed_roles({"vision_model"}) + reg.assert_called_once() + finally: + set_services(None) + + +class TestSyncEmbeddingBackend: + def _patch_xberg(self, monkeypatch, *, listed): + reg = MagicMock() + unreg = MagicMock() + monkeypatch.setattr("xberg.list_embedding_backends", lambda: listed) + monkeypatch.setattr("xberg.register_embedding_backend", reg) + monkeypatch.setattr("xberg.unregister_embedding_backend", unreg) + return reg, unreg + + def test_registers_when_absent(self, monkeypatch): + from lilbee.app.services import sync_embedding_backend + + reg, unreg = self._patch_xberg(monkeypatch, listed=[]) + sync_embedding_backend(MagicMock()) + reg.assert_called_once() + unreg.assert_not_called() + + def test_rebinds_to_current_provider_when_already_registered(self, monkeypatch): + """A rebuilt provider must replace the stale binding: unregister then re-register, + else xberg keeps embedding through the shut-down provider after reset_services.""" + from lilbee.app.services import sync_embedding_backend + + reg, unreg = self._patch_xberg(monkeypatch, listed=["lilbee"]) + sync_embedding_backend(MagicMock()) + unreg.assert_called_once_with("lilbee") + reg.assert_called_once() + + def test_registered_backend_routes_to_provider_embed(self, monkeypatch): + """The registered backend must call provider.embed and report cfg.embedding_dim.""" + from lilbee.app.services import sync_embedding_backend + + reg, _unreg = self._patch_xberg(monkeypatch, listed=[]) + monkeypatch.setattr(cfg, "embedding_dim", 7) + provider = MagicMock() + provider.embed.return_value = [[0.0] * 7] + sync_embedding_backend(provider) + backend = reg.call_args.args[0] + assert backend.name() == "lilbee" + assert backend.dimensions() == 7 + assert backend.embed(["hello"]) == [[0.0] * 7] + provider.embed.assert_called_once_with(["hello"]) + # Lifecycle hooks are no-ops but must exist (xberg calls initialize). + assert backend.initialize() is None + assert backend.shutdown() is None + + +class TestSyncTokenizerBackend: + def _patch_xberg(self, monkeypatch, *, listed): + reg = MagicMock() + unreg = MagicMock() + monkeypatch.setattr("xberg.list_tokenizer_backends", lambda: listed) + monkeypatch.setattr("xberg.register_tokenizer_backend", reg) + monkeypatch.setattr("xberg.unregister_tokenizer_backend", unreg) + return reg, unreg + + def test_registers_when_enabled_and_absent(self, monkeypatch): + from lilbee.app.services import sync_tokenizer_backend + + monkeypatch.setattr(cfg, "token_sizing", True) + reg, unreg = self._patch_xberg(monkeypatch, listed=[]) + sync_tokenizer_backend(MagicMock()) + reg.assert_called_once() + unreg.assert_not_called() + + def test_rebinds_to_current_provider_when_already_registered(self, monkeypatch): + """A rebuilt provider must replace the stale binding: unregister then re-register, + else xberg keeps counting through the shut-down provider after reset_services.""" + from lilbee.app.services import sync_tokenizer_backend + + monkeypatch.setattr(cfg, "token_sizing", True) + reg, unreg = self._patch_xberg(monkeypatch, listed=["lilbee"]) + sync_tokenizer_backend(MagicMock()) + unreg.assert_called_once_with("lilbee") + reg.assert_called_once() + + def test_unregisters_when_disabled(self, monkeypatch): + from lilbee.app.services import sync_tokenizer_backend + + monkeypatch.setattr(cfg, "token_sizing", False) + reg, unreg = self._patch_xberg(monkeypatch, listed=["lilbee"]) + sync_tokenizer_backend(MagicMock()) + unreg.assert_called_once_with("lilbee") + reg.assert_not_called() + + def test_noop_when_disabled_and_absent(self, monkeypatch): + from lilbee.app.services import sync_tokenizer_backend + + monkeypatch.setattr(cfg, "token_sizing", False) + reg, unreg = self._patch_xberg(monkeypatch, listed=[]) + sync_tokenizer_backend(MagicMock()) + reg.assert_not_called() + unreg.assert_not_called() + + def test_registered_backend_routes_to_provider_count_tokens(self, monkeypatch): + from lilbee.app.services import sync_tokenizer_backend + + monkeypatch.setattr(cfg, "token_sizing", True) + reg, _unreg = self._patch_xberg(monkeypatch, listed=[]) + provider = MagicMock() + provider.count_tokens.return_value = 42 + sync_tokenizer_backend(provider) + backend = reg.call_args.args[0] + assert backend.name() == "lilbee" + assert backend.count_tokens("hello") == 42 + provider.count_tokens.assert_called_once_with("hello") + + def test_settings_change_syncs_tokenizer_backend(self, monkeypatch): + """Toggling token_sizing via any settings path re-syncs the backend.""" + from lilbee.app.services import set_services + from lilbee.app.settings import _invalidate_caches + from tests.conftest import make_mock_services + + set_services(make_mock_services()) + try: + monkeypatch.setattr(cfg, "token_sizing", True) + reg, _unreg = self._patch_xberg(monkeypatch, listed=[]) + _invalidate_caches({"token_sizing"}) + reg.assert_called_once() + finally: + set_services(None) + + class TestServicesDataclass: def test_fields_are_immutable(self): from lilbee.app.services import CrawlerSyncState, Services @@ -258,3 +446,49 @@ def test_no_op_when_services_uncached(self): assert services_mod.peek_services() is None reset_store() assert services_mod.peek_services() is None + + +class TestGetServicesThreadSafety: + def test_concurrent_first_touch_builds_one_container(self, monkeypatch): + """Concurrent first ``get_services()`` calls must build exactly one container. + + A duplicate concurrent build re-registers xberg's process-global backends + mid-flight, and the losing thread raises 'Embedding backend already + registered' -- surfacing as failed download tasks when several workers + first-touch services at once.""" + import threading + import time + + from lilbee.app import services as services_mod + from tests.conftest import make_mock_services + + builds: list[int] = [] + + def slow_build(config, provider=None, registry=None): + builds.append(threading.get_ident()) + time.sleep(0.05) + return make_mock_services() + + monkeypatch.setattr(services_mod, "build_services", slow_build) + monkeypatch.setattr("lilbee.app.settings.reconcile_embedding_dim", lambda registry: None) + monkeypatch.setattr(cfg, "worker_pool_eager_start", False) + services_mod._state.singleton = None + + results: list[object] = [] + errors: list[BaseException] = [] + + def touch() -> None: + try: + results.append(services_mod.get_services()) + except BaseException as exc: + errors.append(exc) + + threads = [threading.Thread(target=touch) for _ in range(4)] + for t in threads: + t.start() + for t in threads: + t.join() + + assert not errors + assert len(builds) == 1 + assert len({id(r) for r in results}) == 1 diff --git a/tests/test_settings.py b/tests/test_settings.py index 20f167557..e78f581d2 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -4,6 +4,8 @@ import pytest +from lilbee.app.settings_map import SETTINGS_MAP, get_default +from lilbee.config_meta import WRITABLE_CONFIG_FIELDS from lilbee.core import settings @@ -207,12 +209,37 @@ def test_reranker_type_is_load_affecting(self): assert "reranker_type" in LOAD_AFFECTING_KEYS def test_reranker_fields_in_settings_map(self): - from lilbee.app.settings_map import SETTINGS_MAP assert "reranker_type" in SETTINGS_MAP assert "reranker_prompt" in SETTINGS_MAP assert SETTINGS_MAP["reranker_type"].choices == ("auto", "cross_encoder", "llm") + def test_neighbor_expansion_in_settings_map(self): + + defn = SETTINGS_MAP["neighbor_expansion"] + assert defn.writable is True + assert defn.nullable is False + assert defn.group == "Retrieval" + assert get_default("neighbor_expansion") == 0 + + def test_fusion_knobs_in_settings_map(self): + """The four adaptive-fusion / structural-filter knobs (which gate the + on-by-default fusion behavior) are on the settings surface with their + shipped defaults, so a dropped or typo'd entry fails CI.""" + + assert get_default("lexical_fusion_weight") == 1.0 + assert get_default("adaptive_fusion") is True + assert get_default("adaptive_fusion_margin") == 0.15 + assert get_default("filter_structural_chunks") is False + for key in ( + "lexical_fusion_weight", + "adaptive_fusion", + "adaptive_fusion_margin", + "filter_structural_chunks", + ): + assert SETTINGS_MAP[key].writable is True, key + assert SETTINGS_MAP[key].group == "Retrieval", key + class TestReplicaDefaults: """embed/vision replica counts default to 0 = auto (one per GPU at placement).""" @@ -238,11 +265,50 @@ def test_replicas_reject_negative(self): Config(embed_replicas=-1) +class TestTableExtractionSetting: + """The table-extraction flag is writable, grouped with ingest, and reindex-marked.""" + + def test_table_extraction_in_settings_map(self): + from lilbee.app.settings_map import SETTINGS_MAP, get_default + + defn = SETTINGS_MAP["table_extraction"] + assert defn.writable is True + assert defn.nullable is False + assert defn.type is bool + assert defn.group == "Ingest" + assert get_default("table_extraction") is False + + def test_table_extraction_requires_reindex(self): + from lilbee.config_meta import REINDEX_FIELDS, WRITABLE_CONFIG_FIELDS + + assert "table_extraction" in WRITABLE_CONFIG_FIELDS + assert "table_extraction" in REINDEX_FIELDS + + +class TestLayoutDetectionSetting: + """The layout-detection flag is writable, grouped with ingest, and reindex-marked.""" + + def test_layout_detection_in_settings_map(self): + from lilbee.app.settings_map import SETTINGS_MAP, get_default + + defn = SETTINGS_MAP["layout_detection"] + assert defn.writable is True + assert defn.nullable is False + assert defn.type is bool + assert defn.group == "Ingest" + assert get_default("layout_detection") is False + + def test_layout_detection_requires_reindex(self): + from lilbee.config_meta import REINDEX_FIELDS, WRITABLE_CONFIG_FIELDS + + assert "layout_detection" in WRITABLE_CONFIG_FIELDS + assert "layout_detection" in REINDEX_FIELDS + + class TestMemoryTuningSettingsMap: """The dynamic-ctx tuning knobs are surfaced in the TUI settings map.""" def test_num_ctx_max_in_settings_map(self): - from lilbee.app.settings_map import SETTINGS_MAP, get_default defn = SETTINGS_MAP["num_ctx_max"] assert defn.writable is True @@ -251,7 +317,6 @@ def test_num_ctx_max_in_settings_map(self): assert get_default("num_ctx_max") is None def test_chat_n_ctx_target_in_settings_map(self): - from lilbee.app.settings_map import SETTINGS_MAP, get_default defn = SETTINGS_MAP["chat_n_ctx_target"] assert defn.writable is True @@ -264,7 +329,6 @@ def test_chat_n_ctx_target_in_settings_map(self): assert get_default("chat_n_ctx_target") == 8192 def test_flash_attention_in_settings_map(self): - from lilbee.app.settings_map import SETTINGS_MAP, get_default defn = SETTINGS_MAP["flash_attention"] assert defn.writable is True @@ -273,7 +337,6 @@ def test_flash_attention_in_settings_map(self): assert get_default("flash_attention") is None def test_kv_cache_type_in_settings_map(self): - from lilbee.app.settings_map import SETTINGS_MAP from lilbee.core.config.enums import KvCacheType defn = SETTINGS_MAP["kv_cache_type"] @@ -281,7 +344,6 @@ def test_kv_cache_type_in_settings_map(self): assert defn.choices == tuple(t.value for t in KvCacheType) def test_n_gpu_layers_in_settings_map(self): - from lilbee.app.settings_map import SETTINGS_MAP, get_default defn = SETTINGS_MAP["n_gpu_layers"] assert defn.writable is True @@ -289,7 +351,6 @@ def test_n_gpu_layers_in_settings_map(self): assert get_default("n_gpu_layers") is None def test_vision_ocr_max_tokens_in_settings_map(self): - from lilbee.app.settings_map import SETTINGS_MAP, get_default defn = SETTINGS_MAP["vision_ocr_max_tokens"] assert defn.writable is True @@ -299,7 +360,6 @@ def test_vision_ocr_max_tokens_in_settings_map(self): assert get_default("vision_ocr_max_tokens") == 4096 def test_vision_ocr_concurrency_in_settings_map(self): - from lilbee.app.settings_map import SETTINGS_MAP, get_default defn = SETTINGS_MAP["vision_ocr_concurrency"] assert defn.writable is True @@ -309,7 +369,6 @@ def test_vision_ocr_concurrency_in_settings_map(self): assert get_default("vision_ocr_concurrency") == 4 def test_crawl_render_mode_in_settings_map(self): - from lilbee.app.settings_map import SETTINGS_MAP from lilbee.core.config.enums import CrawlRenderMode defn = SETTINGS_MAP["crawl_render_mode"] @@ -318,14 +377,12 @@ def test_crawl_render_mode_in_settings_map(self): assert defn.choices == tuple(m.value for m in CrawlRenderMode) def test_crawl_render_mode_is_writable_for_programmatic_surfaces(self): - from lilbee.config_meta import WRITABLE_CONFIG_FIELDS # The TUI checkbox persists the choice via apply_settings_update, so the # field must be writable through the HTTP / MCP / programmatic contract. assert "crawl_render_mode" in WRITABLE_CONFIG_FIELDS def test_browser_memory_levers_in_settings_map(self): - from lilbee.app.settings_map import SETTINGS_MAP, get_default recycle = SETTINGS_MAP["crawl_browser_recycle_pages"] assert recycle.writable is True @@ -457,19 +514,16 @@ def test_auto_sync_defaults_true(self): assert Config().auto_sync is True def test_auto_sync_is_writable(self): - from lilbee.config_meta import WRITABLE_CONFIG_FIELDS assert "auto_sync" in WRITABLE_CONFIG_FIELDS def test_auto_sync_in_settings_map(self): - from lilbee.app.settings_map import SETTINGS_MAP assert "auto_sync" in SETTINGS_MAP class TestListSettingRegexMarker: def test_only_regex_list_validates_as_regex(self): - from lilbee.app.settings_map import SETTINGS_MAP assert SETTINGS_MAP["crawl_exclude_patterns"].validate_regex is True # Chromium flag list must not be regex-validated. @@ -496,3 +550,28 @@ def test_win32_platform_sim_does_not_break_save(self, tmp_path, monkeypatch) -> settings.save(tmp_path, {"key": "value", "unicode": "é"}) result = settings.load(tmp_path) assert result["unicode"] == "é" + + +class TestTitleSearchSettings: + """The title-arm knobs are exposed on every settings surface.""" + + def test_title_search_in_settings_map(self): + + defn = SETTINGS_MAP["title_search"] + assert defn.writable is True + assert defn.type is bool + assert defn.group == "Retrieval" + assert get_default("title_search") is False + + def test_title_search_weight_in_settings_map(self): + + defn = SETTINGS_MAP["title_search_weight"] + assert defn.writable is True + assert defn.type is float + assert defn.group == "Retrieval" + assert get_default("title_search_weight") == 0.5 + + def test_title_search_fields_are_writable_for_programmatic_surfaces(self): + + assert "title_search" in WRITABLE_CONFIG_FIELDS + assert "title_search_weight" in WRITABLE_CONFIG_FIELDS diff --git a/tests/test_store.py b/tests/test_store.py index 3fdbb1604..7fc100048 100644 --- a/tests/test_store.py +++ b/tests/test_store.py @@ -11,6 +11,7 @@ CitationRecord, SearchChunk, SearchScope, + SourceMeta, SourceType, Store, cosine_sim, @@ -163,8 +164,107 @@ def test_second_call_optimizes_instead_of_rebuilding(self, store): create_spy.assert_not_called() optimize_spy.assert_called_once() - def test_first_call_creates_without_replace(self, store): - """Fresh table goes through create_fts_index path with replace=False.""" + def test_optimize_failure_keeps_hybrid_ready(self, store): + """An optimize() crash on an already-built index (a LanceDB encoding + bug bites large corpora) must not disable hybrid search: the index + still serves queries, so _fts_ready stays True instead of silently + dropping every query to the vector-only fallback.""" + store.add_chunks(_make_records()) + store.ensure_fts_index() # builds the index + store._fts_ready = False # a fresh process is unaware the index exists yet + table = store.open_table("chunks") + assert table is not None + with mock.patch.object( + type(table), + "optimize", + side_effect=RuntimeError("lance list offset overflow"), + ): + store.ensure_fts_index() + assert store._fts_ready is True + + def test_positional_index_overflow_rebuilds_positionless(self, store): + """A store whose FTS index was built with positions overflows on every + optimize(); catching that specific error rebuilds the index positionless + (replace=True) so index maintenance can complete instead of failing + forever with no remediation short of a full re-ingest.""" + store.add_chunks(_make_records()) + store.ensure_fts_index() + table = store.open_table("chunks") + assert table is not None + overflow = RuntimeError("Max offset 1897296 exceeds length of values 1067891") + with ( + mock.patch.object(type(table), "optimize", side_effect=overflow), + mock.patch.object(type(table), "create_fts_index") as rebuild, + ): + store.ensure_fts_index() + assert any( + c.args[:1] == ("chunk",) + and c.kwargs.get("replace") is True + and c.kwargs.get("with_position") is False + for c in rebuild.call_args_list + ) + assert store._fts_ready is True + + def test_generic_optimize_failure_does_not_rebuild(self, store, caplog): + """An unrelated optimize() failure keeps the existing index and does NOT + pay for a full positionless rebuild it cannot fix. It must also log a + warning so an operator debugging a large corpus is not left in silence.""" + import logging + + store.add_chunks(_make_records()) + store.ensure_fts_index() + table = store.open_table("chunks") + assert table is not None + with ( + mock.patch.object(type(table), "optimize", side_effect=RuntimeError("disk full")), + mock.patch.object(type(table), "create_fts_index") as rebuild, + caplog.at_level(logging.WARNING), + ): + store.ensure_fts_index() + rebuild.assert_not_called() + assert any("optimize()" in r.message for r in caplog.records) + + def test_overflow_rebuild_also_rebuilds_the_title_index_when_enabled(self, store, test_config): + """With the title arm on, a positional-overflow rebuild replaces the + title index too, not just the chunk index.""" + test_config.title_search = True + store.add_chunks(_titled_records("a.pdf", 2, title="zebra manifesto")) + store.ensure_fts_index() + table = store.open_table("chunks") + assert table is not None + overflow = RuntimeError("Max offset 9 exceeds length of values 3") + with ( + mock.patch.object(type(table), "optimize", side_effect=overflow), + mock.patch.object(type(table), "create_fts_index") as rebuild, + ): + store.ensure_fts_index() + rebuilt = { + c.args[0] + for c in rebuild.call_args_list + if c.kwargs.get("replace") is True and c.kwargs.get("with_position") is False + } + assert rebuilt == {"chunk", "title"} + + def test_overflow_rebuild_failure_is_swallowed(self, store): + """If the positionless rebuild itself fails, the store keeps the existing + index and does not propagate: a failed self-heal must not crash sync.""" + store.add_chunks(_make_records()) + store.ensure_fts_index() + table = store.open_table("chunks") + assert table is not None + overflow = RuntimeError("Max offset 9 exceeds length of values 3") + with ( + mock.patch.object(type(table), "optimize", side_effect=overflow), + mock.patch.object( + type(table), "create_fts_index", side_effect=RuntimeError("rebuild boom") + ), + ): + store.ensure_fts_index() # must not raise + assert store._fts_ready is True + + def test_first_call_creates_chunk_index_only_when_title_search_off(self, store): + """With title_search off (default), only the chunk index is built; the + title index is not created on a store that never queries it.""" store.add_chunks(_make_records()) table = store.open_table("chunks") assert table is not None @@ -172,10 +272,40 @@ def test_first_call_creates_without_replace(self, store): with mock.patch.object(type(table), "create_fts_index") as create_spy: store.ensure_fts_index() - create_spy.assert_called_once() + assert [c.args[0] for c in create_spy.call_args_list] == ["chunk"] + assert create_spy.call_args_list[0].kwargs.get("with_position") is False + + def test_first_call_creates_both_indexes_when_title_search_on(self, store, test_config): + """Fresh table creates the chunk and title indexes, both positionless + with replace=False, when the title arm is enabled.""" + test_config.title_search = True + store.add_chunks(_make_records()) + table = store.open_table("chunks") + assert table is not None + + with mock.patch.object(type(table), "create_fts_index") as create_spy: + store.ensure_fts_index() + + assert [c.args[0] for c in create_spy.call_args_list] == ["chunk", "title"] # Verify replace was NOT True (would defeat the purpose of incremental) - _args, kwargs = create_spy.call_args - assert kwargs.get("replace") is False + assert all(c.kwargs.get("replace") is False for c in create_spy.call_args_list) + # Both indexes are positionless: with_position=True overflows LanceDB's + # list encoding on a large corpus, and no lilbee query needs exact-phrase + # matching. + assert all(c.kwargs.get("with_position") is False for c in create_spy.call_args_list) + + def test_fts_quoted_query_matches_terms_not_a_phrase(self, store): + """A quoted query must return term matches, not raise on the positionless index. + + The chunk index carries no token positions, so a phrase query would + error. FTS goes through ``MatchQuery``, which matches the quoted span's + plain terms instead of parsing it as a phrase. + """ + store.add_chunks(_make_records()) + store.ensure_fts_index() + results = store.bm25_probe('"some text"') + assert results + assert all("some text" in r.chunk for r in results) def test_bm25_probe_populates_bm25_score(self, store): """LanceDB FTS returns rows keyed on ``_score``; the probe must surface it as @@ -198,6 +328,15 @@ def test_bm25_probe_filters_by_chunk_type(self, store): assert results assert all(r.chunk_type == ChunkType.WIKI for r in results) + def test_bm25_probe_survives_a_failing_search(self, store): + """A probe whose LanceDB query raises degrades to no hits, not an error.""" + store.add_chunks(_make_records()) + store.ensure_fts_index() + table = store.open_table("chunks") + assert table is not None + with mock.patch.object(type(table), "search", side_effect=RuntimeError("boom")): + assert store.bm25_probe("text") == [] + class TestSearchChunkScoreAlias: @staticmethod @@ -257,6 +396,108 @@ def _make_indexable_records(n, dim): ] +class TestEnsureScalarIndexes: + """source and chunk_type get scalar indexes so their prefilters are lookups.""" + + def test_creates_btree_on_source_and_bitmap_on_chunk_type(self, store): + store.add_chunks(_make_records()) + table = store.open_table("chunks") + assert table is not None + with mock.patch.object(type(table), "create_scalar_index") as spy: + store.ensure_scalar_indexes() + assert [c.args[0] for c in spy.call_args_list] == ["source", "chunk_type"] + kinds = {c.args[0]: c.kwargs.get("index_type") for c in spy.call_args_list} + assert kinds == {"source": "BTREE", "chunk_type": "BITMAP"} + assert all(c.kwargs.get("replace") is False for c in spy.call_args_list) + + def test_idempotent_once_the_indexes_exist(self, store): + store.add_chunks(_make_records()) + store.ensure_scalar_indexes() # builds them for real + table = store.open_table("chunks") + with mock.patch.object(type(table), "create_scalar_index") as spy: + store.ensure_scalar_indexes() + spy.assert_not_called() + + def test_handles_exception_gracefully(self, store): + store.add_chunks(_make_records()) + table = store.open_table("chunks") + assert table is not None + with mock.patch.object( + type(table), "create_scalar_index", side_effect=RuntimeError("boom") + ): + store.ensure_scalar_indexes() # must not raise + + def test_noop_when_no_table(self, store): + store.ensure_scalar_indexes() # empty store, no chunks table yet + + def test_has_scalar_index_is_false_when_listing_raises(self, store): + from lilbee.data.store.lance_helpers import _has_scalar_index + + store.add_chunks(_make_records()) + table = store.open_table("chunks") + assert table is not None + with mock.patch.object(type(table), "list_indices", side_effect=RuntimeError("boom")): + assert _has_scalar_index(table, "source") is False + + def test_indexes_chunk_concepts_source_column(self, store): + """The concept-boost path filters chunk_concepts by chunk_source per + result, so that column gets its own BTree index too.""" + from lilbee.core.config import CHUNK_CONCEPTS_TABLE + from lilbee.data.store import ensure_table + from lilbee.data.store.lance_helpers import _has_scalar_index + from lilbee.retrieval.concepts.schema import _chunk_concepts_schema + + store.add_chunks(_make_records()) + cc = ensure_table(store.get_db(), CHUNK_CONCEPTS_TABLE, _chunk_concepts_schema()) + cc.add([{"chunk_source": "doc.md", "chunk_index": 0, "concept": "alpha"}]) + store.ensure_scalar_indexes() + assert _has_scalar_index(store.open_table(CHUNK_CONCEPTS_TABLE), "chunk_source") + + def test_one_column_failure_does_not_skip_the_other(self, store): + """A BTree failure on 'source' must not skip the independent Bitmap on + 'chunk_type' -- each column gets its own try.""" + store.add_chunks(_make_records()) + table = store.open_table("chunks") + attempted = [] + + def _record(self, column, **kwargs): + attempted.append(column) + if column == "source": + raise RuntimeError("boom") + + with mock.patch.object(type(table), "create_scalar_index", _record): + store.ensure_scalar_indexes() + assert attempted == ["source", "chunk_type"] + + def test_scalar_index_failure_on_populated_table_warns(self, store, caplog): + """A create failure on a non-empty table warns (it silently loses the + prefilter speedup), unlike the benign empty-table case.""" + import logging + + store.add_chunks(_make_records()) + table = store.open_table("chunks") + with ( + mock.patch.object(type(table), "create_scalar_index", side_effect=RuntimeError("boom")), + caplog.at_level(logging.WARNING), + ): + store.ensure_scalar_indexes() + warns = [r for r in caplog.records if r.levelno >= logging.WARNING] + assert any("Scalar index create failed" in r.message for r in warns) + + def test_search_builds_scalar_indexes_on_a_serve_only_store(self, store, test_config): + """A store served without a fresh ingest never ran the ingest path that + builds scalar indexes, so the first search must build them once.""" + store.add_chunks(_make_records()) + assert store._scalar_ready is False # ingest path did not run here + with mock.patch.object( + store, "ensure_scalar_indexes", wraps=store.ensure_scalar_indexes + ) as spy: + store.search([0.5] * test_config.embedding_dim, top_k=3) + store.search([0.5] * test_config.embedding_dim, top_k=3) + spy.assert_called_once() # built once, then the guard skips it + assert store._scalar_ready is True + + class TestEnsureVectorIndex: """Small vaults stay on exact flat search; large ones get an ANN index.""" @@ -603,6 +844,56 @@ def test_hybrid_search_with_fts_index(self, store, test_config): scores = [r.score for r in results] assert all(0.0 <= s <= 1.0 for s in scores) + def test_adaptive_fusion_feeds_a_derived_weight_to_fusion(self, store, test_config): + """With adaptive_fusion on (the default), the per-query factor from + adaptive_weight_scale -- fed the configured margin -- scales the lexical + weight reaching fuse_arms, not the fixed config value. Deleting the + adaptive branch would fail this, unlike a smoke test on the score range.""" + assert test_config.adaptive_fusion is True # shipped default + test_config.adaptive_fusion_margin = 0.42 + store.add_chunks(_make_records()) + store.ensure_fts_index() + query_vec = [0.5] * test_config.embedding_dim + from lilbee.data.store import core as store_core + + with ( + mock.patch.object(store_core, "adaptive_weight_scale", return_value=0.5) as scale, + mock.patch.object(store_core, "fuse_arms", wraps=store_core.fuse_arms) as fuse, + ): + results = store.search(query_vec, top_k=3, query_text="chunk number") + scale.assert_called_once() + # adaptive_weight_scale(vector_rows, margin): the margin is the config value. + assert scale.call_args.args[1] == pytest.approx(0.42) + assert fuse.call_args.kwargs["lexical_weight"] == pytest.approx( + test_config.lexical_fusion_weight * 0.5 + ) + # The normalization denominator is the configured budget (constant), not + # the adapted weight, so scores stay comparable across sub-searches. + assert fuse.call_args.kwargs["weight_total"] == pytest.approx( + 1.0 + test_config.lexical_fusion_weight + ) + assert len(results) > 0 + + def test_fixed_fusion_pins_the_config_weight(self, store, test_config): + """Opting out of adaptive fusion skips adaptive_weight_scale and pins + the fixed lexical_fusion_weight into fuse_arms; a non-default weight must + reach fusion verbatim.""" + test_config.adaptive_fusion = False + test_config.lexical_fusion_weight = 0.3 + store.add_chunks(_make_records()) + store.ensure_fts_index() + query_vec = [0.5] * test_config.embedding_dim + from lilbee.data.store import core as store_core + + with ( + mock.patch.object(store_core, "adaptive_weight_scale") as adapt, + mock.patch.object(store_core, "fuse_arms", wraps=store_core.fuse_arms) as fuse, + ): + results = store.search(query_vec, top_k=3, query_text="chunk number") + adapt.assert_not_called() + assert fuse.call_args.kwargs["lexical_weight"] == pytest.approx(0.3) + assert len(results) > 0 + def test_fallback_to_vector_when_no_query_text(self, store, test_config): records = _make_records() store.add_chunks(records) @@ -1192,31 +1483,58 @@ def test_chunk_type_defaults_to_raw(self, store): results = store.get_chunks_by_source("doc0.md") assert results[0].chunk_type == "raw" - def test_get_chunks_by_source_fallback(self, store): - """Fallback path when table.search() raises (e.g. incompatible FTS builder).""" - from unittest.mock import patch + def test_get_chunks_by_source_filters_with_fts_index_built(self, store): + """The filtered query still selects rows once the chunks table is FTS-indexed. - records = _make_records(n=2) - store.add_chunks(records) + Both chunk-fetch paths rely on the database doing the filtering, so an + FTS-indexed table that rejected ``.where()`` would silently regress them + into whole-table reads. Pin the behavior the fetch paths depend on. + """ + store.add_chunks(_make_records(n=3)) + store.ensure_fts_index() + results = store.get_chunks_by_source("doc1.md") + assert [r.source for r in results] == ["doc1.md"] - # Make table.search() raise to trigger the Arrow fallback - original_open = store.open_table - def _broken_open(name): - table = original_open(name) - if table is None: - return None +def _one_source_records(source: str, n: int) -> list[dict]: + """*n* sequential chunks all belonging to *source*.""" + records = _make_records(n=n) + for record in records: + record["source"] = source + return records - def _raise_search(*args, **kwargs): - raise AttributeError("LanceFtsQueryBuilder has no attribute 'metric'") - table.search = _raise_search - return table +class TestGetChunksByIndices: + def test_returns_requested_indices_in_order(self, store): + store.add_chunks(_one_source_records("a.md", 5)) + results = store.get_chunks_by_indices("a.md", [3, 1]) + assert [r.chunk_index for r in results] == [1, 3] + assert all(r.source == "a.md" for r in results) - with patch.object(store, "open_table", side_effect=_broken_open): - results = store.get_chunks_by_source("doc0.md") - assert len(results) == 1 - assert results[0].source == "doc0.md" + def test_missing_indices_are_absent(self, store): + store.add_chunks(_one_source_records("a.md", 2)) + results = store.get_chunks_by_indices("a.md", [1, 99]) + assert [r.chunk_index for r in results] == [1] + + def test_other_sources_are_excluded(self, store): + store.add_chunks(_one_source_records("a.md", 2) + _one_source_records("b.md", 2)) + results = store.get_chunks_by_indices("a.md", [0, 1]) + assert {r.source for r in results} == {"a.md"} + + def test_empty_indices_returns_empty(self, store): + store.add_chunks(_one_source_records("a.md", 1)) + assert store.get_chunks_by_indices("a.md", []) == [] + + def test_no_table_returns_empty(self, store): + assert store.get_chunks_by_indices("a.md", [0]) == [] + + def test_filters_with_fts_index_built(self, store): + """The compound source+index predicate survives an FTS-indexed table.""" + store.add_chunks(_one_source_records("a.md", 3) + _one_source_records("b.md", 3)) + store.ensure_fts_index() + results = store.get_chunks_by_indices("a.md", [0, 2]) + assert [r.chunk_index for r in results] == [0, 2] + assert {r.source for r in results} == {"a.md"} def test_search_chunk_default_is_raw(self): chunk = SearchChunk( @@ -1687,6 +2005,19 @@ def test_wiki_does_not_match_null(self): assert "IS NULL" not in pred assert pred == "chunk_type = 'wiki'" + def test_raw_scope_includes_table_chunks(self): + """Table chunks are document content, so the raw scope covers them.""" + from lilbee.data.store.lance_helpers import _chunk_type_predicate + + pred = _chunk_type_predicate("raw") + assert "'table'" in pred + + def test_table_filter_is_exact(self): + from lilbee.data.store.lance_helpers import _chunk_type_predicate + + pred = _chunk_type_predicate("table") + assert pred == "chunk_type = 'table'" + class TestEmbeddingModelGate: """Refuse search/ingest when cfg.embedding_model drifts from the persisted _meta row.""" @@ -2196,3 +2527,304 @@ def test_negative_row_count_clamps_to_floor(self): from lilbee.data.store.core import _ANN_NPROBES_FLOOR, _ann_nprobes assert _ann_nprobes(-5) == _ANN_NPROBES_FLOOR + + +def _titled_records(source, n, *, title, base=0.1, dim=None): + """Chunk records for one source with a document title on every row.""" + if dim is None: + dim = cfg.embedding_dim + return [ + { + "source": source, + "content_type": "text", + "chunk_type": "raw", + "page_start": 0, + "page_end": 0, + "line_start": 0, + "line_end": 0, + "chunk": f"plain body words {source} {i}", + "chunk_index": i, + "title": title, + "vector": [base + i / 100] * dim, + } + for i in range(n) + ] + + +def _create_pre_title_chunks_table(store): + """Create the chunks table with the pre-title schema, as an old index would have.""" + import pyarrow as pa + + schema = pa.schema( + [ + pa.field("source", pa.utf8()), + pa.field("content_type", pa.utf8()), + pa.field("chunk_type", pa.utf8()), + pa.field("page_start", pa.int32()), + pa.field("page_end", pa.int32()), + pa.field("line_start", pa.int32()), + pa.field("line_end", pa.int32()), + pa.field("chunk", pa.utf8()), + pa.field("chunk_index", pa.int32()), + pa.field("vector", pa.list_(pa.float32(), cfg.embedding_dim)), + ] + ) + return store.get_db().create_table("chunks", schema=schema) + + +class TestTitleSearch: + """The title lexical arm: BM25 over document titles fused into hybrid search.""" + + def test_title_arm_surfaces_title_only_match(self, store, test_config): + """A term that lives only in a document's title reaches hybrid results + with lexical support (bm25_score) when title_search is on. The title arm + surfaces one representative chunk per matched document, so the document + appears with lexical support even though not every chunk carries it.""" + test_config.title_search = True + store.add_chunks(_titled_records("a.pdf", 2, title="zebra manifesto", base=0.9)) + store.add_chunks(_titled_records("b.pdf", 2, title="meeting notes", base=0.1)) + store.ensure_fts_index() + query_vec = [0.1] * test_config.embedding_dim + results = store.search(query_vec, top_k=4, max_distance=0, query_text="zebra") + matched = [r for r in results if r.source == "a.pdf"] + assert matched + assert any(r.bm25_score is not None for r in matched) + + def test_title_arm_collapses_to_one_deterministic_row_per_document(self, store, test_config): + """Every chunk of a document shares its title, so all tie on BM25. The + arm must collapse each matched document to one deterministic row (its + first chunk), not return an arbitrary tie-ordered subset of that doc.""" + test_config.title_search = True + store.add_chunks(_titled_records("a.pdf", 5, title="zebra manifesto")) + store.ensure_fts_index() + table = store.open_table("chunks") + rows = store._title_arm(table, "zebra", 5, None) + assert [(r.source, r.chunk_index) for r in rows] == [("a.pdf", 0)] + # Deterministic across repeated calls (no implementation-defined tie order). + again = store._title_arm(table, "zebra", 5, None) + assert [(r.source, r.chunk_index) for r in again] == [("a.pdf", 0)] + + def test_title_arm_one_row_per_matched_document(self, store, test_config): + """Two matched documents surface one representative row each, not an + arbitrary flood of one document's chunks.""" + test_config.title_search = True + store.add_chunks(_titled_records("zebra.pdf", 4, title="zebra")) + store.add_chunks(_titled_records("safari.pdf", 4, title="zebra safari")) + store.ensure_fts_index() + table = store.open_table("chunks") + rows = store._title_arm(table, "zebra", 5, None) + assert sorted(r.source for r in rows) == ["safari.pdf", "zebra.pdf"] + assert all(r.chunk_index == 0 for r in rows) + + def test_title_search_weight_reaches_fusion(self, store, test_config): + """A non-default title_search_weight is threaded into fuse_arms, not + hardcoded: the config value is what weights the title arm.""" + test_config.title_search = True + test_config.title_search_weight = 0.2 + store.add_chunks(_titled_records("a.pdf", 2, title="zebra manifesto")) + store.ensure_fts_index() + query_vec = [0.1] * test_config.embedding_dim + from lilbee.data.store import core as store_core + + with mock.patch.object(store_core, "fuse_arms", wraps=store_core.fuse_arms) as fuse: + store.search(query_vec, top_k=4, max_distance=0, query_text="zebra") + assert fuse.call_args.kwargs["title_weight"] == pytest.approx(0.2) + # With the title arm enabled, its weight is part of the constant + # denominator whether or not this query's title arm returned rows. + assert fuse.call_args.kwargs["weight_total"] == pytest.approx( + 1.0 + test_config.lexical_fusion_weight + 0.2 + ) + + def test_adaptive_fusion_scales_the_title_arm_too(self, store, test_config): + """Adaptive fusion downweights lexical; the title arm is also lexical, so + it must be scaled by the same confidence, not left at full weight (which + would re-admit the signal adaptive fusion just silenced).""" + test_config.title_search = True + test_config.title_search_weight = 0.5 + assert test_config.adaptive_fusion is True + store.add_chunks(_titled_records("a.pdf", 2, title="zebra manifesto")) + store.ensure_fts_index() + query_vec = [0.1] * test_config.embedding_dim + from lilbee.data.store import core as store_core + + with ( + mock.patch.object(store_core, "adaptive_weight_scale", return_value=0.25), + mock.patch.object(store_core, "fuse_arms", wraps=store_core.fuse_arms) as fuse, + ): + store.search(query_vec, top_k=4, max_distance=0, query_text="zebra") + assert fuse.call_args.kwargs["lexical_weight"] == pytest.approx( + test_config.lexical_fusion_weight * 0.25 + ) + assert fuse.call_args.kwargs["title_weight"] == pytest.approx(0.5 * 0.25) + + def test_title_arm_off_by_default(self, store, test_config): + """With title_search off, a title-only term earns no lexical support.""" + assert test_config.title_search is False + store.add_chunks(_titled_records("a.pdf", 2, title="zebra manifesto")) + store.ensure_fts_index() + query_vec = [0.1] * test_config.embedding_dim + results = store.search(query_vec, top_k=4, max_distance=0, query_text="zebra") + assert all(r.bm25_score is None for r in results) + + def test_title_arm_respects_chunk_type_filter(self, store, test_config): + from lilbee.data.store.lance_helpers import _has_fts_index + + test_config.title_search = True # the title index is built only when enabled + store.add_chunks(_titled_records("a.pdf", 2, title="zebra manifesto")) + store.ensure_fts_index() + table = store.open_table("chunks") + assert _has_fts_index(table, "title") + assert store._title_arm(table, "zebra", 5, ChunkType.RAW) + assert store._title_arm(table, "zebra", 5, ChunkType.WIKI) == [] + + def test_title_arm_failure_degrades_to_empty(self, store, test_config): + """A query-time title-arm failure returns no rows instead of raising, + so a broken title index can't take down the healthy chunk-BM25 arm and + collapse the whole hybrid search to vector-only.""" + test_config.title_search = True + store.add_chunks(_titled_records("a.pdf", 2, title="zebra manifesto")) + store.ensure_fts_index() + table = store.open_table("chunks") + from lilbee.data.store import core as store_core + + with mock.patch.object(store_core, "_lexical_rows", side_effect=RuntimeError("boom")): + assert store._title_arm(table, "zebra", 5, None) == [] + + def test_old_index_without_title_column_still_searches(self, store, test_config): + """Feature detection: a pre-title index searches fine and the title arm + silently contributes nothing.""" + test_config.title_search = True + table = _create_pre_title_chunks_table(store) + table.add(_make_records()) + store.ensure_fts_index() + assert store._title_arm(table, "chunk", 5, None) == [] + query_vec = [0.5] * test_config.embedding_dim + results = store.search(query_vec, top_k=3, max_distance=0, query_text="chunk number") + assert results + assert all(r.title is None for r in results) + + def test_add_chunks_evolves_pre_title_table(self, store): + """A write to an old index adds the title column in place.""" + table = _create_pre_title_chunks_table(store) + table.add(_make_records()) + assert "title" not in table.schema.names + store.add_chunks(_titled_records("new.pdf", 1, title="fresh document")) + assert "title" in store.open_table("chunks").schema.names + rows = store.get_chunks_by_source("new.pdf") + assert [r.title for r in rows] == ["fresh document"] + + def test_pre_title_migration_warns_to_rebuild(self, store, caplog): + """Migrating an old store logs that pre-upgrade docs stay title-blind + until a rebuild, so the silence doesn't hide the gap.""" + import logging + + table = _create_pre_title_chunks_table(store) + table.add(_make_records()) + with caplog.at_level(logging.WARNING): + store.add_chunks(_titled_records("new.pdf", 1, title="fresh document")) + assert any("lilbee rebuild" in r.message for r in caplog.records) + + def test_bm25_probe_stays_chunk_scoped(self, store): + """The probe pins the chunk column: a title-only term is not a probe hit.""" + store.add_chunks(_titled_records("a.pdf", 2, title="zebra manifesto")) + store.ensure_fts_index() + assert store.bm25_probe("zebra") == [] + assert store.bm25_probe("plain body words") + + def test_title_index_failure_never_blocks_chunk_index(self, store, test_config, caplog): + """A failing title index leaves chunk FTS ready; the arm degrades to + empty. Because the arm is enabled, the failure warns (not debug) so an + opted-in title arm that cannot build is not a silent no-op.""" + import logging + + test_config.title_search = True + store.add_chunks(_titled_records("a.pdf", 1, title="zebra manifesto")) + table = store.open_table("chunks") + real_create = type(table).create_fts_index + + def _fail_title(self, column, **kwargs): + if column == "title": + raise RuntimeError("boom") + return real_create(self, column, **kwargs) + + with ( + mock.patch.object(type(table), "create_fts_index", _fail_title), + caplog.at_level(logging.WARNING), + ): + store.ensure_fts_index() + assert store._fts_ready + assert store._title_arm(store.open_table("chunks"), "zebra", 5, None) == [] + warnings = [r for r in caplog.records if r.levelno >= logging.WARNING] + assert any("title" in r.message.lower() for r in warnings) + + +class TestSourceMetadata: + """Extraction-time document metadata persisted on the sources table.""" + + def test_upsert_source_persists_meta(self, store): + store.upsert_source( + "a.pdf", + "hash1", + 3, + meta=SourceMeta(title="The Title", authors="Ada, Grace", created_at="2021-05-01"), + ) + row = store.get_sources()[0] + assert row["title"] == "The Title" + assert row["authors"] == "Ada, Grace" + assert row["created_at"] == "2021-05-01" + + def test_absent_meta_persists_null(self, store): + store.upsert_source("a.pdf", "hash1", 3) + row = store.get_sources()[0] + assert row["title"] is None + assert row["authors"] is None + assert row["created_at"] is None + + def test_pre_meta_sources_table_evolves_in_place(self, store): + """An old sources table gains the metadata columns on the next write.""" + import pyarrow as pa + + old_schema = pa.schema( + [ + pa.field("filename", pa.utf8()), + pa.field("file_hash", pa.utf8()), + pa.field("ingested_at", pa.utf8()), + pa.field("chunk_count", pa.int32()), + pa.field("source_type", pa.utf8()), + ] + ) + table = store.get_db().create_table("_sources", schema=old_schema) + table.add( + [ + { + "filename": "old.pdf", + "file_hash": "h0", + "ingested_at": "2020-01-01T00:00:00+00:00", + "chunk_count": 1, + "source_type": "document", + } + ] + ) + store.upsert_source("new.pdf", "h1", 2, meta=SourceMeta(title="New Doc")) + rows = {r["filename"]: r for r in store.get_sources()} + assert rows["old.pdf"]["title"] is None + assert rows["new.pdf"]["title"] == "New Doc" + + def test_batched_write_persists_meta(self, store): + from lilbee.data.store import ChunkWrite, SourceMeta + + records = _titled_records("doc.pdf", 1, title="Batched Title") + store.write_chunks_batch( + [ + ChunkWrite( + "doc.pdf", + "h", + records, + needs_cleanup=False, + meta=SourceMeta(title="Batched Title", authors="Ada"), + ) + ] + ) + row = store.get_sources()[0] + assert row["title"] == "Batched Title" + assert row["authors"] == "Ada" diff --git a/tests/test_store_fusion.py b/tests/test_store_fusion.py index 5e20e68e5..28035ced8 100644 --- a/tests/test_store_fusion.py +++ b/tests/test_store_fusion.py @@ -3,7 +3,12 @@ import pytest from lilbee.data.store import SearchChunk -from lilbee.data.store.fusion import fuse_arms, normalized_bm25, vector_similarity +from lilbee.data.store.fusion import ( + adaptive_lexical_weight, + fuse_arms, + normalized_bm25, + vector_similarity, +) def _chunk(source, idx, *, distance=None, bm25=None, dim=4): @@ -99,6 +104,126 @@ def test_sorted_descending_by_score(self): assert all(r.score is not None for r in fused) +class TestWeightTotalNormalization: + """A constant reference denominator keeps scores comparable across the + separate sub-searches that Searcher merges. Without it, each sub-search + normalizes by its own per-query total_weight, so a peaked sub-search + (lexical silenced) inflates its rows against a flat one's.""" + + def test_weight_total_pins_the_denominator(self): + # Lexical silenced (weight 0): the vector-only top hit must still be + # scored against the supplied constant denominator, not 1.0. + fused = fuse_arms( + [_chunk("a.md", 0, distance=0.3)], [], lexical_weight=0.0, weight_total=2.0 + ) + assert fused[0].score == pytest.approx(0.5) + + def test_cross_subsearch_scores_share_one_scale(self): + # Same identical-strength vector-only top hit, two sub-searches whose + # adaptive lexical weight differs: with one shared weight_total they land + # on the same score instead of 1.0 vs 0.5. + peaked = fuse_arms( + [_chunk("a.md", 0, distance=0.3)], [], lexical_weight=0.0, weight_total=2.0 + ) + flat = fuse_arms( + [_chunk("a.md", 0, distance=0.3)], [], lexical_weight=1.0, weight_total=2.0 + ) + assert peaked[0].score == pytest.approx(flat[0].score) + + def test_weight_total_preserves_within_call_order(self): + # A uniform denominator is a monotonic rescale, so the ranking inside one + # call is identical to the default per-call normalization. + vec = [_chunk("near.md", 0, distance=0.1), _chunk("far.md", 1, distance=0.9)] + lex = [_chunk("far.md", 1, bm25=30.0)] + default = [r.source for r in fuse_arms(vec, lex, lexical_weight=1.0)] + pinned = [r.source for r in fuse_arms(vec, lex, lexical_weight=1.0, weight_total=2.0)] + assert default == pinned + + def test_weight_total_defaults_to_per_call_when_absent(self): + # Direct callers that omit weight_total keep the original behavior. + no_arg = fuse_arms([_chunk("a.md", 0, distance=0.3)], [_chunk("a.md", 0, bm25=9.0)]) + explicit = fuse_arms( + [_chunk("a.md", 0, distance=0.3)], + [_chunk("a.md", 0, bm25=9.0)], + weight_total=2.0, + ) + assert no_arg[0].score == pytest.approx(explicit[0].score) + + +class TestAdaptiveLexicalWeight: + """Per-query lexical weight gated by the vector arm's confidence.""" + + def test_peaked_dense_silences_lexical(self): + # top similarity 1.0 (distance 0), field ~0.3: a wide margin => weight ~0. + rows = [_chunk("a.md", 0, distance=0.0)] + [ + _chunk("b.md", i, distance=0.7) for i in range(1, 5) + ] + assert adaptive_lexical_weight(rows, 1.0, 0.3) == pytest.approx(0.0) + + def test_flat_dense_keeps_full_weight(self): + # every row equally similar: zero margin => the arm keeps base_weight. + rows = [_chunk("a.md", i, distance=0.5) for i in range(5)] + assert adaptive_lexical_weight(rows, 1.0, 0.3) == pytest.approx(1.0) + + def test_scales_linearly_between(self): + # top sim 0.6, field 0.3, margin 0.3, scale 0.6 => confidence 0.5 => half. + rows = [_chunk("a.md", 0, distance=0.4)] + [ + _chunk("b.md", i, distance=0.7) for i in range(1, 4) + ] + assert adaptive_lexical_weight(rows, 1.0, 0.6) == pytest.approx(0.5) + + def test_respects_base_weight(self): + rows = [_chunk("a.md", i, distance=0.5) for i in range(3)] + assert adaptive_lexical_weight(rows, 0.5, 0.3) == pytest.approx(0.5) + + def test_too_few_rows_returns_base(self): + assert adaptive_lexical_weight([_chunk("a.md", 0, distance=0.1)], 1.0, 0.3) == 1.0 + assert adaptive_lexical_weight([], 1.0, 0.3) == 1.0 + + def test_non_positive_margin_scale_disables(self): + rows = [_chunk("a.md", 0, distance=0.0), _chunk("b.md", 1, distance=0.9)] + assert adaptive_lexical_weight(rows, 1.0, 0.0) == 1.0 + + def test_ignores_rows_without_distance(self): + # lexical-only rows carry no distance; they must not enter the signal. + rows = [ + _chunk("a.md", 0, distance=0.0), + _chunk("b.md", 1, distance=0.6), + _chunk("c.md", 2, bm25=9.0), + ] + both = adaptive_lexical_weight(rows, 1.0, 0.4) + two = adaptive_lexical_weight(rows[:2], 1.0, 0.4) + assert both == pytest.approx(two) + + +class TestLexicalFusionWeight: + """The BM25 arm's fusion weight can be lowered so a strong dense arm dominates.""" + + def _lex_row(self, weight: float): + vec = [_chunk("noise.md", 0, distance=0.5)] + lex = [_chunk("cat.pdf", 0, bm25=35.0)] + return next(r for r in fuse_arms(vec, lex, lexical_weight=weight) if r.source == "cat.pdf") + + def test_default_weight_gives_the_arms_equal_voice(self): + vec = [_chunk("noise.md", 0, distance=0.5)] + lex = [_chunk("cat.pdf", 0, bm25=35.0)] + default = {r.source: r.score for r in fuse_arms(vec, lex)} + explicit = {r.source: r.score for r in fuse_arms(vec, lex, lexical_weight=1.0)} + assert default == explicit + + def test_zero_weight_drops_the_lexical_only_arm(self): + """A fully-silenced lexical arm contributes no rows at all, so a + BM25-only hit never enters the pool carrying lexical provenance (and + with it the downstream distance/structural exemptions).""" + vec = [_chunk("noise.md", 0, distance=0.5)] + lex = [_chunk("cat.pdf", 0, bm25=35.0)] + fused = fuse_arms(vec, lex, lexical_weight=0.0) + assert "cat.pdf" not in [r.source for r in fused] + + def test_lower_weight_shrinks_the_lexical_contribution(self): + assert self._lex_row(0.5).score < self._lex_row(1.0).score + + class TestRegressionMechanism: """Synthetic reproduction of the graded-A/B regression shape: a lexical query whose relevant passages are mutually similar (they all quote the @@ -186,3 +311,59 @@ def test_drops_vector_only_far_row_keeps_supported(self): far_supported = _chunk("identifier.md", 0, distance=1.4, bm25=25.0) kept = _drop_unsupported_far_rows([far_unsupported, far_supported], 0.75) assert [r.source for r in kept] == ["identifier.md"] + + +class TestTitleArmFusion: + """The optional third arm: BM25 over document titles, weight-normalized.""" + + def test_top_of_all_three_arms_scores_one(self): + fused = fuse_arms( + [_chunk("a.md", 0, distance=0.3)], + [_chunk("a.md", 0, bm25=12.0)], + [_chunk("a.md", 0, bm25=3.0)], + title_weight=0.5, + ) + assert fused[0].score == pytest.approx(1.0) + + def test_title_only_row_scores_its_weight_share(self): + weight = 0.5 + fused = fuse_arms([], [], [_chunk("t.md", 0, bm25=3.0)], title_weight=weight) + assert fused[0].score == pytest.approx(weight / (2.0 + weight)) + + def test_empty_title_arm_matches_two_arm_scores(self): + """No title rows = the classic two-arm fusion, share-for-share.""" + vector = [_chunk("a.md", 0, distance=0.3), _chunk("b.md", 0, distance=0.4)] + fts = [_chunk("a.md", 0, bm25=12.0)] + two_arm = fuse_arms(vector, fts) + with_empty_title = fuse_arms(vector, fts, [], title_weight=0.5) + assert [(r.source, r.score) for r in two_arm] == [ + (r.source, r.score) for r in with_empty_title + ] + + def test_title_match_counts_as_lexical_support(self): + """A row only the title arm matched carries bm25_score, so the + distance exemption sees lexical support.""" + fused = fuse_arms( + [_chunk("a.md", 0, distance=1.5)], + [], + [_chunk("a.md", 0, bm25=4.0)], + title_weight=0.5, + ) + assert fused[0].bm25_score == pytest.approx(4.0) + assert fused[0].distance == pytest.approx(1.5) + + def test_chunk_arm_bm25_provenance_wins_over_title(self): + """When both lexical arms match a row, the first-seen bm25_score is kept.""" + fused = fuse_arms( + [], + [_chunk("a.md", 0, bm25=12.0)], + [_chunk("a.md", 0, bm25=3.0)], + title_weight=0.5, + ) + assert fused[0].bm25_score == pytest.approx(12.0) + + def test_title_weight_scales_contribution(self): + low = fuse_arms([], [], [_chunk("t.md", 0, bm25=3.0)], title_weight=0.2) + high = fuse_arms([], [], [_chunk("t.md", 0, bm25=3.0)], title_weight=1.0) + assert low[0].score < high[0].score + assert high[0].score == pytest.approx(1.0 / 3.0) diff --git a/tests/test_store_ranking.py b/tests/test_store_ranking.py new file mode 100644 index 000000000..bdb27c0ff --- /dev/null +++ b/tests/test_store_ranking.py @@ -0,0 +1,50 @@ +"""Tests for the store's MMR ranking helper's config scoping.""" + +from __future__ import annotations + +from lilbee.core.config import cfg, config_scope +from lilbee.data.store import SearchChunk, mmr_rerank + + +def _chunk(source: str, vector: list[float]) -> SearchChunk: + return SearchChunk( + source=source, + content_type="text", + page_start=0, + page_end=0, + line_start=0, + line_end=0, + chunk=source, + chunk_index=0, + distance=0.5, + vector=vector, + ) + + +class TestMmrRerankConfigScoping: + def test_default_lambda_honors_the_scoped_config(self): + """Omitting mmr_lambda must read the ACTIVE config, not the process + global: under the library API a config_scope binding is the caller's + config, and every sibling in the data path uses active_config().""" + # Two near-duplicates and one distinct doc, query aligned with the pair. + results = [ + _chunk("dup_a.md", [1.0, 0.0]), + _chunk("dup_b.md", [0.99, 0.01]), + _chunk("other.md", [0.0, 1.0]), + ] + query = [1.0, 0.0] + + scoped = cfg.model_copy() + # Pure relevance: both near-duplicates win the top two slots. + scoped.mmr_lambda = 1.0 + with config_scope(scoped): + relevance_only = mmr_rerank(query, results, top_k=2) + + # Maximum diversity: the distinct doc displaces the near-duplicate. + scoped_diverse = cfg.model_copy() + scoped_diverse.mmr_lambda = 0.0 + with config_scope(scoped_diverse): + diversity_only = mmr_rerank(query, results, top_k=2) + + assert [r.source for r in relevance_only] == ["dup_a.md", "dup_b.md"] + assert "other.md" in [r.source for r in diversity_only] diff --git a/tests/test_structural.py b/tests/test_structural.py new file mode 100644 index 000000000..f3d8fb312 --- /dev/null +++ b/tests/test_structural.py @@ -0,0 +1,83 @@ +"""Tests for the document-structure (TOC / cover-page) chunk detector.""" + +from lilbee.retrieval.query.structural import is_structural_chunk + +# A table of contents: section titles followed by dot leaders and page numbers. +TOC = """Contents +A. Executive Summary ................................................. 1 +B. Introduction ..................................................... 3 +C. Regional Overview ............................................... 9 +D. Budget and Staffing ............................................. 10 +E. Program Findings ................................................ 11 +F. Recommendations ................................................. 13 +""" + +# A classification-banner cover/title page. +COVER = """UNCLASSIFIED +NATIONAL PROGRAM REVIEW BOARD +OFFICE OF STRATEGIC ASSESSMENT +Fiscal Year 2024 Consolidated Annual Report on +Program Performance and Oversight +Information Cut Off Date: 1 JUNE 2024 +UNCLASSIFIED +""" + +# Real prose that must NOT be flagged. +PROSE = ( + "During the reporting period, the office received no reports indicating that the " + "program had an adverse effect on regional operations. However, many reports from " + "field staff described transient delays. The office continues to evaluate each case " + "against a standardized methodology, and it has resolved the majority of reported " + "incidents as routine administrative issues." +) + +# Prose that happens to reference a page and carry a classification header. +PROSE_WITH_HEADER = ( + "UNCLASSIFIED. As detailed on page 12, the assessment concluded that the shortfall was " + "a routine scheduling gap. The budget records and the staffing figures were " + "consistent, and the case was closed. No irregularity was observed at any point " + "during the review, which lasted several weeks." +) + + +class TestIsStructuralChunk: + def test_table_of_contents_is_structural(self): + assert is_structural_chunk(TOC) is True + + def test_cover_page_is_structural(self): + assert is_structural_chunk(COVER) is True + + def test_real_prose_is_not_structural(self): + assert is_structural_chunk(PROSE) is False + + def test_prose_with_classification_header_and_page_ref_is_not_structural(self): + # Many sentences => fails the cover-page gate; one "page 12" => not a TOC. + assert is_structural_chunk(PROSE_WITH_HEADER) is False + + def test_empty_is_not_structural(self): + assert is_structural_chunk("") is False + assert is_structural_chunk(" \n ") is False + + def test_single_dot_leader_line_is_not_a_toc(self): + assert is_structural_chunk("See the appendix ......... 42") is False + + def test_short_all_caps_without_classification_is_not_a_cover(self): + # A shouting heading with no classification banner is left alone. + assert is_structural_chunk("REGIONAL OVERVIEW AND PROGRAM FINDINGS") is False + + def test_short_classified_body_page_is_not_a_cover(self): + # A short body page carries a classification banner and caps but real + # content; its full sentences must keep it out of scope so the answer + # does not lose the page it needs. + body = ( + "UNCLASSIFIED. The office assessment concluded the shortfall was a " + "routine scheduling gap, and the case was resolved. BUDGET and STAFFING " + "data were CONSISTENT across the entire review." + ) + assert is_structural_chunk(body) is False + + def test_long_classified_body_is_not_a_cover(self): + # A real document body that opens with a classification banner but runs + # long is content, not a cover page: the word-count gate protects it. + body = "UNCLASSIFIED " + " ".join(f"finding{i} detail" for i in range(80)) + assert is_structural_chunk(body) is False diff --git a/tests/test_summarize_history.py b/tests/test_summarize_history.py index dc8bf3fd0..278c74e8f 100644 --- a/tests/test_summarize_history.py +++ b/tests/test_summarize_history.py @@ -197,7 +197,7 @@ def test_merged_notes_over_the_cap_get_one_compression_pass() -> None: long_note = "note " * 300 # ~375 tokens, well over summary_cap(2048) provider = _provider(long_note) cfg.chat_n_ctx_target = 2048 - plan = plan_compaction(_msgs(60), "", ctx_target=2048) + plan = plan_compaction(_msgs(60), ctx_target=2048) result = _searcher(provider).summarize_history(_msgs(60)) assert provider.chat.call_count == len(plan.batches) + 1, "one merge pass on top of the batches" assert result.summary diff --git a/tests/test_system.py b/tests/test_system.py index 9f82cedef..35f94e6b4 100644 --- a/tests/test_system.py +++ b/tests/test_system.py @@ -67,11 +67,18 @@ def test_is_network_path_false_when_mounts_unreadable(self, tmp_path, monkeypatc @pytest.mark.skipif(sys.platform == "win32", reason="POSIX mount-path semantics") def test_is_network_path_uses_raw_path_when_resolve_fails(self, mounts_file, monkeypatch): # Path.resolve has no injectable seam, so this one branch patches it. - def _raise(self): - raise OSError("resolve failed") + # Raise only for the path under test; an unconditional patch also hits + # the config validator and blows up in fixture teardown. + target = Path("/workspace/models/m.gguf") + real_resolve = Path.resolve + + def _raise(self, strict=False): + if self == target: + raise OSError("resolve failed") + return real_resolve(self, strict=strict) monkeypatch.setattr(Path, "resolve", _raise) - assert is_network_path(Path("/workspace/models/m.gguf")) is True + assert is_network_path(target) is True class TestHelpers: diff --git a/tests/test_tokenizer_backend.py b/tests/test_tokenizer_backend.py new file mode 100644 index 000000000..b1b7be7b1 --- /dev/null +++ b/tests/test_tokenizer_backend.py @@ -0,0 +1,56 @@ +"""Tests for lilbee's xberg tokenizer backend (token-budgeted chunk sizing).""" + +from __future__ import annotations + +from lilbee.data.ingest.types import TokenizerBackendName +from lilbee.data.tokenizer_backend import LilbeeTokenizerBackend, _estimate_tokens + + +def test_name_is_lilbee(): + backend = LilbeeTokenizerBackend(count_fn=lambda _t: 1) + assert backend.name() == TokenizerBackendName.LILBEE + + +def test_initialize_and_shutdown_are_noops(): + backend = LilbeeTokenizerBackend(count_fn=lambda _t: 1) + assert backend.initialize() is None + assert backend.shutdown() is None + + +def test_exact_count_is_returned(): + backend = LilbeeTokenizerBackend(count_fn=lambda _t: 7) + assert backend.count_tokens("hello world") == 7 + + +def test_empty_text_is_zero_without_calling_count_fn(): + calls: list[str] = [] + + def count_fn(text: str) -> int: + calls.append(text) + return 5 + + backend = LilbeeTokenizerBackend(count_fn=count_fn) + assert backend.count_tokens("") == 0 + assert calls == [] + + +def test_exception_falls_back_to_char_estimate(): + def boom(_text: str) -> int: + raise RuntimeError("embedder unreachable") + + backend = LilbeeTokenizerBackend(count_fn=boom) + text = "x" * 9 + assert backend.count_tokens(text) == _estimate_tokens(text) + + +def test_zero_count_for_non_empty_text_falls_back_to_estimate(): + """xberg rejects a zero count for non-empty text; degrade to the estimate.""" + backend = LilbeeTokenizerBackend(count_fn=lambda _t: 0) + text = "x" * 9 + assert backend.count_tokens(text) == _estimate_tokens(text) + + +def test_estimate_is_conservative_and_at_least_one(): + assert _estimate_tokens("") == 1 + assert _estimate_tokens("abc") == 1 + assert _estimate_tokens("a" * 9) == 3 diff --git a/tests/test_tui_screens.py b/tests/test_tui_screens.py index ad1c2aeee..3b4c929e9 100644 --- a/tests/test_tui_screens.py +++ b/tests/test_tui_screens.py @@ -12,6 +12,7 @@ import pytest from textual.app import App, ComposeResult from textual.widgets import DataTable, Footer, Static +from textual.widgets._tabbed_content import ContentTabs from conftest import TEST_EMBED_REF, TEST_LOCAL_REF from lilbee.app.services import set_services @@ -4712,6 +4713,32 @@ async def test_catalog_load_more_isolates_per_task_offset(): assert screen._hf_offset_by_task[ModelTask.RERANK] == 0 +async def test_catalog_tabs_never_force_an_active_id_at_construction(): + """The strip composes without a forced-active id, and Chat still wins. + + A forced id arms `Tabs._on_mount`'s unguarded set, which raises "No Tab with + id" when the tab children mount late. + """ + from textual.widgets import TabbedContent + + from lilbee.cli.tui.screens.catalog import TAB_CHAT, CatalogScreen + + app = CatalogTestApp() + async with app.run_test(size=(120, 40)) as _pilot: + with _patch_catalog()[0], _patch_catalog()[1], _patch_catalog()[2]: + screen = CatalogScreen() + app.push_screen(screen) + await _pilot.pause() + tabs = screen.query_one("#catalog-tabs", TabbedContent) + strip = tabs.query_one(ContentTabs) + assert strip._first_active is None + for _ in range(20): + await _pilot.pause() + if tabs.active == TAB_CHAT: + break + assert tabs.active == TAB_CHAT + + async def test_catalog_tab_activation_fetches_lazily(): """Activating an unfetched task tab fires its first HF page fetch. diff --git a/tests/test_tui_widgets.py b/tests/test_tui_widgets.py index 0f141bc7e..7ef07ec05 100644 --- a/tests/test_tui_widgets.py +++ b/tests/test_tui_widgets.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Any +from typing import Any, Never from unittest import mock import pytest @@ -2755,15 +2755,21 @@ def test_add_arg_completions(self, tmp_path: object) -> None: assert any("testfile.txt" in x for x in r) def test_path_exists_swallows_oserror(self, monkeypatch: pytest.MonkeyPatch) -> None: - """A path the OS refuses to stat must read as missing, not raise.""" - from pathlib import Path as P + """A path the OS refuses to stat reads as missing, not raise. + Patches this module's Path, not pathlib.Path.expanduser: a global patch + also hits the config validator and blows up in fixture teardown. + """ from lilbee.cli.tui.widgets import autocomplete - def _boom(self: P) -> P: - raise OSError("bad path") + class _BoomPath: + def __init__(self, *_args: object) -> None: + pass + + def expanduser(self) -> Never: + raise OSError("bad path") - monkeypatch.setattr(P, "expanduser", _boom) + monkeypatch.setattr(autocomplete, "Path", _BoomPath) assert autocomplete._path_exists("~oops") is False def test_add_complete_path_collapses_so_enter_submits(self, tmp_path: object) -> None: diff --git a/tests/test_vision.py b/tests/test_vision.py index 0575d1b20..821d849be 100644 --- a/tests/test_vision.py +++ b/tests/test_vision.py @@ -1,133 +1,19 @@ -"""Tests for vision model OCR extraction.""" +"""Tests for the vision-message helpers (prompt resolution + image messages).""" -from pathlib import Path -from unittest import mock +from __future__ import annotations -import pytest -from lilbee.app.services import CrawlerSyncState, Services, set_services - - -@pytest.fixture() -def mock_provider(): - """Create a mock provider with the full LLMProvider surface. - - ``vision_ocr`` is part of the protocol now, so every concrete provider - implements it; tests mock it directly rather than the chat fallthrough. - """ - provider = mock.MagicMock( - spec=[ - "chat", - "embed", - "vision_ocr", - "list_models", - "pull_model", - "show_model", - "shutdown", - ] - ) - store = mock.MagicMock() - embedder = mock.MagicMock() - reranker = mock.MagicMock() - concepts = mock.MagicMock() - searcher = mock.MagicMock() - registry = mock.MagicMock() - services = Services( - provider=provider, - store=store, - embedder=embedder, - reranker=reranker, - concepts=concepts, - clusterer=mock.MagicMock(), - searcher=searcher, - registry=registry, - hf_client=mock.MagicMock(), - ingest_lock_registry=mock.MagicMock(), - model_manager=mock.MagicMock(), - crawler_semaphore=None, - crawler_sync_state=CrawlerSyncState(), - ) - set_services(services) - yield provider - set_services(None) - - -def _mock_iterator(num_pages: int = 1) -> mock.MagicMock: - """Build a mock PdfPageIterator that yields (index, png_bytes) tuples.""" - pages = [(i, b"\x89PNG" + bytes(f"page-{i}", "utf-8")) for i in range(num_pages)] - it = mock.MagicMock() - it.__len__ = mock.Mock(return_value=num_pages) - it.__iter__ = mock.Mock(return_value=iter(pages)) - it.__enter__ = mock.Mock(return_value=it) - it.__exit__ = mock.Mock(return_value=False) - return it - - -class TestPdfPageCount: - def test_returns_page_count(self) -> None: - mock_iter = _mock_iterator(num_pages=5) - with mock.patch("kreuzberg.PdfPageIterator", return_value=mock_iter): - from lilbee.vision import pdf_page_count - - assert pdf_page_count(Path("test.pdf")) == 5 - - def test_empty_pdf_returns_zero(self) -> None: - mock_iter = _mock_iterator(num_pages=0) - with mock.patch("kreuzberg.PdfPageIterator", return_value=mock_iter): - from lilbee.vision import pdf_page_count - - assert pdf_page_count(Path("empty.pdf")) == 0 - - def test_passes_dpi(self) -> None: - mock_iter = _mock_iterator(num_pages=1) - mock_cls = mock.patch("kreuzberg.PdfPageIterator", return_value=mock_iter) - with mock_cls as patched: - from lilbee.vision import _RASTER_DPI, pdf_page_count - - pdf_page_count(Path("test.pdf")) - patched.assert_called_once_with(mock.ANY, dpi=_RASTER_DPI) - - -class TestRasterizePdf: - def test_yields_index_and_png_bytes(self) -> None: - mock_iter = _mock_iterator(num_pages=2) - with mock.patch("kreuzberg.PdfPageIterator", return_value=mock_iter): - from lilbee.vision import rasterize_pdf - - pages = list(rasterize_pdf(Path("test.pdf"))) - - assert len(pages) == 2 - assert pages[0][0] == 0 - assert pages[1][0] == 1 - assert all(data.startswith(b"\x89PNG") for _, data in pages) - - def test_empty_pdf_yields_nothing(self) -> None: - mock_iter = _mock_iterator(num_pages=0) - with mock.patch("kreuzberg.PdfPageIterator", return_value=mock_iter): - from lilbee.vision import rasterize_pdf - - pages = list(rasterize_pdf(Path("empty.pdf"))) - - assert pages == [] - - def test_uses_context_manager(self) -> None: - mock_iter = _mock_iterator(num_pages=1) - with mock.patch("kreuzberg.PdfPageIterator", return_value=mock_iter): - from lilbee.vision import rasterize_pdf - - list(rasterize_pdf(Path("test.pdf"))) +class TestResolveOcrPrompt: + def test_native_prompt_for_known_family(self) -> None: + from lilbee.vision import resolve_ocr_prompt - mock_iter.__enter__.assert_called_once() - mock_iter.__exit__.assert_called_once() + assert resolve_ocr_prompt("vendor/glm-ocr-1b") == "OCR" + assert resolve_ocr_prompt("deepseek-ocr").startswith("<|grounding|>") - def test_passes_dpi(self) -> None: - mock_iter = _mock_iterator(num_pages=1) - mock_cls = mock.patch("kreuzberg.PdfPageIterator", return_value=mock_iter) - with mock_cls as patched: - from lilbee.vision import _RASTER_DPI, rasterize_pdf + def test_generic_prompt_for_unknown_model(self) -> None: + from lilbee.vision import OCR_PROMPT, resolve_ocr_prompt - list(rasterize_pdf(Path("test.pdf"))) - patched.assert_called_once_with(mock.ANY, dpi=_RASTER_DPI) + assert resolve_ocr_prompt("vendor/qwen-vl") == OCR_PROMPT class TestPngToDataUrl: @@ -139,7 +25,6 @@ def test_encodes_png_bytes(self) -> None: png_bytes = b"\x89PNG\r\n\x1a\n" result = _png_to_data_url(png_bytes) assert result.startswith("data:image/png;base64,") - # Verify round-trip encoded = result.split(",", 1)[1] assert base64.b64decode(encoded) == png_bytes @@ -159,29 +44,3 @@ def test_builds_openai_format(self) -> None: assert content[0]["image_url"]["url"].startswith("data:image/png;base64,") assert content[1]["type"] == "text" assert content[1]["text"] == "describe this" - - -class TestResolveOcrPrompt: - """The OCR prompt is resolved per model: native for specialists, generic fallback otherwise.""" - - def test_deepseek_gets_its_grounding_prompt(self): - from lilbee.vision import resolve_ocr_prompt - - prompt = resolve_ocr_prompt("ggml-org/DeepSeek-OCR-GGUF") - assert prompt == "<|grounding|>Convert the document to markdown." - - def test_glm_ocr_gets_terse_prompt(self): - from lilbee.vision import resolve_ocr_prompt - - assert resolve_ocr_prompt("ggml-org/GLM-OCR-GGUF") == "OCR" - - def test_match_is_case_insensitive_and_works_on_a_gguf_path(self): - from lilbee.vision import resolve_ocr_prompt - - path = "/models/ggml-org/GLM-OCR-GGUF/glm-ocr-Q8_0.gguf" - assert resolve_ocr_prompt(path) == "OCR" - - def test_unknown_model_falls_back_to_generic_prompt(self): - from lilbee.vision import OCR_PROMPT, resolve_ocr_prompt - - assert resolve_ocr_prompt("unsloth/Qwen3-VL-8B-Instruct-GGUF") == OCR_PROMPT diff --git a/tests/test_vision_ocr_backend.py b/tests/test_vision_ocr_backend.py new file mode 100644 index 000000000..7859d2ca8 --- /dev/null +++ b/tests/test_vision_ocr_backend.py @@ -0,0 +1,172 @@ +"""Tests for the lilbee vision OCR backend (xberg custom OCR plugin).""" + +from __future__ import annotations + +from lilbee.data.ingest.types import MARKDOWN_MIME, OcrBackendName +from lilbee.data.ingest.vision_ocr_backend import ( + VisionOcrBackend, + backend_options_for, + ocr_request, + ocr_requests, +) + + +def _backend(ocr_fn=None, model="vendor/glm-ocr"): + calls: list[tuple] = [] + + def default_fn(image_bytes, model, prompt, *, timeout): + calls.append((image_bytes, model, prompt, timeout)) + return "# extracted" + + be = VisionOcrBackend(ocr_fn=ocr_fn or default_fn, model_ref_fn=lambda: model) + return be, calls + + +def _cfg(*, vlm_prompt=None, backend_options=None): + """The native xberg OcrConfig object xberg hands process_image.""" + from xberg import OcrConfig + + return OcrConfig( + backend=OcrBackendName.LILBEE_VISION, + vlm_prompt=vlm_prompt, + backend_options=backend_options, + ) + + +class TestProtocol: + def test_name_is_enum_value(self): + be, _ = _backend() + assert be.name() == OcrBackendName.LILBEE_VISION == "lilbee-vision" + + def test_backend_type_is_custom(self): + from xberg import OcrBackendType + + be, _ = _backend() + assert be.backend_type() == OcrBackendType.CUSTOM + + def test_supports_all_languages(self): + be, _ = _backend() + assert be.supported_languages() == [] + assert be.supports_language("eng") is True + assert be.supports_language("zho") is True + + def test_version_is_str(self): + be, _ = _backend() + assert isinstance(be.version(), str) and be.version() + + def test_version_falls_back_when_package_missing(self, monkeypatch): + from importlib.metadata import PackageNotFoundError + + from lilbee.data.ingest import vision_ocr_backend as mod + + def _raise(_name): + raise PackageNotFoundError + + monkeypatch.setattr(mod, "version", _raise) + be, _ = _backend() + assert be.version() == "0" + + def test_initialize_shutdown_noop(self): + be, _ = _backend() + assert be.initialize() is None + assert be.shutdown() is None + + def test_capability_flags_are_image_only(self): + be, _ = _backend() + assert be.supports_table_detection() is False + assert be.supports_document_processing() is False + assert be.emits_structured_markdown() is False + + def test_process_image_file_reads_bytes_and_delegates(self, tmp_path): + be, calls = _backend() + img = tmp_path / "scan.png" + img.write_bytes(b"PNG-on-disk") + out = be.process_image_file(str(img), _cfg()) + assert out.content == "# extracted" + assert out.mime_type == MARKDOWN_MIME + assert calls[0][0] == b"PNG-on-disk" + + def test_process_document_is_unsupported(self): + import pytest + + be, _ = _backend() + with pytest.raises(NotImplementedError): + be.process_document("/tmp/doc.pdf", _cfg()) + + +class TestProcessImage: + def test_returns_extracted_document(self): + """xberg expects a native ExtractedDocument back, with the OCR text as markdown.""" + from xberg import ExtractedDocument + + be, _ = _backend() + out = be.process_image(b"PNG", _cfg()) + assert isinstance(out, ExtractedDocument) + assert out.content == "# extracted" + assert out.mime_type == MARKDOWN_MIME + + def test_passes_model_and_resolved_prompt(self): + be, calls = _backend(model="vendor/glm-ocr") + be.process_image(b"PNG", _cfg()) + _, model, prompt, _ = calls[0] + assert model == "vendor/glm-ocr" + # glm-ocr has a native prompt; resolve_ocr_prompt picks it, not the generic one. + assert prompt == "OCR" + + def test_vlm_prompt_overrides_resolved(self): + be, calls = _backend() + be.process_image(b"PNG", _cfg(vlm_prompt="custom prompt")) + assert calls[0][2] == "custom prompt" + + def test_request_context_supplies_timeout_and_fires_progress(self): + ticks: list[int] = [] + be, calls = _backend() + with ocr_request(on_page=lambda: ticks.append(1), timeout=12.5) as token: + be.process_image(b"PNG", _cfg(backend_options=backend_options_for(token))) + assert calls[0][3] == 12.5 + assert ticks == [1] + + def test_json_string_backend_options_resolve(self): + # The native round-trip hands backend_options back as a JSON STRING + # (alef serializes the map). The token must resolve from that shape or + # per-page progress and the OCR timeout silently vanish on every real + # extraction, while dict-based unit tests stay green. + import json + + ticks: list[int] = [] + be, calls = _backend() + with ocr_request(on_page=lambda: ticks.append(1), timeout=7.5) as token: + be.process_image(b"PNG", _cfg(backend_options=json.dumps(backend_options_for(token)))) + assert calls[0][3] == 7.5 + assert ticks == [1] + + def test_no_context_uses_zero_timeout_and_no_tick(self): + be, calls = _backend() + be.process_image(b"PNG", _cfg(backend_options=backend_options_for("unknown-token"))) + assert calls[0][3] == 0.0 + + def test_malformed_backend_options_ignored(self): + # Non-JSON and valid-but-non-object backend_options both resolve to no token. + be, calls = _backend() + be.process_image(b"PNG", _cfg(backend_options="not-json")) + be.process_image(b"PNG", _cfg(backend_options="123")) + assert calls[0][3] == 0.0 + assert calls[1][3] == 0.0 + + +class TestRegistry: + def test_token_registered_within_scope_and_cleaned_after(self): + with ocr_request(timeout=3.0) as token: + ctx = ocr_requests.get(token) + assert ctx is not None and ctx.timeout == 3.0 + assert ocr_requests.get(token) is None + + def test_get_none_token_returns_none(self): + assert ocr_requests.get(None) is None + + def test_backend_options_round_trip(self): + token = "abc123" + be, calls = _backend() + be.process_image(b"PNG", _cfg(backend_options=backend_options_for(token))) + # token not registered -> no context, zero timeout + assert calls[0][3] == 0.0 diff --git a/tests/test_xberg_extract.py b/tests/test_xberg_extract.py new file mode 100644 index 000000000..28e7f4ae7 --- /dev/null +++ b/tests/test_xberg_extract.py @@ -0,0 +1,44 @@ +"""Tests for the xberg async-extract bridge (lilbee.data.xberg_extract).""" + +from __future__ import annotations + +from unittest import mock + +import pytest + +from lilbee.data import xberg_extract + + +class _FakeResult: + def __init__(self, results, errors=()): + self.results = list(results) + self.errors = list(errors) + + +def test_first_returns_single_document(): + doc = object() + assert xberg_extract._first(_FakeResult([doc])) is doc + + +def test_first_raises_on_extraction_error(): + with pytest.raises(RuntimeError, match="boom"): + xberg_extract._first(_FakeResult([], errors=["boom"])) + + +def test_first_raises_when_no_document(): + with pytest.raises(RuntimeError, match="no document"): + xberg_extract._first(_FakeResult([])) + + +@pytest.mark.asyncio +async def test_extract_document_offloads_when_a_loop_is_running(): + """Called from a thread with a live event loop, the sync bridge runs the + coroutine on a worker thread instead of re-entering the running loop.""" + doc = object() + + async def fake_extract(_input, _config): + return _FakeResult([doc]) + + with mock.patch("xberg.extract", fake_extract): + out = xberg_extract.extract_document(b"data", "text/plain", config=mock.MagicMock()) + assert out is doc diff --git a/tools/wheel-build/build_lilbee_binary.sh b/tools/wheel-build/build_lilbee_binary.sh index 9a569e117..50d44e72d 100755 --- a/tools/wheel-build/build_lilbee_binary.sh +++ b/tools/wheel-build/build_lilbee_binary.sh @@ -129,7 +129,7 @@ uv run --no-sync python -m nuitka \ --include-package=tiktoken --include-package-data=tiktoken \ --include-package=tiktoken_ext --include-package-data=tiktoken_ext \ --include-package-data=numpy \ - --include-package=kreuzberg --include-package-data=kreuzberg \ + --include-package=xberg --include-package-data=xberg \ --include-package=litellm --include-package=litellm.llms --include-package-data=litellm \ --include-package=crawl4ai --include-package-data=crawl4ai \ --include-package=fake_useragent --include-package-data=fake_useragent \ diff --git a/tools/wheel-build/check_macos_floor.sh b/tools/wheel-build/check_macos_floor.sh new file mode 100755 index 000000000..a5191b38f --- /dev/null +++ b/tools/wheel-build/check_macos_floor.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# Fail if any native library in the build venv is built for a newer macOS than +# the deployment target the standalone binary claims. +# +# Nuitka bundles these dylibs into the onefile, and dyld enforces each one's +# LC_BUILD_VERSION minos independently of the launcher's. A dependency built on +# a macOS 15 runner therefore ships a binary that dies at load on macOS 11-14 +# even though the launcher itself is pinned to 11.0 -- the failure lands on the +# user, not on the build, so nothing here catches it without an explicit check. +# +# Usage: +# MACOSX_DEPLOYMENT_TARGET=11.0 tools/wheel-build/check_macos_floor.sh +# No-op off macOS. +set -euo pipefail + +[[ "$(uname -s)" == "Darwin" ]] || exit 0 + +FLOOR="${MACOSX_DEPLOYMENT_TARGET:?MACOSX_DEPLOYMENT_TARGET is required}" +SITE_PACKAGES="$(uv run --no-sync python -c 'import sysconfig; print(sysconfig.get_paths()["purelib"])')" + +# Sort -V orders versions, so the greater of {floor, minos} is the last line; a +# minos that is not the floor and sorts above it is newer than the floor. +exceeds_floor() { + [[ "$1" != "$FLOOR" && "$(printf '%s\n%s\n' "$FLOOR" "$1" | sort -V | tail -1)" == "$1" ]] +} + +offenders="" +while IFS= read -r lib; do + # A Mach-O may carry no LC_BUILD_VERSION (older linkers emit LC_VERSION_MIN_MACOSX + # instead, and text files caught by the glob carry neither); read both, skip neither-case. + minos="$(otool -l "$lib" 2>/dev/null | awk '/LC_BUILD_VERSION|LC_VERSION_MIN_MACOSX/{f=1} f&&/minos|version/{print $2; exit}')" + [[ -n "$minos" ]] || continue + if exceeds_floor "$minos"; then + offenders+=" $minos ${lib#"$SITE_PACKAGES"/}"$'\n' + fi +done < <(find "$SITE_PACKAGES" \( -name '*.so' -o -name '*.dylib' \) -type f) + +if [[ -n "$offenders" ]]; then + echo "ERROR: these bundled libraries require a newer macOS than the ${FLOOR} floor" >&2 + echo " this binary claims, so it would fail to launch below their minos:" >&2 + printf '%s' "$offenders" >&2 + exit 1 +fi + +echo "macOS floor OK: every bundled library loads on ${FLOOR}+" diff --git a/uv.lock b/uv.lock index cd12a1f07..079938113 100644 --- a/uv.lock +++ b/uv.lock @@ -2,11 +2,14 @@ version = 1 revision = 1 requires-python = ">=3.11" resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform != 'win32'", + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version < '3.12' and sys_platform == 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'win32'", - "python_full_version < '3.12'", + "python_full_version < '3.12' and sys_platform != 'win32'", ] [[package]] @@ -20,11 +23,11 @@ wheels = [ [[package]] name = "aiohappyeyeballs" -version = "2.6.1" +version = "2.7.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760 } +sdist = { url = "https://files.pythonhosted.org/packages/ce/f4/eec0465c2f67b2664688d0240b3212d5196fd89e741df67ddb81f8d35658/aiohappyeyeballs-2.7.1.tar.gz", hash = "sha256:065665c041c42a5938ed220bdcd7230f22527fbec085e1853d2402c8a3615d9d", size = 24757 } wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265 }, + { url = "https://files.pythonhosted.org/packages/71/43/1947f06babed6b3f1d7f38b0c767f52df66bfb2bc10b468c4a7de9eceff2/aiohappyeyeballs-2.7.1-py3-none-any.whl", hash = "sha256:9243213661e29250eb41368e5daa826fc017156c3b8a11440826b2e3ed376472", size = 15038 }, ] [[package]] @@ -177,7 +180,8 @@ dependencies = [ { name = "networkx" }, { name = "numpy" }, { name = "rtree" }, - { name = "scipy" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "scipy", version = "1.18.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "shapely" }, { name = "trimesh" }, ] @@ -206,37 +210,78 @@ wheels = [ [[package]] name = "anyio" -version = "4.12.1" +version = "4.14.2" 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 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592 }, +sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813 }, +] + +[[package]] +name = "ast-serialize" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/ad/0d70a3a2d6e01968d985415259e8ec7ad3f777903f9b1c1f3c8c44642c60/ast_serialize-0.6.0.tar.gz", hash = "sha256:aadd3ffcf4858c9726bf3515f7b199c7eadbe504f96028e4a87172c0da65a8fe", size = 61489 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/12/3e5f575f156555547c250a8b0d1347517a3a20fc7f4492e9703a69d4f45e/ast_serialize-0.6.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:a7520b672827885bafeae7501f684d14d47d17e5f45256f9df547686cca52264", size = 1177640 }, + { url = "https://files.pythonhosted.org/packages/a2/a4/921a9e27951627983b0f368859ea00f8330a551dc0bf4c2fdcb11855a98b/ast_serialize-0.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a14191beec7e0c078d2fc1f6edc0aee88bcd4db9f18e1bc9f8052b559c22dddc", size = 1168111 }, + { url = "https://files.pythonhosted.org/packages/00/69/950cf404de7b8782cf95e5c1237e25e2aa46177b287f39f9eeddf481fd6f/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32ef62ec34cf6be20ad77d4799556638fbdf187f3ae10698dfb20ef9f2c89516", size = 1227656 }, + { url = "https://files.pythonhosted.org/packages/4c/a8/46f8f6a6479d9d2273980957bb091a506c55f5b95d3c029ee58518a78407/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:13b7769970a39983b0adf2f38917b1cd3b8946f76df045756c3d741bc689f089", size = 1227706 }, + { url = "https://files.pythonhosted.org/packages/b7/b9/9ac415bda0a40e49eab8fea3b2741c19c98bb84d57d62c4cfc6230eb67be/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f7a408601bb3edaefb3bc67a4c01f5235e3253653b6a5729a2ee2382b35341c", size = 1431705 }, + { url = "https://files.pythonhosted.org/packages/e5/06/8807115d441444879f7561b5eede5ac18fc80392f11826d61ccf31f503b1/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8670bfa51208a2c0c8d138928e40e998fab158f9200d53bb80c088b5b8eda7b8", size = 1249533 }, + { url = "https://files.pythonhosted.org/packages/3e/c0/c2ba82ef9618650357d9421a1fdb27ffec862a7f57e8e2de82a3ccd11e12/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4826809eb8597a8cd59fd924b6d7c285b8969a1e0007e2cb652cab62376270f", size = 1252619 }, + { url = "https://files.pythonhosted.org/packages/0f/a7/fa31d52dd4102cede29fb9634e98d214129b2783b4f95528c6dc6a8f6587/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:577a6c189068686869f5f1ddc38363f3ae1808a4753b577266f9202071a7bb66", size = 1242983 }, + { url = "https://files.pythonhosted.org/packages/b1/20/ddf742b5ad3c4bafd3466f2265037cfd99bc1b9a5ee46a5d58c90d523242/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:085de7f62dc9cc247eb01e965a362707d1d90b1d89a82c5bf78301a60a3c417b", size = 1296148 }, + { url = "https://files.pythonhosted.org/packages/24/cb/9f6f217cce8b3b632c5568b478d195a35e79dce4dbe309438cb89ba6ea4f/ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9f8a8b78b13173de6a9ec22111d9be674874cd5bdccda04f14ae5ebc2bef403a", size = 1403826 }, + { url = "https://files.pythonhosted.org/packages/2d/f8/9d16d4f0107a183924425cc0e7618d8bf76f96b45afa9ff19f924ed1ad57/ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f2ff3baffc3a29c1f15bc9098aa0c09763410262d5e6cef42116f7356c184554", size = 1502943 }, + { url = "https://files.pythonhosted.org/packages/80/dd/bbc1c38756350dddf7e24acae1c9482ef42051c267417e019aecc1ed4075/ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0067b25fce104eaae5b88383de9ab803faeb671831e14ca698b771b356e2600f", size = 1497632 }, + { url = "https://files.pythonhosted.org/packages/42/7e/9daffefcf5b97e6bb4c3e0b3c024c1aee9722f23d3cf7cd2ff80d6fb4a40/ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c617417f9cbb0cb144f6283c3cbe0d2e0f01beaf9f608f662b21191058a626ec", size = 1448858 }, + { url = "https://files.pythonhosted.org/packages/e5/1f/f9baaab81a677ea0af7d2458cac2f94ebcc85958f8a3c15ba9d9e5dab653/ast_serialize-0.6.0-cp314-cp314t-win32.whl", hash = "sha256:5337cb256dcea3df9288205213d1601581536526b8f4da44b6974f1180f3252a", size = 1052600 }, + { url = "https://files.pythonhosted.org/packages/9e/1f/41b535866519512d8cf6669cb2cff7823b7672bb6279c0333b4ff89d7d9f/ast_serialize-0.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2d947e45cafc4b09bd7528917fa84c517654a43de173c79785574b7b3068ac24", size = 1095570 }, + { url = "https://files.pythonhosted.org/packages/50/64/e472fe3e3a2d33d874b987e8518aedf24562919e3b6161a4fa1797e89c0f/ast_serialize-0.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:6e15ec740436e1a0d62de848641abe5f3a2f89a7f94907d534795ac91bbacf14", size = 1067267 }, + { url = "https://files.pythonhosted.org/packages/52/19/ac8348ae8711c9b5ae834634f635780cab62a0f5e6f988882e048b89c2ae/ast_serialize-0.6.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:093cb8bb91b720d8523580498d031791bb1bbaa048599c3d21085d380e11a596", size = 1185367 }, + { url = "https://files.pythonhosted.org/packages/c1/f6/ec7ec652c51db77c2f61d8573338e13e4704303265ccc658cb4031d9f354/ast_serialize-0.6.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:e61580a69faf47e3689795367ed211f2a10fd741478cc0f36a0f128793360aad", size = 1178657 }, + { url = "https://files.pythonhosted.org/packages/6f/02/613a7534a41d0122f37d1e0c64aa8ac78bfb831f8c92f6db057a311abb3c/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:305802f2ce2a7c4e87835078ea85c58b586ddda8095b92fe2ead9364ae19c80a", size = 1238620 }, + { url = "https://files.pythonhosted.org/packages/4d/21/087957bba486242afc52f49b2d9e21c9dad00289356cf9efe67084015a9d/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c7b8b8f0c42f752ea00b2b7d7c090b3f80d9c1c5c75cadf16423790a0cc74081", size = 1236075 }, + { url = "https://files.pythonhosted.org/packages/82/04/78128bbb170071c2c72a210a181f1c00e11cc1cec60a8beef747b07f9201/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd5b91b9e6f2356ace3a556963b0cd783b395fbbb0bb17b4defc283415466e77", size = 1441348 }, + { url = "https://files.pythonhosted.org/packages/64/64/62fb99d6faf199b4c3e5b08a07136e9a0d7664bb249c6de3670e5b63e9b6/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4d6ef91590258ada18909b9caea344dac4de2013906b035473cd674a43f4b790", size = 1258580 }, + { url = "https://files.pythonhosted.org/packages/ca/87/b4d6c38e0ccd5e85dc54cecdf933a152c60b28fe5d993a6d8a72fa6d5896/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcbed41e9386059fc0261d602445ede0976c2ecec2939688bcbcb9ed0b6f28b7", size = 1261693 }, + { url = "https://files.pythonhosted.org/packages/0e/4b/3676ca2191f39bafb75f93f99b2f429ec464586158fece2165f3572805dc/ast_serialize-0.6.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:cdc4e6f930b9090c2f92c9036ad12ffb8e6e44d4a5ba06f1458a05d60f203f7b", size = 1252517 }, + { url = "https://files.pythonhosted.org/packages/f3/58/494ef8c4b4acb2f4a265ac934caf45f792a08fe27d6b853de35ad991941a/ast_serialize-0.6.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:897ac47b5637be41c0c07061c8a912fafa967ef1dc73fa115e4bfa70882a093b", size = 1304843 }, + { url = "https://files.pythonhosted.org/packages/b1/f2/13736d920ab3d49bbee80ef1a277dd7b7aaf3b3545efd9d2a8114fe05525/ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c4af9a1386166e40ed01464991806f89038a2d89782576c7774876fa77034e32", size = 1413698 }, + { url = "https://files.pythonhosted.org/packages/a8/5a/e046f3899e2acba4677d7427b76431443a1aa1a0e583dfb05b55b69d55cf/ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:c901adbd750029b9ac4ad3d6aa56853e0ad4875119fbf52b7b8298afc223828b", size = 1512209 }, + { url = "https://files.pythonhosted.org/packages/cc/c7/e42aaca7bb2d22a7c06d5a8c7930086c5a334e93d716e6fa5e6647a4515f/ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:3ae22a366b752ab4496191525b78b097b5b72d531752e3c1dd7e383a8f2c8a1a", size = 1508464 }, + { url = "https://files.pythonhosted.org/packages/95/93/5524a3dc6c3f593de3228ed9cbef73afa047625b7000ec21b7f58e6eb4d4/ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4ed29121da8b3fdc291002801a1de0f76248fa07dce89157a5f277842cf6126e", size = 1457164 }, + { url = "https://files.pythonhosted.org/packages/4f/c0/36a6ffb4d653cf621427b4c4928671f53ad800c453474de2b82564a44ad9/ast_serialize-0.6.0-cp39-abi3-pyemscripten_2026_0_wasm32.whl", hash = "sha256:b1dac4e09d341c1300ba69cdcbe62867b32a8c75d90db9bf4d083bec3b039f0b", size = 863014 }, + { url = "https://files.pythonhosted.org/packages/09/c7/7d5ad8b49e1278e1c2a1e0274bd7850560b3f09313aa00c13bc8d5544792/ast_serialize-0.6.0-cp39-abi3-win32.whl", hash = "sha256:82c312a7844d2fdeb4d5c48bd3d215bf940dafd4704e1a9bcf252a99010a99b1", size = 1063165 }, + { url = "https://files.pythonhosted.org/packages/47/ae/6710c14ecb276031cf10249f6adf5a59e2d3fdb3b5183bd59f70524067ee/ast_serialize-0.6.0-cp39-abi3-win_amd64.whl", hash = "sha256:113b58346f9ceb664352032770caca817d4a3c86f611c6088e6ef65ddaa70f0e", size = 1101444 }, + { url = "https://files.pythonhosted.org/packages/66/40/c53deb2cd0c9b0fb636d24d9f40924cf2e65028e6b20b10cd5c1eeb2c730/ast_serialize-0.6.0-cp39-abi3-win_arm64.whl", hash = "sha256:ccd132fe8db56f61fe743b1f644d01b8d65b83248a8da506f3132bda86d6ed5e", size = 1072965 }, ] [[package]] name = "attrs" -version = "25.4.0" +version = "26.1.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 } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055 } wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615 }, + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548 }, ] [[package]] name = "beautifulsoup4" -version = "4.14.3" +version = "4.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "soupsieve" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737 } +sdist = { url = "https://files.pythonhosted.org/packages/43/65/318323f98dbee45d42dff61d8f047181bc6f2268a9068cfad035a46be5af/beautifulsoup4-4.15.0.tar.gz", hash = "sha256:288e3ca7d54b06f2ac191970bc275c1939cb46d450b255bf6718b04aa37ab4f7", size = 632571 } wheels = [ - { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721 }, + { url = "https://files.pythonhosted.org/packages/88/c6/92fcd42f1ba33e1184263f25bfabf3d27c383410470f169e4b8163bf9c17/beautifulsoup4-4.15.0-py3-none-any.whl", hash = "sha256:d6f88de62e1d4e38ecb1077eb9724cd0eff29d2a08ca16a401e9b9e93f117cf9", size = 109924 }, ] [[package]] @@ -337,81 +382,109 @@ wheels = [ [[package]] name = "certifi" -version = "2026.2.25" +version = "2026.6.17" 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 } +sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594 } wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684 }, + { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289 }, ] [[package]] name = "cffi" -version = "2.0.0" +version = "2.1.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 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344 }, - { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560 }, - { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613 }, - { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476 }, - { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374 }, - { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597 }, - { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574 }, - { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971 }, - { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972 }, - { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078 }, - { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076 }, - { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820 }, - { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932 }, - { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557 }, - { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909 }, - { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402 }, - { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328 }, - { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650 }, - { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487 }, - { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726 }, - { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195 }, +sdist = { url = "https://files.pythonhosted.org/packages/57/5f/ff100cae70ebe9d8df1c01a00e510e45d9adb5c1fdda84791b199141de97/cffi-2.1.0.tar.gz", hash = "sha256:efc1cdd798b1aaf39b4610bba7aad28c9bea9b910f25c784ccf9ec1fa719d1f9", size = 531036 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/67/85c89a59ba36a671e79638f44d466749f08179266a57e4f2ffdf92174072/cffi-2.1.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:02cb7ff33ded4f1532476731f89ede53e2e488a8e6205515a82144246ffa7dcc", size = 183845 }, + { url = "https://files.pythonhosted.org/packages/ea/dd/e3b0baa2d3d6a857ac72b7efbf18e32e487c9cdafcc13049ad765495b15e/cffi-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f5bce581e6b8c235e566a14768a943b172ada3ed73537bb0c0be1edee312d4e7", size = 184186 }, + { url = "https://files.pythonhosted.org/packages/65/68/9f3ef890cf3c6ab97bd531c5677f67613d302165d16f8142b2811782a614/cffi-2.1.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:30b65779d598c370374fefabf138d456fd6f3216bfa7bedfab1ba82025b0cd93", size = 211892 }, + { url = "https://files.pythonhosted.org/packages/22/d7/1a74539db16d8bfd839ff1515948948efbb162e574650fd3d846896eea95/cffi-2.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88023dfe18799507b73f1dbb0d14326a17465de1bc9c9c7655c22845e9ddc3a2", size = 218793 }, + { url = "https://files.pythonhosted.org/packages/ec/d1/9a5b7169499e8e8d8e636de70b97ac7c9447104d2ff1a2cd94790cea5162/cffi-2.1.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:0a96b74cda968eebbad56d973efe5098974f0a9fb323865bf99ea1fd24e3e64c", size = 205737 }, + { url = "https://files.pythonhosted.org/packages/ba/b0/e131a9c41f10607926278453d9596163594fe1c4ebc46efe3b5e5b34eb84/cffi-2.1.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a5781494d4d400a3f47f8f1da94b324f6e6b440a53387774002890a2a2f4b50f", size = 204909 }, + { url = "https://files.pythonhosted.org/packages/fb/d2/4398416cd699b35167947c6e22aca52c47e69ad5695073c9f1f2c52e04aa/cffi-2.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aa7a1b53a2a4452ada2d1b5dade9960b2522f1e61293a811a077439e39029565", size = 217883 }, + { url = "https://files.pythonhosted.org/packages/a2/a5/d4fe77b589e5e82d43ebc809bf2e6474afe8e48e32ea050b9357645b6471/cffi-2.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9d8272c0e483b024e1b9ad029821470ed8ec65631dbd90217469da0e7cd89f1c", size = 221251 }, + { url = "https://files.pythonhosted.org/packages/22/f0/a2fc43084c0433caf7f461bccc013e28f848d04ee1c5ed7fce71423cf4d9/cffi-2.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7762faa47e8ff7eb80bd261d9a7d8eea2d8baa69de5e95b70c1f338bbe712f02", size = 214250 }, + { url = "https://files.pythonhosted.org/packages/04/8c/b925975448cf20634a9fbd5efceb807219db452653648d2897c0989cab2d/cffi-2.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:89095c1968b4ba8285840e131bf2891b09ae137fe2146905acae0354fbce1b5e", size = 219441 }, + { url = "https://files.pythonhosted.org/packages/eb/da/5c4918a2d61d86fa927d716cb3d8e4626ef8dc8f605a599d32f33897f59a/cffi-2.1.0-cp311-cp311-win32.whl", hash = "sha256:64c753a0f87a256020004f37a1c8c02c480e725f910f0b2a0f3f07debd1b2479", size = 174496 }, + { url = "https://files.pythonhosted.org/packages/f9/c8/6c2de1d55cf35ef8b92885d5ef280790f0fb9634d87ea1cc315176aecd61/cffi-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:4f26194e3d95e06501b942642855aed4f953d55e95d7d01b7c4483db3ecff458", size = 185113 }, + { url = "https://files.pythonhosted.org/packages/9e/4e/e8d7cb5783f1841a3c8fb3a7735838d7484d08ec08c9f984b14cac1ac0e9/cffi-2.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:35aaea0c7ee0e58a5cd8c2fd1a48fdf7ece0d2699b7ecdda08194e9ce5dd9b3d", size = 179927 }, + { url = "https://files.pythonhosted.org/packages/1e/85/990925db5df586ec90beb97529c853497e7f85ba0234830447faf41c3057/cffi-2.1.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:df2b82571a1b30f58a87bf4e5a9e78d2b1eff6c6ce8fd3aa3757221f93f0863f", size = 184829 }, + { url = "https://files.pythonhosted.org/packages/4b/92/e7bb136ad6b5352603732cf907ef862ca103f20f2031c1735a46300c20c9/cffi-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78474632761faa0fb96f30b1c928c84ebcf68713cbb80d15bab09dfe61640fde", size = 184728 }, + { url = "https://files.pythonhosted.org/packages/c3/c0/d1ec30ffb370f748f2fb54425972bfef9871e0132e82fb589c46b6676049/cffi-2.1.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:5972433ad71a9e46516584ef60a0fda12d9dc459938d1539c3ddecf9bdc1368d", size = 214815 }, + { url = "https://files.pythonhosted.org/packages/1b/dc/5620cf930688be01f2d673804291de757a934c90b946dbdc3d84130c2ea4/cffi-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b6422532152adf4e59b110cb2808cee7a033800952f5c036b4af047ee43199e7", size = 222429 }, + { url = "https://files.pythonhosted.org/packages/4b/a4/77b53abbf7a1e0beb9637edbef2a94d15f9c822f591e85d439ffd91519a6/cffi-2.1.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:46b1c8db8f6122420f32d02fffb924c2fe9bc772d228c7c711748fff56aabb2b", size = 210315 }, + { url = "https://files.pythonhosted.org/packages/58/0c/f528df19cc94b675087324d4760d9e6d5bfae97d6217aa4fac43de4f5fcc/cffi-2.1.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9fafc5aa2e2a39aaf7f8cc0c1f044a9b07fca12e558dca53a3cc5c654ad67a7", size = 208859 }, + { url = "https://files.pythonhosted.org/packages/62/f2/c9522a81c32132799a1972c39f5c5f8b4c8b9f00488a23feaa6c06f07741/cffi-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e9f50d192a3e525b15a75ab5114e442d83d657b7ec29182a991bc9a88fd3a66", size = 221844 }, + { url = "https://files.pythonhosted.org/packages/6e/28/bd53988b9833e8f8ad539d26f4c07a6b3f6bcb1e9e02e7ca038250b3428d/cffi-2.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:98fff996e983a36d3aa2eca83af40c5821202e7e6f32d13ae94e3d2286f10cfe", size = 225287 }, + { url = "https://files.pythonhosted.org/packages/79/99/0d0fd37f055224085f42bbb2c022d002e17dde4a97972822327b07d84101/cffi-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:379de10ce1ba048b1448599d1b37b24caee16309d1ac98d3982fc997f768700b", size = 223681 }, + { url = "https://files.pythonhosted.org/packages/b0/80/c138990aa2a70b1a269f6e06348729836d733d6f970867943f61d367f8cc/cffi-2.1.0-cp312-cp312-win32.whl", hash = "sha256:9b8f0f26ca4e7513c534d351eca551947d053fac438f2a04ac96d882909b0d3a", size = 175269 }, + { url = "https://files.pythonhosted.org/packages/a8/eb/f636456ff21a83fc13c032b58cc5dde061691546ac79efa284b2989b7982/cffi-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:c97f080ea627e2863524c5af3836e2270b5f5dfff1f104392b959f8df0c5d384", size = 185881 }, + { url = "https://files.pythonhosted.org/packages/dd/2c/400ea43e721727dca8a65c4521390e9196757caba4a45643acb2b63271b8/cffi-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:6d194185eabd279f1c05ebe3504265ddfc5ad2b58d0714f7db9f01da592e9eb6", size = 180088 }, + { url = "https://files.pythonhosted.org/packages/96/88/a996879e2eeccb815f6e3a5967b12a308257412acec882039d386bd2aa7b/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:10537b1df4967ca26d21e5072d7d54188354483b91dc75058968d3f0cf13fbda", size = 194331 }, + { url = "https://files.pythonhosted.org/packages/58/85/7ae00d5c8dd6266f4e944c3db630f3c5c9a98b61d469c714d848b1d8138a/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a95b05f9baf29b91171b3a8bd2020b028835243e7b0ff6bb23e2a3c228518b1b", size = 196966 }, + { url = "https://files.pythonhosted.org/packages/8c/e9/45c3a76ad8d43ad9261f4c95436da61128d3ca545d72b9612c0ab5be0b1c/cffi-2.1.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:15faec4adfff450819f3aee0e2e02c812de6edb88203aa58807955db2003472a", size = 184795 }, + { url = "https://files.pythonhosted.org/packages/84/4c/82f132cb4418ee6d953d982b19191e87e2a6372c8a4ce36e50b69d6ade4a/cffi-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:716ff8ec22f20b4d988b12884086bcef0fc99737043e503f7a3935a6be99b1ea", size = 184746 }, + { url = "https://files.pythonhosted.org/packages/a0/1c/4ed5a0e5bdca6cbc275556de3328dd1b76fd0c11cc13c88fe66d1d8715f2/cffi-2.1.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:63960549e4f8dc41e31accb97b975abaecfc44c03e396c093a6436763c2ea7db", size = 214747 }, + { url = "https://files.pythonhosted.org/packages/3a/a6/e879bb68cc23a2bc9ba8f4b7d8019f0c2694bad2ab6c4a3701d429439f58/cffi-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ff067a8d8d880e7809e4ac88eb009bb848870115317b306666502ccad30b147f", size = 222392 }, + { url = "https://files.pythonhosted.org/packages/88/f6/01890cfd63c08f8eb96a8319b0443690197d240a8bd6346048cf7bde9190/cffi-2.1.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3b926723c13eba9f81d2ef3820d63aeceec3b2d4639906047bf675cb8a7a500d", size = 210285 }, + { url = "https://files.pythonhosted.org/packages/a6/cf/2b684132056f438567b61e19d690dd31cd0921ace051e0a458be6074369e/cffi-2.1.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:47ff3a8bfd8cb9da1af7524b965127095055654c177fcfc7578debcb015eecd0", size = 208801 }, + { url = "https://files.pythonhosted.org/packages/6f/08/f2e7d62c460faae0926f2d6e423694aa409ced3bc1fe2927a0a6e5f05416/cffi-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:799416bae98336e400981ff6e532d67d5c709cfb30afb79865a1315f94b0e224", size = 221808 }, + { url = "https://files.pythonhosted.org/packages/38/37/04f54b8e63a02f3d908332c9effbf8c366167c6f733ed8a3d4f79b7e2a1e/cffi-2.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:961be50688f7fba2fa65f63712d3b9b341a22311f5253460ce933f52f0de1c8c", size = 225241 }, + { url = "https://files.pythonhosted.org/packages/a9/d6/c72eecca433cd3e681c65ed313ab4835d9d4a379704d0f628a6a05f51c2e/cffi-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bf5c6cf48238b0eb4c086978c492ad1cbc22373fc5b2d7353b3a598ce6db887a", size = 223588 }, + { url = "https://files.pythonhosted.org/packages/c6/4b/e706f67279140f92939da3475ad610df18bfd52d50f14953a8e5fede71d5/cffi-2.1.0-cp313-cp313-win32.whl", hash = "sha256:db3eb7d46527159a878ec3460e9d40615bc25ba337d477db681aea6e4f05c5d2", size = 175248 }, + { url = "https://files.pythonhosted.org/packages/5a/47/59eb7975cb0e4ef0afa764ea945b29a5bb4537a9f771cb7d6c8a5dd74c95/cffi-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:8e74a6135550c4748af665b1b1118b6aab33b1fc6a16f9aff630af107c3b4512", size = 185717 }, + { url = "https://files.pythonhosted.org/packages/5a/af/34fee85c48f8d94efc8597bc09470c9dd274c145f1c12e0fbc6ab6d38d74/cffi-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:2282cd5e38aa8accd03e99d1256af8411c84cdbee6a89d841b563fdbd1f3e50f", size = 180114 }, + { url = "https://files.pythonhosted.org/packages/d8/f0/81478e482afa03f6d18dc8f2afb5edc45b3080853b634b5ed91961be0998/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d2117334c3af3bdcb9a88522b844a2bdb5efdc4f71c6c822df55486ae1c3347a", size = 194142 }, + { url = "https://files.pythonhosted.org/packages/7d/95/8de304305cd9204974b0ca051b86d307cafca13aa575a0ef1b44d92c0d8c/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:702c436735fbe99d59ada02a1f65cfc0d31c0ee8b7290912f8fbc5cd1e4b16c3", size = 196819 }, + { url = "https://files.pythonhosted.org/packages/20/71/7c8372d30e42415602ed9f268f7cfd66f1b855fed881ecd168bcb45dbc0b/cffi-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1ff3456eab0d889592d1936d6125bbfbc7ae4d3354a700f8bd80450a66445d4d", size = 184965 }, + { url = "https://files.pythonhosted.org/packages/d6/5c/584e626835f0375c928176c04137c96927165cb8733cdb3150ec04e5ee5e/cffi-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c4165821e131d6d4ca444347c2b694e2311bcfa3fe5a861cc72968f28867beac", size = 184952 }, + { url = "https://files.pythonhosted.org/packages/2e/d2/065fcae1c73979fac8e054462478d0ff8a29c40cdc2ed7ea5676a061df53/cffi-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:276f20fffd7b396e12516ba8edf9509210ac248cbbc5acbc39cd512f9f59ebe6", size = 222353 }, + { url = "https://files.pythonhosted.org/packages/ed/a5/e8bbb1ce5b3ac2f53ad6a10bde44318a5a8d99d4f4a000d44a6e39aeb3e4/cffi-2.1.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7d5980a3433d4b71a5e120f9dd551403d7824e31e2e67124fe2769c404c06913", size = 210051 }, + { url = "https://files.pythonhosted.org/packages/28/ed/c127d3ac36e899c965e3361357c3befacd6578c03f40125183e41c3b219e/cffi-2.1.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6ca4919c6e4f89aa99c42510b42cf54596892c00b3f9077f6bdd1505e24b9c8d", size = 208630 }, + { url = "https://files.pythonhosted.org/packages/cc/d7/97d3136f81db489ec8d1d67748c110d6c994268fd7528014aa9f2b085e4e/cffi-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d53d10f7da99ae46f7373b9150393e9c5eab9b224909982b43832668de4779f5", size = 221593 }, + { url = "https://files.pythonhosted.org/packages/d3/27/93195977168ee63aed233a1a0993a2178798654d1f4bddcdd321d6fd3b21/cffi-2.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c351efb95e832a853a29361675f33a7ce53de1a109cd73fd47af0712213aa4ce", size = 225146 }, + { url = "https://files.pythonhosted.org/packages/b3/c1/6dbd291ee2ae5a50a034aa057207081f545923bbf15dad4511e985aafff5/cffi-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:dbf7c7a88e2bac086f06d14577332760bdeecc42bdec8ac4077f6260557d9326", size = 223240 }, + { url = "https://files.pythonhosted.org/packages/0f/6f/ade5ce9863a57992a6ea3d0d10d7e29b8749fc127204b3d493d667b2815f/cffi-2.1.0-cp314-cp314-win32.whl", hash = "sha256:1854b724d00f6654c742097d5387569021be12d3a0f770eae1df8f8acfcc6acd", size = 177723 }, + { url = "https://files.pythonhosted.org/packages/41/de/92b9eeed4ae4a21d6fd9b2a2c8505cbed573299902ea73981cc13f7ff62c/cffi-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:1b96bfe2c4bd825681b7d311ad6d9b7280a091f43e8f63da5729638083cd3bfb", size = 187937 }, + { url = "https://files.pythonhosted.org/packages/2e/1a/cc6ae6c2913a03aab8898eee57963cf1035b8df5872ed8b9115fcc7e2be8/cffi-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:7d28dff1db6764108bc30788d85d61c876beff416d9a49cb9dd7c5a9f34f5804", size = 183001 }, + { url = "https://files.pythonhosted.org/packages/14/f0/134c00ce0779ec86dea2aa1aac69339c2741a8045072676763512363a2ea/cffi-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7ea6b3e2c4250ff1de21c630fe72d0f63eb95c2c32ffbf64a358cf4a8836d714", size = 188538 }, + { url = "https://files.pythonhosted.org/packages/50/d8/3b86aba791cb610d24e8a3e1b2cd529e71fa15096b04e4d4e360049d4a4c/cffi-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6af371f3767faeffc6ac1ef57cdfd25844403e9d3f476c5537caee499de96376", size = 188230 }, + { url = "https://files.pythonhosted.org/packages/14/d0/117dcd9209255ad8571fbc8c92ef32593a1d294dcec91ddc4e4db50606f2/cffi-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb4e8997a49aa2c08a3e43c9045d224448b8941d88e7ac163c7d383e560cbf98", size = 223899 }, + { url = "https://files.pythonhosted.org/packages/b6/3d/f20f8b886b254e3ad10e15cd4186d3aed49f3e6a35ab37aab9f8f25f7c03/cffi-2.1.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf01d8c84cbea96b944c73b22182e6c7c432b3475632b8111dbfdc95ddad6e13", size = 211652 }, + { url = "https://files.pythonhosted.org/packages/28/3b/fad54de07260b93ddeef4b96d0131d57ea900675df1d410ae1deee52d7a6/cffi-2.1.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:33eb1ad83ebe8f313e0df035c406227d55a79456704a863fad9842136af5ad7d", size = 210755 }, + { url = "https://files.pythonhosted.org/packages/cc/82/3d5c705acb7abbba9bbd7d79b8e62e0f25b6120eb7ae6ac49f1b721722fe/cffi-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ac0f1a2d0cfa7eea3f2aaf006ab6e70e8feeb16b75d65b7e5939982ca2f11056", size = 223933 }, + { url = "https://files.pythonhosted.org/packages/6c/d0/47e338384ab6b1004241002fa616301020cea4fc95f283506565d252f276/cffi-2.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c16914df9fb7f500e440e6875fa23ff5e0b31db01fa9c06af98d59a91f0dc2e4", size = 226749 }, + { url = "https://files.pythonhosted.org/packages/70/25/65bd5b58ea4bfdfc15cde02cb5365f89ef8ab8b2adfb8fe5c4bd4233382f/cffi-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5ecbd0499275d57506d397eebe1981cee87b47fcd9ef5c22cab7ed7644a39a94", size = 225703 }, + { url = "https://files.pythonhosted.org/packages/dc/78/aa01ac599a8a4322533d45a1f9bc93b338276d2d59dabbe7c6d92a775c81/cffi-2.1.0-cp314-cp314t-win32.whl", hash = "sha256:7d034dcffa09e9a46c93fa3a3be402096cb5354ac6e41ab8e5cc9cd8b642ad76", size = 182857 }, + { url = "https://files.pythonhosted.org/packages/b9/26/d00496b22de4d4228f32dde94ad996f350c8aad676d63bcca0743c8dea4d/cffi-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0582a58f3051372229ca8e7f5f589f9e5632678208d8636fea3676711fdf7fe5", size = 194065 }, + { url = "https://files.pythonhosted.org/packages/d5/dd/0c7dbf815a579ff005008a2d815a55d6bb047c349eef536d9dc53d3f0a8d/cffi-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:510aeeeac94811b138077451da1fb18b308a5feab47dd2b603af55804155e1c8", size = 186404 }, + { url = "https://files.pythonhosted.org/packages/55/c7/8c8c50cb11c6750051daf12164098a9a6f027ac4356967fd4d800a07f242/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:2e9dabb9abcb7ad15938c7196ad5c1718a4e6d33cc79b4c0209bdb64c4a54a5c", size = 194121 }, + { url = "https://files.pythonhosted.org/packages/99/e2/67680bf19a6b60d2bb7ff83baefa2a4c3d2d7dc0f3277034b802e1fc504c/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:37f525a7e7e50c017fdebe58b787be310ad59357ae43a053943a6e1a6c526001", size = 196820 }, + { url = "https://files.pythonhosted.org/packages/ed/da/4bbe583a3b3a5c8c60892124fe17f3fa3656523faf0d3484eae90f091853/cffi-2.1.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:95f2954c2c9473d892eca6e0409f3568b37ab62a8eedb122461f73cc273476e3", size = 184936 }, + { url = "https://files.pythonhosted.org/packages/e5/4b/1f4c36ab273980d7aa75bb126ea4f8971f24a96108acad3a0a084028c57b/cffi-2.1.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:cdf2448aab5f661c9315308ec8b93f4e8a1a67a3c733f8631067a2b67d5913dc", size = 185045 }, + { url = "https://files.pythonhosted.org/packages/ef/c3/ad299dc38f3583f8d916b299f028af418a9ec98bc695fcbebeae7420691c/cffi-2.1.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:90bec57cf82089383bd06a605b3eb8daebf7e5a668520beaf6e327a83a947699", size = 222342 }, + { url = "https://files.pythonhosted.org/packages/eb/d8/df4543cc087245044ed02ef3ad8e0a26619d0075ac7a77a12dc81177851b/cffi-2.1.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6274dcb2d15cef48daa73ed1be5a40d501d74dccd0cd6db364776d12cb6ba022", size = 210073 }, + { url = "https://files.pythonhosted.org/packages/2c/0e/fac738d73728c6cea2a88a2883dca54892496cbba88a1dc1f2909cb8a6f5/cffi-2.1.0-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:2b71d409cccee78310ab5dec549aed052aaea483346e282c7b02362596e01bb0", size = 208551 }, + { url = "https://files.pythonhosted.org/packages/e6/3f/0b04a700dd64f465c93020253a793a82c9b4dff9961f48facd0df945d9b8/cffi-2.1.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7d3538f9c0e50670f4deb93dbb696576e60590369cae2faf7de681e597a8a1f1", size = 221649 }, + { url = "https://files.pythonhosted.org/packages/5d/7c/b7379a5704c79eda57ce075869ba70a0368d1c850f803b3c0d078d39dcaf/cffi-2.1.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:8f9ec95b8a043d3dfbc74d9abc6f7baf524dd27a8dc160b0a32ff9cdab650c28", size = 225203 }, + { url = "https://files.pythonhosted.org/packages/5a/02/d5e6c43ea85c41bda2a184a3418f195fe7cf602967a8d2b94e085b83deef/cffi-2.1.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:af5e2915d41fe6c961694d7bfdc8562942638200f3ce2765dfb8b745cf997629", size = 223263 }, + { url = "https://files.pythonhosted.org/packages/2c/d8/772b8259bf75749adffb1c546828978381fb516f60cf701f6c83daf60c85/cffi-2.1.0-cp315-cp315-win32.whl", hash = "sha256:0a42c688d19fca6e095a53c6a6e2295a5b050a8b289f109adab02a9e61a25de6", size = 177696 }, + { url = "https://files.pythonhosted.org/packages/2f/dd/afa2191fc6d57fedd26e5844a2fe2fcc0bbfa00961bbaa5a41e4921e7cca/cffi-2.1.0-cp315-cp315-win_amd64.whl", hash = "sha256:bccbbb5ee76a61f9d99b5bf3846a51d7fca4b6a732fe46f89295610edaf41853", size = 187914 }, + { url = "https://files.pythonhosted.org/packages/05/ef/6cd4f8c671517162379dc79cfae5aea9106bc38abb89628d5c16adf6a838/cffi-2.1.0-cp315-cp315-win_arm64.whl", hash = "sha256:8d35c139744adb3e727cd51b1a18324bbe44b8bd41bf8322bca4d41289f48eda", size = 183004 }, + { url = "https://files.pythonhosted.org/packages/11/b6/12fc55092817a5faa26fb8c40c7f9d662e11a46ee248c137aafc42517d92/cffi-2.1.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:f9912624a0c0b834b7520d7769b3644453aabc0a7e1c839da7359f050750e9bc", size = 188378 }, + { url = "https://files.pythonhosted.org/packages/8d/2e/cdac88979f295fde5daa69622c7d2111e56e7ceb94f211357fbe452339e4/cffi-2.1.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:df92f2aba50eb4d96718b68ef76f2e57a57b54f2fa62333496d16c6d585a85ca", size = 188319 }, + { url = "https://files.pythonhosted.org/packages/e0/27/1d0b408497e41a74795af122d7b603c418c5fed0171450f899afd04e594f/cffi-2.1.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0520e1f4c35f44e209cbbb421b67eec42e6a157f59444dfb6058874ff3610e5d", size = 223904 }, + { url = "https://files.pythonhosted.org/packages/8b/31/e115c985105dd7ffb32444505f18ceb874bb42d992af05d5dced7ecf1980/cffi-2.1.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3681e031db29958a7502f5c0c9d6bbc4c36cb20f7b104086fa642d1799631ff8", size = 211554 }, + { url = "https://files.pythonhosted.org/packages/5a/67/9e6e09409336d9e515c58367e7cfcf4f89df06ad25252675595a58eb59d5/cffi-2.1.0-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:762f99479dcb369f60ab9017ad4ab97a36a1dd7c1ee5a3b15db0f4b8659120cd", size = 210795 }, + { url = "https://files.pythonhosted.org/packages/19/e5/d3cc82a4a0be7902af279c04181ad038449c096734464a5ae1de3e1401bd/cffi-2.1.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0611e7ebf90573a535ebdc33ae9da222d037853983e13359f580fab781ca017f", size = 223843 }, + { url = "https://files.pythonhosted.org/packages/b9/65/b434abc97ce7cecc2c640fde160507c0ecc7e21544b483ba3325d2e2ea17/cffi-2.1.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:86cf8755a791f72c85dc287128cc62d4f24d392e3f1e15837245623f4a33cccc", size = 226773 }, + { url = "https://files.pythonhosted.org/packages/b5/9f/d4dc66ca651eb1145a133314cda721abf13cfac3d28c4a0402263ae6ad75/cffi-2.1.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:ba00f661f8ba35d075c937174e27c2c421cec3942fd2e0ea3e66996757c0fdd9", size = 225719 }, + { url = "https://files.pythonhosted.org/packages/68/5a/e536c528bc8057496c360c0978559a2dc45653f89dd6151078aa7d8fca1a/cffi-2.1.0-cp315-cp315t-win32.whl", hash = "sha256:cb96698e3c7413d906ce83f8ffd245ec1bd94707541f299d0ce4d6b0193e982b", size = 182760 }, + { url = "https://files.pythonhosted.org/packages/d3/0b/0ffe8b82d3875bced5fa1e7986a7a46b748262a40ab7f60b475eb9fb1bb3/cffi-2.1.0-cp315-cp315t-win_amd64.whl", hash = "sha256:f146d154428a2523f9cc7936c02353c2459b8f6cf07d3cd1ee1c0a611109c5d5", size = 193769 }, + { url = "https://files.pythonhosted.org/packages/a0/17/1073b53b68c9b5ca6914adf5f8bf55aacc2d3be102418c90700160ea8605/cffi-2.1.0-cp315-cp315t-win_arm64.whl", hash = "sha256:cbb7640ce37159548d2147b5b8c241f962143d4c71231431820783f4dc78f210", size = 186405 }, ] [[package]] @@ -425,103 +498,113 @@ wheels = [ [[package]] name = "chardet" -version = "7.4.0.post1" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3e/38/fe380893cbba72febb24d5dc0c2f9ac99f437153c36a409a8e254ed77bb6/chardet-7.4.0.post1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2769be12361a6c7873392e435c708eca88c9f0fb6a647af75fa1386db64032d6", size = 851312 }, - { url = "https://files.pythonhosted.org/packages/5e/24/3c1522d777b66e2e3615ee33d1d4291c47b0ec258a9471b559339b01fac5/chardet-7.4.0.post1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8e1eaa942ae81d43d535092ff3ba660c967344178cc3876b54834a56c1207f3a", size = 837425 }, - { url = "https://files.pythonhosted.org/packages/3b/0d/be32abacdb6ed59b5e53b55da04102946b03eadac8a0bb107e359b22e257/chardet-7.4.0.post1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:941af534a9b77d4b912e173e98680225340cf8827537e465bd6498b3e98d0cb8", size = 857193 }, - { url = "https://files.pythonhosted.org/packages/e3/a2/dab58511fbeef06dd88866568ea1a11b2f15654223cafc2681e2da84b1f2/chardet-7.4.0.post1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ad98a6c2e61624b1120919353d222121b8f5848b9d33c885d949fe0235575682", size = 863486 }, - { url = "https://files.pythonhosted.org/packages/e1/3a/f392d9b8465575140f250a8571e6cc643b08c8b650d84d0b499b542a0f2f/chardet-7.4.0.post1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6cddf6f1a0834ab5a6894819a6f4c3cd8c2cc86a12fc4efdc87eadb9ce7186ab", size = 858813 }, - { url = "https://files.pythonhosted.org/packages/89/e0/7747b1bd30b8686088581382e1465463f40d27d25db94eccfd872f088ac7/chardet-7.4.0.post1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db07ed10259c0e93a55e3c285d2d78111b000e574aa4f94d89de53c72fb28127", size = 853961 }, - { url = "https://files.pythonhosted.org/packages/2e/24/de2ba4786ada1c10147612cb7ff469ac8b835a47e9e5a772ce15735a8f4a/chardet-7.4.0.post1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86d7ca798051fce39b980241f4e93a40e3ef1fb568e282afcdcdbf6efa56bada", size = 837780 }, - { url = "https://files.pythonhosted.org/packages/29/59/133001a6a7549dd34a3c28d1358122b0e68a59f27840efb2102b72eb73cf/chardet-7.4.0.post1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45adca296eddec38b803ce352bffcc5fff40576246e74fcd2aa358f9c813ffe0", size = 859436 }, - { url = "https://files.pythonhosted.org/packages/c3/00/2eec7b47263524f204b3225d023f7d75e9c06a0a75c06a4c85faf2aec246/chardet-7.4.0.post1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f03aef24d91168232db8b01e4d6726676708be503e6aa07e28ab808e6d0fe606", size = 868655 }, - { url = "https://files.pythonhosted.org/packages/00/a2/36e5b1a46a36293cac237fa5c61f9e11497e025ec2e4b10e8d187dede9b9/chardet-7.4.0.post1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5aec4e167de05470a3e2466a1ae751d7f0ad15510f63633fdd01a0405515df7a", size = 862406 }, - { url = "https://files.pythonhosted.org/packages/e9/32/83a15c6077e7f240834ffd9ed78ef12f20f6e1924d7d7986d33f3d2af905/chardet-7.4.0.post1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdb3785c8700b3d0b354553827a166480a439f9754f7366f795bbe8b42d6daf", size = 853792 }, - { url = "https://files.pythonhosted.org/packages/83/d3/80554c1cc15631446c9b90aec6fe63b7310aa0b82d3004f7ba38bd8a8270/chardet-7.4.0.post1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6285d35f79d0cdc8838d3cb01876f979c8419a74662e8de39444e40639e0b2b", size = 837634 }, - { url = "https://files.pythonhosted.org/packages/6b/4d/e9bbe23cec7394ed1190f5af688efd1b41dea8515371f0b1ee6ad4c09682/chardet-7.4.0.post1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c23262011179db48e600012e296133ab577d8e9682c91a19221164732ccb4427", size = 858692 }, - { url = "https://files.pythonhosted.org/packages/d1/3b/6103194ea934f1c3a4ea080905c8849f71e83de455c16cb625d25f49b779/chardet-7.4.0.post1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:329aa8766c4917d3acc1b1d0462f0b2e820e24e9f341d0f858aee85396ae3002", size = 867879 }, - { url = "https://files.pythonhosted.org/packages/72/06/317627e347072507e448e0515b736cdb650826c57c7217ce1361615d7a85/chardet-7.4.0.post1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0cbfa21c8526022a62d1ebd1799b6e2c3598779231c0397372b6e26a4b1f4d28", size = 862092 }, - { url = "https://files.pythonhosted.org/packages/6f/11/74e49a9fa914afe7e751263969894c61e2c49b7e71c0047bcaab64c8117f/chardet-7.4.0.post1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:70af3a05ca9476461160474f1324612d04b885101216894e60266c8c16510084", size = 853233 }, - { url = "https://files.pythonhosted.org/packages/7e/a3/a358e5fb4144da67fe9572b4506ce3a065dc24e9cc030d1b049c6b1e914e/chardet-7.4.0.post1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:79c96d41ee63b098205d071caeee272968fe7a81a65151af2c02d1f51d2e2b4f", size = 837902 }, - { url = "https://files.pythonhosted.org/packages/b6/8e/488e166cc97bf7544d246c628a1abd7af1e369c4068cedb02a5f0abe8a74/chardet-7.4.0.post1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2eb2b2ed1662e74bebcdcd8f3872fd23378605601065a94ca265040026a11b3", size = 860652 }, - { url = "https://files.pythonhosted.org/packages/19/0e/d3ed1a607f64c831ff2c35adc43d1a5db1cce4494585d494d3465e4b593b/chardet-7.4.0.post1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:069f4ff169142c45198b513702d7596b60944d3d8ebcfed2626e7716430e90e0", size = 868211 }, - { url = "https://files.pythonhosted.org/packages/8d/6a/9ad635e2b75e6b058c1104a2821a1ed444161279f112df2c0f914193dff1/chardet-7.4.0.post1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e801976901fb7bc48f13653a7ca78e81fa65fe615d0d33c9383385c73e571927", size = 862775 }, - { url = "https://files.pythonhosted.org/packages/91/d7/47988d40231b41376f5a66346ef3b322c81091dfd4c0f84df5a1e3bb06b5/chardet-7.4.0.post1-py3-none-any.whl", hash = "sha256:57a62ef50f69bc2fb3a3ea1ffffec6d10f3d2112d3b05d6e3cb15c2c9b55f6cc", size = 624666 }, +version = "7.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/b6/9df434a8eeba2e6628c465a1dfa31034228ef79b26f76f46278f4ef7e49d/chardet-7.4.3.tar.gz", hash = "sha256:cc1d4eb92a4ec1c2df3b490836ffa46922e599d34ce0bb75cf41fd2bf6303d56", size = 784800 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/52/505c207f334d51e937cbaa27ff95776e16e2d120e13cbe491cd7b3a70b50/chardet-7.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:25a862cddc6a9ac07023e808aedd297115345fbaabc2690479481ddc0f980e09", size = 870747 }, + { url = "https://files.pythonhosted.org/packages/14/4b/d3c79495dee4831b8bebca2790e72cb90f0c5849c940570a7c7e5b70b952/chardet-7.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7005c88da26fd95d8abb8acbe6281d833e9a9181b03cf49b4546c4555389bd97", size = 853210 }, + { url = "https://files.pythonhosted.org/packages/b9/99/f6a822ad1bde25a4c38dc3e770485e78e0893dfd871cd6e18ed3ea3a795e/chardet-7.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc50f28bad067393cce0af9091052c3b8df7a23115afd8ba7b2e0947f0cef1f8", size = 873625 }, + { url = "https://files.pythonhosted.org/packages/b1/10/31932775c94a86814f76b41c4a772b52abfb0e6125324f32c6da1196c297/chardet-7.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3da294de1a681097848ab58bd3f2771a674f8039d2d87a5538b28856b815e9", size = 883436 }, + { url = "https://files.pythonhosted.org/packages/6c/63/0f43e3acf2c436fdb32a0f904aeb03a2904d2126eed34a042a194d235926/chardet-7.4.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:93c45e116dd51b66226a53ade3f9f635e870de5399b90e00ce45dcc311093bf4", size = 876589 }, + { url = "https://files.pythonhosted.org/packages/5d/a6/e9b8f8a3e99602792b01fa7d0a731737615ab56d8bfd0b52935a0ef88b85/chardet-7.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:ccc1f83ab4bcfb901cf39e0c4ba6bc6e726fc6264735f10e24ceb5cb47387578", size = 941866 }, + { url = "https://files.pythonhosted.org/packages/61/33/29de185079e6675c3f375546e30a559b7ddc75ce972f18d6e566cd9ea4eb/chardet-7.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:75d3c65cc16bddf40b8da1fd25ba84fca5f8070f2b14e86083653c1c85aee971", size = 874870 }, + { url = "https://files.pythonhosted.org/packages/9c/2f/4c5af01fd1a7506a1d5375403d68925eac70289229492db5aa68b58103d8/chardet-7.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:29af5999f654e8729d251f1724a62b538b1262d9292cccaefddf8a02aae1ef6a", size = 854859 }, + { url = "https://files.pythonhosted.org/packages/36/21/edb36ad5dfa48d7f8eed97ab43931ecdaa8c15166c21b1d614967e49d681/chardet-7.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:626f00299ad62dfe937058a09572beed442ccc7b58f87aa667949b20fd3db235", size = 875032 }, + { url = "https://files.pythonhosted.org/packages/e5/59/a32a241d861cf180853a11c8e5a67641cb1b2af13c3a5ccce83ec07e2c9f/chardet-7.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9a4904dd5f071b7a7d7f50b4a67a86db3c902d243bf31708f1d5cde2f68239cb", size = 888283 }, + { url = "https://files.pythonhosted.org/packages/87/2e/e1ee6a77abf3782c00e05b89c4d4328c8353bf9500661c4348df1dd68614/chardet-7.4.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5d2879598bc220689e8ce509fe9c3f37ad2fca53a36be9c9bd91abdd91dd364f", size = 879974 }, + { url = "https://files.pythonhosted.org/packages/32/60/fca69c534602a7ced04280c952a246ad1edde2a6ca3a164f65d32ac41fe7/chardet-7.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:4b2799bd58e7245cfa8d4ab2e8ad1d76a5c3a5b1f32318eb6acca4c69a3e7101", size = 943973 }, + { url = "https://files.pythonhosted.org/packages/7c/43/79ac9b4db5bc87020c9dbc419125371d80882d1d197e9c4765ba8682b605/chardet-7.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a9e4486df251b8962e86ea9f139ca235aa6e0542a00f7844c9a04160afb99aa9", size = 873769 }, + { url = "https://files.pythonhosted.org/packages/55/5f/25bdec773905bff0ff6cf35ca73b17bd05593b4f87bd8c5fa43705f7167d/chardet-7.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4fbff1907925b0c5a1064cffb5e040cd5e338585c9c552625f30de6bc2f3107a", size = 853991 }, + { url = "https://files.pythonhosted.org/packages/b4/07/a29380ee0b215d23d77733b5ad60c5c0c7969650e080c667acdf9462040d/chardet-7.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:365135eaf37ba65a828f8e668eb0a8c38c479dcbec724dc25f4dfd781049c357", size = 874024 }, + { url = "https://files.pythonhosted.org/packages/a8/b1/3338e121cbd4c8a126b8ccb1061170c2ce51a53f678c502793ea49c6fd6d/chardet-7.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfc134b70c846c21ead8e43ada3ae1a805fff732f6922f8abcf2ff27b8f6493d", size = 887410 }, + { url = "https://files.pythonhosted.org/packages/63/1c/44a9a9e0c59c185a5d307ceaeee8768afa1558f0a24f7a4b5fa11b67586b/chardet-7.4.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9acd9988a93e09390f3cd231201ea7166c415eb8da1b735928990ffc05cb9fbb", size = 879269 }, + { url = "https://files.pythonhosted.org/packages/1b/b3/5d0e77ea774bd3224321c248880ea0c0379000ac5c2bb6d77609549de247/chardet-7.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:e1b98790c284ff813f18f7cf7de5f05ea2435a080030c7f1a8318f3a4f80b131", size = 944155 }, + { url = "https://files.pythonhosted.org/packages/70/a8/bf0811d859e13801279a2ae64f37a408027b282f2047bc0001c75dd356ad/chardet-7.4.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d892d3dcd652fdef53e3d6327d39b17c0df40a899dfc919abaeb64c974497531", size = 872887 }, + { url = "https://files.pythonhosted.org/packages/51/ac/b9d68ebddfe1b02c77af5bf81120e12b036b4432dc6af7a303d90e2bc38b/chardet-7.4.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:acc46d1b8b7d5783216afe15db56d1c179b9a40e5a1558bc13164c4fd20674c4", size = 853964 }, + { url = "https://files.pythonhosted.org/packages/2a/81/17fa103ea9caf5d325a5e4051ab2ba65996fd66baa60b81ee41af1f54e10/chardet-7.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ac3bf11c645734a1701a3804e43eabd98851838192267d08c353a834ab79fea", size = 876006 }, + { url = "https://files.pythonhosted.org/packages/c2/20/193faab46a68ea550587331a698c3dca8099f8901d10937c4443135c7ed9/chardet-7.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e3bd9f936e04bae89c254262af08d9e5b98f805175ba1e29d454e6cba3107b7", size = 887680 }, + { url = "https://files.pythonhosted.org/packages/40/c6/94a3c673327392652ee8bdea9a45bc8a5f5365197a7387d68f0eed007115/chardet-7.4.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:27cc23da03630cdecc9aa81a895aa86629c211f995cd57651f0fbc280717bf93", size = 879865 }, + { url = "https://files.pythonhosted.org/packages/b1/2c/cad8b5e3623a987f3c930b68e2bdd06cfc388cd91cd42ed05f1227701b73/chardet-7.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:b95c934b9ad59e2ba8abb9be49df70d3ad1b0d95d864b9fdb7588d4fa8bd921c", size = 939594 }, + { url = "https://files.pythonhosted.org/packages/33/e0/d06e42fd6f02a58e5e227e5106587751cb38adcff0aaf949add744b78b6e/chardet-7.4.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c77867f0c1cb8bd819502249fcdc500364aedb07881e11b743726fa2148e7b6e", size = 889714 }, + { url = "https://files.pythonhosted.org/packages/d4/ed/40d091954d48abea037baae6be8fb79905e5f78d34d12ea955132c7d8011/chardet-7.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cf1efeaf65a6ef2f5b9cc3a1df6f08ba2831b369ccaa4c7018eaf90aa757bb11", size = 872319 }, + { url = "https://files.pythonhosted.org/packages/bb/77/82a46821dbfbdfe062710d2bf2ede13426304e3567a23c57d919c0c31630/chardet-7.4.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f3504c139a2ad544077dd2d9e412cd08b01786843d76997cd43bb6de311723c", size = 892021 }, + { url = "https://files.pythonhosted.org/packages/49/57/42d30c562bda5b4a839766c1aad8d5856b798ad2a1c3247b72a679afec94/chardet-7.4.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457f619882ba66327d4d8d14c6c342269bdb1e4e1c38e8117df941d14d351b04", size = 902509 }, + { url = "https://files.pythonhosted.org/packages/8c/6c/0a40afdb50a0fe041ab95553b835a8160b6cf0e81edf2ae2fe9f5224cbf9/chardet-7.4.3-py3-none-any.whl", hash = "sha256:1173b74051570cf08099d7429d92e4882d375ad4217f92a6e5240ccfb26f231e", size = 626562 }, ] [[package]] name = "charset-normalizer" -version = "3.4.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1d/35/02daf95b9cd686320bb622eb148792655c9412dbb9b67abb5694e5910a24/charset_normalizer-3.4.5.tar.gz", hash = "sha256:95adae7b6c42a6c5b5b559b1a99149f090a57128155daeea91732c8d970d8644", size = 134804 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/9e/bcec3b22c64ecec47d39bf5167c2613efd41898c019dccd4183f6aa5d6a7/charset_normalizer-3.4.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:610f72c0ee565dfb8ae1241b666119582fdbfe7c0975c175be719f940e110694", size = 279531 }, - { url = "https://files.pythonhosted.org/packages/58/12/81fd25f7e7078ab5d1eedbb0fac44be4904ae3370a3bf4533c8f2d159acd/charset_normalizer-3.4.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60d68e820af339df4ae8358c7a2e7596badeb61e544438e489035f9fbf3246a5", size = 188006 }, - { url = "https://files.pythonhosted.org/packages/ae/6e/f2d30e8c27c1b0736a6520311982cf5286cfc7f6cac77d7bc1325e3a23f2/charset_normalizer-3.4.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b473fc8dca1c3ad8559985794815f06ca3fc71942c969129070f2c3cdf7281", size = 205085 }, - { url = "https://files.pythonhosted.org/packages/d0/90/d12cefcb53b5931e2cf792a33718d7126efb116a320eaa0742c7059a95e4/charset_normalizer-3.4.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d4eb8ac7469b2a5d64b5b8c04f84d8bf3ad340f4514b98523805cbf46e3b3923", size = 200545 }, - { url = "https://files.pythonhosted.org/packages/03/f4/44d3b830a20e89ff82a3134912d9a1cf6084d64f3b95dcad40f74449a654/charset_normalizer-3.4.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bcb3227c3d9aaf73eaaab1db7ccd80a8995c509ee9941e2aae060ca6e4e5d81", size = 193863 }, - { url = "https://files.pythonhosted.org/packages/25/4b/f212119c18a6320a9d4a730d1b4057875cdeabf21b3614f76549042ef8a8/charset_normalizer-3.4.5-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:75ee9c1cce2911581a70a3c0919d8bccf5b1cbc9b0e5171400ec736b4b569497", size = 181827 }, - { url = "https://files.pythonhosted.org/packages/74/00/b26158e48b425a202a92965f8069e8a63d9af1481dfa206825d7f74d2a3c/charset_normalizer-3.4.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d1401945cb77787dbd3af2446ff2d75912327c4c3a1526ab7955ecf8600687c", size = 191085 }, - { url = "https://files.pythonhosted.org/packages/c4/c2/1c1737bf6fd40335fe53d28fe49afd99ee4143cc57a845e99635ce0b9b6d/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a45e504f5e1be0bd385935a8e1507c442349ca36f511a47057a71c9d1d6ea9e", size = 190688 }, - { url = "https://files.pythonhosted.org/packages/5a/3d/abb5c22dc2ef493cd56522f811246a63c5427c08f3e3e50ab663de27fcf4/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e09f671a54ce70b79a1fc1dc6da3072b7ef7251fadb894ed92d9aa8218465a5f", size = 183077 }, - { url = "https://files.pythonhosted.org/packages/44/33/5298ad4d419a58e25b3508e87f2758d1442ff00c2471f8e0403dab8edad5/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d01de5e768328646e6a3fa9e562706f8f6641708c115c62588aef2b941a4f88e", size = 206706 }, - { url = "https://files.pythonhosted.org/packages/7b/17/51e7895ac0f87c3b91d276a449ef09f5532a7529818f59646d7a55089432/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:131716d6786ad5e3dc542f5cc6f397ba3339dc0fb87f87ac30e550e8987756af", size = 191665 }, - { url = "https://files.pythonhosted.org/packages/90/8f/cce9adf1883e98906dbae380d769b4852bb0fa0004bc7d7a2243418d3ea8/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a374cc0b88aa710e8865dc1bd6edb3743c59f27830f0293ab101e4cf3ce9f85", size = 201950 }, - { url = "https://files.pythonhosted.org/packages/08/ca/bce99cd5c397a52919e2769d126723f27a4c037130374c051c00470bcd38/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d31f0d1671e1534e395f9eb84a68e0fb670e1edb1fe819a9d7f564ae3bc4e53f", size = 195830 }, - { url = "https://files.pythonhosted.org/packages/87/4f/2e3d023a06911f1281f97b8f036edc9872167036ca6f55cc874a0be6c12c/charset_normalizer-3.4.5-cp311-cp311-win32.whl", hash = "sha256:cace89841c0599d736d3d74a27bc5821288bb47c5441923277afc6059d7fbcb4", size = 132029 }, - { url = "https://files.pythonhosted.org/packages/fe/1f/a853b73d386521fd44b7f67ded6b17b7b2367067d9106a5c4b44f9a34274/charset_normalizer-3.4.5-cp311-cp311-win_amd64.whl", hash = "sha256:f8102ae93c0bc863b1d41ea0f4499c20a83229f52ed870850892df555187154a", size = 142404 }, - { url = "https://files.pythonhosted.org/packages/b4/10/dba36f76b71c38e9d391abe0fd8a5b818790e053c431adecfc98c35cd2a9/charset_normalizer-3.4.5-cp311-cp311-win_arm64.whl", hash = "sha256:ed98364e1c262cf5f9363c3eca8c2df37024f52a8fa1180a3610014f26eac51c", size = 132796 }, - { url = "https://files.pythonhosted.org/packages/9c/b6/9ee9c1a608916ca5feae81a344dffbaa53b26b90be58cc2159e3332d44ec/charset_normalizer-3.4.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ed97c282ee4f994ef814042423a529df9497e3c666dca19be1d4cd1129dc7ade", size = 280976 }, - { url = "https://files.pythonhosted.org/packages/f8/d8/a54f7c0b96f1df3563e9190f04daf981e365a9b397eedfdfb5dbef7e5c6c/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0294916d6ccf2d069727d65973c3a1ca477d68708db25fd758dd28b0827cff54", size = 189356 }, - { url = "https://files.pythonhosted.org/packages/42/69/2bf7f76ce1446759a5787cb87d38f6a61eb47dbbdf035cfebf6347292a65/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dc57a0baa3eeedd99fafaef7511b5a6ef4581494e8168ee086031744e2679467", size = 206369 }, - { url = "https://files.pythonhosted.org/packages/10/9c/949d1a46dab56b959d9a87272482195f1840b515a3380e39986989a893ae/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ed1a9a204f317ef879b32f9af507d47e49cd5e7f8e8d5d96358c98373314fc60", size = 203285 }, - { url = "https://files.pythonhosted.org/packages/67/5c/ae30362a88b4da237d71ea214a8c7eb915db3eec941adda511729ac25fa2/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ad83b8f9379176c841f8865884f3514d905bcd2a9a3b210eaa446e7d2223e4d", size = 196274 }, - { url = "https://files.pythonhosted.org/packages/b2/07/c9f2cb0e46cb6d64fdcc4f95953747b843bb2181bda678dc4e699b8f0f9a/charset_normalizer-3.4.5-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:a118e2e0b5ae6b0120d5efa5f866e58f2bb826067a646431da4d6a2bdae7950e", size = 184715 }, - { url = "https://files.pythonhosted.org/packages/36/64/6b0ca95c44fddf692cd06d642b28f63009d0ce325fad6e9b2b4d0ef86a52/charset_normalizer-3.4.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:754f96058e61a5e22e91483f823e07df16416ce76afa4ebf306f8e1d1296d43f", size = 193426 }, - { url = "https://files.pythonhosted.org/packages/50/bc/a730690d726403743795ca3f5bb2baf67838c5fea78236098f324b965e40/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0c300cefd9b0970381a46394902cd18eaf2aa00163f999590ace991989dcd0fc", size = 191780 }, - { url = "https://files.pythonhosted.org/packages/97/4f/6c0bc9af68222b22951552d73df4532b5be6447cee32d58e7e8c74ecbb7b/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c108f8619e504140569ee7de3f97d234f0fbae338a7f9f360455071ef9855a95", size = 185805 }, - { url = "https://files.pythonhosted.org/packages/dd/b9/a523fb9b0ee90814b503452b2600e4cbc118cd68714d57041564886e7325/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d1028de43596a315e2720a9849ee79007ab742c06ad8b45a50db8cdb7ed4a82a", size = 208342 }, - { url = "https://files.pythonhosted.org/packages/4d/61/c59e761dee4464050713e50e27b58266cc8e209e518c0b378c1580c959ba/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:19092dde50335accf365cce21998a1c6dd8eafd42c7b226eb54b2747cdce2fac", size = 193661 }, - { url = "https://files.pythonhosted.org/packages/1c/43/729fa30aad69783f755c5ad8649da17ee095311ca42024742701e202dc59/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4354e401eb6dab9aed3c7b4030514328a6c748d05e1c3e19175008ca7de84fb1", size = 204819 }, - { url = "https://files.pythonhosted.org/packages/87/33/d9b442ce5a91b96fc0840455a9e49a611bbadae6122778d0a6a79683dd31/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a68766a3c58fde7f9aaa22b3786276f62ab2f594efb02d0a1421b6282e852e98", size = 198080 }, - { url = "https://files.pythonhosted.org/packages/56/5a/b8b5a23134978ee9885cee2d6995f4c27cc41f9baded0a9685eabc5338f0/charset_normalizer-3.4.5-cp312-cp312-win32.whl", hash = "sha256:1827734a5b308b65ac54e86a618de66f935a4f63a8a462ff1e19a6788d6c2262", size = 132630 }, - { url = "https://files.pythonhosted.org/packages/70/53/e44a4c07e8904500aec95865dc3f6464dc3586a039ef0df606eb3ac38e35/charset_normalizer-3.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:728c6a963dfab66ef865f49286e45239384249672cd598576765acc2a640a636", size = 142856 }, - { url = "https://files.pythonhosted.org/packages/ea/aa/c5628f7cad591b1cf45790b7a61483c3e36cf41349c98af7813c483fd6e8/charset_normalizer-3.4.5-cp312-cp312-win_arm64.whl", hash = "sha256:75dfd1afe0b1647449e852f4fb428195a7ed0588947218f7ba929f6538487f02", size = 132982 }, - { url = "https://files.pythonhosted.org/packages/f5/48/9f34ec4bb24aa3fdba1890c1bddb97c8a4be1bd84ef5c42ac2352563ad05/charset_normalizer-3.4.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac59c15e3f1465f722607800c68713f9fbc2f672b9eb649fe831da4019ae9b23", size = 280788 }, - { url = "https://files.pythonhosted.org/packages/0e/09/6003e7ffeb90cc0560da893e3208396a44c210c5ee42efff539639def59b/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:165c7b21d19365464e8f70e5ce5e12524c58b48c78c1f5a57524603c1ab003f8", size = 188890 }, - { url = "https://files.pythonhosted.org/packages/42/1e/02706edf19e390680daa694d17e2b8eab4b5f7ac285e2a51168b4b22ee6b/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:28269983f25a4da0425743d0d257a2d6921ea7d9b83599d4039486ec5b9f911d", size = 206136 }, - { url = "https://files.pythonhosted.org/packages/c7/87/942c3def1b37baf3cf786bad01249190f3ca3d5e63a84f831e704977de1f/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d27ce22ec453564770d29d03a9506d449efbb9fa13c00842262b2f6801c48cce", size = 202551 }, - { url = "https://files.pythonhosted.org/packages/94/0a/af49691938dfe175d71b8a929bd7e4ace2809c0c5134e28bc535660d5262/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0625665e4ebdddb553ab185de5db7054393af8879fb0c87bd5690d14379d6819", size = 195572 }, - { url = "https://files.pythonhosted.org/packages/20/ea/dfb1792a8050a8e694cfbde1570ff97ff74e48afd874152d38163d1df9ae/charset_normalizer-3.4.5-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:c23eb3263356d94858655b3e63f85ac5d50970c6e8febcdde7830209139cc37d", size = 184438 }, - { url = "https://files.pythonhosted.org/packages/72/12/c281e2067466e3ddd0595bfaea58a6946765ace5c72dfa3edc2f5f118026/charset_normalizer-3.4.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e6302ca4ae283deb0af68d2fbf467474b8b6aedcd3dab4db187e07f94c109763", size = 193035 }, - { url = "https://files.pythonhosted.org/packages/ba/4f/3792c056e7708e10464bad0438a44708886fb8f92e3c3d29ec5e2d964d42/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e51ae7d81c825761d941962450f50d041db028b7278e7b08930b4541b3e45cb9", size = 191340 }, - { url = "https://files.pythonhosted.org/packages/e7/86/80ddba897127b5c7a9bccc481b0cd36c8fefa485d113262f0fe4332f0bf4/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:597d10dec876923e5c59e48dbd366e852eacb2b806029491d307daea6b917d7c", size = 185464 }, - { url = "https://files.pythonhosted.org/packages/4d/00/b5eff85ba198faacab83e0e4b6f0648155f072278e3b392a82478f8b988b/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5cffde4032a197bd3b42fd0b9509ec60fb70918d6970e4cc773f20fc9180ca67", size = 208014 }, - { url = "https://files.pythonhosted.org/packages/c8/11/d36f70be01597fd30850dde8a1269ebc8efadd23ba5785808454f2389bde/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2da4eedcb6338e2321e831a0165759c0c620e37f8cd044a263ff67493be8ffb3", size = 193297 }, - { url = "https://files.pythonhosted.org/packages/1a/1d/259eb0a53d4910536c7c2abb9cb25f4153548efb42800c6a9456764649c0/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:65a126fb4b070d05340a84fc709dd9e7c75d9b063b610ece8a60197a291d0adf", size = 204321 }, - { url = "https://files.pythonhosted.org/packages/84/31/faa6c5b9d3688715e1ed1bb9d124c384fe2fc1633a409e503ffe1c6398c1/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7a80a9242963416bd81f99349d5f3fce1843c303bd404f204918b6d75a75fd6", size = 197509 }, - { url = "https://files.pythonhosted.org/packages/fd/a5/c7d9dd1503ffc08950b3260f5d39ec2366dd08254f0900ecbcf3a6197c7c/charset_normalizer-3.4.5-cp313-cp313-win32.whl", hash = "sha256:f1d725b754e967e648046f00c4facc42d414840f5ccc670c5670f59f83693e4f", size = 132284 }, - { url = "https://files.pythonhosted.org/packages/b9/0f/57072b253af40c8aa6636e6de7d75985624c1eb392815b2f934199340a89/charset_normalizer-3.4.5-cp313-cp313-win_amd64.whl", hash = "sha256:e37bd100d2c5d3ba35db9c7c5ba5a9228cbcffe5c4778dc824b164e5257813d7", size = 142630 }, - { url = "https://files.pythonhosted.org/packages/31/41/1c4b7cc9f13bd9d369ce3bc993e13d374ce25fa38a2663644283ecf422c1/charset_normalizer-3.4.5-cp313-cp313-win_arm64.whl", hash = "sha256:93b3b2cc5cf1b8743660ce77a4f45f3f6d1172068207c1defc779a36eea6bb36", size = 133254 }, - { url = "https://files.pythonhosted.org/packages/43/be/0f0fd9bb4a7fa4fb5067fb7d9ac693d4e928d306f80a0d02bde43a7c4aee/charset_normalizer-3.4.5-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8197abe5ca1ffb7d91e78360f915eef5addff270f8a71c1fc5be24a56f3e4873", size = 280232 }, - { url = "https://files.pythonhosted.org/packages/28/02/983b5445e4bef49cd8c9da73a8e029f0825f39b74a06d201bfaa2e55142a/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2aecdb364b8a1802afdc7f9327d55dad5366bc97d8502d0f5854e50712dbc5f", size = 189688 }, - { url = "https://files.pythonhosted.org/packages/d0/88/152745c5166437687028027dc080e2daed6fe11cfa95a22f4602591c42db/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a66aa5022bf81ab4b1bebfb009db4fd68e0c6d4307a1ce5ef6a26e5878dfc9e4", size = 206833 }, - { url = "https://files.pythonhosted.org/packages/cb/0f/ebc15c8b02af2f19be9678d6eed115feeeccc45ce1f4b098d986c13e8769/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d77f97e515688bd615c1d1f795d540f32542d514242067adcb8ef532504cb9ee", size = 202879 }, - { url = "https://files.pythonhosted.org/packages/38/9c/71336bff6934418dc8d1e8a1644176ac9088068bc571da612767619c97b3/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01a1ed54b953303ca7e310fafe0fe347aab348bd81834a0bcd602eb538f89d66", size = 195764 }, - { url = "https://files.pythonhosted.org/packages/b7/95/ce92fde4f98615661871bc282a856cf9b8a15f686ba0af012984660d480b/charset_normalizer-3.4.5-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:b2d37d78297b39a9eb9eb92c0f6df98c706467282055419df141389b23f93362", size = 183728 }, - { url = "https://files.pythonhosted.org/packages/1c/e7/f5b4588d94e747ce45ae680f0f242bc2d98dbd4eccfab73e6160b6893893/charset_normalizer-3.4.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e71bbb595973622b817c042bd943c3f3667e9c9983ce3d205f973f486fec98a7", size = 192937 }, - { url = "https://files.pythonhosted.org/packages/f9/29/9d94ed6b929bf9f48bf6ede6e7474576499f07c4c5e878fb186083622716/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4cd966c2559f501c6fd69294d082c2934c8dd4719deb32c22961a5ac6db0df1d", size = 192040 }, - { url = "https://files.pythonhosted.org/packages/15/d2/1a093a1cf827957f9445f2fe7298bcc16f8fc5e05c1ed2ad1af0b239035e/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d5e52d127045d6ae01a1e821acfad2f3a1866c54d0e837828538fabe8d9d1bd6", size = 184107 }, - { url = "https://files.pythonhosted.org/packages/0f/7d/82068ce16bd36135df7b97f6333c5d808b94e01d4599a682e2337ed5fd14/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:30a2b1a48478c3428d047ed9690d57c23038dac838a87ad624c85c0a78ebeb39", size = 208310 }, - { url = "https://files.pythonhosted.org/packages/84/4e/4dfb52307bb6af4a5c9e73e482d171b81d36f522b21ccd28a49656baa680/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d8ed79b8f6372ca4254955005830fd61c1ccdd8c0fac6603e2c145c61dd95db6", size = 192918 }, - { url = "https://files.pythonhosted.org/packages/08/a4/159ff7da662cf7201502ca89980b8f06acf3e887b278956646a8aeb178ab/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:c5af897b45fa606b12464ccbe0014bbf8c09191e0a66aab6aa9d5cf6e77e0c94", size = 204615 }, - { url = "https://files.pythonhosted.org/packages/d6/62/0dd6172203cb6b429ffffc9935001fde42e5250d57f07b0c28c6046deb6b/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1088345bcc93c58d8d8f3d783eca4a6e7a7752bbff26c3eee7e73c597c191c2e", size = 197784 }, - { url = "https://files.pythonhosted.org/packages/c7/5e/1aab5cb737039b9c59e63627dc8bbc0d02562a14f831cc450e5f91d84ce1/charset_normalizer-3.4.5-cp314-cp314-win32.whl", hash = "sha256:ee57b926940ba00bca7ba7041e665cc956e55ef482f851b9b65acb20d867e7a2", size = 133009 }, - { url = "https://files.pythonhosted.org/packages/40/65/e7c6c77d7aaa4c0d7974f2e403e17f0ed2cb0fc135f77d686b916bf1eead/charset_normalizer-3.4.5-cp314-cp314-win_amd64.whl", hash = "sha256:4481e6da1830c8a1cc0b746b47f603b653dadb690bcd851d039ffaefe70533aa", size = 143511 }, - { url = "https://files.pythonhosted.org/packages/ba/91/52b0841c71f152f563b8e072896c14e3d83b195c188b338d3cc2e582d1d4/charset_normalizer-3.4.5-cp314-cp314-win_arm64.whl", hash = "sha256:97ab7787092eb9b50fb47fa04f24c75b768a606af1bcba1957f07f128a7219e4", size = 133775 }, - { url = "https://files.pythonhosted.org/packages/c5/60/3a621758945513adfd4db86827a5bafcc615f913dbd0b4c2ed64a65731be/charset_normalizer-3.4.5-py3-none-any.whl", hash = "sha256:9db5e3fcdcee89a78c04dffb3fe33c79f77bd741a624946db2591c81b2fc85b0", size = 55455 }, +version = "3.4.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", size = 152439 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/e3/85ec501f206fb049259288c1f3506e53876937fb00edb47009348e66756b/charset_normalizer-3.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5", size = 317075 }, + { url = "https://files.pythonhosted.org/packages/c3/69/2a5385192e67175f7d8bd5ce4f57c24bc956439adeae5c13a99aa28a53d1/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2", size = 213837 }, + { url = "https://files.pythonhosted.org/packages/b3/46/03ddc7da576d814fe0a36dd1f0fd3258e95404b4b2e3c026b7923d7e133f/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a", size = 235503 }, + { url = "https://files.pythonhosted.org/packages/4e/6e/de0229a7ef40f6f9d28a837eebf4ec47bdca5dab4e900c84f22919af636a/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29", size = 229944 }, + { url = "https://files.pythonhosted.org/packages/a5/34/49b9060e8418b14fb5cba9cf6bfb383111e2538a03a1fb18e66a95aeb3d5/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c", size = 221276 }, + { url = "https://files.pythonhosted.org/packages/44/95/80282cce0fae9c3061203d723ee87da996aed79679e65d8935050ee7ca1f/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b", size = 205260 }, + { url = "https://files.pythonhosted.org/packages/0c/74/2f62c8821b969ea3bd67cc2e6976834f48ca5d12664d2559ebcd9bcfbed7/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db", size = 217786 }, + { url = "https://files.pythonhosted.org/packages/d9/8d/feabb82cb49fcad14515b1d7d1ca4787b0da7fc723a212bf89bc9e0fac52/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993", size = 216798 }, + { url = "https://files.pythonhosted.org/packages/a5/ff/c946d63bc3786d5b84d960b0f7ab7e25b828486a946b5aa997625bcaf6a6/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da", size = 206429 }, + { url = "https://files.pythonhosted.org/packages/af/ba/5e5007c370702f85d2ef75791fac7943ed41e080364a673b20142e430e3e/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3", size = 223066 }, + { url = "https://files.pythonhosted.org/packages/83/d5/9096aa3cf532dfad237861544eb47a0f20d5adbf1039760fed8eaae935d9/charset_normalizer-3.4.9-cp311-cp311-win32.whl", hash = "sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d", size = 150456 }, + { url = "https://files.pythonhosted.org/packages/ed/a1/e29995109e455dc8eff8d0fac6ae509be39561318a7cfeac5d33ad029213/charset_normalizer-3.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1", size = 161410 }, + { url = "https://files.pythonhosted.org/packages/4f/8d/1569f4d0032d6ba2a4fe4591c35bf87868c600c41a71eb5c2e1ffa8464c2/charset_normalizer-3.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec", size = 152649 }, + { url = "https://files.pythonhosted.org/packages/70/4a/ecbd131485c07fcdfad54e28946d513e3da22ef3b4bd854dcafae54ec739/charset_normalizer-3.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0", size = 319300 }, + { url = "https://files.pythonhosted.org/packages/ec/96/5d9364e3342d69f3a045e1777bc47c85c383e6e9466d561b33fdb419d1f9/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9", size = 215802 }, + { url = "https://files.pythonhosted.org/packages/4b/4c/5361f9aa7f2cb58d94f2ab831b3d493f69efb1d239654b4744e3c09527cb/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44", size = 237171 }, + { url = "https://files.pythonhosted.org/packages/50/78/ce342ca4ff30b2eb49fe6d9578df85974f90c67d294113e94efdd9664cbd/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9", size = 233075 }, + { url = "https://files.pythonhosted.org/packages/01/c4/4fa4c8b3097a11f3c5f09a35b72ed6855fb1d332469504962ab7bafcc702/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd", size = 224256 }, + { url = "https://files.pythonhosted.org/packages/87/3a/ad914516df7e358a81aae018caa5e0470ba827fa6d763b1d2e87d920a5f6/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84", size = 208784 }, + { url = "https://files.pythonhosted.org/packages/d7/74/3c12f9755717dfe5c5c87da63f35d765fa0c00382ec26bf23f7fae34f2ba/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b", size = 219928 }, + { url = "https://files.pythonhosted.org/packages/33/9a/895095b83e7907abd6d3d99aad3a38ad0d9686cc186cb0c94c24320fe63e/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde", size = 218489 }, + { url = "https://files.pythonhosted.org/packages/a1/34/ef5c05f412f42520d7709b7d3784d19640839eb7366ded1755511585429f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39", size = 210267 }, + { url = "https://files.pythonhosted.org/packages/83/dc/9b29fa4412b318bf3bfea985c35d67eb55e04b59a7c3f2237168b0e0be6f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62", size = 226030 }, + { url = "https://files.pythonhosted.org/packages/0e/42/6dbc00b8cd16011691203e33570fa42ed5746599a2e878112d16eab403a3/charset_normalizer-3.4.9-cp312-cp312-win32.whl", hash = "sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642", size = 151185 }, + { url = "https://files.pythonhosted.org/packages/80/cc/f920afd1a23c58ccd53c1d36085a71893a4737ff5e66e0371efab6809850/charset_normalizer-3.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0", size = 162557 }, + { url = "https://files.pythonhosted.org/packages/f0/e6/0386d43a261ff4e4b30c5857af7df877254b46bec7b9d1b74b6bf969a90b/charset_normalizer-3.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2", size = 152665 }, + { url = "https://files.pythonhosted.org/packages/b2/06/97ec2aeae780b31d742b6352218b43841a6871e2564578ca522dce4a45c3/charset_normalizer-3.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614", size = 317688 }, + { url = "https://files.pythonhosted.org/packages/d0/39/8ff066c672434225f8d25f8b739f992af250944392173dcc88362681c9bf/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698", size = 214982 }, + { url = "https://files.pythonhosted.org/packages/92/8f/3a47a3667c83c2df9483d91644c6c107de3bf8874aa1793da9d3012eb986/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b", size = 236460 }, + { url = "https://files.pythonhosted.org/packages/f1/60/b22cdbee7e4013dab8b0d7647fc6181120fbbbc8f7025c226d15bd5a47fc/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9", size = 232003 }, + { url = "https://files.pythonhosted.org/packages/ea/f8/72eb13dcabe7257035cea8aefd922caad2f110d252bf9f67c4c2ca763aee/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33", size = 223149 }, + { url = "https://files.pythonhosted.org/packages/b0/3e/faee8f9de92b14ee1198e9163252bb15efee7301b31256a3b6d9ebfdd0dd/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63", size = 207901 }, + { url = "https://files.pythonhosted.org/packages/3a/25/45f30093ae27dd7b92a793b61882a38685f993700113ca36e0c9c14965e1/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0", size = 219176 }, + { url = "https://files.pythonhosted.org/packages/48/18/c8f397329c35e32f6a837e488986f4ae03bd2abebc453b48714991630c2f/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe", size = 217356 }, + { url = "https://files.pythonhosted.org/packages/86/7e/5ce0bba863470fd1902d5e5843968951bddf38abe4742fc97116ef4598b3/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35", size = 209614 }, + { url = "https://files.pythonhosted.org/packages/6c/ef/2473d3c4d869155be4af1191111d59c4d5c4e0173026f7e85b176e23bf65/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8", size = 224991 }, + { url = "https://files.pythonhosted.org/packages/d0/a3/53ddae3db108a088156aa8ddfafd411ebbc1340f48c5573f697b27f69a39/charset_normalizer-3.4.9-cp313-cp313-win32.whl", hash = "sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9", size = 150622 }, + { url = "https://files.pythonhosted.org/packages/e8/ef/6953a77c7cf2c2ff9998e6f575ab3e380119f100223381565a4f94c1f836/charset_normalizer-3.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115", size = 161947 }, + { url = "https://files.pythonhosted.org/packages/6e/fb/d560d1d1555debbfe7849d9cac6145c1b537709d79576bf22557ed803b82/charset_normalizer-3.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012", size = 152594 }, + { url = "https://files.pythonhosted.org/packages/7e/8d/496817fa0944239ecae662dd57ea765cfeaec6a735f9f025d4b7b72e7143/charset_normalizer-3.4.9-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380", size = 317253 }, + { url = "https://files.pythonhosted.org/packages/2b/f9/ef4a69ea338ad3c0deceea0f5f7d2380ae8b52132b06d652cb0d2cd86706/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9", size = 215898 }, + { url = "https://files.pythonhosted.org/packages/8c/e7/5ddfd76fc061eb52de219658a4aa431cbacadf0a0219c8854f00da50d289/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4", size = 236718 }, + { url = "https://files.pythonhosted.org/packages/49/ba/768fa3f36048d81c477a0ce61f813bc1454d80917ccfe550abd9f44f5e24/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a", size = 232519 }, + { url = "https://files.pythonhosted.org/packages/f4/c4/b3e049d2aa3766180c78507110543d9d50894cc97f57de543f1be521dcdc/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046", size = 223143 }, + { url = "https://files.pythonhosted.org/packages/19/79/55c32d06d76ae4feafe053f061f3e3ab70bcf19f4007797ce8c3efda7830/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81", size = 206742 }, + { url = "https://files.pythonhosted.org/packages/10/e0/47c079dd82d217c807479cd59ffd30af56307ea31c108b75758970459ad3/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917", size = 219191 }, + { url = "https://files.pythonhosted.org/packages/42/ab/b9bc2e77d6b44a7e46ef62ec5cac1c9a6ba7b9135a5d560f002696ec9995/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41", size = 218328 }, + { url = "https://files.pythonhosted.org/packages/f1/78/c9c71d599f5aa2d42bcdd35cbbd46d7f535351a57e40ff7d8e5a7e219401/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1", size = 207406 }, + { url = "https://files.pythonhosted.org/packages/f6/39/c914445c321a845097ce4f6ac7de9a18228a77b766272125a1ce00d851eb/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf", size = 225157 }, + { url = "https://files.pythonhosted.org/packages/9b/f2/c0d4b8508565a36bc5c624e88ed297f5b0b1095011034d7f5b83a69908b5/charset_normalizer-3.4.9-cp314-cp314-win32.whl", hash = "sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48", size = 151095 }, + { url = "https://files.pythonhosted.org/packages/49/fd/a1d26144398c67486422a72bf5812cda22cb4ccfcd95a290fb41ceb4b8e2/charset_normalizer-3.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b", size = 162796 }, + { url = "https://files.pythonhosted.org/packages/20/95/d75e82f8ce9fd323ebf059c16c9aadefb22a1ecde13b7840b35835e4886c/charset_normalizer-3.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519", size = 153334 }, + { url = "https://files.pythonhosted.org/packages/00/5e/17398df3a139985ba9d11ed072531986f408c8fca952835ef1ab1820c02b/charset_normalizer-3.4.9-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198", size = 338848 }, + { url = "https://files.pythonhosted.org/packages/cd/91/7253a32e86b7e1d1239b1b36ba6dd0f021a21107ab33054b53119cc083b9/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32", size = 223022 }, + { url = "https://files.pythonhosted.org/packages/cb/32/2e64bd2be10e89c61e57ebe6a93fd98ae88eb7ebe414b5121f22c96c69eb/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632", size = 241590 }, + { url = "https://files.pythonhosted.org/packages/3d/ef/d96ec496cfea0c21db43b0ad03891308b02388d054cc902cf0e5a1ad6a88/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf", size = 239584 }, + { url = "https://files.pythonhosted.org/packages/d4/ce/9af95f7876194bd7a14e3dfe4a4de2e0bff02666a3910d72beafd06cc297/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990", size = 230224 }, + { url = "https://files.pythonhosted.org/packages/52/94/af74dde74a3996bd959c350709bfe50e297823d70a8c1cbd54b838880863/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d", size = 212667 }, + { url = "https://files.pythonhosted.org/packages/ee/f0/f1c4fe746c395922961b5916ed1d7d6e7d4c84851d19ed43cc89980ec953/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e", size = 227179 }, + { url = "https://files.pythonhosted.org/packages/e4/56/6c745619ac397e8871e2bcd3cea1eec86b877488f33888b3aef5c3ed506e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c", size = 225372 }, + { url = "https://files.pythonhosted.org/packages/78/ad/98aae8630ac71f16711968e38a5acfecce41b778bf2f0312851020f565a8/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2", size = 215222 }, + { url = "https://files.pythonhosted.org/packages/f7/40/9593d54209765207a7f11073c06494c1721e4ca4a0a426c597679bf7f91e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534", size = 231958 }, + { url = "https://files.pythonhosted.org/packages/b1/27/693ee5e8a18191eb38647360c51cd505013e2bd3b366aa43fd5344c21e3c/charset_normalizer-3.4.9-cp314-cp314t-win32.whl", hash = "sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226", size = 155580 }, + { url = "https://files.pythonhosted.org/packages/80/3f/bd97d3d9c613013d07cb7733d299385b41df37f0471310f5a73dc359f0b8/charset_normalizer-3.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177", size = 167620 }, + { url = "https://files.pythonhosted.org/packages/3d/c6/eee9dca4439b1061f76373f06ea855678cc4a64c1c3c90b50e479edbb8eb/charset_normalizer-3.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501", size = 158037 }, + { url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538 }, ] [[package]] @@ -550,11 +633,11 @@ wheels = [ [[package]] name = "cloudpathlib" -version = "0.23.0" +version = "0.24.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f4/18/2ac35d6b3015a0c74e923d94fc69baf8307f7c3233de015d69f99e17afa8/cloudpathlib-0.23.0.tar.gz", hash = "sha256:eb38a34c6b8a048ecfd2b2f60917f7cbad4a105b7c979196450c2f541f4d6b4b", size = 53126 } +sdist = { url = "https://files.pythonhosted.org/packages/06/19/58bc6b5d7d0f81c7209b05445af477e147c486552f96665a5912211839b9/cloudpathlib-0.24.0.tar.gz", hash = "sha256:c521a984e77b47e656fe78e20a7e3e260e0ab45fc69e33ac01094227c979e34a", size = 53600 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/8a/c4bb04426d608be4a3171efa2e233d2c59a5c8937850c10d098e126df18e/cloudpathlib-0.23.0-py3-none-any.whl", hash = "sha256:8520b3b01468fee77de37ab5d50b1b524ea6b4a8731c35d1b7407ac0cd716002", size = 62755 }, + { url = "https://files.pythonhosted.org/packages/c2/5b/ba933f896d9b0b07608d575a8501e2b4e32166b60d84c430a4a7285ebe64/cloudpathlib-0.24.0-py3-none-any.whl", hash = "sha256:b1c51e2d2ec7dc4fed6538991f4aea849d6cf11a7e6b9069f86e461aa1f9b5b4", size = 63214 }, ] [[package]] @@ -577,101 +660,86 @@ wheels = [ [[package]] name = "coverage" -version = "7.13.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/56/95b7e30fa389756cb56630faa728da46a27b8c6eb46f9d557c68fff12b65/coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91", size = 827239 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/ad/b59e5b451cf7172b8d1043dc0fa718f23aab379bc1521ee13d4bd9bfa960/coverage-7.13.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d490ba50c3f35dd7c17953c68f3270e7ccd1c6642e2d2afe2d8e720b98f5a053", size = 219278 }, - { url = "https://files.pythonhosted.org/packages/f1/17/0cb7ca3de72e5f4ef2ec2fa0089beafbcaaaead1844e8b8a63d35173d77d/coverage-7.13.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:19bc3c88078789f8ef36acb014d7241961dbf883fd2533d18cb1e7a5b4e28b11", size = 219783 }, - { url = "https://files.pythonhosted.org/packages/ab/63/325d8e5b11e0eaf6d0f6a44fad444ae58820929a9b0de943fa377fe73e85/coverage-7.13.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3998e5a32e62fdf410c0dbd3115df86297995d6e3429af80b8798aad894ca7aa", size = 250200 }, - { url = "https://files.pythonhosted.org/packages/76/53/c16972708cbb79f2942922571a687c52bd109a7bd51175aeb7558dff2236/coverage-7.13.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e264226ec98e01a8e1054314af91ee6cde0eacac4f465cc93b03dbe0bce2fd7", size = 252114 }, - { url = "https://files.pythonhosted.org/packages/eb/c2/7ab36d8b8cc412bec9ea2d07c83c48930eb4ba649634ba00cb7e4e0f9017/coverage-7.13.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3aa4e7b9e416774b21797365b358a6e827ffadaaca81b69ee02946852449f00", size = 254220 }, - { url = "https://files.pythonhosted.org/packages/d6/4d/cf52c9a3322c89a0e6febdfbc83bb45c0ed3c64ad14081b9503adee702e7/coverage-7.13.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:71ca20079dd8f27fcf808817e281e90220475cd75115162218d0e27549f95fef", size = 256164 }, - { url = "https://files.pythonhosted.org/packages/78/e9/eb1dd17bd6de8289df3580e967e78294f352a5df8a57ff4671ee5fc3dcd0/coverage-7.13.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e2f25215f1a359ab17320b47bcdaca3e6e6356652e8256f2441e4ef972052903", size = 250325 }, - { url = "https://files.pythonhosted.org/packages/71/07/8c1542aa873728f72267c07278c5cc0ec91356daf974df21335ccdb46368/coverage-7.13.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d65b2d373032411e86960604dc4edac91fdfb5dca539461cf2cbe78327d1e64f", size = 251913 }, - { url = "https://files.pythonhosted.org/packages/74/d7/c62e2c5e4483a748e27868e4c32ad3daa9bdddbba58e1bc7a15e252baa74/coverage-7.13.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94eb63f9b363180aff17de3e7c8760c3ba94664ea2695c52f10111244d16a299", size = 249974 }, - { url = "https://files.pythonhosted.org/packages/98/9f/4c5c015a6e98ced54efd0f5cf8d31b88e5504ecb6857585fc0161bb1e600/coverage-7.13.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e856bf6616714c3a9fbc270ab54103f4e685ba236fa98c054e8f87f266c93505", size = 253741 }, - { url = "https://files.pythonhosted.org/packages/bd/59/0f4eef89b9f0fcd9633b5d350016f54126ab49426a70ff4c4e87446cabdc/coverage-7.13.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:65dfcbe305c3dfe658492df2d85259e0d79ead4177f9ae724b6fb245198f55d6", size = 249695 }, - { url = "https://files.pythonhosted.org/packages/b5/2c/b7476f938deb07166f3eb281a385c262675d688ff4659ad56c6c6b8e2e70/coverage-7.13.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b507778ae8a4c915436ed5c2e05b4a6cecfa70f734e19c22a005152a11c7b6a9", size = 250599 }, - { url = "https://files.pythonhosted.org/packages/b8/34/c3420709d9846ee3785b9f2831b4d94f276f38884032dca1457fa83f7476/coverage-7.13.4-cp311-cp311-win32.whl", hash = "sha256:784fc3cf8be001197b652d51d3fd259b1e2262888693a4636e18879f613a62a9", size = 221780 }, - { url = "https://files.pythonhosted.org/packages/61/08/3d9c8613079d2b11c185b865de9a4c1a68850cfda2b357fae365cf609f29/coverage-7.13.4-cp311-cp311-win_amd64.whl", hash = "sha256:2421d591f8ca05b308cf0092807308b2facbefe54af7c02ac22548b88b95c98f", size = 222715 }, - { url = "https://files.pythonhosted.org/packages/18/1a/54c3c80b2f056164cc0a6cdcb040733760c7c4be9d780fe655f356f433e4/coverage-7.13.4-cp311-cp311-win_arm64.whl", hash = "sha256:79e73a76b854d9c6088fe5d8b2ebe745f8681c55f7397c3c0a016192d681045f", size = 221385 }, - { url = "https://files.pythonhosted.org/packages/d1/81/4ce2fdd909c5a0ed1f6dedb88aa57ab79b6d1fbd9b588c1ac7ef45659566/coverage-7.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02231499b08dabbe2b96612993e5fc34217cdae907a51b906ac7fca8027a4459", size = 219449 }, - { url = "https://files.pythonhosted.org/packages/5d/96/5238b1efc5922ddbdc9b0db9243152c09777804fb7c02ad1741eb18a11c0/coverage-7.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40aa8808140e55dc022b15d8aa7f651b6b3d68b365ea0398f1441e0b04d859c3", size = 219810 }, - { url = "https://files.pythonhosted.org/packages/78/72/2f372b726d433c9c35e56377cf1d513b4c16fe51841060d826b95caacec1/coverage-7.13.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5b856a8ccf749480024ff3bd7310adaef57bf31fd17e1bfc404b7940b6986634", size = 251308 }, - { url = "https://files.pythonhosted.org/packages/5d/a0/2ea570925524ef4e00bb6c82649f5682a77fac5ab910a65c9284de422600/coverage-7.13.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c048ea43875fbf8b45d476ad79f179809c590ec7b79e2035c662e7afa3192e3", size = 254052 }, - { url = "https://files.pythonhosted.org/packages/e8/ac/45dc2e19a1939098d783c846e130b8f862fbb50d09e0af663988f2f21973/coverage-7.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7b38448866e83176e28086674fe7368ab8590e4610fb662b44e345b86d63ffa", size = 255165 }, - { url = "https://files.pythonhosted.org/packages/2d/4d/26d236ff35abc3b5e63540d3386e4c3b192168c1d96da5cb2f43c640970f/coverage-7.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de6defc1c9badbf8b9e67ae90fd00519186d6ab64e5cc5f3d21359c2a9b2c1d3", size = 257432 }, - { url = "https://files.pythonhosted.org/packages/ec/55/14a966c757d1348b2e19caf699415a2a4c4f7feaa4bbc6326a51f5c7dd1b/coverage-7.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7eda778067ad7ffccd23ecffce537dface96212576a07924cbf0d8799d2ded5a", size = 251716 }, - { url = "https://files.pythonhosted.org/packages/77/33/50116647905837c66d28b2af1321b845d5f5d19be9655cb84d4a0ea806b4/coverage-7.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e87f6c587c3f34356c3759f0420693e35e7eb0e2e41e4c011cb6ec6ecbbf1db7", size = 253089 }, - { url = "https://files.pythonhosted.org/packages/c2/b4/8efb11a46e3665d92635a56e4f2d4529de6d33f2cb38afd47d779d15fc99/coverage-7.13.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8248977c2e33aecb2ced42fef99f2d319e9904a36e55a8a68b69207fb7e43edc", size = 251232 }, - { url = "https://files.pythonhosted.org/packages/51/24/8cd73dd399b812cc76bb0ac260e671c4163093441847ffe058ac9fda1e32/coverage-7.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:25381386e80ae727608e662474db537d4df1ecd42379b5ba33c84633a2b36d47", size = 255299 }, - { url = "https://files.pythonhosted.org/packages/03/94/0a4b12f1d0e029ce1ccc1c800944a9984cbe7d678e470bb6d3c6bc38a0da/coverage-7.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ee756f00726693e5ba94d6df2bdfd64d4852d23b09bb0bc700e3b30e6f333985", size = 250796 }, - { url = "https://files.pythonhosted.org/packages/73/44/6002fbf88f6698ca034360ce474c406be6d5a985b3fdb3401128031eef6b/coverage-7.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fdfc1e28e7c7cdce44985b3043bc13bbd9c747520f94a4d7164af8260b3d91f0", size = 252673 }, - { url = "https://files.pythonhosted.org/packages/de/c6/a0279f7c00e786be75a749a5674e6fa267bcbd8209cd10c9a450c655dfa7/coverage-7.13.4-cp312-cp312-win32.whl", hash = "sha256:01d4cbc3c283a17fc1e42d614a119f7f438eabb593391283adca8dc86eff1246", size = 221990 }, - { url = "https://files.pythonhosted.org/packages/77/4e/c0a25a425fcf5557d9abd18419c95b63922e897bc86c1f327f155ef234a9/coverage-7.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:9401ebc7ef522f01d01d45532c68c5ac40fb27113019b6b7d8b208f6e9baa126", size = 222800 }, - { url = "https://files.pythonhosted.org/packages/47/ac/92da44ad9a6f4e3a7debd178949d6f3769bedca33830ce9b1dcdab589a37/coverage-7.13.4-cp312-cp312-win_arm64.whl", hash = "sha256:b1ec7b6b6e93255f952e27ab58fbc68dcc468844b16ecbee881aeb29b6ab4d8d", size = 221415 }, - { url = "https://files.pythonhosted.org/packages/db/23/aad45061a31677d68e47499197a131eea55da4875d16c1f42021ab963503/coverage-7.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b66a2da594b6068b48b2692f043f35d4d3693fb639d5ea8b39533c2ad9ac3ab9", size = 219474 }, - { url = "https://files.pythonhosted.org/packages/a5/70/9b8b67a0945f3dfec1fd896c5cefb7c19d5a3a6d74630b99a895170999ae/coverage-7.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3599eb3992d814d23b35c536c28df1a882caa950f8f507cef23d1cbf334995ac", size = 219844 }, - { url = "https://files.pythonhosted.org/packages/97/fd/7e859f8fab324cef6c4ad7cff156ca7c489fef9179d5749b0c8d321281c2/coverage-7.13.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:93550784d9281e374fb5a12bf1324cc8a963fd63b2d2f223503ef0fd4aa339ea", size = 250832 }, - { url = "https://files.pythonhosted.org/packages/e4/dc/b2442d10020c2f52617828862d8b6ee337859cd8f3a1f13d607dddda9cf7/coverage-7.13.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b720ce6a88a2755f7c697c23268ddc47a571b88052e6b155224347389fdf6a3b", size = 253434 }, - { url = "https://files.pythonhosted.org/packages/5a/88/6728a7ad17428b18d836540630487231f5470fb82454871149502f5e5aa2/coverage-7.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b322db1284a2ed3aa28ffd8ebe3db91c929b7a333c0820abec3d838ef5b3525", size = 254676 }, - { url = "https://files.pythonhosted.org/packages/7c/bc/21244b1b8cedf0dff0a2b53b208015fe798d5f2a8d5348dbfece04224fff/coverage-7.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4594c67d8a7c89cf922d9df0438c7c7bb022ad506eddb0fdb2863359ff78242", size = 256807 }, - { url = "https://files.pythonhosted.org/packages/97/a0/ddba7ed3251cff51006737a727d84e05b61517d1784a9988a846ba508877/coverage-7.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53d133df809c743eb8bce33b24bcababb371f4441340578cd406e084d94a6148", size = 251058 }, - { url = "https://files.pythonhosted.org/packages/9b/55/e289addf7ff54d3a540526f33751951bf0878f3809b47f6dfb3def69c6f7/coverage-7.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76451d1978b95ba6507a039090ba076105c87cc76fc3efd5d35d72093964d49a", size = 252805 }, - { url = "https://files.pythonhosted.org/packages/13/4e/cc276b1fa4a59be56d96f1dabddbdc30f4ba22e3b1cd42504c37b3313255/coverage-7.13.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7f57b33491e281e962021de110b451ab8a24182589be17e12a22c79047935e23", size = 250766 }, - { url = "https://files.pythonhosted.org/packages/94/44/1093b8f93018f8b41a8cf29636c9292502f05e4a113d4d107d14a3acd044/coverage-7.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1731dc33dc276dafc410a885cbf5992f1ff171393e48a21453b78727d090de80", size = 254923 }, - { url = "https://files.pythonhosted.org/packages/8b/55/ea2796da2d42257f37dbea1aab239ba9263b31bd91d5527cdd6db5efe174/coverage-7.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:bd60d4fe2f6fa7dff9223ca1bbc9f05d2b6697bc5961072e5d3b952d46e1b1ea", size = 250591 }, - { url = "https://files.pythonhosted.org/packages/d4/fa/7c4bb72aacf8af5020675aa633e59c1fbe296d22aed191b6a5b711eb2bc7/coverage-7.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9181a3ccead280b828fae232df12b16652702b49d41e99d657f46cc7b1f6ec7a", size = 252364 }, - { url = "https://files.pythonhosted.org/packages/5c/38/a8d2ec0146479c20bbaa7181b5b455a0c41101eed57f10dd19a78ab44c80/coverage-7.13.4-cp313-cp313-win32.whl", hash = "sha256:f53d492307962561ac7de4cd1de3e363589b000ab69617c6156a16ba7237998d", size = 222010 }, - { url = "https://files.pythonhosted.org/packages/e2/0c/dbfafbe90a185943dcfbc766fe0e1909f658811492d79b741523a414a6cc/coverage-7.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:e6f70dec1cc557e52df5306d051ef56003f74d56e9c4dd7ddb07e07ef32a84dd", size = 222818 }, - { url = "https://files.pythonhosted.org/packages/04/d1/934918a138c932c90d78301f45f677fb05c39a3112b96fd2c8e60503cdc7/coverage-7.13.4-cp313-cp313-win_arm64.whl", hash = "sha256:fb07dc5da7e849e2ad31a5d74e9bece81f30ecf5a42909d0a695f8bd1874d6af", size = 221438 }, - { url = "https://files.pythonhosted.org/packages/52/57/ee93ced533bcb3e6df961c0c6e42da2fc6addae53fb95b94a89b1e33ebd7/coverage-7.13.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40d74da8e6c4b9ac18b15331c4b5ebc35a17069410cad462ad4f40dcd2d50c0d", size = 220165 }, - { url = "https://files.pythonhosted.org/packages/c5/e0/969fc285a6fbdda49d91af278488d904dcd7651b2693872f0ff94e40e84a/coverage-7.13.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4223b4230a376138939a9173f1bdd6521994f2aff8047fae100d6d94d50c5a12", size = 220516 }, - { url = "https://files.pythonhosted.org/packages/b1/b8/9531944e16267e2735a30a9641ff49671f07e8138ecf1ca13db9fd2560c7/coverage-7.13.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1d4be36a5114c499f9f1f9195e95ebf979460dbe2d88e6816ea202010ba1c34b", size = 261804 }, - { url = "https://files.pythonhosted.org/packages/8a/f3/e63df6d500314a2a60390d1989240d5f27318a7a68fa30ad3806e2a9323e/coverage-7.13.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:200dea7d1e8095cc6e98cdabe3fd1d21ab17d3cee6dab00cadbb2fe35d9c15b9", size = 263885 }, - { url = "https://files.pythonhosted.org/packages/f3/67/7654810de580e14b37670b60a09c599fa348e48312db5b216d730857ffe6/coverage-7.13.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8eb931ee8e6d8243e253e5ed7336deea6904369d2fd8ae6e43f68abbf167092", size = 266308 }, - { url = "https://files.pythonhosted.org/packages/37/6f/39d41eca0eab3cc82115953ad41c4e77935286c930e8fad15eaed1389d83/coverage-7.13.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75eab1ebe4f2f64d9509b984f9314d4aa788540368218b858dad56dc8f3e5eb9", size = 267452 }, - { url = "https://files.pythonhosted.org/packages/50/6d/39c0fbb8fc5cd4d2090811e553c2108cf5112e882f82505ee7495349a6bf/coverage-7.13.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c35eb28c1d085eb7d8c9b3296567a1bebe03ce72962e932431b9a61f28facf26", size = 261057 }, - { url = "https://files.pythonhosted.org/packages/a4/a2/60010c669df5fa603bb5a97fb75407e191a846510da70ac657eb696b7fce/coverage-7.13.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb88b316ec33760714a4720feb2816a3a59180fd58c1985012054fa7aebee4c2", size = 263875 }, - { url = "https://files.pythonhosted.org/packages/3e/d9/63b22a6bdbd17f1f96e9ed58604c2a6b0e72a9133e37d663bef185877cf6/coverage-7.13.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7d41eead3cc673cbd38a4417deb7fd0b4ca26954ff7dc6078e33f6ff97bed940", size = 261500 }, - { url = "https://files.pythonhosted.org/packages/70/bf/69f86ba1ad85bc3ad240e4c0e57a2e620fbc0e1645a47b5c62f0e941ad7f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:fb26a934946a6afe0e326aebe0730cdff393a8bc0bbb65a2f41e30feddca399c", size = 265212 }, - { url = "https://files.pythonhosted.org/packages/ae/f2/5f65a278a8c2148731831574c73e42f57204243d33bedaaf18fa79c5958f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:dae88bc0fc77edaa65c14be099bd57ee140cf507e6bfdeea7938457ab387efb0", size = 260398 }, - { url = "https://files.pythonhosted.org/packages/ef/80/6e8280a350ee9fea92f14b8357448a242dcaa243cb2c72ab0ca591f66c8c/coverage-7.13.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:845f352911777a8e722bfce168958214951e07e47e5d5d9744109fa5fe77f79b", size = 262584 }, - { url = "https://files.pythonhosted.org/packages/22/63/01ff182fc95f260b539590fb12c11ad3e21332c15f9799cb5e2386f71d9f/coverage-7.13.4-cp313-cp313t-win32.whl", hash = "sha256:2fa8d5f8de70688a28240de9e139fa16b153cc3cbb01c5f16d88d6505ebdadf9", size = 222688 }, - { url = "https://files.pythonhosted.org/packages/a9/43/89de4ef5d3cd53b886afa114065f7e9d3707bdb3e5efae13535b46ae483d/coverage-7.13.4-cp313-cp313t-win_amd64.whl", hash = "sha256:9351229c8c8407645840edcc277f4a2d44814d1bc34a2128c11c2a031d45a5dd", size = 223746 }, - { url = "https://files.pythonhosted.org/packages/35/39/7cf0aa9a10d470a5309b38b289b9bb07ddeac5d61af9b664fe9775a4cb3e/coverage-7.13.4-cp313-cp313t-win_arm64.whl", hash = "sha256:30b8d0512f2dc8c8747557e8fb459d6176a2c9e5731e2b74d311c03b78451997", size = 222003 }, - { url = "https://files.pythonhosted.org/packages/92/11/a9cf762bb83386467737d32187756a42094927150c3e107df4cb078e8590/coverage-7.13.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:300deaee342f90696ed186e3a00c71b5b3d27bffe9e827677954f4ee56969601", size = 219522 }, - { url = "https://files.pythonhosted.org/packages/d3/28/56e6d892b7b052236d67c95f1936b6a7cf7c3e2634bf27610b8cbd7f9c60/coverage-7.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29e3220258d682b6226a9b0925bc563ed9a1ebcff3cad30f043eceea7eaf2689", size = 219855 }, - { url = "https://files.pythonhosted.org/packages/e5/69/233459ee9eb0c0d10fcc2fe425a029b3fa5ce0f040c966ebce851d030c70/coverage-7.13.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:391ee8f19bef69210978363ca930f7328081c6a0152f1166c91f0b5fdd2a773c", size = 250887 }, - { url = "https://files.pythonhosted.org/packages/06/90/2cdab0974b9b5bbc1623f7876b73603aecac11b8d95b85b5b86b32de5eab/coverage-7.13.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0dd7ab8278f0d58a0128ba2fca25824321f05d059c1441800e934ff2efa52129", size = 253396 }, - { url = "https://files.pythonhosted.org/packages/ac/15/ea4da0f85bf7d7b27635039e649e99deb8173fe551096ea15017f7053537/coverage-7.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78cdf0d578b15148b009ccf18c686aa4f719d887e76e6b40c38ffb61d264a552", size = 254745 }, - { url = "https://files.pythonhosted.org/packages/99/11/bb356e86920c655ca4d61daee4e2bbc7258f0a37de0be32d233b561134ff/coverage-7.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:48685fee12c2eb3b27c62f2658e7ea21e9c3239cba5a8a242801a0a3f6a8c62a", size = 257055 }, - { url = "https://files.pythonhosted.org/packages/c9/0f/9ae1f8cb17029e09da06ca4e28c9e1d5c1c0a511c7074592e37e0836c915/coverage-7.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4e83efc079eb39480e6346a15a1bcb3e9b04759c5202d157e1dd4303cd619356", size = 250911 }, - { url = "https://files.pythonhosted.org/packages/89/3a/adfb68558fa815cbc29747b553bc833d2150228f251b127f1ce97e48547c/coverage-7.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecae9737b72408d6a950f7e525f30aca12d4bd8dd95e37342e5beb3a2a8c4f71", size = 252754 }, - { url = "https://files.pythonhosted.org/packages/32/b1/540d0c27c4e748bd3cd0bd001076ee416eda993c2bae47a73b7cc9357931/coverage-7.13.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ae4578f8528569d3cf303fef2ea569c7f4c4059a38c8667ccef15c6e1f118aa5", size = 250720 }, - { url = "https://files.pythonhosted.org/packages/c7/95/383609462b3ffb1fe133014a7c84fc0dd01ed55ac6140fa1093b5af7ebb1/coverage-7.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6fdef321fdfbb30a197efa02d48fcd9981f0d8ad2ae8903ac318adc653f5df98", size = 254994 }, - { url = "https://files.pythonhosted.org/packages/f7/ba/1761138e86c81680bfc3c49579d66312865457f9fe405b033184e5793cb3/coverage-7.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b0f6ccf3dbe577170bebfce1318707d0e8c3650003cb4b3a9dd744575daa8b5", size = 250531 }, - { url = "https://files.pythonhosted.org/packages/f8/8e/05900df797a9c11837ab59c4d6fe94094e029582aab75c3309a93e6fb4e3/coverage-7.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75fcd519f2a5765db3f0e391eb3b7d150cce1a771bf4c9f861aeab86c767a3c0", size = 252189 }, - { url = "https://files.pythonhosted.org/packages/00/bd/29c9f2db9ea4ed2738b8a9508c35626eb205d51af4ab7bf56a21a2e49926/coverage-7.13.4-cp314-cp314-win32.whl", hash = "sha256:8e798c266c378da2bd819b0677df41ab46d78065fb2a399558f3f6cae78b2fbb", size = 222258 }, - { url = "https://files.pythonhosted.org/packages/a7/4d/1f8e723f6829977410efeb88f73673d794075091c8c7c18848d273dc9d73/coverage-7.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:245e37f664d89861cf2329c9afa2c1fe9e6d4e1a09d872c947e70718aeeac505", size = 223073 }, - { url = "https://files.pythonhosted.org/packages/51/5b/84100025be913b44e082ea32abcf1afbf4e872f5120b7a1cab1d331b1e13/coverage-7.13.4-cp314-cp314-win_arm64.whl", hash = "sha256:ad27098a189e5838900ce4c2a99f2fe42a0bf0c2093c17c69b45a71579e8d4a2", size = 221638 }, - { url = "https://files.pythonhosted.org/packages/a7/e4/c884a405d6ead1370433dad1e3720216b4f9fd8ef5b64bfd984a2a60a11a/coverage-7.13.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:85480adfb35ffc32d40918aad81b89c69c9cc5661a9b8a81476d3e645321a056", size = 220246 }, - { url = "https://files.pythonhosted.org/packages/81/5c/4d7ed8b23b233b0fffbc9dfec53c232be2e695468523242ea9fd30f97ad2/coverage-7.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:79be69cf7f3bf9b0deeeb062eab7ac7f36cd4cc4c4dd694bd28921ba4d8596cc", size = 220514 }, - { url = "https://files.pythonhosted.org/packages/2f/6f/3284d4203fd2f28edd73034968398cd2d4cb04ab192abc8cff007ea35679/coverage-7.13.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:caa421e2684e382c5d8973ac55e4f36bed6821a9bad5c953494de960c74595c9", size = 261877 }, - { url = "https://files.pythonhosted.org/packages/09/aa/b672a647bbe1556a85337dc95bfd40d146e9965ead9cc2fe81bde1e5cbce/coverage-7.13.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14375934243ee05f56c45393fe2ce81fe5cc503c07cee2bdf1725fb8bef3ffaf", size = 264004 }, - { url = "https://files.pythonhosted.org/packages/79/a1/aa384dbe9181f98bba87dd23dda436f0c6cf2e148aecbb4e50fc51c1a656/coverage-7.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25a41c3104d08edb094d9db0d905ca54d0cd41c928bb6be3c4c799a54753af55", size = 266408 }, - { url = "https://files.pythonhosted.org/packages/53/5e/5150bf17b4019bc600799f376bb9606941e55bd5a775dc1e096b6ffea952/coverage-7.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f01afcff62bf9a08fb32b2c1d6e924236c0383c02c790732b6537269e466a72", size = 267544 }, - { url = "https://files.pythonhosted.org/packages/e0/ed/f1de5c675987a4a7a672250d2c5c9d73d289dbf13410f00ed7181d8017dd/coverage-7.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eb9078108fbf0bcdde37c3f4779303673c2fa1fe8f7956e68d447d0dd426d38a", size = 260980 }, - { url = "https://files.pythonhosted.org/packages/b3/e3/fe758d01850aa172419a6743fe76ba8b92c29d181d4f676ffe2dae2ba631/coverage-7.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e086334e8537ddd17e5f16a344777c1ab8194986ec533711cbe6c41cde841b6", size = 263871 }, - { url = "https://files.pythonhosted.org/packages/b6/76/b829869d464115e22499541def9796b25312b8cf235d3bb00b39f1675395/coverage-7.13.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:725d985c5ab621268b2edb8e50dfe57633dc69bda071abc470fed55a14935fd3", size = 261472 }, - { url = "https://files.pythonhosted.org/packages/14/9e/caedb1679e73e2f6ad240173f55218488bfe043e38da577c4ec977489915/coverage-7.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3c06f0f1337c667b971ca2f975523347e63ec5e500b9aa5882d91931cd3ef750", size = 265210 }, - { url = "https://files.pythonhosted.org/packages/3a/10/0dd02cb009b16ede425b49ec344aba13a6ae1dc39600840ea6abcb085ac4/coverage-7.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:590c0ed4bf8e85f745e6b805b2e1c457b2e33d5255dd9729743165253bc9ad39", size = 260319 }, - { url = "https://files.pythonhosted.org/packages/92/8e/234d2c927af27c6d7a5ffad5bd2cf31634c46a477b4c7adfbfa66baf7ebb/coverage-7.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eb30bf180de3f632cd043322dad5751390e5385108b2807368997d1a92a509d0", size = 262638 }, - { url = "https://files.pythonhosted.org/packages/2f/64/e5547c8ff6964e5965c35a480855911b61509cce544f4d442caa759a0702/coverage-7.13.4-cp314-cp314t-win32.whl", hash = "sha256:c4240e7eded42d131a2d2c4dec70374b781b043ddc79a9de4d55ca71f8e98aea", size = 223040 }, - { url = "https://files.pythonhosted.org/packages/c7/96/38086d58a181aac86d503dfa9c47eb20715a79c3e3acbdf786e92e5c09a8/coverage-7.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4c7d3cc01e7350f2f0f6f7036caaf5673fb56b6998889ccfe9e1c1fe75a9c932", size = 224148 }, - { url = "https://files.pythonhosted.org/packages/ce/72/8d10abd3740a0beb98c305e0c3faf454366221c0f37a8bcf8f60020bb65a/coverage-7.13.4-cp314-cp314t-win_arm64.whl", hash = "sha256:23e3f687cf945070d1c90f85db66d11e3025665d8dafa831301a0e0038f3db9b", size = 222172 }, - { url = "https://files.pythonhosted.org/packages/0d/4a/331fe2caf6799d591109bb9c08083080f6de90a823695d412a935622abb2/coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0", size = 211242 }, +version = "7.15.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/d0/55fe630f4cf94e3fcba868240fad8c8cdd1f764e2a932f8926347e6ec4cd/coverage-7.15.2.tar.gz", hash = "sha256:3df60dc267f0a2ca23cb7a9ab1109c62b9335ffbf519fcfe167157c28c09b81d", size = 927741 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/3a/54536704f507d4573bf9161c4d0dd3dd59b6d85e48c664e901b6844d8e33/coverage-7.15.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2f1ec6f304b156669cfde653b4e9a953f5de87e247ea02ac599bce0ab2744036", size = 221414 }, + { url = "https://files.pythonhosted.org/packages/b6/d9/8ba925d29743e3577b21e4d8c11a702b76bc93c41e7fdfd1177af63d4b8d/coverage-7.15.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4d3361879d736f469f45723c11ea1a5bbdaf1f6928f0e632c940378b5aa9b660", size = 221913 }, + { url = "https://files.pythonhosted.org/packages/09/54/a855f3aa0187f2b431ade4e4791b77b56282cfb5d201c83ec26a31b5b36a/coverage-7.15.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c6a98d698f9e2c8008d0370ec7fc452ebfcc530002ae2d0061170d768b992589", size = 252332 }, + { url = "https://files.pythonhosted.org/packages/8e/d3/13ac97b4370640ba3452fc8559b06cc2f479ce3ba4a0b632a73e44c38a7d/coverage-7.15.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d50dd325e18ec25bfcc10cd7f99b04df1ab9ec76b0918c260e60817ad0643dee", size = 254243 }, + { url = "https://files.pythonhosted.org/packages/88/83/5eca144942d8d0659d3f55176517f4a59cdc65eefd17146a0770935a3ebd/coverage-7.15.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67d7602480a47bdf5b675635403625553ebaa70d5a62a657c035149fd401cea0", size = 256352 }, + { url = "https://files.pythonhosted.org/packages/4e/ba/d3db2e01a50fc88cdb4c0f19542bcf6f61489e34dc9aa3538413e2459a38/coverage-7.15.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cee0f89f4767a6057c8fbf168f8135f18be651300496086bd873e3189fed0487", size = 258313 }, + { url = "https://files.pythonhosted.org/packages/78/b3/aba83416e9177df28e5186d856c19158c59fc0e7e814aaa61a4a2354ad1b/coverage-7.15.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a29ec5305a7335aacee2d799e3422e91e1c8a12474986e2b3b07e315c91be82f", size = 252449 }, + { url = "https://files.pythonhosted.org/packages/6a/a5/4b00ecac0194431ab451b0f6710f8e2517d04cef60f821b14dec4637d575/coverage-7.15.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:48ccc6395958eda89093ecdc35644c86f23a8b23a7f4d44958812b721aad67c1", size = 254043 }, + { url = "https://files.pythonhosted.org/packages/75/b6/cfa209b4313ee7f1b34da47efcd789ea51c024ad35af390e00f5a3c10a2e/coverage-7.15.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:81f382c5a94b434ec1f6da607edb904c76d7212e618cd4d1bc9f97bed4120ef5", size = 252107 }, + { url = "https://files.pythonhosted.org/packages/36/67/e8cac5a6954038c98d7fe7eb9802afe7ab3ecb637bb7cc00e69b4148b56d/coverage-7.15.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:bbc808daf4f5cd567af8075ecc72d21c6dfef9a254709a621a84c217c935ebc0", size = 255873 }, + { url = "https://files.pythonhosted.org/packages/2c/92/395cca9f330a86c3fe3471d73e2c102116c4c58fdc619dbbc125c6e93a54/coverage-7.15.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:a4c46b247b5d4b78f613bd89fea926d32b25c6cc61a50bd1e99ba310348f3dad", size = 251826 }, + { url = "https://files.pythonhosted.org/packages/51/60/3e91b20295439652424f426b7086ec5bf4fbe3f604c73eda22b986c4fd6b/coverage-7.15.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:094dd37f3ef7b2da8b068b583d1f4c40f91c65197e16c52a71962d5d537fc5db", size = 252735 }, + { url = "https://files.pythonhosted.org/packages/a5/eb/8c07839005e5e3c6b3877d3a6e2a80ce766589f31dd2b6882b78d59a7b8c/coverage-7.15.2-cp311-cp311-win32.whl", hash = "sha256:a63b9e190711134d581c4d703df5df09851b1acf99792c7aacbbe9f41f0283c9", size = 223500 }, + { url = "https://files.pythonhosted.org/packages/2e/98/59d83c257cd59f0fbaf9d9ddb26b744a576760dfd1ae16e516408894a02b/coverage-7.15.2-cp311-cp311-win_amd64.whl", hash = "sha256:8bb9f4b4279187560796a4cdaca3b0a93dd97e48ee667df005f4ed9a97403688", size = 223973 }, + { url = "https://files.pythonhosted.org/packages/ea/09/2d285c8bef5c4f695d120c1c96dc11715638aa8e134069f210bb6a62a9fe/coverage-7.15.2-cp311-cp311-win_arm64.whl", hash = "sha256:8c726b232659cbd2ae57ade46509eb068c9bd7a06df9fcbff6fe484870006934", size = 223519 }, + { url = "https://files.pythonhosted.org/packages/6a/50/eb5bf42e531611a9f8d272556b1ed4de503f84a91413584094487cf69f8f/coverage-7.15.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1adac78e5abc7c5438f7a209c9ca69d06542f0bf481d728b6989ea80b813fdf9", size = 221587 }, + { url = "https://files.pythonhosted.org/packages/06/d1/da99af464c335d4e023a6efcd7ec30f63b88a43c93745154ab74ffb31cea/coverage-7.15.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b868acc62aa5de3be7a9d05c2333bf8359ca987e43f9cb30ff8fbda6a024ab73", size = 221943 }, + { url = "https://files.pythonhosted.org/packages/5b/8a/13c42723d61ca447eafa18732e8141dd6a63f2732e1c7e1502c182dd88d7/coverage-7.15.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6f6966fc30e6f06ca8f98fb0ce51eda6b111b3ee8d066a8b1ec9e77fa06ab55d", size = 253450 }, + { url = "https://files.pythonhosted.org/packages/d7/29/99021303f98fbdcb63504b4d07bea4cc025b9b2dd907c4f07c85d50a0dab/coverage-7.15.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:68af907f595ab01a78f794932ff3bdf929c316d3000810d38dbc247129e26f8b", size = 256187 }, + { url = "https://files.pythonhosted.org/packages/f9/a8/fd503715ed6ca9c5d742923aa5209257340b367a867b2ced0c7d4ba8a0b9/coverage-7.15.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:afa29e2eff3d5729267e2cb2fd4ce9d61c952932fb2694e34ccb5d9540c6a296", size = 257301 }, + { url = "https://files.pythonhosted.org/packages/da/40/3f4b8fb409810036ebc2857d36adc0498c6e957b5df0290c5036b2e143f1/coverage-7.15.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bbf44513ceb1589e31948e20eafbde9deaface90e1a1afa5f5f77b4423d17ce6", size = 259562 }, + { url = "https://files.pythonhosted.org/packages/0b/8a/9bdffbef47db77cce3d6b02a28f7e919b19f0106c4b080c2c2246040f885/coverage-7.15.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9deddf09eecb717b7f980414b43d90a5b22ff3967d2949ab29cb0aa83d9e9098", size = 253841 }, + { url = "https://files.pythonhosted.org/packages/1b/1e/9031efde019d31a06646261fce6dfc5c3c74e951e27a71e5c9a424563178/coverage-7.15.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ae901f7e55ba405c84ee1cab3d3e962e4e871e4a2bcb9c90911adbd69b42ac5a", size = 255221 }, + { url = "https://files.pythonhosted.org/packages/56/db/787acde872389fc84a9ef9d8cd1ccc658e391ab4cb5b28092a714426a394/coverage-7.15.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a0f47002c6eeb7c280228467a4cb0cc15ca2103a8421b986b2d3ec04a0f9bd8b", size = 253366 }, + { url = "https://files.pythonhosted.org/packages/2f/9b/6f57bc4b93c842eef1695f8cdaf2318e35e7ba54f5ba80d84be213ab7858/coverage-7.15.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1cd7a5beb7af3e864a13b1f0fb26efd3695da43ef0daf71e586adfffaf34d5b2", size = 257434 }, + { url = "https://files.pythonhosted.org/packages/88/26/b3186a21b2acc83e451118978905c81c7072c3333707804db09a78c096a2/coverage-7.15.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:97a5c5457a9fb1d6c4e06cfb5dc835871fbfb6a6a51addc9e925bdeff5ef7440", size = 252935 }, + { url = "https://files.pythonhosted.org/packages/20/c2/c9f3376b2e717ea69ed7a6e9a5fcab968fb0b290db6cf4bd9a1fc7541b75/coverage-7.15.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0901cfe6c13bcd2302da4f83e884555d2a22bda6e4c476f09ef204ba20ca536e", size = 254807 }, + { url = "https://files.pythonhosted.org/packages/f0/e1/dfc15401f4a8aaeb486e1ba3e9e3c40522a6e38bd0ecf0b3f29cb8082957/coverage-7.15.2-cp312-cp312-win32.whl", hash = "sha256:b171bdd71cb7ff792bf32e376173b0ace7e7963e7e57c58dfc42063a6a7174cd", size = 223641 }, + { url = "https://files.pythonhosted.org/packages/91/40/81b6d809d320cd366ec5bdf8176575e897dcb8efe7fb4b489ef9e93e4d13/coverage-7.15.2-cp312-cp312-win_amd64.whl", hash = "sha256:582edc45c2040543fef83341be23c43024a3ab3ae0c2d8bc498a06282905ad40", size = 224172 }, + { url = "https://files.pythonhosted.org/packages/ef/28/9f14ec438149f7de557f45518f09b4a7917b795cc37083aa7db482693f8c/coverage-7.15.2-cp312-cp312-win_arm64.whl", hash = "sha256:a638db90c61cd219aeee65e83a24fdaa57269a741ae0cf773309208ac862cee3", size = 223556 }, + { url = "https://files.pythonhosted.org/packages/fc/d5/f8c838e6b7282976f7c918884b792df7a0c42c5bba5d99c60ad2d221d56d/coverage-7.15.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1121caa19159a38b5463eaae4b1e1fde81e525b15ecc5e000cd5b1a108f743a8", size = 221606 }, + { url = "https://files.pythonhosted.org/packages/bf/37/97c926376364f66298cc44893b89cdf17b8bc406376497c4061ae4b8a8ff/coverage-7.15.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a300c6934e0989c327b9e8a1e110329da4641149f872bbe9f70168be66da76c1", size = 221982 }, + { url = "https://files.pythonhosted.org/packages/b7/30/a36050a6e83c2135ee0776f452ca3948224befc6d7f26acecc082d0c106a/coverage-7.15.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2617f8799d268fabdeef42a7e89ac3a23e1deee9025427db2df970f99a89a578", size = 252972 }, + { url = "https://files.pythonhosted.org/packages/31/d3/06b5f1daf95f0f15ab05bd75f26ba5f3c8b33d0bb72f3aaa3cf41d1bad3a/coverage-7.15.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7dc2950a2992cd676d35c20ae63522836deeb034f08874699d14068710af3dc1", size = 255569 }, + { url = "https://files.pythonhosted.org/packages/81/1c/9afb3f8de2b8d36960391c48559a2e3ff96594b58099f115921549ea8d0d/coverage-7.15.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9e36686f7a442185db2400b3df171aac520869faf9deb59df687d28659eda2a6", size = 256806 }, + { url = "https://files.pythonhosted.org/packages/64/d8/b989f96061a5e32d82fddd1b1b9ff48a7c8f8ae7606f0e80fd9de54b1e33/coverage-7.15.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d29ca7bd67af6e12e74632d65f026eabc1364da5c254494cd914446a28a3ef7", size = 258936 }, + { url = "https://files.pythonhosted.org/packages/b8/fa/f99771f5110457c7b511c1935ca49ddf288218eaa84322e028b9334146ae/coverage-7.15.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:db9c8438057e5b0f6a22a0af99c0c1d26b57fbbdbd1be5861ddb8f897fcc3a2d", size = 253178 }, + { url = "https://files.pythonhosted.org/packages/f6/96/c098a6044d119c751ceede7be91035fa8310170ec24a6523aff72f0a5793/coverage-7.15.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:63022c4c8dec1d0342f05c3ede99842fe3d007689acc45e86f123a1746e4a026", size = 254934 }, + { url = "https://files.pythonhosted.org/packages/b2/a2/1457b3a7a50c8d77500103b97a046db863e2f59a1cf6d2f814595f349885/coverage-7.15.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6c0be82b4d4aa5b2704e08518e2252f3e3d110164bcca826816801052e48a7aa", size = 252898 }, + { url = "https://files.pythonhosted.org/packages/6c/0e/76958874c471ecfcdde0d2b2747bb2c61bdbf34a40636f4ce9db9923e643/coverage-7.15.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4510fb9cdf6bb02dfa6af0be4a534b8102d086e22e4a33f8836df663da3d660d", size = 257056 }, + { url = "https://files.pythonhosted.org/packages/7c/7c/3d7c4e3bf58baa40327dc7edc2272b17cf02299366d52763db1b0ca1556a/coverage-7.15.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:42ec3d989421b174a2ab607c1539f24127ad362757b7f1c0c0d7a2993f7eb37b", size = 252718 }, + { url = "https://files.pythonhosted.org/packages/c8/b8/1cecffed9ce14fb25be9ba42d37b6bb61485c9a3ddd43cd3dde36b6087d8/coverage-7.15.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e8f91bce78e32343af184c3b7fa28fcf5a9e2641f4b6623d392038f804939188", size = 254490 }, + { url = "https://files.pythonhosted.org/packages/6c/2c/42984561bc7f4c045dca67516a0c50ee5ef8d84352dbeb5559dc86c4823e/coverage-7.15.2-cp313-cp313-win32.whl", hash = "sha256:434e68d531858205895eb0d74b73d20b84260de426387d53c422a5acda2cf050", size = 223647 }, + { url = "https://files.pythonhosted.org/packages/41/9f/39c7c9245efc583beddf89a87683574e663ed93637f3afb6cd7b88405676/coverage-7.15.2-cp313-cp313-win_amd64.whl", hash = "sha256:26c3b04a6377fd7c09800921fa934e3a17c0020439cd59df73e73ae1d4b6a78c", size = 224190 }, + { url = "https://files.pythonhosted.org/packages/c7/de/3a2883cf8a213659280ef4b403059e17a9acaeb7fc7fd4105e1226ff2e6d/coverage-7.15.2-cp313-cp313-win_arm64.whl", hash = "sha256:3ed010aa1b69cda8e827aabfca9866216c980e2dca82ab9a78c5f83689964c8b", size = 223583 }, + { url = "https://files.pythonhosted.org/packages/81/5f/aed265fd7a3551a394f36dfe41868aee709b7f95db4052205b4ad1563ac3/coverage-7.15.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:40f633c5c5fc783732f6312280122e859538fa24461235597c13d803ea9a108a", size = 221650 }, + { url = "https://files.pythonhosted.org/packages/6b/2c/222ba12a545189017120f8eddfc1a0bd4616b47d5d4a8d99421edb2fe4c6/coverage-7.15.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:075560438765b7a2ef43bf7aa7758661b53d889df47f062a31bda6c1ade553a2", size = 221988 }, + { url = "https://files.pythonhosted.org/packages/aa/38/304b5877ab46e6c290b4292cfcf3fe28245f0e5597cad7f6acc91fc7e0a4/coverage-7.15.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:25fd15dd40a0a2c51a500d664ca29053c09c3259d998407bf982b6e114696138", size = 253029 }, + { url = "https://files.pythonhosted.org/packages/6c/58/821b533b8db9e44cf1d8a97bd525149ced40dde1d0093da02cb78e715244/coverage-7.15.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b9a6367e4aff723e8ee8190836836124284e8fcd4265e307c844010cfa074f3f", size = 255536 }, + { url = "https://files.pythonhosted.org/packages/f1/f2/7aa06604c389d32ea7f0a6a988359a7eafc3cd3f8e7bc2e88cd2fdf0b877/coverage-7.15.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9854ca62c152874b2060772503535be2e8f53f70b8aaa7686b094888d872f984", size = 256881 }, + { url = "https://files.pythonhosted.org/packages/a2/4f/1ef342339c7916d0096bc5888cc0f653882cc7bc8f897d5cb89143287c9b/coverage-7.15.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:913b6c56e110da40e035bbd168353bf7aaa2544a5eaccea5d98a4629aac156c7", size = 259196 }, + { url = "https://files.pythonhosted.org/packages/fe/f4/7ed055d7a9c5ec13b161773a115a5ccc6b0081d568c31fad830806306cc7/coverage-7.15.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aaccad4129d735a8a4d526f26929894c9a4e8ef7034566f210b176749d6906e3", size = 253036 }, + { url = "https://files.pythonhosted.org/packages/14/79/ea82cca18c242a3a38b6c017da39726aa62dcb64aa635abf79b92009975c/coverage-7.15.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a164b50081fc7357331c4024ef4d17b78ba325f8380d05f5a69599a7e05257ee", size = 254887 }, + { url = "https://files.pythonhosted.org/packages/a4/ba/a136db3c0d9562b00e10b72540dbf3a33cd3bc5b95060c9308e247494623/coverage-7.15.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:bfd341ccf78128e72c094bc70cc25b3ef309c33c7c2c66ba3ed4309549e02de1", size = 252852 }, + { url = "https://files.pythonhosted.org/packages/17/17/ea334246b16b7d059953fad6fdefa11e33c68efbd3fe37b1098120a1fac2/coverage-7.15.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:1473b3ba8e7ee0f076117b1a72c23f579a2b9e2bb742f48a8d86ea27ca93f91a", size = 257128 }, + { url = "https://files.pythonhosted.org/packages/ed/c3/074fb66d46d607855f710876b117cbda562c5ab08363528e78820449f937/coverage-7.15.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:17c432b5f73ad52ef46fb06019f6fa7c66ce381961cf0f7dfd1d3a4bd3a98145", size = 252668 }, + { url = "https://files.pythonhosted.org/packages/e1/c1/f620850ada9b36435921c9a3a8057013422b1d964eb4bf37fe138724d192/coverage-7.15.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:77f0ef5011df53a4bd1b35211ab122287f8d9b8d7aa1c4553e5c2deb24b1d446", size = 254325 }, + { url = "https://files.pythonhosted.org/packages/cc/31/a729ca3689404493af82ef8e6ff70bd88bdda8da89aeef6ca9b387aeb2b4/coverage-7.15.2-cp314-cp314-win32.whl", hash = "sha256:f653e5d7248c1191ec988a85c72edeab46c3ff44f90639a4ed4874ec0be90243", size = 223844 }, + { url = "https://files.pythonhosted.org/packages/c6/83/5d809dc808fb1698c671f3e372259bb9158e64b7ea526fc6ab7de64de9fe/coverage-7.15.2-cp314-cp314-win_amd64.whl", hash = "sha256:9911f31aad8906abe337c271343485cf20df5e70df5d2f57f9f136e7b55f26bc", size = 224331 }, + { url = "https://files.pythonhosted.org/packages/16/4e/35e488548e952795829e129995c4174df33bf432b591d1aa42c8d9e4e7ad/coverage-7.15.2-cp314-cp314-win_arm64.whl", hash = "sha256:e38def96ad59853824c97953fdcd2c320a84ba3ce99b417db78af8bb6c3db635", size = 223760 }, + { url = "https://files.pythonhosted.org/packages/ed/49/dd2c86cd6374038f6e415fb5bfb86db5218553209c081384a020369dee79/coverage-7.15.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:835ec4e20b45f0a7f63ed78f94065aca00de033403df8377bfe8b9c6abc0a7be", size = 222384 }, + { url = "https://files.pythonhosted.org/packages/d3/74/173ff17a1c0808e5a438f549f6f145d5ac7528f2791310b63523e3200ac7/coverage-7.15.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7466cc7ab6dc0db871d264bf99e8779f0917ee63d40730af0552f71535a6e072", size = 222647 }, + { url = "https://files.pythonhosted.org/packages/84/f8/b8cba872162356fb44ac79c10309d987206a4461e32072fc29228dad7331/coverage-7.15.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e370c12133095ff18432de8c044962be85a5a96d90c6fcbce8e17e76236d2328", size = 264013 }, + { url = "https://files.pythonhosted.org/packages/ee/67/a807a7586d0b8cae485308ddd55756f0806c92f8e0b411bacbf23c48edf3/coverage-7.15.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fe41909c9515c3bfdb5f02c4d1f857dba322d9a9a1178069b91eea77889df63a", size = 266135 }, + { url = "https://files.pythonhosted.org/packages/ce/67/cd78771dc985f7e4ebdcc82b1a96d9a932af9e806f01f2f91a89f4c72e80/coverage-7.15.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6aa28cfb6488e5453b5b762d65f73aa586380f6693a04d58078ce228a29b06c0", size = 268555 }, + { url = "https://files.pythonhosted.org/packages/18/3e/10134cf81275188c58568f324fc74aedff32c63ca4d5bbc513a91944a6f0/coverage-7.15.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bcc0aae933921d03096f53b0b03eeb702129fd406dee59f08d2efacc68681fa5", size = 269674 }, + { url = "https://files.pythonhosted.org/packages/75/4a/771b77de446cba985dc414bbc5844bd21604da05dbc044286df8318a48a7/coverage-7.15.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7c63387e21ab21f512c69c9756a8c7dadd322c7275edb064064433c9a09c3743", size = 263101 }, + { url = "https://files.pythonhosted.org/packages/5f/b5/70a7011da15f4071943361183aefa27847f3e3aec4fd335f1cb3d3a622b1/coverage-7.15.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e55510bc98ae943cece9e667a6c0fe94c6a92913720dea34243657a17993d0c", size = 266007 }, + { url = "https://files.pythonhosted.org/packages/b4/0d/f9547e804ce7ad49646ffeffac26699510efbe6c0f751b66fdc960c4e825/coverage-7.15.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:2ff08701be2d1556fc78b326c80a3e8042da09352ecb3819105f8e386c8a3071", size = 263611 }, + { url = "https://files.pythonhosted.org/packages/ac/59/f576a396659c0efd351f5c1544f67c3560e89c7761cabf7f65e412beeda5/coverage-7.15.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:38c9518b7103826c403a461544e3c2e77151e8676d06eaed85911a97e962584a", size = 267344 }, + { url = "https://files.pythonhosted.org/packages/7c/5d/c2e4fce3579c0cb635024293f1a32bbe26df101b3e3a69f22243d1352b6c/coverage-7.15.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:dee88b1ed88587abd8c0269a1fc1f4cc77f7750d1dfde2869e2a123af420e67d", size = 262456 }, + { url = "https://files.pythonhosted.org/packages/bb/dd/956287d69436b66094bc4b57ac2da71e43bfd2a5524e958900b9f582fcf8/coverage-7.15.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2fbeeeecea279727f8ac16c8e1133ddfeee793e985c86ae343d6a5ce744eef8c", size = 264771 }, + { url = "https://files.pythonhosted.org/packages/2c/5a/6f979530c2734c575de77cf58f5f28d51f7123a94b5030fd9156fe5f363c/coverage-7.15.2-cp314-cp314t-win32.whl", hash = "sha256:cb0fddaa6884be6aae36ced9544b5e90f7d5f03845a2853bf47a14953a4e8688", size = 224151 }, + { url = "https://files.pythonhosted.org/packages/54/7e/27f6b2a74d484742f4017553e710b01e396b23d809df3e95ca0bb9a2824b/coverage-7.15.2-cp314-cp314t-win_amd64.whl", hash = "sha256:77f091ea3a9cc611cd29f433565476bc1936c084ac8eee00ea0e7e70c27e4199", size = 224981 }, + { url = "https://files.pythonhosted.org/packages/b1/48/284863423aa474240f6842bd00d680da22f4e6ea2e466618ef7c9c9e69a9/coverage-7.15.2-cp314-cp314t-win_arm64.whl", hash = "sha256:6fc448c377d6eeb00a47c673494bd9bae29280ca53987e1869e67ebedfe20658", size = 224294 }, + { url = "https://files.pythonhosted.org/packages/ec/82/32e3bd191d498e64f6f911ad55d14006a0861e54869d2d32452326399e65/coverage-7.15.2-py3-none-any.whl", hash = "sha256:eb6bcae8d1a9d305351ecb108232441d11c5cfe9de840a04388ba5d2db8d735c", size = 213375 }, ] [package.optional-dependencies] @@ -681,7 +749,7 @@ toml = [ [[package]] name = "crawl4ai" -version = "0.9.0" +version = "0.9.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiofiles" }, @@ -718,9 +786,9 @@ dependencies = [ { name = "unclecode-litellm" }, { name = "xxhash" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/52/37/9b288598bb0049b918eeb00f029592c748465c8a3e819461e95d8b5f9dab/crawl4ai-0.9.0.tar.gz", hash = "sha256:00c1c3516d9ca24a9b5004523ae67974ace8a5baadc6e92e295afca73f077153", size = 591734 } +sdist = { url = "https://files.pythonhosted.org/packages/fc/7f/b32541be7b0a4d69574054a07d05c3c36aa1a935914a661b26099db34d4a/crawl4ai-0.9.2.tar.gz", hash = "sha256:58dbfa05a82c1cfa667a20383a1d0f7a42187304da5e4d0661a6f59b0ed6a406", size = 577156 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c9/19/2d634f26d9ad1281874c503c1043174042fef9af631dc10798f4f22c1544/crawl4ai-0.9.0-py3-none-any.whl", hash = "sha256:1296072d9cd92e79144c6823ed6f27ab3966f16ce44e79fcbe86943b3435c366", size = 499084 }, + { url = "https://files.pythonhosted.org/packages/e0/fe/84960a1747d64d12b4a0d6d9954ee919e44a92dd62e64152552c4e5bf437/crawl4ai-0.9.2-py3-none-any.whl", hash = "sha256:4efb2d0688aa3d66b48721a9031f7257bd2acb52b78d0a89d072741ac685f3f8", size = 480585 }, ] [[package]] @@ -844,6 +912,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/66/66/150e406a2db5535533aa3c946de58f0371f2e412e23f050c704588023e6e/cymem-2.0.13-cp314-cp314t-win_arm64.whl", hash = "sha256:e9027764dc5f1999fb4b4cabee1d0322c59e330c0a6485b436a68275f614277f", size = 39715 }, ] +[[package]] +name = "defusedxml" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604 }, +] + [[package]] name = "deprecation" version = "2.1.0" @@ -903,14 +980,14 @@ wheels = [ [[package]] name = "faker" -version = "40.11.0" +version = "40.31.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "tzdata", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/94/dc/b68e5378e5a7db0ab776efcdd53b6fe374b29d703e156fd5bb4c5437069e/faker-40.11.0.tar.gz", hash = "sha256:7c419299103b13126bd02ec14bd2b47b946edb5a5eedf305e66a193b25f9a734", size = 1957570 } +sdist = { url = "https://files.pythonhosted.org/packages/74/e1/adf05724d5838fa9fe2ada64bf390bbf37b77e3714189ae9f17a2c6d0470/faker-40.31.0.tar.gz", hash = "sha256:af163a937aec99dca5abaeb94dd5008c51c26c6e9af1a26c8db4b3c4e7ca4403", size = 2024136 } wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/fa/a86c6ba66f0308c95b9288b1e3eaccd934b545646f63494a86f1ec2f8c8e/faker-40.11.0-py3-none-any.whl", hash = "sha256:0e9816c950528d2a37d74863f3ef389ea9a3a936cbcde0b11b8499942e25bf90", size = 1989457 }, + { url = "https://files.pythonhosted.org/packages/e0/f5/e427b19d79e241a51ce6fcd69a47e7a9ca791a8bc3da00d91d5be05e1456/faker-40.31.0-py3-none-any.whl", hash = "sha256:bedc97c292a48a6a1bbe471a9076d7395a196421ede1cedde99d5719f6b91027", size = 2061930 }, ] [[package]] @@ -967,11 +1044,11 @@ wheels = [ [[package]] name = "filelock" -version = "3.25.2" +version = "3.30.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/b8/00651a0f559862f3bb7d6f7477b192afe3f583cc5e26403b44e59a55ab34/filelock-3.25.2.tar.gz", hash = "sha256:b64ece2b38f4ca29dd3e810287aa8c48182bbecd1ae6e9ae126c9b35f1382694", size = 40480 } +sdist = { url = "https://files.pythonhosted.org/packages/02/f7/2165ef325da22d854b8f81ca4799395f2eb6afa55cdb52c7710f028b5336/filelock-3.30.2.tar.gz", hash = "sha256:1ea7c857465c897a4a6e64c1aace28ff6b83f5bc66c1c06ea148efa65bc2ec5d", size = 176823 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl", hash = "sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70", size = 26759 }, + { url = "https://files.pythonhosted.org/packages/02/df/05118016cad66cd0d7c9417b2d4fc245be35decc4c36810f3c8dbf729d88/filelock-3.30.2-py3-none-any.whl", hash = "sha256:a64b58f75048ec39589983e97f5117163f822261dcb6ba843e098f05aac9663f", size = 94092 }, ] [[package]] @@ -1081,16 +1158,16 @@ wheels = [ [[package]] name = "fsspec" -version = "2026.2.0" +version = "2026.6.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 } +sdist = { url = "https://files.pythonhosted.org/packages/10/a1/ae4e3e5003468d6391d2c77b6fa1cd73bd5d13511d81c642d7b28ac90ed4/fsspec-2026.6.0.tar.gz", hash = "sha256:f5bac145310fe30e16e1471bd6840b2d990d609e872251d7e674241822abf01a", size = 313646 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl", hash = "sha256:98de475b5cb3bd66bedd5c4679e87b4fdfe1a3bf4d707b151b3c07e58c9a2437", size = 202505 }, + { url = "https://files.pythonhosted.org/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl", hash = "sha256:02e0b71817df9b2169dc30a16832045764def1191b43dcff5bb85bdee212d2a1", size = 203949 }, ] [[package]] name = "gguf" -version = "0.18.0" +version = "0.19.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, @@ -1098,73 +1175,106 @@ dependencies = [ { name = "requests" }, { name = "tqdm" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3f/26/7622a41c39db9d7090225a4bf8368550e59694dcf7313b44f9a82b501209/gguf-0.18.0.tar.gz", hash = "sha256:b4659093d5d0dccdb5902a904d54b327f4052879fe5e90946ad5fce9f8018c2e", size = 107170 } +sdist = { url = "https://files.pythonhosted.org/packages/48/ae/17f1308ae45cd7b08ebb521747d5b23f4efc4d172038a4e228dd5106c3ff/gguf-0.19.0.tar.gz", hash = "sha256:dbadcd6cc7ccd44256f2229fe7c2dff5e8aa5cf0612ab987fd2b1a57e428923f", size = 111220 } wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/0c/e0f1eae7535a97476fb903f65301e35da2a66182b8161066b7eb312b2cb8/gguf-0.18.0-py3-none-any.whl", hash = "sha256:af93f7ef198a265cbde5fa6a6b3101528bca285903949ab0a3e591cd993a1864", size = 114244 }, + { url = "https://files.pythonhosted.org/packages/b3/bb/d71d6da82763528c2c2ed6b59a9d6142c6595545a4c448e2085d155e88c2/gguf-0.19.0-py3-none-any.whl", hash = "sha256:70bcd10edfe697fb2dad6e40af2234b9d8ece9a41a99761405121ebda1c3c1cd", size = 118475 }, ] [[package]] name = "graspologic-native" -version = "1.2.5" +version = "1.3.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/25/2d/62b30d89533643ccf4778a18eb023f291b8877b5d85de3342f07b2d363a7/graspologic_native-1.2.5.tar.gz", hash = "sha256:27ea7e01fa44466c0b4cdd678d4561e5d3dc0cb400015683b7ae1386031257a0", size = 2512729 } +dependencies = [ + { name = "numpy" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "scipy", version = "1.18.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/25/607bcadd51954aa98892e44d1536560abb06a81ff48288982bd8303983ee/graspologic_native-1.3.1.tar.gz", hash = "sha256:fa6be9c292508b8a876d45ac98ee550534d9e33fcf7129a6df8a95573101be4c", size = 2655991 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/86/10748f4c474b0c8f6060dd379bb0c4da5d42779244bb13a58656ffb44a03/graspologic_native-1.2.5-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:bf05f2e162ae2a2a8d6e8cfccbe3586d1faa0b808159ff950478348df557c61e", size = 648437 }, - { url = "https://files.pythonhosted.org/packages/42/cc/b75ea35755340bedda29727e5388390c639ea533f55b9249f5ac3003f656/graspologic_native-1.2.5-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a7fff06ed49c3875cf351bb09a92ae7cbc169ce92dcc4c3439e28e801f822ae", size = 352044 }, - { url = "https://files.pythonhosted.org/packages/8e/55/15e6e4f18bf249b529ac4cd1522b03f5c9ef9284a2f7bfaa1fd1f96464fe/graspologic_native-1.2.5-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53e7e993e7d70fe0d860773fc62812fbb8cb4ef2d11d8661a1f06f8772593915", size = 364644 }, - { url = "https://files.pythonhosted.org/packages/3b/51/21097af79f3d68626539ab829bdbf6cc42933f020e161972927d916e394c/graspologic_native-1.2.5-cp38-abi3-win_amd64.whl", hash = "sha256:c3ef2172d774083d7e2c8e77daccd218571ddeebeb2c1703cebb1a2cc4c56e07", size = 210438 }, + { url = "https://files.pythonhosted.org/packages/01/f7/c3642c95d7c3f60ade4a7b1d583bb76e9410e9ffe4185701ac87b533ccf2/graspologic_native-1.3.1-cp39-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:72200aa468481d78d5b26735030c98f4a9e3388930c93043e1c01ef9fb2a0a62", size = 730640 }, + { url = "https://files.pythonhosted.org/packages/ee/d6/79475dec15daa002c066494283baac911b65d7956ed6a22c5f685c20987e/graspologic_native-1.3.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f4319b6d7f9e01d52986fbc13ffcf50e262351e9c78b0a3453c9130cdd1779c", size = 403084 }, + { url = "https://files.pythonhosted.org/packages/8d/73/5bcb4928f973996465cbd5dc1b33f9cbfbf1227d08fb3e0b8e82bb485a95/graspologic_native-1.3.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f622699db442f55fe754e44abef2586ce389a9e47e049208e4b324edbef9131", size = 417429 }, + { url = "https://files.pythonhosted.org/packages/c4/b7/0c2774a0f566bbb58f23ab450e4a3b89221b79e0c7aff5736c887c16d78b/graspologic_native-1.3.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7fc1fd3fb6ee65c940053774841f6c74d3b88a8ab7afb92d5ac8cc09241394ff", size = 471428 }, + { url = "https://files.pythonhosted.org/packages/5f/25/887e991b4466c6ca07d6670f94b1382df87b302c0c996bc4e0ddd05ddc43/graspologic_native-1.3.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:279d922d977cd4a2cb70590a73fe589bb527e1171448d040b5e2b47e6aa58432", size = 619869 }, + { url = "https://files.pythonhosted.org/packages/64/61/f324b76d007eb765792acba95a39f9c0e749d2338049bc3a67b5adc75f65/graspologic_native-1.3.1-cp39-abi3-win_amd64.whl", hash = "sha256:85dfb19cedd7b9ca627b3d1f283d3270af0a3c2e8378c9e61fe1f56d09a2a1a0", size = 260864 }, + { url = "https://files.pythonhosted.org/packages/37/f6/c6fda035cb637e3fe205cc58c555ea68a091d519f7527b7d1351869cb0cd/graspologic_native-1.3.1-cp39-abi3-win_arm64.whl", hash = "sha256:8a631a522b597ff5cf1907efc9b03eb1bc268d8702cf0777efb85769dff29dab", size = 246883 }, ] [[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 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/47/16400cb42d18d7a6bb46f0626852c1718612e35dcb0dffa16bbaffdf5dd2/greenlet-3.3.2-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:c56692189a7d1c7606cb794be0a8381470d95c57ce5be03fb3d0ef57c7853b86", size = 278890 }, - { url = "https://files.pythonhosted.org/packages/a3/90/42762b77a5b6aa96cd8c0e80612663d39211e8ae8a6cd47c7f1249a66262/greenlet-3.3.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ebd458fa8285960f382841da585e02201b53a5ec2bac6b156fc623b5ce4499f", size = 581120 }, - { url = "https://files.pythonhosted.org/packages/bf/6f/f3d64f4fa0a9c7b5c5b3c810ff1df614540d5aa7d519261b53fba55d4df9/greenlet-3.3.2-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a443358b33c4ec7b05b79a7c8b466f5d275025e750298be7340f8fc63dff2a55", size = 594363 }, - { url = "https://files.pythonhosted.org/packages/9c/8b/1430a04657735a3f23116c2e0d5eb10220928846e4537a938a41b350bed6/greenlet-3.3.2-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4375a58e49522698d3e70cc0b801c19433021b5c37686f7ce9c65b0d5c8677d2", size = 605046 }, - { url = "https://files.pythonhosted.org/packages/72/83/3e06a52aca8128bdd4dcd67e932b809e76a96ab8c232a8b025b2850264c5/greenlet-3.3.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e2cd90d413acbf5e77ae41e5d3c9b3ac1d011a756d7284d7f3f2b806bbd6358", size = 594156 }, - { url = "https://files.pythonhosted.org/packages/70/79/0de5e62b873e08fe3cef7dbe84e5c4bc0e8ed0c7ff131bccb8405cd107c8/greenlet-3.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:442b6057453c8cb29b4fb36a2ac689382fc71112273726e2423f7f17dc73bf99", size = 1554649 }, - { url = "https://files.pythonhosted.org/packages/5a/00/32d30dee8389dc36d42170a9c66217757289e2afb0de59a3565260f38373/greenlet-3.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:45abe8eb6339518180d5a7fa47fa01945414d7cca5ecb745346fc6a87d2750be", size = 1619472 }, - { url = "https://files.pythonhosted.org/packages/f1/3a/efb2cf697fbccdf75b24e2c18025e7dfa54c4f31fab75c51d0fe79942cef/greenlet-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e692b2dae4cc7077cbb11b47d258533b48c8fde69a33d0d8a82e2fe8d8531d5", size = 230389 }, - { url = "https://files.pythonhosted.org/packages/e1/a1/65bbc059a43a7e2143ec4fc1f9e3f673e04f9c7b371a494a101422ac4fd5/greenlet-3.3.2-cp311-cp311-win_arm64.whl", hash = "sha256:02b0a8682aecd4d3c6c18edf52bc8e51eacdd75c8eac52a790a210b06aa295fd", size = 229645 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/9b/40/cc802e067d02af8b60b6771cea7d57e21ef5e6659912814babb42b864713/greenlet-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:34308836d8370bddadb41f5a7ce96879b72e2fdfb4e87729330c6ab52376409f", size = 231081 }, - { url = "https://files.pythonhosted.org/packages/58/2e/fe7f36ff1982d6b10a60d5e0740c759259a7d6d2e1dc41da6d96de32fff6/greenlet-3.3.2-cp312-cp312-win_arm64.whl", hash = "sha256:d3a62fa76a32b462a97198e4c9e99afb9ab375115e74e9a83ce180e7a496f643", size = 230331 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/91/39/5ef5aa23bc545aa0d31e1b9b55822b32c8da93ba657295840b6b34124009/greenlet-3.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:a7945dd0eab63ded0a48e4dcade82939783c172290a7903ebde9e184333ca124", size = 230961 }, - { url = "https://files.pythonhosted.org/packages/62/6b/a89f8456dcb06becff288f563618e9f20deed8dd29beea14f9a168aef64b/greenlet-3.3.2-cp313-cp313-win_arm64.whl", hash = "sha256:394ead29063ee3515b4e775216cb756b2e3b4a7e55ae8fd884f17fa579e6b327", size = 230221 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/f3/ca/2101ca3d9223a1dc125140dbc063644dca76df6ff356531eb27bc267b446/greenlet-3.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:8c4dd0f3997cf2512f7601563cc90dfb8957c0cff1e3a1b23991d4ea1776c492", size = 232034 }, - { url = "https://files.pythonhosted.org/packages/f6/4a/ecf894e962a59dea60f04877eea0fd5724618da89f1867b28ee8b91e811f/greenlet-3.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:cd6f9e2bbd46321ba3bbb4c8a15794d32960e3b0ae2cc4d49a1a53d314805d71", size = 231437 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/29/4b/45d90626aef8e65336bed690106d1382f7a43665e2249017e9527df8823b/greenlet-3.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c04c5e06ec3e022cbfe2cd4a846e1d4e50087444f875ff6d2c2ad8445495cf1a", size = 237086 }, +version = "3.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e2/f1/fbbfef6af0bad0548f09bc28948ea3c275b4edb19e17fc5ca9900a6a634d/greenlet-3.5.3.tar.gz", hash = "sha256:a61efc018fd3eb317eeca31aba90ee9e7f26f22884a79b6c6ec715bf71bb62f1", size = 200270 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/58/5404031044f55afad7aad1aff8be3f22b1bed03e237cfeabbc7e5c8cfde0/greenlet-3.5.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:aca9b4ce85b152b5524ef7d88170efdff80dc0032aa8b75f9aaf7f3479ea95b4", size = 287424 }, + { url = "https://files.pythonhosted.org/packages/b4/bf/1c65e9b94a54d547068fa5b5a8a06f221f3316b48908e08668d29c77cb50/greenlet-3.5.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f71be4920368fe1fabeeaa53d1e3548337e2b223d9565f8ad5e392a75ba23fc", size = 606523 }, + { url = "https://files.pythonhosted.org/packages/b8/c7/b66baacc95775ad511287acb0137b95574a9ce5491902372b7564799d790/greenlet-3.5.3-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d77e67f65f98449e3fb83f795b5d0a8437aead2f874ca89c96576caf4be3af6", size = 618315 }, + { url = "https://files.pythonhosted.org/packages/b0/a0/68afd1ebad40db87dac0a28ffa120726b98bf9c7c40c481b0f63c105d298/greenlet-3.5.3-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e18619ba655ac05d78d80fc83cac4ba892bd6927b99e3b8237aee861aaacc8bb", size = 626155 }, + { url = "https://files.pythonhosted.org/packages/78/2b/28ed29463522fdbe4c15b1f63922041626a7478316b34ab4adda3f0a4aba/greenlet-3.5.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8540f1e6205bd13ca0ce685581037219ca54a1b41a0a15d228c6c9b8ad5903d7", size = 617381 }, + { url = "https://files.pythonhosted.org/packages/07/7f/e327d912239ec4b3b49999e3967389bcf1ee8722b9ee9194d2752ecd558a/greenlet-3.5.3-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:d27c0c653a60d9535f690226474a5cc1036a8b0d7b57504d1c4f89c44a07a80c", size = 421083 }, + { url = "https://files.pythonhosted.org/packages/2a/7b/ad04e9d1337fc04965dc9fc616b6a72cb65a24b800a014c011ec812f5489/greenlet-3.5.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7ef56fe650f50575bf843acde967b9c567687f3c22340941a899b7bc56e956a8", size = 1577771 }, + { url = "https://files.pythonhosted.org/packages/d8/33/6c87ab7ba663f70ca21f3022aad1ffe56d3f3e0521e836c2415e13abcc3c/greenlet-3.5.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5121af01cf911e70056c00d4b46d5e9b5d1415550038573d744138bacb59e6b8", size = 1644048 }, + { url = "https://files.pythonhosted.org/packages/1c/35/f0d8ee998b422cf8693b270f098e55d8d4ec8006b061b333f54f177d28d9/greenlet-3.5.3-cp311-cp311-win_amd64.whl", hash = "sha256:0f41e4a05a3c0cb31b17023eff28dd111e1d16bf7d7d00406cd7df23f31398a7", size = 239137 }, + { url = "https://files.pythonhosted.org/packages/fb/96/b9820295576ef18c9edc404f10e260ae7215ceaf3781a54b720ed2627862/greenlet-3.5.3-cp311-cp311-win_arm64.whl", hash = "sha256:ec6f1af59f6b5f3fc9678e2ea062d8377d22ac644f7844cb7a292910cf12ff44", size = 237630 }, + { url = "https://files.pythonhosted.org/packages/5d/6e/4c37d51a2b7f82d2ff11bb6b5f7d766d9a011726624af255e843727627a3/greenlet-3.5.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:719757059f5a53fd0dde23f78cffeafcdd97b21c850ddb7ca684a3c1a1f122e2", size = 288685 }, + { url = "https://files.pythonhosted.org/packages/7a/73/815dd90131c1b71ebdf53dbc7c276cafec2a1173b97559f97aba72724a87/greenlet-3.5.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:efa9f765dd09f9d0cdac651ffdf631ee59ec5dc6ee7a73e0c012ba9c52fbdf5b", size = 604761 }, + { url = "https://files.pythonhosted.org/packages/9f/57/079cfe76bcef36b153b25607ee91c6fcb58f17f8b23c86bbbeabe0c88d72/greenlet-3.5.3-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7faba15ac005376e02a0384504e0243be3370ce010296a44a820feb342b505ab", size = 617044 }, + { url = "https://files.pythonhosted.org/packages/fb/fb/d97dc261209c80744b7c8132693a30d70ec6e7315e632cb0a10b3fec94dd/greenlet-3.5.3-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5795cd1101371140551c645f2d408b8d3c01a5a29cf8a9bce6e759c983682d23", size = 622351 }, + { url = "https://files.pythonhosted.org/packages/37/87/b4d095775a3fb1bcafbb483fc206b27ebb785724c83051447737085dc54e/greenlet-3.5.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:87142215824be6ac05e2e8e2786eec307ccbc27c36723c3881959df654af6861", size = 614244 }, + { url = "https://files.pythonhosted.org/packages/8e/ac/e5fee13cbbd0e8de312d9a146584b8a51891c68847330ef9dc8b5109d23f/greenlet-3.5.3-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:af4923b3096e26a36d7e9cf24ab88083a20f97d191e3b97f253731ce9b41b28c", size = 425395 }, + { url = "https://files.pythonhosted.org/packages/8a/70/7559b609683650fa2b95b8ab84b4ab0b26556a635d19675e12aa832d826d/greenlet-3.5.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:215275b1b49320987352e6c1b054acca0064f965a2c66992bed9a6f7d913f149", size = 1574210 }, + { url = "https://files.pythonhosted.org/packages/ae/73/be55392074c60fc37655ca40fa6022457bfbf6718e9e342a7b0b41f96dd2/greenlet-3.5.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6b1b0eed82364b0e32c4ea0f221452d33e6bb17ae094d9f72aed9851812747ea", size = 1638627 }, + { url = "https://files.pythonhosted.org/packages/14/40/c57489acf8e37d74e2913d4eff63aa0dba17acccc4bdeef874dde2dbbec9/greenlet-3.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:cde8adafa2365676f74a979744629589999093bc86e2484214f58e61df08902c", size = 239882 }, + { url = "https://files.pythonhosted.org/packages/71/fd/6fea0e3d6600f785069481ee637e09378dd4118acdfd38ad88ae2db31c98/greenlet-3.5.3-cp312-cp312-win_arm64.whl", hash = "sha256:c4e7b79d83805475f0102008843f6eb45fd3bb0b2e88c774adab5fbaab27117d", size = 238211 }, + { url = "https://files.pythonhosted.org/packages/9b/ff/a620267401db30a50cc8450ee90730e2d4a85658c055c0e760d4ed47fb13/greenlet-3.5.3-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:c8d87c2134d871df96ecdea9cec7cbaab286dadab0f56476e57aaf9e8ac11550", size = 287609 }, + { url = "https://files.pythonhosted.org/packages/d6/fa/5401ac78021c826a25b6dde0c705e0a8f29b617509f9185a31dac15fbe1b/greenlet-3.5.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2d185dd1621757e70c3861cceffd5317ab4e7ed7eb09c82994828468527ade5", size = 607435 }, + { url = "https://files.pythonhosted.org/packages/e9/76/1dc144a2e56e65d36405078ed774224375ea520a1870a6e46e08bb4ac7bf/greenlet-3.5.3-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1c514a468149bf8fbbab874188a3535cd8a48a3e353eb53a3d424296f8dbacd3", size = 619787 }, + { url = "https://files.pythonhosted.org/packages/57/61/2f5b1adf256d039f5dab8005de8d3d7ad2b0070a3219c0e036b3fbfeb440/greenlet-3.5.3-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9ad04dd75458c6300b047c61b8639092433d205a25a14e310d6582a480efcca1", size = 625580 }, + { url = "https://files.pythonhosted.org/packages/bf/87/c298cee62df1de4ad7fec32abda73526cff347fd143a6ed4ac369246668a/greenlet-3.5.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:915f887cf2682b66419b879423a2e072634aa7b7dce6f3ada4957cfced3f1e9a", size = 616786 }, + { url = "https://files.pythonhosted.org/packages/3e/d9/ab7fc9e543e44d6879b0a6ef9a4b2188940fd180cc65d6f646883ddf7201/greenlet-3.5.3-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:afaabdd554cd7ae9bbb3ca070b0d7fdfd207dbf1d16865f7233837709d354bda", size = 427933 }, + { url = "https://files.pythonhosted.org/packages/9e/2e/e6f009885ed0705ccf33fe0583c117cfd03cde77e31a596dd5785a30762b/greenlet-3.5.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:766cfd421c13e450feb340cd472a3ed9957d438727b7b4593ad7c76c5d2b0deb", size = 1574316 }, + { url = "https://files.pythonhosted.org/packages/ef/fe/43fd110b01e40da0adb7c90ac7ea744bef2d43dca00de5095fd2351c2a68/greenlet-3.5.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2ecda9ec22edf38fa389369eaed8c3d37c05f3c54e69f69438dbb2cc1de1458b", size = 1638614 }, + { url = "https://files.pythonhosted.org/packages/0f/7c/062447147a61f8b4337b156fe70d32a165fcf2f89d7ca6255e572806705c/greenlet-3.5.3-cp313-cp313-win_amd64.whl", hash = "sha256:c82304750f057167ff60d188df1d0cc1764ce9567eadf03e6a7443bcedd0b30b", size = 239850 }, + { url = "https://files.pythonhosted.org/packages/c7/7e/220a7f5824a64a60443fc03b39dfac4ea63a7fb6d481efa27eafa928e7f4/greenlet-3.5.3-cp313-cp313-win_arm64.whl", hash = "sha256:dc133a1569ee667b2a6ef56ce551084aeefd87a5acbc4736d336d1e2edc6cfc4", size = 238141 }, + { url = "https://files.pythonhosted.org/packages/c3/93/43e116ee114b28737ba7e12952a0d4e2f55944d0f84e42bc91ba7192a3c9/greenlet-3.5.3-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:fd2e02fa07485778536a036222d616ab957b1d533f36b3ed98ce725d9c9d3117", size = 288202 }, + { url = "https://files.pythonhosted.org/packages/82/2f/146d218299046a43d1f029fd544b3d110d0f175a09c715c7e8da4a4a345d/greenlet-3.5.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df0a0628d1597eb0897b62f55d1343f772405fd25f3b2a796c76874b0c2e22e8", size = 654096 }, + { url = "https://files.pythonhosted.org/packages/a0/cc/04738cafb3f45fa991ea44f9de94c47dcec964f5a972300988a6751f49d9/greenlet-3.5.3-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ebd933a6adabc298bab47731a130fe6bfb888bd934eee37810f151159544540d", size = 666304 }, + { url = "https://files.pythonhosted.org/packages/86/a9/73fa62893d5b84b4205544e6b673c654cc43aa5b9899bac00f04d64af73d/greenlet-3.5.3-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8d19fe6c39ebff9259f07bcc685d3290f8fa4ea2278e51dd0008e4d6b0f2d814", size = 670657 }, + { url = "https://files.pythonhosted.org/packages/ce/aa/4e0dad5e605c270c784ab911c43da6adb136ccd4d81180f763ca429a723d/greenlet-3.5.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b9d501b40e80b70e32323c799dd9b420a5577a9601469d362ae1ffb690f3a7c", size = 663635 }, + { url = "https://files.pythonhosted.org/packages/29/7e/2ffce64929fb3cab7b65d5a0b20aaf9764e227681d731b041077fc9a525a/greenlet-3.5.3-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:962c5df2db8cb446da51edf1ca5296c389d93b99c9d8aa2ee4c7d0d8f1218260", size = 473497 }, + { url = "https://files.pythonhosted.org/packages/d1/50/13efdbea246fe3d3b735e191fec08fb50809f53cd2383ebe123d0809e44b/greenlet-3.5.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a1fad1d11e7d6aab184107baa8e4ece11ccba3ec9599cd7efa5ff4d70d43256a", size = 1621252 }, + { url = "https://files.pythonhosted.org/packages/f7/22/c0a336ae4a1410fd5f5121098e5bfbf1865f64c5ef80b4b5412886c4a332/greenlet-3.5.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fad5aec764399f1b5cc347ad250a59660f20c8f8888ea6bae1f93b769cce1154", size = 1684824 }, + { url = "https://files.pythonhosted.org/packages/7a/94/91aec0030bea75c4b3244251d0de60a1f3432d1ecb53ab6c437fb5c3ba61/greenlet-3.5.3-cp314-cp314-win_amd64.whl", hash = "sha256:7669aa24cf2a1041d6f7899575b494a3ab4cf68bfcc8609b1dc0be7272db835e", size = 240754 }, + { url = "https://files.pythonhosted.org/packages/e5/06/68d0983e79e02138f64b4d303c500c27ddb48e5e77f3debb80888a921eae/greenlet-3.5.3-cp314-cp314-win_arm64.whl", hash = "sha256:5b4807c4082c9d1b6d9eed56fcd041863e37f2228106eef24c30ca096e238605", size = 239549 }, + { url = "https://files.pythonhosted.org/packages/91/95/3e161213d7f1d378d15aa9e792093e9bfe01844680d04b7fd6e0107c9098/greenlet-3.5.3-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:271a8ea7c1024e8a0d7dd2be66dd66dda8a07193f41a17b9e924f7600f5b62be", size = 296389 }, + { url = "https://files.pythonhosted.org/packages/00/92/715c44721abe2b4d1ae9abde4179411868a5bff312479f54e105d372f131/greenlet-3.5.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19131729ae0ddc3c2e1ef85e650169b5e37ee32e400f215f78b94d7b0d567310", size = 653382 }, + { url = "https://files.pythonhosted.org/packages/a0/83/37a10372a1090a6624cca8e74c12df1a36c2dc36429ed0255b7fb1aeee23/greenlet-3.5.3-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1540dd8e5fc2a5aec40fbb98ef8e149fa47c89a4b4a1cf2575a14d3d1869d7a8", size = 659401 }, + { url = "https://files.pythonhosted.org/packages/cb/73/8faec206b851c22b1733545fda900829a1f3f5b1c78ae7e0fb3dba57d9f4/greenlet-3.5.3-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b897d97759425953f69a9c0fac67f8fe333ec0ce7377ef186fb2b0c3ad5e354d", size = 659582 }, + { url = "https://files.pythonhosted.org/packages/db/e2/d1509cad4207da559cc42986ecdd8fc67ad0d1bba2bf03023c467fd5e0f3/greenlet-3.5.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e81fa194a1d20967877bdf9c7794db2bc99063e5be36aee710c08f04c5bb087f", size = 656969 }, + { url = "https://files.pythonhosted.org/packages/b4/55/50c19e49f8045834ada71ef12f8ad048eba8517c6aa41161bed676328fae/greenlet-3.5.3-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:3236754d423955ea08e9bb5f6c04a7895f9e22c290b66aa7653fcb922d839eb0", size = 491037 }, + { url = "https://files.pythonhosted.org/packages/86/7d/eaf70de20aadca3a5884aec58362861c64ce45e7b277f47ed026926a3b89/greenlet-3.5.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:55cf4d777485d43110e47133cbba6d74a8885a87ec1227ef0267f9ee80c5aa21", size = 1617822 }, + { url = "https://files.pythonhosted.org/packages/8a/f9/414d38fc400ae4350d4185eaad1827676f7cf5287b9136e0ed1cbbe20a7f/greenlet-3.5.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:12a248ba75f6a9a236375f52296c498c89ff1d8badf32deb9eca7abd5853f7da", size = 1677983 }, + { url = "https://files.pythonhosted.org/packages/e4/15/7edb977e08f9bff702fe42d6c902702786ff6b9694058b4e6a2a6ac90e57/greenlet-3.5.3-cp314-cp314t-win_amd64.whl", hash = "sha256:efc6bd60ea02e085862c74a3ef64b147ffc6f1a5ea7d9f26e7a939943f68c1e3", size = 243626 }, + { url = "https://files.pythonhosted.org/packages/2c/8a/93928dce91e6b3598b5e779e8d1fd6576a504640c58e78627077f6a7a91a/greenlet-3.5.3-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:ea03f2f04367845d6b58eeed276e1e56e51f0b97d8ad5a88a7d20a91dc9056cc", size = 288860 }, + { url = "https://files.pythonhosted.org/packages/4f/ca/69db42d447a1378043e2c8f19c09cbbd1263371505053c496b49066d3d16/greenlet-3.5.3-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78dbef602fda6d97d957eb7937f70c9ce9e9527330347f8f6b6f9e554a9e7a47", size = 659747 }, + { url = "https://files.pythonhosted.org/packages/a8/0b/af7ac2ef8dd41e3da1a40dda6305c23b9a03e13ba975ec916357b50f8575/greenlet-3.5.3-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f73857adb8fee13fa56c172bd11262f888c0c648f9fea113e777bb2c7904a81", size = 670419 }, + { url = "https://files.pythonhosted.org/packages/25/aa/952cf28c2ff949a8c971134fb43854dd7eaa737218723aaef758f8c9aead/greenlet-3.5.3-cp315-cp315-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cefa9cef4b371f9844c6053db71f1138bc6807bab1578b0dae5149c1f1141357", size = 674261 }, + { url = "https://files.pythonhosted.org/packages/51/1e/1d51640cacbfc455dbe9f9a9f594c49e4e244f63b9971a2f4764e46cc53d/greenlet-3.5.3-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:232fec92e823addaf02d9472cf7381e24a1d046a6ced1103c5caa4c21b9dfc1d", size = 668787 }, + { url = "https://files.pythonhosted.org/packages/dc/f2/b00d6f5e63e531a93562b2ec1a4c320fbee91f580fc42e6417af69d706e5/greenlet-3.5.3-cp315-cp315-manylinux_2_39_riscv64.whl", hash = "sha256:6219b6d04dbf6ba6084d77dc609e8473060dc55f759cbf626d512122781fa128", size = 480322 }, + { url = "https://files.pythonhosted.org/packages/21/66/4030d5b0b5894500023f003bb054d9bb354dfbd1e186c3a296759172f5f5/greenlet-3.5.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:2421c3564da9429d5586d46ca31ebb26516b5498a802cf65c041a8e8a8980d34", size = 1626305 }, + { url = "https://files.pythonhosted.org/packages/0e/50/5221371c7550108dfa3c378debc41d032aa9c78e89abb01d8011cfc93289/greenlet-3.5.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e0f0d160f0b2e558e6c75f7930967183255dc9735e5f5b8cae58ee09c9576d8b", size = 1688631 }, + { url = "https://files.pythonhosted.org/packages/68/5d/00d469daae3c65d2bf620b10eee82eb022127d483c6bc8c69fae6f3fbf17/greenlet-3.5.3-cp315-cp315-win_amd64.whl", hash = "sha256:dd99329bbc15ca78dcc583dba05d0b1b0bae01ab6c2174989f5aaee3e41ac930", size = 241027 }, + { url = "https://files.pythonhosted.org/packages/e7/e8/883785b44c5780ed71e83d3e4437e710470be17a2e181e8b601e2da0dc4a/greenlet-3.5.3-cp315-cp315-win_arm64.whl", hash = "sha256:499fef2acede88c1864a57bb586b4bf533c81e1b82df7ab93451cdb47dfec227", size = 240085 }, + { url = "https://files.pythonhosted.org/packages/1c/da/4f4a8450962fad137c1c8981a3f1b8919d06c829993d4d476f9c525d5173/greenlet-3.5.3-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:176bc16a721fa5fc294d70b87b4dfa5fbdd251b3da5d5372735ecef9bd7d6d0c", size = 297221 }, + { url = "https://files.pythonhosted.org/packages/57/66/b3bfae3e220a9b63ea539a0eea681800c69ab1aada757eae8789f183e7ce/greenlet-3.5.3-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:629b614d2b786e89c50440e246f33eea78f58a962d0bdbbcc809e6d13605903f", size = 657221 }, + { url = "https://files.pythonhosted.org/packages/7b/81/b6d4d73a709684fc77e7fa034d7c2fe82cffa9fc920fadcaa659c2626213/greenlet-3.5.3-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2b2e857ae16f5f72142edf75f9f176fe7526ba19a2841df1420516f83831c9f2", size = 663226 }, + { url = "https://files.pythonhosted.org/packages/e9/39/0e0938a75115b939d42733a2a12e1d349653c9531fe6fe563e8a681f04e6/greenlet-3.5.3-cp315-cp315t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:16d192579ed281051396dddd7f7754dac6259e6b1fb26378c87b66622f8e3f91", size = 663706 }, + { url = "https://files.pythonhosted.org/packages/f5/07/e210b02b589f16e74ff48b730690e4a34ffe984219fce4f3c1a0e7ec8545/greenlet-3.5.3-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e515757e2e36bcbf1fad09a46e1557e8b1ae1797d4b44d09da7deed88ad28608", size = 660802 }, + { url = "https://files.pythonhosted.org/packages/5b/41/35d1c678cdb3c3b9e6bee691728e563cfb294202b23c7a4c3c2ccc343589/greenlet-3.5.3-cp315-cp315t-manylinux_2_39_riscv64.whl", hash = "sha256:4399eb8d041f20b68d943918bc55502a93d6fdc0a37c14da7881c04139acee9d", size = 498803 }, + { url = "https://files.pythonhosted.org/packages/eb/2e/5303eb3fa06bca089060f479707182a93e360683bc252acf846c3090d34e/greenlet-3.5.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:b363d46ed1ea431825fdb01471bb024fc08399bad1572a616e853c7684415adb", size = 1622157 }, + { url = "https://files.pythonhosted.org/packages/54/70/50de47a488f14df260b50ae34fb5d56016e308b098eab02c878b5223c26a/greenlet-3.5.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:e44da2f5bbdaabaf7d80b73dbb430c7035771e9f244e3c8b769715c9d8fa0a16", size = 1681159 }, + { url = "https://files.pythonhosted.org/packages/a7/13/1055e1dda7882073eda533e2b96c62e55bbd2db7fda6d5ece992febc7071/greenlet-3.5.3-cp315-cp315t-win_amd64.whl", hash = "sha256:8ff8bed3e3baa20a3ea261ce00526f1898ad4801d4886fd2220580ee0ad8fadf", size = 244007 }, + { url = "https://files.pythonhosted.org/packages/b4/0d/ca7d15afbdc397e3401134c9e1800d51d12b829661786187a4ad08fe484f/greenlet-3.5.3-cp315-cp315t-win_arm64.whl", hash = "sha256:b7068bd09f761f3f5b4d214c2bed063186b2a86148c740b3873e3f56d79bac31", size = 242586 }, ] [[package]] @@ -1191,43 +1301,35 @@ wheels = [ [[package]] name = "hf-xet" -version = "1.4.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/53/92/ec9ad04d0b5728dca387a45af7bc98fbb0d73b2118759f5f6038b61a57e8/hf_xet-1.4.3.tar.gz", hash = "sha256:8ddedb73c8c08928c793df2f3401ec26f95be7f7e516a7bee2fbb546f6676113", size = 670477 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/72/43/724d307b34e353da0abd476e02f72f735cdd2bc86082dee1b32ea0bfee1d/hf_xet-1.4.3-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:7551659ba4f1e1074e9623996f28c3873682530aee0a846b7f2f066239228144", size = 3800935 }, - { url = "https://files.pythonhosted.org/packages/2b/d2/8bee5996b699262edb87dbb54118d287c0e1b2fc78af7cdc41857ba5e3c4/hf_xet-1.4.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:bee693ada985e7045997f05f081d0e12c4c08bd7626dc397f8a7c487e6c04f7f", size = 3558942 }, - { url = "https://files.pythonhosted.org/packages/c3/a1/e993d09cbe251196fb60812b09a58901c468127b7259d2bf0f68bf6088eb/hf_xet-1.4.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:21644b404bb0100fe3857892f752c4d09642586fd988e61501c95bbf44b393a3", size = 4207657 }, - { url = "https://files.pythonhosted.org/packages/64/44/9eb6d21e5c34c63e5e399803a6932fa983cabdf47c0ecbcfe7ea97684b8c/hf_xet-1.4.3-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:987f09cfe418237812896a6736b81b1af02a3a6dcb4b4944425c4c4fca7a7cf8", size = 3986765 }, - { url = "https://files.pythonhosted.org/packages/ea/7b/8ad6f16fdb82f5f7284a34b5ec48645bd575bdcd2f6f0d1644775909c486/hf_xet-1.4.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:60cf7fc43a99da0a853345cf86d23738c03983ee5249613a6305d3e57a5dca74", size = 4188162 }, - { url = "https://files.pythonhosted.org/packages/1b/c4/39d6e136cbeea9ca5a23aad4b33024319222adbdc059ebcda5fc7d9d5ff4/hf_xet-1.4.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2815a49a7a59f3e2edf0cf113ae88e8cb2ca2a221bf353fb60c609584f4884d4", size = 4424525 }, - { url = "https://files.pythonhosted.org/packages/46/f2/adc32dae6bdbc367853118b9878139ac869419a4ae7ba07185dc31251b76/hf_xet-1.4.3-cp313-cp313t-win_amd64.whl", hash = "sha256:42ee323265f1e6a81b0e11094564fb7f7e0ec75b5105ffd91ae63f403a11931b", size = 3671610 }, - { url = "https://files.pythonhosted.org/packages/e2/19/25d897dcc3f81953e0c2cde9ec186c7a0fee413eb0c9a7a9130d87d94d3a/hf_xet-1.4.3-cp313-cp313t-win_arm64.whl", hash = "sha256:27c976ba60079fb8217f485b9c5c7fcd21c90b0367753805f87cb9f3cdc4418a", size = 3528529 }, - { url = "https://files.pythonhosted.org/packages/ec/36/3e8f85ca9fe09b8de2b2e10c63b3b3353d7dda88a0b3d426dffbe7b8313b/hf_xet-1.4.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:5251d5ece3a81815bae9abab41cf7ddb7bcb8f56411bce0827f4a3071c92fdc6", size = 3801019 }, - { url = "https://files.pythonhosted.org/packages/b5/9c/defb6cb1de28bccb7bd8d95f6e60f72a3d3fa4cb3d0329c26fb9a488bfe7/hf_xet-1.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1feb0f3abeacee143367c326a128a2e2b60868ec12a36c225afb1d6c5a05e6d2", size = 3558746 }, - { url = "https://files.pythonhosted.org/packages/c1/bd/8d001191893178ff8e826e46ad5299446e62b93cd164e17b0ffea08832ec/hf_xet-1.4.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8b301fc150290ca90b4fccd079829b84bb4786747584ae08b94b4577d82fb791", size = 4207692 }, - { url = "https://files.pythonhosted.org/packages/ce/48/6790b402803250e9936435613d3a78b9aaeee7973439f0918848dde58309/hf_xet-1.4.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:d972fbe95ddc0d3c0fc49b31a8a69f47db35c1e3699bf316421705741aab6653", size = 3986281 }, - { url = "https://files.pythonhosted.org/packages/51/56/ea62552fe53db652a9099eda600b032d75554d0e86c12a73824bfedef88b/hf_xet-1.4.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c5b48db1ee344a805a1b9bd2cda9b6b65fe77ed3787bd6e87ad5521141d317cd", size = 4187414 }, - { url = "https://files.pythonhosted.org/packages/7d/f5/bc1456d4638061bea997e6d2db60a1a613d7b200e0755965ec312dc1ef79/hf_xet-1.4.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:22bdc1f5fb8b15bf2831440b91d1c9bbceeb7e10c81a12e8d75889996a5c9da8", size = 4424368 }, - { url = "https://files.pythonhosted.org/packages/e4/76/ab597bae87e1f06d18d3ecb8ed7f0d3c9a37037fc32ce76233d369273c64/hf_xet-1.4.3-cp314-cp314t-win_amd64.whl", hash = "sha256:0392c79b7cf48418cd61478c1a925246cf10639f4cd9d94368d8ca1e8df9ea07", size = 3672280 }, - { url = "https://files.pythonhosted.org/packages/62/05/2e462d34e23a09a74d73785dbed71cc5dbad82a72eee2ad60a72a554155d/hf_xet-1.4.3-cp314-cp314t-win_arm64.whl", hash = "sha256:681c92a07796325778a79d76c67011764ecc9042a8c3579332b61b63ae512075", size = 3528945 }, - { url = "https://files.pythonhosted.org/packages/ac/9f/9c23e4a447b8f83120798f9279d0297a4d1360bdbf59ef49ebec78fe2545/hf_xet-1.4.3-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:d0da85329eaf196e03e90b84c2d0aca53bd4573d097a75f99609e80775f98025", size = 3805048 }, - { url = "https://files.pythonhosted.org/packages/0b/f8/7aacb8e5f4a7899d39c787b5984e912e6c18b11be136ef13947d7a66d265/hf_xet-1.4.3-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:e23717ce4186b265f69afa66e6f0069fe7efbf331546f5c313d00e123dc84583", size = 3562178 }, - { url = "https://files.pythonhosted.org/packages/df/9a/a24b26dc8a65f0ecc0fe5be981a19e61e7ca963b85e062c083f3a9100529/hf_xet-1.4.3-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc360b70c815bf340ed56c7b8c63aacf11762a4b099b2fe2c9bd6d6068668c08", size = 4212320 }, - { url = "https://files.pythonhosted.org/packages/53/60/46d493db155d2ee2801b71fb1b0fd67696359047fdd8caee2c914cc50c79/hf_xet-1.4.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:39f2d2e9654cd9b4319885733993807aab6de9dfbd34c42f0b78338d6617421f", size = 3991546 }, - { url = "https://files.pythonhosted.org/packages/bc/f5/067363e1c96c6b17256910830d1b54099d06287e10f4ec6ec4e7e08371fc/hf_xet-1.4.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:49ad8a8cead2b56051aa84d7fce3e1335efe68df3cf6c058f22a65513885baac", size = 4193200 }, - { url = "https://files.pythonhosted.org/packages/42/4b/53951592882d9c23080c7644542fda34a3813104e9e11fa1a7d82d419cb8/hf_xet-1.4.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7716d62015477a70ea272d2d68cd7cad140f61c52ee452e133e139abfe2c17ba", size = 4429392 }, - { url = "https://files.pythonhosted.org/packages/8a/21/75a6c175b4e79662ad8e62f46a40ce341d8d6b206b06b4320d07d55b188c/hf_xet-1.4.3-cp37-abi3-win_amd64.whl", hash = "sha256:6b591fcad34e272a5b02607485e4f2a1334aebf1bc6d16ce8eb1eb8978ac2021", size = 3677359 }, - { url = "https://files.pythonhosted.org/packages/8a/7c/44314ecd0e89f8b2b51c9d9e5e7a60a9c1c82024ac471d415860557d3cd8/hf_xet-1.4.3-cp37-abi3-win_arm64.whl", hash = "sha256:7c2c7e20bcfcc946dc67187c203463f5e932e395845d098cc2a93f5b67ca0b47", size = 3533664 }, +version = "1.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/63/39/67be8d71f900d9a55761b6022821d6679fb56c64f1b6063d5af2c2606727/hf_xet-1.5.2.tar.gz", hash = "sha256:73044bd31bae33c984af832d19c752a0dffb67518fee9ddbd91d616e1101cf47", size = 903674 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/be/525eabac5d1736b679c39e342ecd4292534012546a2d18f0043c8e3b6021/hf_xet-1.5.2-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:4a5ecb9cda8512ba2aa8ee5d37c87a1422992165892d653098c7b90247481c3b", size = 4064284 }, + { url = "https://files.pythonhosted.org/packages/c5/3f/699749dd78442480eda4e4fca494284b0e3542e4063cc37654d5fdc929e6/hf_xet-1.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8764488197c1d7b1378c8438c18d2eea902e150dbca0b0f0d2d32603fb9b5576", size = 3828537 }, + { url = "https://files.pythonhosted.org/packages/22/d7/2658ac0a5b9f4664ca27ce31bd015044fe9dea50ed455fb5197aba819c11/hf_xet-1.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8d7446f72abbf7e01ca5ff131786bc2e74a56393462c17a6bf1e303fbab81db4", size = 4417133 }, + { url = "https://files.pythonhosted.org/packages/d9/58/8343f3cb63c8fa058d576136df3871550f7d5214a8f048a7ea2eab6ac906/hf_xet-1.5.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:580e59e29bf37aece1f2b68537de1e3fb04f43a23d910dcf6f128280b5bfbba4", size = 4212613 }, + { url = "https://files.pythonhosted.org/packages/0c/33/a968f4e4535037b36941ec00714625fb60e026302407e7e26ca9f3e65f4e/hf_xet-1.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bee28c619622d36968056532fd49cf2b35ca75099b1d616c31a618a893491380", size = 4412710 }, + { url = "https://files.pythonhosted.org/packages/7b/d9/9e33981173dbaf194ba0015202b02d467b624d44d4eba89e1bf06c0d2995/hf_xet-1.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e396ab0faf6298199ad7a95305c3ca8498cb825978a6485be6d00587ee4ec577", size = 4628455 }, + { url = "https://files.pythonhosted.org/packages/e9/4b/cc682832de4264a03880a2d1b5ec3e1fab3bf307f508817250baafdb9996/hf_xet-1.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:fd3add255549e8ef58fa35b2e42dc016961c050600444e7d77d030ba6b57120e", size = 3979044 }, + { url = "https://files.pythonhosted.org/packages/ea/09/b2cdf2a0fb39a08af3222b96092a36bd3b40c54123eef07de4422e870971/hf_xet-1.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d6f9c58549407b84b9a5383afd68db0acc42345326a3159990b36a5ca8a20e4e", size = 3808037 }, + { url = "https://files.pythonhosted.org/packages/de/ba/2b70603c7552db82baeb2623e2336898304a17328845151be4fe1f48d420/hf_xet-1.5.2-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:f922b8f5fb84f1dd3d7ab7a1316354a1bca9b1c73ecfc19c76e51a2a49d29799", size = 4033760 }, + { url = "https://files.pythonhosted.org/packages/60/ac/b097a86a1e4a6098f3a79382643ab09d5733d87ccc864877ad1e12b49b70/hf_xet-1.5.2-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:045f84440c55cdeb659cf1a1dd48c77bcd0d2e93632e2fea8f2c3bdee79f38ed", size = 3841438 }, + { url = "https://files.pythonhosted.org/packages/d3/35/db860aa3a0780660324a506ad4b3d322ddc6ecbba4b9340aed0942cbf21c/hf_xet-1.5.2-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:db78c39c83d6279daddc98e2238f373ab8980685556d42472b4ec51abcf03e8c", size = 4428006 }, + { url = "https://files.pythonhosted.org/packages/af/6b/832dd980af4b0c3ae0660e309285f2ffcdff2faa38129390dbb47aa4a3f9/hf_xet-1.5.2-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7db73c810500c54c6760be8c39d4b2e476974de85424c50063efc22fdda13025", size = 4221099 }, + { url = "https://files.pythonhosted.org/packages/9e/05/ae50f0d34e3254e6c3e208beb2519f6b8673016fc4b3643badaf6450d186/hf_xet-1.5.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6395cfe3c9cbead4f16b31808b0e67eac428b66c656f856e99636adaddea878f", size = 4420766 }, + { url = "https://files.pythonhosted.org/packages/07/a9/c050bc2743a2bcd68928bfee157b08681667a164a24ec95fbfcfcd717e08/hf_xet-1.5.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:cde8cd167126bb6109b2ceb19b844433a4988643e8f3e01dd9dd0e4a34535097", size = 4636716 }, + { url = "https://files.pythonhosted.org/packages/e9/f8/68b01c5c2edb56ac9a67b3d076ffddcb90867abaee923923eb34e7a14e76/hf_xet-1.5.2-cp38-abi3-win_amd64.whl", hash = "sha256:ecf63d1cb69a9a7319910f8f83fcf9b46e7a32dfcf4b8f8eeddb55f647306e65", size = 3988373 }, + { url = "https://files.pythonhosted.org/packages/39/c6/988383e9dc17294d536fcbcd6fd16eed882e411ad16c954984a53e47b09c/hf_xet-1.5.2-cp38-abi3-win_arm64.whl", hash = "sha256:1da28519496eb7c8094c11e4d25509b4a468457a0302d58136099db2fd9a671d", size = 3816957 }, ] [[package]] name = "hpack" -version = "4.1.0" +version = "4.2.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 } +sdist = { url = "https://files.pythonhosted.org/packages/26/5b/fcabf6028144a8723726318b07a32c2f3314acdff6265743cf08a344b18e/hpack-4.2.0.tar.gz", hash = "sha256:0895cfa3b5531fc65fe439c05eb65144f123bf7a394fcaa56aa423548d8e45c0", size = 51300 } wheels = [ - { url = "https://files.pythonhosted.org/packages/07/c6/80c95b1b2b94682a72cbdbfb85b81ae2daffa4291fbfa1b1464502ede10d/hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496", size = 34357 }, + { url = "https://files.pythonhosted.org/packages/71/b4/4a9fcfb2aef6ba44d9073ecd301443aa00b3dac95de5619f2a7de7ec8a91/hpack-4.2.0-py3-none-any.whl", hash = "sha256:858ac0b02280fa582b5080d68db0899c62a80375e0e5413a74970c5e518b6986", size = 34246 }, ] [[package]] @@ -1274,9 +1376,10 @@ wheels = [ [[package]] name = "huggingface-hub" -version = "1.11.0" +version = "1.23.0" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "click" }, { 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'" }, @@ -1284,21 +1387,20 @@ dependencies = [ { name = "packaging" }, { name = "pyyaml" }, { name = "tqdm" }, - { name = "typer" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/dc/89/e7aa12d8a6b9259bed10671abb25ae6fa437c0f88a86ecbf59617bae7759/huggingface_hub-1.11.0.tar.gz", hash = "sha256:15fb3713c7f9cdff7b808a94fd91664f661ab142796bb48c9cd9493e8d166278", size = 761749 } +sdist = { url = "https://files.pythonhosted.org/packages/1a/8f/999e4dda11c6187c78f090eac00895a47e11a0049308f07579bcb7aa3aa2/huggingface_hub-1.23.0.tar.gz", hash = "sha256:c04997fb8bbdace1e57b7703d30ed7678af51f70d00d241819ff411b92ae9a88", size = 919163 } wheels = [ - { url = "https://files.pythonhosted.org/packages/37/02/4f3f8997d1ea7fe0146b343e5e14bd065fa87af790d07e5576d31b31cc18/huggingface_hub-1.11.0-py3-none-any.whl", hash = "sha256:42a6de0afbfeb5e022222d36398f029679db4eb4778801aafda32257ae9131ab", size = 645499 }, + { url = "https://files.pythonhosted.org/packages/f1/ce/13b2ba57838b8db1e6bd033c1b21ce0b9f6153b87d4e4939f77074e41eb0/huggingface_hub-1.23.0-py3-none-any.whl", hash = "sha256:b1d604788f5adc7f0eb246e03e0ec19011ca06e38400218c347dccc3dffa64a2", size = 770336 }, ] [[package]] name = "humanize" -version = "4.15.0" +version = "4.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/66/a3921783d54be8a6870ac4ccffcd15c4dc0dd7fcce51c6d63b8c63935276/humanize-4.15.0.tar.gz", hash = "sha256:1dd098483eb1c7ee8e32eb2e99ad1910baefa4b75c3aff3a82f4d78688993b10", size = 83599 } +sdist = { url = "https://files.pythonhosted.org/packages/0a/ea/13a1ef3c12d12662905801495283530251918b70d62d368f1d2e0272c70d/humanize-4.16.0.tar.gz", hash = "sha256:7dc2244a2f84a4bfb1d36c37bac80cd78e35cdc5c119206d87b018e1445f3a3f", size = 89515 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c5/7b/bca5613a0c3b542420cf92bd5e5fb8ebd5435ce1011a091f66bb7693285e/humanize-4.15.0-py3-none-any.whl", hash = "sha256:b1186eb9f5a9749cd9cb8565aee77919dd7c8d076161cf44d70e59e3301e1769", size = 132203 }, + { url = "https://files.pythonhosted.org/packages/b0/aa/0b7365d30fed43e7a3449aba1fe20a0a7174d9cf13e282af4e69ac825441/humanize-4.16.0-py3-none-any.whl", hash = "sha256:353eb2f34c09d098b2880eee8bef21832eae6d174f48c5762fff7e5fcb74d01d", size = 137209 }, ] [[package]] @@ -1321,11 +1423,11 @@ wheels = [ [[package]] name = "idna" -version = "3.15" +version = "3.18" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245 } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340 }, + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455 }, ] [[package]] @@ -1363,87 +1465,88 @@ wheels = [ [[package]] name = "jiter" -version = "0.13.0" +version = "0.16.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 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/71/29/499f8c9eaa8a16751b1c0e45e6f5f1761d180da873d417996cc7bddc8eef/jiter-0.13.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ea026e70a9a28ebbdddcbcf0f1323128a8db66898a06eaad3a4e62d2f554d096", size = 311157 }, - { url = "https://files.pythonhosted.org/packages/50/f6/566364c777d2ab450b92100bea11333c64c38d32caf8dc378b48e5b20c46/jiter-0.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66aa3e663840152d18cc8ff1e4faad3dd181373491b9cfdc6004b92198d67911", size = 319729 }, - { url = "https://files.pythonhosted.org/packages/73/dd/560f13ec5e4f116d8ad2658781646cca91b617ae3b8758d4a5076b278f70/jiter-0.13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3524798e70655ff19aec58c7d05adb1f074fecff62da857ea9be2b908b6d701", size = 354766 }, - { url = "https://files.pythonhosted.org/packages/7c/0d/061faffcfe94608cbc28a0d42a77a74222bdf5055ccdbe5fd2292b94f510/jiter-0.13.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ec7e287d7fbd02cb6e22f9a00dd9c9cd504c40a61f2c61e7e1f9690a82726b4c", size = 362587 }, - { url = "https://files.pythonhosted.org/packages/92/c9/c66a7864982fd38a9773ec6e932e0398d1262677b8c60faecd02ffb67bf3/jiter-0.13.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:47455245307e4debf2ce6c6e65a717550a0244231240dcf3b8f7d64e4c2f22f4", size = 487537 }, - { url = "https://files.pythonhosted.org/packages/6c/86/84eb4352cd3668f16d1a88929b5888a3fe0418ea8c1dfc2ad4e7bf6e069a/jiter-0.13.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ee9da221dca6e0429c2704c1b3655fe7b025204a71d4d9b73390c759d776d165", size = 373717 }, - { url = "https://files.pythonhosted.org/packages/6e/09/9fe4c159358176f82d4390407a03f506a8659ed13ca3ac93a843402acecf/jiter-0.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24ab43126d5e05f3d53a36a8e11eb2f23304c6c1117844aaaf9a0aa5e40b5018", size = 362683 }, - { url = "https://files.pythonhosted.org/packages/c9/5e/85f3ab9caca0c1d0897937d378b4a515cae9e119730563572361ea0c48ae/jiter-0.13.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9da38b4fedde4fb528c740c2564628fbab737166a0e73d6d46cb4bb5463ff411", size = 392345 }, - { url = "https://files.pythonhosted.org/packages/12/4c/05b8629ad546191939e6f0c2f17e29f542a398f4a52fb987bc70b6d1eb8b/jiter-0.13.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0b34c519e17658ed88d5047999a93547f8889f3c1824120c26ad6be5f27b6cf5", size = 517775 }, - { url = "https://files.pythonhosted.org/packages/4d/88/367ea2eb6bc582c7052e4baf5ddf57ebe5ab924a88e0e09830dfb585c02d/jiter-0.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d2a6394e6af690d462310a86b53c47ad75ac8c21dc79f120714ea449979cb1d3", size = 551325 }, - { url = "https://files.pythonhosted.org/packages/f3/12/fa377ffb94a2f28c41afaed093e0d70cfe512035d5ecb0cad0ae4792d35e/jiter-0.13.0-cp311-cp311-win32.whl", hash = "sha256:0f0c065695f616a27c920a56ad0d4fc46415ef8b806bf8fc1cacf25002bd24e1", size = 204709 }, - { url = "https://files.pythonhosted.org/packages/cb/16/8e8203ce92f844dfcd3d9d6a5a7322c77077248dbb12da52d23193a839cd/jiter-0.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:0733312953b909688ae3c2d58d043aa040f9f1a6a75693defed7bc2cc4bf2654", size = 204560 }, - { url = "https://files.pythonhosted.org/packages/44/26/97cc40663deb17b9e13c3a5cf29251788c271b18ee4d262c8f94798b8336/jiter-0.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:5d9b34ad56761b3bf0fbe8f7e55468704107608512350962d3317ffd7a4382d5", size = 189608 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/ac/4a/9f2c23255d04a834398b9c2e0e665382116911dc4d06b795710503cdad25/jiter-0.13.0-cp312-cp312-win32.whl", hash = "sha256:0bf670e3b1445fc4d31612199f1744f67f889ee1bbae703c4b54dc097e5dd394", size = 203024 }, - { url = "https://files.pythonhosted.org/packages/09/ee/f0ae675a957ae5a8f160be3e87acea6b11dc7b89f6b7ab057e77b2d2b13a/jiter-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:15db60e121e11fe186c0b15236bd5d18381b9ddacdcf4e659feb96fc6c969c92", size = 205424 }, - { url = "https://files.pythonhosted.org/packages/1b/02/ae611edf913d3cbf02c97cdb90374af2082c48d7190d74c1111dde08bcdd/jiter-0.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:41f92313d17989102f3cb5dd533a02787cdb99454d494344b0361355da52fcb9", size = 186818 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/27/da/68f38d12e7111d2016cd198161b36e1f042bd115c169255bcb7ec823a3bf/jiter-0.13.0-cp313-cp313-win32.whl", hash = "sha256:36ebfbcffafb146d0e6ffb3e74d51e03d9c35ce7c625c8066cdbfc7b953bdc72", size = 200630 }, - { url = "https://files.pythonhosted.org/packages/25/65/3bd1a972c9a08ecd22eb3b08a95d1941ebe6938aea620c246cf426ae09c2/jiter-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:8d76029f077379374cf0dbc78dbe45b38dec4a2eb78b08b5194ce836b2517afc", size = 202602 }, - { url = "https://files.pythonhosted.org/packages/15/fe/13bd3678a311aa67686bb303654792c48206a112068f8b0b21426eb6851e/jiter-0.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:bb7613e1a427cfcb6ea4544f9ac566b93d5bf67e0d48c787eca673ff9c9dff2b", size = 185939 }, - { 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 }, - { 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 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/a2/ec/a4c72c822695fa80e55d2b4142b73f0012035d9fcf90eccc56bc060db37c/jiter-0.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2b4972c6df33731aac0742b64fd0d18e0a69bc7d6e03108ce7d40c85fd9e3e6d", size = 201950 }, - { url = "https://files.pythonhosted.org/packages/b6/00/393553ec27b824fbc29047e9c7cd4a3951d7fbe4a76743f17e44034fa4e4/jiter-0.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:701a1e77d1e593c1b435315ff625fd071f0998c5f02792038a5ca98899261b7d", size = 185852 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/18/05/981c9669d86850c5fbb0d9e62bba144787f9fba84546ba43d624ee27ef29/jiter-0.13.0-cp314-cp314-win32.whl", hash = "sha256:632bf7c1d28421c00dd8bbb8a3bac5663e1f57d5cd5ed962bce3c73bf62608e6", size = 202108 }, - { url = "https://files.pythonhosted.org/packages/8d/96/cdcf54dd0b0341db7d25413229888a346c7130bd20820530905fdb65727b/jiter-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:f22ef501c3f87ede88f23f9b11e608581c14f04db59b6a801f354397ae13739f", size = 204027 }, - { url = "https://files.pythonhosted.org/packages/fb/f9/724bcaaab7a3cd727031fe4f6995cb86c4bd344909177c186699c8dec51a/jiter-0.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:07b75fe09a4ee8e0c606200622e571e44943f47254f95e2436c8bdcaceb36d7d", size = 187199 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/47/66/eea81dfff765ed66c68fd2ed8c96245109e13c896c2a5015c7839c92367e/jiter-0.13.0-cp314-cp314t-win32.whl", hash = "sha256:24dc96eca9f84da4131cdf87a95e6ce36765c3b156fc9ae33280873b1c32d5f6", size = 201196 }, - { url = "https://files.pythonhosted.org/packages/ff/32/4ac9c7a76402f8f00d00842a7f6b83b284d0cf7c1e9d4227bc95aa6d17fa/jiter-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0a8d76c7524087272c8ae913f5d9d608bd839154b62c4322ef65723d2e5bb0b8", size = 204215 }, - { url = "https://files.pythonhosted.org/packages/f9/8e/7def204fea9f9be8b3c21a6f2dd6c020cf56c7d5ff753e0e23ed7f9ea57e/jiter-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2c26cf47e2cad140fa23b6d58d435a7c0161f5c514284802f25e87fddfe11024", size = 187152 }, - { url = "https://files.pythonhosted.org/packages/79/b3/3c29819a27178d0e461a8571fb63c6ae38be6dc36b78b3ec2876bbd6a910/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b1cbfa133241d0e6bdab48dcdc2604e8ba81512f6bbd68ec3e8e1357dd3c316c", size = 307016 }, - { url = "https://files.pythonhosted.org/packages/eb/ae/60993e4b07b1ac5ebe46da7aa99fdbb802eb986c38d26e3883ac0125c4e0/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:db367d8be9fad6e8ebbac4a7578b7af562e506211036cba2c06c3b998603c3d2", size = 305024 }, - { url = "https://files.pythonhosted.org/packages/77/fa/2227e590e9cf98803db2811f172b2d6460a21539ab73006f251c66f44b14/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45f6f8efb2f3b0603092401dc2df79fa89ccbc027aaba4174d2d4133ed661434", size = 339337 }, - { url = "https://files.pythonhosted.org/packages/2d/92/015173281f7eb96c0ef580c997da8ef50870d4f7f4c9e03c845a1d62ae04/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:597245258e6ad085d064780abfb23a284d418d3e61c57362d9449c6c7317ee2d", size = 346395 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, +sdist = { url = "https://files.pythonhosted.org/packages/1d/1f/10936e16d8860c70698a1aa939a46aa0224813b782bce4e000e637da0b2d/jiter-0.16.0.tar.gz", hash = "sha256:7b24c3492c5f4f84a37946ad9cf504910cf6a782d6a4e0689b6673c5894b4a1c", size = 176431 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/3f/fae6cc967d120ec89e31c5418a51176d8278b3087fbb384a9176754f353c/jiter-0.16.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:67fddeda1688f0cce2d2ae83ccf8a80f79936f2d2997d6cc2261f82fdb54a4d3", size = 309289 }, + { url = "https://files.pythonhosted.org/packages/c8/e3/97c6c3562c077f6247d6e6ce5c82562500b6316c0d928e97e106b7a1321a/jiter-0.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c90c0f63df322be920eda6ce622e3083d8906ba267f8220fe7873213b8b4430e", size = 315181 }, + { url = "https://files.pythonhosted.org/packages/7b/89/d8d073f8aa2667e46c6c0873f86fe4a512bba4293cc730f626a076211a62/jiter-0.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64c0203212098470032aabcde9356fc168f377aade3e43def61dfe17e92f2037", size = 340939 }, + { url = "https://files.pythonhosted.org/packages/87/c9/db4fda3ed73fb864139305e935e5b8b38a5a24692a5a9dd356c22f1b9c8d/jiter-0.16.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12288303c9844e61e1651d02a9a6f6633e47d39f897d6991d1427161ce6b746e", size = 364932 }, + { url = "https://files.pythonhosted.org/packages/a2/74/52b5e86241057f52ddd7c9a580f90effb51f9d06239f6fc612279b91a838/jiter-0.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cf109d010b4b05a105afb3d43be36a21322d345ad3111e13d15f680afef0e5b", size = 461132 }, + { url = "https://files.pythonhosted.org/packages/a9/87/544a700f7447c1f31c5d7833821a4daa5683165c2d5a094fbf5b5800c3dc/jiter-0.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62c1b7fe1f77925acf5af68b6140b8810fa87dfd4dc0a9c8568ec2fa2a10429c", size = 374857 }, + { url = "https://files.pythonhosted.org/packages/40/cd/0fcc3f7d39183674d5bfa9ec640faaeb506c60be7c8f94625dfba366e37c/jiter-0.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8597d23c87f59294f83bcb6229b9ed1fccee13dbba967b46930d2f1759466fee", size = 347053 }, + { url = "https://files.pythonhosted.org/packages/5c/ae/c7e64e7932ad597fa395b61440b249ada6366716e25c6e08dd2afbd021e6/jiter-0.16.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:3126a5dbad56401989ac769aca0cb56005bfb3e2366eea0ca99d1a91c3c1ee03", size = 356153 }, + { url = "https://files.pythonhosted.org/packages/d4/1c/1c719044f14da814e1a060191ab19b96f3e99207bc5b4bfc6d6be34b3f80/jiter-0.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c4b4717bdb35ae456f831a6b08d01880fff399887a6bbc526a583a406e484eea", size = 393956 }, + { url = "https://files.pythonhosted.org/packages/3b/dc/7b2f303a2847207e265503853a2d964a55354cffd62a5f2936c155486798/jiter-0.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:adff21bc78edfe086c15eb495b900306076de378dc2337c132401fc39bd79c91", size = 521081 }, + { url = "https://files.pythonhosted.org/packages/c2/5f/501cf6e1e09caeb420195179ffc6f62aca603f1220ec53fd80d0d70b3e56/jiter-0.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dab907db06fc593645e73109acf4581ba5b548897d28b9348dc41ddc8343b2d3", size = 552085 }, + { url = "https://files.pythonhosted.org/packages/79/54/aa5be86520113b79455c3877f3d1f07a348098df4083ba3688e9537e52dd/jiter-0.16.0-cp311-cp311-win32.whl", hash = "sha256:560b2cf3fb03240cd34f27409a238547488708f05b7c3924f571a60422251ec7", size = 206755 }, + { url = "https://files.pythonhosted.org/packages/64/ec/2feb893eb330bd69b413866f4d5daada33c3962f1c6f270c91ca2d87fdf9/jiter-0.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:e431cfc9caf44c1d5459ff77d4e64cbf85fddb6a35dad836a15c6a9ec23087c1", size = 199155 }, + { url = "https://files.pythonhosted.org/packages/b9/9c/ca040d94415048a3666fc237774df8151c96f8d2b661cbe3b184acc95876/jiter-0.16.0-cp311-cp311-win_arm64.whl", hash = "sha256:2a8e9e39cf083016137aa5cadafe3188adc2ba6ba1fbf1e5d18889ad3e9ad056", size = 194403 }, + { url = "https://files.pythonhosted.org/packages/83/2b/52ace16ed031354f0539749a49e4bf33797d82bea5137910835fa4b09793/jiter-0.16.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:67c3bc1760f8c99d805dcab4e644027142a53b1d5d861f18780ebdbd5d40b72a", size = 306943 }, + { url = "https://files.pythonhosted.org/packages/94/2e/34957c2c1b661c252ba9bcc60ae0bddc27e0f7202c6073326a13c5390eec/jiter-0.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5af7780e4a26bd7d0d989592bf9ef12ebf806b74ab709223ecca37c749872ea9", size = 307779 }, + { url = "https://files.pythonhosted.org/packages/88/6c/59bd309cab4460c54cf1079f3eb7fe7af6a4c895c5c957a53378693bad2b/jiter-0.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5bf78d0e05e45cfdd66558893938d59afe3d1b1a824a202039b20e607d25a72", size = 335826 }, + { url = "https://files.pythonhosted.org/packages/3b/8c/f5ef7b65f0df47afa16596969defb281ebb86e96df346d62be6fd853d620/jiter-0.16.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4444a83f946605990c98f625cdd3d2725bfb818158760c5748c653170a20e0e", size = 362573 }, + { url = "https://files.pythonhosted.org/packages/2b/0b/ace4354da061ee38844a0c27dc2c21eecd27aea119e8da324bea987522d0/jiter-0.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a23f0e4f957e1be65752d2dfac9a5a06b1917af8dc85deb639c3b9d02e31290", size = 457979 }, + { url = "https://files.pythonhosted.org/packages/55/40/c0253d3772eb9dcd8e6606ee9b2d53ec8e5b814589c47f140aa585f21eaa/jiter-0.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c22a488f7b9218e245a0025a9ba6b100e2e54700831cf4cf16833a27fba3ad01", size = 372302 }, + { url = "https://files.pythonhosted.org/packages/a8/d2/4839422241aa12860ce597b20068727094ba0bc480723c74924ca5bad483/jiter-0.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46add52f4ad47a08bfb1219f3e673da972191489a33016edefdb5ea55bfa8c48", size = 343805 }, + { url = "https://files.pythonhosted.org/packages/e2/59/e196888a05befdda7dbe299b722d56f2f6eec65402bc34c0a3306d595feb/jiter-0.16.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:9c8a956fd72c2cf1e730d01ea080341f13aa0a97a4a33b51abebe725b7ae9ca9", size = 351107 }, + { url = "https://files.pythonhosted.org/packages/ec/74/4cd9e0fca65232136400354b630fbfcd2de634e22ccbb96567725981b548/jiter-0.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:561926e0573ffe4a32498420a76d64b16c513e1ab413b9d28158a8764ac701e5", size = 388441 }, + { url = "https://files.pythonhosted.org/packages/d9/8c/554691e48bc711299c0a293dd8a6179e24b2d66a54dc295421fcf64569c0/jiter-0.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:44d019fa8cdaf89bf29c71b39e3712143fdd0ac76725c6ef954f9957a5ea8730", size = 516354 }, + { url = "https://files.pythonhosted.org/packages/a4/cb/01e9d69dc2cc6759d4f91e230b34489c4fdb2518992650633f9e20bece89/jiter-0.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0df91907609837f33341b8e6fe73b95991fdaa57caf1a0fbd343dffe826f386f", size = 547880 }, + { url = "https://files.pythonhosted.org/packages/79/70/2953195f1c6ad00f49fa67e13df7e60acb3dd4f387101bc15abccddd905e/jiter-0.16.0-cp312-cp312-win32.whl", hash = "sha256:51d7b836acb0108d7c77df1742332cac2a1fa04a74d6dacec46e7091f0e91274", size = 203473 }, + { url = "https://files.pythonhosted.org/packages/2d/05/2909a8b10699a4d560f8c502b6b2c5f3991b682b1922c1eedda242b225bd/jiter-0.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:1878349266f8ee36ecb1375cc5ba2f115f35fd9f0a1a4119e725e379126647f7", size = 196905 }, + { url = "https://files.pythonhosted.org/packages/e9/a9/6b82bb1c8d7790d602489b967b982a909e5d092875a6c2ade96444c8dfc5/jiter-0.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:2ed5738ae4af18271a51a528b8811b0cbfa4a1858de9d83359e4169855d6a331", size = 190618 }, + { url = "https://files.pythonhosted.org/packages/91/c0/555fc60473d30d66894ba825e63615e3be7524fac23858356afa7a38906c/jiter-0.16.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:41977aa5654023948c2dae2a81cbf9c43343954bef1cd59a154dd15a4d84c195", size = 306203 }, + { url = "https://files.pythonhosted.org/packages/d0/2b/c3eaf16f5d7c9bad66ea32f40a95bd169b29a91217fcc7f081375157e99c/jiter-0.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d28bb3c26762358dadf3e5bf0bccd29ae987d65e6988d2e6f49829c76b003c09", size = 306489 }, + { url = "https://files.pythonhosted.org/packages/96/3f/02fdfc6705cad96127d883af5c34e4867f554f29ec7705ec1a46156400a9/jiter-0.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0542a7189c26920778658fc8fcf2af8bae05bae9924577f71804acef37996536", size = 335453 }, + { url = "https://files.pythonhosted.org/packages/b2/a6/e4bda5920d4b0d7c5dfb7174ce4a6b2e4d3e11c9162c452ef0eab4cdbdbd/jiter-0.16.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8fb8de1e23a0cb2a7f53c335049c7b72b6db41aa6227cdcc0972a1de5cb39450", size = 361625 }, + { url = "https://files.pythonhosted.org/packages/b7/97/4e6b59b2c6e55cbb3e183595f81ad65dcfb21c915fee5e19e335df21bc55/jiter-0.16.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b72d0b2990ca754a9102779ac98d8597b7cb31678958562214a007f909eab78e", size = 456958 }, + { url = "https://files.pythonhosted.org/packages/15/e0/97e9557686d2f94f4b93786eccb7eed28e9228ad132ea8237f44727314a7/jiter-0.16.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5f91b1c27fc22a57993d5a5cb8a627cb8ed4b10502716fac1ffbfe1d19d84e8", size = 372017 }, + { url = "https://files.pythonhosted.org/packages/0f/94/db768b6938e0df35c86beeba3dfbbb025c9ee5c19e1aa271f2396e50864d/jiter-0.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c682bea068a90b764577bdb78a60a4c1d1606daf9cd4c893832a37c7cc9d9026", size = 343320 }, + { url = "https://files.pythonhosted.org/packages/c1/d6/5a59d938244a30735fe62d9433fd325f9021ea29d89780ea4596ea93bc89/jiter-0.16.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:8d031aabecc4f1b6276adfb42e3aabb77c89d468bf616600e8d3a11328929053", size = 350520 }, + { url = "https://files.pythonhosted.org/packages/67/f8/c4a857f49c9af125f6bbcac7e3eee7f7978ed89682833062e2dbf62576b1/jiter-0.16.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eab2cd170150e70153de16896a1774e3a1dca80154c56b54d7a812c479a7165e", size = 387550 }, + { url = "https://files.pythonhosted.org/packages/8b/d6/5fbc2f7d6b67b754caa61a993a2e626e815dec47ffc2f9e35f01adfebec7/jiter-0.16.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:6edb63a46e65a82c26800a868e49b2cac30dd5a4218b88d74bc2c848c8ad60bb", size = 515424 }, + { url = "https://files.pythonhosted.org/packages/ed/54/284f0164b64a5fed915fea6ba7e9ba9b3d8d37c67d59cf2e3bb99d45cdfe/jiter-0.16.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:659039cc50b5addcc35fcc87ae2c1833b7c0a8e5326ef631a75e4478447bcf84", size = 546981 }, + { url = "https://files.pythonhosted.org/packages/13/c5/2a467585a576594384e1d2c43e1224deaafc085f24e243529cf98beef8e1/jiter-0.16.0-cp313-cp313-win32.whl", hash = "sha256:c9c53be232c2e206ef9cdbad81a48bfa74c3d3f08bcf8124630a8a748aad993e", size = 202853 }, + { url = "https://files.pythonhosted.org/packages/88/6a/de61d04b9eec69c71719968d2f716532a3bc121170c44a39e14979c6be81/jiter-0.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:baad945ed47f163ad833314f8e3288c396118934f94e7bbb9e243ce4b341a4fd", size = 196160 }, + { url = "https://files.pythonhosted.org/packages/19/4b/b390ed59bafb3f31d008d1218578f10327714484b334439947f7e5b11e7f/jiter-0.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:3c1fd2dbe1b0af19e987f03fe66c5f5bd105a2229c1aff4ab14890b24f41d21a", size = 189862 }, + { url = "https://files.pythonhosted.org/packages/a7/89/bc4f1b57d5da938fd344a466396541e586d161320d70bffd929aaafcd8f4/jiter-0.16.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b2c61484666ad42726029af0c00ef4541f0f3b5cdc550221f56c2343208018ee", size = 308239 }, + { url = "https://files.pythonhosted.org/packages/65/7a/c415453e5213001bf3b411ff65dec3d303b0e76a4a2cfea9768cd4960994/jiter-0.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:63efadc657488f45db1c676d81e704cac2abf3fdb892def1faea61db053127e2", size = 308928 }, + { url = "https://files.pythonhosted.org/packages/11/fc/1f4fb7ebf9a724c7741994f4aae18fba1e2f3133df14521a79194952c34a/jiter-0.16.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf0d73f50e7b6935677854f6e8e31d499ca7064dd24734f703e060f5b237d883", size = 336998 }, + { url = "https://files.pythonhosted.org/packages/a0/8d/72cadaac05ccfa7cc3a0a2232862e6c72443ca40cf300ba8b57f9f18b69b/jiter-0.16.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf3ea07d9bc8e7d03a9fbc051295462e6dbc295b894fd72457c3136e3e43d898", size = 362112 }, + { url = "https://files.pythonhosted.org/packages/58/4a/c4b0d5f651fda90a24ffce9f8d56cde462a2e09d31ae3de3c68cef34c04e/jiter-0.16.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:26798522707abb47d767db536e4148ceac1b14446bf028ee85e579a2e043cfe5", size = 459807 }, + { url = "https://files.pythonhosted.org/packages/80/58/ef77879ea9aa56b50824edc5a445e226422c7a8d211f3fd2a56bcb9493cf/jiter-0.16.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bc837c1b9631be10abfe0191537fe8009838204cec7e44827401ace390ddb567", size = 373181 }, + { url = "https://files.pythonhosted.org/packages/49/2e/ffbc3f254e4d8a66da3062c624a7df4b7c2b2cf9e1fe43cf394b3e104041/jiter-0.16.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49060fd70737fad59d33ba9dcc0d83247dc9e77187de26053a19c16c9f32bd69", size = 344927 }, + { url = "https://files.pythonhosted.org/packages/9a/f6/0be5dc6d64a89f80aa8fec984f94dedb2973e251edcae55841d60786d578/jiter-0.16.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:adbb8edeadd431bc4477879d5d371ece7cb1334486584e0f252656dd7ffada29", size = 352754 }, + { url = "https://files.pythonhosted.org/packages/da/6e/7d31243b3b91cd261dd19e9d3557fc3251a80883d3d8049c86174e7ab7af/jiter-0.16.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:31aaee5b80f672c1dc21272bcfb9cbdcfc1ea04ff50f00ed5af500b80c44fa93", size = 390553 }, + { url = "https://files.pythonhosted.org/packages/25/33/51ae371fde3c88897520f62b4d5f8b27ad7103e2bb10812ff52195609853/jiter-0.16.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:6722bcef4ffc86c835574b1b2fac6b33b9fb4a889c781e67950e891591f3c55a", size = 516900 }, + { url = "https://files.pythonhosted.org/packages/a0/45/6449b3d123ea439ba79507c657288f461d55049e7bcbdc2cf8eb8210f491/jiter-0.16.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:5ab4f50ff971b611d656554ea10b75f80097392c827bc32923c6eeb6386c8b00", size = 548754 }, + { url = "https://files.pythonhosted.org/packages/9b/e7/fd2fb11ae3e2649333da3aa170d04d7b3000bbdc3b270f6513382fdf4e04/jiter-0.16.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:710cc51d4ebdcd3c1f70b232c1db1ea1344a075770422bbd4bede5708335acbe", size = 122381 }, + { url = "https://files.pythonhosted.org/packages/26/80/f0b147a62c315a164ed2168908286ca302310824c218d3aae52b06c0c9a9/jiter-0.16.0-cp314-cp314-win32.whl", hash = "sha256:57b37fc887a32d44798e4d8ebfa7c9683ff3da1d5bf38f08d1bb3573ccb39106", size = 204578 }, + { url = "https://files.pythonhosted.org/packages/5e/e6/4758a14304b4523a6f5adb2419340086aa3593bd4327c2b25b5948a90548/jiter-0.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:cbd18dd5e2df96b580487b5745adf57ef64ad89ba2d9662fc3c19386acce7db8", size = 198154 }, + { url = "https://files.pythonhosted.org/packages/26/be/41fa54a2e7ea41d6c99f1dc5b1f0fd4cb474680304b5d268dd518e81da3a/jiter-0.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:a32d2027a9fa67f109ff245a3252ece3ccc32cc56703e1deab6cc846a59e0585", size = 191458 }, + { url = "https://files.pythonhosted.org/packages/81/6b/59127338b86d9fe4d99418f5a15118bea778103ee0fe9d9dd7e0af174e95/jiter-0.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2577196f4474ef3fc4779a088a23b0897bbf86f9ea3679c372d45b8383b43207", size = 316739 }, + { url = "https://files.pythonhosted.org/packages/2d/95/49461034d5388196d3dabf98748935f017b7785d8f3f5349f834bcc4ed0d/jiter-0.16.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:616e89e008a93c01104161c75b4988e58716b01d62307ebfe161e52a56d2a818", size = 340911 }, + { url = "https://files.pythonhosted.org/packages/cd/97/a4369f2fb82cb3dda13b98622f31249b2e014b223fe64ee534413ad72294/jiter-0.16.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e2e9efbe042210df657bade597f66d6d75723e3d8f45a12ea6d8167ff8bbce3", size = 361747 }, + { url = "https://files.pythonhosted.org/packages/28/51/49b6ed456261646e1906016a6760367a28aacd3c24805e4e5fe64116c1db/jiter-0.16.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f4d9e473a5ce7d27fef8b848df4dc16e283893d3f53b4a585e72c9595f3c284", size = 460225 }, + { url = "https://files.pythonhosted.org/packages/33/b5/5689aff4f66c5b60be63106e591dbfcba2190df97d2c9c7cf052361ddb98/jiter-0.16.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d30a4a1c87713060c8d1cc59a7b6c8fb6b8ef0a6900368014c76c87922a2929", size = 373169 }, + { url = "https://files.pythonhosted.org/packages/a2/96/3ae1b85ee0d6d6cab254fb7f8da018272b932bbf2d69b07e98aa2a96c746/jiter-0.16.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bae96332410f866e5900d809298b1ed82735932986c672495f9701daacd80620", size = 350332 }, + { url = "https://files.pythonhosted.org/packages/15/32/c99d7bafd78986556c95bf60ce84c6cc98786eac56066c12d7f828bb6747/jiter-0.16.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:da3d7ec75dc83bb18bca888b5edfae0656a26849056c59e05a7728badd17e7af", size = 353377 }, + { url = "https://files.pythonhosted.org/packages/0e/4b/f99a8e571287c3dec766bcc18528bbe8e8fb5365522ab5e6d64c93e87066/jiter-0.16.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ee6162b77d49a9939229df666dfa8af3e656b6701b54c4c84966d740e189264e", size = 387746 }, + { url = "https://files.pythonhosted.org/packages/75/69/c78a5b3f71040e34eb5917df26fb7ae9a2174cad1ccbf277512507c53a6e/jiter-0.16.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:63ffdbdae7d4499f4cda14eadc12ddcabef0fc0c081191bdc2247489cb698077", size = 517292 }, + { url = "https://files.pythonhosted.org/packages/c2/f7/095b38eda4c70d03651c403f29a5590f16d12ddc5d544aac9f9cddf72277/jiter-0.16.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a111256a7193bea0759267b10385e5870949c239ed7b6ddbaaf57573edb38734", size = 549259 }, + { url = "https://files.pythonhosted.org/packages/2e/c5/6a0207d90e5f656d95af98ebd0934f382d37674416f215aeda2ff8063e51/jiter-0.16.0-cp314-cp314t-win32.whl", hash = "sha256:de5ba8763e56b793561f43bed197c9ea55776daa5e9a6b91eed68a909bc9cdbf", size = 206523 }, + { url = "https://files.pythonhosted.org/packages/a5/31/c757d5f30a8980fd945ce7b98be10be9e4ff59c7c42f5fd86804c2e87db8/jiter-0.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b8a3f9a6008048fe9def7bf465180564a6e458047d2ce499149cfbe73c3ae9db", size = 200366 }, + { url = "https://files.pythonhosted.org/packages/7c/a2/d88de6d313d734a544a7901353ad5db67cb38dcfcd91713b7979dafc345d/jiter-0.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0fa25b09b13075c46f5bc174f2690525a925a4fc2f7c82969a2bbabff22386ce", size = 190516 }, + { url = "https://files.pythonhosted.org/packages/06/d3/8e278946d43eeca2585b4dd0834a887cd71136329b837f3a16ed86a8b4b0/jiter-0.16.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:850ccb1d7eedb4200f4014b1c0e8a577de114fc3cd88faad646dcc9bc4bb12ad", size = 304518 }, + { url = "https://files.pythonhosted.org/packages/72/43/28d4ef495028bf0506a413d4db3f4eb3e7288a382e0f065f306a17bbeb5e/jiter-0.16.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:e34e97bda77eb63242a410243c071e28ac7e0d8c0948c5ee658498690a4b2f2f", size = 310207 }, + { url = "https://files.pythonhosted.org/packages/e0/ca/c366b1012da1d640de975d9683acd44e4d150d9068845d0ca2610435253f/jiter-0.16.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7dc85ea77d4abbae8bad0d3538678aedee75bceec4e2f6c8dfb1c74772e5aa5", size = 342771 }, + { url = "https://files.pythonhosted.org/packages/16/52/50cc4056fc1ae02e7154704e7ecc89df0afb8300222cfe8a52d3f67e4730/jiter-0.16.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17ca7fae79f6d99cd9a042b75f917eaada7b895cfc7dd2ee3a16089dcaec7a85", size = 346468 }, + { url = "https://files.pythonhosted.org/packages/98/ab/664fd8c4be028b2bedd3d2ff08769c4ede23d0dbc87a77c62384a0515b5d/jiter-0.16.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:f17d61a28b4b3e0e3e2ba98490c70501403b4d196f78732439160e7fd3678127", size = 303106 }, + { url = "https://files.pythonhosted.org/packages/1a/07/421f1d5b65493a76e16027b848aba6a7d28073ae75944fa4289cc914d39f/jiter-0.16.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:96e38eea538c8ddf853a35727c7be0741c76c13f04148ac5c116222f50ece3b3", size = 304658 }, + { url = "https://files.pythonhosted.org/packages/0a/db/bba1155f01a01c3c37a89425d571da751bbedf5c54247b831a04cb971798/jiter-0.16.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d284fb8d94d5855d60c44fefcab4bf966f1da6fada73992b01f6f0c9bc0c6702", size = 339719 }, + { url = "https://files.pythonhosted.org/packages/78/f7/18a1afcd64f35314b68c1f23afcd9994d0bc13e65cc77517afff4e83986d/jiter-0.16.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64d613743df53199b1aa256a7d328340da6d7078aac7705a7db9d7a791e9cfd2", size = 343885 }, ] [[package]] @@ -1482,33 +1585,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437 }, ] -[[package]] -name = "kreuzberg" -version = "4.9.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/67/d4/6559b625ab7606ffebf9b025745bc202296df1a788573b406785b8214366/kreuzberg-4.9.2.tar.gz", hash = "sha256:59a99e14e2156530ef0b1344be53baef50bde87f0908099ac6127a88e0c9bbc1", size = 2437544 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/a1/acb48088d55b2387c2cfc4d24e92248fc0ef0657f3acb20b80b391c97a8d/kreuzberg-4.9.2-cp310-abi3-macosx_14_0_arm64.whl", hash = "sha256:5113ff1b9878fa92eb00c9cbb8b1d06600a770477e76f0045d20788a50c7ac7e", size = 28753304 }, - { url = "https://files.pythonhosted.org/packages/f3/40/4d58dcdf4cc14180183afc6ea2fa1b504b64f5154b8fea3d16930ff9cc65/kreuzberg-4.9.2-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:953fd353023585873f33f93d1e03f4e4bb747aef0c86320c450db3f8f10acef3", size = 33258395 }, - { url = "https://files.pythonhosted.org/packages/ec/bc/c12c0777b83069f1228e5a4bb341e69aa03ce5c6fe9f7b65936931b8c94a/kreuzberg-4.9.2-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c4c6d300d03b0ea8936e1b3faedf22d559cb790bb364417121df22bb81f890a6", size = 35500862 }, - { url = "https://files.pythonhosted.org/packages/48/b3/fb0c529d4ff2935dd17bb52f334d95425e10c2b1786929112302f82da8a7/kreuzberg-4.9.2-cp310-abi3-win_amd64.whl", hash = "sha256:0a4b905df6534e88eeaa17dc27cfbc3dd49283085bedc658a447d4c9499c8f6b", size = 33066471 }, -] - [[package]] name = "lance-namespace" -version = "0.5.2" +version = "0.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "lance-namespace-urllib3-client" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2b/c6/aec0d7752e15536564b50cf9a8926f0e5d7780aa3ab8ce8bca46daa55659/lance_namespace-0.5.2.tar.gz", hash = "sha256:566cc33091b5631793ab411f095d46c66391db0a62343cd6b4470265bb04d577", size = 10274 } +sdist = { url = "https://files.pythonhosted.org/packages/00/81/4cf8d0412e1f37b2bfa70d0aeb9c7ae4ab73607534e44d60b55efb485306/lance_namespace-0.9.0.tar.gz", hash = "sha256:f738b641cc615b17323baa4eb47900f184688739ee3d2ea9fe39396b9588e53d", size = 11637 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d6/3d/737c008d8fb2861e7ce260e2ffab0d5058eae41556181f80f1a1c3b52ef5/lance_namespace-0.5.2-py3-none-any.whl", hash = "sha256:6ccaf5649bf6ee6aa92eed9c535a114b7b4eb08e89f40426f58bc1466cbcffa3", size = 12087 }, + { url = "https://files.pythonhosted.org/packages/e1/fe/f38747c9610ade83dd9a99a0470b9432b6f21ce4e2bb5524edbe66f626fd/lance_namespace-0.9.0-py3-none-any.whl", hash = "sha256:f785ff10927e4ce0db69986576670fedd37f8a33521e8a4630c6be22db8061b2", size = 13501 }, ] [[package]] name = "lance-namespace-urllib3-client" -version = "0.5.2" +version = "0.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, @@ -1516,14 +1607,14 @@ dependencies = [ { name = "typing-extensions" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e9/64/51622c93ec8c164483c83b68764e5e76e52286c0137a8247bc6a7fac25f4/lance_namespace_urllib3_client-0.5.2.tar.gz", hash = "sha256:8a3a238006e6eabc01fc9d385ac3de22ba933aef0ae8987558f3c3199c9b3799", size = 172578 } +sdist = { url = "https://files.pythonhosted.org/packages/d3/c3/32d0e2618549ace857c80a457e5915ef3e1145661baff876c8a5ec27be5b/lance_namespace_urllib3_client-0.9.0.tar.gz", hash = "sha256:cf796fa5307fa4dde91fe4bec2af28b90ba79191852d4394e8fe44276538e40f", size = 235805 } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/10/f86d994498b37f7f35d0b8c2f7626a16fe4cb1949b518c1e5d5052ecf95f/lance_namespace_urllib3_client-0.5.2-py3-none-any.whl", hash = "sha256:83cefb6fd6e5df0b99b5e866ee3d46300d375b75e8af32c27bc16fbf7c1a5978", size = 300351 }, + { url = "https://files.pythonhosted.org/packages/cc/ab/c8754da0a1efc817f8480100cfe12b7e04034df834759de8ecf02beff3cc/lance_namespace_urllib3_client-0.9.0-py3-none-any.whl", hash = "sha256:be819c8cffb1e460a3a504dbf52d1ca009560a48e7202b8c4279998e4adf9fe4", size = 405586 }, ] [[package]] name = "lancedb" -version = "0.33.0" +version = "0.34.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "deprecation" }, @@ -1536,12 +1627,10 @@ dependencies = [ { name = "tqdm" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/09/2f/d5a4b2a5bb1f800936c76a6d8a4daf127a86fcab621eeb70b574a5adc774/lancedb-0.33.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:d4eaf6fa7c2eac619208f1d396f4de635ee0f535673067118a31c1181575c48b", size = 48338115 }, - { url = "https://files.pythonhosted.org/packages/07/12/31787b93a856b2c31382c7771dc22fb05575b70b87c9efe454269f4f0948/lancedb-0.33.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c6c2402ed2744245ae76c4167c0461da0a7a80f1608e0ec491c1548ea2b4302", size = 51162262 }, - { url = "https://files.pythonhosted.org/packages/49/b7/081cc29f8e06bf12191b99ab3fe702aceebdb0914476b821a8c0445cacc8/lancedb-0.33.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ebf1ffad811e6254a93931a79489ba1f21f48564bdfa06abae846f5fcaaf3e8", size = 54381368 }, - { url = "https://files.pythonhosted.org/packages/1c/bd/e0f4bd621f10ecf96a801b0166e87799ed7ca5a9dbabcef9a6c766a58ef3/lancedb-0.33.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:13da39f80adfea59e5831fe64e4166b2d70a2f843e6507bf644c4fe4c350087c", size = 51188986 }, - { url = "https://files.pythonhosted.org/packages/d9/1a/a8647a432ac6aa59cdce1fc061a7050ea4278bcab364539b78af2ecf72d2/lancedb-0.33.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:21b712825f0a00225e8974a41352c4ea84b0899ef8c23b17f672fadc38bd8346", size = 54440958 }, - { url = "https://files.pythonhosted.org/packages/08/6c/d0cc8da784cd7ed3b4940a5d1f3e7702e2d99a0a348ba81a376eed782810/lancedb-0.33.0-cp39-abi3-win_amd64.whl", hash = "sha256:4ba78c6202b0f6c2ce8edc7aa470e550d2da56271c7cbdd10428613f1f7126f9", size = 58751944 }, + { url = "https://files.pythonhosted.org/packages/df/f7/5262b9aa593f790757163c0165ab0da1dda054758901bea7e4f02c9cb633/lancedb-0.34.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:c462f2e6f933cad659fd0179394eaab578acbc9151fe2ef41bc29b36ecca5058", size = 52654213 }, + { url = "https://files.pythonhosted.org/packages/69/99/05ea0d32229ebea695193ff20c15d6ecae25785ad82a9d4723d98832a284/lancedb-0.34.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:48829e88e708947d0520454ab9e4f8efa35f3e3626469eadd3a6e061b89cb223", size = 55434501 }, + { url = "https://files.pythonhosted.org/packages/cd/4e/4325c13d5afa93c466428a5a0f168ad4d96f5eb4a77bbe7c5100d39c9897/lancedb-0.34.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:05ba8a5b58e064edfbe5be71b1abf2e411b4eaf295d1a173dcb1a55c5bfb5285", size = 58659359 }, + { url = "https://files.pythonhosted.org/packages/d9/5d/8ca165f1386caf6c4d1c515afd52f345b66432264eecfdfb7fd33eefd9af/lancedb-0.34.0-cp39-abi3-win_amd64.whl", hash = "sha256:51cbc11808f9e3332819b9367c975b3a888541447a8e7bea09c57c852a279153", size = 63530726 }, ] [[package]] @@ -1555,75 +1644,77 @@ wheels = [ [[package]] name = "librt" -version = "0.8.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/56/9c/b4b0c54d84da4a94b37bd44151e46d5e583c9534c7e02250b961b1b6d8a8/librt-0.8.1.tar.gz", hash = "sha256:be46a14693955b3bd96014ccbdb8339ee8c9346fbe11c1b78901b55125f14c73", size = 177471 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/01/0e748af5e4fee180cf7cd12bd12b0513ad23b045dccb2a83191bde82d168/librt-0.8.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:681dc2451d6d846794a828c16c22dc452d924e9f700a485b7ecb887a30aad1fd", size = 65315 }, - { url = "https://files.pythonhosted.org/packages/9d/4d/7184806efda571887c798d573ca4134c80ac8642dcdd32f12c31b939c595/librt-0.8.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3b4350b13cc0e6f5bec8fa7caf29a8fb8cdc051a3bae45cfbfd7ce64f009965", size = 68021 }, - { url = "https://files.pythonhosted.org/packages/ae/88/c3c52d2a5d5101f28d3dc89298444626e7874aa904eed498464c2af17627/librt-0.8.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ac1e7817fd0ed3d14fd7c5df91daed84c48e4c2a11ee99c0547f9f62fdae13da", size = 194500 }, - { url = "https://files.pythonhosted.org/packages/d6/5d/6fb0a25b6a8906e85b2c3b87bee1d6ed31510be7605b06772f9374ca5cb3/librt-0.8.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:747328be0c5b7075cde86a0e09d7a9196029800ba75a1689332348e998fb85c0", size = 205622 }, - { url = "https://files.pythonhosted.org/packages/b2/a6/8006ae81227105476a45691f5831499e4d936b1c049b0c1feb17c11b02d1/librt-0.8.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0af2bd2bc204fa27f3d6711d0f360e6b8c684a035206257a81673ab924aa11e", size = 218304 }, - { url = "https://files.pythonhosted.org/packages/ee/19/60e07886ad16670aae57ef44dada41912c90906a6fe9f2b9abac21374748/librt-0.8.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d480de377f5b687b6b1bc0c0407426da556e2a757633cc7e4d2e1a057aa688f3", size = 211493 }, - { url = "https://files.pythonhosted.org/packages/9c/cf/f666c89d0e861d05600438213feeb818c7514d3315bae3648b1fc145d2b6/librt-0.8.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d0ee06b5b5291f609ddb37b9750985b27bc567791bc87c76a569b3feed8481ac", size = 219129 }, - { url = "https://files.pythonhosted.org/packages/8f/ef/f1bea01e40b4a879364c031476c82a0dc69ce068daad67ab96302fed2d45/librt-0.8.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9e2c6f77b9ad48ce5603b83b7da9ee3e36b3ab425353f695cba13200c5d96596", size = 213113 }, - { url = "https://files.pythonhosted.org/packages/9b/80/cdab544370cc6bc1b72ea369525f547a59e6938ef6863a11ab3cd24759af/librt-0.8.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:439352ba9373f11cb8e1933da194dcc6206daf779ff8df0ed69c5e39113e6a99", size = 212269 }, - { url = "https://files.pythonhosted.org/packages/9d/9c/48d6ed8dac595654f15eceab2035131c136d1ae9a1e3548e777bb6dbb95d/librt-0.8.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:82210adabbc331dbb65d7868b105185464ef13f56f7f76688565ad79f648b0fe", size = 234673 }, - { url = "https://files.pythonhosted.org/packages/16/01/35b68b1db517f27a01be4467593292eb5315def8900afad29fabf56304ba/librt-0.8.1-cp311-cp311-win32.whl", hash = "sha256:52c224e14614b750c0a6d97368e16804a98c684657c7518752c356834fff83bb", size = 54597 }, - { url = "https://files.pythonhosted.org/packages/71/02/796fe8f02822235966693f257bf2c79f40e11337337a657a8cfebba5febc/librt-0.8.1-cp311-cp311-win_amd64.whl", hash = "sha256:c00e5c884f528c9932d278d5c9cbbea38a6b81eb62c02e06ae53751a83a4d52b", size = 61733 }, - { url = "https://files.pythonhosted.org/packages/28/ad/232e13d61f879a42a4e7117d65e4984bb28371a34bb6fb9ca54ec2c8f54e/librt-0.8.1-cp311-cp311-win_arm64.whl", hash = "sha256:f7cdf7f26c2286ffb02e46d7bac56c94655540b26347673bea15fa52a6af17e9", size = 52273 }, - { url = "https://files.pythonhosted.org/packages/95/21/d39b0a87ac52fc98f621fb6f8060efb017a767ebbbac2f99fbcbc9ddc0d7/librt-0.8.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a28f2612ab566b17f3698b0da021ff9960610301607c9a5e8eaca62f5e1c350a", size = 66516 }, - { url = "https://files.pythonhosted.org/packages/69/f1/46375e71441c43e8ae335905e069f1c54febee63a146278bcee8782c84fd/librt-0.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:60a78b694c9aee2a0f1aaeaa7d101cf713e92e8423a941d2897f4fa37908dab9", size = 68634 }, - { url = "https://files.pythonhosted.org/packages/0a/33/c510de7f93bf1fa19e13423a606d8189a02624a800710f6e6a0a0f0784b3/librt-0.8.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:758509ea3f1eba2a57558e7e98f4659d0ea7670bff49673b0dde18a3c7e6c0eb", size = 198941 }, - { url = "https://files.pythonhosted.org/packages/dd/36/e725903416409a533d92398e88ce665476f275081d0d7d42f9c4951999e5/librt-0.8.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:039b9f2c506bd0ab0f8725aa5ba339c6f0cd19d3b514b50d134789809c24285d", size = 209991 }, - { url = "https://files.pythonhosted.org/packages/30/7a/8d908a152e1875c9f8eac96c97a480df425e657cdb47854b9efaa4998889/librt-0.8.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bb54f1205a3a6ab41a6fd71dfcdcbd278670d3a90ca502a30d9da583105b6f7", size = 224476 }, - { url = "https://files.pythonhosted.org/packages/a8/b8/a22c34f2c485b8903a06f3fe3315341fe6876ef3599792344669db98fcff/librt-0.8.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:05bd41cdee35b0c59c259f870f6da532a2c5ca57db95b5f23689fcb5c9e42440", size = 217518 }, - { url = "https://files.pythonhosted.org/packages/79/6f/5c6fea00357e4f82ba44f81dbfb027921f1ab10e320d4a64e1c408d035d9/librt-0.8.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adfab487facf03f0d0857b8710cf82d0704a309d8ffc33b03d9302b4c64e91a9", size = 225116 }, - { url = "https://files.pythonhosted.org/packages/f2/a0/95ced4e7b1267fe1e2720a111685bcddf0e781f7e9e0ce59d751c44dcfe5/librt-0.8.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:153188fe98a72f206042be10a2c6026139852805215ed9539186312d50a8e972", size = 217751 }, - { url = "https://files.pythonhosted.org/packages/93/c2/0517281cb4d4101c27ab59472924e67f55e375bc46bedae94ac6dc6e1902/librt-0.8.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dd3c41254ee98604b08bd5b3af5bf0a89740d4ee0711de95b65166bf44091921", size = 218378 }, - { url = "https://files.pythonhosted.org/packages/43/e8/37b3ac108e8976888e559a7b227d0ceac03c384cfd3e7a1c2ee248dbae79/librt-0.8.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e0d138c7ae532908cbb342162b2611dbd4d90c941cd25ab82084aaf71d2c0bd0", size = 241199 }, - { url = "https://files.pythonhosted.org/packages/4b/5b/35812d041c53967fedf551a39399271bbe4257e681236a2cf1a69c8e7fa1/librt-0.8.1-cp312-cp312-win32.whl", hash = "sha256:43353b943613c5d9c49a25aaffdba46f888ec354e71e3529a00cca3f04d66a7a", size = 54917 }, - { url = "https://files.pythonhosted.org/packages/de/d1/fa5d5331b862b9775aaf2a100f5ef86854e5d4407f71bddf102f4421e034/librt-0.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:ff8baf1f8d3f4b6b7257fcb75a501f2a5499d0dda57645baa09d4d0d34b19444", size = 62017 }, - { url = "https://files.pythonhosted.org/packages/c7/7c/c614252f9acda59b01a66e2ddfd243ed1c7e1deab0293332dfbccf862808/librt-0.8.1-cp312-cp312-win_arm64.whl", hash = "sha256:0f2ae3725904f7377e11cc37722d5d401e8b3d5851fb9273d7f4fe04f6b3d37d", size = 52441 }, - { url = "https://files.pythonhosted.org/packages/c5/3c/f614c8e4eaac7cbf2bbdf9528790b21d89e277ee20d57dc6e559c626105f/librt-0.8.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7e6bad1cd94f6764e1e21950542f818a09316645337fd5ab9a7acc45d99a8f35", size = 66529 }, - { url = "https://files.pythonhosted.org/packages/ab/96/5836544a45100ae411eda07d29e3d99448e5258b6e9c8059deb92945f5c2/librt-0.8.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cf450f498c30af55551ba4f66b9123b7185362ec8b625a773b3d39aa1a717583", size = 68669 }, - { url = "https://files.pythonhosted.org/packages/06/53/f0b992b57af6d5531bf4677d75c44f095f2366a1741fb695ee462ae04b05/librt-0.8.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:eca45e982fa074090057132e30585a7e8674e9e885d402eae85633e9f449ce6c", size = 199279 }, - { url = "https://files.pythonhosted.org/packages/f3/ad/4848cc16e268d14280d8168aee4f31cea92bbd2b79ce33d3e166f2b4e4fc/librt-0.8.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c3811485fccfda840861905b8c70bba5ec094e02825598bb9d4ca3936857a04", size = 210288 }, - { url = "https://files.pythonhosted.org/packages/52/05/27fdc2e95de26273d83b96742d8d3b7345f2ea2bdbd2405cc504644f2096/librt-0.8.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e4af413908f77294605e28cfd98063f54b2c790561383971d2f52d113d9c363", size = 224809 }, - { url = "https://files.pythonhosted.org/packages/7a/d0/78200a45ba3240cb042bc597d6f2accba9193a2c57d0356268cbbe2d0925/librt-0.8.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5212a5bd7fae98dae95710032902edcd2ec4dc994e883294f75c857b83f9aba0", size = 218075 }, - { url = "https://files.pythonhosted.org/packages/af/72/a210839fa74c90474897124c064ffca07f8d4b347b6574d309686aae7ca6/librt-0.8.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e692aa2d1d604e6ca12d35e51fdc36f4cda6345e28e36374579f7ef3611b3012", size = 225486 }, - { url = "https://files.pythonhosted.org/packages/a3/c1/a03cc63722339ddbf087485f253493e2b013039f5b707e8e6016141130fa/librt-0.8.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4be2a5c926b9770c9e08e717f05737a269b9d0ebc5d2f0060f0fe3fe9ce47acb", size = 218219 }, - { url = "https://files.pythonhosted.org/packages/58/f5/fff6108af0acf941c6f274a946aea0e484bd10cd2dc37610287ce49388c5/librt-0.8.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fd1a720332ea335ceb544cf0a03f81df92abd4bb887679fd1e460976b0e6214b", size = 218750 }, - { url = "https://files.pythonhosted.org/packages/71/67/5a387bfef30ec1e4b4f30562c8586566faf87e47d696768c19feb49e3646/librt-0.8.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2af9e01e0ef80d95ae3c720be101227edae5f2fe7e3dc63d8857fadfc5a1d", size = 241624 }, - { url = "https://files.pythonhosted.org/packages/d4/be/24f8502db11d405232ac1162eb98069ca49c3306c1d75c6ccc61d9af8789/librt-0.8.1-cp313-cp313-win32.whl", hash = "sha256:086a32dbb71336627e78cc1d6ee305a68d038ef7d4c39aaff41ae8c9aa46e91a", size = 54969 }, - { url = "https://files.pythonhosted.org/packages/5c/73/c9fdf6cb2a529c1a092ce769a12d88c8cca991194dfe641b6af12fa964d2/librt-0.8.1-cp313-cp313-win_amd64.whl", hash = "sha256:e11769a1dbda4da7b00a76cfffa67aa47cfa66921d2724539eee4b9ede780b79", size = 62000 }, - { url = "https://files.pythonhosted.org/packages/d3/97/68f80ca3ac4924f250cdfa6e20142a803e5e50fca96ef5148c52ee8c10ea/librt-0.8.1-cp313-cp313-win_arm64.whl", hash = "sha256:924817ab3141aca17893386ee13261f1d100d1ef410d70afe4389f2359fea4f0", size = 52495 }, - { url = "https://files.pythonhosted.org/packages/c9/6a/907ef6800f7bca71b525a05f1839b21f708c09043b1c6aa77b6b827b3996/librt-0.8.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6cfa7fe54fd4d1f47130017351a959fe5804bda7a0bc7e07a2cdbc3fdd28d34f", size = 66081 }, - { url = "https://files.pythonhosted.org/packages/1b/18/25e991cd5640c9fb0f8d91b18797b29066b792f17bf8493da183bf5caabe/librt-0.8.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:228c2409c079f8c11fb2e5d7b277077f694cb93443eb760e00b3b83cb8b3176c", size = 68309 }, - { url = "https://files.pythonhosted.org/packages/a4/36/46820d03f058cfb5a9de5940640ba03165ed8aded69e0733c417bb04df34/librt-0.8.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7aae78ab5e3206181780e56912d1b9bb9f90a7249ce12f0e8bf531d0462dd0fc", size = 196804 }, - { url = "https://files.pythonhosted.org/packages/59/18/5dd0d3b87b8ff9c061849fbdb347758d1f724b9a82241aa908e0ec54ccd0/librt-0.8.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:172d57ec04346b047ca6af181e1ea4858086c80bdf455f61994c4aa6fc3f866c", size = 206907 }, - { url = "https://files.pythonhosted.org/packages/d1/96/ef04902aad1424fd7299b62d1890e803e6ab4018c3044dca5922319c4b97/librt-0.8.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b1977c4ea97ce5eb7755a78fae68d87e4102e4aaf54985e8b56806849cc06a3", size = 221217 }, - { url = "https://files.pythonhosted.org/packages/6d/ff/7e01f2dda84a8f5d280637a2e5827210a8acca9a567a54507ef1c75b342d/librt-0.8.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10c42e1f6fd06733ef65ae7bebce2872bcafd8d6e6b0a08fe0a05a23b044fb14", size = 214622 }, - { url = "https://files.pythonhosted.org/packages/1e/8c/5b093d08a13946034fed57619742f790faf77058558b14ca36a6e331161e/librt-0.8.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4c8dfa264b9193c4ee19113c985c95f876fae5e51f731494fc4e0cf594990ba7", size = 221987 }, - { url = "https://files.pythonhosted.org/packages/d3/cc/86b0b3b151d40920ad45a94ce0171dec1aebba8a9d72bb3fa00c73ab25dd/librt-0.8.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:01170b6729a438f0dedc4a26ed342e3dc4f02d1000b4b19f980e1877f0c297e6", size = 215132 }, - { url = "https://files.pythonhosted.org/packages/fc/be/8588164a46edf1e69858d952654e216a9a91174688eeefb9efbb38a9c799/librt-0.8.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7b02679a0d783bdae30d443025b94465d8c3dc512f32f5b5031f93f57ac32071", size = 215195 }, - { url = "https://files.pythonhosted.org/packages/f5/f2/0b9279bea735c734d69344ecfe056c1ba211694a72df10f568745c899c76/librt-0.8.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:190b109bb69592a3401fe1ffdea41a2e73370ace2ffdc4a0e8e2b39cdea81b78", size = 237946 }, - { url = "https://files.pythonhosted.org/packages/e9/cc/5f2a34fbc8aeb35314a3641f9956fa9051a947424652fad9882be7a97949/librt-0.8.1-cp314-cp314-win32.whl", hash = "sha256:e70a57ecf89a0f64c24e37f38d3fe217a58169d2fe6ed6d70554964042474023", size = 50689 }, - { url = "https://files.pythonhosted.org/packages/a0/76/cd4d010ab2147339ca2b93e959c3686e964edc6de66ddacc935c325883d7/librt-0.8.1-cp314-cp314-win_amd64.whl", hash = "sha256:7e2f3edca35664499fbb36e4770650c4bd4a08abc1f4458eab9df4ec56389730", size = 57875 }, - { url = "https://files.pythonhosted.org/packages/84/0f/2143cb3c3ca48bd3379dcd11817163ca50781927c4537345d608b5045998/librt-0.8.1-cp314-cp314-win_arm64.whl", hash = "sha256:0d2f82168e55ddefd27c01c654ce52379c0750ddc31ee86b4b266bcf4d65f2a3", size = 48058 }, - { url = "https://files.pythonhosted.org/packages/d2/0e/9b23a87e37baf00311c3efe6b48d6b6c168c29902dfc3f04c338372fd7db/librt-0.8.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c74a2da57a094bd48d03fa5d196da83d2815678385d2978657499063709abe1", size = 68313 }, - { url = "https://files.pythonhosted.org/packages/db/9a/859c41e5a4f1c84200a7d2b92f586aa27133c8243b6cac9926f6e54d01b9/librt-0.8.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a355d99c4c0d8e5b770313b8b247411ed40949ca44e33e46a4789b9293a907ee", size = 70994 }, - { url = "https://files.pythonhosted.org/packages/4c/28/10605366ee599ed34223ac2bf66404c6fb59399f47108215d16d5ad751a8/librt-0.8.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2eb345e8b33fb748227409c9f1233d4df354d6e54091f0e8fc53acdb2ffedeb7", size = 220770 }, - { url = "https://files.pythonhosted.org/packages/af/8d/16ed8fd452dafae9c48d17a6bc1ee3e818fd40ef718d149a8eff2c9f4ea2/librt-0.8.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9be2f15e53ce4e83cc08adc29b26fb5978db62ef2a366fbdf716c8a6c8901040", size = 235409 }, - { url = "https://files.pythonhosted.org/packages/89/1b/7bdf3e49349c134b25db816e4a3db6b94a47ac69d7d46b1e682c2c4949be/librt-0.8.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:785ae29c1f5c6e7c2cde2c7c0e148147f4503da3abc5d44d482068da5322fd9e", size = 246473 }, - { url = "https://files.pythonhosted.org/packages/4e/8a/91fab8e4fd2a24930a17188c7af5380eb27b203d72101c9cc000dbdfd95a/librt-0.8.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d3a7da44baf692f0c6aeb5b2a09c5e6fc7a703bca9ffa337ddd2e2da53f7732", size = 238866 }, - { url = "https://files.pythonhosted.org/packages/b9/e0/c45a098843fc7c07e18a7f8a24ca8496aecbf7bdcd54980c6ca1aaa79a8e/librt-0.8.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5fc48998000cbc39ec0d5311312dda93ecf92b39aaf184c5e817d5d440b29624", size = 250248 }, - { url = "https://files.pythonhosted.org/packages/82/30/07627de23036640c952cce0c1fe78972e77d7d2f8fd54fa5ef4554ff4a56/librt-0.8.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e96baa6820280077a78244b2e06e416480ed859bbd8e5d641cf5742919d8beb4", size = 240629 }, - { url = "https://files.pythonhosted.org/packages/fb/c1/55bfe1ee3542eba055616f9098eaf6eddb966efb0ca0f44eaa4aba327307/librt-0.8.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31362dbfe297b23590530007062c32c6f6176f6099646bb2c95ab1b00a57c382", size = 239615 }, - { url = "https://files.pythonhosted.org/packages/2b/39/191d3d28abc26c9099b19852e6c99f7f6d400b82fa5a4e80291bd3803e19/librt-0.8.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc3656283d11540ab0ea01978378e73e10002145117055e03722417aeab30994", size = 263001 }, - { url = "https://files.pythonhosted.org/packages/b9/eb/7697f60fbe7042ab4e88f4ee6af496b7f222fffb0a4e3593ef1f29f81652/librt-0.8.1-cp314-cp314t-win32.whl", hash = "sha256:738f08021b3142c2918c03692608baed43bc51144c29e35807682f8070ee2a3a", size = 51328 }, - { url = "https://files.pythonhosted.org/packages/7c/72/34bf2eb7a15414a23e5e70ecb9440c1d3179f393d9349338a91e2781c0fb/librt-0.8.1-cp314-cp314t-win_amd64.whl", hash = "sha256:89815a22daf9c51884fb5dbe4f1ef65ee6a146e0b6a8df05f753e2e4a9359bf4", size = 58722 }, - { url = "https://files.pythonhosted.org/packages/b2/c8/d148e041732d631fc76036f8b30fae4e77b027a1e95b7a84bb522481a940/librt-0.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:bf512a71a23504ed08103a13c941f763db13fb11177beb3d9244c98c29fb4a61", size = 48755 }, +version = "0.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/2f/3908645ddddab7120b46295e541ead308109fa48dbec7d67d7a778870d60/librt-0.13.0.tar.gz", hash = "sha256:1d2a610c14ac0d0750ee0a3ab8548e83155258387891caaca04def4bf7289781", size = 211402 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/25/a6498964cfeec270c468cffdc118f69c29b412593610d55fa1327ca51ff4/librt-0.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1b5a7bbff495baedbd9b916c367d66854008f8f3b575908ded477c499dc60082", size = 148029 }, + { url = "https://files.pythonhosted.org/packages/78/59/dc86d1bffd8e0c2818bace29d9f7783cfbb8e0673bf3673b5bbd5bbe0420/librt-0.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34bc7938b9fdf14fe32a406c19c71faf894c5cee7e7474bd0be2f17200b82d14", size = 153036 }, + { url = "https://files.pythonhosted.org/packages/29/3f/b923826660f02f286186cd9303d52bb05ced0a13708edc104dc8480920e3/librt-0.13.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f40e56b61b41be5f7dec938cfeffd660668cf4b5e72c78e7bd671d66b7bc2c79", size = 493062 }, + { url = "https://files.pythonhosted.org/packages/88/87/6c0980a9c9b1302cb68d108906697b89eceb55889bb1dcf77c109aa56ca5/librt-0.13.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:9c5d02b89de5acd0379a51ec44a89476fb03df6145442e1c8ecd6bee2f91b176", size = 485510 }, + { url = "https://files.pythonhosted.org/packages/32/81/795ae3b9df5dd94079fb807e38191855e023e8c6249014ae6bc3f0d9a490/librt-0.13.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7db9a3ff32ef5f7d1703d93831a3316cdf0b537de6a1cc03cc8fdd09b9194e89", size = 515909 }, + { url = "https://files.pythonhosted.org/packages/20/e5/182de15abce8907108a6fdb41487de65beb5099b74dc5841b19b099168db/librt-0.13.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3dbb2a31882456cadc7053378e81ad7ed7693db4ac9f98ab5f81ef034aa8ec9f", size = 508620 }, + { url = "https://files.pythonhosted.org/packages/32/03/33978d32db76e1f66377e8f78e42a2ca3c162143331677d1f50bbad36cfb/librt-0.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c6014e3c80f9c1fe268ef8b0e0ef113bac672cc032f2f93866e7ddad4f3e663d", size = 530363 }, + { url = "https://files.pythonhosted.org/packages/e6/f5/b291fbd2d00f7d8287bcbf67b5aa0c6afed4bc26cef23e079629c47a2c04/librt-0.13.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:091b60a4d2174fc1ec5c34cdc0b72efb6224753d76b7da61ebeab7a191aec8bd", size = 534209 }, + { url = "https://files.pythonhosted.org/packages/3e/03/6f41f17939d191bc21609f220da8509316bc62797f078545fe83be522e78/librt-0.13.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:66cb1138f384a191a6d75f986064841fcfdc0cea98f7bd9c9ab9b38049917588", size = 514254 }, + { url = "https://files.pythonhosted.org/packages/af/c2/2e4befa5410a7443019c14abccc94ff619797171f6b72013635fb87f31d7/librt-0.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:17221a7569f8f292aa0014226e48aa25b8c2b08da18088cd230953d0ea0f9cd1", size = 557611 }, + { url = "https://files.pythonhosted.org/packages/ab/54/8b69f81448417adbc040a2185f4e2eece1e1994b7dcfaeed4662b30f98a5/librt-0.13.0-cp311-cp311-win32.whl", hash = "sha256:fc67741da44c6eaa90e01eafb586bbba9b51eb5b6ed381ee6f5ae72eb3316d21", size = 104906 }, + { url = "https://files.pythonhosted.org/packages/76/5a/f4aaf37b50f2fde12c8c663b83fdd499cdc24f957f19543d7414bfcc9e25/librt-0.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:cc99dfb62b23c9207c33d0be8a2e2af7a42e21e6ea388b380a0c948c7b88953b", size = 125852 }, + { url = "https://files.pythonhosted.org/packages/f2/99/bf1820e6feeabc2f218c24450ec0c995d6a91e8ba0fd3caf042c9e8adb2a/librt-0.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:40ccd13c252d3fe473ffc8a57be7565abc8b64cf1b108344c859d5164f7f3e0c", size = 111832 }, + { url = "https://files.pythonhosted.org/packages/f0/f4/b2933ddae222dac338476abb872641169a5cfed2c2bb5444a5b07b32b0c3/librt-0.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30536798f4504c0fad0885b1d371b0539abb081e4570c9d7c641cb51141b49f0", size = 150990 }, + { url = "https://files.pythonhosted.org/packages/90/ef/db98f744ca50e6efc9c95c70ee49b77aefac31f6a3fc7c83754a42d6a74f/librt-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:93d24ebb82aa4420b1409c389e7857bc35bd0b668007ac8172427d5c73cc8cc5", size = 155238 }, + { url = "https://files.pythonhosted.org/packages/03/e7/a197e7bc72baf2c61ce7fdc6906a5054dc05bd8da0819aa894e4857bf87e/librt-0.13.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cb8a1adce42d8b75485a5d56a9623a50bcab995b6079f1dac59fc44034dd93d9", size = 503073 }, + { url = "https://files.pythonhosted.org/packages/f8/e7/7887712e27da7c1ab80fcabb1de6eb24243964f6557cae530d4b70706dbd/librt-0.13.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0763ca2ab66058174f9dee426dc64f5e0a89c24a7df8d3fe3f1836c04e25de4b", size = 496528 }, + { url = "https://files.pythonhosted.org/packages/94/f0/f2283385bb6b950b26a1410f4ce51ec27231e0b3a4b925c46366d218b198/librt-0.13.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b222493da6e7b6199db9bd79502436cf5a27da3c1f7fa83c7e285444fc93fd03", size = 531786 }, + { url = "https://files.pythonhosted.org/packages/36/11/69ac3b54766ffba5fd7e5acebfb048d66dbe1f9f2d14516c2b3edc59cf87/librt-0.13.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fadc63331f4388c3dc90090448f682a7e9feafc11481391c1e94f2f907a3976e", size = 524393 }, + { url = "https://files.pythonhosted.org/packages/61/5f/d72f95fd444a926a3c14b4e24979474116988dd57a45be242077c45d3c22/librt-0.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:70d9c62a4cffd9f23396cd5ef93fc5d11b31596b9b7d6306074abe3d5fcf09bd", size = 543026 }, + { url = "https://files.pythonhosted.org/packages/c4/08/dcd9993ad192737a004ba263d549f8ea605b326b952e7d6205c7d4170b76/librt-0.13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:66c0e7e6b02a155576df2c77ec933a70b72da726e248c494abf690923e624348", size = 546829 }, + { url = "https://files.pythonhosted.org/packages/96/d5/6d9bb2f54e4109a956b7128836529653eb9d740f784bc47ed10a02c1000e/librt-0.13.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ac04bcd3328eb91d99dfedf6a60d9c1f15d3434e6f6daf922f0420f7d90b85c7", size = 535700 }, + { url = "https://files.pythonhosted.org/packages/8c/f2/10946922503858a359492fa27f13e86228bde702116a740ac7b3cd185f24/librt-0.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:db327e7271e653c32040b85ae6188059c924b57d7e1e29f935523fa017cd4e82", size = 573566 }, + { url = "https://files.pythonhosted.org/packages/48/a8/94f00e3c99479a18088af3685ea016c42f3c7d5d1964d8dbb40c08d7f1aa/librt-0.13.0-cp312-cp312-win32.whl", hash = "sha256:860bd1d8ba48456ce08feaf8d343a8aaeb2fa086f2bcaa2a923fa3f7a3ff9aa3", size = 106099 }, + { url = "https://files.pythonhosted.org/packages/c9/7b/2da9c74c1ed25a89cc4e1c8e007ea2eb4a0f1fafa3e70d757fe3242c5c5c/librt-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:e54a315caf843c8d77e388cadc56ea9ded569935ee2d2347d7ea94992e5aa6fa", size = 126934 }, + { url = "https://files.pythonhosted.org/packages/d0/65/aead61bbf3b5358593f9d4779d2a0e88eaf6ec191a6342dde36dd1df6371/librt-0.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:c718e99a0992127af84385378460db624103b559ab260435abcfe77a4e4ed1c1", size = 112236 }, + { url = "https://files.pythonhosted.org/packages/67/3b/18e7b63255297a2bdc9c25c8d6d4ca8eca9f63aceb1252c0f7427ac7099e/librt-0.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a468951af16155824e88bdd8326ebe5bdb371f3ec0ac04642994b98201d914f3", size = 151027 }, + { url = "https://files.pythonhosted.org/packages/4d/68/e2248452c00d1a03b45fee1752cdc8f790a476efd2402b75181da88a9e61/librt-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ae01d8512cc17079e53425635327dbf3f7ff57a42c00dec348bf79791c56444c", size = 155152 }, + { url = "https://files.pythonhosted.org/packages/0e/16/52b1c99bf19057a062aac39c900cbb81499f6f75d6c537c14463d247ba78/librt-0.13.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32c26893cd085c1efe83219e78d866da23fb20a066101b8f68210004361d224c", size = 502499 }, + { url = "https://files.pythonhosted.org/packages/9f/54/b811151805c795f55e0dedee6ec687b75f9982a8105d240ea3910737a77b/librt-0.13.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5929da1981a46bcf4b28b1b9499905f0ff58e2419da402a048234e9783acbc4b", size = 496108 }, + { url = "https://files.pythonhosted.org/packages/8f/f8/094d6b2bd93f3fdaa54db54cc788c4a365333bddad65ab02e04da0b1d004/librt-0.13.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:94b85d664d777bab6c0d709416cb42938251fda9e221b79e3a2215d85df5f4f9", size = 531576 }, + { url = "https://files.pythonhosted.org/packages/2e/40/541733d5755824f968f7ec39d78ffbd75d145964157ae5e69a09ec6d7326/librt-0.13.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:531b2df3e9fe96b1fcf73a6d165921e4656be5f58d631d384ebce344298368db", size = 524390 }, + { url = "https://files.pythonhosted.org/packages/c6/b5/255673cfdbf5ba663339d36cd863c897289ab4337577e19f9405ce059f36/librt-0.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:109b84a9edf69ad89dc1f66358659e14a031baca95e3e5b0060bd903ede8efd6", size = 543053 }, + { url = "https://files.pythonhosted.org/packages/9e/11/ab5005e9c9850710f21e354201bf090646349d3fabf5f951eaf70235729e/librt-0.13.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1304368a3e7ffc3e9db986796cc5326fdb5943a3567ecc137cff318e4240c0e7", size = 546387 }, + { url = "https://files.pythonhosted.org/packages/a2/04/a5d7ce1d1df1afd15ca283dcdf7530ac073e12d69ae8c40879dda96f7868/librt-0.13.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e4f9b472e7d308d94b62c801982065661158c6ed02790d6c7ddb4337cea0f9c1", size = 535970 }, + { url = "https://files.pythonhosted.org/packages/5a/76/927e267a6daa290174ac281b23c9804c8829b042ade9c6f24a065f540958/librt-0.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9f836c37478f167a81200d8c8b2c920a22224564bed2c23d7aeec760965c367a", size = 573582 }, + { url = "https://files.pythonhosted.org/packages/10/24/b6c5213efe39c19f9e13605644d0cf063b4ddaa33ac2e45b088e23a70e2e/librt-0.13.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:4000d961ff9598ac6ea603c6c836a5ed49bc205ade5fc378b998dfe1e2c36628", size = 82189 }, + { url = "https://files.pythonhosted.org/packages/4c/00/d29736be177a906ac0b84a5b04b4fbfa22c776dc2f366de4172b0f968c08/librt-0.13.0-cp313-cp313-win32.whl", hash = "sha256:79e44cff71750d299d61a678e49995b0d5935a9cda238c2574daeca3ba536927", size = 106193 }, + { url = "https://files.pythonhosted.org/packages/c8/ac/aff6fb45393cb8912f39dfb156ef6b2d1cadb207ff465fc8f66141054be8/librt-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:54dab44a847d5ad1acd05c8a83fe518ae685516ecf4d3f7cc6e3df2a66767650", size = 126962 }, + { url = "https://files.pythonhosted.org/packages/d9/3a/d68cb2b334d53fd30fac81d3a489ce4ba0d9506f4df43fcf676b68352b19/librt-0.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:d4cb6fbfdf874340ab5e51450753c0f817b6958a3621125ee695bbc3de866566", size = 112127 }, + { url = "https://files.pythonhosted.org/packages/7b/66/f49ae0d592bd45b6941e9a8bafcb6a87cddcd501ee7874707e767f01b585/librt-0.13.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:25218d94b1d2cbc0ba1d8a3f9dc9af578d9646e5ed16443a70cde1dfdcce6d71", size = 149818 }, + { url = "https://files.pythonhosted.org/packages/3d/50/51c76d74014d04fb95b6506d286808984b78a2f7a41039094e6b2194ac48/librt-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f26629539d4893c2957a16c41bb058e1e135c1f150f6a2e25ed047f64cf3f5c6", size = 154071 }, + { url = "https://files.pythonhosted.org/packages/b8/fe/f19b0f5f82d5a1f2da736586bc840abd00ce07d6388136ae80b7333883fc/librt-0.13.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4517d47b2b8af26975a406fba7d314de9696d864252e0257c6ea90238cfe27f", size = 494168 }, + { url = "https://files.pythonhosted.org/packages/94/bc/b8550c75775127fd31a5f20e8775997f7b527ad661fc8ddccd7497c064f7/librt-0.13.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f19e181de5b3a1148bb3420b8c4b0b0ea0fce6950099724ad151d6cea5acc180", size = 491054 }, + { url = "https://files.pythonhosted.org/packages/30/14/4d0204867623df3f33f86efd3d3692ba5e01321443f4d6eab35a22697618/librt-0.13.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22034924f5b42d5a56371cf271771bfeaabf235a7a8b6264bef2d20013f786c6", size = 523006 }, + { url = "https://files.pythonhosted.org/packages/19/0a/c45fc9a260934696bace1ac5df1e148ac92bd71767aee3bf7cd7a4534f4c/librt-0.13.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7897db4e95e22468bdda33d8e012ceacd0182abf001e6389d763f0def6286b9", size = 515058 }, + { url = "https://files.pythonhosted.org/packages/13/0a/50c5ce45b326854ef8fa6ae4c36cf5142e5c55315eaf9e51d0ae73ac4da3/librt-0.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1ce61b3746545029d4f5c17d6bd74b676254ad98433086c846ffb5e8fa73f007", size = 534025 }, + { url = "https://files.pythonhosted.org/packages/89/2d/08c413c8f93fc13b8103624fce38e5caa86cd08cbbc8465870ab287af54b/librt-0.13.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:46c330e82565962c761dbce7941be2cff7db674ee807455a8d0cadc5f9b759b0", size = 540557 }, + { url = "https://files.pythonhosted.org/packages/b3/c1/93af71fb4a364952210051811dd4e40174e79656b050c89cacac18af3330/librt-0.13.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:375f5af8f99cbaa99dd293af986e3d57caabc9ba81a5d3f021603764854197a1", size = 523201 }, + { url = "https://files.pythonhosted.org/packages/c1/6e/9766f07b676a4889d9f8bc2864e9ba5fff165653143ef4dda7df6aa34d16/librt-0.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9320d34c3376ae204b2cd176e8d4883a013934e0aef822f1aed9c536490c275d", size = 565740 }, + { url = "https://files.pythonhosted.org/packages/a2/1e/664e3472ce2b6e10e9b83f29d4a36eb982ff6b5a169ae7567bba3a4c4ff5/librt-0.13.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:9af313c66157a69dc69ea0059a66961692250e0dc95af9c385a48ffb770a0d16", size = 81611 }, + { url = "https://files.pythonhosted.org/packages/2f/d4/8582a4d65e2234673685e07309d02c230b28a85724eb0acbf13f019b7f6e/librt-0.13.0-cp314-cp314-win32.whl", hash = "sha256:f2a7253458e34f33543551394ae4fe104b497ec2a65ac266074de64c1df82e37", size = 100106 }, + { url = "https://files.pythonhosted.org/packages/63/ce/0cb99efe6086b46cd985dc26672166fae312a239690e75871f7fafbd3fc5/librt-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:a3dfe4edf10e8ed7e55b026a8bfc2c2a8704218b659cd4bffdf604fab966dc39", size = 121209 }, + { url = "https://files.pythonhosted.org/packages/26/85/4f3ccb083a3c9b0d42e223acdb3c3f507953324a59cdcab4826e8e2e3b89/librt-0.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:68a5faee4bba381cb93b5961f684a514cf0053cb92308ff9c792c2fea0b174c6", size = 106404 }, + { url = "https://files.pythonhosted.org/packages/b2/77/333191499538c8e8189de7a4cba8e6f49ee949fd6d6e6324b21fd1522466/librt-0.13.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a38fb81d8376dfa2f8963b265fec07637802b0d01e2a127c19c66cb070fb24f5", size = 159231 }, + { url = "https://files.pythonhosted.org/packages/7a/9e/2aa83758f22c278b837a1d8025898434ce2b8bff36678d5330ecaef56dff/librt-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d4c8d9bd5abce34b2e75edb3bf37ab0f34e49b1f915a40ae8468eb7c85bc5b46", size = 161300 }, + { url = "https://files.pythonhosted.org/packages/bb/c0/86791e936553ca763d6b3c2fb4d31d596cd00e14fa631c283a40ba01559a/librt-0.13.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:387e2f1d27e89bffe0d3f520f0da0662c973fd607ca16c1808f8a5085419485e", size = 582056 }, + { url = "https://files.pythonhosted.org/packages/a8/d3/a9ec15984a185e000c4d2a16ba28bd623124ad4c38a10974c7ff78e3a893/librt-0.13.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:4f6db193d2e5e0ed60359b9a5a682cd67205d0d3b1e459a867dd4b5c4e7eaa7a", size = 562758 }, + { url = "https://files.pythonhosted.org/packages/3c/af/dbe36b78b19c06a55097f99305e4ea9458e2273e6ae16a3cbecaad7ee978/librt-0.13.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d38604854e8d22faadf683ec6c02bb0f886e2ba56ef981a1c36ee275f21ea22", size = 602095 }, + { url = "https://files.pythonhosted.org/packages/2a/a8/2966891b4dd2830f5203fbee92ac2c4947653a2390ba73dfa44244fad025/librt-0.13.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:371f7ce73026815dafd51c50ce38416e91428b28c4b2ec97cd39271164b0045c", size = 593452 }, + { url = "https://files.pythonhosted.org/packages/61/f5/4df8bfc8405ecf8c0d525b4d69636f694bdd8620b313ec8b76e54a5926cc/librt-0.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3aaedf52171bee90860704c560bc798fe83b76247df47568e0197e9b13c735a0", size = 623729 }, + { url = "https://files.pythonhosted.org/packages/d6/13/9ac202dffc8db06f75d06c08c2f9f6ff054be67d21272dcc078fa1cc0c57/librt-0.13.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:96bad8725a4f196a798366c25ce075d1f7543a4ec045ffc13e6a7ec095cdab04", size = 617077 }, + { url = "https://files.pythonhosted.org/packages/6e/f0/ebe38610716aee5cb28efd95089bb90192096179802779381e1c5dcf239c/librt-0.13.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:6bf6a559ffe4a93bbea6cf31ddf01a7fd9ba342ef51f27beb178e318b74acd61", size = 599561 }, + { url = "https://files.pythonhosted.org/packages/4f/5c/c2e72e236fff7abc716d5b1753b8b8cd3ea85ac46fe17d2e7c51d4e1c723/librt-0.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:301067672387902c55f94b51d5022304b36c966ea9fe1f21caab99a9bef487c9", size = 645511 }, + { url = "https://files.pythonhosted.org/packages/0c/99/6203ce619dee940d6bfbe099ec3fe4be00a68e9d60f70abf906cf124fe66/librt-0.13.0-cp314-cp314t-win32.whl", hash = "sha256:5fdcf34f86de8fb66d7dc7589f96ba91c4aa46671200d400e6fd6f109a483f18", size = 104357 }, + { url = "https://files.pythonhosted.org/packages/52/dd/843b6314087c41657c7036d7914d8f294bdf9b580aa8513ea0588c8e9a3d/librt-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:260c33e92263fa629b4f6d3c51967a1c2158fe6c33237aaa3ebeac586b085259", size = 126998 }, + { url = "https://files.pythonhosted.org/packages/5f/5d/3dcec2884ba1b0806d1408612555c38dd5d68e90156b59f75f6e36435c3a/librt-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2f281549a4c52ac7bb97997f14353f8bd0e53a34ca0dad1c905cfd0b4a58ae99", size = 110771 }, ] [[package]] @@ -1637,7 +1728,6 @@ dependencies = [ { name = "httpx" }, { name = "huggingface-hub" }, { name = "jinja2" }, - { name = "kreuzberg" }, { name = "lancedb" }, { name = "lilbee-engine" }, { name = "litestar" }, @@ -1646,12 +1736,14 @@ dependencies = [ { name = "pillow" }, { name = "psutil" }, { name = "pydantic-settings" }, + { name = "regex" }, { name = "textual" }, { name = "tiktoken" }, { name = "tree-sitter-language-pack" }, { name = "typer" }, { name = "typing-extensions" }, { name = "uvicorn" }, + { name = "xberg" }, ] [package.optional-dependencies] @@ -1705,20 +1797,20 @@ requires-dist = [ { name = "httpx" }, { name = "huggingface-hub", specifier = ">=1.11.0" }, { name = "jinja2", specifier = ">=2.11.3" }, - { name = "kreuzberg", specifier = ">=4.9.1,<5" }, { name = "lancedb" }, { name = "lilbee", extras = ["crawler", "litellm", "graph"], marker = "extra == 'release'" }, { name = "lilbee-engine", directory = "packaging/engine-wheel" }, { name = "litellm", marker = "extra == 'litellm'", specifier = ">=1.84.0,<1.92" }, { name = "litestar", specifier = ">=2.0" }, { name = "mcp", specifier = ">=1.26.0,<2" }, - { name = "numpy" }, + { name = "numpy", specifier = "<2.5" }, { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'cuda12'" }, { name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'cuda12'" }, { name = "nvidia-cuda-runtime-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'cuda12'" }, { name = "pillow", specifier = ">=11.3.0" }, { name = "psutil", specifier = ">=5.9" }, { name = "pydantic-settings", specifier = ">=2.14.2" }, + { name = "regex" }, { name = "spacy", marker = "extra == 'graph'", specifier = ">=3.8" }, { name = "textual", specifier = ">=0.75" }, { name = "tiktoken" }, @@ -1726,6 +1818,7 @@ requires-dist = [ { name = "typer", specifier = ">=0.12" }, { name = "typing-extensions", specifier = ">=4.5.0" }, { name = "uvicorn", specifier = ">=0.30" }, + { name = "xberg", specifier = ">=1.0.0rc30" }, ] provides-extras = ["litellm", "graph", "crawler", "cuda12", "release"] @@ -1766,7 +1859,7 @@ wheels = [ [[package]] name = "litellm" -version = "1.89.1" +version = "1.91.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, @@ -1782,9 +1875,9 @@ dependencies = [ { name = "tiktoken" }, { name = "tokenizers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e8/1b/de398e6cef46d4cbc4395c895330f30eab05439bf6d0c5c53b0203661eeb/litellm-1.89.1.tar.gz", hash = "sha256:eb9292f90afe46dcce4bfef6bbeaa54494945813763e1c0917f4e9a1c584c1a4", size = 14071586 } +sdist = { url = "https://files.pythonhosted.org/packages/36/33/879369fe202498a307e1130bae1f59c5f226362afc07829ba5a0279a563d/litellm-1.91.3.tar.gz", hash = "sha256:096cee401dfd353f050422adc6ed9b25da0fc5e110fc923ce3f0ffa5bc5c2957", size = 14875767 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/24/81f03088876a13b628fdbaf746ee746e1dff127d63f339ef72f1a3801c91/litellm-1.89.1-py3-none-any.whl", hash = "sha256:a52a67625d89cb1787ef48c4b3c1ab9c2574ea304f56900bc631844297a13bd4", size = 15484855 }, + { url = "https://files.pythonhosted.org/packages/d6/3c/157c54c924f9e6025797545a0cd915a98e98ad8d3f9011502a575781d4b5/litellm-1.91.3-py3-none-any.whl", hash = "sha256:5be4df2bdf5459f46a6224a75046f365dc979995a37a81f5aa3ce29f92f534cf", size = 16674504 }, ] [[package]] @@ -1822,73 +1915,116 @@ wheels = [ [[package]] name = "lxml" -version = "5.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/76/3d/14e82fc7c8fb1b7761f7e748fd47e2ec8276d137b6acfe5a4bb73853e08f/lxml-5.4.0.tar.gz", hash = "sha256:d12832e1dbea4be280b22fd0ea7c9b87f0d8fc51ba06e92dc62d52f804f78ebd", size = 3679479 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/81/2d/67693cc8a605a12e5975380d7ff83020dcc759351b5a066e1cced04f797b/lxml-5.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:98a3912194c079ef37e716ed228ae0dcb960992100461b704aea4e93af6b0bb9", size = 8083240 }, - { url = "https://files.pythonhosted.org/packages/73/53/b5a05ab300a808b72e848efd152fe9c022c0181b0a70b8bca1199f1bed26/lxml-5.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0ea0252b51d296a75f6118ed0d8696888e7403408ad42345d7dfd0d1e93309a7", size = 4387685 }, - { url = "https://files.pythonhosted.org/packages/d8/cb/1a3879c5f512bdcd32995c301886fe082b2edd83c87d41b6d42d89b4ea4d/lxml-5.4.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b92b69441d1bd39f4940f9eadfa417a25862242ca2c396b406f9272ef09cdcaa", size = 4991164 }, - { url = "https://files.pythonhosted.org/packages/f9/94/bbc66e42559f9d04857071e3b3d0c9abd88579367fd2588a4042f641f57e/lxml-5.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20e16c08254b9b6466526bc1828d9370ee6c0d60a4b64836bc3ac2917d1e16df", size = 4746206 }, - { url = "https://files.pythonhosted.org/packages/66/95/34b0679bee435da2d7cae895731700e519a8dfcab499c21662ebe671603e/lxml-5.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7605c1c32c3d6e8c990dd28a0970a3cbbf1429d5b92279e37fda05fb0c92190e", size = 5342144 }, - { url = "https://files.pythonhosted.org/packages/e0/5d/abfcc6ab2fa0be72b2ba938abdae1f7cad4c632f8d552683ea295d55adfb/lxml-5.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ecf4c4b83f1ab3d5a7ace10bafcb6f11df6156857a3c418244cef41ca9fa3e44", size = 4825124 }, - { url = "https://files.pythonhosted.org/packages/5a/78/6bd33186c8863b36e084f294fc0a5e5eefe77af95f0663ef33809cc1c8aa/lxml-5.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cef4feae82709eed352cd7e97ae062ef6ae9c7b5dbe3663f104cd2c0e8d94ba", size = 4876520 }, - { url = "https://files.pythonhosted.org/packages/3b/74/4d7ad4839bd0fc64e3d12da74fc9a193febb0fae0ba6ebd5149d4c23176a/lxml-5.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:df53330a3bff250f10472ce96a9af28628ff1f4efc51ccba351a8820bca2a8ba", size = 4765016 }, - { url = "https://files.pythonhosted.org/packages/24/0d/0a98ed1f2471911dadfc541003ac6dd6879fc87b15e1143743ca20f3e973/lxml-5.4.0-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:aefe1a7cb852fa61150fcb21a8c8fcea7b58c4cb11fbe59c97a0a4b31cae3c8c", size = 5362884 }, - { url = "https://files.pythonhosted.org/packages/48/de/d4f7e4c39740a6610f0f6959052b547478107967362e8424e1163ec37ae8/lxml-5.4.0-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:ef5a7178fcc73b7d8c07229e89f8eb45b2908a9238eb90dcfc46571ccf0383b8", size = 4902690 }, - { url = "https://files.pythonhosted.org/packages/07/8c/61763abd242af84f355ca4ef1ee096d3c1b7514819564cce70fd18c22e9a/lxml-5.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d2ed1b3cb9ff1c10e6e8b00941bb2e5bb568b307bfc6b17dffbbe8be5eecba86", size = 4944418 }, - { url = "https://files.pythonhosted.org/packages/f9/c5/6d7e3b63e7e282619193961a570c0a4c8a57fe820f07ca3fe2f6bd86608a/lxml-5.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:72ac9762a9f8ce74c9eed4a4e74306f2f18613a6b71fa065495a67ac227b3056", size = 4827092 }, - { url = "https://files.pythonhosted.org/packages/71/4a/e60a306df54680b103348545706a98a7514a42c8b4fbfdcaa608567bb065/lxml-5.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f5cb182f6396706dc6cc1896dd02b1c889d644c081b0cdec38747573db88a7d7", size = 5418231 }, - { url = "https://files.pythonhosted.org/packages/27/f2/9754aacd6016c930875854f08ac4b192a47fe19565f776a64004aa167521/lxml-5.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:3a3178b4873df8ef9457a4875703488eb1622632a9cee6d76464b60e90adbfcd", size = 5261798 }, - { url = "https://files.pythonhosted.org/packages/38/a2/0c49ec6941428b1bd4f280650d7b11a0f91ace9db7de32eb7aa23bcb39ff/lxml-5.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e094ec83694b59d263802ed03a8384594fcce477ce484b0cbcd0008a211ca751", size = 4988195 }, - { url = "https://files.pythonhosted.org/packages/7a/75/87a3963a08eafc46a86c1131c6e28a4de103ba30b5ae903114177352a3d7/lxml-5.4.0-cp311-cp311-win32.whl", hash = "sha256:4329422de653cdb2b72afa39b0aa04252fca9071550044904b2e7036d9d97fe4", size = 3474243 }, - { url = "https://files.pythonhosted.org/packages/fa/f9/1f0964c4f6c2be861c50db380c554fb8befbea98c6404744ce243a3c87ef/lxml-5.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:fd3be6481ef54b8cfd0e1e953323b7aa9d9789b94842d0e5b142ef4bb7999539", size = 3815197 }, - { url = "https://files.pythonhosted.org/packages/f8/4c/d101ace719ca6a4ec043eb516fcfcb1b396a9fccc4fcd9ef593df34ba0d5/lxml-5.4.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b5aff6f3e818e6bdbbb38e5967520f174b18f539c2b9de867b1e7fde6f8d95a4", size = 8127392 }, - { url = "https://files.pythonhosted.org/packages/11/84/beddae0cec4dd9ddf46abf156f0af451c13019a0fa25d7445b655ba5ccb7/lxml-5.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942a5d73f739ad7c452bf739a62a0f83e2578afd6b8e5406308731f4ce78b16d", size = 4415103 }, - { url = "https://files.pythonhosted.org/packages/d0/25/d0d93a4e763f0462cccd2b8a665bf1e4343dd788c76dcfefa289d46a38a9/lxml-5.4.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:460508a4b07364d6abf53acaa0a90b6d370fafde5693ef37602566613a9b0779", size = 5024224 }, - { url = "https://files.pythonhosted.org/packages/31/ce/1df18fb8f7946e7f3388af378b1f34fcf253b94b9feedb2cec5969da8012/lxml-5.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:529024ab3a505fed78fe3cc5ddc079464e709f6c892733e3f5842007cec8ac6e", size = 4769913 }, - { url = "https://files.pythonhosted.org/packages/4e/62/f4a6c60ae7c40d43657f552f3045df05118636be1165b906d3423790447f/lxml-5.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ca56ebc2c474e8f3d5761debfd9283b8b18c76c4fc0967b74aeafba1f5647f9", size = 5290441 }, - { url = "https://files.pythonhosted.org/packages/9e/aa/04f00009e1e3a77838c7fc948f161b5d2d5de1136b2b81c712a263829ea4/lxml-5.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a81e1196f0a5b4167a8dafe3a66aa67c4addac1b22dc47947abd5d5c7a3f24b5", size = 4820165 }, - { url = "https://files.pythonhosted.org/packages/c9/1f/e0b2f61fa2404bf0f1fdf1898377e5bd1b74cc9b2cf2c6ba8509b8f27990/lxml-5.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00b8686694423ddae324cf614e1b9659c2edb754de617703c3d29ff568448df5", size = 4932580 }, - { url = "https://files.pythonhosted.org/packages/24/a2/8263f351b4ffe0ed3e32ea7b7830f845c795349034f912f490180d88a877/lxml-5.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:c5681160758d3f6ac5b4fea370495c48aac0989d6a0f01bb9a72ad8ef5ab75c4", size = 4759493 }, - { url = "https://files.pythonhosted.org/packages/05/00/41db052f279995c0e35c79d0f0fc9f8122d5b5e9630139c592a0b58c71b4/lxml-5.4.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:2dc191e60425ad70e75a68c9fd90ab284df64d9cd410ba8d2b641c0c45bc006e", size = 5324679 }, - { url = "https://files.pythonhosted.org/packages/1d/be/ee99e6314cdef4587617d3b3b745f9356d9b7dd12a9663c5f3b5734b64ba/lxml-5.4.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:67f779374c6b9753ae0a0195a892a1c234ce8416e4448fe1e9f34746482070a7", size = 4890691 }, - { url = "https://files.pythonhosted.org/packages/ad/36/239820114bf1d71f38f12208b9c58dec033cbcf80101cde006b9bde5cffd/lxml-5.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:79d5bfa9c1b455336f52343130b2067164040604e41f6dc4d8313867ed540079", size = 4955075 }, - { url = "https://files.pythonhosted.org/packages/d4/e1/1b795cc0b174efc9e13dbd078a9ff79a58728a033142bc6d70a1ee8fc34d/lxml-5.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3d3c30ba1c9b48c68489dc1829a6eede9873f52edca1dda900066542528d6b20", size = 4838680 }, - { url = "https://files.pythonhosted.org/packages/72/48/3c198455ca108cec5ae3662ae8acd7fd99476812fd712bb17f1b39a0b589/lxml-5.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1af80c6316ae68aded77e91cd9d80648f7dd40406cef73df841aa3c36f6907c8", size = 5391253 }, - { url = "https://files.pythonhosted.org/packages/d6/10/5bf51858971c51ec96cfc13e800a9951f3fd501686f4c18d7d84fe2d6352/lxml-5.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4d885698f5019abe0de3d352caf9466d5de2baded00a06ef3f1216c1a58ae78f", size = 5261651 }, - { url = "https://files.pythonhosted.org/packages/2b/11/06710dd809205377da380546f91d2ac94bad9ff735a72b64ec029f706c85/lxml-5.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aea53d51859b6c64e7c51d522c03cc2c48b9b5d6172126854cc7f01aa11f52bc", size = 5024315 }, - { url = "https://files.pythonhosted.org/packages/f5/b0/15b6217834b5e3a59ebf7f53125e08e318030e8cc0d7310355e6edac98ef/lxml-5.4.0-cp312-cp312-win32.whl", hash = "sha256:d90b729fd2732df28130c064aac9bb8aff14ba20baa4aee7bd0795ff1187545f", size = 3486149 }, - { url = "https://files.pythonhosted.org/packages/91/1e/05ddcb57ad2f3069101611bd5f5084157d90861a2ef460bf42f45cced944/lxml-5.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1dc4ca99e89c335a7ed47d38964abcb36c5910790f9bd106f2a8fa2ee0b909d2", size = 3817095 }, - { url = "https://files.pythonhosted.org/packages/87/cb/2ba1e9dd953415f58548506fa5549a7f373ae55e80c61c9041b7fd09a38a/lxml-5.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:773e27b62920199c6197130632c18fb7ead3257fce1ffb7d286912e56ddb79e0", size = 8110086 }, - { url = "https://files.pythonhosted.org/packages/b5/3e/6602a4dca3ae344e8609914d6ab22e52ce42e3e1638c10967568c5c1450d/lxml-5.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ce9c671845de9699904b1e9df95acfe8dfc183f2310f163cdaa91a3535af95de", size = 4404613 }, - { url = "https://files.pythonhosted.org/packages/4c/72/bf00988477d3bb452bef9436e45aeea82bb40cdfb4684b83c967c53909c7/lxml-5.4.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9454b8d8200ec99a224df8854786262b1bd6461f4280064c807303c642c05e76", size = 5012008 }, - { url = "https://files.pythonhosted.org/packages/92/1f/93e42d93e9e7a44b2d3354c462cd784dbaaf350f7976b5d7c3f85d68d1b1/lxml-5.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cccd007d5c95279e529c146d095f1d39ac05139de26c098166c4beb9374b0f4d", size = 4760915 }, - { url = "https://files.pythonhosted.org/packages/45/0b/363009390d0b461cf9976a499e83b68f792e4c32ecef092f3f9ef9c4ba54/lxml-5.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0fce1294a0497edb034cb416ad3e77ecc89b313cff7adbee5334e4dc0d11f422", size = 5283890 }, - { url = "https://files.pythonhosted.org/packages/19/dc/6056c332f9378ab476c88e301e6549a0454dbee8f0ae16847414f0eccb74/lxml-5.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:24974f774f3a78ac12b95e3a20ef0931795ff04dbb16db81a90c37f589819551", size = 4812644 }, - { url = "https://files.pythonhosted.org/packages/ee/8a/f8c66bbb23ecb9048a46a5ef9b495fd23f7543df642dabeebcb2eeb66592/lxml-5.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:497cab4d8254c2a90bf988f162ace2ddbfdd806fce3bda3f581b9d24c852e03c", size = 4921817 }, - { url = "https://files.pythonhosted.org/packages/04/57/2e537083c3f381f83d05d9b176f0d838a9e8961f7ed8ddce3f0217179ce3/lxml-5.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:e794f698ae4c5084414efea0f5cc9f4ac562ec02d66e1484ff822ef97c2cadff", size = 4753916 }, - { url = "https://files.pythonhosted.org/packages/d8/80/ea8c4072109a350848f1157ce83ccd9439601274035cd045ac31f47f3417/lxml-5.4.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:2c62891b1ea3094bb12097822b3d44b93fc6c325f2043c4d2736a8ff09e65f60", size = 5289274 }, - { url = "https://files.pythonhosted.org/packages/b3/47/c4be287c48cdc304483457878a3f22999098b9a95f455e3c4bda7ec7fc72/lxml-5.4.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:142accb3e4d1edae4b392bd165a9abdee8a3c432a2cca193df995bc3886249c8", size = 4874757 }, - { url = "https://files.pythonhosted.org/packages/2f/04/6ef935dc74e729932e39478e44d8cfe6a83550552eaa072b7c05f6f22488/lxml-5.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1a42b3a19346e5601d1b8296ff6ef3d76038058f311902edd574461e9c036982", size = 4947028 }, - { url = "https://files.pythonhosted.org/packages/cb/f9/c33fc8daa373ef8a7daddb53175289024512b6619bc9de36d77dca3df44b/lxml-5.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4291d3c409a17febf817259cb37bc62cb7eb398bcc95c1356947e2871911ae61", size = 4834487 }, - { url = "https://files.pythonhosted.org/packages/8d/30/fc92bb595bcb878311e01b418b57d13900f84c2b94f6eca9e5073ea756e6/lxml-5.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4f5322cf38fe0e21c2d73901abf68e6329dc02a4994e483adbcf92b568a09a54", size = 5381688 }, - { url = "https://files.pythonhosted.org/packages/43/d1/3ba7bd978ce28bba8e3da2c2e9d5ae3f8f521ad3f0ca6ea4788d086ba00d/lxml-5.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0be91891bdb06ebe65122aa6bf3fc94489960cf7e03033c6f83a90863b23c58b", size = 5242043 }, - { url = "https://files.pythonhosted.org/packages/ee/cd/95fa2201041a610c4d08ddaf31d43b98ecc4b1d74b1e7245b1abdab443cb/lxml-5.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:15a665ad90054a3d4f397bc40f73948d48e36e4c09f9bcffc7d90c87410e478a", size = 5021569 }, - { url = "https://files.pythonhosted.org/packages/2d/a6/31da006fead660b9512d08d23d31e93ad3477dd47cc42e3285f143443176/lxml-5.4.0-cp313-cp313-win32.whl", hash = "sha256:d5663bc1b471c79f5c833cffbc9b87d7bf13f87e055a5c86c363ccd2348d7e82", size = 3485270 }, - { url = "https://files.pythonhosted.org/packages/fc/14/c115516c62a7d2499781d2d3d7215218c0731b2c940753bf9f9b7b73924d/lxml-5.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:bcb7a1096b4b6b24ce1ac24d4942ad98f983cd3810f9711bcd0293f43a9d8b9f", size = 3814606 }, +version = "6.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/3b/aab6728cae887456f409b4d75e8a01856e4f04bd510de38052a47768b680/lxml-6.1.1.tar.gz", hash = "sha256:ba96ae44888e0185281e937633a743ea90d5a196c6000f82565ebb0580012d40", size = 4197430 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/b0/83f481780d1548750b8ce2ec824073deef2f452d9cd1a6faff8507e3d16d/lxml-6.1.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:53b7d2b7a10b1c35c0a5e21e9224accf60c1bbfba523990732e521b2b73adef2", size = 8526461 }, + { url = "https://files.pythonhosted.org/packages/b9/d5/30fa0f808002c7329397bfbb24e306789c0b29f04aa5842c07b174b4216f/lxml-6.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ff3f333630ab480244a1bff72043e511a91eb22e7595dead8653ee5612dd8f3d", size = 4595375 }, + { url = "https://files.pythonhosted.org/packages/4f/d2/edb71cf0e561581a7c5eb2626244320eb04e9f8ce6d563184fd668b45073/lxml-6.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a4bbea04c97f6d78a48e3fbc1cb9116d2780b1b39e03a23f6eb9b603fd61f510", size = 4923654 }, + { url = "https://files.pythonhosted.org/packages/4c/77/1bc7eeb0de4577d783fb625aa092cc9357883bba35845a3666bf1259f3dc/lxml-6.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:db1d75f6617a49c1c01bc7023713e0ff59ab32c9579ae62a7674c0e34f3b0b0a", size = 5067921 }, + { url = "https://files.pythonhosted.org/packages/1b/3c/c0690d74bd2bc17bc03b5b0d093569ead597dd0bfa088bf99eef8c24e19c/lxml-6.1.1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a12689be69a28ddaa0ab99a5a1137da2afd5f8f16df7b5680b66f616d3eda1d", size = 5002456 }, + { url = "https://files.pythonhosted.org/packages/66/8d/d1b3271af0c0f1e27e8472a849e4d2c65bc7766884b9ad2da9e76e145c88/lxml-6.1.1-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b73c339ae29b90fd2d06e58ebd555a751bde9cd6bbd36cc0281b9a2c94e9d8", size = 5202776 }, + { url = "https://files.pythonhosted.org/packages/7a/45/689824ffb237fd10125ad273f32b28ff04dc6203c2822c85ff65a93df65e/lxml-6.1.1-cp311-cp311-manylinux_2_28_i686.whl", hash = "sha256:752d3bbfe874715ccd0aec7f88d7fc623c0f1fd7aa7b3238a084e017bad2a009", size = 5329945 }, + { url = "https://files.pythonhosted.org/packages/5d/c0/ef73af53767e958fd87d437c170f272e2f6e6c0f854939f133a895f1e711/lxml-6.1.1-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:6b1761fbf9ec984e2e9d9c589ef5f5fd684b7c19f92aadd567a26c5224958db6", size = 4659237 }, + { url = "https://files.pythonhosted.org/packages/a0/5e/e1158e40397585e91cb0472374a1f63d0926a1ddeaa92f13d1a1ffe306d5/lxml-6.1.1-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d680fbcb768404c601ecb43519ecd8461f6954cb11c06a78962f666832ccfca8", size = 5265904 }, + { url = "https://files.pythonhosted.org/packages/a0/16/8687e5d1400ed1c0bc41dace232ebb7553952b618ea1f2e5fb6e2cfbbe23/lxml-6.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:162af1091cd785f2f27e62d3547ae9bc58ec5c86dd314d67021fd02463708d83", size = 5045225 }, + { url = "https://files.pythonhosted.org/packages/ca/18/d877bd1ae2e5ffdfd4836565aba350db31feb2f2656d6ce70316ed66a05e/lxml-6.1.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e9308ff8241c532df3f3e570f9a5aeed6c853f888512ba4b75638d7c11c95ef6", size = 4712721 }, + { url = "https://files.pythonhosted.org/packages/44/4d/1f44fd1d770b10dacbf6b5c6e520f4d6e0708744930f719dc04e67cab981/lxml-6.1.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5f6994074ebae6ffb04447268e37dc16edc304f9859cf91acb86e0af6c1b395c", size = 5252549 }, + { url = "https://files.pythonhosted.org/packages/64/5d/1d66b84f850089254c230ef6ea6b267a5a54e2e179a5d960036a05d501d7/lxml-6.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:80c2dfadb855da477cf73373ad29a333535dedb9b12bad02c9814c8e2b43bf08", size = 5226877 }, + { url = "https://files.pythonhosted.org/packages/ad/00/84c4b5302d42a2d0184f38d538c8a197f33b52a50bd4f7bcfe990bce3036/lxml-6.1.1-cp311-cp311-win32.whl", hash = "sha256:30a89d3ac8faec007453fb541f3f46807eeec88edd5826f6e3fe001752a2c621", size = 3594072 }, + { url = "https://files.pythonhosted.org/packages/61/9d/2e2f7d876349f45e0f3e29f72da311668853d59b58d473a2dea4f0160135/lxml-6.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:abbefa31eee84842140f67acef1c828e28bba8bbf0c3bc6e5492a9af88152c28", size = 4025469 }, + { url = "https://files.pythonhosted.org/packages/b0/d5/570e6390e4110331e6208b2ba83d1482cc9146808ee118b22824a34c1070/lxml-6.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:dcb292aa7fe485ceff7af4f92e46c5af397daec5dff64871a528f0fc47a3cc5b", size = 3667640 }, + { url = "https://files.pythonhosted.org/packages/6a/6e/c4add832b6fc1e887125b96f880d7b9b70aae5248718e046b1704bcac4b9/lxml-6.1.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:104c09bda8d2a562824c0e319d0768ce26a779b7601e0931d33b09b53c392ef7", size = 8570821 }, + { url = "https://files.pythonhosted.org/packages/22/00/ff3009c88e65de8011630acf8ab5a09cb2becd2aaf47fba2f3449f6224e9/lxml-6.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:25c6997a9a534e016695a0ba06b2f07945de682731ff01065b6d5a4474179da1", size = 4624252 }, + { url = "https://files.pythonhosted.org/packages/42/95/bb63f0fd62e554fe078e1fb3c8fe9083c14ddc7ad7fa178d10e57e071ac7/lxml-6.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c921ba5c51e4e9f63b8b00267d06566e1f63407408a0496da2d1d0bfc819c7fc", size = 4930746 }, + { url = "https://files.pythonhosted.org/packages/eb/99/0013e8d9b5960f4f041cf0b73e2f80c23eb5205b1f7bfb20203243651359/lxml-6.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:54a7f95e4de5fb94e2f9f4b9055c6ba33bf3d628fd77a1d647c5923caa2cdcdc", size = 5093723 }, + { url = "https://files.pythonhosted.org/packages/29/91/317b332636bfc7bddcff828d41b3307f50043f4b237e40849c333d80fa1a/lxml-6.1.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f2ec43df44b1f76249ee0a615334f9b5b060e1c8bd90e706dad2d14d02f383", size = 5005557 }, + { url = "https://files.pythonhosted.org/packages/42/2f/cc9bf06afe70f9c9093ae60855d9759da9db601ec4080f7473319666ffd7/lxml-6.1.1-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:70ef8a7e102a1508f8121aae5b0867abd663f72c14f0a9c937e6554cb4587b7b", size = 5631036 }, + { url = "https://files.pythonhosted.org/packages/08/f6/af32e23e563971ffb0fb86be52bc5be5c2c118858ffc119bf6a9039b173d/lxml-6.1.1-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ebe6af670449830d6d9b752c256a983291c766a1365ba5d5460048f9e33a7818", size = 5240367 }, + { url = "https://files.pythonhosted.org/packages/78/83/8555d40948b09ce86f1bd0c68a7ac31d07b1929f92cc1b074006c97ef2d2/lxml-6.1.1-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:27acc820660aaffa4f7c087f29120e12980f7779d56d8492d263170111284740", size = 5350171 }, + { url = "https://files.pythonhosted.org/packages/63/75/5d92da93729b7bad783689e6496049fa40927b45bec7bf183c981de3ca70/lxml-6.1.1-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:1db753c9115ec7100d073b744d17e25e88a8f90f5c39b2f5dd878149af59671f", size = 4694874 }, + { url = "https://files.pythonhosted.org/packages/c5/b5/3aad415a9a25b822e783f15deeb4dffccf5113030f1afa2222dd929313d9/lxml-6.1.1-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c4f469aebd783bb741c2ecb2a681008fd26bfe5c16a9a72ed5467f834e810df2", size = 5244492 }, + { url = "https://files.pythonhosted.org/packages/f1/a1/5fcf7eb9904b80086aa47dcf0027de07b1bb990afad2e6823144c368ae04/lxml-6.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:766b010012d59470072c1816b5b6c69f1d243e5db36ea5968e94accf430a4635", size = 5048232 }, + { url = "https://files.pythonhosted.org/packages/77/74/1f601b63c7a69fcdf10fa9b148c81da8442204194f6c55509cc485c786b9/lxml-6.1.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b8d812c6011c08b8111a15e54dd990b8923692d80adf35488bee34026c35accf", size = 4777023 }, + { url = "https://files.pythonhosted.org/packages/a2/b9/7a78f51aec95b1bf780d78e12705a9f6533284f8693dc5c0e6724fa53d3f/lxml-6.1.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:fe0306bd29505a9177aac19f1877174b0e7422c222a59f70b2cd41633448c3dc", size = 5645773 }, + { url = "https://files.pythonhosted.org/packages/a5/6e/98a7b7ad54e4e74fa1f20fff776913980619d0ebe5558232d7da6580bdd8/lxml-6.1.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5ba186ad207446c65d3bb3d3e0412b032b1d9f595e59861e2354798c5703d955", size = 5233088 }, + { url = "https://files.pythonhosted.org/packages/65/d1/bc0ed2427bf609f2ee10da303a6a226f9c8bce94f945dc29a32ce55de6e4/lxml-6.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aa366a1e55b8ebfe8ca8ddc3cfe75c8ebade181aeb0f661d0cb05986b647f72a", size = 5260995 }, + { url = "https://files.pythonhosted.org/packages/69/8b/6772e1a4b513fc50a8d931f19edde0e13ae6918510a1e13ff67864f3e5ed/lxml-6.1.1-cp312-cp312-win32.whl", hash = "sha256:126c93f7f56f0eda92f6d8c619edc463a4f23d9252f1c9d0405a76f25fa9f11a", size = 3596382 }, + { url = "https://files.pythonhosted.org/packages/1b/89/45198e9624762af2dfd2cb8782598477ceb29f6e59caab560388ae1f4ec1/lxml-6.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:26e6eda8d38c1fcab1090dd196ee87cbd13788e531937610e2589085de074e77", size = 3997255 }, + { url = "https://files.pythonhosted.org/packages/90/a9/7a54b6834088d9ae528a7b780584ba6a39a9457b0ac330479f20ffbc9449/lxml-6.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:6540377fbd53fe1b629172288c464fb18db11ce1fa7dc15891da10aa9dcc3e7f", size = 3659610 }, + { url = "https://files.pythonhosted.org/packages/a5/eb/7e6f37c5584ccbb2ff267f56fd0339016938c1c8684cfefab9b33ffc2f36/lxml-6.1.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:68a9198d0fc122d14bb76837de9aa80cf84caed990b5b237f532ed87d3706736", size = 8559780 }, + { url = "https://files.pythonhosted.org/packages/a1/36/587c2521cf23a2cd6c9c22108aa7528f683a1f195ed7ccd23a4b1786ad36/lxml-6.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7d47866cb32fb503450b6edc9df355d10dc49836af2e89901bd6ac6b0896d9d9", size = 4618006 }, + { url = "https://files.pythonhosted.org/packages/6e/ca/ab7bfe2bf4c972af5e7878262845ead3a24a929a9b04bc11c7c1ece6c82a/lxml-6.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb7c9811bfaa8b1ed5ed319f5d370dfbcaa59d52ea64be2a5a85e18195930354", size = 4924139 }, + { url = "https://files.pythonhosted.org/packages/6b/55/a0c72851dfee5ecc689f949723a73dea457758912542cb955b108eaf0d8f/lxml-6.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:762ff394d5bd56da0cf034a23dcce4e13923f15321a2adfa2ac00201dc6d3fca", size = 5082329 }, + { url = "https://files.pythonhosted.org/packages/f0/b6/0608f7d61a3b96cc67e5648a3d906e31a5082093e10e7be65b3886289938/lxml-6.1.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a088f287f7d8275a33c07f2cac6c50b9319309a0200a39e7e75d80c707723099", size = 4993564 }, + { url = "https://files.pythonhosted.org/packages/4c/66/ae227524b066d29d55bf0b453d93d2d793c40218657d643dcbbca13b8faf/lxml-6.1.1-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e902da4b04e6b52e5893900d4b8ab46068f75f3561f01bf1080957f9fd932ed6", size = 5613467 }, + { url = "https://files.pythonhosted.org/packages/a6/76/dbe4a00b50385e40194231dcfe5a12c059de7cf90e89c83407d2b085b719/lxml-6.1.1-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d4962d4c66bf830a7e59ed6cfc17d148149898a3aefa8ec6e59763e6e3ed085", size = 5228304 }, + { url = "https://files.pythonhosted.org/packages/1c/01/00b1b8442ed2041793336868ba0b9ea4b13d7da7c085c6404c207a63bf79/lxml-6.1.1-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:581d4c8ae690a6609e64862dd6b7c2489635c2d13907fc2b20f2bc200ff1d21e", size = 5341607 }, + { url = "https://files.pythonhosted.org/packages/63/36/1ad29931e9a4638bb707869f01d423a6c815f82152138d1a40dfcfde2b95/lxml-6.1.1-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:876e1ff5930ed8bf295ec5ef9a8155e9b6b1876bbf1deed8b3a8069311875a8f", size = 4700168 }, + { url = "https://files.pythonhosted.org/packages/3c/d1/a9536cecf9be18a0dc72d32bead283a2332d1ffebd2dd3ac70ce444686e5/lxml-6.1.1-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9eb9b5a968f6e0f6d640092a567e14529ff8cea2e29d00da6f78a79fa49f013c", size = 5232487 }, + { url = "https://files.pythonhosted.org/packages/0e/77/b4fb1e03bf5d130e879214d3100092e386418807fb74dd0adc4b0a48f351/lxml-6.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:aa49e06d94aba782c6a02eecb7e507969e7e7a41b267f1b359bb35585f295d5b", size = 5044231 }, + { url = "https://files.pythonhosted.org/packages/26/4c/d00daeeb0a5530c4028a9232aa1b93db3ef4ed2158c116ea73c79a9765b3/lxml-6.1.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:70cdfd80589d59e43e18005dd7244e8895e93db8ab6a620b7e23df5445a4e3d2", size = 4769450 }, + { url = "https://files.pythonhosted.org/packages/ed/6a/715a3a8d156ce42f29cf014706f5410c2ff3b02267774110fc23266409fe/lxml-6.1.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:aad9aa39483ed8ec44d6d2e59e5b98a0d80676ef0d92f44bfc374836111f62f5", size = 5635874 }, + { url = "https://files.pythonhosted.org/packages/45/37/0544bc21dde2a88f3a17b504e6fc79c0e01d25a33c2f6079724e9e72b9c7/lxml-6.1.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d49514be2f28d895c38cf9d2b72d7b9a07d00314519f456c0b50b53cfcf4c785", size = 5223987 }, + { url = "https://files.pythonhosted.org/packages/4d/f8/f6a5e8185bcb28c2befae3d31f8e3df3b811cb0f47746517a81279fcafe1/lxml-6.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:47402e62c52ff5988c1e8c6c63177f5708bccf48e366dea4e3dcf1e645e04947", size = 5250276 }, + { url = "https://files.pythonhosted.org/packages/c7/f2/1a2b9f1b7a49d45495369be7ef9ad05b262930f2eab3e3145706fca8083f/lxml-6.1.1-cp313-cp313-win32.whl", hash = "sha256:3483644525531e1d5762b0c44a8e18b6efba321b6dcf8a8952de10b037618bca", size = 3596903 }, + { url = "https://files.pythonhosted.org/packages/e6/99/f4ffb024f238eec2131aaa09f3278fb6129cf892741bf68e1fc1afb8c100/lxml-6.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:a10bd2fd62e8ce916ececb342f348f190724a098c1faa056fdfb2a22ad5e8660", size = 3995869 }, + { url = "https://files.pythonhosted.org/packages/d1/53/70eb8c5c6037f27448f1e3c54ebede9545a801ae63f0a7254afca4fe8e45/lxml-6.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:424aa57aca0897eb922aef34395bd1289b3b6f04e6bae20ea123c0c7e333cffc", size = 3658490 }, + { url = "https://files.pythonhosted.org/packages/13/e2/2e325795566de01d0d7c3bb57d3c370616b2d07b01214e84eec5d3b10963/lxml-6.1.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:19b7ab10b210b0b3ad7985d9ac4eb66ab09a90b20fe6e2f7ba55d01a234345d0", size = 8577146 }, + { url = "https://files.pythonhosted.org/packages/93/cf/5630b5e4be7d2e6bee8efe83865c925221103cf0221303b104ce134b01e2/lxml-6.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c08e5c694306507275f2290073350c4f32e383db15213b2c69e7ff39c1193840", size = 4623866 }, + { url = "https://files.pythonhosted.org/packages/d2/51/3904907c063451cf8d4a5c9fe0cad95fa1f4ec57f4e3884fa0731bd7a305/lxml-6.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:74a9717fd0d82effef5c2854f0d917231d5324b5a3eb7275c43ac9fa32f97a14", size = 4950022 }, + { url = "https://files.pythonhosted.org/packages/94/cd/9c7611a51c37a2830928405817cc5d56a97f64fab83cc3f628748b135749/lxml-6.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efe0374196335f93b53269acd811b944f2e6bdc88e8894f214bd636455484909", size = 5086695 }, + { url = "https://files.pythonhosted.org/packages/da/d6/24e3b5906abb0b674ff2ae195bc3ce59708df2bcd17cf17703b2d7dd643a/lxml-6.1.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac931cdc9442c1763b8a8f6cd62c0c938737eafc5be75eff88df55fc73bc0d00", size = 5031642 }, + { url = "https://files.pythonhosted.org/packages/2d/db/6ec54f99019838bff54785c51da07f189eb4676861c5f2730962b0d8d665/lxml-6.1.1-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:aee395f5d0927f947758b4ec119fd5fc8ec71f07a1c5c52077b30b04c0fa6955", size = 5647338 }, + { url = "https://files.pythonhosted.org/packages/42/3d/ef4dcfffd22d27a61805d8ed9f7fb888495bc6aa88648fa07c1eaa5586b6/lxml-6.1.1-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9395002973c827b3ed67db77e6ec09f092919a587022174554096a269378fb13", size = 5239528 }, + { url = "https://files.pythonhosted.org/packages/62/bb/37fb3f0dff146bdcfa78eec47879273820b2a0bf350ec236ce14bd0b1c26/lxml-6.1.1-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:73bc2086f141224ebddb7fc5c6a36ca58b31b94b561e1dfe8e073e3270fad1e7", size = 5350730 }, + { url = "https://files.pythonhosted.org/packages/90/42/43253f168388df4fae1f38c01df36ddb9bee39e2048167b54cdcbae85ea3/lxml-6.1.1-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:3779def59032b81e44a5f70096ef6bf2082f8d901937dca354474ba09782e245", size = 4697530 }, + { url = "https://files.pythonhosted.org/packages/eb/a8/c5a8504f81bbdfc8e7094c2c850cdb4ed6777fc4d5ddd9e5ab819f3b0d54/lxml-6.1.1-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:86c89b9d55ebf820ad7c90bc533410f0d098054f293351f10603c0c46ff598f5", size = 5250670 }, + { url = "https://files.pythonhosted.org/packages/77/b7/c7e76ab18744d75e21f320ebf9ff9d1ceae2b54dd431ea5a64caf26c9672/lxml-6.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19607c6bbff2a44cf3fe8250abccd20942d3462473e0a721d01d379ed017e462", size = 5084485 }, + { url = "https://files.pythonhosted.org/packages/31/31/b35c53f8ef7b7c31cacd23d3638652fff7bcd1deb6eedb709ab43b685908/lxml-6.1.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:c6ed5141a5c7507cf3ee76bd363b0d6f801e3321adc35b5d825a23115faa5465", size = 4737635 }, + { url = "https://files.pythonhosted.org/packages/d9/06/31f23c813a7fe8e0cb1b175e915b08c9bf4e86d225b210feadbdbe519667/lxml-6.1.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:62aeb7e85b5d60320b9d77eef2e773994e2c0ce10121b277e0a19804e1654a5a", size = 5670681 }, + { url = "https://files.pythonhosted.org/packages/1a/bc/ce619bccc89b1fd9ad8a8e1330ee3f3beff9f2ff95b712d7bbcdd6e22fc3/lxml-6.1.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b1b963fd8f5caa68e99dfae060d54de1fe9cba899b8718b44a00cdca53c3e590", size = 5238229 }, + { url = "https://files.pythonhosted.org/packages/2f/5d/b329acbbedc0b619ebc2be6cf7ee9ed07e80892c88d4dfd612c33805789a/lxml-6.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:63876be28efefa04a1df615b46770e82042cce445cfdce55160522f57b231ccb", size = 5264191 }, + { url = "https://files.pythonhosted.org/packages/d6/85/be36fb1425b30db3c3f9df75fe86343ebffb79e6320bd7f588e25bfeac39/lxml-6.1.1-cp314-cp314-win32.whl", hash = "sha256:7f7a92e8583f06b1fd49d01158143b8461cfcd135dcb10ec807270a3051bd603", size = 3657202 }, + { url = "https://files.pythonhosted.org/packages/b8/ce/3cf9a827342269f54d405a6202397de63f07c69cbd6ce7d183a3f0cba1e9/lxml-6.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:b2d444f2e66624d68e9c6b211e28a76e22fff5fcabcfff4deac18b529b7d4137", size = 4064497 }, + { url = "https://files.pythonhosted.org/packages/d9/3e/1a957bde8f0760039e627f94699f82caa782c9d838d86c3d28245ee67212/lxml-6.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:3fd9728a2735fda14f4e8235830c86b539e9661e849665bf926d3f867943b4bf", size = 3741991 }, + { url = "https://files.pythonhosted.org/packages/78/b2/00ed55b3a2efa4658fb795c38d1090ec9b3e8a6c3683d4441fa517f09c3b/lxml-6.1.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:787b2496d0dbe8cd180984e8d29e3a6f76e7ea34db781cb3bd55e4ba1ef8b4ee", size = 8827545 }, + { url = "https://files.pythonhosted.org/packages/c0/73/74573db19baa618d5f266f2407898b087ff6927115b00b71e5fc1b700847/lxml-6.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2c8daa471358dc2d6fcf02165e80ec68f77871a286df95bc5cc3816153b0fd2c", size = 4735736 }, + { url = "https://files.pythonhosted.org/packages/16/02/6f7061f4f95f51e545d48e87647c54791d204a4e881be4156e7a26ba5338/lxml-6.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:acd7d70b64c0aae0c7922cca83d288a16f5f6da523637697872253415269baef", size = 4970291 }, + { url = "https://files.pythonhosted.org/packages/b0/02/55fc057d8283427dea7d6edb102e7a840239c77a64a983d92f62a304c0e9/lxml-6.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4f0dd2f01f9f8a89f565d000e03abcf0a13d692a346c8d22f628d49af098777a", size = 5102822 }, + { url = "https://files.pythonhosted.org/packages/e4/48/8e1cf78d89d66850121d9255a2a24414c98f775da93b90cf976956c24b14/lxml-6.1.1-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b7e8a14c8634bf6f7a568634cb395305a6d964aeb5b7ee32248094bed3a7e2c", size = 5027923 }, + { url = "https://files.pythonhosted.org/packages/ed/00/0632a0647612c8af24d26997b3b961397daa9d5b2581444805933629a4cb/lxml-6.1.1-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:86281fbdd6a8162756f8d603f37e3435bfa38043adb79c6dc6a2dfee065e7525", size = 5595843 }, + { url = "https://files.pythonhosted.org/packages/bc/86/ab008a7dc360711b66858d61c80a5979a70a09f2aa2b05d9698df80b803d/lxml-6.1.1-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5d7152ec39ca7c402d8fb9bad86140a15b9503bd0c54484e3f1bbe3dd37ceca", size = 5224515 }, + { url = "https://files.pythonhosted.org/packages/75/c6/2702ff375e728e34f56d9a45339a9cf7e4427e917f542225242d63a05afa/lxml-6.1.1-cp314-cp314t-manylinux_2_28_i686.whl", hash = "sha256:88d8cb75b9d82858497a5393e3c63cfbf03035225e4b35a49ed7ccb151e4dc0e", size = 5312511 }, + { url = "https://files.pythonhosted.org/packages/b7/57/a5807c98f87a86f10ef9ffab35516df7c0f0c4b6d5d33e9f608ab9c04a31/lxml-6.1.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f64ec5397ea6a41fc1b4af0380d79b44a755b5531dcaccd9940fb260dca93038", size = 4639206 }, + { url = "https://files.pythonhosted.org/packages/1f/e1/8a0a2c35734812395f4da4eaf33748a7e5705bfb2a58b128da764339d5ec/lxml-6.1.1-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d34bbf07dbc7ca5970671b1512e928991fb5e9d95365636c9b2d8b4f53af405e", size = 5232404 }, + { url = "https://files.pythonhosted.org/packages/c2/e2/0e6a4dd5ad84d01d99aa7bae7cfefd4a760a0e0f8176818241de17d9b6c0/lxml-6.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:17e0e18d4ad8adbd0399291bc44845b69d9dd68439a3cdebdf35ff902ec05072", size = 5083769 }, + { url = "https://files.pythonhosted.org/packages/a0/7e/161f33d463f6ffc1c7679104b65086dea120080d49dde4d238f015aaee2f/lxml-6.1.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:3ab541146f1f6968c462d6c2ac495148e8cdba2f8347700b2141b6ec5a75bf52", size = 4758936 }, + { url = "https://files.pythonhosted.org/packages/f1/fb/2369825e3f6ca99305bf9f7b7085fda91c8b0922a89e54d900974aa3ef85/lxml-6.1.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2a0217714657e023ef4293500f65aa20fce6164c8fd6b08fa5bd4a859fb14b9b", size = 5620296 }, + { url = "https://files.pythonhosted.org/packages/30/90/d61e383146f74c5ab683947ea14dc7b82778838ab9b95ea73a23b60d0191/lxml-6.1.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:05a82eb6e1530a64f26225b55cbd178113bd0b5af1c2b625f25e5296742c26d2", size = 5228598 }, + { url = "https://files.pythonhosted.org/packages/76/2d/2dafd8149e94b05bb070690efd5bb2680720681e03ff03fc57d2b70a1105/lxml-6.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9e36f163528fc50cbef305f02a5fd66d404edf7049cdaff211dbc2cba5a7013e", size = 5247845 }, + { url = "https://files.pythonhosted.org/packages/ce/68/b30e913340c380ddac9580c6e6230991fc37240ec4f64704833e4f3e2769/lxml-6.1.1-cp314-cp314t-win32.whl", hash = "sha256:649dda677cf3bd6ac9ae14007ba0c824ded8ce5808b53fc7431d9140399118c1", size = 3897345 }, + { url = "https://files.pythonhosted.org/packages/3c/4e/9eb2af5335545f9fbcd7af57bcf87c6025d31eaa31b14ec184a6c8675328/lxml-6.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:793033d6c5cdf33a573f910d9bea14ef8f5771820411d118da8e1182edb53d5e", size = 4393350 }, + { url = "https://files.pythonhosted.org/packages/7f/2c/0f1e93c636720e8a3eb59af2bfda99d98b55891e1c53bc30c2e0e865f01b/lxml-6.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:58bb955caba94e467d2a96da17660d2d704e0675894cba21ab8a775b8621fd1c", size = 3817223 }, + { url = "https://files.pythonhosted.org/packages/b5/32/86a3f0f724a3a402d4627937a7fc27b160e45e7012b4adf47f6e1e844511/lxml-6.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:31033dc34636ea6b7d5cc11b1ddbda78a14de858ba9d3e1ed4b69a3085bc521e", size = 3930127 }, + { url = "https://files.pythonhosted.org/packages/40/44/d832e82af08723761556d004b1d04d281c09f9a8cecd7d3148548c9941a3/lxml-6.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3893c14c4b6ac5b2d54ba8cf03e99fe5104e592de491f19bd6b82756c09f8004", size = 4210769 }, + { url = "https://files.pythonhosted.org/packages/6d/39/0dc5949f759ed7d951e0bb8c2f2d9d7aca1908d22352fa84a8afd2ea54af/lxml-6.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c07da4cebf6889f03ebac8d238f62318e29f495de0aa18a51ea14e61ae907e2e", size = 4318163 }, + { url = "https://files.pythonhosted.org/packages/e6/fb/8ab3845fe046ba4cbf74536bcf6801a774b7caf4350de1c5d37f1f0a9e90/lxml-6.1.1-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f6f0ce10945fab9c4c06ce14e22af9059d1a87493a9af4501a5b0b9187e21cf2", size = 4250945 }, + { url = "https://files.pythonhosted.org/packages/68/1b/7553ab136894374ffae8851ec06f98f511cd8e66246e41b6be059d0a7289/lxml-6.1.1-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f8844cd288697c6425c9beba919302241e3278871dc6519515e72b04e987abcf", size = 4401664 }, + { url = "https://files.pythonhosted.org/packages/db/a4/441aee36c6f6b249823d20fd91f9be9ab89d7c5a8ae542a4a4ca6d342d56/lxml-6.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:ed21202aec73cda4d55d1ce57b389aadb90ffb044e6cd1080b8347efe1b1ec84", size = 3508989 }, ] [[package]] name = "markdown-it-py" -version = "4.0.0" +version = "4.2.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 } +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454 } wheels = [ - { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321 }, + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687 }, ] [package.optional-dependencies] @@ -1997,14 +2133,14 @@ wheels = [ [[package]] name = "mdit-py-plugins" -version = "0.5.0" +version = "0.6.1" 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 } +sdist = { url = "https://files.pythonhosted.org/packages/59/fc/f8d0863f8862f25602c0404d75568e89fb6b4109804645e5cdfb1be5cf56/mdit_py_plugins-0.6.1.tar.gz", hash = "sha256:a2bca0f039f39dbd35fb74ae1b5f998608c437463371f0ff7f49a19a17a114d0", size = 56114 } 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 }, + { url = "https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl", hash = "sha256:214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d", size = 66663 }, ] [[package]] @@ -2018,50 +2154,50 @@ wheels = [ [[package]] name = "msgspec" -version = "0.20.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ea/9c/bfbd12955a49180cbd234c5d29ec6f74fe641698f0cd9df154a854fc8a15/msgspec-0.20.0.tar.gz", hash = "sha256:692349e588fde322875f8d3025ac01689fead5901e7fb18d6870a44519d62a29", size = 317862 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/03/59/fdcb3af72f750a8de2bcf39d62ada70b5eb17b06d7f63860e0a679cb656b/msgspec-0.20.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:09e0efbf1ac641fedb1d5496c59507c2f0dc62a052189ee62c763e0aae217520", size = 193345 }, - { url = "https://files.pythonhosted.org/packages/5a/15/3c225610da9f02505d37d69a77f4a2e7daae2a125f99d638df211ba84e59/msgspec-0.20.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23ee3787142e48f5ee746b2909ce1b76e2949fbe0f97f9f6e70879f06c218b54", size = 186867 }, - { url = "https://files.pythonhosted.org/packages/81/36/13ab0c547e283bf172f45491edfdea0e2cecb26ae61e3a7b1ae6058b326d/msgspec-0.20.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:81f4ac6f0363407ac0465eff5c7d4d18f26870e00674f8fcb336d898a1e36854", size = 215351 }, - { url = "https://files.pythonhosted.org/packages/6b/96/5c095b940de3aa6b43a71ec76275ac3537b21bd45c7499b5a17a429110fa/msgspec-0.20.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bb4d873f24ae18cd1334f4e37a178ed46c9d186437733351267e0a269bdf7e53", size = 219896 }, - { url = "https://files.pythonhosted.org/packages/98/7a/81a7b5f01af300761087b114dafa20fb97aed7184d33aab64d48874eb187/msgspec-0.20.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b92b8334427b8393b520c24ff53b70f326f79acf5f74adb94fd361bcff8a1d4e", size = 220389 }, - { url = "https://files.pythonhosted.org/packages/70/c0/3d0cce27db9a9912421273d49eab79ce01ecd2fed1a2f1b74af9b445f33c/msgspec-0.20.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:562c44b047c05cc0384e006fae7a5e715740215c799429e0d7e3e5adf324285a", size = 223348 }, - { url = "https://files.pythonhosted.org/packages/89/5e/406b7d578926b68790e390d83a1165a9bfc2d95612a1a9c1c4d5c72ea815/msgspec-0.20.0-cp311-cp311-win_amd64.whl", hash = "sha256:d1dcc93a3ce3d3195985bfff18a48274d0b5ffbc96fa1c5b89da6f0d9af81b29", size = 188713 }, - { url = "https://files.pythonhosted.org/packages/47/87/14fe2316624ceedf76a9e94d714d194cbcb699720b210ff189f89ca4efd7/msgspec-0.20.0-cp311-cp311-win_arm64.whl", hash = "sha256:aa387aa330d2e4bd69995f66ea8fdc87099ddeedf6fdb232993c6a67711e7520", size = 174229 }, - { url = "https://files.pythonhosted.org/packages/d9/6f/1e25eee957e58e3afb2a44b94fa95e06cebc4c236193ed0de3012fff1e19/msgspec-0.20.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2aba22e2e302e9231e85edc24f27ba1f524d43c223ef5765bd8624c7df9ec0a5", size = 196391 }, - { url = "https://files.pythonhosted.org/packages/7f/ee/af51d090ada641d4b264992a486435ba3ef5b5634bc27e6eb002f71cef7d/msgspec-0.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:716284f898ab2547fedd72a93bb940375de9fbfe77538f05779632dc34afdfde", size = 188644 }, - { url = "https://files.pythonhosted.org/packages/49/d6/9709ee093b7742362c2934bfb1bbe791a1e09bed3ea5d8a18ce552fbfd73/msgspec-0.20.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:558ed73315efa51b1538fa8f1d3b22c8c5ff6d9a2a62eff87d25829b94fc5054", size = 218852 }, - { url = "https://files.pythonhosted.org/packages/5c/a2/488517a43ccf5a4b6b6eca6dd4ede0bd82b043d1539dd6bb908a19f8efd3/msgspec-0.20.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:509ac1362a1d53aa66798c9b9fd76872d7faa30fcf89b2fba3bcbfd559d56eb0", size = 224937 }, - { url = "https://files.pythonhosted.org/packages/d5/e8/49b832808aa23b85d4f090d1d2e48a4e3834871415031ed7c5fe48723156/msgspec-0.20.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1353c2c93423602e7dea1aa4c92f3391fdfc25ff40e0bacf81d34dbc68adb870", size = 222858 }, - { url = "https://files.pythonhosted.org/packages/9f/56/1dc2fa53685dca9c3f243a6cbecd34e856858354e455b77f47ebd76cf5bf/msgspec-0.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cb33b5eb5adb3c33d749684471c6a165468395d7aa02d8867c15103b81e1da3e", size = 227248 }, - { url = "https://files.pythonhosted.org/packages/5a/51/aba940212c23b32eedce752896205912c2668472ed5b205fc33da28a6509/msgspec-0.20.0-cp312-cp312-win_amd64.whl", hash = "sha256:fb1d934e435dd3a2b8cf4bbf47a8757100b4a1cfdc2afdf227541199885cdacb", size = 190024 }, - { url = "https://files.pythonhosted.org/packages/41/ad/3b9f259d94f183daa9764fef33fdc7010f7ecffc29af977044fa47440a83/msgspec-0.20.0-cp312-cp312-win_arm64.whl", hash = "sha256:00648b1e19cf01b2be45444ba9dc961bd4c056ffb15706651e64e5d6ec6197b7", size = 175390 }, - { url = "https://files.pythonhosted.org/packages/8a/d1/b902d38b6e5ba3bdddbec469bba388d647f960aeed7b5b3623a8debe8a76/msgspec-0.20.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9c1ff8db03be7598b50dd4b4a478d6fe93faae3bd54f4f17aa004d0e46c14c46", size = 196463 }, - { url = "https://files.pythonhosted.org/packages/57/b6/eff0305961a1d9447ec2b02f8c73c8946f22564d302a504185b730c9a761/msgspec-0.20.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f6532369ece217fd37c5ebcfd7e981f2615628c21121b7b2df9d3adcf2fd69b8", size = 188650 }, - { url = "https://files.pythonhosted.org/packages/99/93/f2ec1ae1de51d3fdee998a1ede6b2c089453a2ee82b5c1b361ed9095064a/msgspec-0.20.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9a1697da2f85a751ac3cc6a97fceb8e937fc670947183fb2268edaf4016d1ee", size = 218834 }, - { url = "https://files.pythonhosted.org/packages/28/83/36557b04cfdc317ed8a525c4993b23e43a8fbcddaddd78619112ca07138c/msgspec-0.20.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7fac7e9c92eddcd24c19d9e5f6249760941485dff97802461ae7c995a2450111", size = 224917 }, - { url = "https://files.pythonhosted.org/packages/8f/56/362037a1ed5be0b88aced59272442c4b40065c659700f4b195a7f4d0ac88/msgspec-0.20.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f953a66f2a3eb8d5ea64768445e2bb301d97609db052628c3e1bcb7d87192a9f", size = 222821 }, - { url = "https://files.pythonhosted.org/packages/92/75/fa2370ec341cedf663731ab7042e177b3742645c5dd4f64dc96bd9f18a6b/msgspec-0.20.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:247af0313ae64a066d3aea7ba98840f6681ccbf5c90ba9c7d17f3e39dbba679c", size = 227227 }, - { url = "https://files.pythonhosted.org/packages/f1/25/5e8080fe0117f799b1b68008dc29a65862077296b92550632de015128579/msgspec-0.20.0-cp313-cp313-win_amd64.whl", hash = "sha256:67d5e4dfad52832017018d30a462604c80561aa62a9d548fc2bd4e430b66a352", size = 189966 }, - { url = "https://files.pythonhosted.org/packages/79/b6/63363422153937d40e1cb349c5081338401f8529a5a4e216865decd981bf/msgspec-0.20.0-cp313-cp313-win_arm64.whl", hash = "sha256:91a52578226708b63a9a13de287b1ec3ed1123e4a088b198143860c087770458", size = 175378 }, - { url = "https://files.pythonhosted.org/packages/bb/18/62dc13ab0260c7d741dda8dc7f481495b93ac9168cd887dda5929880eef8/msgspec-0.20.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:eead16538db1b3f7ec6e3ed1f6f7c5dec67e90f76e76b610e1ffb5671815633a", size = 196407 }, - { url = "https://files.pythonhosted.org/packages/dd/1d/b9949e4ad6953e9f9a142c7997b2f7390c81e03e93570c7c33caf65d27e1/msgspec-0.20.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:703c3bb47bf47801627fb1438f106adbfa2998fe586696d1324586a375fca238", size = 188889 }, - { url = "https://files.pythonhosted.org/packages/1e/19/f8bb2dc0f1bfe46cc7d2b6b61c5e9b5a46c62298e8f4d03bbe499c926180/msgspec-0.20.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6cdb227dc585fb109305cee0fd304c2896f02af93ecf50a9c84ee54ee67dbb42", size = 219691 }, - { url = "https://files.pythonhosted.org/packages/b8/8e/6b17e43f6eb9369d9858ee32c97959fcd515628a1df376af96c11606cf70/msgspec-0.20.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27d35044dd8818ac1bd0fedb2feb4fbdff4e3508dd7c5d14316a12a2d96a0de0", size = 224918 }, - { url = "https://files.pythonhosted.org/packages/1c/db/0e833a177db1a4484797adba7f429d4242585980b90882cc38709e1b62df/msgspec-0.20.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b4296393a29ee42dd25947981c65506fd4ad39beaf816f614146fa0c5a6c91ae", size = 223436 }, - { url = "https://files.pythonhosted.org/packages/c3/30/d2ee787f4c918fd2b123441d49a7707ae9015e0e8e1ab51aa7967a97b90e/msgspec-0.20.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:205fbdadd0d8d861d71c8f3399fe1a82a2caf4467bc8ff9a626df34c12176980", size = 227190 }, - { url = "https://files.pythonhosted.org/packages/ff/37/9c4b58ff11d890d788e700b827db2366f4d11b3313bf136780da7017278b/msgspec-0.20.0-cp314-cp314-win_amd64.whl", hash = "sha256:7dfebc94fe7d3feec6bc6c9df4f7e9eccc1160bb5b811fbf3e3a56899e398a6b", size = 193950 }, - { url = "https://files.pythonhosted.org/packages/e9/4e/cab707bf2fa57408e2934e5197fc3560079db34a1e3cd2675ff2e47e07de/msgspec-0.20.0-cp314-cp314-win_arm64.whl", hash = "sha256:2ad6ae36e4a602b24b4bf4eaf8ab5a441fec03e1f1b5931beca8ebda68f53fc0", size = 179018 }, - { url = "https://files.pythonhosted.org/packages/4c/06/3da3fc9aaa55618a8f43eb9052453cfe01f82930bca3af8cea63a89f3a11/msgspec-0.20.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f84703e0e6ef025663dd1de828ca028774797b8155e070e795c548f76dde65d5", size = 200389 }, - { url = "https://files.pythonhosted.org/packages/83/3b/cc4270a5ceab40dfe1d1745856951b0a24fd16ac8539a66ed3004a60c91e/msgspec-0.20.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7c83fc24dd09cf1275934ff300e3951b3adc5573f0657a643515cc16c7dee131", size = 193198 }, - { url = "https://files.pythonhosted.org/packages/cd/ae/4c7905ac53830c8e3c06fdd60e3cdcfedc0bbc993872d1549b84ea21a1bd/msgspec-0.20.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f13ccb1c335a124e80c4562573b9b90f01ea9521a1a87f7576c2e281d547f56", size = 225973 }, - { url = "https://files.pythonhosted.org/packages/d9/da/032abac1de4d0678d99eaeadb1323bd9d247f4711c012404ba77ed6f15ca/msgspec-0.20.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:17c2b5ca19f19306fc83c96d85e606d2cc107e0caeea85066b5389f664e04846", size = 229509 }, - { url = "https://files.pythonhosted.org/packages/69/52/fdc7bdb7057a166f309e0b44929e584319e625aaba4771b60912a9321ccd/msgspec-0.20.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d931709355edabf66c2dd1a756b2d658593e79882bc81aae5964969d5a291b63", size = 230434 }, - { url = "https://files.pythonhosted.org/packages/cb/fe/1dfd5f512b26b53043884e4f34710c73e294e7cc54278c3fe28380e42c37/msgspec-0.20.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:565f915d2e540e8a0c93a01ff67f50aebe1f7e22798c6a25873f9fda8d1325f8", size = 231758 }, - { url = "https://files.pythonhosted.org/packages/97/f6/9ba7121b8e0c4e0beee49575d1dbc804e2e72467692f0428cf39ceba1ea5/msgspec-0.20.0-cp314-cp314t-win_amd64.whl", hash = "sha256:726f3e6c3c323f283f6021ebb6c8ccf58d7cd7baa67b93d73bfbe9a15c34ab8d", size = 206540 }, - { url = "https://files.pythonhosted.org/packages/c8/3e/c5187de84bb2c2ca334ab163fcacf19a23ebb1d876c837f81a1b324a15bf/msgspec-0.20.0-cp314-cp314t-win_arm64.whl", hash = "sha256:93f23528edc51d9f686808a361728e903d6f2be55c901d6f5c92e44c6d546bfc", size = 183011 }, +version = "0.21.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/60/f79b9b013a16fa3a58350c9295ddc6789f2e335f36ea61ed10a21b215364/msgspec-0.21.1.tar.gz", hash = "sha256:2313508e394b0d208f8f56892ca9b2799e2561329de9763b19619595a6c0f72c", size = 319193 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/7f/bbc4e74cd33d316b75541149e4d35b163b63bce066530ae185a2ec3b5bfc/msgspec-0.21.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b504b6e7f7a22a24b27232b73034421692147865162daaec9f3bf62439007c87", size = 193131 }, + { url = "https://files.pythonhosted.org/packages/c1/60/504886af1aaf854112663b842d5eea9a15d9588f9bf7d0d2df736424b84d/msgspec-0.21.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4692b7c1609155708c4418f88e92f63c13fdf08aa095c84bae82bad75b53389b", size = 186597 }, + { url = "https://files.pythonhosted.org/packages/fa/54/d24ddeaa65b5278c9e67f48ce3c17a9831e8f3722f3c8322ee120aca22ef/msgspec-0.21.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d3124010b3815451494c85ff345e693cb9fe5889cfcbbef39ed8622e0e72319c", size = 215158 }, + { url = "https://files.pythonhosted.org/packages/9f/75/bb79c8b89a93ae23cd33c0d802373f16feaf9633f05d8af77091350dda0a/msgspec-0.21.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6badc03b9725352219cca017bfe71c61f2fbd0fb5982b410ac17c97c213deb30", size = 219856 }, + { url = "https://files.pythonhosted.org/packages/b4/9c/c5ca26b46f0ebbd3a6683695ef89396712cb9e4199fd1f0bc1dd968216b1/msgspec-0.21.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5d2d4116ebe3035a78d9ec76e99a9d64e5fa6d44fe61a9c5de7fd1acf54bcc69", size = 220314 }, + { url = "https://files.pythonhosted.org/packages/c8/31/645a351c4285dce40ed6755c3dcc0aa648e26dacb20a98018fe2cce5e87b/msgspec-0.21.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0d1009f6715f5bff3b54d4ff5c7428ad96197e0534e1645b8e9b955890c84664", size = 223215 }, + { url = "https://files.pythonhosted.org/packages/09/af/8bf15736a6dd3cb4f90c5467f6dc39197d2daaf10754490cdc0aa17b7312/msgspec-0.21.1-cp311-cp311-win_amd64.whl", hash = "sha256:c6faffe5bb644ec884052679af4dfd776d4b5ca90e4a7ec7e7e319e4e6b93a6e", size = 188554 }, + { url = "https://files.pythonhosted.org/packages/ef/29/cc7db3a165b62d16e64a83f82eccb79655055cb5bc1f60459a6f9d7c82f2/msgspec-0.21.1-cp311-cp311-win_arm64.whl", hash = "sha256:ee9e3f11fa94603f7d673bf795cfa31b549c4a2c723bc39b45beb1e7f5a3fb99", size = 174517 }, + { url = "https://files.pythonhosted.org/packages/6e/cf/317224852c00248c620a9bcf4b26e2e4ab8afd752f18d2a6ef73ebd423b6/msgspec-0.21.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d4248cf0b6129b7d230eacd493c17cc2d4f3989f3bb7f633a928a85b7dcfa251", size = 196188 }, + { url = "https://files.pythonhosted.org/packages/6d/81/074612945c0666078f7366f40000013de9f6ba687491d450df699bceebc9/msgspec-0.21.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5102c7e9b3acff82178449b85006d96310e690291bb1ea0142f1b24bcb8aabcb", size = 188473 }, + { url = "https://files.pythonhosted.org/packages/8a/37/655101799590bcc5fddb2bd3fe0e6194e816c2d1da7c361725f5eb89a910/msgspec-0.21.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:846758412e9518252b2ac9bffd6f0e54d9ff614f5f9488df7749f81ff5c80920", size = 218871 }, + { url = "https://files.pythonhosted.org/packages/b5/d1/d4cd9fe89c7d400d7a18f86ccc94daa3f0927f53558846fcb60791dce5d6/msgspec-0.21.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21995e74b5c598c2e004110ad66ec7f1b8c20bf2bcf3b2de8fd9a3094422d3ff", size = 225025 }, + { url = "https://files.pythonhosted.org/packages/24/bf/e20549e602b9edccadeeff98760345a416f9cce846a657e8b18e3396b212/msgspec-0.21.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6129f0cca52992e898fd5344187f7c8127b63d810b2fd73e36fca73b4c6475ee", size = 222672 }, + { url = "https://files.pythonhosted.org/packages/b4/68/04d7a8f0f786545cf9b8c280c57aa6befb5977af6e884b8b54191cbe44b3/msgspec-0.21.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ef3ec2296248d1f8b9231acb051b6d471dfde8f21819e86c9adaaa9f42918521", size = 227303 }, + { url = "https://files.pythonhosted.org/packages/cc/4d/619866af2840875be408047bf9e70ceafbae6ab50660de7134ed1b25eb86/msgspec-0.21.1-cp312-cp312-win_amd64.whl", hash = "sha256:d4ab834a054c6f0cbeef6df9e7e1b33d5f1bc7b86dea1d2fd7cad003873e783d", size = 190017 }, + { url = "https://files.pythonhosted.org/packages/5e/2e/a8f9eca8fd00e097d7a9e99ba8a4685db994494448e3d4f0b7f6e9a3c0f7/msgspec-0.21.1-cp312-cp312-win_arm64.whl", hash = "sha256:628aaa35c74950a8c59da330d7e98917e1c7188f983745782027748ee4ca573e", size = 175345 }, + { url = "https://files.pythonhosted.org/packages/7e/74/f11ede02839b19ff459f88e3145df5d711626ca84da4e23520cebf819367/msgspec-0.21.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:764173717a01743f007e9f74520ed281f24672c604514f7d76c1c3a10e8edb66", size = 196176 }, + { url = "https://files.pythonhosted.org/packages/bb/40/4476c1bd341418a046c4955aff632ec769315d1e3cb94e6acf86d461f9ed/msgspec-0.21.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:344c7cd0eaed1fb81d7959f99100ef71ec9b536881a376f11b9a6c4803365697", size = 188524 }, + { url = "https://files.pythonhosted.org/packages/ca/d9/9e9d7d7e5061b47540d03d640fab9b3965ba7ae49c1b2154861c8f007518/msgspec-0.21.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48943e278b3854c2f89f955ddc6f9f430d3f0784b16e47d10604ee0463cd21f5", size = 218880 }, + { url = "https://files.pythonhosted.org/packages/74/66/2bb344f34abb4b57e60c7c9c761994e0417b9718ec1460bf00c296f2a7ea/msgspec-0.21.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9aa659ebb0101b1cbc31461212b87e341d961f0ab0772aaf068a99e001ec4aa", size = 225050 }, + { url = "https://files.pythonhosted.org/packages/1a/84/7c1e412f76092277bf760cef12b7979d03314d259ab5b5cafde5d0c1722d/msgspec-0.21.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7b27d1a8ead2b6f5b0c4f2d07b8be1ccfcc041c8a0e704781edebe3ae13c484", size = 222713 }, + { url = "https://files.pythonhosted.org/packages/4e/27/0bba04b2b4ef05f3d068429410bc71d2cea925f1596a8f41152cccd5edb8/msgspec-0.21.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:38fe93e86b61328fe544cb7fd871fad5a27c8734bfda90f65e5dbe288ae50f61", size = 227259 }, + { url = "https://files.pythonhosted.org/packages/b0/2d/09574b0eea02fed2c2c1383dbaae2c7f79dc16dcd6487a886000afb5d7c4/msgspec-0.21.1-cp313-cp313-win_amd64.whl", hash = "sha256:8bc666331c35fcce05a7cd2d6221adbe0f6058f8e750711413d22793c080ac6a", size = 189857 }, + { url = "https://files.pythonhosted.org/packages/46/34/105b1576ad182879914f0c821f17ee1d13abb165cb060448f96fe2aff078/msgspec-0.21.1-cp313-cp313-win_arm64.whl", hash = "sha256:42bb1241e0750c1a4346f2aa84db26c5ffd99a4eb3a954927d9f149ff2f42898", size = 175403 }, + { url = "https://files.pythonhosted.org/packages/5a/ad/86954e987d1d6a5c579e2c2e7832b65e0fff194179fdac4f581536086024/msgspec-0.21.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fab48eb45fdbfbdb2c0edfec00ffc53b6b6085beefc6b50b61e01659f9f8757f", size = 196261 }, + { url = "https://files.pythonhosted.org/packages/d1/a1/c5e46c3e42b866199365e35d11dddfd1fbd8bba4fdb3c52f965b1607ce94/msgspec-0.21.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3cb779ea0c35bc807ff941d415875c1f69ca0be91a2e907ab99a171811d86a9a", size = 188729 }, + { url = "https://files.pythonhosted.org/packages/85/7d/1e29a319d678d6cb962ae5bdf32a6858ebdf38f73bc654c0e9c742a0c2c8/msgspec-0.21.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:68604db36b3b4dd9bf160e436e12798a4738848144cea1aca1cb984011eb160f", size = 219866 }, + { url = "https://files.pythonhosted.org/packages/25/1f/cca084ca2572810fff12ea9dbdcbe39eac048f40daf4a9077b49fcbe8cee/msgspec-0.21.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3d6b9dc50948eaf65df54d2fd0ff66e6d8c32f116037209ee861810eb9b676cb", size = 224993 }, + { url = "https://files.pythonhosted.org/packages/71/94/d2120fc9d419a89a3a7c13e5b7078798c4b392a96a02a6e2b3ce43a8766c/msgspec-0.21.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:52c5e21930942302394429c5a582ce7e6b62c7f983b3760834c2ce107e0dd6df", size = 223535 }, + { url = "https://files.pythonhosted.org/packages/75/17/42418b66a3ad972a89bab73dd78b79cc6282bb488a25e73c853cee7443b9/msgspec-0.21.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:abbb39d65681fa24ed394e01af3d59d869068324f900c61d06062b7fb9980f2f", size = 227222 }, + { url = "https://files.pythonhosted.org/packages/c4/33/265c894268cca88ff67b144ca2b4c522fc8b9a6f1966a3640c70516e78e1/msgspec-0.21.1-cp314-cp314-win_amd64.whl", hash = "sha256:5666b1b560b97b6ec2eb3fca8a502298ebac56e13bbca1f88523538ce83d01ea", size = 193810 }, + { url = "https://files.pythonhosted.org/packages/3b/8f/a6d35f25bf1fc63c492fdd88fdce01ba0875ead48c2b91f90f33653b4131/msgspec-0.21.1-cp314-cp314-win_arm64.whl", hash = "sha256:d8b8578e4c83b14ceea4cef0d0b747e31d9330fe4b03b2b2ad4063866a178f93", size = 179125 }, + { url = "https://files.pythonhosted.org/packages/c6/39/74839641e64b99d87da55af0fc472854d42b46e2183b9e2a67fe1bb2a512/msgspec-0.21.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:15f523d51c00ebad412213bfe9f06f0a50ec2b93e0c19e824a2d267cabb48ea2", size = 200171 }, + { url = "https://files.pythonhosted.org/packages/70/9b/ce0cca6d2d87fcd4b6ff97600790494e64f26a2c55d61507cd2755c16193/msgspec-0.21.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4e47390360583ba3d5c6cb44cf0a9f61b0a06a899d3c2c00627cedebb2e2884b", size = 192879 }, + { url = "https://files.pythonhosted.org/packages/a7/08/673a7bb05e5702dc787ddd3011195b509f9867927970da59052211929987/msgspec-0.21.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f60800e6299b798142dc40b0644da77ceac5ea0568be58228417eae14135c847", size = 226281 }, + { url = "https://files.pythonhosted.org/packages/7d/45/86508cf57283e9070b3c447e3ab25b792a7a0855a3ea4e0c6d111ac34c97/msgspec-0.21.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5f8e9dfcd98419cf7568808470c4317a3fb30bef0e3715b568730a2b272a20d7", size = 229863 }, + { url = "https://files.pythonhosted.org/packages/2c/62/e7c9367cd08d590559faacd711edbae36840342843e669440363f33c7d36/msgspec-0.21.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:92d89dfad13bd1ea640dc3e37e724ed380da1030b272bdf5ecafb983c3ad7c75", size = 230445 }, + { url = "https://files.pythonhosted.org/packages/42/b4/c0f54632103846b658a10930025f4de41c8724b5e4805a5f3b395586cb7e/msgspec-0.21.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0d03867786e5d7ba25d666df4b11320c27170f4aeafcb8e3a8b0a50a4fb742ca", size = 231822 }, + { url = "https://files.pythonhosted.org/packages/ea/1d/0d85cc79d0ccf5508e9c846cc66552a6a16bf92abd1dbd8362617f7b35cd/msgspec-0.21.1-cp314-cp314t-win_amd64.whl", hash = "sha256:740fbf1c9d59992ca3537d6fbe9ebbf9eaf726a65fbf31448e0ecbc710697a63", size = 206650 }, + { url = "https://files.pythonhosted.org/packages/90/91/56c5d560f20e6c20e9e4f55bd0e458f7f162aa689ee350346c04c48eac0b/msgspec-0.21.1-cp314-cp314t-win_arm64.whl", hash = "sha256:0d2cc73df6058d811a126ac3a8ad63a4dfa210c82f9cf5a004802eaf4712de90", size = 183149 }, ] [[package]] @@ -2248,41 +2384,54 @@ wheels = [ [[package]] name = "mypy" -version = "1.19.1" +version = "2.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "ast-serialize" }, { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, { name = "mypy-extensions" }, { name = "pathspec" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539 }, - { url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163 }, - { url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629 }, - { url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933 }, - { url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754 }, - { url = "https://files.pythonhosted.org/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", size = 10055772 }, - { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053 }, - { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134 }, - { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616 }, - { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847 }, - { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976 }, - { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104 }, - { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927 }, - { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730 }, - { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581 }, - { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252 }, - { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848 }, - { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510 }, - { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744 }, - { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815 }, - { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047 }, - { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998 }, - { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476 }, - { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872 }, - { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239 }, +sdist = { url = "https://files.pythonhosted.org/packages/12/af/4e516a05d3ca2eb9283e9ec45b2c02225c1514dd6da49fd3c9eaa6639370/mypy-2.3.0.tar.gz", hash = "sha256:465965d41cd9a2726694e983e8ce7113259327bec798115d1e1dfa2a52fb666e", size = 3988104 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/b9/d75b3082b05f1b3028828aeb18e74ae5ab0a0936051bbf1f32f59f654747/mypy-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3419d00717afbc5265b50dd14b1278f29ea4884dd398ab67873489ac093fd329", size = 14838725 }, + { url = "https://files.pythonhosted.org/packages/a9/50/79a65c6ea6e115bc73296038a4543b2d5c91f07912b918a2c616a2514bba/mypy-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cfca8ee88544090f86b6dcce05ec55d66eb48a762412ac2507810ba4bd793b6f", size = 13911128 }, + { url = "https://files.pythonhosted.org/packages/90/48/e11ed7716c26953ca321f726e452e374dbf81a6f2b8b212ec02af29b6b8f/mypy-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:75cbb4b9ef04a0c84a957f07abc4504fbf64b8dcc145675101f2d3a78a4b1d6a", size = 14146742 }, + { url = "https://files.pythonhosted.org/packages/06/72/6807565b1c4861ef66f7fdd98b51c61556356eab80235717b46c53bb8627/mypy-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:982e3d53dd23d0a4cef67dd66791fdbede0cf38f9eb617bf47663554c51e1e36", size = 15081418 }, + { url = "https://files.pythonhosted.org/packages/00/80/1ea14c5d80e589e415973db3e47c78c2219a305b808b2b506395342c1d79/mypy-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:85c5385b93012ffa3b31479ab579aef5415f4f3a32c6cf1ae07a984d2a0ff461", size = 15328164 }, + { url = "https://files.pythonhosted.org/packages/37/28/8223157404a3d51920078459c37f80fbdc590e1d8ea049dc5ce48643022a/mypy-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:13b1b16e2fa39f3b2e33fb1c468abc7a69369fa2e886b4b87b5afc81472325cd", size = 11136472 }, + { url = "https://files.pythonhosted.org/packages/6f/cc/ea27e5959c5f258585a756b252031f3b313583d81b5064b2bebc41d3706b/mypy-2.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:b5cd2f027a972a4a5f2278a11fac9747f5f81a53a30b714d74950b6807e55568", size = 10135800 }, + { url = "https://files.pythonhosted.org/packages/dc/94/0e7e592619e2133596a47cdd642534b0456545c218430bd3b9d8fefdd1b1/mypy-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d53fc67b9d28a43c6199077f49fea0f05839e36cf6158500331c9549225e5a5", size = 15026523 }, + { url = "https://files.pythonhosted.org/packages/f6/d2/1e1731df090a857df2807177a4626863e5ac0f0256513c35780efe53986f/mypy-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fbc00cee7bdbb9291979ddc9d08034a29dfcda4932628c9bbc28c1edd589df0c", size = 14032189 }, + { url = "https://files.pythonhosted.org/packages/44/95/cab921f4a806e171f34113e6181dd23c55358ccf6a80741269ef594a410e/mypy-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:04e617030eca5221909c8b7d8d7fd1c637948199aa2100b2ad9813feb07e1491", size = 14198696 }, + { url = "https://files.pythonhosted.org/packages/66/80/e6d008bb19fe446e3662d85e0e2717bf9f2d611a2164fb29d6e067dbf46c/mypy-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56c184d2c20ca6b6378d58d1960270a767f41f5e44acbbd27f05effef4f4e1d7", size = 15286904 }, + { url = "https://files.pythonhosted.org/packages/db/83/94397c9293608a364aa03e8084fb34ede4ae976a260384b9b52929308135/mypy-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3961a4a34b05f7c74b0f05aa51fbfe99a2d1e126038df40318d15c8f558b7ef3", size = 15528342 }, + { url = "https://files.pythonhosted.org/packages/cf/96/d8b37d819adec6cfccfb1fd3afc1735d94717ddeafb45536db9c6943e09b/mypy-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:b1942b9314d4c784b8ea1dbab4972603290e5dd5630f06675f13aec97526bc4c", size = 11218346 }, + { url = "https://files.pythonhosted.org/packages/2b/cd/cd9f725b19b19e5b530a154cf9bcf9e94279c5d55b3c34fb42b3aa48ea1b/mypy-2.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:be51653d7669d7d7955d613b8d0bb57d5b652eaf71a873ddf65ac87254dd2595", size = 10204525 }, + { url = "https://files.pythonhosted.org/packages/6e/ae/f7d056eb0294586a572d0d0d89580ec633c064db520f11d37d5a2fb833bd/mypy-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:91ad22a52ae2c7e621c2f67c94d5a17f66b3209a4cff5cf8a573579835c69e97", size = 14947298 }, + { url = "https://files.pythonhosted.org/packages/32/d5/db3e7af01e7844d21662c6ddc1f7825ec7cb4053f0391ac02faf3638396f/mypy-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:99ac767cc5d3b64c8d0ae226ead10c96694f94e4e7da1668642225dcd4e75aac", size = 13950768 }, + { url = "https://files.pythonhosted.org/packages/d9/fb/43c031f0190513d1ec248ed037eceb742ddd2a4d74bbf406658a28173837/mypy-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de6d2c484742a4d7b0ed6d07b143375624d3b899c5749c7b3c947f56261f48a6", size = 14151586 }, + { url = "https://files.pythonhosted.org/packages/ec/c3/f8b2ffc60883084da91be51af58e88a7ffd4ff9795acb7d902ff88d31eb1/mypy-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7da939dd335cfd2ad788bdfd081c9f4e47634ab995e5a45eb15fd1e5bc052f8b", size = 15227411 }, + { url = "https://files.pythonhosted.org/packages/83/2e/16b917fc7adcf03f1aadddfc93aab804ffb234b1ab09c0ffd6d92a5d34a2/mypy-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7247eb2824f996722a949530183394921ca71deb9680052a338cf53cff7925c2", size = 15478790 }, + { url = "https://files.pythonhosted.org/packages/c0/88/aaa65a93c73d0cdae7e42f8adb302bf6885bb281302084f99d0290a35347/mypy-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:75b0984bb3cbd76bb5c9291a8671f7ae66ca3b51c7584c358fc2e923259f0757", size = 11234919 }, + { url = "https://files.pythonhosted.org/packages/35/19/b40de63f1a80e63bc2d40f0679a6a8dbd34e95176c8122119bdf406aa552/mypy-2.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:d78fcf900b59cb7e82cb7e3a235e31b462d9333d92285bd1e4952d355b8ffba1", size = 10201510 }, + { url = "https://files.pythonhosted.org/packages/a4/58/fa0ae047da911f540284009b4f44b96fe09d83c076d7c103e9d645f46303/mypy-2.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ea317b060ce83e26050f8f9e4d7d6bf44ed7597c8ff9990bccffbb9d1d8522db", size = 14941909 }, + { url = "https://files.pythonhosted.org/packages/15/14/2ba1d61452d7c2a7fe12741e8d374e52b183476b07aa7f9e2a0d02b0720a/mypy-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:094af99f92638aa92852326188b85a89e50f4a472f44827c03362228482f0762", size = 13967581 }, + { url = "https://files.pythonhosted.org/packages/ed/5a/483fb9e5ffbbb1a28dccc7b0a13d141b17ac769b6c9f488c0a0c63698962/mypy-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de121747278144fc9ae7caa2e978cf5df12aebc82933182f5b3b86081a30baef", size = 14168807 }, + { url = "https://files.pythonhosted.org/packages/ae/77/70d7a10732063beb74ad713682cf871e88f5c5fa39bfc8beff8a524bf9cb/mypy-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:37fa4de896a84e2dc9200d91e614c22563b43d1a266789d4bbac7b22ebe6192b", size = 15200144 }, + { url = "https://files.pythonhosted.org/packages/56/72/766218ac783be4fdfcd699b90037b63017348a3e86fb2c1fbfb18302637d/mypy-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f1b3a98dfd21058bc759bb3337d5d1f61d0fdf9f3cf9c00f4291790fb5427bff", size = 15460389 }, + { url = "https://files.pythonhosted.org/packages/38/4e/8a9db7411ecb8ec0cb1fd05dba432f28bafffcd38b4e887714a4a0506689/mypy-2.3.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:944c665d984157cb96a679dfb7a4a81dd1d36b24b9c284b699514e6e626b82d4", size = 7753664 }, + { url = "https://files.pythonhosted.org/packages/65/4c/c3f8bfd6ed0e5e38b5a244403b27f821d433443df5a15a278417c10a3a3c/mypy-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:4359424140d985192c778c1ce2c114a10c1ca58a381ed79cfa70d37df94b299f", size = 11417237 }, + { url = "https://files.pythonhosted.org/packages/3c/00/89a32eaf5ccf174bc4f90db0eaea5d70636c01b8d49f384bdab2e8834390/mypy-2.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:3dd0bed92c4bdec57c42505b96416fb9e6a5aa7be84d2809bcd5f2ecec2860d7", size = 10389252 }, + { url = "https://files.pythonhosted.org/packages/31/56/104f93d69aa9f339b6b9d3b0a7faa699b8b466c942cf3ae86cc2a2ec0915/mypy-2.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:691fdc37132b1ae628d834f672e74de83462d9fb4aff621835767fb43a8dd373", size = 16385495 }, + { url = "https://files.pythonhosted.org/packages/d2/03/f1d2123313f55efafdd27706960f43a771c62f1b68426c76043f3ab9ebf3/mypy-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:aec15d465d477558fd842757b487849007311cf3897849cdda0e3162ac0ac556", size = 15098155 }, + { url = "https://files.pythonhosted.org/packages/e5/5d/d5f9200399b445e81726c4f23becee33f233aee81c72680b1ef3a258b641/mypy-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b352b7e49f5e6576009e8df730e1ff4f915cb565b851b396d2ffe2f5a6f5da88", size = 15514155 }, + { url = "https://files.pythonhosted.org/packages/cd/ce/69977c555f08faa3190cfde44189b89dbd56861b1ab97aa18fc5f3a2e4a3/mypy-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c6c6bf687b17f90dbfcad95b960d32eaa0154c00da45f03ab50bf8952e047fe", size = 16766351 }, + { url = "https://files.pythonhosted.org/packages/bc/92/6648b6caa3ab9e00f9ac0c2a78307805f873dd48139b24a6f6f7c3667bbf/mypy-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f4ed18f111bfe2d599bca7468e7f9251042c1c2118f762c8de2766a56d773c60", size = 17043490 }, + { url = "https://files.pythonhosted.org/packages/7c/ab/0dc91d80f3f016634c68d451f294a97320fe903a9b6f90b9e57b3f7f1717/mypy-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0b025a93cffb9781d231f232be07a17912f35f10a313c24f301c81e842870654", size = 12146869 }, + { url = "https://files.pythonhosted.org/packages/85/b5/4c964d02634ba81f4d1c84838e5c5b18ab06d13ed568960f5d6318495ccc/mypy-2.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:adebc76aab4f3495a88b41d48aa4aff0c03f2822501da76625afcca5975f19e5", size = 10965113 }, + { url = "https://files.pythonhosted.org/packages/2c/fa/fdc54fe583ba3cafbcedfb70eeeaf03849f75b1827a07096c7bd996f582d/mypy-2.3.0-py3-none-any.whl", hash = "sha256:6b1cdb579446b60432432b2b2403a6201b4b475a004d7f488511c9ba177c9e88", size = 2753292 }, ] [[package]] @@ -2305,17 +2454,18 @@ wheels = [ [[package]] name = "nltk" -version = "3.9.4" +version = "3.10.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, + { name = "defusedxml" }, { name = "joblib" }, { name = "regex" }, { name = "tqdm" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/74/a1/b3b4adf15585a5bc4c357adde150c01ebeeb642173ded4d871e89468767c/nltk-3.9.4.tar.gz", hash = "sha256:ed03bc098a40481310320808b2db712d95d13ca65b27372f8a403949c8b523d0", size = 2946864 } +sdist = { url = "https://files.pythonhosted.org/packages/96/02/df4f105b28a7c16b0e41423bc09cf0f1b8a305df4ef0b10ca74a2e4c648c/nltk-3.10.0.tar.gz", hash = "sha256:4fbac1d98203cbcd1b5d94a2877fb822300072d80604a5e7fae49d2c5f84e8c1", size = 3089244 } wheels = [ - { url = "https://files.pythonhosted.org/packages/9d/91/04e965f8e717ba0ab4bdca5c112deeab11c9e750d94c4d4602f050295d39/nltk-3.9.4-py3-none-any.whl", hash = "sha256:f2fa301c3a12718ce4a0e9305c5675299da5ad9e26068218b69d692fda84828f", size = 1552087 }, + { url = "https://files.pythonhosted.org/packages/6e/89/a0b0f35e2820d6a99d75ea1c11977ee6d5c9e6658eceb45b0c7620881faa/nltk-3.10.0-py3-none-any.whl", hash = "sha256:54ff84d4916d3ef127e8953bee0023f6a6b320b75d634a19e06ef056d3d244bf", size = 1716144 }, ] [[package]] @@ -2329,81 +2479,81 @@ wheels = [ [[package]] name = "numpy" -version = "2.4.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/10/8b/c265f4823726ab832de836cdd184d0986dcf94480f81e8739692a7ac7af2/numpy-2.4.3.tar.gz", hash = "sha256:483a201202b73495f00dbc83796c6ae63137a9bdade074f7648b3e32613412dd", size = 20727743 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/51/5093a2df15c4dc19da3f79d1021e891f5dcf1d9d1db6ba38891d5590f3fe/numpy-2.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:33b3bf58ee84b172c067f56aeadc7ee9ab6de69c5e800ab5b10295d54c581adb", size = 16957183 }, - { url = "https://files.pythonhosted.org/packages/b5/7c/c061f3de0630941073d2598dc271ac2f6cbcf5c83c74a5870fea07488333/numpy-2.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8ba7b51e71c05aa1f9bc3641463cd82308eab40ce0d5c7e1fd4038cbf9938147", size = 14968734 }, - { url = "https://files.pythonhosted.org/packages/ef/27/d26c85cbcd86b26e4f125b0668e7a7c0542d19dd7d23ee12e87b550e95b5/numpy-2.4.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a1988292870c7cb9d0ebb4cc96b4d447513a9644801de54606dc7aabf2b7d920", size = 5475288 }, - { url = "https://files.pythonhosted.org/packages/2b/09/3c4abbc1dcd8010bf1a611d174c7aa689fc505585ec806111b4406f6f1b1/numpy-2.4.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:23b46bb6d8ecb68b58c09944483c135ae5f0e9b8d8858ece5e4ead783771d2a9", size = 6805253 }, - { url = "https://files.pythonhosted.org/packages/21/bc/e7aa3f6817e40c3f517d407742337cbb8e6fc4b83ce0b55ab780c829243b/numpy-2.4.3-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a016db5c5dba78fa8fe9f5d80d6708f9c42ab087a739803c0ac83a43d686a470", size = 15969479 }, - { url = "https://files.pythonhosted.org/packages/78/51/9f5d7a41f0b51649ddf2f2320595e15e122a40610b233d51928dd6c92353/numpy-2.4.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:715de7f82e192e8cae5a507a347d97ad17598f8e026152ca97233e3666daaa71", size = 16901035 }, - { url = "https://files.pythonhosted.org/packages/64/6e/b221dd847d7181bc5ee4857bfb026182ef69499f9305eb1371cbb1aea626/numpy-2.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2ddb7919366ee468342b91dea2352824c25b55814a987847b6c52003a7c97f15", size = 17325657 }, - { url = "https://files.pythonhosted.org/packages/eb/b8/8f3fd2da596e1063964b758b5e3c970aed1949a05200d7e3d46a9d46d643/numpy-2.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a315e5234d88067f2d97e1f2ef670a7569df445d55400f1e33d117418d008d52", size = 18635512 }, - { url = "https://files.pythonhosted.org/packages/5c/24/2993b775c37e39d2f8ab4125b44337ab0b2ba106c100980b7c274a22bee7/numpy-2.4.3-cp311-cp311-win32.whl", hash = "sha256:2b3f8d2c4589b1a2028d2a770b0fc4d1f332fb5e01521f4de3199a896d158ddd", size = 6238100 }, - { url = "https://files.pythonhosted.org/packages/76/1d/edccf27adedb754db7c4511d5eac8b83f004ae948fe2d3509e8b78097d4c/numpy-2.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:77e76d932c49a75617c6d13464e41203cd410956614d0a0e999b25e9e8d27eec", size = 12609816 }, - { url = "https://files.pythonhosted.org/packages/92/82/190b99153480076c8dce85f4cfe7d53ea84444145ffa54cb58dcd460d66b/numpy-2.4.3-cp311-cp311-win_arm64.whl", hash = "sha256:eb610595dd91560905c132c709412b512135a60f1851ccbd2c959e136431ff67", size = 10485757 }, - { url = "https://files.pythonhosted.org/packages/a9/ed/6388632536f9788cea23a3a1b629f25b43eaacd7d7377e5d6bc7b9deb69b/numpy-2.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:61b0cbabbb6126c8df63b9a3a0c4b1f44ebca5e12ff6997b80fcf267fb3150ef", size = 16669628 }, - { url = "https://files.pythonhosted.org/packages/74/1b/ee2abfc68e1ce728b2958b6ba831d65c62e1b13ce3017c13943f8f9b5b2e/numpy-2.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7395e69ff32526710748f92cd8c9849b361830968ea3e24a676f272653e8983e", size = 14696872 }, - { url = "https://files.pythonhosted.org/packages/ba/d1/780400e915ff5638166f11ca9dc2c5815189f3d7cf6f8759a1685e586413/numpy-2.4.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:abdce0f71dcb4a00e4e77f3faf05e4616ceccfe72ccaa07f47ee79cda3b7b0f4", size = 5203489 }, - { url = "https://files.pythonhosted.org/packages/0b/bb/baffa907e9da4cc34a6e556d6d90e032f6d7a75ea47968ea92b4858826c4/numpy-2.4.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:48da3a4ee1336454b07497ff7ec83903efa5505792c4e6d9bf83d99dc07a1e18", size = 6550814 }, - { url = "https://files.pythonhosted.org/packages/7b/12/8c9f0c6c95f76aeb20fc4a699c33e9f827fa0d0f857747c73bb7b17af945/numpy-2.4.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32e3bef222ad6b052280311d1d60db8e259e4947052c3ae7dd6817451fc8a4c5", size = 15666601 }, - { url = "https://files.pythonhosted.org/packages/bd/79/cc665495e4d57d0aa6fbcc0aa57aa82671dfc78fbf95fe733ed86d98f52a/numpy-2.4.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7dd01a46700b1967487141a66ac1a3cf0dd8ebf1f08db37d46389401512ca97", size = 16621358 }, - { url = "https://files.pythonhosted.org/packages/a8/40/b4ecb7224af1065c3539f5ecfff879d090de09608ad1008f02c05c770cb3/numpy-2.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:76f0f283506c28b12bba319c0fab98217e9f9b54e6160e9c79e9f7348ba32e9c", size = 17016135 }, - { url = "https://files.pythonhosted.org/packages/f7/b1/6a88e888052eed951afed7a142dcdf3b149a030ca59b4c71eef085858e43/numpy-2.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:737f630a337364665aba3b5a77e56a68cc42d350edd010c345d65a3efa3addcc", size = 18345816 }, - { url = "https://files.pythonhosted.org/packages/f3/8f/103a60c5f8c3d7fc678c19cd7b2476110da689ccb80bc18050efbaeae183/numpy-2.4.3-cp312-cp312-win32.whl", hash = "sha256:26952e18d82a1dbbc2f008d402021baa8d6fc8e84347a2072a25e08b46d698b9", size = 5960132 }, - { url = "https://files.pythonhosted.org/packages/d7/7c/f5ee1bf6ed888494978046a809df2882aad35d414b622893322df7286879/numpy-2.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:65f3c2455188f09678355f5cae1f959a06b778bc66d535da07bf2ef20cd319d5", size = 12316144 }, - { url = "https://files.pythonhosted.org/packages/71/46/8d1cb3f7a00f2fb6394140e7e6623696e54c6318a9d9691bb4904672cf42/numpy-2.4.3-cp312-cp312-win_arm64.whl", hash = "sha256:2abad5c7fef172b3377502bde47892439bae394a71bc329f31df0fd829b41a9e", size = 10220364 }, - { url = "https://files.pythonhosted.org/packages/b6/d0/1fe47a98ce0df229238b77611340aff92d52691bcbc10583303181abf7fc/numpy-2.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b346845443716c8e542d54112966383b448f4a3ba5c66409771b8c0889485dd3", size = 16665297 }, - { url = "https://files.pythonhosted.org/packages/27/d9/4e7c3f0e68dfa91f21c6fb6cf839bc829ec920688b1ce7ec722b1a6202fb/numpy-2.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2629289168f4897a3c4e23dc98d6f1731f0fc0fe52fb9db19f974041e4cc12b9", size = 14691853 }, - { url = "https://files.pythonhosted.org/packages/3a/66/bd096b13a87549683812b53ab211e6d413497f84e794fb3c39191948da97/numpy-2.4.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:bb2e3cf95854233799013779216c57e153c1ee67a0bf92138acca0e429aefaee", size = 5198435 }, - { url = "https://files.pythonhosted.org/packages/a2/2f/687722910b5a5601de2135c891108f51dfc873d8e43c8ed9f4ebb440b4a2/numpy-2.4.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:7f3408ff897f8ab07a07fbe2823d7aee6ff644c097cc1f90382511fe982f647f", size = 6546347 }, - { url = "https://files.pythonhosted.org/packages/bf/ec/7971c4e98d86c564750393fab8d7d83d0a9432a9d78bb8a163a6dc59967a/numpy-2.4.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:decb0eb8a53c3b009b0962378065589685d66b23467ef5dac16cbe818afde27f", size = 15664626 }, - { url = "https://files.pythonhosted.org/packages/7e/eb/7daecbea84ec935b7fc732e18f532073064a3816f0932a40a17f3349185f/numpy-2.4.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5f51900414fc9204a0e0da158ba2ac52b75656e7dce7e77fb9f84bfa343b4cc", size = 16608916 }, - { url = "https://files.pythonhosted.org/packages/df/58/2a2b4a817ffd7472dca4421d9f0776898b364154e30c95f42195041dc03b/numpy-2.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6bd06731541f89cdc01b261ba2c9e037f1543df7472517836b78dfb15bd6e476", size = 17015824 }, - { url = "https://files.pythonhosted.org/packages/4a/ca/627a828d44e78a418c55f82dd4caea8ea4a8ef24e5144d9e71016e52fb40/numpy-2.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22654fe6be0e5206f553a9250762c653d3698e46686eee53b399ab90da59bd92", size = 18334581 }, - { url = "https://files.pythonhosted.org/packages/cd/c0/76f93962fc79955fcba30a429b62304332345f22d4daec1cb33653425643/numpy-2.4.3-cp313-cp313-win32.whl", hash = "sha256:d71e379452a2f670ccb689ec801b1218cd3983e253105d6e83780967e899d687", size = 5958618 }, - { url = "https://files.pythonhosted.org/packages/b1/3c/88af0040119209b9b5cb59485fa48b76f372c73068dbf9254784b975ac53/numpy-2.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:0a60e17a14d640f49146cb38e3f105f571318db7826d9b6fef7e4dce758faecd", size = 12312824 }, - { url = "https://files.pythonhosted.org/packages/58/ce/3d07743aced3d173f877c3ef6a454c2174ba42b584ab0b7e6d99374f51ed/numpy-2.4.3-cp313-cp313-win_arm64.whl", hash = "sha256:c9619741e9da2059cd9c3f206110b97583c7152c1dc9f8aafd4beb450ac1c89d", size = 10221218 }, - { url = "https://files.pythonhosted.org/packages/62/09/d96b02a91d09e9d97862f4fc8bfebf5400f567d8eb1fe4b0cc4795679c15/numpy-2.4.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7aa4e54f6469300ebca1d9eb80acd5253cdfa36f2c03d79a35883687da430875", size = 14819570 }, - { url = "https://files.pythonhosted.org/packages/b5/ca/0b1aba3905fdfa3373d523b2b15b19029f4f3031c87f4066bd9d20ef6c6b/numpy-2.4.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d1b90d840b25874cf5cd20c219af10bac3667db3876d9a495609273ebe679070", size = 5326113 }, - { url = "https://files.pythonhosted.org/packages/c0/63/406e0fd32fcaeb94180fd6a4c41e55736d676c54346b7efbce548b94a914/numpy-2.4.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a749547700de0a20a6718293396ec237bb38218049cfce788e08fcb716e8cf73", size = 6646370 }, - { url = "https://files.pythonhosted.org/packages/b6/d0/10f7dc157d4b37af92720a196be6f54f889e90dcd30dce9dc657ed92c257/numpy-2.4.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94f3c4a151a2e529adf49c1d54f0f57ff8f9b233ee4d44af623a81553ab86368", size = 15723499 }, - { url = "https://files.pythonhosted.org/packages/66/f1/d1c2bf1161396629701bc284d958dc1efa3a5a542aab83cf11ee6eb4cba5/numpy-2.4.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22c31dc07025123aedf7f2db9e91783df13f1776dc52c6b22c620870dc0fab22", size = 16657164 }, - { url = "https://files.pythonhosted.org/packages/1a/be/cca19230b740af199ac47331a21c71e7a3d0ba59661350483c1600d28c37/numpy-2.4.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:148d59127ac95979d6f07e4d460f934ebdd6eed641db9c0db6c73026f2b2101a", size = 17081544 }, - { url = "https://files.pythonhosted.org/packages/b9/c5/9602b0cbb703a0936fb40f8a95407e8171935b15846de2f0776e08af04c7/numpy-2.4.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a97cbf7e905c435865c2d939af3d93f99d18eaaa3cabe4256f4304fb51604349", size = 18380290 }, - { url = "https://files.pythonhosted.org/packages/ed/81/9f24708953cd30be9ee36ec4778f4b112b45165812f2ada4cc5ea1c1f254/numpy-2.4.3-cp313-cp313t-win32.whl", hash = "sha256:be3b8487d725a77acccc9924f65fd8bce9af7fac8c9820df1049424a2115af6c", size = 6082814 }, - { url = "https://files.pythonhosted.org/packages/e2/9e/52f6eaa13e1a799f0ab79066c17f7016a4a8ae0c1aefa58c82b4dab690b4/numpy-2.4.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1ec84fd7c8e652b0f4aaaf2e6e9cc8eaa9b1b80a537e06b2e3a2fb176eedcb26", size = 12452673 }, - { url = "https://files.pythonhosted.org/packages/c4/04/b8cece6ead0b30c9fbd99bb835ad7ea0112ac5f39f069788c5558e3b1ab2/numpy-2.4.3-cp313-cp313t-win_arm64.whl", hash = "sha256:120df8c0a81ebbf5b9020c91439fccd85f5e018a927a39f624845be194a2be02", size = 10290907 }, - { url = "https://files.pythonhosted.org/packages/70/ae/3936f79adebf8caf81bd7a599b90a561334a658be4dcc7b6329ebf4ee8de/numpy-2.4.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5884ce5c7acfae1e4e1b6fde43797d10aa506074d25b531b4f54bde33c0c31d4", size = 16664563 }, - { url = "https://files.pythonhosted.org/packages/9b/62/760f2b55866b496bb1fa7da2a6db076bef908110e568b02fcfc1422e2a3a/numpy-2.4.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:297837823f5bc572c5f9379b0c9f3a3365f08492cbdc33bcc3af174372ebb168", size = 14702161 }, - { url = "https://files.pythonhosted.org/packages/32/af/a7a39464e2c0a21526fb4fb76e346fb172ebc92f6d1c7a07c2c139cc17b1/numpy-2.4.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:a111698b4a3f8dcbe54c64a7708f049355abd603e619013c346553c1fd4ca90b", size = 5208738 }, - { url = "https://files.pythonhosted.org/packages/29/8c/2a0cf86a59558fa078d83805589c2de490f29ed4fb336c14313a161d358a/numpy-2.4.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:4bd4741a6a676770e0e97fe9ab2e51de01183df3dcbcec591d26d331a40de950", size = 6543618 }, - { url = "https://files.pythonhosted.org/packages/aa/b8/612ce010c0728b1c363fa4ea3aa4c22fe1c5da1de008486f8c2f5cb92fae/numpy-2.4.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:54f29b877279d51e210e0c80709ee14ccbbad647810e8f3d375561c45ef613dd", size = 15680676 }, - { url = "https://files.pythonhosted.org/packages/a9/7e/4f120ecc54ba26ddf3dc348eeb9eb063f421de65c05fc961941798feea18/numpy-2.4.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:679f2a834bae9020f81534671c56fd0cc76dd7e5182f57131478e23d0dc59e24", size = 16613492 }, - { url = "https://files.pythonhosted.org/packages/2c/86/1b6020db73be330c4b45d5c6ee4295d59cfeef0e3ea323959d053e5a6909/numpy-2.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d84f0f881cb2225c2dfd7f78a10a5645d487a496c6668d6cc39f0f114164f3d0", size = 17031789 }, - { url = "https://files.pythonhosted.org/packages/07/3a/3b90463bf41ebc21d1b7e06079f03070334374208c0f9a1f05e4ae8455e7/numpy-2.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d213c7e6e8d211888cc359bab7199670a00f5b82c0978b9d1c75baf1eddbeac0", size = 18339941 }, - { url = "https://files.pythonhosted.org/packages/a8/74/6d736c4cd962259fd8bae9be27363eb4883a2f9069763747347544c2a487/numpy-2.4.3-cp314-cp314-win32.whl", hash = "sha256:52077feedeff7c76ed7c9f1a0428558e50825347b7545bbb8523da2cd55c547a", size = 6007503 }, - { url = "https://files.pythonhosted.org/packages/48/39/c56ef87af669364356bb011922ef0734fc49dad51964568634c72a009488/numpy-2.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:0448e7f9caefb34b4b7dd2b77f21e8906e5d6f0365ad525f9f4f530b13df2afc", size = 12444915 }, - { url = "https://files.pythonhosted.org/packages/9d/1f/ab8528e38d295fd349310807496fabb7cf9fe2e1f70b97bc20a483ea9d4a/numpy-2.4.3-cp314-cp314-win_arm64.whl", hash = "sha256:b44fd60341c4d9783039598efadd03617fa28d041fc37d22b62d08f2027fa0e7", size = 10494875 }, - { url = "https://files.pythonhosted.org/packages/e6/ef/b7c35e4d5ef141b836658ab21a66d1a573e15b335b1d111d31f26c8ef80f/numpy-2.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0a195f4216be9305a73c0e91c9b026a35f2161237cf1c6de9b681637772ea657", size = 14822225 }, - { url = "https://files.pythonhosted.org/packages/cd/8d/7730fa9278cf6648639946cc816e7cc89f0d891602584697923375f801ed/numpy-2.4.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:cd32fbacb9fd1bf041bf8e89e4576b6f00b895f06d00914820ae06a616bdfef7", size = 5328769 }, - { url = "https://files.pythonhosted.org/packages/47/01/d2a137317c958b074d338807c1b6a383406cdf8b8e53b075d804cc3d211d/numpy-2.4.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:2e03c05abaee1f672e9d67bc858f300b5ccba1c21397211e8d77d98350972093", size = 6649461 }, - { url = "https://files.pythonhosted.org/packages/5c/34/812ce12bc0f00272a4b0ec0d713cd237cb390666eb6206323d1cc9cedbb2/numpy-2.4.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d1ce23cce91fcea443320a9d0ece9b9305d4368875bab09538f7a5b4131938a", size = 15725809 }, - { url = "https://files.pythonhosted.org/packages/25/c0/2aed473a4823e905e765fee3dc2cbf504bd3e68ccb1150fbdabd5c39f527/numpy-2.4.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c59020932feb24ed49ffd03704fbab89f22aa9c0d4b180ff45542fe8918f5611", size = 16655242 }, - { url = "https://files.pythonhosted.org/packages/f2/c8/7e052b2fc87aa0e86de23f20e2c42bd261c624748aa8efd2c78f7bb8d8c6/numpy-2.4.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9684823a78a6cd6ad7511fc5e25b07947d1d5b5e2812c93fe99d7d4195130720", size = 17080660 }, - { url = "https://files.pythonhosted.org/packages/f3/3d/0876746044db2adcb11549f214d104f2e1be00f07a67edbb4e2812094847/numpy-2.4.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0200b25c687033316fb39f0ff4e3e690e8957a2c3c8d22499891ec58c37a3eb5", size = 18380384 }, - { url = "https://files.pythonhosted.org/packages/07/12/8160bea39da3335737b10308df4f484235fd297f556745f13092aa039d3b/numpy-2.4.3-cp314-cp314t-win32.whl", hash = "sha256:5e10da9e93247e554bb1d22f8edc51847ddd7dde52d85ce31024c1b4312bfba0", size = 6154547 }, - { url = "https://files.pythonhosted.org/packages/42/f3/76534f61f80d74cc9cdf2e570d3d4eeb92c2280a27c39b0aaf471eda7b48/numpy-2.4.3-cp314-cp314t-win_amd64.whl", hash = "sha256:45f003dbdffb997a03da2d1d0cb41fbd24a87507fb41605c0420a3db5bd4667b", size = 12633645 }, - { url = "https://files.pythonhosted.org/packages/1f/b6/7c0d4334c15983cec7f92a69e8ce9b1e6f31857e5ee3a413ac424e6bd63d/numpy-2.4.3-cp314-cp314t-win_arm64.whl", hash = "sha256:4d382735cecd7bcf090172489a525cd7d4087bc331f7df9f60ddc9a296cf208e", size = 10565454 }, - { url = "https://files.pythonhosted.org/packages/64/e4/4dab9fb43c83719c29241c535d9e07be73bea4bc0c6686c5816d8e1b6689/numpy-2.4.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c6b124bfcafb9e8d3ed09130dbee44848c20b3e758b6bbf006e641778927c028", size = 16834892 }, - { url = "https://files.pythonhosted.org/packages/c9/29/f8b6d4af90fed3dfda84ebc0df06c9833d38880c79ce954e5b661758aa31/numpy-2.4.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:76dbb9d4e43c16cf9aa711fcd8de1e2eeb27539dcefb60a1d5e9f12fae1d1ed8", size = 14893070 }, - { url = "https://files.pythonhosted.org/packages/9a/04/a19b3c91dbec0a49269407f15d5753673a09832daed40c45e8150e6fa558/numpy-2.4.3-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:29363fbfa6f8ee855d7569c96ce524845e3d726d6c19b29eceec7dd555dab152", size = 5399609 }, - { url = "https://files.pythonhosted.org/packages/79/34/4d73603f5420eab89ea8a67097b31364bf7c30f811d4dd84b1659c7476d9/numpy-2.4.3-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:bc71942c789ef415a37f0d4eab90341425a00d538cd0642445d30b41023d3395", size = 6714355 }, - { url = "https://files.pythonhosted.org/packages/58/ad/1100d7229bb248394939a12a8074d485b655e8ed44207d328fdd7fcebc7b/numpy-2.4.3-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e58765ad74dcebd3ef0208a5078fba32dc8ec3578fe84a604432950cd043d79", size = 15800434 }, - { url = "https://files.pythonhosted.org/packages/0c/fd/16d710c085d28ba4feaf29ac60c936c9d662e390344f94a6beaa2ac9899b/numpy-2.4.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e236dbda4e1d319d681afcbb136c0c4a8e0f1a5c58ceec2adebb547357fe857", size = 16729409 }, - { url = "https://files.pythonhosted.org/packages/57/a7/b35835e278c18b85206834b3aa3abe68e77a98769c59233d1f6300284781/numpy-2.4.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:4b42639cdde6d24e732ff823a3fa5b701d8acad89c4142bc1d0bd6dc85200ba5", size = 12504685 }, +version = "2.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/49/ec46835a70be8fa6446c495126ac84fdb28cb2558e1620ffb87a10c8b64c/numpy-2.4.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4", size = 16969194 }, + { url = "https://files.pythonhosted.org/packages/0e/0d/f5957185c0ee2f3e12f78715aa9e3b353fd83633316c8532b38faa37e3f6/numpy-2.4.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d", size = 14964111 }, + { url = "https://files.pythonhosted.org/packages/ad/40/40a40ee0ddf7ceb782c49af278894b686e586d65d8c1889c8b5da01a3d7d/numpy-2.4.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8", size = 5469159 }, + { url = "https://files.pythonhosted.org/packages/63/13/f9a8046535cb21deae82f8d03de9617e08882d274fad2539630761888228/numpy-2.4.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8155154c7c691289fe18f510b5d4657c68c67989f293f0535a91360392ff6538", size = 6798936 }, + { url = "https://files.pythonhosted.org/packages/33/a8/6fa8c1a345a8c85dbb21932c447bee07c30a2c2a3f31e369c0a84b300147/numpy-2.4.6-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47", size = 15966692 }, + { url = "https://files.pythonhosted.org/packages/02/03/74fe2a4cb3817d94d86402f2506554130a2f01414e299b5a843e5a8a957f/numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93", size = 16918164 }, + { url = "https://files.pythonhosted.org/packages/c5/80/3615be3313f7e7696609bc194b9f0101da809df79e859bdb84e0cd043f46/numpy-2.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8", size = 17322877 }, + { url = "https://files.pythonhosted.org/packages/ca/ac/a691e0fe2675e370d0e08ff905adc49a1c8830e8cae03efe4477e92cd55d/numpy-2.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6", size = 18651487 }, + { url = "https://files.pythonhosted.org/packages/15/a7/9bc1cd626d7bf6869bfedf27b91b6ab5dd607758bf8e959d6fa80c6a59cb/numpy-2.4.6-cp311-cp311-win32.whl", hash = "sha256:ddea102b48f9e339f3948bf22040944184627a30fdf7f858667673b9c5f033c8", size = 6233945 }, + { url = "https://files.pythonhosted.org/packages/c5/31/7fc6239c12bce7e931463251cca4426c465e1876ba3cc785402ef4dd8f4e/numpy-2.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:1e254a00cdf42b1e4d5b3d68d33af63268d41340d8885df2ab6470f2e1500147", size = 12608406 }, + { url = "https://files.pythonhosted.org/packages/27/83/140f85a466595a16382996a1bf06b2b54bcd597488921b0c9daaeeda72af/numpy-2.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:ed9749eef4cbd126da3dc1d6bcb3a57f5eb7ac6a6484146bdbf743f552dfc577", size = 10479528 }, + { url = "https://files.pythonhosted.org/packages/95/2a/3d7b5ac8aac24feaf9ad7ed58f45b0bbc06d37e4338ae84c9f2298b570f9/numpy-2.4.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1", size = 16689119 }, + { url = "https://files.pythonhosted.org/packages/ea/12/92c4c131527599e8288d6918e888d88726f84d805d784b771f32408aeaef/numpy-2.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb", size = 14699246 }, + { url = "https://files.pythonhosted.org/packages/ad/fe/c0a6b7b2ca128a8fb228575147073b660656734b8ebe4d76c8fd748dcc79/numpy-2.4.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41", size = 5204410 }, + { url = "https://files.pythonhosted.org/packages/f3/d4/9770d14ba719432bb90a421bfd443872ed0f70f7264b64bec12ea363d5fd/numpy-2.4.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698", size = 6551240 }, + { url = "https://files.pythonhosted.org/packages/c9/c6/50a46a6205feba2343f1d6d17438107c5dc491ed1c736e6ea68689fd906b/numpy-2.4.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f", size = 15671012 }, + { url = "https://files.pythonhosted.org/packages/99/60/14115e6364fa676c5397c2ad3004e527e9aa487abf5d0706ec81bbd08529/numpy-2.4.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853", size = 16645538 }, + { url = "https://files.pythonhosted.org/packages/ae/c5/693cbe59e57db94d2231fa519ca3978dc9e19da5a8f088588f5c6e947ff2/numpy-2.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a", size = 17020706 }, + { url = "https://files.pythonhosted.org/packages/ef/fc/85b7c4eff9b4966ade25c2273cf7e7012e92366c032058653934b37de044/numpy-2.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2", size = 18368541 }, + { url = "https://files.pythonhosted.org/packages/f6/81/e1b27545deedce7f4a0b348618c6b62d74e36a4dc9ccd42f3eb2f85eee32/numpy-2.4.6-cp312-cp312-win32.whl", hash = "sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45", size = 5962825 }, + { url = "https://files.pythonhosted.org/packages/ab/ca/feab00bd44aa5fe1ad2c18f08b4d3bb92e26484b0b1d1443897809ed528c/numpy-2.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751", size = 12321687 }, + { url = "https://files.pythonhosted.org/packages/63/cf/5a6d34850a39d1093558564f77ee8e8e0bee5061151b8f05a55711001ec7/numpy-2.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8", size = 10221482 }, + { url = "https://files.pythonhosted.org/packages/fb/82/bdab26d7438c6791ca31b7c024ca37c1eab8b726ba236129005cd4a06e45/numpy-2.4.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0", size = 16684648 }, + { url = "https://files.pythonhosted.org/packages/1b/30/a80189bcc7f5e4258b3fbc3968d909d1756f54d023299ecc39ad6fdb9ef8/numpy-2.4.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb", size = 14693902 }, + { url = "https://files.pythonhosted.org/packages/97/12/70b5d0d7c15e1ebb8a6a84a8caa1d19e181d84fb58bb6d70aca29099dec1/numpy-2.4.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f", size = 5198992 }, + { url = "https://files.pythonhosted.org/packages/ba/8c/ebd2a8f8a83541f8d38cc5667e8c2b69cecfd30da6e45693e8158857d44b/numpy-2.4.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3", size = 6546944 }, + { url = "https://files.pythonhosted.org/packages/bb/c5/7b863a97a91671a0338f4253bd3b5a3d3852f0692dae91711c9f4a10e787/numpy-2.4.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b", size = 15669392 }, + { url = "https://files.pythonhosted.org/packages/a5/9d/3584b9984ca4c047aea75214ce1a4c4c73d849bd71b604264b7f5653f8a8/numpy-2.4.6-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089", size = 16633220 }, + { url = "https://files.pythonhosted.org/packages/05/ae/7c67fba23bd98caec7c99261f3a16072ade14813486b0282cb29846de832/numpy-2.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a", size = 17020800 }, + { url = "https://files.pythonhosted.org/packages/d9/5d/3b6725cb31d983c5e66916f5d36f6d7e5521129e4c4404d64f918292a5b6/numpy-2.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605", size = 18357600 }, + { url = "https://files.pythonhosted.org/packages/f7/da/2ccc6c2fe8898dee01d90c75c5f5f914a23daf99e3e0f59516a08760c8b5/numpy-2.4.6-cp313-cp313-win32.whl", hash = "sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91", size = 5961134 }, + { url = "https://files.pythonhosted.org/packages/b5/cd/9cc4dc876fb065d5c220aae4d5e14826b2715331bb7618ce1fb07a679d99/numpy-2.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359", size = 12318598 }, + { url = "https://files.pythonhosted.org/packages/39/1e/c0bcba1f8694116485fe28fd1be698c278fcda4141c5b0e53a2aed8b12a8/numpy-2.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778", size = 10222272 }, + { url = "https://files.pythonhosted.org/packages/63/6d/cc5619247c8f4204e507f5883528372e4ac4bb189e579fb859a12e480b1f/numpy-2.4.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1", size = 14821197 }, + { url = "https://files.pythonhosted.org/packages/00/58/f1c39161c87d9e9bed660f1ed4bafc0e403d5ec9650b6dd77aead07d489b/numpy-2.4.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe", size = 5326287 }, + { url = "https://files.pythonhosted.org/packages/af/57/3917ab0fd97f271a8694513581b8a36c655f111c446852c302f04ccdb6fc/numpy-2.4.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997", size = 6646763 }, + { url = "https://files.pythonhosted.org/packages/eb/0f/037e64c494b67581ae18193d770adef354c41f3f2c8ebf865602d949bf8f/numpy-2.4.6-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20", size = 15728070 }, + { url = "https://files.pythonhosted.org/packages/21/a6/5d2bae9c9542eb4df16dc9c46dc79c186e9bad53805dfa5399a6023c6db0/numpy-2.4.6-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d", size = 16681752 }, + { url = "https://files.pythonhosted.org/packages/92/14/23d1dfb410ae362cd59ce53e936b1513d545eb40db3949ced632e19a459e/numpy-2.4.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67", size = 17086024 }, + { url = "https://files.pythonhosted.org/packages/4b/6e/23595a2c642cdf3bc567877064bdd7f91c8b0038a4453cf2daf7248eafe9/numpy-2.4.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd", size = 18403398 }, + { url = "https://files.pythonhosted.org/packages/8a/90/0ac3bc947217e66dec77e7cbc6a1979d1af70b6461b82f620d3bccd5e4c8/numpy-2.4.6-cp313-cp313t-win32.whl", hash = "sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab", size = 6084971 }, + { url = "https://files.pythonhosted.org/packages/77/71/5673e351671a1d2bd6063b91b44f70c0affea7d1516fa7a6572941ba4aa1/numpy-2.4.6-cp313-cp313t-win_amd64.whl", hash = "sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75", size = 12458532 }, + { url = "https://files.pythonhosted.org/packages/3f/88/19d3503c5046e688f049274b27a3ef3d771152fa80d3ba3d01a3dff61abe/numpy-2.4.6-cp313-cp313t-win_arm64.whl", hash = "sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd", size = 10291881 }, + { url = "https://files.pythonhosted.org/packages/f8/91/3ab2044d05fd16d343c5ac2e69b127f1b2854040dd20b193257c78028bd3/numpy-2.4.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079", size = 16683458 }, + { url = "https://files.pythonhosted.org/packages/8e/62/764ce66fa4147ae6d73071a3abf804ffe606f174618697c571acdf26a7c9/numpy-2.4.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7", size = 14704559 }, + { url = "https://files.pythonhosted.org/packages/60/61/23f27c172f022e04025b7dc2367f4d63c1a398120607ec896228649a6f48/numpy-2.4.6-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5", size = 5209716 }, + { url = "https://files.pythonhosted.org/packages/03/71/21cf70dc6ea3e3acb95fc53a265b2fc248b981f0194ceb5b475271b8809d/numpy-2.4.6-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096", size = 6543947 }, + { url = "https://files.pythonhosted.org/packages/d5/91/64288395ee1799bd2e0b04a305dce9666da90c961e1f3fe982a05ee1c036/numpy-2.4.6-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b", size = 15685197 }, + { url = "https://files.pythonhosted.org/packages/f3/eb/ebffaa97dc55502df69584a8f0dcf07f69a3e0b3e2323670a2722db9aa39/numpy-2.4.6-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8", size = 16638245 }, + { url = "https://files.pythonhosted.org/packages/b8/0b/54f9da33128d7e350fab89c7455902eeae70349ee52bddb448dc4a576f45/numpy-2.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402", size = 17036587 }, + { url = "https://files.pythonhosted.org/packages/b6/f0/fdebc1052db1cc37c64beb22072d67cd6d1c71adca1299f53dec2b5e20d3/numpy-2.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb", size = 18363226 }, + { url = "https://files.pythonhosted.org/packages/aa/b4/298628d98c72b57e57f7165ae6a481a1deaf6f3c28262a6e4c739c275930/numpy-2.4.6-cp314-cp314-win32.whl", hash = "sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1", size = 6010196 }, + { url = "https://files.pythonhosted.org/packages/df/ac/46de6dda46478f7942f839e094970be2d4a861e005c4b3bf07c92e291a09/numpy-2.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261", size = 12450334 }, + { url = "https://files.pythonhosted.org/packages/78/92/b8b798ac784102c0da830d2257d59358e3d3d90d1e2b3f2575dad976c5cf/numpy-2.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6", size = 10495678 }, + { url = "https://files.pythonhosted.org/packages/30/34/ec28d1aa8115971537c01469ab2011ee96827930f0a124de1000cc2a7ed7/numpy-2.4.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a", size = 14823672 }, + { url = "https://files.pythonhosted.org/packages/16/bd/f6d1fede4e54e8042a7ff97bb495510f3c220f94bcd9e8b228e87c92cc0d/numpy-2.4.6-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e", size = 5328731 }, + { url = "https://files.pythonhosted.org/packages/f4/f0/e105b9e2fd728a9910103884decd6951d9dd73896b914a98d9a231de02ee/numpy-2.4.6-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e", size = 6649805 }, + { url = "https://files.pythonhosted.org/packages/82/dd/1206a7ca6ab15e3f02069707ca96222e202af681bb73756da7527f3cb837/numpy-2.4.6-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43", size = 15730496 }, + { url = "https://files.pythonhosted.org/packages/51/e7/38d3ea825dcab85a591734decb2f6c67caa7c8367d374df1a1c3842f9b07/numpy-2.4.6-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e", size = 16679616 }, + { url = "https://files.pythonhosted.org/packages/93/b7/caabfdf53edf663e0b4eb74d7d405d83baef09eb5e83bcd32d601d72b93e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895", size = 17085145 }, + { url = "https://files.pythonhosted.org/packages/f9/45/68d7c33a6bcf3e5aa3bdbd57a367e6f615286dfd6482f97e8ffeb734306e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4", size = 18403813 }, + { url = "https://files.pythonhosted.org/packages/9c/50/0753655aa844c99cd9e018aacf76f130f1bd81d881bb74bc0aef5d73a8ba/numpy-2.4.6-cp314-cp314t-win32.whl", hash = "sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063", size = 6156982 }, + { url = "https://files.pythonhosted.org/packages/b2/d4/7c67becf668f973cb490cec3e98dfd799d866f9c989a54d355672cfa0db6/numpy-2.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627", size = 12638908 }, + { url = "https://files.pythonhosted.org/packages/43/bb/e1c71a4295b1b1d1393d50dbb4f2a36283c6859d9d3892e84f00ec5a91d5/numpy-2.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66", size = 10565867 }, + { url = "https://files.pythonhosted.org/packages/de/12/b422cc84439adc0d00de605bf4a308890ae5c26f2c71fbd73e5d08fbb0dd/numpy-2.4.6-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:55cced7c52e981362f708ad635198e97a752dfba412cc03c23bbf3bd8d5cd662", size = 16847511 }, + { url = "https://files.pythonhosted.org/packages/44/53/f481bef68011740f8849418d82db07230e825013f31f4eef5ba5b805316a/numpy-2.4.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7", size = 14889064 }, + { url = "https://files.pythonhosted.org/packages/7f/57/42ed575c10ced8af951d426bc4e1f8aff16fd851db33f067036215a7f860/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f", size = 5394157 }, + { url = "https://files.pythonhosted.org/packages/6a/ef/f66cc724fcc36c1e364c67f51ae9146090b8b584f27d58b97fdae3edd737/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:948424b06129ce883307e8cff868c31396d8dc7630a59c61d70d98dbe70f222c", size = 6708728 }, + { url = "https://files.pythonhosted.org/packages/1a/9c/c531f2293b91265d8b48e9b329f54fdd7ffae73cb4134ea10cca4237e9cc/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0", size = 15798374 }, + { url = "https://files.pythonhosted.org/packages/1a/b0/413077f6b1153ed3cba361401c6783bbad6114804a000cc22eb71c13e190/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02", size = 16747286 }, + { url = "https://files.pythonhosted.org/packages/15/ce/e5ec180bc41812edcd8daeb8639d205622c0e8c02259d8ab25a0201b3c2a/numpy-2.4.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73", size = 12504263 }, ] [[package]] @@ -2411,7 +2561,7 @@ name = "nvidia-cublas-cu12" version = "12.9.2.10" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cuda-nvrtc-cu12", marker = "python_full_version < '3.12' or sys_platform != 'win32'" }, + { name = "nvidia-cuda-nvrtc-cu12", marker = "sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/cb/c0/0a517bfe63ccd3b92eb254d264e28fca3c7cab75d07daea315250fb1bf73/nvidia_cublas_cu12-12.9.2.10-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:e4f53a8ca8c5d6e8c492d0d0a3d565ecb59a751b19cfdaa4f6da0ab2104c1702", size = 581240110 }, @@ -2435,7 +2585,7 @@ wheels = [ [[package]] name = "openai" -version = "2.29.0" +version = "2.46.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -2447,9 +2597,9 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b4/15/203d537e58986b5673e7f232453a2a2f110f22757b15921cbdeea392e520/openai-2.29.0.tar.gz", hash = "sha256:32d09eb2f661b38d3edd7d7e1a2943d1633f572596febe64c0cd370c86d52bec", size = 671128 } +sdist = { url = "https://files.pythonhosted.org/packages/af/ac/f725c4efbda8657d02be684607e5a2e5ce362e4790fdbcbdfb7c15018647/openai-2.46.0.tar.gz", hash = "sha256:0421e0735ac41451cad894af4cddf0435bfbf8cbc538ac0e15b3c062f2ddc06a", size = 1114628 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/b1/35b6f9c8cf9318e3dbb7146cc82dab4cf61182a8d5406fc9b50864362895/openai-2.29.0-py3-none-any.whl", hash = "sha256:b7c5de513c3286d17c5e29b92c4c98ceaf0d775244ac8159aeb1bddf840eb42a", size = 1141533 }, + { url = "https://files.pythonhosted.org/packages/ea/7b/206238ebcb50b235942b1c66dba4974776f2057402a8d91c399be587d66a/openai-2.46.0-py3-none-any.whl", hash = "sha256:672381db55efb3a1e2610f29304c130cccdd0b319bace4d492b2443cb64c1e7c", size = 1637556 }, ] [[package]] @@ -2463,39 +2613,39 @@ wheels = [ [[package]] name = "packaging" -version = "26.0" +version = "26.2" 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 } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134 } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366 }, + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195 }, ] [[package]] name = "patchright" -version = "1.58.2" +version = "1.61.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "greenlet" }, { name = "pyee" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/41/2f/afacd242f1ac8265275531c2e1be387f0c3b87ed14accff118c1e824695e/patchright-1.58.2-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:3930464552e52f4d5283998db5797e1797c1869206bce25c065b2d84a69e6bfb", size = 42237382 }, - { url = "https://files.pythonhosted.org/packages/9b/38/e8f173299b05bbf5fd0278fbee5ceaf25eab93fece203bb5b08ae924d604/patchright-1.58.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:be76fa83f5b36219375fc0ed52f76de800eb2388844c185bb857a2e107caea13", size = 41025905 }, - { url = "https://files.pythonhosted.org/packages/ba/08/5c97f3f3300a93c62b417b5dac86d22ad771e0941cd5b59c6054d7716197/patchright-1.58.2-py3-none-macosx_11_0_universal2.whl", hash = "sha256:8dc1005c5683c8661de461e5ee85f857b43758f1e2599a7d8a44c50c6ad9c5d7", size = 42237381 }, - { url = "https://files.pythonhosted.org/packages/e5/2b/cb8b7053f2ede3586d89cb7e45f7b643751f8d97b4dfa9af7f4188aac3f9/patchright-1.58.2-py3-none-manylinux1_x86_64.whl", hash = "sha256:13aef416c59f23f0fb552658281890ef349db2bee2e449c159560867c2e6cb61", size = 46221550 }, - { url = "https://files.pythonhosted.org/packages/bc/d9/33f3c4839ddbc3255ab012457220d56d7a910174a0a41424f6424a8b156f/patchright-1.58.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e68d0c538b5bd2bd6ef0b1327e9e766c3919d5aeade8b7bd4b29ecd3adfc0b4", size = 45950498 }, - { url = "https://files.pythonhosted.org/packages/bb/63/3b054f25a44721b9a530ec12de33d6b5d94cd9952748c2586b2a64ef62ba/patchright-1.58.2-py3-none-win32.whl", hash = "sha256:7dac724893fde90d726b125f7c35507a2afb5480c23cb57f88a31484d131de98", size = 36802278 }, - { url = "https://files.pythonhosted.org/packages/c4/11/f06d2f6ae8e0c1aea4b17b18a105dc2ad28e358217896eb3720e80e2d297/patchright-1.58.2-py3-none-win_amd64.whl", hash = "sha256:9b740c13343a6e412efe052d0c17a65910cc4e3fd0fd6b62c1ac8dc1eec4c158", size = 36802282 }, - { url = "https://files.pythonhosted.org/packages/f5/ae/a85dca1ebcdfc63e5838783c0929d82066dacd7448e29911d052bbd286cb/patchright-1.58.2-py3-none-win_arm64.whl", hash = "sha256:958cd884787d140dd464ec2901ea85b9634aad5e8444a267f407ee648de04667", size = 33072202 }, + { url = "https://files.pythonhosted.org/packages/79/42/a76e2ea17dd4feccbf5ef6df017fb2e67f4b72a6f559f59d9aad1197f83a/patchright-1.61.2-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:43ae10cd2a3e46b222179a120ab39ba88f801f3a42aa41cab45fac9928f2103d", size = 43727572 }, + { url = "https://files.pythonhosted.org/packages/57/e7/1ff89853c9aeaf26cd20559f401375ec71a40e4041cf7ace03d41a741f06/patchright-1.61.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a3da24d7890631255c7bef467b94ffe7f7bfa36f96fc72c10d5787d3cb396873", size = 42512921 }, + { url = "https://files.pythonhosted.org/packages/e4/d0/d730536a7fc43c3aaf2781518595ca71ffa1fc2f24e580c0d0cf04e00eca/patchright-1.61.2-py3-none-macosx_11_0_universal2.whl", hash = "sha256:edef852aa7d28791fa2d3f7ce135edfea2ce07a2bc5e0a74fb2dfe269af62223", size = 43727573 }, + { url = "https://files.pythonhosted.org/packages/a2/b5/c76dcda275cb0d9651321e343f3268609ae24cef196bff56751754ffba16/patchright-1.61.2-py3-none-manylinux1_x86_64.whl", hash = "sha256:545bdf2c0bb5f9ad78ab315e91c8c2990cc6a702e7f18650b71fe1071049b5c1", size = 47729338 }, + { url = "https://files.pythonhosted.org/packages/19/ef/5de6feaedf47a6ba8efb9e8c4f8a42bad29cc59c35bbd088b1729a36b271/patchright-1.61.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1824da30cf6560749bc1bb4a7f002fec325d5b709090dd7c8c1b643899331cee", size = 47426969 }, + { url = "https://files.pythonhosted.org/packages/6c/6e/ce2c5d03a4c785ac8f3e0180bbf83ef85149cf41fdfa41c09bd8cbcfd945/patchright-1.61.2-py3-none-win32.whl", hash = "sha256:a5aaf4cec2bd38714f2447f84bcdfe7dcb592a16e9616bf6d2d872066d1c78af", size = 38155048 }, + { url = "https://files.pythonhosted.org/packages/46/c8/fceb57b9d32ed078f53fc6a757c6e4b54cb117f59412938252a3359105e5/patchright-1.61.2-py3-none-win_amd64.whl", hash = "sha256:506bca9b72e609fd8fee35e2a3ad26e5d92e25337002f5c2cdad53d5cacb0b67", size = 38155052 }, + { url = "https://files.pythonhosted.org/packages/33/96/67a791c6b7cd76424b44d356beaf414ab97c3aef2d42fad4641e7905b41d/patchright-1.61.2-py3-none-win_arm64.whl", hash = "sha256:3dee1e2daada9f98653763831f688d39c924e0cf0eeb988e8047b703766cee73", size = 34268624 }, ] [[package]] name = "pathspec" -version = "1.0.4" +version = "1.1.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200 } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206 }, + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328 }, ] [[package]] @@ -2585,42 +2735,42 @@ wheels = [ [[package]] name = "platformdirs" -version = "4.9.4" +version = "4.10.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/19/56/8d4c30c8a1d07013911a8fdbd8f89440ef9f08d07a1b50ab8ca8be5a20f9/platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934", size = 28737 } +sdist = { url = "https://files.pythonhosted.org/packages/d7/47/e4501f49c178ae1d9f4a75073fda4204f52647993f075a9db4d14930e0c5/platformdirs-4.10.0.tar.gz", hash = "sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7", size = 31224 } wheels = [ - { url = "https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868", size = 21216 }, + { url = "https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a", size = 22743 }, ] [[package]] name = "playwright" -version = "1.58.0" +version = "1.61.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "greenlet" }, { name = "pyee" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/f8/c9/9c6061d5703267f1baae6a4647bfd1862e386fbfdb97d889f6f6ae9e3f64/playwright-1.58.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:96e3204aac292ee639edbfdef6298b4be2ea0a55a16b7068df91adac077cc606", size = 42251098 }, - { url = "https://files.pythonhosted.org/packages/e0/40/59d34a756e02f8c670f0fee987d46f7ee53d05447d43cd114ca015cb168c/playwright-1.58.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:70c763694739d28df71ed578b9c8202bb83e8fe8fb9268c04dd13afe36301f71", size = 41039625 }, - { url = "https://files.pythonhosted.org/packages/e1/ee/3ce6209c9c74a650aac9028c621f357a34ea5cd4d950700f8e2c4b7fe2c4/playwright-1.58.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:185e0132578733d02802dfddfbbc35f42be23a45ff49ccae5081f25952238117", size = 42251098 }, - { url = "https://files.pythonhosted.org/packages/f1/af/009958cbf23fac551a940d34e3206e6c7eed2b8c940d0c3afd1feb0b0589/playwright-1.58.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:c95568ba1eda83812598c1dc9be60b4406dffd60b149bc1536180ad108723d6b", size = 46235268 }, - { url = "https://files.pythonhosted.org/packages/d9/a6/0e66ad04b6d3440dae73efb39540c5685c5fc95b17c8b29340b62abbd952/playwright-1.58.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f9999948f1ab541d98812de25e3a8c410776aa516d948807140aff797b4bffa", size = 45964214 }, - { url = "https://files.pythonhosted.org/packages/0e/4b/236e60ab9f6d62ed0fd32150d61f1f494cefbf02304c0061e78ed80c1c32/playwright-1.58.0-py3-none-win32.whl", hash = "sha256:1e03be090e75a0fabbdaeab65ce17c308c425d879fa48bb1d7986f96bfad0b99", size = 36815998 }, - { url = "https://files.pythonhosted.org/packages/41/f8/5ec599c5e59d2f2f336a05b4f318e733077cd5044f24adb6f86900c3e6a7/playwright-1.58.0-py3-none-win_amd64.whl", hash = "sha256:a2bf639d0ce33b3ba38de777e08697b0d8f3dc07ab6802e4ac53fb65e3907af8", size = 36816005 }, - { url = "https://files.pythonhosted.org/packages/c8/c4/cc0229fea55c87d6c9c67fe44a21e2cd28d1d558a5478ed4d617e9fb0c93/playwright-1.58.0-py3-none-win_arm64.whl", hash = "sha256:32ffe5c303901a13a0ecab91d1c3f74baf73b84f4bedbb6b935f5bc11cc98e1b", size = 33085919 }, + { url = "https://files.pythonhosted.org/packages/44/ee/31e4e0db36588b817a10b299a0285082545fde7d36543c2abe498bb3d61a/playwright-1.61.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:ff138c3a604f69911e9d42fd036e55c2a171e5616edf04c1e7f60a2a285540b0", size = 43421877 }, + { url = "https://files.pythonhosted.org/packages/42/35/71395dd3ecc798965be4a3ef8c443217d4abca168e7cb34536304f9489e6/playwright-1.61.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:009588c2a7e499bc5a8b425b61fa65490968bbda9cd69e0cf2cff10f8304659a", size = 42205016 }, + { url = "https://files.pythonhosted.org/packages/f4/44/323164cf5cd1647bdefce76ffce27651aadb959d089b48f53ea40918276e/playwright-1.61.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:9f7de4536088d12037c13a52b7ea34b59270b78926bb56935070597ffac6b1af", size = 43421884 }, + { url = "https://files.pythonhosted.org/packages/ab/f8/a35bf179e4ba2522c1893635094a64e407572547bd61528820fc0abc87fe/playwright-1.61.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:54f3b39f6eab832e33458c1dd7da0b5682aedab3b09ae731b5c59fa12fd2024e", size = 47421381 }, + { url = "https://files.pythonhosted.org/packages/b7/eb/e3f922348ec17c315f98c463f72faa1181a1c3de0bfe31a8d2edf6561723/playwright-1.61.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93454322ade8c11d5d6c211bfd91bdfb9ffb4810e3e026371bcbc4bec1b7ee4c", size = 47120545 }, + { url = "https://files.pythonhosted.org/packages/c2/a6/5be4e52b40a9c0c8a073e7c5b0785c05cf5a9ea8f8a7b5b260e32d970342/playwright-1.61.0-py3-none-win32.whl", hash = "sha256:372d55a6f1248fa1dd47599686980cb8fb5bbe6fcda59eab793eb657c11d8a9b", size = 37844841 }, + { url = "https://files.pythonhosted.org/packages/6c/fd/2b78036e5fbe9d5f5645bbe08a1eac7160c51243c0093963edbcf67c35d9/playwright-1.61.0-py3-none-win_amd64.whl", hash = "sha256:35c6cc4589a5d00964a59d7b3e59641e0aac0c02f15479a7af77d20f6bc79597", size = 37844846 }, + { url = "https://files.pythonhosted.org/packages/27/0d/1b0f3c4ee4eb0514bc805b5c2f9a223e5b6de4f11a926f5235d51d0fc81b/playwright-1.61.0-py3-none-win_arm64.whl", hash = "sha256:e9fcbffcf557a8620fdedd92491eb59a32d18e23d6f3b4f6214b952be324fe51", size = 33955127 }, ] [[package]] name = "playwright-stealth" -version = "2.0.2" +version = "2.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "playwright" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/61/ee/871901103c7b2a12070011fd4d978191f8f962837bf8bb51847274f528fa/playwright_stealth-2.0.2.tar.gz", hash = "sha256:ac57e51873190da5e653e03720e948c8f0a3d06b098f1d56763103d23ee48143", size = 24902 } +sdist = { url = "https://files.pythonhosted.org/packages/a4/db/6ade5d539c7d151b9defc78fafa8b65aa52352617d0e7699b47008bd801f/playwright_stealth-2.0.3.tar.gz", hash = "sha256:1d8e488fbdd8f190f1269ea8cf5d57d14df3a9f1af1001c41ee3588b2aac3133", size = 25751 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f1/30/f95f087f4b071611a7f63a2a0c9af4df3ac046dae2a693bfdacd70512867/playwright_stealth-2.0.2-py3-none-any.whl", hash = "sha256:37a5733f481b9c0ad602cf71491aa5a7c96c2a2fe4fa1e7ab764d2cd35520f2f", size = 33209 }, + { url = "https://files.pythonhosted.org/packages/b5/10/607c409712c02a26c4cb794820514cb7fdaaeac15fb05bed917fb8a354b3/playwright_stealth-2.0.3-py3-none-any.whl", hash = "sha256:1887ade423ab7ff8ae16d363a30a38de0b5817e1e4a29d47b74bf3a0e3dbfcb4", size = 34385 }, ] [[package]] @@ -2715,101 +2865,113 @@ wheels = [ [[package]] name = "propcache" -version = "0.4.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8c/d4/4e2c9aaf7ac2242b9358f98dccd8f90f2605402f5afeff6c578682c2c491/propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf", size = 80208 }, - { url = "https://files.pythonhosted.org/packages/c2/21/d7b68e911f9c8e18e4ae43bdbc1e1e9bbd971f8866eb81608947b6f585ff/propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5", size = 45777 }, - { url = "https://files.pythonhosted.org/packages/d3/1d/11605e99ac8ea9435651ee71ab4cb4bf03f0949586246476a25aadfec54a/propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e", size = 47647 }, - { url = "https://files.pythonhosted.org/packages/58/1a/3c62c127a8466c9c843bccb503d40a273e5cc69838805f322e2826509e0d/propcache-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566", size = 214929 }, - { url = "https://files.pythonhosted.org/packages/56/b9/8fa98f850960b367c4b8fe0592e7fc341daa7a9462e925228f10a60cf74f/propcache-0.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165", size = 221778 }, - { url = "https://files.pythonhosted.org/packages/46/a6/0ab4f660eb59649d14b3d3d65c439421cf2f87fe5dd68591cbe3c1e78a89/propcache-0.4.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc", size = 228144 }, - { url = "https://files.pythonhosted.org/packages/52/6a/57f43e054fb3d3a56ac9fc532bc684fc6169a26c75c353e65425b3e56eef/propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48", size = 210030 }, - { url = "https://files.pythonhosted.org/packages/40/e2/27e6feebb5f6b8408fa29f5efbb765cd54c153ac77314d27e457a3e993b7/propcache-0.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570", size = 208252 }, - { url = "https://files.pythonhosted.org/packages/9e/f8/91c27b22ccda1dbc7967f921c42825564fa5336a01ecd72eb78a9f4f53c2/propcache-0.4.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85", size = 202064 }, - { url = "https://files.pythonhosted.org/packages/f2/26/7f00bd6bd1adba5aafe5f4a66390f243acab58eab24ff1a08bebb2ef9d40/propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e", size = 212429 }, - { url = "https://files.pythonhosted.org/packages/84/89/fd108ba7815c1117ddca79c228f3f8a15fc82a73bca8b142eb5de13b2785/propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757", size = 216727 }, - { url = "https://files.pythonhosted.org/packages/79/37/3ec3f7e3173e73f1d600495d8b545b53802cbf35506e5732dd8578db3724/propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f", size = 205097 }, - { url = "https://files.pythonhosted.org/packages/61/b0/b2631c19793f869d35f47d5a3a56fb19e9160d3c119f15ac7344fc3ccae7/propcache-0.4.1-cp311-cp311-win32.whl", hash = "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1", size = 38084 }, - { url = "https://files.pythonhosted.org/packages/f4/78/6cce448e2098e9f3bfc91bb877f06aa24b6ccace872e39c53b2f707c4648/propcache-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6", size = 41637 }, - { url = "https://files.pythonhosted.org/packages/9c/e9/754f180cccd7f51a39913782c74717c581b9cc8177ad0e949f4d51812383/propcache-0.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239", size = 38064 }, - { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061 }, - { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037 }, - { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324 }, - { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505 }, - { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242 }, - { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474 }, - { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575 }, - { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736 }, - { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019 }, - { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376 }, - { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988 }, - { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615 }, - { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066 }, - { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655 }, - { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789 }, - { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750 }, - { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780 }, - { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308 }, - { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182 }, - { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215 }, - { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112 }, - { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442 }, - { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398 }, - { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920 }, - { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748 }, - { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877 }, - { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437 }, - { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586 }, - { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790 }, - { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158 }, - { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451 }, - { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374 }, - { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396 }, - { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950 }, - { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856 }, - { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420 }, - { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254 }, - { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205 }, - { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873 }, - { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739 }, - { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514 }, - { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781 }, - { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396 }, - { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897 }, - { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789 }, - { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152 }, - { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869 }, - { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596 }, - { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981 }, - { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490 }, - { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371 }, - { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424 }, - { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566 }, - { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130 }, - { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625 }, - { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209 }, - { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797 }, - { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140 }, - { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257 }, - { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097 }, - { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455 }, - { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372 }, - { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411 }, - { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712 }, - { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557 }, - { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015 }, - { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880 }, - { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938 }, - { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641 }, - { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510 }, - { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161 }, - { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393 }, - { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546 }, - { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259 }, - { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428 }, - { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305 }, +version = "0.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/44/c87281c333769159c50594f22610f77398a47ccbfbbf23074e744e86f87c/propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427", size = 50208 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/f1/8a8cc1c2c7e7934ab77e0163414f736fadbc0f5e8dd9673b952355ac175b/propcache-0.5.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:74b70780220e2dd89175ca24b81b68b67c83db499ae611e7f2313cb329801c78", size = 90744 }, + { url = "https://files.pythonhosted.org/packages/c2/f4/651b1225e976bd1a2ba5cfba0c29d096581c2636b437e3a9a7ab6276270a/propcache-0.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a4840ab0ae0216d952f4b53dc6d0b992bfc2bedbfe360bdd9b548bc184c08959", size = 52033 }, + { url = "https://files.pythonhosted.org/packages/15/a8/8ede85d6aa1f79fc7dc2f8fd2c8d65920b8272c3892903c8a1affde48cfb/propcache-0.5.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c6844ba6364fb12f403928a82cfd295ab103a2b315c77c747b2dbe4a41894ea7", size = 52754 }, + { url = "https://files.pythonhosted.org/packages/7d/fe/b3551b41bbc2f5b5bb088fc6920567cd43101253e68fbaa261339eb96fe1/propcache-0.5.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2293949b855ce597f2826452d17c2d545fb5622379c4ea6fdf525e9b8e8a2511", size = 57573 }, + { url = "https://files.pythonhosted.org/packages/83/27/ab851ebd1b7172e3e161f5f8d39e315d54a91bea246f01f4d872d3376aef/propcache-0.5.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0fd59b5af35f74da48d905dcbad55449ba13be91823cb05a9bd590bbf5b61660", size = 60645 }, + { url = "https://files.pythonhosted.org/packages/95/7d/466b3d18022e9897cbda9c735c493c5bd747d7a4c6f5ea1480b4cec434b6/propcache-0.5.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29f9309a2e42b0d273be006fdb4be2d6c39a47f6f57d8fb1cf9f81481df81b66", size = 61563 }, + { url = "https://files.pythonhosted.org/packages/27/1b/16ab7f2cf2041da2f60d156ba64c2484eadf9168075b4ff43c3ef60045af/propcache-0.5.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5aaa2b923c1944ac8febd6609cb373540a5563e7cbcb0fd770f75dace2eb817b", size = 58888 }, + { url = "https://files.pythonhosted.org/packages/0a/67/bb777ffd907633563bf35fd859c4ce97b0512c32f4633cf5d1eb7c33512b/propcache-0.5.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66ea454f095ddf5b6b14f56c064c0941c4788be11e18d2464cf643bf7203ff67", size = 59253 }, + { url = "https://files.pythonhosted.org/packages/b9/42/64f8d90b73fd9cdc1499b48057ff6d9cd2a98a25734c9bb62ecf07e87061/propcache-0.5.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:95f1e3f4760d404b13c9976c0229b2b49a3c8e2c62a9ce92efdd2b11ada75e3f", size = 57558 }, + { url = "https://files.pythonhosted.org/packages/eb/02/dba5bc03c9041f2092ea55a449caf5dfe68352c6654511b29ba0654ddb69/propcache-0.5.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:85341b12b9d55bad0bded24cac341bb34289469e03a11f3f583ea1cc1db0326c", size = 55007 }, + { url = "https://files.pythonhosted.org/packages/14/c0/43f649c7aa2a77a3b100d84e9dea3a483120ecb608bfe36ce49eaff517fe/propcache-0.5.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:26a4dca084132874e639895c3135dfad5eb20bae209f62d1aeb31b03e601c3c0", size = 60355 }, + { url = "https://files.pythonhosted.org/packages/83/c0/435dafd27f1cb4a495381dae60e25883ccfe4020bb72818e8184c1678092/propcache-0.5.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3b199b9b2b3d6a7edf3183ba8a9a137a22b97f7df525feb5ae1eccf026d2a9c6", size = 59057 }, + { url = "https://files.pythonhosted.org/packages/53/ae/6e292df9135d659944e96cb3389258e4a663e5b2b5f6c217ef0ddc8d2f73/propcache-0.5.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e59bc9e66329185b93dab73f210f1a37f81cb40f321501db8017c9aea15dba27", size = 61938 }, + { url = "https://files.pythonhosted.org/packages/0b/42/314ebc50d8159055411fd6b0bda322ff510e4b1f7d2e4927940ad0f6af20/propcache-0.5.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:552ffadf6ad409844bc5919c42a0a83d88314cedddaea0e41e80a8b8fffe881f", size = 59731 }, + { url = "https://files.pythonhosted.org/packages/b8/9b/2da6dee38871c3c8772fabc2758325a5c9077d6d18c597737dc04dd884cd/propcache-0.5.2-cp311-cp311-win32.whl", hash = "sha256:cd416c1de191973c52ff1a12a57446bfc7642797b282d7caf2162d7d1b8aa9a0", size = 38966 }, + { url = "https://files.pythonhosted.org/packages/42/4e/f17363fb58c0afe05b067361cb6d86ed2d29de6506779a27547c4d183075/propcache-0.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:44e488ef40dbb452700b2b1f8188934121f6648f52c295055662d2191959ff82", size = 42135 }, + { url = "https://files.pythonhosted.org/packages/c6/eb/6af6685077d22e8b33358d3c548e3282706a0b3cd85044ffba4e5dd08e3b/propcache-0.5.2-cp311-cp311-win_arm64.whl", hash = "sha256:54adaa85a22078d1e306304a40984dc5be99d599bf3dc0a24dc98f7daeab89ab", size = 38381 }, + { url = "https://files.pythonhosted.org/packages/4a/cb/e27bc2b2737a0bb49962b275efa051e8f1c35a936df7d5139b6b658b7dc9/propcache-0.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba", size = 95887 }, + { url = "https://files.pythonhosted.org/packages/e6/13/b8ae04c59392f8d11c6cd9fb4011d1dc7c86b81225c770280300e259ffe1/propcache-0.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a", size = 54654 }, + { url = "https://files.pythonhosted.org/packages/2c/7d/49777a3e20b55863d4794384a38acd460c04157b0a00f8602b0d508b8431/propcache-0.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf", size = 55190 }, + { url = "https://files.pythonhosted.org/packages/44/c7/085d0cd63062e84044e3f05797749c3f8e3938ff3aeb0eb2f69d43fafc91/propcache-0.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144", size = 59995 }, + { url = "https://files.pythonhosted.org/packages/9c/42/32cf8e3009e92b2645cf1e944f701e8ea4e924dffde1ee26db860bcbf7e4/propcache-0.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9", size = 63422 }, + { url = "https://files.pythonhosted.org/packages/9e/1b/f112433f99fc979431b87a39ef169e3f8df070d99a72792c56d6937ac48b/propcache-0.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42", size = 64342 }, + { url = "https://files.pythonhosted.org/packages/14/15/5574111ae50dd6e879456888c0eadd4c5a869959775854e18e18a6b345f3/propcache-0.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476", size = 61639 }, + { url = "https://files.pythonhosted.org/packages/cc/da/4d775080b1490c0ae604acda868bd71aabe3a89ed16f2aa4339eb8a283e7/propcache-0.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba", size = 61588 }, + { url = "https://files.pythonhosted.org/packages/04/ac/f076982cbe2195ee9cf32de5a1e46951d9fb399fc207f390562dd0fd8fb2/propcache-0.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a", size = 60029 }, + { url = "https://files.pythonhosted.org/packages/70/60/189be62e0dd898dce3b331e1b8c7a543cd3a405ac0c81fe8ee8a9d5d77e1/propcache-0.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64", size = 56774 }, + { url = "https://files.pythonhosted.org/packages/ea/9e/93377b9c7939c1ffae98f878dee955efadfd638078bc86dbc21f9d52f651/propcache-0.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913", size = 63532 }, + { url = "https://files.pythonhosted.org/packages/14/f9/590ef6cfb9b8028d516d287812ece32bb0bc5f11fbb9c8bf6b2e6313fec8/propcache-0.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1", size = 61592 }, + { url = "https://files.pythonhosted.org/packages/b4/5e/70958b3034c297a630bba2f17ca7abc2d5f39a803ad7e370ab79d1ecd022/propcache-0.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33", size = 64788 }, + { url = "https://files.pythonhosted.org/packages/12/fd/77fe5936d8c3086ca9048f7f415f122ed82e53884a9ec193646b42deef06/propcache-0.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a", size = 62514 }, + { url = "https://files.pythonhosted.org/packages/cf/74/66bd798b5b3be70aa1b391f5cc9d6a0a5532d7fd3b19ec0b213e72e6ad9d/propcache-0.5.2-cp312-cp312-win32.whl", hash = "sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031", size = 39018 }, + { url = "https://files.pythonhosted.org/packages/61/7c/5c0d34aa3024694d6dcb9271cdbdd08c4e47c1c0ad95ec7e7bc74cdea145/propcache-0.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42", size = 42322 }, + { url = "https://files.pythonhosted.org/packages/4d/91/875812f1a3feb20ceba818ef39fbe4d92f1081e04ac815c822496d0d038b/propcache-0.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84", size = 38172 }, + { url = "https://files.pythonhosted.org/packages/c5/09/f049e45385503fe67db75a6b6186a7b9f0c3930366dc960522c312a825b1/propcache-0.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a", size = 94457 }, + { url = "https://files.pythonhosted.org/packages/6b/65/83d1d05655baf63113731bd5a1008435e14f8d1e5a06cbe4ec5b23ad7a31/propcache-0.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117", size = 53835 }, + { url = "https://files.pythonhosted.org/packages/a9/12/a6ba6482bb5ea3260c000c9b20881c95fa11c6b30173715668259f844ed7/propcache-0.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098", size = 54545 }, + { url = "https://files.pythonhosted.org/packages/a9/19/7fa086f5764c59ec8a8e157cd93aa8497acc00aba9dcdec56bfffb32602d/propcache-0.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4", size = 59886 }, + { url = "https://files.pythonhosted.org/packages/a1/e4/5d7663dc8235956c8f5281698a3af1d351d8820341ddd890f59d9a9127f2/propcache-0.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e", size = 63261 }, + { url = "https://files.pythonhosted.org/packages/4a/4a/15a03adee24d6350da4292caeac44c34c033d2afe5e87eb370f38854560f/propcache-0.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7", size = 64184 }, + { url = "https://files.pythonhosted.org/packages/8b/c6/979176efdaa3d239e36d503d5af63a0a773b36662ed8f52e5b6a6d9fd40e/propcache-0.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d", size = 61534 }, + { url = "https://files.pythonhosted.org/packages/c8/22/63e8cd1bae4c2d2be6493b6b7d10566ddafad88137cfbc99964a1119853c/propcache-0.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a", size = 61500 }, + { url = "https://files.pythonhosted.org/packages/60/5a/28e5d9acbac1cc9ccb67045e8c1b943aa8d79fdf39c93bd73cacd68008ea/propcache-0.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2", size = 59994 }, + { url = "https://files.pythonhosted.org/packages/f3/40/db650677f554a95b9c01a7c9d93d629e93a15562f5deb4573c9ee136fed2/propcache-0.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa", size = 56884 }, + { url = "https://files.pythonhosted.org/packages/80/45/70b39b89516ff8b96bf732fa6fded8cef20f293cb1508690101c3c07ec51/propcache-0.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853", size = 63464 }, + { url = "https://files.pythonhosted.org/packages/f9/e2/fa59d3a89eac5534293124af4f1d0d0ada091ce4a0ab4610ce03fd2bdd8d/propcache-0.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a", size = 61588 }, + { url = "https://files.pythonhosted.org/packages/0b/97/efb547a55c4bc7381cfb202d6a2239ac621045277bc1ea5dfd3a7f0516c0/propcache-0.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704", size = 64667 }, + { url = "https://files.pythonhosted.org/packages/92/56/f5c7d9b4b7595d5127da38974d791b2153f3d1eae6c674af3583ace92ad3/propcache-0.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4", size = 62463 }, + { url = "https://files.pythonhosted.org/packages/bd/3b/484a3a65fc9f9f60c41dcd17b428bace5389544e2c680994534a20755066/propcache-0.5.2-cp313-cp313-win32.whl", hash = "sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d", size = 38621 }, + { url = "https://files.pythonhosted.org/packages/1c/fd/3f0f10dba4dabad3bf53102be007abf55481067952bde0fdddff439e7c61/propcache-0.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757", size = 41649 }, + { url = "https://files.pythonhosted.org/packages/90/ec/6ce619cc32bb500a482f811f9cd509368b4e58e638d13f2c68f370d6b475/propcache-0.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f", size = 37636 }, + { url = "https://files.pythonhosted.org/packages/1b/82/c1d268bbbf2ef981c5bf0fbbe746db617c66e3bcefe431a1aa8943fbe23a/propcache-0.5.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d", size = 98872 }, + { url = "https://files.pythonhosted.org/packages/f4/d4/52c871e73e864e6b34c0e2d58ac1ec5ccd149497ddc7ad2137ae98323a35/propcache-0.5.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa", size = 56257 }, + { url = "https://files.pythonhosted.org/packages/67/f0/9b90ca2a210b3d09bcfcd96ecd0f55545c091535abce2a45de2775cfd357/propcache-0.5.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94", size = 56696 }, + { url = "https://files.pythonhosted.org/packages/9d/0e/6e9d4ba07c8e56e21ddec1e75f12148142b21ca83a51871babce095334f4/propcache-0.5.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164", size = 62378 }, + { url = "https://files.pythonhosted.org/packages/65/19/c10badaa463dde8a27ce884f8ee2ec37e6035b7c9f5ff0c8f74f06f08dac/propcache-0.5.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f", size = 65283 }, + { url = "https://files.pythonhosted.org/packages/b0/b6/93bea99ca80e19cef6512a8580e5b7857bbe09422d9daa7fd4ef5723306c/propcache-0.5.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c", size = 66616 }, + { url = "https://files.pythonhosted.org/packages/83/e4/5c7462e50625f051f37fb38b8224f7639f667184bbd34424ec83819bb1b7/propcache-0.5.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc", size = 63773 }, + { url = "https://files.pythonhosted.org/packages/ca/b6/99238894047b13c823be25027e736626cd414a52a5e30d2c3347c2733529/propcache-0.5.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f", size = 63664 }, + { url = "https://files.pythonhosted.org/packages/85/1e/a3a1a63116a2b8edb415a8bb9a6f0c34bd03830b1e18e8ce2904e1dc1cf4/propcache-0.5.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb", size = 62643 }, + { url = "https://files.pythonhosted.org/packages/e4/03/893cf147de2fc6543c5eaa07ad833170e7e2a2385725bbebe8c0503723bb/propcache-0.5.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751", size = 59595 }, + { url = "https://files.pythonhosted.org/packages/86/3b/04c1a2e12c57766568ba75ba72b3bf2042818d4c1425fab6fc07155c7cff/propcache-0.5.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836", size = 65711 }, + { url = "https://files.pythonhosted.org/packages/1c/34/80f8d0099f8d6bacc4de1624c85672681c8cd1149ca2da0e38fd120b817f/propcache-0.5.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f", size = 64247 }, + { url = "https://files.pythonhosted.org/packages/f3/1a/8b08f3a5f1037e9e370c55883ceeeee0f6dd0416fb2d2d67b8bfc91f2a79/propcache-0.5.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55", size = 67102 }, + { url = "https://files.pythonhosted.org/packages/34/68/8bdb7bb7756d76e005490649d10e4a8369e610c74d619f71e1aedf889e9c/propcache-0.5.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568", size = 64964 }, + { url = "https://files.pythonhosted.org/packages/0a/aa/50fb0b5d3968b61a510926ff8b8465f1d6e976b3ab74496d7a4b9fc42515/propcache-0.5.2-cp313-cp313t-win32.whl", hash = "sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191", size = 42546 }, + { url = "https://files.pythonhosted.org/packages/ae/4c/0ddbae64321bd4a95bcbfc19307238016b5b1fee645c84626c8d539e5b74/propcache-0.5.2-cp313-cp313t-win_amd64.whl", hash = "sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7", size = 46330 }, + { url = "https://files.pythonhosted.org/packages/00/d9/9cddc8efb78d8af264c5ec9f6d10b62f57c515feda8d321595f56010fb23/propcache-0.5.2-cp313-cp313t-win_arm64.whl", hash = "sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96", size = 40521 }, + { url = "https://files.pythonhosted.org/packages/e2/ea/23ee535d90ce8bcc465a3028eb3cc0ce3bd1005f4bb27710b30587de798d/propcache-0.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999", size = 94662 }, + { url = "https://files.pythonhosted.org/packages/b5/06/c5a52f419b5d8972f8d46a7577476090d8e3263ff589ce40b5ca4968d5be/propcache-0.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e", size = 53928 }, + { url = "https://files.pythonhosted.org/packages/63/b1/4260d67d6bd85e58a66b72d54ce15d5de789b6f3870cc6bedf8ff9667401/propcache-0.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539", size = 54650 }, + { url = "https://files.pythonhosted.org/packages/70/06/2f46c318e3307cd7a6a7481def374ce838c0fe20084b39dd54b0879d0e99/propcache-0.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e", size = 59912 }, + { url = "https://files.pythonhosted.org/packages/4c/29/fe1aebec2ce57ab985a9c382bded1124431f85078113aa222c5d278430d4/propcache-0.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979", size = 63300 }, + { url = "https://files.pythonhosted.org/packages/b4/18/2334b26768b6c82be8c69e83671b767d5ef426aa09b0cba6c2ea47816774/propcache-0.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80", size = 64208 }, + { url = "https://files.pythonhosted.org/packages/2b/76/7f1bfd6afff4c5e38e36a3c6d68eb5f4b7311ea80baf693db78d95b603c4/propcache-0.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825", size = 61633 }, + { url = "https://files.pythonhosted.org/packages/c4/46/b3ff8aba2b4953a3e50de2cf72f1b5748b8eca93b15f3dc2c84339084c09/propcache-0.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39", size = 61724 }, + { url = "https://files.pythonhosted.org/packages/c5/01/814cfcafbcff954f94c01cf30e097ddc88a076b5440fbcf4570753437d40/propcache-0.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4", size = 60069 }, + { url = "https://files.pythonhosted.org/packages/da/68/5c6f7622d510cc666a300687e06fd060c1a43361c0c9b20d284f06d8096a/propcache-0.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5", size = 57099 }, + { url = "https://files.pythonhosted.org/packages/55/27/9cb0b4c679124085327957d42521c99dba04c88c90c3e55a6f0b633ebccc/propcache-0.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702", size = 63391 }, + { url = "https://files.pythonhosted.org/packages/f0/9d/7258aaa5bdf60fc6f27591eef6fe52768cb0beda7140be477c8b12c9794a/propcache-0.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3", size = 61626 }, + { url = "https://files.pythonhosted.org/packages/8e/0d/41c602003e8a9b16fe1e7eadf62c7bfba9d5474370b24200bf48b315f45f/propcache-0.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5", size = 64781 }, + { url = "https://files.pythonhosted.org/packages/8b/f3/38e66b1856e9bd079deea015bc4a55f7767c0e4db2f7dcf69e7e680ba4ce/propcache-0.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4", size = 62570 }, + { url = "https://files.pythonhosted.org/packages/95/ca/bbfe9b910ce57dde8bb4876b4520fc02a4e89497c10de26be936758a3aaa/propcache-0.5.2-cp314-cp314-win32.whl", hash = "sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0", size = 39436 }, + { url = "https://files.pythonhosted.org/packages/61/d2/45c9defbaa1ea297035d9d4cce9e8f80daafbf19319c6007f157c6256ea9/propcache-0.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c", size = 42373 }, + { url = "https://files.pythonhosted.org/packages/44/68/9ea5103f41d5217d7d6ec24db90018e23aebec070c3f9a6e54d12b841fd8/propcache-0.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0", size = 38554 }, + { url = "https://files.pythonhosted.org/packages/8a/81/fadf555f42d3b762eea8a53950b0489fdc0aa9da5f8ed9e10ce0a4e01b48/propcache-0.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb", size = 99395 }, + { url = "https://files.pythonhosted.org/packages/f5/c9/c61e134a686949cf7971af3a390148b1156f7be81c73bc0cd12c873e2d48/propcache-0.5.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078", size = 56653 }, + { url = "https://files.pythonhosted.org/packages/cb/73/daf935ea7048ddd7ec8eec5345b4a40b619d2d178b3c0a0900796bc3c794/propcache-0.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa", size = 56914 }, + { url = "https://files.pythonhosted.org/packages/79/9f/aba959b435ea18617edd7cf0a7ad0b9c574b8fc7e3d2cd55fb59cb255d33/propcache-0.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917", size = 62567 }, + { url = "https://files.pythonhosted.org/packages/6c/a1/859942de9a791ff42f6141736f5b37749b8f53e65edfa49638c67dd67e6a/propcache-0.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe", size = 65542 }, + { url = "https://files.pythonhosted.org/packages/b5/61/315bc0fd6c0fc7f80a528b8afd209e5fc4a875ea79571b91b8f50f442907/propcache-0.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03", size = 66845 }, + { url = "https://files.pythonhosted.org/packages/47/f7/9f8122e3132e8e354ac41975ef8f1099be7d5a16bc7ae562734e993665c0/propcache-0.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335", size = 63985 }, + { url = "https://files.pythonhosted.org/packages/c8/54/c317819ec157cbf6f35df9df9657a6f82daf34d5faf15948b2f639c2192e/propcache-0.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285", size = 63999 }, + { url = "https://files.pythonhosted.org/packages/5a/56/387e3f7dfce0a9233df41fb888aa1c30222cb4bbbf09537c02dd9bd85fe2/propcache-0.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837", size = 62779 }, + { url = "https://files.pythonhosted.org/packages/a1/9c/596784cb5824ed61ee960d3f8655a3f0993e107c6e98ab6c818b7fb92ccb/propcache-0.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8", size = 59796 }, + { url = "https://files.pythonhosted.org/packages/c2/3d/1a6cfa1726a48542c1e8784a0761421476a5b68e09b7f36bf95eb954aaba/propcache-0.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366", size = 66023 }, + { url = "https://files.pythonhosted.org/packages/e4/0e/05fd6990369477076e4e280bcb970de760fddf0161a46e988bc95f7940ec/propcache-0.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56", size = 64448 }, + { url = "https://files.pythonhosted.org/packages/cd/86/5f8da315a4309c62c10c0b2516b17492d5d3bbe1bb862b96604db67e2a37/propcache-0.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d", size = 67329 }, + { url = "https://files.pythonhosted.org/packages/da/d3/3368efe79ab21f0cdf86ef49895811c9cc933131d4cde1f28a624e22e712/propcache-0.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2", size = 65172 }, + { url = "https://files.pythonhosted.org/packages/d5/07/127e8b0bacfb325396196f9d976a22453049b89b9b2b08477cc3145faa44/propcache-0.5.2-cp314-cp314t-win32.whl", hash = "sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821", size = 43813 }, + { url = "https://files.pythonhosted.org/packages/88/fb/46dad6c0ae49ed230ab1b16c890c2b6314e2403e6c412976f4a72d64a527/propcache-0.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370", size = 47764 }, + { url = "https://files.pythonhosted.org/packages/e7/c4/a47d0a63aa309d10d59ede6e9d4cff03a344a79d1f0f4cd0cd74997b53e0/propcache-0.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6", size = 41140 }, + { url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036 }, ] [[package]] @@ -2851,52 +3013,45 @@ wheels = [ [[package]] name = "pyarrow" -version = "23.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/88/22/134986a4cc224d593c1afde5494d18ff629393d74cc2eddb176669f234a4/pyarrow-23.0.1.tar.gz", hash = "sha256:b8c5873e33440b2bc2f4a79d2b47017a89c5a24116c055625e6f2ee50523f019", size = 1167336 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b0/41/8e6b6ef7e225d4ceead8459427a52afdc23379768f54dd3566014d7618c1/pyarrow-23.0.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:6f0147ee9e0386f519c952cc670eb4a8b05caa594eeffe01af0e25f699e4e9bb", size = 34302230 }, - { url = "https://files.pythonhosted.org/packages/bf/4a/1472c00392f521fea03ae93408bf445cc7bfa1ab81683faf9bc188e36629/pyarrow-23.0.1-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:0ae6e17c828455b6265d590100c295193f93cc5675eb0af59e49dbd00d2de350", size = 35850050 }, - { url = "https://files.pythonhosted.org/packages/0c/b2/bd1f2f05ded56af7f54d702c8364c9c43cd6abb91b0e9933f3d77b4f4132/pyarrow-23.0.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:fed7020203e9ef273360b9e45be52a2a47d3103caf156a30ace5247ffb51bdbd", size = 44491918 }, - { url = "https://files.pythonhosted.org/packages/0b/62/96459ef5b67957eac38a90f541d1c28833d1b367f014a482cb63f3b7cd2d/pyarrow-23.0.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:26d50dee49d741ac0e82185033488d28d35be4d763ae6f321f97d1140eb7a0e9", size = 47562811 }, - { url = "https://files.pythonhosted.org/packages/7d/94/1170e235add1f5f45a954e26cd0e906e7e74e23392dcb560de471f7366ec/pyarrow-23.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3c30143b17161310f151f4a2bcfe41b5ff744238c1039338779424e38579d701", size = 48183766 }, - { url = "https://files.pythonhosted.org/packages/0e/2d/39a42af4570377b99774cdb47f63ee6c7da7616bd55b3d5001aa18edfe4f/pyarrow-23.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db2190fa79c80a23fdd29fef4b8992893f024ae7c17d2f5f4db7171fa30c2c78", size = 50607669 }, - { url = "https://files.pythonhosted.org/packages/00/ca/db94101c187f3df742133ac837e93b1f269ebdac49427f8310ee40b6a58f/pyarrow-23.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:f00f993a8179e0e1c9713bcc0baf6d6c01326a406a9c23495ec1ba9c9ebf2919", size = 27527698 }, - { url = "https://files.pythonhosted.org/packages/9a/4b/4166bb5abbfe6f750fc60ad337c43ecf61340fa52ab386da6e8dbf9e63c4/pyarrow-23.0.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:f4b0dbfa124c0bb161f8b5ebb40f1a680b70279aa0c9901d44a2b5a20806039f", size = 34214575 }, - { url = "https://files.pythonhosted.org/packages/e1/da/3f941e3734ac8088ea588b53e860baeddac8323ea40ce22e3d0baa865cc9/pyarrow-23.0.1-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:7707d2b6673f7de054e2e83d59f9e805939038eebe1763fe811ee8fa5c0cd1a7", size = 35832540 }, - { url = "https://files.pythonhosted.org/packages/88/7c/3d841c366620e906d54430817531b877ba646310296df42ef697308c2705/pyarrow-23.0.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:86ff03fb9f1a320266e0de855dee4b17da6794c595d207f89bba40d16b5c78b9", size = 44470940 }, - { url = "https://files.pythonhosted.org/packages/2c/a5/da83046273d990f256cb79796a190bbf7ec999269705ddc609403f8c6b06/pyarrow-23.0.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:813d99f31275919c383aab17f0f455a04f5a429c261cc411b1e9a8f5e4aaaa05", size = 47586063 }, - { url = "https://files.pythonhosted.org/packages/5b/3c/b7d2ebcff47a514f47f9da1e74b7949138c58cfeb108cdd4ee62f43f0cf3/pyarrow-23.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bf5842f960cddd2ef757d486041d57c96483efc295a8c4a0e20e704cbbf39c67", size = 48173045 }, - { url = "https://files.pythonhosted.org/packages/43/b2/b40961262213beaba6acfc88698eb773dfce32ecdf34d19291db94c2bd73/pyarrow-23.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:564baf97c858ecc03ec01a41062e8f4698abc3e6e2acd79c01c2e97880a19730", size = 50621741 }, - { url = "https://files.pythonhosted.org/packages/f6/70/1fdda42d65b28b078e93d75d371b2185a61da89dda4def8ba6ba41ebdeb4/pyarrow-23.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:07deae7783782ac7250989a7b2ecde9b3c343a643f82e8a4df03d93b633006f0", size = 27620678 }, - { url = "https://files.pythonhosted.org/packages/47/10/2cbe4c6f0fb83d2de37249567373d64327a5e4d8db72f486db42875b08f6/pyarrow-23.0.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6b8fda694640b00e8af3c824f99f789e836720aa8c9379fb435d4c4953a756b8", size = 34210066 }, - { url = "https://files.pythonhosted.org/packages/cb/4f/679fa7e84dadbaca7a65f7cdba8d6c83febbd93ca12fa4adf40ba3b6362b/pyarrow-23.0.1-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:8ff51b1addc469b9444b7c6f3548e19dc931b172ab234e995a60aea9f6e6025f", size = 35825526 }, - { url = "https://files.pythonhosted.org/packages/f9/63/d2747d930882c9d661e9398eefc54f15696547b8983aaaf11d4a2e8b5426/pyarrow-23.0.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:71c5be5cbf1e1cb6169d2a0980850bccb558ddc9b747b6206435313c47c37677", size = 44473279 }, - { url = "https://files.pythonhosted.org/packages/b3/93/10a48b5e238de6d562a411af6467e71e7aedbc9b87f8d3a35f1560ae30fb/pyarrow-23.0.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:9b6f4f17b43bc39d56fec96e53fe89d94bac3eb134137964371b45352d40d0c2", size = 47585798 }, - { url = "https://files.pythonhosted.org/packages/5c/20/476943001c54ef078dbf9542280e22741219a184a0632862bca4feccd666/pyarrow-23.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fc13fc6c403d1337acab46a2c4346ca6c9dec5780c3c697cf8abfd5e19b6b37", size = 48179446 }, - { url = "https://files.pythonhosted.org/packages/4b/b6/5dd0c47b335fcd8edba9bfab78ad961bd0fd55ebe53468cc393f45e0be60/pyarrow-23.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5c16ed4f53247fa3ffb12a14d236de4213a4415d127fe9cebed33d51671113e2", size = 50623972 }, - { url = "https://files.pythonhosted.org/packages/d5/09/a532297c9591a727d67760e2e756b83905dd89adb365a7f6e9c72578bcc1/pyarrow-23.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:cecfb12ef629cf6be0b1887f9f86463b0dd3dc3195ae6224e74006be4736035a", size = 27540749 }, - { url = "https://files.pythonhosted.org/packages/a5/8e/38749c4b1303e6ae76b3c80618f84861ae0c55dd3c2273842ea6f8258233/pyarrow-23.0.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:29f7f7419a0e30264ea261fdc0e5fe63ce5a6095003db2945d7cd78df391a7e1", size = 34471544 }, - { url = "https://files.pythonhosted.org/packages/a3/73/f237b2bc8c669212f842bcfd842b04fc8d936bfc9d471630569132dc920d/pyarrow-23.0.1-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:33d648dc25b51fd8055c19e4261e813dfc4d2427f068bcecc8b53d01b81b0500", size = 35949911 }, - { url = "https://files.pythonhosted.org/packages/0c/86/b912195eee0903b5611bf596833def7d146ab2d301afeb4b722c57ffc966/pyarrow-23.0.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:cd395abf8f91c673dd3589cadc8cc1ee4e8674fa61b2e923c8dd215d9c7d1f41", size = 44520337 }, - { url = "https://files.pythonhosted.org/packages/69/c2/f2a717fb824f62d0be952ea724b4f6f9372a17eed6f704b5c9526f12f2f1/pyarrow-23.0.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:00be9576d970c31defb5c32eb72ef585bf600ef6d0a82d5eccaae96639cf9d07", size = 47548944 }, - { url = "https://files.pythonhosted.org/packages/84/a7/90007d476b9f0dc308e3bc57b832d004f848fd6c0da601375d20d92d1519/pyarrow-23.0.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c2139549494445609f35a5cda4eb94e2c9e4d704ce60a095b342f82460c73a83", size = 48236269 }, - { url = "https://files.pythonhosted.org/packages/b0/3f/b16fab3e77709856eb6ac328ce35f57a6d4a18462c7ca5186ef31b45e0e0/pyarrow-23.0.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7044b442f184d84e2351e5084600f0d7343d6117aabcbc1ac78eb1ae11eb4125", size = 50604794 }, - { url = "https://files.pythonhosted.org/packages/e9/a1/22df0620a9fac31d68397a75465c344e83c3dfe521f7612aea33e27ab6c0/pyarrow-23.0.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a35581e856a2fafa12f3f54fce4331862b1cfb0bef5758347a858a4aa9d6bae8", size = 27660642 }, - { url = "https://files.pythonhosted.org/packages/8d/1b/6da9a89583ce7b23ac611f183ae4843cd3a6cf54f079549b0e8c14031e73/pyarrow-23.0.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:5df1161da23636a70838099d4aaa65142777185cc0cdba4037a18cee7d8db9ca", size = 34238755 }, - { url = "https://files.pythonhosted.org/packages/ae/b5/d58a241fbe324dbaeb8df07be6af8752c846192d78d2272e551098f74e88/pyarrow-23.0.1-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:fa8e51cb04b9f8c9c5ace6bab63af9a1f88d35c0d6cbf53e8c17c098552285e1", size = 35847826 }, - { url = "https://files.pythonhosted.org/packages/54/a5/8cbc83f04aba433ca7b331b38f39e000efd9f0c7ce47128670e737542996/pyarrow-23.0.1-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:0b95a3994f015be13c63148fef8832e8a23938128c185ee951c98908a696e0eb", size = 44536859 }, - { url = "https://files.pythonhosted.org/packages/36/2e/c0f017c405fcdc252dbccafbe05e36b0d0eb1ea9a958f081e01c6972927f/pyarrow-23.0.1-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:4982d71350b1a6e5cfe1af742c53dfb759b11ce14141870d05d9e540d13bc5d1", size = 47614443 }, - { url = "https://files.pythonhosted.org/packages/af/6b/2314a78057912f5627afa13ba43809d9d653e6630859618b0fd81a4e0759/pyarrow-23.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c250248f1fe266db627921c89b47b7c06fee0489ad95b04d50353537d74d6886", size = 48232991 }, - { url = "https://files.pythonhosted.org/packages/40/f2/1bcb1d3be3460832ef3370d621142216e15a2c7c62602a4ea19ec240dd64/pyarrow-23.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5f4763b83c11c16e5f4c15601ba6dfa849e20723b46aa2617cb4bffe8768479f", size = 50645077 }, - { url = "https://files.pythonhosted.org/packages/eb/3f/b1da7b61cd66566a4d4c8383d376c606d1c34a906c3f1cb35c479f59d1aa/pyarrow-23.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:3a4c85ef66c134161987c17b147d6bffdca4566f9a4c1d81a0a01cdf08414ea5", size = 28234271 }, - { url = "https://files.pythonhosted.org/packages/b5/78/07f67434e910a0f7323269be7bfbf58699bd0c1d080b18a1ab49ba943fe8/pyarrow-23.0.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:17cd28e906c18af486a499422740298c52d7c6795344ea5002a7720b4eadf16d", size = 34488692 }, - { url = "https://files.pythonhosted.org/packages/50/76/34cf7ae93ece1f740a04910d9f7e80ba166b9b4ab9596a953e9e62b90fe1/pyarrow-23.0.1-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:76e823d0e86b4fb5e1cf4a58d293036e678b5a4b03539be933d3b31f9406859f", size = 35964383 }, - { url = "https://files.pythonhosted.org/packages/46/90/459b827238936d4244214be7c684e1b366a63f8c78c380807ae25ed92199/pyarrow-23.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a62e1899e3078bf65943078b3ad2a6ddcacf2373bc06379aac61b1e548a75814", size = 44538119 }, - { url = "https://files.pythonhosted.org/packages/28/a1/93a71ae5881e99d1f9de1d4554a87be37da11cd6b152239fb5bd924fdc64/pyarrow-23.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:df088e8f640c9fae3b1f495b3c64755c4e719091caf250f3a74d095ddf3c836d", size = 47571199 }, - { url = "https://files.pythonhosted.org/packages/88/a3/d2c462d4ef313521eaf2eff04d204ac60775263f1fb08c374b543f79f610/pyarrow-23.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:46718a220d64677c93bc243af1d44b55998255427588e400677d7192671845c7", size = 48259435 }, - { url = "https://files.pythonhosted.org/packages/cc/f1/11a544b8c3d38a759eb3fbb022039117fd633e9a7b19e4841cc3da091915/pyarrow-23.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a09f3876e87f48bc2f13583ab551f0379e5dfb83210391e68ace404181a20690", size = 50629149 }, - { url = "https://files.pythonhosted.org/packages/50/f2/c0e76a0b451ffdf0cf788932e182758eb7558953f4f27f1aff8e2518b653/pyarrow-23.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:527e8d899f14bd15b740cd5a54ad56b7f98044955373a17179d5956ddb93d9ce", size = 28365807 }, +version = "25.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/27/f3/95428098d1fa7d04432fb750eed06b41304c2f6a5d3319985e64db2d9d41/pyarrow-25.0.0.tar.gz", hash = "sha256:d2d697008b5ec06d75952ef260c2e9a8a0f6ccfce24266c04c9c8ade927cb3b4", size = 1199181 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/98/ae2b5acf9876dbeffa6f320776242c52caab062df55c8ac5501ed2679e74/pyarrow-25.0.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:2e3b6544e26e393fe2cd530f523e36c1c8d3c345bbbb60cca3fd866be8322517", size = 35939080 }, + { url = "https://files.pythonhosted.org/packages/80/09/3de2a968edbd496c86cb8b932cdbee2d4b08c4a28e9884a15e5c705a646b/pyarrow-25.0.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:b724d127783b4c19f088fcdfc844cbc318809246a30307bcabd5ed02045e890e", size = 37633420 }, + { url = "https://files.pythonhosted.org/packages/19/86/8399243a4ce080426ec37db18d5e29148b7ec960a8a8c7f9059a7bf6ef0a/pyarrow-25.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:244f98a595f70fa4fd35faa7508c4ae67e14a173397a4b3b49d2b3c360fb0062", size = 46861050 }, + { url = "https://files.pythonhosted.org/packages/7b/79/72d704b02bc5fc6d06954d76a0208c1e79cad3ab370f6d6a91ffe5078870/pyarrow-25.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:0222f0071d13313962a88d21bf28b80d355ac39d81bfa6ff3fe00eeaf748e4be", size = 50056458 }, + { url = "https://files.pythonhosted.org/packages/06/5d/3c31a60b6403d63cad2e0f829096f5fc5763a129ead4207a5d4690b96448/pyarrow-25.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b58726f118c079f9d4ed7e904975d4f15fd69d0741ba511a4e2dcaa4ef16354f", size = 49957793 }, + { url = "https://files.pythonhosted.org/packages/34/f7/8f8a019061f9863a831915329264372a87ed25eaf9109ce56eb0e84012c5/pyarrow-25.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:38a2c887cb3883e241b70201688db34133b6dfadd04f03c8f9213df53770c18e", size = 53100544 }, + { url = "https://files.pythonhosted.org/packages/f1/e2/738071e95c5ddad7b3dfc12f569ffa992db89d7d7b4a95258fd184191249/pyarrow-25.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:161649d60a7a46c613a19fd795763ea8a88c36ba997dd99d9bc66e6794ee36e8", size = 27848311 }, + { url = "https://files.pythonhosted.org/packages/73/44/fdd3a4377807b7dcabe2d4b5aa99dbbc98e2e5df3f1ca4e7f0aec492d987/pyarrow-25.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:149730a3d1f0fb59d663a0b8aa210adfd9c17c27cd94a0d143e60daea8320d4e", size = 35850884 }, + { url = "https://files.pythonhosted.org/packages/bf/71/9f053177a7709b8c90abb00a2375b916286f9f0d6cfb21a5cadd4ef811e8/pyarrow-25.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:0721332c30fdd453fdd1fc203b2ac1f4c9db5aea28fa38d41f2574c4b068b9ec", size = 37616197 }, + { url = "https://files.pythonhosted.org/packages/95/1a/22bfb6597dcdc861fa83c39c06e1457cb56f698940eff42fbb25de30e8e5/pyarrow-25.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:fa1482b3da10cac2d4db6e26b81da543e237616af2ef6d466018b31ca586496f", size = 46841966 }, + { url = "https://files.pythonhosted.org/packages/55/0e/cd705c042bc4fe7022478db577fcab4abdcfabb9bc37ab7a75556b3fcb2b/pyarrow-25.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:5d1dbf24e151042f2fa3c129563f65d66674128868496fb008c4272b16bdf778", size = 50088993 }, + { url = "https://files.pythonhosted.org/packages/98/ee/d822e1ee31fe31ec5d057210e0605c950b975dcd8d9a332976cc859a9df8/pyarrow-25.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:20887a762dd61dcc530f93a140840ab1f6aa7836b33270e42d627ab3cf11e537", size = 49941005 }, + { url = "https://files.pythonhosted.org/packages/33/1b/207a90cc64619a095eb75a263ae069735f2810056d43c667befd573ec083/pyarrow-25.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:58d1ab556b0cea1c93fdb799b24ad58adb2f2a2788dbce782a94f64ae1a5cc9b", size = 53112355 }, + { url = "https://files.pythonhosted.org/packages/7e/fe/81d1e5f8beed15c01e98649d5c6e2167b67fd395884a2488f18bf1cf0dba/pyarrow-25.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:3f356afe61186395c861d5cd63dc21ff7d5fa335012a4668d979257df7fea0f5", size = 27945954 }, + { url = "https://files.pythonhosted.org/packages/6c/c8/098ce17d778fd9d29e40bb8c5f19a40cc90c3f0b46c9057b0d7993f42f54/pyarrow-25.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:8831a3ba52fa7cdb78d368d968b1dcd06171e6dff5461e16d90de91d371e47bc", size = 35844549 }, + { url = "https://files.pythonhosted.org/packages/bc/66/24c28877219abf6263d909b1592c97ff82c59f13a59acbed11fc87c0654f/pyarrow-25.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:5f4bacb60f91dd2fca6c52f1b9a0012cd090e0294f1f781dc1881a247a352f8e", size = 37610397 }, + { url = "https://files.pythonhosted.org/packages/53/55/6d1d5f5aff317ec5de9421594679ed51ed828fe7e2ce209327f819d801e4/pyarrow-25.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:59516c822d5fd8e544aaa0dfe72f36fed5d4c24ea8390aab1bcd31d7e959c6be", size = 46841701 }, + { url = "https://files.pythonhosted.org/packages/b5/5d/f790fb6965ab54c9da0dda7856abc75fd0d7648d865f8d603c111d203a64/pyarrow-25.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6f9dbd83e91c239a1f5ee7ce13f108b5f6c0efbe40a4375260d8f08b43ad05e9", size = 50090118 }, + { url = "https://files.pythonhosted.org/packages/0c/8c/faf025357ebf31bc96777f234277aa31e2aeca6dd4ecaa391f29085473c2/pyarrow-25.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:18dcc8cc50b5e72eae6fcbfc6c8776c21a007176b27a3cdec5c2f5bcf126708d", size = 49945559 }, + { url = "https://files.pythonhosted.org/packages/07/a1/bd051871708ea99a5e0fc711926c26c6f2c6d0130c7aaac8093e34998af6/pyarrow-25.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4ec1895a87aa834c3b99b7a1e758747eb8bb57f922b32c0e0fa04afb8d6998b1", size = 53114238 }, + { url = "https://files.pythonhosted.org/packages/7c/31/737f0c3cffcd6af647849477d1dd68045deac2e3963c3f9f211bedc48540/pyarrow-25.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:77c8d1ae46a44b4006e8db1cc977bbcc6ce4873c92f74137d68e45503b97fb18", size = 27861162 }, + { url = "https://files.pythonhosted.org/packages/55/c7/581ccbcdb3d897eb2893328d68db3d52eca373bf2a7e964d0a6276b8e85b/pyarrow-25.0.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:72132b9a8a0a1840197794d4dea26080069b6b0981c116bc078762dc9691b21b", size = 35878945 }, + { url = "https://files.pythonhosted.org/packages/64/d1/ccb01db7329ea0411ef4fbd9b62a04d3268b36777d4e758d5e39b91ddeab/pyarrow-25.0.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:e009ef945e498dca2f050ea10d2e9764cb44017254826fc4574fdb8d2530173b", size = 37630854 }, + { url = "https://files.pythonhosted.org/packages/af/9f/2d81ba89d1e4198d0cb25fe7529de936830fdaec0db926bb52a1ef7080d4/pyarrow-25.0.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:f57a39dbcb416345401c2e77a4373669b45fd111a1768e6cf267a7a0607ff0ec", size = 46905617 }, + { url = "https://files.pythonhosted.org/packages/6a/29/0ed312ec800fb536f93783215126cee4b8977dcfeccba6f0f44df0cc87d7/pyarrow-25.0.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:447df764beb07c544f0178a5f6b70ef44b9ecf382b3cdfad4c2d7867353c3887", size = 50119765 }, + { url = "https://files.pythonhosted.org/packages/ca/88/cab5063ba0c4d46a9f6b4b7eb1c9029dc0302d65cd5ab3510c949a386568/pyarrow-25.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ac5dfeee59f9ceb4d45ba76e83b026c38c24334135bb329d8274baa49cec3c62", size = 50027563 }, + { url = "https://files.pythonhosted.org/packages/7b/fb/4d24f1b7fe2e042dc4ef315ef75e4e702d8e46fe10c37e63caff00502b03/pyarrow-25.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f0f100dacf2c0f400601664a79d1a907ced4740514bb2b00917341038e2ce76f", size = 53162437 }, + { url = "https://files.pythonhosted.org/packages/fa/65/da20806de93ca6ee91e72cb6a9b08b3ac890b46efc8d94a7326c651c4c81/pyarrow-25.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:2e093efbecb5317372f819228fa4b4e6157eee48d3f0a7b0303705ebf81a7104", size = 28613262 }, + { url = "https://files.pythonhosted.org/packages/86/9f/c632afb1d3ef4a7814cee236718235f3a47eac46e97eb87df40f550b6b48/pyarrow-25.0.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:26be35b80780d2d21f4bae3d568b1666337c3a89722cc1794c956a77017cb24e", size = 36120702 }, + { url = "https://files.pythonhosted.org/packages/36/0a/093d53a0e72ad06e45d6443e00651bbc2d21af4211295086cbf4d873d3b9/pyarrow-25.0.0-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:6f4812bfbf11ca7d8faf59eb8fff8bf4dd25ce3a38b62baa010cc17a0926d1b2", size = 37750674 }, + { url = "https://files.pythonhosted.org/packages/8a/18/b37fc31a69cff4bdfb8842683def5612f551b93fff6f44375e4a4a6a5535/pyarrow-25.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b8af8ceedf0c9c160fd2b63440f2d205b9404db85866c1217bfea601de7cfb50", size = 46912304 }, + { url = "https://files.pythonhosted.org/packages/32/35/5cae19ba72493e5598022468b56f6a5571f399f485bf412f157356476caa/pyarrow-25.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:c70a5fd9a82bd1a702fd482bdc62d38dcb672fb2b449b1d7c0d7d1f4be7b7bfe", size = 50073652 }, + { url = "https://files.pythonhosted.org/packages/2e/a5/ddd508424bdfd5e6945765e9e2ffc687e2f6115972badc8ecf423076c407/pyarrow-25.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0490a7f8b38ffe11cc26526b50c65d111cb54ddac3717cec781806793f1244dc", size = 50058654 }, + { url = "https://files.pythonhosted.org/packages/5f/a4/324d0db203ff5eebe8694ec2d6ec5a23f9aaa5d02e5b8c692914c518c33c/pyarrow-25.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e83916bbcf380866b4e14255850b33323ff678dc9758411d0409cdd2523880b0", size = 53140153 }, + { url = "https://files.pythonhosted.org/packages/bd/8d/d236e9c82fe315f9128885c8be3ec719f41965a1eb6b6f4b42470904cd41/pyarrow-25.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:13240f0d3dc5932ccd0bfa90cd76d835680b9d94a7661c635df4b703d40ce849", size = 28743657 }, ] [[package]] @@ -2910,7 +3065,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.12.5" +version = "2.13.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -2918,106 +3073,111 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591 } +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775 } wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580 }, + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262 }, ] [[package]] name = "pydantic-core" -version = "2.41.5" +version = "2.46.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873 }, - { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826 }, - { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869 }, - { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890 }, - { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740 }, - { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021 }, - { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378 }, - { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761 }, - { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303 }, - { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355 }, - { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875 }, - { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549 }, - { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305 }, - { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902 }, - { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990 }, - { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003 }, - { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200 }, - { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578 }, - { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504 }, - { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816 }, - { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366 }, - { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698 }, - { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603 }, - { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591 }, - { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068 }, - { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908 }, - { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145 }, - { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179 }, - { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403 }, - { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206 }, - { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307 }, - { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258 }, - { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917 }, - { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186 }, - { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164 }, - { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146 }, - { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788 }, - { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133 }, - { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852 }, - { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679 }, - { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766 }, - { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005 }, - { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622 }, - { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725 }, - { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040 }, - { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691 }, - { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897 }, - { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302 }, - { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877 }, - { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680 }, - { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960 }, - { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102 }, - { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039 }, - { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126 }, - { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489 }, - { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288 }, - { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255 }, - { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760 }, - { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092 }, - { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385 }, - { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832 }, - { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585 }, - { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078 }, - { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914 }, - { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560 }, - { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244 }, - { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955 }, - { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906 }, - { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607 }, - { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769 }, - { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441 }, - { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291 }, - { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632 }, - { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905 }, - { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495 }, - { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388 }, - { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879 }, - { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017 }, - { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980 }, - { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865 }, - { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256 }, - { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762 }, - { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141 }, - { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317 }, - { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992 }, - { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302 }, +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872 }, + { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255 }, + { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827 }, + { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051 }, + { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314 }, + { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146 }, + { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685 }, + { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420 }, + { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122 }, + { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573 }, + { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139 }, + { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433 }, + { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513 }, + { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114 }, + { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298 }, + { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158 }, + { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724 }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742 }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418 }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274 }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940 }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516 }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854 }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306 }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044 }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133 }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464 }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823 }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919 }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604 }, + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306 }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906 }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802 }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446 }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757 }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275 }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467 }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417 }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782 }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782 }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334 }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986 }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693 }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819 }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411 }, + { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079 }, + { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179 }, + { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926 }, + { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785 }, + { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733 }, + { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534 }, + { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732 }, + { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627 }, + { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141 }, + { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325 }, + { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990 }, + { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978 }, + { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354 }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238 }, + { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251 }, + { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593 }, + { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226 }, + { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605 }, + { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777 }, + { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641 }, + { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404 }, + { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219 }, + { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594 }, + { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542 }, + { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146 }, + { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309 }, + { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736 }, + { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575 }, + { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624 }, + { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325 }, + { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589 }, + { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552 }, + { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984 }, + { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417 }, + { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527 }, + { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024 }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696 }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590 }, + { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782 }, + { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146 }, + { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492 }, + { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604 }, + { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828 }, + { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000 }, + { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286 }, + { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071 }, ] [[package]] @@ -3084,7 +3244,7 @@ wheels = [ [[package]] name = "pytest" -version = "9.0.3" +version = "9.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -3093,36 +3253,36 @@ dependencies = [ { name = "pluggy" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165 } +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249 }, + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536 }, ] [[package]] name = "pytest-asyncio" -version = "1.3.0" +version = "1.4.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 } +sdist = { url = "https://files.pythonhosted.org/packages/43/7c/d36d04db312ecf4298932ef77e6e4a9e8ad017906e24e34f0b0c361a2473/pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42", size = 58514 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075 }, + { url = "https://files.pythonhosted.org/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1", size = 16930 }, ] [[package]] name = "pytest-cov" -version = "7.0.0" +version = "7.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "coverage", extra = ["toml"] }, { name = "pluggy" }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328 } +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424 }, + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876 }, ] [[package]] @@ -3189,15 +3349,15 @@ wheels = [ [[package]] name = "python-discovery" -version = "1.4.2" +version = "1.4.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, { name = "platformdirs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0b/1a/cbbaf13b730abb0a16b964d984e19f2fe520c21a4dc664051359a3f5a9e7/python_discovery-1.4.2.tar.gz", hash = "sha256:8f3746c4b4968d22afbb97d36e1a0e5b66e6c0f297290f2e95f05b9b8bf18690", size = 70277 } +sdist = { url = "https://files.pythonhosted.org/packages/4c/81/58c70036dffeccb7fe7d79d6260c69f7a28272bbd3909c29a01ea9422744/python_discovery-1.4.4.tar.gz", hash = "sha256:5cad33982d412c1f3ffb8f9ca4ea292c9680bca3942451d30b69c37fce53a4a3", size = 72212 } wheels = [ - { url = "https://files.pythonhosted.org/packages/1a/82/a70006589557f267f15bd384c0642ad49f0d97b690c3a05b166b9dcbad3b/python_discovery-1.4.2-py3-none-any.whl", hash = "sha256:475803f53b7b2ed6e490e27373f9d8340f7d2eebf9acdaf645d7d714c97bb500", size = 33886 }, + { url = "https://files.pythonhosted.org/packages/9d/ae/84bc0d2440c95772272bb6f4b3d09ccf08b2898fce89b3d4f969a9fc74e9/python_discovery-1.4.4-py3-none-any.whl", hash = "sha256:abebe9120b43453b68c908acfb1e72a19d1a959ed2cb620ad38fc57d08056dbe", size = 34181 }, ] [[package]] @@ -3220,21 +3380,24 @@ wheels = [ [[package]] name = "pywin32" -version = "311" +version = "312" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031 }, - { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308 }, - { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930 }, - { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543 }, - { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040 }, - { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102 }, - { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700 }, - { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700 }, - { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318 }, - { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714 }, - { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800 }, - { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540 }, + { url = "https://files.pythonhosted.org/packages/1f/f5/10a6e845a00fc5e7afd0a988b744f403d4d57162a28d160a093c4d9322f0/pywin32-312-cp311-cp311-win32.whl", hash = "sha256:17948aeadbdb091f0ced6ef0841620794e68327b94ee415571c1203594b7215c", size = 6362659 }, + { url = "https://files.pythonhosted.org/packages/35/c4/dcd2d62b5944b6d5db53413a5899016ccd57ffcb7278f3f81655d25d2027/pywin32-312-cp311-cp311-win_amd64.whl", hash = "sha256:d11417d84412f859b722fad0841b3614459ed0047f7542d8362e77884f6b6e8a", size = 6928825 }, + { url = "https://files.pythonhosted.org/packages/b7/56/3cbb433fe4501cdba2eb9040f56a4e1a8243faa4186b25295564d1a7a79d/pywin32-312-cp311-cp311-win_arm64.whl", hash = "sha256:b2200a054ca6d6625c4842fc56a4976a4b47f96b73dbe5538c3f813a80359f47", size = 6721875 }, + { url = "https://files.pythonhosted.org/packages/83/ff/32aa7d2ed0ab12b323aaa64f9b75e6ad4f8fd09f9ccfc28c79414d46838d/pywin32-312-cp312-cp312-win32.whl", hash = "sha256:dab4f65ac9c4e48400a2a0530c46c3c579cd5905ecd11b80692373915269208b", size = 6371877 }, + { url = "https://files.pythonhosted.org/packages/03/d9/77040d3b43df3f3be32ea289433d660d2727f5ba327bc73be835127d9d60/pywin32-312-cp312-cp312-win_amd64.whl", hash = "sha256:b457f6d628a47e8a7346ce22acb7e1a46a4a78b52e1d17e1af56871bd19a93bc", size = 6914841 }, + { url = "https://files.pythonhosted.org/packages/e3/cc/7b1ec671775756020a0ee7f4feeaf3c568f0ab86bd3900088cf986937a92/pywin32-312-cp312-cp312-win_arm64.whl", hash = "sha256:6017c58e12f6809fbb0555b75df144c2922a9ffd18e4b9b5afa863b6c1a9d950", size = 6727901 }, + { url = "https://files.pythonhosted.org/packages/2d/41/12fbfd7f36ed2146d8bc9de96c2741296bf0d490b98508496cff322e274c/pywin32-312-cp313-cp313-win32.whl", hash = "sha256:7a27df850933d16a8eabfbaeb73d52b273e2da667f80d70b01a89d1f6828d02c", size = 6370184 }, + { url = "https://files.pythonhosted.org/packages/ba/db/36a78e3403099d31d9746d13fdcde5accc43c1155f375a34d15983a479a7/pywin32-312-cp313-cp313-win_amd64.whl", hash = "sha256:c53e878d15a1c44788082bfe712a905433473aa38f86375b7cf8b45e3acbaaf9", size = 6914298 }, + { url = "https://files.pythonhosted.org/packages/84/37/c1697194092b76de9ed47ca124323f02c57ffc8a45c06f88a3d5acaf01eb/pywin32-312-cp313-cp313-win_arm64.whl", hash = "sha256:59aba5d5940842075343a5ddc6b11f1cdf0d1567fe745290359dfbcc7c2eb831", size = 6727640 }, + { url = "https://files.pythonhosted.org/packages/fc/2b/1f3cded5822fd49c02f40544cbb5f58c7cfd6b1694869fd476cb6170ee97/pywin32-312-cp314-cp314-win32.whl", hash = "sha256:a77a90fbb6881238d2ca9c6fd797b25817f3768fe78d214a90137ff055a75f5b", size = 6468928 }, + { url = "https://files.pythonhosted.org/packages/21/82/3bf86d2e2808902013132e1ce905a7da0da53790f3836c64bf44d55e24f3/pywin32-312-cp314-cp314-win_amd64.whl", hash = "sha256:a4dd3a848290ef724347b19f301045831d8e802fa4464f491b98b1e0a081432e", size = 7024157 }, + { url = "https://files.pythonhosted.org/packages/a4/0e/73f6d6800b4f27655abd9e9f6aaeaefcddb2b946e4674efa2bab184a7f7b/pywin32-312-cp314-cp314-win_arm64.whl", hash = "sha256:9fce94568364e0155e6dfb781ac5d95903be8baf28670632beab1b523f300daa", size = 6839598 }, + { url = "https://files.pythonhosted.org/packages/eb/61/caa39686032d2ebdd04ff0ab5cbe163126c0066d98e00c9018646e42393b/pywin32-312-cp315-cp315-win32.whl", hash = "sha256:5c1fbe4a937a73ae9297384a3da38518cbc694c68ad8a809b2e19acd350f03ed", size = 6471159 }, + { url = "https://files.pythonhosted.org/packages/0f/cd/7e1de64a4a6f69c04214169657ccab0d93a670ea50e35eb8f489d7378249/pywin32-312-cp315-cp315-win_amd64.whl", hash = "sha256:c2f03a0f73f804a13c2735b99392b0cd426bb4f2c4d0178e5ac966a0f21618d5", size = 7025293 }, + { url = "https://files.pythonhosted.org/packages/23/ed/4532e9388e65fa16b46776ef47ad631a64eda1631884488af707666350ed/pywin32-312-cp315-cp315-win_arm64.whl", hash = "sha256:a8597d28f267b39074aef51fa593530082b39cbe5a074226096857b1fed2dfb9", size = 6840337 }, ] [[package]] @@ -3320,124 +3483,124 @@ wheels = [ [[package]] name = "regex" -version = "2026.2.28" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8b/71/41455aa99a5a5ac1eaf311f5d8efd9ce6433c03ac1e0962de163350d0d97/regex-2026.2.28.tar.gz", hash = "sha256:a729e47d418ea11d03469f321aaf67cdee8954cde3ff2cf8403ab87951ad10f2", size = 415184 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/db/8cbfd0ba3f302f2d09dd0019a9fcab74b63fee77a76c937d0e33161fb8c1/regex-2026.2.28-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e621fb7c8dc147419b28e1702f58a0177ff8308a76fa295c71f3e7827849f5d9", size = 488462 }, - { url = "https://files.pythonhosted.org/packages/5d/10/ccc22c52802223f2368731964ddd117799e1390ffc39dbb31634a83022ee/regex-2026.2.28-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0d5bef2031cbf38757a0b0bc4298bb4824b6332d28edc16b39247228fbdbad97", size = 290774 }, - { url = "https://files.pythonhosted.org/packages/62/b9/6796b3bf3101e64117201aaa3a5a030ec677ecf34b3cd6141b5d5c6c67d5/regex-2026.2.28-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bcb399ed84eabf4282587ba151f2732ad8168e66f1d3f85b1d038868fe547703", size = 288724 }, - { url = "https://files.pythonhosted.org/packages/9c/02/291c0ae3f3a10cea941d0f5366da1843d8d1fa8a25b0671e20a0e454bb38/regex-2026.2.28-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c1b34dfa72f826f535b20712afa9bb3ba580020e834f3c69866c5bddbf10098", size = 791924 }, - { url = "https://files.pythonhosted.org/packages/0f/57/f0235cc520d9672742196c5c15098f8f703f2758d48d5a7465a56333e496/regex-2026.2.28-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:851fa70df44325e1e4cdb79c5e676e91a78147b1b543db2aec8734d2add30ec2", size = 860095 }, - { url = "https://files.pythonhosted.org/packages/b3/7c/393c94cbedda79a0f5f2435ebd01644aba0b338d327eb24b4aa5b8d6c07f/regex-2026.2.28-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:516604edd17b1c2c3e579cf4e9b25a53bf8fa6e7cedddf1127804d3e0140ca64", size = 906583 }, - { url = "https://files.pythonhosted.org/packages/2c/73/a72820f47ca5abf2b5d911d0407ba5178fc52cf9780191ed3a54f5f419a2/regex-2026.2.28-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7ce83654d1ab701cb619285a18a8e5a889c1216d746ddc710c914ca5fd71022", size = 800234 }, - { url = "https://files.pythonhosted.org/packages/34/b3/6e6a4b7b31fa998c4cf159a12cbeaf356386fbd1a8be743b1e80a3da51e4/regex-2026.2.28-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2791948f7c70bb9335a9102df45e93d428f4b8128020d85920223925d73b9e1", size = 772803 }, - { url = "https://files.pythonhosted.org/packages/10/e7/5da0280c765d5a92af5e1cd324b3fe8464303189cbaa449de9a71910e273/regex-2026.2.28-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:03a83cc26aa2acda6b8b9dfe748cf9e84cbd390c424a1de34fdcef58961a297a", size = 781117 }, - { url = "https://files.pythonhosted.org/packages/76/39/0b8d7efb256ae34e1b8157acc1afd8758048a1cf0196e1aec2e71fd99f4b/regex-2026.2.28-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ec6f5674c5dc836994f50f1186dd1fafde4be0666aae201ae2fcc3d29d8adf27", size = 854224 }, - { url = "https://files.pythonhosted.org/packages/21/ff/a96d483ebe8fe6d1c67907729202313895d8de8495569ec319c6f29d0438/regex-2026.2.28-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:50c2fc924749543e0eacc93ada6aeeb3ea5f6715825624baa0dccaec771668ae", size = 761898 }, - { url = "https://files.pythonhosted.org/packages/89/bd/d4f2e75cb4a54b484e796017e37c0d09d8a0a837de43d17e238adf163f4e/regex-2026.2.28-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ba55c50f408fb5c346a3a02d2ce0ebc839784e24f7c9684fde328ff063c3cdea", size = 844832 }, - { url = "https://files.pythonhosted.org/packages/8a/a7/428a135cf5e15e4e11d1e696eb2bf968362f8ea8a5f237122e96bc2ae950/regex-2026.2.28-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:edb1b1b3a5576c56f08ac46f108c40333f222ebfd5cf63afdfa3aab0791ebe5b", size = 788347 }, - { url = "https://files.pythonhosted.org/packages/a9/59/68691428851cf9c9c3707217ab1d9b47cfeec9d153a49919e6c368b9e926/regex-2026.2.28-cp311-cp311-win32.whl", hash = "sha256:948c12ef30ecedb128903c2c2678b339746eb7c689c5c21957c4a23950c96d15", size = 266033 }, - { url = "https://files.pythonhosted.org/packages/42/8b/1483de1c57024e89296cbcceb9cccb3f625d416ddb46e570be185c9b05a9/regex-2026.2.28-cp311-cp311-win_amd64.whl", hash = "sha256:fd63453f10d29097cc3dc62d070746523973fb5aa1c66d25f8558bebd47fed61", size = 277978 }, - { url = "https://files.pythonhosted.org/packages/a4/36/abec45dc6e7252e3dbc797120496e43bb5730a7abf0d9cb69340696a2f2d/regex-2026.2.28-cp311-cp311-win_arm64.whl", hash = "sha256:00f2b8d9615aa165fdff0a13f1a92049bfad555ee91e20d246a51aa0b556c60a", size = 270340 }, - { url = "https://files.pythonhosted.org/packages/07/42/9061b03cf0fc4b5fa2c3984cbbaed54324377e440a5c5a29d29a72518d62/regex-2026.2.28-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fcf26c3c6d0da98fada8ae4ef0aa1c3405a431c0a77eb17306d38a89b02adcd7", size = 489574 }, - { url = "https://files.pythonhosted.org/packages/77/83/0c8a5623a233015595e3da499c5a1c13720ac63c107897a6037bb97af248/regex-2026.2.28-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02473c954af35dd2defeb07e44182f5705b30ea3f351a7cbffa9177beb14da5d", size = 291426 }, - { url = "https://files.pythonhosted.org/packages/9e/06/3ef1ac6910dc3295ebd71b1f9bfa737e82cfead211a18b319d45f85ddd09/regex-2026.2.28-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9b65d33a17101569f86d9c5966a8b1d7fbf8afdda5a8aa219301b0a80f58cf7d", size = 289200 }, - { url = "https://files.pythonhosted.org/packages/dd/c9/8cc8d850b35ab5650ff6756a1cb85286e2000b66c97520b29c1587455344/regex-2026.2.28-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e71dcecaa113eebcc96622c17692672c2d104b1d71ddf7adeda90da7ddeb26fc", size = 796765 }, - { url = "https://files.pythonhosted.org/packages/e9/5d/57702597627fc23278ebf36fbb497ac91c0ce7fec89ac6c81e420ca3e38c/regex-2026.2.28-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:481df4623fa4969c8b11f3433ed7d5e3dc9cec0f008356c3212b3933fb77e3d8", size = 863093 }, - { url = "https://files.pythonhosted.org/packages/02/6d/f3ecad537ca2811b4d26b54ca848cf70e04fcfc138667c146a9f3157779c/regex-2026.2.28-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:64e7c6ad614573e0640f271e811a408d79a9e1fe62a46adb602f598df42a818d", size = 909455 }, - { url = "https://files.pythonhosted.org/packages/9e/40/bb226f203caa22c1043c1ca79b36340156eca0f6a6742b46c3bb222a3a57/regex-2026.2.28-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6b08a06976ff4fb0d83077022fde3eca06c55432bb997d8c0495b9a4e9872f4", size = 802037 }, - { url = "https://files.pythonhosted.org/packages/44/7c/c6d91d8911ac6803b45ca968e8e500c46934e58c0903cbc6d760ee817a0a/regex-2026.2.28-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:864cdd1a2ef5716b0ab468af40139e62ede1b3a53386b375ec0786bb6783fc05", size = 775113 }, - { url = "https://files.pythonhosted.org/packages/dc/8d/4a9368d168d47abd4158580b8c848709667b1cd293ff0c0c277279543bd0/regex-2026.2.28-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:511f7419f7afab475fd4d639d4aedfc54205bcb0800066753ef68a59f0f330b5", size = 784194 }, - { url = "https://files.pythonhosted.org/packages/cc/bf/2c72ab5d8b7be462cb1651b5cc333da1d0068740342f350fcca3bca31947/regex-2026.2.28-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b42f7466e32bf15a961cf09f35fa6323cc72e64d3d2c990b10de1274a5da0a59", size = 856846 }, - { url = "https://files.pythonhosted.org/packages/7c/f4/6b65c979bb6d09f51bb2d2a7bc85de73c01ec73335d7ddd202dcb8cd1c8f/regex-2026.2.28-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8710d61737b0c0ce6836b1da7109f20d495e49b3809f30e27e9560be67a257bf", size = 763516 }, - { url = "https://files.pythonhosted.org/packages/8e/32/29ea5e27400ee86d2cc2b4e80aa059df04eaf78b4f0c18576ae077aeff68/regex-2026.2.28-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4390c365fd2d45278f45afd4673cb90f7285f5701607e3ad4274df08e36140ae", size = 849278 }, - { url = "https://files.pythonhosted.org/packages/1d/91/3233d03b5f865111cd517e1c95ee8b43e8b428d61fa73764a80c9bb6f537/regex-2026.2.28-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cb3b1db8ff6c7b8bf838ab05583ea15230cb2f678e569ab0e3a24d1e8320940b", size = 790068 }, - { url = "https://files.pythonhosted.org/packages/76/92/abc706c1fb03b4580a09645b206a3fc032f5a9f457bc1a8038ac555658ab/regex-2026.2.28-cp312-cp312-win32.whl", hash = "sha256:f8ed9a5d4612df9d4de15878f0bc6aa7a268afbe5af21a3fdd97fa19516e978c", size = 266416 }, - { url = "https://files.pythonhosted.org/packages/fa/06/2a6f7dff190e5fa9df9fb4acf2fdf17a1aa0f7f54596cba8de608db56b3a/regex-2026.2.28-cp312-cp312-win_amd64.whl", hash = "sha256:01d65fd24206c8e1e97e2e31b286c59009636c022eb5d003f52760b0f42155d4", size = 277297 }, - { url = "https://files.pythonhosted.org/packages/b7/f0/58a2484851fadf284458fdbd728f580d55c1abac059ae9f048c63b92f427/regex-2026.2.28-cp312-cp312-win_arm64.whl", hash = "sha256:c0b5ccbb8ffb433939d248707d4a8b31993cb76ab1a0187ca886bf50e96df952", size = 270408 }, - { url = "https://files.pythonhosted.org/packages/87/f6/dc9ef48c61b79c8201585bf37fa70cd781977da86e466cd94e8e95d2443b/regex-2026.2.28-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6d63a07e5ec8ce7184452cb00c41c37b49e67dc4f73b2955b5b8e782ea970784", size = 489311 }, - { url = "https://files.pythonhosted.org/packages/95/c8/c20390f2232d3f7956f420f4ef1852608ad57aa26c3dd78516cb9f3dc913/regex-2026.2.28-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e59bc8f30414d283ae8ee1617b13d8112e7135cb92830f0ec3688cb29152585a", size = 291285 }, - { url = "https://files.pythonhosted.org/packages/d2/a6/ba1068a631ebd71a230e7d8013fcd284b7c89c35f46f34a7da02082141b1/regex-2026.2.28-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:de0cf053139f96219ccfabb4a8dd2d217c8c82cb206c91d9f109f3f552d6b43d", size = 289051 }, - { url = "https://files.pythonhosted.org/packages/1d/1b/7cc3b7af4c244c204b7a80924bd3d85aecd9ba5bc82b485c5806ee8cda9e/regex-2026.2.28-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb4db2f17e6484904f986c5a657cec85574c76b5c5e61c7aae9ffa1bc6224f95", size = 796842 }, - { url = "https://files.pythonhosted.org/packages/24/87/26bd03efc60e0d772ac1e7b60a2e6325af98d974e2358f659c507d3c76db/regex-2026.2.28-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:52b017b35ac2214d0db5f4f90e303634dc44e4aba4bd6235a27f97ecbe5b0472", size = 863083 }, - { url = "https://files.pythonhosted.org/packages/ae/54/aeaf4afb1aa0a65e40de52a61dc2ac5b00a83c6cb081c8a1d0dda74f3010/regex-2026.2.28-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:69fc560ccbf08a09dc9b52ab69cacfae51e0ed80dc5693078bdc97db2f91ae96", size = 909412 }, - { url = "https://files.pythonhosted.org/packages/12/2f/049901def913954e640d199bbc6a7ca2902b6aeda0e5da9d17f114100ec2/regex-2026.2.28-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e61eea47230eba62a31f3e8a0e3164d0f37ef9f40529fb2c79361bc6b53d2a92", size = 802101 }, - { url = "https://files.pythonhosted.org/packages/7d/a5/512fb9ff7f5b15ea204bb1967ebb649059446decacccb201381f9fa6aad4/regex-2026.2.28-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4f5c0b182ad4269e7381b7c27fdb0408399881f7a92a4624fd5487f2971dfc11", size = 775260 }, - { url = "https://files.pythonhosted.org/packages/d1/a8/9a92935878aba19bd72706b9db5646a6f993d99b3f6ed42c02ec8beb1d61/regex-2026.2.28-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:96f6269a2882fbb0ee76967116b83679dc628e68eaea44e90884b8d53d833881", size = 784311 }, - { url = "https://files.pythonhosted.org/packages/09/d3/fc51a8a738a49a6b6499626580554c9466d3ea561f2b72cfdc72e4149773/regex-2026.2.28-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b5acd4b6a95f37c3c3828e5d053a7d4edaedb85de551db0153754924cb7c83e3", size = 856876 }, - { url = "https://files.pythonhosted.org/packages/08/b7/2e641f3d084b120ca4c52e8c762a78da0b32bf03ef546330db3e2635dc5f/regex-2026.2.28-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2234059cfe33d9813a3677ef7667999caea9eeaa83fef98eb6ce15c6cf9e0215", size = 763632 }, - { url = "https://files.pythonhosted.org/packages/fe/6d/0009021d97e79ee99f3d8641f0a8d001eed23479ade4c3125a5480bf3e2d/regex-2026.2.28-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:c15af43c72a7fb0c97cbc66fa36a43546eddc5c06a662b64a0cbf30d6ac40944", size = 849320 }, - { url = "https://files.pythonhosted.org/packages/05/7a/51cfbad5758f8edae430cb21961a9c8d04bce1dae4d2d18d4186eec7cfa1/regex-2026.2.28-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9185cc63359862a6e80fe97f696e04b0ad9a11c4ac0a4a927f979f611bfe3768", size = 790152 }, - { url = "https://files.pythonhosted.org/packages/90/3d/a83e2b6b3daa142acb8c41d51de3876186307d5cb7490087031747662500/regex-2026.2.28-cp313-cp313-win32.whl", hash = "sha256:fb66e5245db9652abd7196ace599b04d9c0e4aa7c8f0e2803938377835780081", size = 266398 }, - { url = "https://files.pythonhosted.org/packages/85/4f/16e9ebb1fe5425e11b9596c8d57bf8877dcb32391da0bfd33742e3290637/regex-2026.2.28-cp313-cp313-win_amd64.whl", hash = "sha256:71a911098be38c859ceb3f9a9ce43f4ed9f4c6720ad8684a066ea246b76ad9ff", size = 277282 }, - { url = "https://files.pythonhosted.org/packages/07/b4/92851335332810c5a89723bf7a7e35c7209f90b7d4160024501717b28cc9/regex-2026.2.28-cp313-cp313-win_arm64.whl", hash = "sha256:39bb5727650b9a0275c6a6690f9bb3fe693a7e6cc5c3155b1240aedf8926423e", size = 270382 }, - { url = "https://files.pythonhosted.org/packages/24/07/6c7e4cec1e585959e96cbc24299d97e4437a81173217af54f1804994e911/regex-2026.2.28-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:97054c55db06ab020342cc0d35d6f62a465fa7662871190175f1ad6c655c028f", size = 492541 }, - { url = "https://files.pythonhosted.org/packages/7c/13/55eb22ada7f43d4f4bb3815b6132183ebc331c81bd496e2d1f3b8d862e0d/regex-2026.2.28-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0d25a10811de831c2baa6aef3c0be91622f44dd8d31dd12e69f6398efb15e48b", size = 292984 }, - { url = "https://files.pythonhosted.org/packages/5b/11/c301f8cb29ce9644a5ef85104c59244e6e7e90994a0f458da4d39baa8e17/regex-2026.2.28-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d6cfe798d8da41bb1862ed6e0cba14003d387c3c0c4a5d45591076ae9f0ce2f8", size = 291509 }, - { url = "https://files.pythonhosted.org/packages/b5/43/aabe384ec1994b91796e903582427bc2ffaed9c4103819ed3c16d8e749f3/regex-2026.2.28-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd0ce43e71d825b7c0661f9c54d4d74bd97c56c3fd102a8985bcfea48236bacb", size = 809429 }, - { url = "https://files.pythonhosted.org/packages/04/b8/8d2d987a816720c4f3109cee7c06a4b24ad0e02d4fc74919ab619e543737/regex-2026.2.28-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00945d007fd74a9084d2ab79b695b595c6b7ba3698972fadd43e23230c6979c1", size = 869422 }, - { url = "https://files.pythonhosted.org/packages/fc/ad/2c004509e763c0c3719f97c03eca26473bffb3868d54c5f280b8cd4f9e3d/regex-2026.2.28-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bec23c11cbbf09a4df32fe50d57cbdd777bc442269b6e39a1775654f1c95dee2", size = 915175 }, - { url = "https://files.pythonhosted.org/packages/55/c2/fd429066da487ef555a9da73bf214894aec77fc8c66a261ee355a69871a8/regex-2026.2.28-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5cdcc17d935c8f9d3f4db5c2ebe2640c332e3822ad5d23c2f8e0228e6947943a", size = 812044 }, - { url = "https://files.pythonhosted.org/packages/5b/ca/feedb7055c62a3f7f659971bf45f0e0a87544b6b0cf462884761453f97c5/regex-2026.2.28-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a448af01e3d8031c89c5d902040b124a5e921a25c4e5e07a861ca591ce429341", size = 782056 }, - { url = "https://files.pythonhosted.org/packages/95/30/1aa959ed0d25c1dd7dd5047ea8ba482ceaef38ce363c401fd32a6b923e60/regex-2026.2.28-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:10d28e19bd4888e4abf43bd3925f3c134c52fdf7259219003588a42e24c2aa25", size = 798743 }, - { url = "https://files.pythonhosted.org/packages/3b/1f/dadb9cf359004784051c897dcf4d5d79895f73a1bbb7b827abaa4814ae80/regex-2026.2.28-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:99985a2c277dcb9ccb63f937451af5d65177af1efdeb8173ac55b61095a0a05c", size = 864633 }, - { url = "https://files.pythonhosted.org/packages/a7/f1/b9a25eb24e1cf79890f09e6ec971ee5b511519f1851de3453bc04f6c902b/regex-2026.2.28-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:e1e7b24cb3ae9953a560c563045d1ba56ee4749fbd05cf21ba571069bd7be81b", size = 770862 }, - { url = "https://files.pythonhosted.org/packages/02/9a/c5cb10b7aa6f182f9247a30cc9527e326601f46f4df864ac6db588d11fcd/regex-2026.2.28-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d8511a01d0e4ee1992eb3ba19e09bc1866fe03f05129c3aec3fdc4cbc77aad3f", size = 854788 }, - { url = "https://files.pythonhosted.org/packages/0a/50/414ba0731c4bd40b011fa4703b2cc86879ec060c64f2a906e65a56452589/regex-2026.2.28-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:aaffaecffcd2479ce87aa1e74076c221700b7c804e48e98e62500ee748f0f550", size = 800184 }, - { url = "https://files.pythonhosted.org/packages/69/50/0c7290987f97e7e6830b0d853f69dc4dc5852c934aae63e7fdcd76b4c383/regex-2026.2.28-cp313-cp313t-win32.whl", hash = "sha256:ef77bdde9c9eba3f7fa5b58084b29bbcc74bcf55fdbeaa67c102a35b5bd7e7cc", size = 269137 }, - { url = "https://files.pythonhosted.org/packages/68/80/ef26ff90e74ceb4051ad6efcbbb8a4be965184a57e879ebcbdef327d18fa/regex-2026.2.28-cp313-cp313t-win_amd64.whl", hash = "sha256:98adf340100cbe6fbaf8e6dc75e28f2c191b1be50ffefe292fb0e6f6eefdb0d8", size = 280682 }, - { url = "https://files.pythonhosted.org/packages/69/8b/fbad9c52e83ffe8f97e3ed1aa0516e6dff6bb633a41da9e64645bc7efdc5/regex-2026.2.28-cp313-cp313t-win_arm64.whl", hash = "sha256:2fb950ac1d88e6b6a9414381f403797b236f9fa17e1eee07683af72b1634207b", size = 271735 }, - { url = "https://files.pythonhosted.org/packages/cf/03/691015f7a7cb1ed6dacb2ea5de5682e4858e05a4c5506b2839cd533bbcd6/regex-2026.2.28-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:78454178c7df31372ea737996fb7f36b3c2c92cccc641d251e072478afb4babc", size = 489497 }, - { url = "https://files.pythonhosted.org/packages/c6/ba/8db8fd19afcbfa0e1036eaa70c05f20ca8405817d4ad7a38a6b4c2f031ac/regex-2026.2.28-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:5d10303dd18cedfd4d095543998404df656088240bcfd3cd20a8f95b861f74bd", size = 291295 }, - { url = "https://files.pythonhosted.org/packages/5a/79/9aa0caf089e8defef9b857b52fc53801f62ff868e19e5c83d4a96612eba1/regex-2026.2.28-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:19a9c9e0a8f24f39d575a6a854d516b48ffe4cbdcb9de55cb0570a032556ecff", size = 289275 }, - { url = "https://files.pythonhosted.org/packages/eb/26/ee53117066a30ef9c883bf1127eece08308ccf8ccd45c45a966e7a665385/regex-2026.2.28-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09500be324f49b470d907b3ef8af9afe857f5cca486f853853f7945ddbf75911", size = 797176 }, - { url = "https://files.pythonhosted.org/packages/05/1b/67fb0495a97259925f343ae78b5d24d4a6624356ae138b57f18bd43006e4/regex-2026.2.28-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fb1c4ff62277d87a7335f2c1ea4e0387b8f2b3ad88a64efd9943906aafad4f33", size = 863813 }, - { url = "https://files.pythonhosted.org/packages/a0/1d/93ac9bbafc53618091c685c7ed40239a90bf9f2a82c983f0baa97cb7ae07/regex-2026.2.28-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b8b3f1be1738feadc69f62daa250c933e85c6f34fa378f54a7ff43807c1b9117", size = 908678 }, - { url = "https://files.pythonhosted.org/packages/c7/7a/a8f5e0561702b25239846a16349feece59712ae20598ebb205580332a471/regex-2026.2.28-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc8ed8c3f41c27acb83f7b6a9eb727a73fc6663441890c5cb3426a5f6a91ce7d", size = 801528 }, - { url = "https://files.pythonhosted.org/packages/96/5d/ed6d4cbde80309854b1b9f42d9062fee38ade15f7eb4909f6ef2440403b5/regex-2026.2.28-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa539be029844c0ce1114762d2952ab6cfdd7c7c9bd72e0db26b94c3c36dcc5a", size = 775373 }, - { url = "https://files.pythonhosted.org/packages/6a/e9/6e53c34e8068b9deec3e87210086ecb5b9efebdefca6b0d3fa43d66dcecb/regex-2026.2.28-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7900157786428a79615a8264dac1f12c9b02957c473c8110c6b1f972dcecaddf", size = 784859 }, - { url = "https://files.pythonhosted.org/packages/48/3c/736e1c7ca7f0dcd2ae33819888fdc69058a349b7e5e84bc3e2f296bbf794/regex-2026.2.28-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0b1d2b07614d95fa2bf8a63fd1e98bd8fa2b4848dc91b1efbc8ba219fdd73952", size = 857813 }, - { url = "https://files.pythonhosted.org/packages/6e/7c/48c4659ad9da61f58e79dbe8c05223e0006696b603c16eb6b5cbfbb52c27/regex-2026.2.28-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b389c61aa28a79c2e0527ac36da579869c2e235a5b208a12c5b5318cda2501d8", size = 763705 }, - { url = "https://files.pythonhosted.org/packages/cf/a1/bc1c261789283128165f71b71b4b221dd1b79c77023752a6074c102f18d8/regex-2026.2.28-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f467cb602f03fbd1ab1908f68b53c649ce393fde056628dc8c7e634dab6bfc07", size = 848734 }, - { url = "https://files.pythonhosted.org/packages/10/d8/979407faf1397036e25a5ae778157366a911c0f382c62501009f4957cf86/regex-2026.2.28-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e8c8cb2deba42f5ec1ede46374e990f8adc5e6456a57ac1a261b19be6f28e4e6", size = 789871 }, - { url = "https://files.pythonhosted.org/packages/03/23/da716821277115fcb1f4e3de1e5dc5023a1e6533598c486abf5448612579/regex-2026.2.28-cp314-cp314-win32.whl", hash = "sha256:9036b400b20e4858d56d117108d7813ed07bb7803e3eed766675862131135ca6", size = 271825 }, - { url = "https://files.pythonhosted.org/packages/91/ff/90696f535d978d5f16a52a419be2770a8d8a0e7e0cfecdbfc31313df7fab/regex-2026.2.28-cp314-cp314-win_amd64.whl", hash = "sha256:1d367257cd86c1cbb97ea94e77b373a0bbc2224976e247f173d19e8f18b4afa7", size = 280548 }, - { url = "https://files.pythonhosted.org/packages/69/f9/5e1b5652fc0af3fcdf7677e7df3ad2a0d47d669b34ac29a63bb177bb731b/regex-2026.2.28-cp314-cp314-win_arm64.whl", hash = "sha256:5e68192bb3a1d6fb2836da24aa494e413ea65853a21505e142e5b1064a595f3d", size = 273444 }, - { url = "https://files.pythonhosted.org/packages/d3/eb/8389f9e940ac89bcf58d185e230a677b4fd07c5f9b917603ad5c0f8fa8fe/regex-2026.2.28-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:a5dac14d0872eeb35260a8e30bac07ddf22adc1e3a0635b52b02e180d17c9c7e", size = 492546 }, - { url = "https://files.pythonhosted.org/packages/7b/c7/09441d27ce2a6fa6a61ea3150ea4639c1dcda9b31b2ea07b80d6937b24dd/regex-2026.2.28-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ec0c608b7a7465ffadb344ed7c987ff2f11ee03f6a130b569aa74d8a70e8333c", size = 292986 }, - { url = "https://files.pythonhosted.org/packages/fb/69/4144b60ed7760a6bd235e4087041f487aa4aa62b45618ce018b0c14833ea/regex-2026.2.28-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c7815afb0ca45456613fdaf60ea9c993715511c8d53a83bc468305cbc0ee23c7", size = 291518 }, - { url = "https://files.pythonhosted.org/packages/2d/be/77e5426cf5948c82f98c53582009ca9e94938c71f73a8918474f2e2990bb/regex-2026.2.28-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b059e71ec363968671693a78c5053bd9cb2fe410f9b8e4657e88377ebd603a2e", size = 809464 }, - { url = "https://files.pythonhosted.org/packages/45/99/2c8c5ac90dc7d05c6e7d8e72c6a3599dc08cd577ac476898e91ca787d7f1/regex-2026.2.28-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8cf76f1a29f0e99dcfd7aef1551a9827588aae5a737fe31442021165f1920dc", size = 869553 }, - { url = "https://files.pythonhosted.org/packages/53/34/daa66a342f0271e7737003abf6c3097aa0498d58c668dbd88362ef94eb5d/regex-2026.2.28-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:180e08a435a0319e6a4821c3468da18dc7001987e1c17ae1335488dfe7518dd8", size = 915289 }, - { url = "https://files.pythonhosted.org/packages/c5/c7/e22c2aaf0a12e7e22ab19b004bb78d32ca1ecc7ef245949935463c5567de/regex-2026.2.28-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e496956106fd59ba6322a8ea17141a27c5040e5ee8f9433ae92d4e5204462a0", size = 812156 }, - { url = "https://files.pythonhosted.org/packages/7f/bb/2dc18c1efd9051cf389cd0d7a3a4d90f6804b9fff3a51b5dc3c85b935f71/regex-2026.2.28-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bba2b18d70eeb7b79950f12f633beeecd923f7c9ad6f6bae28e59b4cb3ab046b", size = 782215 }, - { url = "https://files.pythonhosted.org/packages/17/1e/9e4ec9b9013931faa32226ec4aa3c71fe664a6d8a2b91ac56442128b332f/regex-2026.2.28-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6db7bfae0f8a2793ff1f7021468ea55e2699d0790eb58ee6ab36ae43aa00bc5b", size = 798925 }, - { url = "https://files.pythonhosted.org/packages/71/57/a505927e449a9ccb41e2cc8d735e2abe3444b0213d1cf9cb364a8c1f2524/regex-2026.2.28-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d0b02e8b7e5874b48ae0f077ecca61c1a6a9f9895e9c6dfb191b55b242862033", size = 864701 }, - { url = "https://files.pythonhosted.org/packages/a6/ad/c62cb60cdd93e13eac5b3d9d6bd5d284225ed0e3329426f94d2552dd7cca/regex-2026.2.28-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:25b6eb660c5cf4b8c3407a1ed462abba26a926cc9965e164268a3267bcc06a43", size = 770899 }, - { url = "https://files.pythonhosted.org/packages/3c/5a/874f861f5c3d5ab99633e8030dee1bc113db8e0be299d1f4b07f5b5ec349/regex-2026.2.28-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:5a932ea8ad5d0430351ff9c76c8db34db0d9f53c1d78f06022a21f4e290c5c18", size = 854727 }, - { url = "https://files.pythonhosted.org/packages/6b/ca/d2c03b0efde47e13db895b975b2be6a73ed90b8ba963677927283d43bf74/regex-2026.2.28-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1c2c95e1a2b0f89d01e821ff4de1be4b5d73d1f4b0bf679fa27c1ad8d2327f1a", size = 800366 }, - { url = "https://files.pythonhosted.org/packages/14/bd/ee13b20b763b8989f7c75d592bfd5de37dc1181814a2a2747fedcf97e3ba/regex-2026.2.28-cp314-cp314t-win32.whl", hash = "sha256:bbb882061f742eb5d46f2f1bd5304055be0a66b783576de3d7eef1bed4778a6e", size = 274936 }, - { url = "https://files.pythonhosted.org/packages/cb/e7/d8020e39414c93af7f0d8688eabcecece44abfd5ce314b21dfda0eebd3d8/regex-2026.2.28-cp314-cp314t-win_amd64.whl", hash = "sha256:6591f281cb44dc13de9585b552cec6fc6cf47fb2fe7a48892295ee9bc4a612f9", size = 284779 }, - { url = "https://files.pythonhosted.org/packages/13/c0/ad225f4a405827486f1955283407cf758b6d2fb966712644c5f5aef33d1b/regex-2026.2.28-cp314-cp314t-win_arm64.whl", hash = "sha256:dee50f1be42222f89767b64b283283ef963189da0dda4a515aa54a5563c62dec", size = 275010 }, +version = "2026.7.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/37/451aaddbf50922f34d744ad5ca919ae1fcfac112123885d9728f52a484b3/regex-2026.7.10.tar.gz", hash = "sha256:1050fedf0a8a92e843971120c2f57c3a99bea86c0dfa1d63a9fac053fe54b135", size = 416282 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/16/bfd13770be1acd1c05506b93fc6be15c759d6417595d1ba334d355efbf26/regex-2026.7.10-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:66d2c35587cd601c95965d5c0415058ba5cfd6ffbab7624ce198bd967102b341", size = 494639 }, + { url = "https://files.pythonhosted.org/packages/e6/b4/0086215709f0f705661f13ba81516287538886ef0d589c545c12b0484669/regex-2026.7.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:28a0973eeffff4292f5a7ee498ab65d5e94ee8cc9cea364239251eb4a260a0f1", size = 295920 }, + { url = "https://files.pythonhosted.org/packages/8e/9e/8e07d0eea46d2cf36bf4d3794634bb0a820f016d31bc349dfef008d96b02/regex-2026.7.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8331484450b3894298bef8abecce532171ff6ac60b71f999eed10f2c01941a8a", size = 290673 }, + { url = "https://files.pythonhosted.org/packages/b7/56/d83c446de21c70ff49d2f1b2ff2196ac79a4ac6373d2cfe496011a250600/regex-2026.7.10-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0639b2488b775a0109f55a5a2172deebdedb4b6c5ab0d48c90b43cbf5de58d17", size = 792378 }, + { url = "https://files.pythonhosted.org/packages/dc/0e/d265e0cc6da47aea97e90eb896be2d2e8f92d16add13bac04fa46a0fd972/regex-2026.7.10-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:be4223af640d0aa04c05db81d5d96ada3ead9c09187d892fd37f4f97829480be", size = 861790 }, + { url = "https://files.pythonhosted.org/packages/b4/a5/62655f6208d1170a3e9188d6a45d4af0a5ae3b9da8b87d474818ac5ff016/regex-2026.7.10-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d3c75d57a00109255e60bc9c623b6ececaf7905eaab845c79f036670ed4750a2", size = 906530 }, + { url = "https://files.pythonhosted.org/packages/86/b7/d65aa2e9ffb18677cd0afbcf5990da8519a4e50778deb1bca49f043c5174/regex-2026.7.10-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:724ee9379568658ec06362cf24325c5315cc5a67f61dfe585bfeff58300a355b", size = 799912 }, + { url = "https://files.pythonhosted.org/packages/8f/19/3a5ce23ea2eb1fe36306aef49c79746ce297e4b434aeb981b525c661413a/regex-2026.7.10-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:732c19e5828eb287d01edb83b2eb87f283ba8e5fc3441c732709d3e8cbd14aaa", size = 773675 }, + { url = "https://files.pythonhosted.org/packages/fe/76/3c0eaa426700dd2ba14f2335f2b700a4e1484202254192ae440b83b8352a/regex-2026.7.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:982d07727c809b42a3968785354f11c3728414e4e90af0754345b431b2c32561", size = 781711 }, + { url = "https://files.pythonhosted.org/packages/a4/a8/a5a3fad84f9a7f897619f0f8e0a2c64946e9709044a186a8f869fb5c332f/regex-2026.7.10-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4574feca202f8c470bf678aed8b5d89df04aaf8dc677f3b83d92825051301c0f", size = 854539 }, + { url = "https://files.pythonhosted.org/packages/f8/c7/47e9b8c8ee77723b9eda74f517b6b25d2f555cf276c063a9eeea35bd86d5/regex-2026.7.10-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:80151ca5bfc6c4524186b3e08b499e97319b2001fc265ed2d4fc12c0d5692cdf", size = 763378 }, + { url = "https://files.pythonhosted.org/packages/36/09/e27e42d9d42edf71205c7e6f5b2902bc874ea03c557c80da03b8ed16c9bf/regex-2026.7.10-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:bb52e10e453b5493afe1f7702a2973bc10f4dd8901c0f2ed869ffaa3f8319296", size = 844663 }, + { url = "https://files.pythonhosted.org/packages/a1/b5/2423acb98362184ad9c8eebabafa15188d6a177daab919add8f2120fc6cd/regex-2026.7.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e37aba1994d73b4944053ab65a15f313bd5c28c885dd7f0d494a11749d89db6e", size = 789236 }, + { url = "https://files.pythonhosted.org/packages/60/ed/b387e84c8a3d6aa115dfb56865437a3fbaf28f4a6fb3b76cc6cce38ced70/regex-2026.7.10-cp311-cp311-win32.whl", hash = "sha256:6cbedeb5112f59dbd169385459b9943310bdd241c6966c19c5f6e2295055c93a", size = 266774 }, + { url = "https://files.pythonhosted.org/packages/c4/64/f30a163a65ed1f07ad12c53af00a6bd2a7251a5329fba5a08adc6f9e81a3/regex-2026.7.10-cp311-cp311-win_amd64.whl", hash = "sha256:b1963ec5ba4d52788fb0eac6aca6eb8040e8e318c7e47ebbdfc09440c802919c", size = 277959 }, + { url = "https://files.pythonhosted.org/packages/2b/de/61c8174171134cebb834ca9f8fe2ff8f49d8a3dd43453b48b537d0fbb49b/regex-2026.7.10-cp311-cp311-win_arm64.whl", hash = "sha256:3750c42d47712e362158a04d0fd80131f73a55e8c715b2885442a0ff6f9fc3fc", size = 276918 }, + { url = "https://files.pythonhosted.org/packages/b3/9c/2503d4ccf3452dc323f8baa3cf3ee10406037d52735c76cfced81423f183/regex-2026.7.10-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7252b48b0c60100095088fbeb281fca9a4fcf678a4e04b1c520c3f8613c952c4", size = 497114 }, + { url = "https://files.pythonhosted.org/packages/91/eb/04534f4263a4f658cd20a511e9d6124350044f2214eb24fee2db96acf318/regex-2026.7.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:da6ef4cb8d457aab0482b50120136ae94238aaa421863eaa7d599759742c72d6", size = 297422 }, + { url = "https://files.pythonhosted.org/packages/ca/2d/35809de392ab66ba439b58c3187ae3b8b53c883233f284b59961e5725c99/regex-2026.7.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fe7ff456c22725c9d9017f7a2a7df2b51af6df77314176760b22e2d05278e181", size = 292110 }, + { url = "https://files.pythonhosted.org/packages/ad/1e/5ce0fbe9aab071893ce2b7df020d0f561f7b411ec334124302468d587884/regex-2026.7.10-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3463a5f26be513a49e4d497debcf1b252a2db7b92c77d89621aa90b83d2dd38", size = 796800 }, + { url = "https://files.pythonhosted.org/packages/d4/67/c1ccbada395c10e334763b583e1039b1660b142303ebb941d4269130b22f/regex-2026.7.10-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:948dfc62683a6947b9b486c4598d8f6e3ecc542478b6767b87d52be68aeb55c6", size = 865509 }, + { url = "https://files.pythonhosted.org/packages/0e/06/f0b31afc16c1208f945b66290eb2a9936ab8becdfb23bbcedb91cc5f9d9b/regex-2026.7.10-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c2cbd385d82f63bb35edb60b09b08abad3619bd0a4a492ae59e55afaf98e1b9d", size = 912395 }, + { url = "https://files.pythonhosted.org/packages/0b/1c/8687de3a6c3220f4f872a9bf4bcd8dc249f2a96e7dddfa93de8bd4d16399/regex-2026.7.10-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f6222cafe00e072bb2b8f14142cd969637411fbc4dd3b1d73a90a3b817fa046f", size = 801308 }, + { url = "https://files.pythonhosted.org/packages/5f/e3/60a40ec02a2315d826414a125640aceb6f30450574c530c8f352110ece0e/regex-2026.7.10-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:65ee5d1ac3cd541325f5ac92625b1c1505f4d171520dd931bda7952895c5321a", size = 777120 }, + { url = "https://files.pythonhosted.org/packages/6a/9a/ec579b4f840ac59bc7c192b56e66abd4cbf385615300d59f7c94bf6863ae/regex-2026.7.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aa34473fbcc108fea403074f3f45091461b18b2047d136f16ffaa4c65ad46a68", size = 785164 }, + { url = "https://files.pythonhosted.org/packages/ad/1c/60d88afd5f98d4b0fb1f8b8969270628140dc01c7ff93a939f2aa83f31a6/regex-2026.7.10-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d028d189d8f38d7ff292f22187c0df37f2317f554d2ed9a2908ada330af57c0", size = 860161 }, + { url = "https://files.pythonhosted.org/packages/2a/40/08ae3ba45fe79e48c9a888a3389a7ee7e2d8c580d2d996da5ece02dfdcb9/regex-2026.7.10-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:396ea70e4ea1f19571940add3bad9fd3eb6a19dc610d0d01f692bc1ba0c10cb4", size = 765829 }, + { url = "https://files.pythonhosted.org/packages/12/e6/e613c6755d19aca9d977cdc3418a1991ffc8f386779752dd8fdfa888ea89/regex-2026.7.10-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:ebbf0d83ed5271991d666e54bb6c90ac2c55fb2ef3a88740c6af85dc85de2402", size = 852170 }, + { url = "https://files.pythonhosted.org/packages/03/33/89072f2060e6b844b4916d5bc40ef01e973640c703025707869264ec75ab/regex-2026.7.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:58a4571b2a093f6f6ee4fd281faa8ebf645abcf575f758173ea2605c7a1e1ecb", size = 789550 }, + { url = "https://files.pythonhosted.org/packages/e3/3c/4bc8be9a155035e63780ccac1da101f36194946fdc3f6fce90c7179fc6df/regex-2026.7.10-cp312-cp312-win32.whl", hash = "sha256:eac1207936555aa691ce32df1432b478f2729d54e6d93a1f4db9215bcd8eb47d", size = 267151 }, + { url = "https://files.pythonhosted.org/packages/35/73/9f5aade65bb98cc6e99c336e45a49a658300720c16721f3e687f8d754fec/regex-2026.7.10-cp312-cp312-win_amd64.whl", hash = "sha256:ecae626449d00db8c08f8f1fc00047a32d6d7eb5402b3976f5c3fda2b80a7a4f", size = 277751 }, + { url = "https://files.pythonhosted.org/packages/36/6f/d069dd12872ea1d50e17319d342f89e2072cae4b62f4245009a1108c74d8/regex-2026.7.10-cp312-cp312-win_arm64.whl", hash = "sha256:87794549a3f5c1c2bdfba2380c1bf87b931e375f4133d929da44f95e396bf5fe", size = 277063 }, + { url = "https://files.pythonhosted.org/packages/e0/88/0c977b9f3ba9b08645516eca236388c340f56f7a87054d41a187a04e134c/regex-2026.7.10-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:4db009b4fc533d79af3e841d6c8538730423f82ea8508e353a3713725de7901c", size = 496868 }, + { url = "https://files.pythonhosted.org/packages/f6/51/600882cd5d9a3cf083fd66a4064f5b7f243ba2a7de2437d42823e286edaf/regex-2026.7.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b96341cb29a3faa5db05aff29c77d141d827414f145330e5d8846892119351c1", size = 297306 }, + { url = "https://files.pythonhosted.org/packages/52/6f/48a912054ffcb756e374207bb8f4430c5c3e0ffa9627b3c7b6661844b30a/regex-2026.7.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:14d27f6bd04beb01f6a25a1153d73e58c290fd45d92ba56af1bb44199fd1010d", size = 291950 }, + { url = "https://files.pythonhosted.org/packages/1a/c8/8e1c3c86ebcee7effccbd1f7fc54fe3af22aa0e9204503e2baea4a6ff001/regex-2026.7.10-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e6b6a11bf898cca3ce7bfaa17b646901107f3975677fbd5097f36e5eb5641983", size = 796817 }, + { url = "https://files.pythonhosted.org/packages/65/39/3e49d9ff0e0737eb8180a00569b47aabb59b84611f48392eba4d998d91a0/regex-2026.7.10-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:234f8e0d65cf1df9becadae98648f74030ee85a8f12edcb5eb0f60a22a602197", size = 865513 }, + { url = "https://files.pythonhosted.org/packages/70/57/6511ad809bb3122c65bbeeffa5b750652bb03d273d29f3acb0754109b183/regex-2026.7.10-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:91b916d495db3e1b473c7c8e68733beec4dce8e487442db61764fff94f59740e", size = 912391 }, + { url = "https://files.pythonhosted.org/packages/cc/29/a1b0c109c9e878cb04b931bfe4c54332d692b93c322e127b5ae9f25b0d9e/regex-2026.7.10-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f0d4ccf70b1d13711242de0ba78967db5c35d12ac408378c70e06295c3f6644", size = 801338 }, + { url = "https://files.pythonhosted.org/packages/33/be/171c3dad4d77000e1befeff2883ca88734696dfd97b2951e5e074f32e4dd/regex-2026.7.10-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c622f4c638a725c39abcb2e680b1bd592663c83b672a4ed350a17f806d75618e", size = 777149 }, + { url = "https://files.pythonhosted.org/packages/33/61/41ab0de0e4574da1071c151f67d1eb9db3d92c43e31d64d2e6863c3d89bf/regex-2026.7.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:41a47c2b28d9421e2509a4583a22510dc31d83212fcf38e1508a7013140f71a8", size = 785216 }, + { url = "https://files.pythonhosted.org/packages/66/28/372859ea693736f07cf7023247c7eca8f221d9c6df8697ff9f93371cca08/regex-2026.7.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:13fba679fe035037e9d5286620f88bbfd105df4d5fcd975942edd282ab986775", size = 860229 }, + { url = "https://files.pythonhosted.org/packages/50/b1/e1d32cd944b599534ae655d35e8640d0ec790c0fa12e1fb29bf434d50f55/regex-2026.7.10-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8e26a075fa9945b9e44a3d02cc83d776c3b76bb1ff4b133bbfa620d5650131da", size = 765797 }, + { url = "https://files.pythonhosted.org/packages/0c/62/79a2cd9556a3329351e370929743ef4f0ccc0aaff6b3dc414ae5fa4a1302/regex-2026.7.10-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d0834c84ae8750ae1c4cede59b0afd4d2f775be958e11b18a3eea24ed9d0d9f1", size = 852130 }, + { url = "https://files.pythonhosted.org/packages/66/58/76fec29898cf5d359ab63face50f9d4f7135cc2eca3477139227b1d09952/regex-2026.7.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64722a5031aeace7f6c8d5ea9a9b22d9368af0d6e8fa532585da8158549ea963", size = 789644 }, + { url = "https://files.pythonhosted.org/packages/f6/06/3c7cec7817bda293e13c8f88aed227bbcf8b37e5990936ff6442a8fdf11a/regex-2026.7.10-cp313-cp313-win32.whl", hash = "sha256:74ae61d8573ecd51b5eeee7be2218e4c56e99c14fa8fcf97cf7519611d4be92e", size = 267130 }, + { url = "https://files.pythonhosted.org/packages/88/6c/e2a6f9a6a905f923cfc912298a5949737e9504b1ca24f29eda8d04d05ece/regex-2026.7.10-cp313-cp313-win_amd64.whl", hash = "sha256:5e792367e5f9b4ffb8cad93f1beaa91837056b94da98aa5c65a0db0c1b474927", size = 277722 }, + { url = "https://files.pythonhosted.org/packages/00/a6/9d8935aaa940c388496aa1a0c82669cc4b5d06291c2712d595e3f0cf16d3/regex-2026.7.10-cp313-cp313-win_arm64.whl", hash = "sha256:82ab8330e7e2e416c2d42fcec67f02c242393b8681014750d4b70b3f158e1f08", size = 277059 }, + { url = "https://files.pythonhosted.org/packages/7d/e9/26decfd3e85c09e42ff7b0d23a6f51085ca4c268db15f084928ca33459c6/regex-2026.7.10-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:2b93eafd92c4128bab2f93500e8912cc9ecb3d3765f6685b902c6820d0909b6b", size = 501508 }, + { url = "https://files.pythonhosted.org/packages/38/a5/5b167cebde101945690219bf34361481c9f07e858a4f46d9996b80ec1490/regex-2026.7.10-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3f03b92fb6ec739df042e45b06423fc717ecf0063e07ffe2897f7b2d5735e1e8", size = 299705 }, + { url = "https://files.pythonhosted.org/packages/f6/20/7909be4b9f449f8c282c14b6762d59aa722aeaeebe7ee4f9bb623eeaa5e0/regex-2026.7.10-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:bb5aab464a0c5e03a97abad5bdf54517061ebbf72340d576e99ff661a42575cc", size = 294605 }, + { url = "https://files.pythonhosted.org/packages/82/88/e52550185d6fda68f549b01239698697de47320fd599f5e880b1986b7673/regex-2026.7.10-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fadb07dbe36a541283ff454b1a268afd54b077d917043f2e1e5615372cb5f200", size = 811747 }, + { url = "https://files.pythonhosted.org/packages/06/98/16c255c909714de1ee04da6ae30f3ee04170f300cdc0dcf57a314ee4816a/regex-2026.7.10-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:21150500b970b12202879dfd82e7fd809d8e853140fff84d08e57a90cf1e154e", size = 871203 }, + { url = "https://files.pythonhosted.org/packages/3b/32/423ed27c9bae2092a453e853da2b6628a658d08bb5a6117db8d591183d85/regex-2026.7.10-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a68b637451d64ba30ed8ae125c973fa834cc2d37dfa7f154c2b479015d477ba8", size = 917334 }, + { url = "https://files.pythonhosted.org/packages/73/87/74dac8efb500db31cb000fda6bae2be45fc2fbf1fa9412f445fbb8acbe37/regex-2026.7.10-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e23458d8903e33e7d27196d7a311523dc4e2f4137a5f34e4dbd30c8d37ff33e", size = 816379 }, + { url = "https://files.pythonhosted.org/packages/a8/9f/1859403654e3e030b288f06d49233c6a4f889d62b84c4ef3f3a28653173d/regex-2026.7.10-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cae27622c094558e519abf3242cf4272db961d12c5c9a9ffb7a1b44b2627d5c6", size = 785563 }, + { url = "https://files.pythonhosted.org/packages/6d/d8/35d30d6bdf1ef6a5430e8982607b3a6db4df1ddedbe001e43435585d88ba/regex-2026.7.10-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ee877b6d78f9dff1da94fef51ae8cf9cce0967e043fdcc864c40b85cf293c192", size = 801415 }, + { url = "https://files.pythonhosted.org/packages/f7/22/630f31f5ea4826167b2b064d9cac2093a5b3222af380aa432cfe1a5dabcd/regex-2026.7.10-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:2c66a8a1969cfd506d1e203c0005fd0fc3fe6efc83c945606566b6f9611d4851", size = 866560 }, + { url = "https://files.pythonhosted.org/packages/8d/14/f5914a6d9c5bc63b9bed8c9a1169fb0be35dbe05cdc460e17d953031a366/regex-2026.7.10-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:2bc350e1c5fa250f30ab0c3e38e5cfdffcd82cb8af224df69955cab4e3003812", size = 772877 }, + { url = "https://files.pythonhosted.org/packages/c1/0f/7c13999eef3e4186f7c79d4950fa56f041bf4de107682fb82c80db605ff9/regex-2026.7.10-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:53f54993b462f3f91fea0f2076b46deb6619a5f45d70dbd1f543f789d8b900ef", size = 856648 }, + { url = "https://files.pythonhosted.org/packages/a4/71/a48e43909b6450fb48fa94e783bef2d9a37179258bc32ef2283955df7be7/regex-2026.7.10-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cfcec18f7da682c4e2d82112829ce906569cb8d69fa6c26f3a50dfbed5ceb682", size = 803520 }, + { url = "https://files.pythonhosted.org/packages/e0/b8/f037d1bf2c133cb24ceb6e7d81d08417080390eddab6ddfd701aa7091874/regex-2026.7.10-cp313-cp313t-win32.whl", hash = "sha256:a2d6d30be35ddd70ce0f8ee259a4c25f24d6d689a45a5ac440f03e6bcc5a21d1", size = 269168 }, + { url = "https://files.pythonhosted.org/packages/b6/9c/eaac34f8452a838956e7e89852ad049678cdc1af5d14f72d3b3b658b1ea5/regex-2026.7.10-cp313-cp313t-win_amd64.whl", hash = "sha256:c57b6ad3f7a1bdd101b2966f29dc161adf49727b1e8d3e1e89db2eda8a75c344", size = 280004 }, + { url = "https://files.pythonhosted.org/packages/cd/a9/e22e997587bc1d588b0b2cd0572027d39dd3a006216e40bbf0361688c51c/regex-2026.7.10-cp313-cp313t-win_arm64.whl", hash = "sha256:3d8ef9df02c8083c7b4b855e3cb87c8e0ebbcfea088d98c7a886aaefdf88d837", size = 279308 }, + { url = "https://files.pythonhosted.org/packages/6a/4a/a7fa3ada9bd2d2ce20d56dfceec6b2a51afeed9bf3d8286355ceec5f0628/regex-2026.7.10-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:39f81d1fdf594446495f2f4edd8e62d8eda0f7a802c77ac596dc8448ad4cc5ca", size = 497087 }, + { url = "https://files.pythonhosted.org/packages/0f/7e/ca0b1a87192e5828dbc16f16ae6caca9b67f25bf729a3348468a5ff52755/regex-2026.7.10-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:441edc66a54063f8269d1494fc8474d06605e71e8a918f4bcfd079ebda4ce042", size = 297307 }, + { url = "https://files.pythonhosted.org/packages/b2/6c/fb40bb34275d3cd4d7a376d5fb2ea1f0f4a96fd884fa83c0c4ae869001bf/regex-2026.7.10-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cfeb11990f59e59a0df26c648f0adfcbf27be77241250636f5769eb08db662be", size = 292163 }, + { url = "https://files.pythonhosted.org/packages/18/0b/34cbea16c8fea9a18475a7e8f5837c70af451e738bfeb4eb5b029b7dc07a/regex-2026.7.10-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:460176b2db044a292baaee6891106566739657877af89a251cded228689015a6", size = 797064 }, + { url = "https://files.pythonhosted.org/packages/87/77/f6805d97f15f5a710bdfd56a768f3468c978239daf9e1b15efd8935e1967/regex-2026.7.10-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9dc55698737aca028848bde418d6c51d74f2a5fd44872d3c8b56b626729adb89", size = 866155 }, + { url = "https://files.pythonhosted.org/packages/a2/e3/a2a905807bba3bcd90d6ebbb67d27af2adf7d41708175cbc6b956a0c75f1/regex-2026.7.10-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d3e10779f60c000213a5b53f518824bd07b3dc119333b26d70c6be1c27b5c794", size = 911596 }, + { url = "https://files.pythonhosted.org/packages/a2/ca/a3126888b2c6f33c7e29144fedf85f6d5a52a400024fa045ad8fc0550ef1/regex-2026.7.10-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:38a5926601aaccf379512746b86eb0ac1d29121f6c776dac6ac5b31077432f2c", size = 800713 }, + { url = "https://files.pythonhosted.org/packages/66/19/9d252fd969f726c8b56b4bacf910811cc70495a110907b3a7ccb96cd9cad/regex-2026.7.10-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a72ecf5bfd3fc8d57927f7e3ded2487e144472f39010c3acaec3f6f3ff53f361", size = 777286 }, + { url = "https://files.pythonhosted.org/packages/40/7a/5f1bf433fa446ecb3aab87bb402603dc9e171ef8052c1bb8690bb4e255a3/regex-2026.7.10-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d50714405845c1010c871098558cfe5718fe39d2a2fab5f95c8863caeb7a82b3", size = 785826 }, + { url = "https://files.pythonhosted.org/packages/99/ca/69f3a7281d86f1b592338007f3e535cc219d771448e2b61c0b56e4f9d05b/regex-2026.7.10-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ec1c44cf9bd22079aac37a07cb49a29ced9050ab5bddf24e50aba298f1e34d90", size = 860957 }, + { url = "https://files.pythonhosted.org/packages/bc/11/487ff55c8d515ec9dd60d7ba3c129eeaa9e527358ed9e8a054a9e9430f81/regex-2026.7.10-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:9e9aaef25a40d1f1e1bbb1d0eb0190c4a64a7a1750f7eb67b8399bed6f4fd2a6", size = 765959 }, + { url = "https://files.pythonhosted.org/packages/73/e1/fa034e6fa8896a09bd0d5e19c81fdc024411ab37980950a0401dccee8f6d/regex-2026.7.10-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:e54e088dc64dd2766014e7cfe5f8bc45399400fd486816e494f93e3f0f55da06", size = 851447 }, + { url = "https://files.pythonhosted.org/packages/ee/a5/b9427ed53b0e14c540dc436d56aaf57a19fb9183c6e7abd66f4b4368fbad/regex-2026.7.10-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:834271b1ff2cfa1f67fcd65a48bf11d11e9ab837e21bf79ce554efb648599ae8", size = 789418 }, + { url = "https://files.pythonhosted.org/packages/ba/52/aab92420c8aa845c7bcbe68dc65023d4a9e9ea785abf0beb2198f0de5ba1/regex-2026.7.10-cp314-cp314-win32.whl", hash = "sha256:f988a1cec68058f71a38471813fba9e87dffe855582682e8a10e40ece12567a2", size = 272538 }, + { url = "https://files.pythonhosted.org/packages/99/16/5c7050e0ef7dd8889441924ff0a2c33b7f0587c0ccb0953fe7ca997d673b/regex-2026.7.10-cp314-cp314-win_amd64.whl", hash = "sha256:2129e4a5e86f26926982d883dff815056f2e98220fdf630e59f961b578a26c43", size = 280796 }, + { url = "https://files.pythonhosted.org/packages/e8/1a/4f6099d2ba271502fdb97e697bae2ed0213c0d87f2273fe7d21e2e401d12/regex-2026.7.10-cp314-cp314-win_arm64.whl", hash = "sha256:9cd5b6805396157b4cf993a6940cbb8663161f29b4df2458c1c9991f099299c5", size = 281017 }, + { url = "https://files.pythonhosted.org/packages/19/02/4061fc71f64703e0df61e782c2894c3fbc089d277767eff6e16099581c73/regex-2026.7.10-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:103e8f3acc3dcede88c0331c8612766bdcfc47c9250c5477f0e10e0550b9da49", size = 501467 }, + { url = "https://files.pythonhosted.org/packages/73/a5/8d42b2f3fd672908a05582effd0f88438bf9bb4e8e02d69a62c723e23601/regex-2026.7.10-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:538ddb143f5ca085e372def17ef3ed9d74b50ad7fc431bd85dc50a9af1a7076f", size = 299700 }, + { url = "https://files.pythonhosted.org/packages/65/70/36fa4b46f73d268c0dbe77c40e62da2cd4833ee206d3b2e438c2034e1f36/regex-2026.7.10-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6e3448e86b05ce87d4eb50f9c680860830f3b32493660b39f43957d6263e2eba", size = 294590 }, + { url = "https://files.pythonhosted.org/packages/e0/a7/b6db1823f3a233c2a46f854fdc986f4fd424a84ed557b7751f2998efb266/regex-2026.7.10-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5eab9d3f981c423afd1a61db055cfe83553c3f6455949e334db04722469dd0a2", size = 811925 }, + { url = "https://files.pythonhosted.org/packages/b6/7d/f8bee4c210c42c7e8b952bb9fb7099dd7fb2f4bd0f33d0d65a8ab08aafc0/regex-2026.7.10-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:177f930af3ad72e1045f8877540e0c43a38f7d328cf05f31963d0bd5f7ecf067", size = 871257 }, + { url = "https://files.pythonhosted.org/packages/5d/78/22adf72e614ba0216b996e9aaef5712c23699e360ea127bb3d5ee1a7666f/regex-2026.7.10-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dd3b6d97beb39afb412f2c79522b9e099463c31f4c49ab8347c5a2ca3531c478", size = 917551 }, + { url = "https://files.pythonhosted.org/packages/03/f7/ebc15a39e81e6b58da5f913b91fc293a25c6700d353c14d5cd25fc85712a/regex-2026.7.10-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8679f0652a183d93da646fcec8da8228db0be40d1595da37e6d74c2dc8c4713c", size = 816436 }, + { url = "https://files.pythonhosted.org/packages/5c/33/20bc2bdd57f7e0fcc51be37e4c4d1bca7f0b4af8dc0a148c23220e689da8/regex-2026.7.10-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:494b19a5805438aeb582de99f9d97603d8fd48e6f4cc74d0088bb292b4da3b70", size = 785935 }, + { url = "https://files.pythonhosted.org/packages/b4/51/87ff99c849b56309c40214a72b54b0eef320d0516a8a516970cc8be1b725/regex-2026.7.10-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0911e34151a5429d0325dae538ba9851ec0b62426bdfd613060cda8f1c36ec7f", size = 801494 }, + { url = "https://files.pythonhosted.org/packages/16/11/fde67d49083fef489b7e0f841e2e5736516795b166c9867f05956c1e494b/regex-2026.7.10-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b862572b7a5f5ed47d2ba5921e63bf8d9e3b682f859d8f11e0e5ca46f7e82173", size = 866549 }, + { url = "https://files.pythonhosted.org/packages/e6/b5/31a156c36acf10181d88f55a66c688d5454a344e53ccc03d49f4a48a2297/regex-2026.7.10-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:3f361215e000d68a4aff375106637b83c80be36091d83ee5107ad3b32bd73f48", size = 773089 }, + { url = "https://files.pythonhosted.org/packages/27/bb/734e978c904726664df47ae36ce5eca5065de5141185ae46efec063476a2/regex-2026.7.10-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:4533af6099543db32ef26abc2b2f824781d4eebb309ab9296150fd1a0c7eb07d", size = 856710 }, + { url = "https://files.pythonhosted.org/packages/6d/e5/dc35cea074dbdcb9776c4b0542a3bc326ff08454af0768ef35f3fc66e7fa/regex-2026.7.10-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:668ab85105361d0200e3545bec198a1acfc6b0aeb5fff8897647a826e5a171be", size = 803621 }, + { url = "https://files.pythonhosted.org/packages/a6/b2/124564af46bc0b592785610b3985315610af0a07f4cf21fa36e06c2398dd/regex-2026.7.10-cp314-cp314t-win32.whl", hash = "sha256:dd7715817a187edd7e2a2390908757f7ba42148e59cad755fb8ee1160c628eca", size = 274558 }, + { url = "https://files.pythonhosted.org/packages/4e/9c/cd813ce9f3404c0443915175c1e339c5afd8fcda04310102eaf233015eef/regex-2026.7.10-cp314-cp314t-win_amd64.whl", hash = "sha256:78712d4954234df5ca24fdadb65a2ab034213f0cdfde376c272f9fc5e09866bb", size = 283687 }, + { url = "https://files.pythonhosted.org/packages/1b/d3/3dae6a6ce46144940e64425e32b8573a393a009aeaf75fa6752a35399056/regex-2026.7.10-cp314-cp314t-win_arm64.whl", hash = "sha256:749b92640e1970e881fdf22a411d74bf9d049b154f4ef7232eeb9a90dd8be7f3", size = 283377 }, ] [[package]] name = "reportlab" -version = "4.4.10" +version = "5.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "charset-normalizer" }, { name = "pillow" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/48/57/28bfbf0a775b618b6e4d854ef8dd3f5c8988e5d614d8898703502a35f61c/reportlab-4.4.10.tar.gz", hash = "sha256:5cbbb34ac3546039d0086deb2938cdec06b12da3cdb836e813258eb33cd28487", size = 3714962 } +sdist = { url = "https://files.pythonhosted.org/packages/41/d6/4b7b0cf56880eb96533e607967be6a939e344675601e033d113a0bfa1f4e/reportlab-5.0.0.tar.gz", hash = "sha256:e4494a0c6623ae213bb856fba523171b2b54a7bf629fda02d5e525a7b899a784", size = 3701928 } wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/2e/e1798b8b248e1517e74c6cdf10dd6edd485044e7edf46b5f11ffcc5a0add/reportlab-4.4.10-py3-none-any.whl", hash = "sha256:5abc815746ae2bc44e7ff25db96814f921349ca814c992c7eac3c26029bf7c24", size = 1955400 }, + { url = "https://files.pythonhosted.org/packages/a3/07/70085c17a369605f15e301d10ab902115019b1126c7253d964afc230c7d6/reportlab-5.0.0-py3-none-any.whl", hash = "sha256:9d5a3affa84919e1111ede580031266a570e93b1ce388219621347965ff1d93c", size = 1956710 }, ] [[package]] name = "requests" -version = "2.33.1" +version = "2.34.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -3445,144 +3608,159 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120 } +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947 }, + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075 }, ] [[package]] name = "rich" -version = "14.3.3" +version = "15.0.0" 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 } +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680 } wheels = [ - { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458 }, + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654 }, ] [[package]] name = "rich-click" -version = "1.9.7" +version = "1.9.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "rich" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/04/27/091e140ea834272188e63f8dd6faac1f5c687582b687197b3e0ec3c78ebf/rich_click-1.9.7.tar.gz", hash = "sha256:022997c1e30731995bdbc8ec2f82819340d42543237f033a003c7b1f843fc5dc", size = 74838 } +sdist = { url = "https://files.pythonhosted.org/packages/f7/ea/21e4867ea0ef881ffd4c0550fc21a061435e50d6324bcd034396633cbc18/rich_click-1.9.8.tar.gz", hash = "sha256:4008f921da88b5d91646c134ec881c1500e5a6b3f093e90e8f29400e09608371", size = 75363 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ca/e5/d708d262b600a352abe01c2ae360d8ff75b0af819b78e9af293191d928e6/rich_click-1.9.7-py3-none-any.whl", hash = "sha256:2f99120fca78f536e07b114d3b60333bc4bb2a0969053b1250869bcdc1b5351b", size = 71491 }, + { url = "https://files.pythonhosted.org/packages/6d/97/a87901aef6b7e7e4a34c6dd6cc17dca8594a592ef9d9dd765fca2b7facf7/rich_click-1.9.8-py3-none-any.whl", hash = "sha256:12873865396e6927835d4eabb1cc3996edcd65b7ac9b2391a29eca4f335a2f93", size = 72189 }, ] [[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 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157 }, - { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676 }, - { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938 }, - { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932 }, - { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830 }, - { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033 }, - { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828 }, - { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683 }, - { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583 }, - { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496 }, - { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669 }, - { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011 }, - { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406 }, - { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024 }, - { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782 }, - { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463 }, - { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139 }, - { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224 }, - { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341 }, - { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298 }, - { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604 }, - { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570 }, - { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532 }, - { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292 }, - { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128 }, - { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542 }, - { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004 }, - { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063 }, - { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099 }, - { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177 }, - { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015 }, - { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736 }, - { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981 }, - { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782 }, - { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191 }, +version = "2026.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/2a/9618a122aeb2a169a28b03889a2995fe297588964333d4a7d67bdf46e147/rpds_py-2026.6.3.tar.gz", hash = "sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4", size = 64051 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/1f/a2dca5ffdbf1d475ffc4e80e4d5d720ff3a00f691795910116960ee12511/rpds_py-2026.6.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7b689145a1485c335569bd056464f3243a29af7ed3871c7be31ad624ba239bc7", size = 342174 }, + { url = "https://files.pythonhosted.org/packages/4d/dc/323d08583c0832911768663d1944f0107fcd4088704858d84b5e06d105a0/rpds_py-2026.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:db08f45aecde626498fb3df07bcf6d2ec040af42e859a4f5040d79c200342911", size = 345513 }, + { url = "https://files.pythonhosted.org/packages/0b/2a/e31989834d18d2f26ec1d2774c5b1eb3331df4ea8ada525175294c94b48a/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acc992ab27b15f852c76755eb2ab7dce86585ddadba6fa5946e58556088845b4", size = 373783 }, + { url = "https://files.pythonhosted.org/packages/87/fe/e80107ee3639585c9941c17d6a42cd65325022f656c023191fce78c324c8/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f88d653e7b3b779d71ae7454e20dcc9b6bae903f33c269db9f2be41bda3f261", size = 378316 }, + { url = "https://files.pythonhosted.org/packages/22/6f/81e3adf81acfb6fa694de2a6e4e7d8863121e3e0799e0a7725e6cf5679c4/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e52655eaf81e32593abedaa4bfe33170c8cfedf3365ed9be6e11e07f148f0278", size = 499423 }, + { url = "https://files.pythonhosted.org/packages/2d/9a/41263969df0ce3d9af2a96d5005a288200af1989aed3354bfceb5fc0b21f/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dfcc8b909769d19db55c7cc9541eb64b9b774b1057ffffb4f1048070475bb9f9", size = 386077 }, + { url = "https://files.pythonhosted.org/packages/5e/19/7e98f468bd50346faff5b10e5297374b443bfdddacc8e9fbc65984539597/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c1255b302953c86a486b81d330d5ee1d5bd937691ce271b6be0ef0e299eaab7", size = 371315 }, + { url = "https://files.pythonhosted.org/packages/99/3c/2b973b4d371906a134b03decfea7f5d9835a2c6d263454392e15b64b5b18/rpds_py-2026.6.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:8d2294a31386bfa251d8c8a39472beee17db67d4f1a6eabea665d35c9a4461c3", size = 383502 }, + { url = "https://files.pythonhosted.org/packages/98/2a/12e2799500af0a307bca76b63361c51f9fe479223561489c29eea1f2ee41/rpds_py-2026.6.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8f23ead891a3b762f35ab3b04623da7056545b48aa60d59957e6789914545da", size = 402673 }, + { url = "https://files.pythonhosted.org/packages/2d/e3/21e5872d165fe08be4f229e3d5ee9d90019c0bf0e5538de60dbd54009450/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:421aba32367055614287a4292b6a17f1939c9452299f7a0209c117e990b646d4", size = 549964 }, + { url = "https://files.pythonhosted.org/packages/1a/d0/5ee0fe36844297de8123bee27bc12078c1a7416ad9f1b8a8ca18d6b0c0ac/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1e5822dfc2f0d4ab7e745eaa6d85945069329beeccef965af3f3bb26058fcab6", size = 615446 }, + { url = "https://files.pythonhosted.org/packages/b1/80/1ea5873cb683f2fbe5f21b23ea1f6d179ead19f3c5b249b7eb5dca568ef2/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:83e35b57523816c8613fd0776b40cd8bb9f596b37ddd2692eb4a6bb5ab2f8c93", size = 576975 }, + { url = "https://files.pythonhosted.org/packages/c9/e1/90ef639217a5ddb15b7f4f61b1c33911fd044ad03c311bafdd2bcab85582/rpds_py-2026.6.3-cp311-cp311-win32.whl", hash = "sha256:de3eceba0b683bcbb1ab93da016d0270df1f9ae7be716b40214c5dafac6ea45a", size = 204453 }, + { url = "https://files.pythonhosted.org/packages/f2/b7/b7a1695d7af36f521fb11e80d6d3adbd744f73b921859bd3c2a2c0dc706f/rpds_py-2026.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:2c54a076ca4d370980ab57bc0e31df57bbe8d41340436a90ef8b1219a3cbb127", size = 223219 }, + { url = "https://files.pythonhosted.org/packages/d7/a2/145afacf796e4506062825941176ad9445c2dcf2b3b6a1f13d3030a15e19/rpds_py-2026.6.3-cp311-cp311-win_arm64.whl", hash = "sha256:168c733a7112e071bb7a66460e667edfcff06c017a3c523f7a8a8e08d0140804", size = 219137 }, + { url = "https://files.pythonhosted.org/packages/5c/be/2e8974163072e7bab7df1a5acd54c4498e75e35d6d18b864d3a9d5dadc92/rpds_py-2026.6.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a0811d33247c3d6128a3001d763f2aa056bb3425204335400ac54f89eec3a0d0", size = 343691 }, + { url = "https://files.pythonhosted.org/packages/a4/73/319dfa745dd668efe89309141ded489126461fcecd2b8f3a3cda185129b6/rpds_py-2026.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:538949e262e46caa31ac01bdb3c1e8f642622922cacbabbae6a8445d9dc33eaf", size = 338542 }, + { url = "https://files.pythonhosted.org/packages/21/63/4239893be1c4d09b709b1a8f6be4188f0870084ff547f46606b8a75f1b03/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55927d532399c2c646100ff7feb48eaa940ad70f42cd68e1328f3ded9f81ca24", size = 368180 }, + { url = "https://files.pythonhosted.org/packages/1c/ca/9c5de382225234ceb37b1844ebdb140db12b2a278bb9efe2fcd19f6c82ce/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f56f1695bc5c0871cbc33dc0130fcf503aab0c57dcc5a6700a4f49eba4f2652e", size = 375067 }, + { url = "https://files.pythonhosted.org/packages/87/dc/863f69d1bf04ade34b7fe0d59b9fdf6f0135fe2d7cbca74f1d665589559d/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:270b293dae9058fc9fcedab50f13cebf46fb8ed1d1d54e0521a9da5d6b211975", size = 490509 }, + { url = "https://files.pythonhosted.org/packages/ce/ef/eac16a12048b45ec7c7fa94f2be3438a5f26bf9cc8580b18a1cfd609b7f6/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:127565fead0a10943b282957bd5447804ff3160ad79f2ad2635e6d249e380680", size = 382754 }, + { url = "https://files.pythonhosted.org/packages/04/8f/d2f3f532616be4d06c316ef119683e832bd3d41e112bf3a88f4151c95b17/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecabd69db66de867690f9797f2f8fa27ba501bbc24540cbdbdc649cd15888ba6", size = 366189 }, + { url = "https://files.pythonhosted.org/packages/e3/29/41a7b0e98a4b44cd676ab7598419623373eb43b20be68c084935c1a8cf88/rpds_py-2026.6.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:58eadac9cd119677b60e1cf8ac4052f35949d71b8a9e5556efccbe82533cf22a", size = 377750 }, + { url = "https://files.pythonhosted.org/packages/2e/05/ecda0bec46f9a1565090bcdc941d023f6a25aff85fda28f89f8d19878152/rpds_py-2026.6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7491ee23305ac3eb59e492b6945881f5cd77a6f731061a3f25b77fd40f9e99a4", size = 395576 }, + { url = "https://files.pythonhosted.org/packages/68/a8/6ed52f03ee6cb854ce78785cc9a9a672eb880e83fd7224d471f667d151f1/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c99f7e8ccb3dd6e3e4bfeac657a7b208c9bac8075f4b078c02d7404c34107fa", size = 543807 }, + { url = "https://files.pythonhosted.org/packages/8f/d6/156c0d3eea27ba09b92562ba2364ba124c0a061b199e17eac637cd25a5e2/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62698275682bf121181861295c9181e789030a2d516071f5b8f3c23c170cd0fc", size = 611187 }, + { url = "https://files.pythonhosted.org/packages/f1/31/774212ed989c62f7f310220089f9b0a3fb8f40f5443d1727abd5d9f52bc9/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a214c993455f99a89aaeadc9b21241900037adc9d97203e374d75513c5911822", size = 573030 }, + { url = "https://files.pythonhosted.org/packages/c9/50/22f73127a41f1ce4f87fe39aadfb9a126345801c274aa93ae88456249327/rpds_py-2026.6.3-cp312-cp312-win32.whl", hash = "sha256:501f9f04a588d6a09179368c57071301445191767c64e4b52a6aa9871f1ef5ed", size = 202185 }, + { url = "https://files.pythonhosted.org/packages/04/3a/f0ee4d4dde9d3b69dedf1b5f74e7a40017046d55052d173e418c6a94f960/rpds_py-2026.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:2c958bf94822e9290a40aaf2a822d4bc5c88099093e3948ad6c571eca9272e5f", size = 220394 }, + { url = "https://files.pythonhosted.org/packages/f3/83/3382fe37f809b59f02aac04dbc4e765b480b46ee0227ed516e3bdc4d3dfc/rpds_py-2026.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:22bffe6042b9bcb0822bcd1955ec00e245daf17b4344e4ed8e9551b976b63e96", size = 215753 }, + { url = "https://files.pythonhosted.org/packages/a4/9e/b818ee580026ec578138e961027a68820c40afeb1ec8f6819b54fb99e196/rpds_py-2026.6.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3cfe765c1da0072636ca06628261e0ea05688e160d5c8a03e0217c3854037223", size = 343012 }, + { url = "https://files.pythonhosted.org/packages/f3/6b/686d9dc4359a8f163cfbbf89ee0b4e586431de22fe8248edb63a8cf50d49/rpds_py-2026.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f4d78253f6996be4901669ad25319f842f740eccf4d58e3c7f3dd39e6dde1d8f", size = 338203 }, + { url = "https://files.pythonhosted.org/packages/9e/9b/069aa329940f8207615e091f5eedbbd40e1e15eac68a0790fd05ccdf796c/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54f45a148e28767bf343d33a684693c70e451c6f4c0e9904709a723fafbdfc1f", size = 367984 }, + { url = "https://files.pythonhosted.org/packages/14/db/34c203e4becff3703e4d3bc121842c00b8689197f398161203a880052f4e/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:842e7b070435622248c7a2c44ae53fa1440e073cc3023bc919fed570884097a7", size = 374815 }, + { url = "https://files.pythonhosted.org/packages/ee/7d/8071067d2cc453d916ad836e828c943f575e8a44612537759002a1e07381/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8020133a74bd81b4572dd8e4be028a6b1ebcd70e6726edc3918008c08bee6ee6", size = 490545 }, + { url = "https://files.pythonhosted.org/packages/a3/42/da06c5aa8f0484ff07f270787434204d9f4535e2f8c3b51ed402267e63c3/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdc7e35386f3847df728fbcb5e887e2d79c19e2fa1eba9e51b6621d23e3243af", size = 382828 }, + { url = "https://files.pythonhosted.org/packages/57/d7/fe978efc2ae50abe48eb7464668ea99f53c010c60aeebb7b35ad27f23661/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acac386b453c2516111b50985d60ce46e7fadb5ea71ae7b25f4c946935bf27cf", size = 365678 }, + { url = "https://files.pythonhosted.org/packages/69/9d/1d8922e1990b2a6eb532b6ff53d3e73d2b3bbffc84116c75826bee73dfc6/rpds_py-2026.6.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:425560c6fa0415f27261727bb20bd097568485e5eb0c121f1949417d1c516885", size = 377811 }, + { url = "https://files.pythonhosted.org/packages/b1/3d/198dceafb4fb034a6a47347e1b0735d34e0bd4a50be4e898d408ee66cb14/rpds_py-2026.6.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a550fb4950a06dde3beb4721f5ad4b25bf4513784665b0a8522c792e2bd822a4", size = 395382 }, + { url = "https://files.pythonhosted.org/packages/1f/f1/13968e49655d40b6b19d8b9140296bbc6f1d86b3f0f6c346cf9f1adddf4b/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f4bca01b63096f606e095734dd56e74e175f94cfbf24ff3d63281cec61f7bb7", size = 543832 }, + { url = "https://files.pythonhosted.org/packages/ac/ab/289bcb1b90bd3e40a2900c561fa0e2087345ecbb094f0b870f2345142b7c/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ccffae9a092a00deb7efd545fe5e2c33c33b88e7c054337e9a74c179347d0b7d", size = 611011 }, + { url = "https://files.pythonhosted.org/packages/1e/16/5043105e679436ccfbc8e5e0dd2d663ed18a8b8113515fd06a5e5d77c83e/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1cf01971c4f2c5553b772a542e4aaf191789cd331bc2cd4ff0e6e65ba49e1e97", size = 572431 }, + { url = "https://files.pythonhosted.org/packages/85/ed/adab103321c0a6565d5ae1c2998349bc3ee175b82ccc5ae8fc04cc413075/rpds_py-2026.6.3-cp313-cp313-win32.whl", hash = "sha256:8c3d1e9c15b9d51ca0391e13da1a25a0a4df3c58a37c9dc368e0736cf7f69df0", size = 201710 }, + { url = "https://files.pythonhosted.org/packages/7b/ed/a03b09668e74e5dabbf2e211f6468e1820c0552f7b0500082da31841bf7b/rpds_py-2026.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:9250a9a0a6fd4648b3f868da8d91a4c52b5811a62df58e753d50ae4454a36f80", size = 219454 }, + { url = "https://files.pythonhosted.org/packages/27/17/b8642c12930b71bc2b25831f6708ccf0f75abcd11883932ec9ce54ba3a78/rpds_py-2026.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:900a67df3fd1660b035a4761c4ce73c382ea6b35f90f9863c36c6fd8bf8b09bb", size = 215063 }, + { url = "https://files.pythonhosted.org/packages/b6/36/7fbe9dcdaf857fb3f63c2a2284b62492d95f5e8334e947e5fb6e7f68c9be/rpds_py-2026.6.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:931908d9fc855d8f74783377822be318edb6dcb19e47169dc038f9a1bf60b06e", size = 344510 }, + { url = "https://files.pythonhosted.org/packages/ba/54/f785cc3d3f60839ca57a5af4927a9f347b07b2799c373fc20f7949f87c7e/rpds_py-2026.6.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7469697dce35be237db177d42e2a2ee26e6dcc5fc052078a6fefabd288c6edd", size = 339495 }, + { url = "https://files.pythonhosted.org/packages/63/ef/d4cdaf309e6b095b43597103cf8c0b951d6cca2acce68c474f75ec12e0c7/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcfbcf66006befb9fd2aeaa9e01feaf881b4dc330a02ba07d2322b1c11be7b5d", size = 369454 }, + { url = "https://files.pythonhosted.org/packages/96/4a/9559a68b7ee15db09d7981212e8c2e219d2a1d6d4faa0391d813c3496a36/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:847927daf4cffbd4e90e42bc890069897101edd015f956cb8721b3473372edda", size = 374583 }, + { url = "https://files.pythonhosted.org/packages/ef/75/8964aa7d2c6e8ac43eba8eb6e6b0fdda1f46d39f2fc3e6aa9f2cb17f485d/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c1ef08a82bfe327cc156da694660f599923e2e6665b6d81c9c2d0ac9ffc8", size = 492919 }, + { url = "https://files.pythonhosted.org/packages/8f/97/6908094ac804115e65aedfd90f1b5fee4eebebd3f6c4cfc5419939267565/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae50181a047c871561212bb97f7932a2d45fb53e947bd9b57ebad85b529cbc53", size = 383725 }, + { url = "https://files.pythonhosted.org/packages/d1/9c/0d1fdc2e7aba23e290d603bc494e97bd205bae262ce33c6b32a69768ed5e/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc319e5a1de4b6913aac94bf6a2f9e847371e0a140a43dd4991db1a09bc2d504", size = 367255 }, + { url = "https://files.pythonhosted.org/packages/c4/fe/f0209ca4a9ed074bc8acb44dfd0e81c3122e94c9689f5645b7973a866719/rpds_py-2026.6.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e4316bf32babbed84e691e352faf967ce2f0f024174a8643c37c94a1080374fc", size = 379060 }, + { url = "https://files.pythonhosted.org/packages/c6/8d/f1cc54c616b9d8897de8738aac148d20afca93f68187475fe194d09a71b9/rpds_py-2026.6.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8c6e5a2f750cc71c3e3b11d71661f21d6f9bc6cebc6564b1466417a1ec03ec77", size = 395960 }, + { url = "https://files.pythonhosted.org/packages/fb/04/aafff00f73aeca2945f734f1d483c64ab8f472d0864ab02377fd8e89c3b2/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4470ce197d4090875cf6affbf1f853338387428df97c4fb7b7106317b8214698", size = 545356 }, + { url = "https://files.pythonhosted.org/packages/fd/cc/e229663b9e4ddac5a4acbe9085dd80a71af2a5d356b8b39d6bff233f24b0/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea964164cc9afa72d4d9b23cc28dafae93693c0a53e0b42acbff15b22c3f9ddd", size = 612319 }, + { url = "https://files.pythonhosted.org/packages/e3/7a/8a0e6d3e6cd066af108b71b43122c3fe158dd9eb86acac626593a2582eb1/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:639c8929aa0afe81be836b04de888460d6bed38b9c54cfc18da8f6bfabf5af5d", size = 573508 }, + { url = "https://files.pythonhosted.org/packages/87/03/2a69ab618a789cf6cf85c86bb844c62d090e700ab1a2aa676b3741b6c516/rpds_py-2026.6.3-cp314-cp314-win32.whl", hash = "sha256:882076c00c0a608b131187055ddc5ae29f2e7eaf870d6168980420d58528a5c8", size = 202504 }, + { url = "https://files.pythonhosted.org/packages/85/62/a3892ba945f4e24c78f352e5de3c7620d8479f73f211406a97263d13c7d2/rpds_py-2026.6.3-cp314-cp314-win_amd64.whl", hash = "sha256:0be972be84cfcaf46c8c6edf690ca0f154ac17babf1f6a955a51579b34ad2dc5", size = 220380 }, + { url = "https://files.pythonhosted.org/packages/3d/e7/c2bd44dc831931815ad11ebb5f430b5a0a4d3caa9de837107876c30c3432/rpds_py-2026.6.3-cp314-cp314-win_arm64.whl", hash = "sha256:2a9c6f195058cb45335e8cc3802745c603d716eb96bc9625950c1aac71c0c703", size = 215976 }, + { url = "https://files.pythonhosted.org/packages/79/9c/fff7b74bce9a091ec9a012a03f9ff5f69364eaf9451060dfc4486da2ffdd/rpds_py-2026.6.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:f90938e92afda60266da758ee7d363447f7f0138c9559f9e1811629580582d90", size = 346840 }, + { url = "https://files.pythonhosted.org/packages/e9/44/77bcb1168b33704908295533d27f10eb811e9e3e193e8993dc99572211d3/rpds_py-2026.6.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ec829541c45bca16e61c7ae50c20501f213605beb75d1aba91a6ee37fbbb56a4", size = 340282 }, + { url = "https://files.pythonhosted.org/packages/87/3c/7a9081c7c9e645b39efe19e4ffbeccd80add246327cd9b888aecffd72317/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd70d95892096cdb26f15a00c45907b17817577aa8d1c76b2dcc2788391f9e9", size = 370403 }, + { url = "https://files.pythonhosted.org/packages/f7/69/af47021eb7dad6ff3396cb001c08f0f3c4d06c20253f75be6421a59fe6b7/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29dfa0533a5d4c94d4dfa1b694fcb56c9c63aad8330ffdd816fd225d0a7a162f", size = 376055 }, + { url = "https://files.pythonhosted.org/packages/81/fc/a3bcf517084396a6dd258c592567a3c011ba4557f2fde23dceaf26e74f2e/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af05d726809bff6b141be124d4c7ce998f9c9c7f30edb1f46c07aa103d540b41", size = 494419 }, + { url = "https://files.pythonhosted.org/packages/c9/eb/13d529d1788135425c7bf207f8463458ca5d92e43f3f701365b83e9dffc1/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9826217f048f620d9a712672818bf231442c1b35d96b227a07eabd11b4bb6945", size = 384848 }, + { url = "https://files.pythonhosted.org/packages/8e/f4/b7ac49f30013aba8f7b9566b1dd07e81de95e708c1374b7bacc5b9bc5c9c/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:536bceea4fa4acf7e1c61da2b5786304367c816c8895be71b8f537c480b0ea1f", size = 371369 }, + { url = "https://files.pythonhosted.org/packages/31/86/6260bafa622f788b07ddec0e52d810305c8b9b0b8c27f58a2ab04bf62b4f/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:bc0011654b91cc4fb2ae701bec0a0ba1e552c0714247fa7af6c59e0ccfa3a4e1", size = 379673 }, + { url = "https://files.pythonhosted.org/packages/19/c3/03f1ee79a047b48daeca157c89a18509cde22b6b951d642b9b0af1be660a/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:539d75de9e0d536c84ff18dfeb805398e58227001ce09231a26a08b9aed1ee0e", size = 397500 }, + { url = "https://files.pythonhosted.org/packages/f0/95/8ed0cd8c377dca12aea498f119fe639fc474d1461545c39d2b5872eb1c0f/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:166cf54d9f44fc6ceb53c7860258dde44a81406646de79f8ed3234fca3b6e538", size = 545978 }, + { url = "https://files.pythonhosted.org/packages/d3/f2/0eb57f0eaa83f8fc152a7e03de968ab77e1f00732bebc892b190c6eebde7/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:d34c20167764fbcf927194d532dd7e0c56772f0a5f943fa5ef9e9afbba8fb9db", size = 613350 }, + { url = "https://files.pythonhosted.org/packages/5b/de/e0674bdbc3ef7634989b3f854c3f34bc1f587d36e5bfdc5c378d57034619/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ea7bb13b7c9a29791f87a0387ba7d3ad3a6d783d827e4d3f27b40a0ff44495e2", size = 576486 }, + { url = "https://files.pythonhosted.org/packages/f2/f6/21101359743cd136ada781e8210a85769578422ba460672eea0e29739200/rpds_py-2026.6.3-cp314-cp314t-win32.whl", hash = "sha256:6de4744d05bd1aa1be4ed7ea1189e3979196808008113bbbf899a460966b925e", size = 201068 }, + { url = "https://files.pythonhosted.org/packages/a6/b2/9574d4d44f7760c2aa32d92a0a4f41698e33f5b204a0bf5c9758f52c79d5/rpds_py-2026.6.3-cp314-cp314t-win_amd64.whl", hash = "sha256:c7b9a2f8f4d8e90af72571d3d495deebdd7e3c75451f5b41719aee166e940fc2", size = 220600 }, + { url = "https://files.pythonhosted.org/packages/08/ae/f23a2697e6ee6340a578b0f136be6483657bef0c6f9497b752bb5c0964bb/rpds_py-2026.6.3-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:e059c5dde6452b44424bd1834557556c226b57781dee1227af23518459722b13", size = 344726 }, + { url = "https://files.pythonhosted.org/packages/c3/63/e7b3a1a5358dd32c930a1062d8e15b67fd6e8922e81df9e91706d66ee5c8/rpds_py-2026.6.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2f7c26fbc5acd2522b95d4177fe4710ffd8e9b20529e703ffbf8db4d93903f05", size = 339587 }, + { url = "https://files.pythonhosted.org/packages/ec/64/10a85681916ca55fffb91b0a211f84e34297c109243484dd6394660a8a7c/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3086b538543802f84c843911242db20447de00d8752dd0efc936dbcf02218ba", size = 369585 }, + { url = "https://files.pythonhosted.org/packages/76/c2/baf95c7c38823e12ba34407c5f5767a89e5cf2233895e56f608167ae9493/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2e5c5ee828d42cb11760761c0af6507927bec42d0ad5458f97c9203b054617", size = 375479 }, + { url = "https://files.pythonhosted.org/packages/6a/94/0aad06c72d65101e11d33528d438cda99a39ce0da99466e156158f2541d3/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0c1e5d10cdc7135537988c74a0188da68e2f3c30813ba3744ab1e42e0480f9", size = 492418 }, + { url = "https://files.pythonhosted.org/packages/b5/17/de3f5a479a1f056535d7489819639d8cd591ea6281d700390b43b1abd745/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c2642a7603ec0b16ed77da4555db3b4b472341904873788327c0b0d7b95f1bb", size = 384123 }, + { url = "https://files.pythonhosted.org/packages/46/7d/bf09bd1b145bb2671c03e1e6d1ab8651858d90d8c7dfeadd85a37a934fd8/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e4320744c1ffdd95a603def63344bfab2d33edeab301c5007e7de9f9f5b3885", size = 367351 }, + { url = "https://files.pythonhosted.org/packages/a3/ea/1bb734f314b8be319149ddee80b18bd41372bdcfbdf88d28131c0cd37719/rpds_py-2026.6.3-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:a9f4645593036b81bbdb36b9c8e0ea0d1c3fee968c4d59db0344c14087ef143a", size = 378827 }, + { url = "https://files.pythonhosted.org/packages/4b/93/d9611e5b25e26df9a3649813ed66193ace9347a7c7fc4ab7cf70e94851c0/rpds_py-2026.6.3-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e55d236be29255554da47abe5c577637db7c24a02b8b46f0ca9524c855801868", size = 395966 }, + { url = "https://files.pythonhosted.org/packages/c3/cb/99d77e16e5534ae1d90629bbe419ba6ee170833a6a85e3aa1cc41726fbbc/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:24e9c5386e16669b674a69c156c8eeefcb578f3b3397b713b08e6d60f3c7b187", size = 545680 }, + { url = "https://files.pythonhosted.org/packages/59/15/11a29755f790cef7a2f755e8e14f4f0c33f39489e1893a632a2eee59672b/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c60924535c75f1566b6eb75b5c31a48a43fef04fa2d0d201acbad8a9969c6107", size = 611853 }, + { url = "https://files.pythonhosted.org/packages/68/86/0c27547e21644da938fb530f7e1a8148dd24d02db07e7a5f2567a17ce710/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:38a2fea2787428f811719ceb9114cb78964a3138838320c29ac39526c79c16ba", size = 573715 }, + { url = "https://files.pythonhosted.org/packages/29/71/4d8fcf700931815594bce892255bbd973b94efaf0fc1932b0590df18d886/rpds_py-2026.6.3-cp315-cp315-win32.whl", hash = "sha256:d483fe17f01ad64b7bf7cc38fcefff1ca9fb83f8c2b2542b68f97ffe0611b369", size = 202864 }, + { url = "https://files.pythonhosted.org/packages/eb/62/b577562de0edbb55b2be85ce5fd09c33e386b9b13eee09833af4240fd5c4/rpds_py-2026.6.3-cp315-cp315-win_amd64.whl", hash = "sha256:67e3a721ffc5d8d2210d3671872298c4a84e4b8035cfe42ffd7cde35d772b146", size = 220430 }, + { url = "https://files.pythonhosted.org/packages/c8/95/d6d0b2509825141eef60669a5739eec88dbc6a48053d6c92993a5704defe/rpds_py-2026.6.3-cp315-cp315-win_arm64.whl", hash = "sha256:6e84adbcf4bf841aed8116a8264b9f50b4cb3e7bd89b516122e616ac56ca269e", size = 215877 }, + { url = "https://files.pythonhosted.org/packages/b7/bf/f3ea278f0afd615c1d0f19cb69043a41526e2bb600c2b536eb192218eb27/rpds_py-2026.6.3-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:ae6dd8f10bd17aad820876d24caec9efdafd80a318d16c0a48edb5e136902c6b", size = 346933 }, + { url = "https://files.pythonhosted.org/packages/9d/29/9907bdf1c5346763cf10b7f6852aad86652168c259def904cbe0082c5864/rpds_py-2026.6.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:bdbd97738551fca3917c1bd7188bec1920bb520104f28e7e1007f9ceb17b7690", size = 340274 }, + { url = "https://files.pythonhosted.org/packages/6f/2c/8e03767b5778ef25cebf74a7a91a2c3806f8eced4c92cb7406bbe060756d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b95977e7211527ab0ba576e286d023389fbeeb32a6b7b771665d333c60e5342", size = 370763 }, + { url = "https://files.pythonhosted.org/packages/2e/e1/df2a7e1ba2efd796af26194250b8d42c821b46592311595162af9ef0528d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15fde0e6fb0d88a60d221204873743e5d9f0b7d29165e62cd86d0413ad74ba6", size = 376467 }, + { url = "https://files.pythonhosted.org/packages/6b/de/8a0814d1946af29cb068fb259aa8622f856df1d0bab58429448726b537f5/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a136d453475ac0fcbda502ef1e6504bd28d6d904700915d278deeab0d00fe140", size = 496689 }, + { url = "https://files.pythonhosted.org/packages/df/f3/f19e0c852ba13694f5a79f3b719331051573cb5693feacf8a88ffffc3a71/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f826877d462181e5eb1c26a0026b8d0cab05d99844ecb6d8bf3627a2ca0c0442", size = 385340 }, + { url = "https://files.pythonhosted.org/packages/e2/ae/7ec3a9d2d4351f99e37bcb06b6b6f954512646bfdbf9742e1de727865daf/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79486287de1730dbaff3dbd124d0ca4d2ef7f9d29bf2544f1f93c09b5bcbbd12", size = 372179 }, + { url = "https://files.pythonhosted.org/packages/d3/ac/9cee911dff2aaa9a5a8354f6610bf2e6a616de9197c5fff4f54f82585f1e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:808345f53cb952433ca2816f1604ff3515608a81784954f38d4452acfe8e61d5", size = 379993 }, + { url = "https://files.pythonhosted.org/packages/83/6b/7c2a07ba88d1e9a936612f7a5d067467ed03d971d5a06f7d309dff044a7e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1967debc37f64f2c4dc90a7f563aec558b471966e12adcac4e1c4240496b6ebf", size = 398909 }, + { url = "https://files.pythonhosted.org/packages/97/0b/776ffcb66783637b0031f6d58d6fb55913c8b5abf00aeecd46bf933fb477/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:f0840b5b17057f7fd918b76183a4b5a0635f43e14eb2ce60dce1d4ee4707ea00", size = 546584 }, + { url = "https://files.pythonhosted.org/packages/55/33/ba3bc04d7092bd553c9b2b195624992d2cc4f3de1f380b7b93cbee67bd79/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:faa679d19a6696fd54259ad321251ad77a13e70e03dd834daa762a44fb6196ef", size = 614357 }, + { url = "https://files.pythonhosted.org/packages/8b/71/14edf065f04630b1a8472f7653cad03f6c478bcf95ea0e6aed55451e33ea/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:23a439f31ccbeff1574e24889128821d1f7917470e830cf6544dced1c662262a", size = 576533 }, + { url = "https://files.pythonhosted.org/packages/ba/76/65002b08596c389105720a8c0d22298b8dc25a4baf89b2ce431343c8b1de/rpds_py-2026.6.3-cp315-cp315t-win32.whl", hash = "sha256:913ca42ccad3f8cc6e292b587ae8ae49c8c823e5dce51a736252fc7c7cdfa577", size = 201204 }, + { url = "https://files.pythonhosted.org/packages/8c/97/d855d6b3c322d1f27e26f5241c42016b56cf01377ea8ed348285f54652f0/rpds_py-2026.6.3-cp315-cp315t-win_amd64.whl", hash = "sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324", size = 220719 }, + { url = "https://files.pythonhosted.org/packages/b4/9c/f0d19ac587fd0e4ab6b72cda355e9c5a6166b01ef7e064e437aef8eb9fef/rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4cf2d36a2357e4d07bb5a4f98801265327b48256867816cfd2ceb001e9754a8f", size = 349791 }, + { url = "https://files.pythonhosted.org/packages/38/c7/1d49d204c9fd2ee6c537601dc4c1ba921e03363ca576bfab94a00254ac9a/rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:30c6dc199b24a5e3e81d50da0f00858c5bbdb2617a750395687f4339c5818171", size = 352842 }, + { url = "https://files.pythonhosted.org/packages/ac/e5/c0b5dc93cd0d4c06ce1f438907649514e2ea077bcd911e3154a51e96c38e/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9891e594296ab9dada6551c8e7b387b2721f27a67eecd528412e8906247a7b90", size = 382094 }, + { url = "https://files.pythonhosted.org/packages/0d/54/ec0e907b4ca8d541112db352409bd15f871c9b243e0c92c9b5a46ae96f01/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5c2dc92304aa48a4a60443b548bb12f12e119d4b72f314015e67b9e1be97fca", size = 388662 }, + { url = "https://files.pythonhosted.org/packages/d3/f4/921c22a4fd0f1c1ac13a3996ffbf0aa67951e2c8ad0d1d9574938a2932e8/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:127e08c0642d880cf32ca47ec2a4a77b901f7e2dd1ad9762adb13955d72ffcc9", size = 504896 }, + { url = "https://files.pythonhosted.org/packages/0b/1b/a114b972cefa1ab1cdb3c7bb177cd3844a12826c507c722d3a73516dbbaf/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8bb68f03f395eb793220b45c097bd4d8c32944393da0fad8b999efac0868fc8c", size = 391545 }, + { url = "https://files.pythonhosted.org/packages/4e/98/af9b3db77d47fcbe6c8c1f36e2c2147ec70292819e99c325f871584a1c11/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3450b693fde92133e9f51060568a4c31fcca76d5e53bbd611e689ca446517e9", size = 380059 }, + { url = "https://files.pythonhosted.org/packages/c9/ba/0efd8668b97c1d26a61566386c636a7a7a09829e474fdf807caa15a2c844/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:5e8d07bddee435a2ff6f1920e18feff28d0bc4533e42f4bf6927fbd073312c41", size = 393235 }, + { url = "https://files.pythonhosted.org/packages/62/90/8c139ee9690f73b0829f32647de6f40d826f8f443af6fa72644f96351aac/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3a83ae6c67b7676b9878378547ca8e93ed77a580037bcbcd1d32f739e1e6089c", size = 413008 }, + { url = "https://files.pythonhosted.org/packages/9c/97/0043896fdd7828ce09a1d9a8b06433714d0960fc4ff3fc4aa72b666b764e/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2bfd04c19ddbd6640de0b51894d764bd2758854d5b75bd102d2ef10cb9c293a9", size = 558118 }, + { url = "https://files.pythonhosted.org/packages/f6/40/02355f0e134f783a8f9814c4680a1bd311d37671577a5964ea838573ff37/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:ca6546b66be9dc4738b1b043d5ebd5488c66c578c5ff0fd0e8065313fe3afb76", size = 623138 }, + { url = "https://files.pythonhosted.org/packages/10/85/48f0abdcef5cce4e034c7a5b0ceeceba0b01bf0d942824f4bb720afe2dec/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:8e65860d238379ed982fd9ba690579b5e95af2f4840f99c772816dbe573cb826", size = 586486 }, ] [[package]] @@ -3603,35 +3781,39 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/51/df/f8629c19c5318601d3121e230f74cbee7a3732339c52b21daa2b82ef9c7d/ruff-0.15.6.tar.gz", hash = "sha256:8394c7bb153a4e3811a4ecdacd4a8e6a4fa8097028119160dffecdcdf9b56ae4", size = 4597916 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/2f/4e03a7e5ce99b517e98d3b4951f411de2b0fa8348d39cf446671adcce9a2/ruff-0.15.6-py3-none-linux_armv6l.whl", hash = "sha256:7c98c3b16407b2cf3d0f2b80c80187384bc92c6774d85fefa913ecd941256fff", size = 10508953 }, - { url = "https://files.pythonhosted.org/packages/70/60/55bcdc3e9f80bcf39edf0cd272da6fa511a3d94d5a0dd9e0adf76ceebdb4/ruff-0.15.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ee7dcfaad8b282a284df4aa6ddc2741b3f4a18b0555d626805555a820ea181c3", size = 10942257 }, - { url = "https://files.pythonhosted.org/packages/e7/f9/005c29bd1726c0f492bfa215e95154cf480574140cb5f867c797c18c790b/ruff-0.15.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3bd9967851a25f038fc8b9ae88a7fbd1b609f30349231dffaa37b6804923c4bb", size = 10322683 }, - { url = "https://files.pythonhosted.org/packages/5f/74/2f861f5fd7cbb2146bddb5501450300ce41562da36d21868c69b7a828169/ruff-0.15.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13f4594b04e42cd24a41da653886b04d2ff87adbf57497ed4f728b0e8a4866f8", size = 10660986 }, - { url = "https://files.pythonhosted.org/packages/c1/a1/309f2364a424eccb763cdafc49df843c282609f47fe53aa83f38272389e0/ruff-0.15.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e2ed8aea2f3fe57886d3f00ea5b8aae5bf68d5e195f487f037a955ff9fbaac9e", size = 10332177 }, - { url = "https://files.pythonhosted.org/packages/30/41/7ebf1d32658b4bab20f8ac80972fb19cd4e2c6b78552be263a680edc55ac/ruff-0.15.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70789d3e7830b848b548aae96766431c0dc01a6c78c13381f423bf7076c66d15", size = 11170783 }, - { url = "https://files.pythonhosted.org/packages/76/be/6d488f6adca047df82cd62c304638bcb00821c36bd4881cfca221561fdfc/ruff-0.15.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:542aaf1de3154cea088ced5a819ce872611256ffe2498e750bbae5247a8114e9", size = 12044201 }, - { url = "https://files.pythonhosted.org/packages/71/68/e6f125df4af7e6d0b498f8d373274794bc5156b324e8ab4bf5c1b4fc0ec7/ruff-0.15.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c22e6f02c16cfac3888aa636e9eba857254d15bbacc9906c9689fdecb1953ab", size = 11421561 }, - { url = "https://files.pythonhosted.org/packages/f1/9f/f85ef5fd01a52e0b472b26dc1b4bd228b8f6f0435975442ffa4741278703/ruff-0.15.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98893c4c0aadc8e448cfa315bd0cc343a5323d740fe5f28ef8a3f9e21b381f7e", size = 11310928 }, - { url = "https://files.pythonhosted.org/packages/8c/26/b75f8c421f5654304b89471ed384ae8c7f42b4dff58fa6ce1626d7f2b59a/ruff-0.15.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:70d263770d234912374493e8cc1e7385c5d49376e41dfa51c5c3453169dc581c", size = 11235186 }, - { url = "https://files.pythonhosted.org/packages/fc/d4/d5a6d065962ff7a68a86c9b4f5500f7d101a0792078de636526c0edd40da/ruff-0.15.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:55a1ad63c5a6e54b1f21b7514dfadc0c7fb40093fa22e95143cf3f64ebdcd512", size = 10635231 }, - { url = "https://files.pythonhosted.org/packages/d6/56/7c3acf3d50910375349016cf33de24be021532042afbed87942858992491/ruff-0.15.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8dc473ba093c5ec238bb1e7429ee676dca24643c471e11fbaa8a857925b061c0", size = 10340357 }, - { url = "https://files.pythonhosted.org/packages/06/54/6faa39e9c1033ff6a3b6e76b5df536931cd30caf64988e112bbf91ef5ce5/ruff-0.15.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:85b042377c2a5561131767974617006f99f7e13c63c111b998f29fc1e58a4cfb", size = 10860583 }, - { url = "https://files.pythonhosted.org/packages/cb/1e/509a201b843b4dfb0b32acdedf68d951d3377988cae43949ba4c4133a96a/ruff-0.15.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cef49e30bc5a86a6a92098a7fbf6e467a234d90b63305d6f3ec01225a9d092e0", size = 11410976 }, - { url = "https://files.pythonhosted.org/packages/6c/25/3fc9114abf979a41673ce877c08016f8e660ad6cf508c3957f537d2e9fa9/ruff-0.15.6-py3-none-win32.whl", hash = "sha256:bbf67d39832404812a2d23020dda68fee7f18ce15654e96fb1d3ad21a5fe436c", size = 10616872 }, - { url = "https://files.pythonhosted.org/packages/89/7a/09ece68445ceac348df06e08bf75db72d0e8427765b96c9c0ffabc1be1d9/ruff-0.15.6-py3-none-win_amd64.whl", hash = "sha256:aee25bc84c2f1007ecb5037dff75cef00414fdf17c23f07dc13e577883dca406", size = 11787271 }, - { url = "https://files.pythonhosted.org/packages/7f/d0/578c47dd68152ddddddf31cd7fc67dc30b7cdf639a86275fda821b0d9d98/ruff-0.15.6-py3-none-win_arm64.whl", hash = "sha256:c34de3dd0b0ba203be50ae70f5910b17188556630e2178fd7d79fc030eb0d837", size = 11060497 }, +version = "0.15.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/06/ae069393fc66e8ff33036d4b368003833bf6e88ccf182e17e7a2f1c754fd/ruff-0.15.22.tar.gz", hash = "sha256:3f15175b1fb580126f58285a5dae6b2ea89000136d980c64499211f116b54809", size = 4785063 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/18/ee54b7ae1e121be7a28ea6da4b67564ebb0530e183a54415ab7e3bcd2c4e/ruff-0.15.22-py3-none-linux_armv6l.whl", hash = "sha256:44423e73493737f5e7c5b41d475483898ff37afcdae38bc3da5085e29af1c2d8", size = 10781258 }, + { url = "https://files.pythonhosted.org/packages/2f/d2/2520cb14761ddbeaf57642a76942fc36adcbdbe53b4532241995f6fc485c/ruff-0.15.22-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b82c6482946e9eda7ff2e091d25b8bad3f718684e1916d41bd56873cee05b697", size = 10999477 }, + { url = "https://files.pythonhosted.org/packages/c9/10/74e53572aa758dfaa678c2a2646b5c5515d884b7ca56be4d2ce03ca4b560/ruff-0.15.22-py3-none-macosx_11_0_arm64.whl", hash = "sha256:11c1c715af53a09f714e011106bffc419751ec8232fcb5da42173284ea3fec6f", size = 10466716 }, + { url = "https://files.pythonhosted.org/packages/1e/cc/44eaaf0844e028182f2d0a8f2190d0f359159aed0a9e5ab861d892f1ae2a/ruff-0.15.22-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:742a29cf29bddb7c8327895d6a10e0e6c5b38a96dd407af9b5d0857f809c0576", size = 10892644 }, + { url = "https://files.pythonhosted.org/packages/9f/21/8edf559014d2b0f82beea19cfb713993ad802ccda16868769979c6090a84/ruff-0.15.22-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72af58b951b0ae395935ae79763dc349bc0eb706319d28f7a33ad2cfb3cfc178", size = 10576719 }, + { url = "https://files.pythonhosted.org/packages/bf/1e/3a13abd392a3b50b62e5938a831f9ab6e588358cacad5c18545b716d2182/ruff-0.15.22-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62d425005c1835eb24e2ee4161cb90e8db263415f4a71c8c72c33abaa6c0c224", size = 11376494 }, + { url = "https://files.pythonhosted.org/packages/bf/3e/422d3d95bcf04dd78e1aeac22184d4f9a8fb2c01865d39d44618484a0317/ruff-0.15.22-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8b9b3f8779a4f08c969defc3c8c35abffaa757e601ed5ae66d6d1db6519969a", size = 12208370 }, + { url = "https://files.pythonhosted.org/packages/1e/91/5d065a0e0a02bf4813f5119ad278462eed081d2b832eb7c021ade0ec9e65/ruff-0.15.22-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1e0dd1b2e4d3d585f897a0d137cbf4eaf6223bef4e8ce34d6bb12556c5f9249e", size = 11581098 }, + { url = "https://files.pythonhosted.org/packages/f6/f9/a0d4871d12fae702eb1f41b686caf05f1f8b124dc6db6f784f53d74918fa/ruff-0.15.22-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:365523eb91d9224e1bcb03b022fbf0facb8f9e23792a2c53d9d4b3924bdbdebb", size = 11399422 }, + { url = "https://files.pythonhosted.org/packages/18/80/c843a5176cddbceb0b7e8dd41cf9993490796c1c469348d384f5a5c13c56/ruff-0.15.22-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:fabfd168afdf29fee5be98b831efa9683c94d7c5a3b58b9ce5a2e38444589a74", size = 11381683 }, + { url = "https://files.pythonhosted.org/packages/d4/00/8485de0ae92239438a36cfc51350db9b9e85c9ebdfaea91b18e422706662/ruff-0.15.22-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:225dbf095a87f1d9f90f5fd7924d2613ee452a75a4308c63a8f50f761787aa7c", size = 10850295 }, + { url = "https://files.pythonhosted.org/packages/fa/91/24977ec2ec72eaf15e4394ace2959fdff2dd1e14f03e005e838023407169/ruff-0.15.22-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1877d63b9d24ed278744f1523fd11b85540566d54641f97c566d7d9dc5ca5296", size = 10579640 }, + { url = "https://files.pythonhosted.org/packages/9c/47/9b51216951974df1f263ac19da550d34252e0ed7218c25f10c5ef9ed7517/ruff-0.15.22-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a1606c510bd7215680d32efab38965f7cdec3ef69f5170a3f4791404ffdd5262", size = 11105077 }, + { url = "https://files.pythonhosted.org/packages/c2/47/20e9d4a3b8016778acea5fc32bb50d35d207500a17ddb529ffa6996feef8/ruff-0.15.22-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:630479b18625f5ffc373f77603a22a9f8ac0acd7ff0501178b5db28ec71e9c64", size = 11490980 }, + { url = "https://files.pythonhosted.org/packages/4d/76/3f72d8fc38c1cb77b38c56a70da9d0c17700cc1cc50f9649c9d3c8f5ba71/ruff-0.15.22-py3-none-win32.whl", hash = "sha256:e5ba0e4a13fd14abbed2a77b517a3911290c6c6c59ef67784328d1668fab76cf", size = 10789165 }, + { url = "https://files.pythonhosted.org/packages/cb/46/4965251734c2b6fcdca1b1b187d20bcac3af0ee5b083b89c910bb961ce3a/ruff-0.15.22-py3-none-win_amd64.whl", hash = "sha256:9be63ba1eb936acd2d1342fb8337c356353706fce233b2a15a09a97037e6acde", size = 11938297 }, + { url = "https://files.pythonhosted.org/packages/57/c9/e69b1ff4c8b69093ef08b8919ab767af0569666865b39c30a8795d88d3c6/ruff-0.15.22-py3-none-win_arm64.whl", hash = "sha256:e1168075b72158510839f250027659cdd78476f40507dd517892304c41318661", size = 11298172 }, ] [[package]] name = "scipy" version = "1.17.1" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.12' and sys_platform == 'win32'", + "python_full_version < '3.12' and sys_platform != 'win32'", +] dependencies = [ - { name = "numpy" }, + { name = "numpy", marker = "python_full_version < '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822 } wheels = [ @@ -3697,6 +3879,65 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165 }, ] +[[package]] +name = "scipy" +version = "1.18.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'win32'", +] +dependencies = [ + { name = "numpy", marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/25/c2700dfaf6442b4effaa91af24ebce5dc9d31bb4a69706313aae70d72cd0/scipy-1.18.0.tar.gz", hash = "sha256:67b2ad2ad54c72ca6d04975a9b2df8c3638c34ddd5b28738e94fc2b57929d378", size = 30774447 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/19/ca10ead60b0acc80b2b833c2c4a4f2ff753d0f58b811f70d911c7e94a25c/scipy-1.18.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:7bd21faaf5a1a3b2eff922d02db5f191b99a6518db9078a8fb23169f6d22259a", size = 31056519 }, + { url = "https://files.pythonhosted.org/packages/96/72/1e6442a00cd2924d361aa1b642ab6373ec35c6fabf311a760be9f76e0f13/scipy-1.18.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:265915e79107de9f946b855e50d7470d5893ec3f54b342e1aa6201cbdcd8bb6b", size = 28681889 }, + { url = "https://files.pythonhosted.org/packages/9b/2d/11dd93d21e147a73ba22bd75c0b9208d3a2e0ec76d53170ce7d9029b1015/scipy-1.18.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9ab7b758be6940954a713ee466e2043e9f6e2ed965c1fce5c91039f4be3d90a9", size = 20423580 }, + { url = "https://files.pythonhosted.org/packages/9c/01/93552f75e0d2a7dd115a45e59209c51e8d514daff02fc887d2623be06fe1/scipy-1.18.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:97b6cddaaee0a779ef6b5ca83c9604b27cc16b2b8fc22c142652df8793319fb8", size = 23054441 }, + { url = "https://files.pythonhosted.org/packages/3c/23/21f5e703643d66f21faa6b4c73195bfcad70c55efcb4f1ab327cd7c4101a/scipy-1.18.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:52a96e21517c7292375c0e27dd796a811f03fcea5fd4d108fdfea8145dcf17ab", size = 33968720 }, + { url = "https://files.pythonhosted.org/packages/dd/aa/1b939f6c67ed68635bb538e6752d3dacc02f66535182e939a89581a44e9c/scipy-1.18.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f55797419e16e7f30cf88ffb3113ce0467f00cfe3f70d5c281730b21769bfc2", size = 35287115 }, + { url = "https://files.pythonhosted.org/packages/b6/ff/eec46be7e9234208f801062b53e1983085eddebd693f6c9bfb03b459830d/scipy-1.18.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ad033410e2e0672ffdc1042110cef20e1c46f8fd0616cee1d44d8d58fad8fc11", size = 35577989 }, + { url = "https://files.pythonhosted.org/packages/84/ca/210d4759c7210bb7d269437421959b39a33434e2776b60c5cb8a763bb30a/scipy-1.18.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4a55985d54c769c872e64b7f4c8a81cc30ef700cc04296abbbf3705439c126de", size = 37421717 }, + { url = "https://files.pythonhosted.org/packages/2b/54/9a9edb45345bd6744da5ddfb6628e5d5185920494c6a67ec45b6381004cb/scipy-1.18.0-cp312-cp312-win_amd64.whl", hash = "sha256:71ccc8faa2dd16ac310233203474a8b5cb67f10dedd54a3116d34943f4b19132", size = 36597428 }, + { url = "https://files.pythonhosted.org/packages/99/0e/33f32a2a58987e26aec0f7df252cbbad1e90ae77bdbc76f40dd4ed0cf0ea/scipy-1.18.0-cp312-cp312-win_arm64.whl", hash = "sha256:d88363fd9d8fbd3511bd273f1a49efb2a540773ddf92a91d57498ce7dd7f3e76", size = 24351481 }, + { url = "https://files.pythonhosted.org/packages/05/52/9c0136c2de7ae0779b7b366447766cec6d9f0702c56bb8ffeb04c8fd3af4/scipy-1.18.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:09143f676d157d9f546d663504ef9c1becb819824f1afc018814176411942446", size = 31036107 }, + { url = "https://files.pythonhosted.org/packages/02/73/0291a64843270f4efb86cdcf2ee0f2048631b65ec6b405398b2b4dbf11bf/scipy-1.18.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:5efe260f69417b97ddae455bfb5a95e8359f7f66ad7fa9522a60feb66f169520", size = 28663303 }, + { url = "https://files.pythonhosted.org/packages/d3/0f/10ffa0b697a572f4e0d48b92a88895d366422f019f723e7e14a84c050dac/scipy-1.18.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:68363b7eaacd8b5dd426df56d782cc156468ac79a127a1b87ca597d6e2e82197", size = 20404960 }, + { url = "https://files.pythonhosted.org/packages/7e/d2/e896cea21ba8edd6c81d4c55b1ffcc717e79698dcbebf9641b4cfb4c6622/scipy-1.18.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:c5557d8be5da8e41353fcd4d21491fdbab83b062fc579e94dc09a7c8ab4f669b", size = 23034074 }, + { url = "https://files.pythonhosted.org/packages/ea/b2/e83ea34279a52c03374477c74006256ec78df65fc877baa4617d6de1d202/scipy-1.18.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0d13bca67c096d89fb95ced0d8921807300fce0275643aef9533cc63a0773468", size = 33942038 }, + { url = "https://files.pythonhosted.org/packages/f6/af/e8fe5fb136f51e2b01678b92cb4106d10d8cd68ec147ead2e7cb0ac75398/scipy-1.18.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a46f9273dbd0eb1cefba61c9b8648b4dfe3cbc14a080176f9a73e44b8336dc7f", size = 35266390 }, + { url = "https://files.pythonhosted.org/packages/3a/49/2c5cbb907b56695fc67517811d1db234dfd83381a84814ec220aded2794d/scipy-1.18.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5aba46108853ddfc77906b6557aac839d2b52e900c1d72a1180adaaab58d265f", size = 35551324 }, + { url = "https://files.pythonhosted.org/packages/bb/73/eda39f7a2d306ff0ffc574afd13c0bbb6d10a603d9a413998ee269487a80/scipy-1.18.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b6f758e35f12757b5d95c00bc6de2438e229c2664b7a92e96f205959d9f2dfa4", size = 37404785 }, + { url = "https://files.pythonhosted.org/packages/b7/d2/ae881ee28d014f38e0ccbfd974a06a919ba9af34f1f74bf42b5301891d63/scipy-1.18.0-cp313-cp313-win_amd64.whl", hash = "sha256:1afac4a847207c7ff8efd321734a50b06d0280b3b2a2c0fc2f413101747ad7c7", size = 36554943 }, + { url = "https://files.pythonhosted.org/packages/70/3a/21154e2d54eb3639c6bf4dbae2e531c68356bfe95990daa30df33b30d556/scipy-1.18.0-cp313-cp313-win_arm64.whl", hash = "sha256:c5dbddf60e58c2312316d097271a8e73d40eaf2eabfa4d95ed7d3695bbf2ce7b", size = 24350911 }, + { url = "https://files.pythonhosted.org/packages/78/b5/915a19b3de2f7430062b509653563db1633ddbb6f021b06731521115d4e2/scipy-1.18.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4c256ee70c0d1a8a2ace807e199ccd4e3f57037433842abb3fb36bc17eaa9578", size = 31036253 }, + { url = "https://files.pythonhosted.org/packages/d7/88/b72def7262e150d16be13fca37a96481138d624e700340bc3362a7588929/scipy-1.18.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:2ef3abc54a4ffc53765374b0d5728532dfdd2585ed23f6b11c206a1f0b1b9af8", size = 28673758 }, + { url = "https://files.pythonhosted.org/packages/91/02/2e636a61a525632c373cf6a9c24442a3ffb79e364d38e98b32042964ac32/scipy-1.18.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:f2a6af57bd9e4a75d70e4117e78a1bbee84f79ae3fbb6d0111005d6ebcc4cb8d", size = 20415514 }, + { url = "https://files.pythonhosted.org/packages/c9/b6/2135974442f6aba159d9d39d774a1c8cb19947016725d69fecc685df45bf/scipy-1.18.0-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:3f1ac564d3bf6c03d861d2cd87a1bea0da2887136f7fb1bf519c05a8971452d6", size = 23034398 }, + { url = "https://files.pythonhosted.org/packages/f6/e6/ba89ec5abf6ee9257c0d1ec985573f3ae32742c24bc03e016388a40b1b15/scipy-1.18.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40395a5fcd1abee49a5c7aaa98c29db393eedc835138560a588c47ec16156690", size = 33998032 }, + { url = "https://files.pythonhosted.org/packages/7f/c4/bc41eb19b0fd0db868f4132920879019318d80cc522ad8f2bca4611af808/scipy-1.18.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ca01e8ae69f1b18e9a58d91afead31be3cef0dd905a10249dac559ee15460a0", size = 35283333 }, + { url = "https://files.pythonhosted.org/packages/53/a4/cbdeef6eb3830a8462a9d4ada814de5fc984345cc9ecf17cbec51a036f1e/scipy-1.18.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7a7f3b01647384dbc3a711e8c6778e0aabbe93959249fef5c7393396bcac0867", size = 35610216 }, + { url = "https://files.pythonhosted.org/packages/80/4d/b2b82502b65f661d1b789c1665dcdf315d5f12194e06fc0b37946294ebae/scipy-1.18.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6aa94e78ec192a30063a5e72e561c28af769dc311190b24fe91774eff1969709", size = 37418960 }, + { url = "https://files.pythonhosted.org/packages/93/3e/902d836831474b0ab5a37d16404f7bc5fafd9efba632890e271ba952635f/scipy-1.18.0-cp314-cp314-win_amd64.whl", hash = "sha256:2d8bbdc6c817f5b4006a54d799d4f5bab6f910193cbb9a1ff310833d4d270f61", size = 37288845 }, + { url = "https://files.pythonhosted.org/packages/b6/43/8d73b337a3bdb14daa0314f0434210747c02d79d729ce1777574a817dcf6/scipy-1.18.0-cp314-cp314-win_arm64.whl", hash = "sha256:18e9575f1569b2c54174e6159d32942e03731177f63dce7975f0a0c88d102f5b", size = 24988971 }, + { url = "https://files.pythonhosted.org/packages/b4/b4/f11918b0508a2787031a0499a03fbe3546f3bb5ca05d01038c45b278c09a/scipy-1.18.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f351e0dd702687d12a402b867a1b4146a256923e1c38317cbc472f6372b94707", size = 31399325 }, + { url = "https://files.pythonhosted.org/packages/7b/d1/1f287b57c0ff0ee5185dff3946d92c8017d39b0e431f0ae79a3ff1859512/scipy-1.18.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:7c7a51b33ce387193c97f228320cf8e87361daa1bba750638677729598b3e677", size = 29092110 }, + { url = "https://files.pythonhosted.org/packages/ff/1a/7b74eb6c392fdcb27d414c0e7558a6d0231eb3b6d73571f479bb81ea8794/scipy-1.18.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:84031d7b052a54fae2f8632e0ec802073d385476eb9a63079bce6e23ef9283d4", size = 20833811 }, + { url = "https://files.pythonhosted.org/packages/7c/ad/f3941716320a7b9cb4d68734a903b45fe16eff5fb7da7e16f2e619304979/scipy-1.18.0-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:56abf29a7c067dde59be8b9a22d606a4ea1b2f2a4b756d9d903c62818f5dacce", size = 23396644 }, + { url = "https://files.pythonhosted.org/packages/22/22/1446b62ffe07f9719b7d9b1b6a4e05a772833ae8f441fe4c22c34c9b250f/scipy-1.18.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ad44305cfa24b1ba5803cbbebf033590ccbac1aa5d612d727b785325ab408b0", size = 34079318 }, + { url = "https://files.pythonhosted.org/packages/56/3b/b87da667098bb470fa30c7011b0ba351ee976dd395c78798c66e941665a3/scipy-1.18.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:945c1761b93f38d7f99ae81ae80c63e621471608c7eeead563f6df025585cd58", size = 35324320 }, + { url = "https://files.pythonhosted.org/packages/f8/a1/c7932f91909759b0267f75fdea34e91309f96b895757534b76a90b6b4344/scipy-1.18.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1a4441f15d620578772a49e5ab48c0ee1f7a0220e387110283062729136b2553", size = 35699541 }, + { url = "https://files.pythonhosted.org/packages/f7/86/5185061a1fcc41d18c5dc2463969b3a3964b31d9ac67b2fb05d4c7ff7670/scipy-1.18.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9aac6192fac56bf2ca534389d24623f07b39ff83317d58287285e7fbd622ff76", size = 37472480 }, + { url = "https://files.pythonhosted.org/packages/31/8e/f04c68e39919a010d34f2ee1367fd705b0a25a02f609d755f0bfbc0a15fc/scipy-1.18.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e40baea28ae7f5475c779741e2d90b1247c78531207b49c7030e698ff81cee3f", size = 37365390 }, + { url = "https://files.pythonhosted.org/packages/d5/19/969dc072906c84dd0a3b05dcf57ea750936087d7873549e408b35cfc3f97/scipy-1.18.0-cp314-cp314t-win_arm64.whl", hash = "sha256:368e0a705903c466aa5f08eefb39e6b1b6b2d659e7352a31fd9e2438365be0f8", size = 25279661 }, +] + [[package]] name = "setuptools" version = "83.0.0" @@ -3785,14 +4026,14 @@ wheels = [ [[package]] name = "smart-open" -version = "7.5.1" +version = "8.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e8/be/a66598b305763861a9ab15ff0f2fbc44e47b1ce7a776797337a4eef37c66/smart_open-7.5.1.tar.gz", hash = "sha256:3f08e16827c4733699e6b2cc40328a3568f900cb12ad9a3ad233ba6c872d9fe7", size = 54034 } +sdist = { url = "https://files.pythonhosted.org/packages/76/53/9c513747547fd595d5c143259129ea8b9c3ea2f6b7bb9dcea2b1966ded3c/smart_open-8.0.1.tar.gz", hash = "sha256:18b1c4496003c6902be17c15f032b5c319f307c89c6ae9e6b028b508bed8b2cf", size = 61882 } wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/ea/dcdecd68acebb49d3fd560473a43499b1635076f7f1ae8641c060fe7ce74/smart_open-7.5.1-py3-none-any.whl", hash = "sha256:3e07cbbd9c8a908bcb8e25d48becf1a5cbb4886fa975e9f34c672ed171df2318", size = 64108 }, + { url = "https://files.pythonhosted.org/packages/c3/96/325b8c507ccecc50421fecc0345a502ee6e4a44785af3c4e6ecbadad624a/smart_open-8.0.1-py3-none-any.whl", hash = "sha256:3e97f90e92a952cb57863dfe132082c400a52eeeb27c067692fb51dbcc5b0089", size = 73504 }, ] [[package]] @@ -3824,7 +4065,7 @@ wheels = [ [[package]] name = "spacy" -version = "3.8.13" +version = "3.8.14" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "catalogue" }, @@ -3847,40 +4088,30 @@ dependencies = [ { name = "wasabi" }, { name = "weasel" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/63/d7/1924f32272f50d2f13275f16d6f869fcec47b2b676b64f91fa206bd30ef4/spacy-3.8.13.tar.gz", hash = "sha256:eb7c03c2bb16593c34d4c91974118f0931c6e6969dbfe895b1a026c6714176cf", size = 1328016 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4c/a8/21f5b9c3998c7a9cdd28715248e489ebd9300b1d349d5220166b951c3df4/spacy-3.8.13-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3fb3ec2a78a72bb87ca1cc22ad9fea2d37a3d88ec68ee6b4a02c967f35c8b0cb", size = 6617511 }, - { url = "https://files.pythonhosted.org/packages/0c/24/0da6c637b3d25a37db605f836e53226cae65f035e865d2909ddf66b89185/spacy-3.8.13-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1c7d719ff171aad00a9aac31e7c7f6e1871be08b0be50c362885c632826d41ce", size = 6441498 }, - { url = "https://files.pythonhosted.org/packages/17/64/1eeb854dc194dde91582eedf523d38c8caa209fce45c79b175e3e7a00b9f/spacy-3.8.13-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:95c2bf7d0afbf699df89cca5431527dff4950a823fd093680618057aa2affff8", size = 32050555 }, - { url = "https://files.pythonhosted.org/packages/05/5b/7de5aae98f0a4547b9e44fd41e3f5820c199d67c78148782a08df83d19a8/spacy-3.8.13-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b118c9ca4250cba8a39c7fc73641820086d476dafb54b5d1f306a3f60098d3cd", size = 32296475 }, - { url = "https://files.pythonhosted.org/packages/96/33/624d0025097b6b26a2c6d1b3dd7e24dec5da495e8c2a0f379bcc3ec26d15/spacy-3.8.13-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:53a6ff574a4505339375e31ba3280ff7235e82e120377614210750fa66cb63df", size = 32288426 }, - { url = "https://files.pythonhosted.org/packages/c2/be/93eec7f8266a6d96fb5229d4588d64bb6dfa037604b5121a703cad35debb/spacy-3.8.13-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cf07faad25a301efbd90da1343c1f62e35185d064c5e087bd0d11a5a6b3358fe", size = 33113494 }, - { url = "https://files.pythonhosted.org/packages/26/3d/0bd7d780642635e662b32fcd530ed5bed1844dbf977bc5eed43efefa034c/spacy-3.8.13-cp311-cp311-win_amd64.whl", hash = "sha256:740cafd3cb2331c48057f6689ed6be5652d35db92aa2980f32f3d78a9aba6300", size = 15359637 }, - { url = "https://files.pythonhosted.org/packages/be/78/11896d5b5987cd37ae498ef1ce795e0dd551f984fffa9cf6a7887655617e/spacy-3.8.13-cp311-cp311-win_arm64.whl", hash = "sha256:189bdd05cfd66fd3d1f79449d38ba4e2d0270743f762db1304617ccad9dd321c", size = 14717010 }, - { url = "https://files.pythonhosted.org/packages/d8/e1/e8f6dc7fc00582b8dcbf40fac5bce6c0ff366cf6db212decdacfaf13e584/spacy-3.8.13-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:dd8ab0deefe9d2c7dccce1be1e65660a32cfc3cc114d0aa222ff52ae11be58ba", size = 6218311 }, - { url = "https://files.pythonhosted.org/packages/31/fe/517de9e25a6ec469738c9e886c15cd96649b338c6ae475b9b3e4c4f5e03d/spacy-3.8.13-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:eab059bc668336d0034f3f69aed6b661b626c174e97cc1b52296d0d923c24405", size = 6033843 }, - { url = "https://files.pythonhosted.org/packages/2c/a8/9a33c69f772f5c32a57c9ef7e692293b558e7bad202264d5586ab087fd29/spacy-3.8.13-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:96a8e141fd7ff749fa6693c9c72187f802d334bcadf48b6b9d6ca9bff315ca73", size = 32725183 }, - { url = "https://files.pythonhosted.org/packages/ba/cf/dcc0ea61270cb23a46d3efc437fe760e7a9c3cabcfaa770591329d1430f2/spacy-3.8.13-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:32c16f5be8c8006197554da3647d83311fb89bb31bb417d38f0d1da585877b0d", size = 33205816 }, - { url = "https://files.pythonhosted.org/packages/90/6b/ce94ba2a166add3376aa2c976cb1a34d6387e9dc61103cfbe9192675d0b6/spacy-3.8.13-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7402154eae9d2c34d097f3f7f2bf3ed4ce6ef9d44e3035af15ccc7f357d122cb", size = 32090348 }, - { url = "https://files.pythonhosted.org/packages/4d/20/df5076a162bda36b04cdfa9cd544ac2be4b47aed957b3922e4c8f75dc25a/spacy-3.8.13-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:879f467578569db73b5811aa0f0b9a3b3fb81a125c2f7a433dd5626bcaca53f6", size = 32991911 }, - { url = "https://files.pythonhosted.org/packages/41/17/316dfdf5f8d1dd33a205e4f5e7190b67db73802180f7d08c8b306eb44375/spacy-3.8.13-cp312-cp312-win_amd64.whl", hash = "sha256:a0c849b5c9cee5a930a5e8a63d0b07e095d4e3d371af6a70c496efa1d0851257", size = 14226861 }, - { url = "https://files.pythonhosted.org/packages/be/a8/a794d5bbf7bc2df6770df2b14cd6811420d0e10fe81830920bf0e891c19a/spacy-3.8.13-cp312-cp312-win_arm64.whl", hash = "sha256:5d7f660f64c7d09778600b27b881a367075fe792cb5cc6932737aeda71a9aa09", size = 13628855 }, - { url = "https://files.pythonhosted.org/packages/a7/2d/dc15440fa58c12bab25cbd368bbf3b754eb11d1d3cc37f3aee47caa63695/spacy-3.8.13-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6db8903cf3fd8b40db9dab669c1e823a8884100f223dc8ec63c3744ff505d5fd", size = 6202080 }, - { url = "https://files.pythonhosted.org/packages/1b/27/6d723fa0c3c9872d7b8c3fb1c76edef65c1db2de441ee87d119128cb82f7/spacy-3.8.13-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c26258bfed2f3bf5563c5994ee752ea8662bbac70924d28fbea4d1c10df48fd0", size = 6015435 }, - { url = "https://files.pythonhosted.org/packages/c0/36/d13f36204290e7753ce849106fb732a1a1ff6c1af0a200f2c2f493a56e60/spacy-3.8.13-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a5f02eab8e6e8e9e790b1e8da05b70b3b1d4b1fb10dc4f44203da96939fdb977", size = 32510675 }, - { url = "https://files.pythonhosted.org/packages/19/37/f2a0c986bc2d59b18547d5ffaabf57fd9d88e129ad7df5ebc9ccdc91e643/spacy-3.8.13-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:17b14f28d83fe85390f420f89760ae36d515f3e0a236f8266c46335549806e3a", size = 32841103 }, - { url = "https://files.pythonhosted.org/packages/37/31/b4a86cc013e4ac3c3c290ecade748c4623709a78db8a368ee0e152931c0b/spacy-3.8.13-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:10fffb387e6afeba582e01efd9b5e0c5431ac323af134f4a71a749255182f4c8", size = 31763223 }, - { url = "https://files.pythonhosted.org/packages/4f/fa/b3a406bdc2636aaa315b8539b88f262e78ca391afc4083e3e9806953b24a/spacy-3.8.13-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:500f6eb9e4711e75fc07d517962dbfb553ba144ccf1b1e8ffc71b014c9ad91b2", size = 32717863 }, - { url = "https://files.pythonhosted.org/packages/d9/97/11ed2a980e7225b0a1a4041cb1cba44f40bd83682a08e450dd6171f054e4/spacy-3.8.13-cp313-cp313-win_amd64.whl", hash = "sha256:1e679a8c5dd86c564a1185d894b1b8e50b52fa81bee518120fc2f86349d1879c", size = 14220413 }, - { url = "https://files.pythonhosted.org/packages/ca/e2/06633e779486f62b3ec7ddeeb4accc10fb9a356b59f5bed3771dfb89931b/spacy-3.8.13-cp313-cp313-win_arm64.whl", hash = "sha256:c086bff909d73be892b4eb6883070dd3e328592b8933f0059a6569c8c7904928", size = 13619060 }, - { url = "https://files.pythonhosted.org/packages/f2/02/bf2943f61a8bd21cca90e4fe19c4da8752c386f02a76e326b33199e09953/spacy-3.8.13-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:edcdd4f99e2711fc90c6ba3e8e07f7dc9753d111377ba7b7b5eb0d7259b0d211", size = 6209920 }, - { url = "https://files.pythonhosted.org/packages/e7/87/fa5e4eb2ccb660d5042eb9144134dc6238c132ec812131bb0aca296bcdd9/spacy-3.8.13-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b8fd0188f78e032ac58f70a00748d591e244f3d4718e0ead2d9418b4148c744d", size = 6040696 }, - { url = "https://files.pythonhosted.org/packages/ed/f3/9132878e0c18d627adffc1d23129a3706385d4f0fbe2ea5839523519452c/spacy-3.8.13-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0dc2d058cb919102bf0634b1b41aff64b867d9bab36bd35979ab658b5b2e4c27", size = 32431416 }, - { url = "https://files.pythonhosted.org/packages/80/73/396c5c3aaef5004d04abbda4ec736ce4d33944aff6722e974cebdd4023fd/spacy-3.8.13-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ef884df658d3b60d91cbc1e9b62a6d932589d2fa3f322c3f739b6ffdf3303e0c", size = 32483676 }, - { url = "https://files.pythonhosted.org/packages/71/01/15ceb2097a817c2912297eac29f801bd71d895e93d7557110937c88f7c77/spacy-3.8.13-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:86f58d686d1ba6f0dc818005ee6ad87ade8dde7224012f1870a0d31abccd349e", size = 31732468 }, - { url = "https://files.pythonhosted.org/packages/03/c4/b4c39083df6209414faec7d7471994316dc6fca68c4d2f1f8f099f25c2e3/spacy-3.8.13-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:dbcb411b9749a1cd5e4325953212b3984d8c0f60b4976943e77fb0c7d5027383", size = 32473870 }, - { url = "https://files.pythonhosted.org/packages/f2/5c/9b0fa420b01809c0170b3cf0bf509ec06b4da96214995360815615b8e8fa/spacy-3.8.13-cp314-cp314-win_amd64.whl", hash = "sha256:bf9b6879d70201acb73fc5f07d550ff488d3b5f15a959e9896717b2635c8e137", size = 14405303 }, - { url = "https://files.pythonhosted.org/packages/fa/58/0001fd8124b62a2a9984278d793e3abfa1b70e969fc26a8668755178db84/spacy-3.8.13-cp314-cp314-win_arm64.whl", hash = "sha256:b2a402f229fcb5dba5454c346468757bd3a5215809e784b85d739ca84916f05b", size = 13834663 }, +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/8c/0ccf32d9a6b4fd8737bba33d599ddb98934399c1d523f825a4beb4bd1495/spacy-3.8.14-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8c55cb123c3edfba8c252ce6ae27ffb3d7f60a53ba5e108c3534421586c5fdda", size = 6617470 }, + { url = "https://files.pythonhosted.org/packages/e1/61/7f7d38e71daac7f91ffd362fb15645b6f9a68ad231e0ed6ff5c1dc6f6930/spacy-3.8.14-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ab6c1ace316338dac334fc93c849994bbd717f9ebf59d2bc4158e978b2f542ee", size = 6441524 }, + { url = "https://files.pythonhosted.org/packages/5c/ef/18385aa5aeb9bcb299e8074da162b24e5c8bea5aa4d1dfa3dbafb35e9d1f/spacy-3.8.14-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7bece0450cd8ab841cfa8527fcc0ce18c4454f28e3b9fca42a450803a067355b", size = 32050591 }, + { url = "https://files.pythonhosted.org/packages/fa/67/5c4a65ed2cedc598ad000a2b9f45afc76bb8d17a592cc01082dffa8bbc50/spacy-3.8.14-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc5e5f2ed121d57d819d247bb59253dc320a58acbd237b85f86c2aa38cab6bd1", size = 32296467 }, + { url = "https://files.pythonhosted.org/packages/03/a7/28c118879791b3a7ffa81796d22203daac428e6f75572f1b8da1539e1ac6/spacy-3.8.14-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3c4e5bc5cdefe39ea139985776a2e8eae05e7ff2bf51ca1bd65247dc45feeb8e", size = 32288404 }, + { url = "https://files.pythonhosted.org/packages/72/1c/32aefcea2468782fcdb994f2f96cac93dc74f6589ce01047db42d9a299a2/spacy-3.8.14-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2c228f4c9ae618173334c17adb748d66b574b6594bc3575233e15cd5ad1cb26b", size = 33113476 }, + { url = "https://files.pythonhosted.org/packages/86/32/fc00532eabeace451175dd9b152ddd636e8f6a42248b5d90141f98be2af5/spacy-3.8.14-cp311-cp311-win_amd64.whl", hash = "sha256:6f51d1ce8b1ba30123f6bef6e795c4bc5466608e6e8a015dc828bd21d399aa9c", size = 15359704 }, + { url = "https://files.pythonhosted.org/packages/de/31/89ff6722ec91f328dc717932849c6f57249c8a9d429d8670a6c8f70e576b/spacy-3.8.14-cp311-cp311-win_arm64.whl", hash = "sha256:c0c6c9d8771cc3708e309b07310d330fc8443a6bca34f4ff20b0f22751d8faf9", size = 14717168 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { url = "https://files.pythonhosted.org/packages/5f/55/4371413a6dfc1fa837282a365498165f828c2f3fe018dfb35336acc869e0/spacy-3.8.14-cp312-cp312-win_amd64.whl", hash = "sha256:9def18c76a4472b326cb91a195623c9ca38a2b86999ad2df9e00b49ba8c63734", size = 14226946 }, + { url = "https://files.pythonhosted.org/packages/f3/5e/12ac876017da6c1e6b72afcc3c8b309996227fd3aa15382cd3311aee21b8/spacy-3.8.14-cp312-cp312-win_arm64.whl", hash = "sha256:d6257133357e4801c9c5d011925af5439b0a015aacf3c16528aa0009982431c7", size = 13628765 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { url = "https://files.pythonhosted.org/packages/12/c2/f1882ec2f5cc9c4e73cf2132997a03c397d7ceeb5ee7f7bb878b51a16365/spacy-3.8.14-cp313-cp313-win_amd64.whl", hash = "sha256:4b6d4f20e291a7c70e37de2f246622b44a0ce82efaa710c9801c6bd599e75177", size = 14220335 }, ] [[package]] @@ -3954,15 +4185,15 @@ wheels = [ [[package]] name = "sse-starlette" -version = "3.3.2" +version = "3.4.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "starlette" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5a/9f/c3695c2d2d4ef70072c3a06992850498b01c6bc9be531950813716b426fa/sse_starlette-3.3.2.tar.gz", hash = "sha256:678fca55a1945c734d8472a6cad186a55ab02840b4f6786f5ee8770970579dcd", size = 32326 } +sdist = { url = "https://files.pythonhosted.org/packages/d2/1b/bc9e3e7a72dcdad7dc7888758f5d00f56f8909ed5cfdff822bd72bb4c520/sse_starlette-3.4.5.tar.gz", hash = "sha256:83072538bc211a2f68b7b0422226c4af3e9b62e106e07034664b832ca019842a", size = 35249 } wheels = [ - { url = "https://files.pythonhosted.org/packages/61/28/8cb142d3fe80c4a2d8af54ca0b003f47ce0ba920974e7990fa6e016402d1/sse_starlette-3.3.2-py3-none-any.whl", hash = "sha256:5c3ea3dad425c601236726af2f27689b74494643f57017cafcb6f8c9acfbb862", size = 14270 }, + { url = "https://files.pythonhosted.org/packages/78/75/c88d3f5dafd59c791da1ce27650d30bf5b70cbf1cbf01cd00e5f9e360915/sse_starlette-3.4.5-py3-none-any.whl", hash = "sha256:e71bad53323f65573c3864a6c3bd0c1eb6e5f092b2e48082b0c35927d19ca296", size = 16518 }, ] [[package]] @@ -3980,7 +4211,7 @@ wheels = [ [[package]] name = "textual" -version = "8.1.1" +version = "8.2.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py", extra = ["linkify"] }, @@ -3990,9 +4221,9 @@ dependencies = [ { name = "rich" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/72/23/8c709655c5f2208ee82ab81b8104802421865535c278a7649b842b129db1/textual-8.1.1.tar.gz", hash = "sha256:eef0256a6131f06a20ad7576412138c1f30f92ddeedd055953c08d97044bc317", size = 1843002 } +sdist = { url = "https://files.pythonhosted.org/packages/00/21/39a76b01bd5eea82a04baaca7580e105d8c59450df03998345bb2cfb307b/textual-8.2.8.tar.gz", hash = "sha256:3f106a9fbc73e39dd266c9712432087de78a6d644084c7c241d6a25c3169115b", size = 1860502 } wheels = [ - { url = "https://files.pythonhosted.org/packages/50/21/421b02bf5943172b7a9320712a5e0d74a02a8f7597284e3f8b5b06c70b8d/textual-8.1.1-py3-none-any.whl", hash = "sha256:6712f96e335cd782e76193dee16b9c8875fe0699d923bc8d3f1228fd23e773a6", size = 719598 }, + { url = "https://files.pythonhosted.org/packages/fb/be/35261223d9416a0751cdff1c7b4a6f881387218a12d439fe22fefebc8c04/textual-8.2.8-py3-none-any.whl", hash = "sha256:267375fd402dc8d981457212efa71f0e3365fd17bba144ba9bb3ed7563cb374a", size = 731418 }, ] [[package]] @@ -4051,207 +4282,251 @@ wheels = [ [[package]] name = "tiktoken" -version = "0.12.0" +version = "0.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "regex" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/4d017d0f76ec3171d469d80fc03dfbb4e48a4bcaddaa831b31d526f05edc/tiktoken-0.12.0.tar.gz", hash = "sha256:b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931", size = 37806 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/de/46/21ea696b21f1d6d1efec8639c204bdf20fde8bafb351e1355c72c5d7de52/tiktoken-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6e227c7f96925003487c33b1b32265fad2fbcec2b7cf4817afb76d416f40f6bb", size = 1051565 }, - { url = "https://files.pythonhosted.org/packages/c9/d9/35c5d2d9e22bb2a5f74ba48266fb56c63d76ae6f66e02feb628671c0283e/tiktoken-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c06cf0fcc24c2cb2adb5e185c7082a82cba29c17575e828518c2f11a01f445aa", size = 995284 }, - { url = "https://files.pythonhosted.org/packages/01/84/961106c37b8e49b9fdcf33fe007bb3a8fdcc380c528b20cc7fbba80578b8/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:f18f249b041851954217e9fd8e5c00b024ab2315ffda5ed77665a05fa91f42dc", size = 1129201 }, - { url = "https://files.pythonhosted.org/packages/6a/d0/3d9275198e067f8b65076a68894bb52fd253875f3644f0a321a720277b8a/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:47a5bc270b8c3db00bb46ece01ef34ad050e364b51d406b6f9730b64ac28eded", size = 1152444 }, - { url = "https://files.pythonhosted.org/packages/78/db/a58e09687c1698a7c592e1038e01c206569b86a0377828d51635561f8ebf/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:508fa71810c0efdcd1b898fda574889ee62852989f7c1667414736bcb2b9a4bd", size = 1195080 }, - { url = "https://files.pythonhosted.org/packages/9e/1b/a9e4d2bf91d515c0f74afc526fd773a812232dd6cda33ebea7f531202325/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a1af81a6c44f008cba48494089dd98cccb8b313f55e961a52f5b222d1e507967", size = 1255240 }, - { url = "https://files.pythonhosted.org/packages/9d/15/963819345f1b1fb0809070a79e9dd96938d4ca41297367d471733e79c76c/tiktoken-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:3e68e3e593637b53e56f7237be560f7a394451cb8c11079755e80ae64b9e6def", size = 879422 }, - { url = "https://files.pythonhosted.org/packages/a4/85/be65d39d6b647c79800fd9d29241d081d4eeb06271f383bb87200d74cf76/tiktoken-0.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b97f74aca0d78a1ff21b8cd9e9925714c15a9236d6ceacf5c7327c117e6e21e8", size = 1050728 }, - { url = "https://files.pythonhosted.org/packages/4a/42/6573e9129bc55c9bf7300b3a35bef2c6b9117018acca0dc760ac2d93dffe/tiktoken-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b90f5ad190a4bb7c3eb30c5fa32e1e182ca1ca79f05e49b448438c3e225a49b", size = 994049 }, - { url = "https://files.pythonhosted.org/packages/66/c5/ed88504d2f4a5fd6856990b230b56d85a777feab84e6129af0822f5d0f70/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:65b26c7a780e2139e73acc193e5c63ac754021f160df919add909c1492c0fb37", size = 1129008 }, - { url = "https://files.pythonhosted.org/packages/f4/90/3dae6cc5436137ebd38944d396b5849e167896fc2073da643a49f372dc4f/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:edde1ec917dfd21c1f2f8046b86348b0f54a2c0547f68149d8600859598769ad", size = 1152665 }, - { url = "https://files.pythonhosted.org/packages/a3/fe/26df24ce53ffde419a42f5f53d755b995c9318908288c17ec3f3448313a3/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:35a2f8ddd3824608b3d650a000c1ef71f730d0c56486845705a8248da00f9fe5", size = 1194230 }, - { url = "https://files.pythonhosted.org/packages/20/cc/b064cae1a0e9fac84b0d2c46b89f4e57051a5f41324e385d10225a984c24/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83d16643edb7fa2c99eff2ab7733508aae1eebb03d5dfc46f5565862810f24e3", size = 1254688 }, - { url = "https://files.pythonhosted.org/packages/81/10/b8523105c590c5b8349f2587e2fdfe51a69544bd5a76295fc20f2374f470/tiktoken-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffc5288f34a8bc02e1ea7047b8d041104791d2ddbf42d1e5fa07822cbffe16bd", size = 878694 }, - { url = "https://files.pythonhosted.org/packages/00/61/441588ee21e6b5cdf59d6870f86beb9789e532ee9718c251b391b70c68d6/tiktoken-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:775c2c55de2310cc1bc9a3ad8826761cbdc87770e586fd7b6da7d4589e13dab3", size = 1050802 }, - { url = "https://files.pythonhosted.org/packages/1f/05/dcf94486d5c5c8d34496abe271ac76c5b785507c8eae71b3708f1ad9b45a/tiktoken-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a01b12f69052fbe4b080a2cfb867c4de12c704b56178edf1d1d7b273561db160", size = 993995 }, - { url = "https://files.pythonhosted.org/packages/a0/70/5163fe5359b943f8db9946b62f19be2305de8c3d78a16f629d4165e2f40e/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:01d99484dc93b129cd0964f9d34eee953f2737301f18b3c7257bf368d7615baa", size = 1128948 }, - { url = "https://files.pythonhosted.org/packages/0c/da/c028aa0babf77315e1cef357d4d768800c5f8a6de04d0eac0f377cb619fa/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:4a1a4fcd021f022bfc81904a911d3df0f6543b9e7627b51411da75ff2fe7a1be", size = 1151986 }, - { url = "https://files.pythonhosted.org/packages/a0/5a/886b108b766aa53e295f7216b509be95eb7d60b166049ce2c58416b25f2a/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:981a81e39812d57031efdc9ec59fa32b2a5a5524d20d4776574c4b4bd2e9014a", size = 1194222 }, - { url = "https://files.pythonhosted.org/packages/f4/f8/4db272048397636ac7a078d22773dd2795b1becee7bc4922fe6207288d57/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9baf52f84a3f42eef3ff4e754a0db79a13a27921b457ca9832cf944c6be4f8f3", size = 1255097 }, - { url = "https://files.pythonhosted.org/packages/8e/32/45d02e2e0ea2be3a9ed22afc47d93741247e75018aac967b713b2941f8ea/tiktoken-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:b8a0cd0c789a61f31bf44851defbd609e8dd1e2c8589c614cc1060940ef1f697", size = 879117 }, - { url = "https://files.pythonhosted.org/packages/ce/76/994fc868f88e016e6d05b0da5ac24582a14c47893f4474c3e9744283f1d5/tiktoken-0.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d5f89ea5680066b68bcb797ae85219c72916c922ef0fcdd3480c7d2315ffff16", size = 1050309 }, - { url = "https://files.pythonhosted.org/packages/f6/b8/57ef1456504c43a849821920d582a738a461b76a047f352f18c0b26c6516/tiktoken-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b4e7ed1c6a7a8a60a3230965bdedba8cc58f68926b835e519341413370e0399a", size = 993712 }, - { url = "https://files.pythonhosted.org/packages/72/90/13da56f664286ffbae9dbcfadcc625439142675845baa62715e49b87b68b/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:fc530a28591a2d74bce821d10b418b26a094bf33839e69042a6e86ddb7a7fb27", size = 1128725 }, - { url = "https://files.pythonhosted.org/packages/05/df/4f80030d44682235bdaecd7346c90f67ae87ec8f3df4a3442cb53834f7e4/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:06a9f4f49884139013b138920a4c393aa6556b2f8f536345f11819389c703ebb", size = 1151875 }, - { url = "https://files.pythonhosted.org/packages/22/1f/ae535223a8c4ef4c0c1192e3f9b82da660be9eb66b9279e95c99288e9dab/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:04f0e6a985d95913cabc96a741c5ffec525a2c72e9df086ff17ebe35985c800e", size = 1194451 }, - { url = "https://files.pythonhosted.org/packages/78/a7/f8ead382fce0243cb625c4f266e66c27f65ae65ee9e77f59ea1653b6d730/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0ee8f9ae00c41770b5f9b0bb1235474768884ae157de3beb5439ca0fd70f3e25", size = 1253794 }, - { url = "https://files.pythonhosted.org/packages/93/e0/6cc82a562bc6365785a3ff0af27a2a092d57c47d7a81d9e2295d8c36f011/tiktoken-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dc2dd125a62cb2b3d858484d6c614d136b5b848976794edfb63688d539b8b93f", size = 878777 }, - { url = "https://files.pythonhosted.org/packages/72/05/3abc1db5d2c9aadc4d2c76fa5640134e475e58d9fbb82b5c535dc0de9b01/tiktoken-0.12.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a90388128df3b3abeb2bfd1895b0681412a8d7dc644142519e6f0a97c2111646", size = 1050188 }, - { url = "https://files.pythonhosted.org/packages/e3/7b/50c2f060412202d6c95f32b20755c7a6273543b125c0985d6fa9465105af/tiktoken-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:da900aa0ad52247d8794e307d6446bd3cdea8e192769b56276695d34d2c9aa88", size = 993978 }, - { url = "https://files.pythonhosted.org/packages/14/27/bf795595a2b897e271771cd31cb847d479073497344c637966bdf2853da1/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:285ba9d73ea0d6171e7f9407039a290ca77efcdb026be7769dccc01d2c8d7fff", size = 1129271 }, - { url = "https://files.pythonhosted.org/packages/f5/de/9341a6d7a8f1b448573bbf3425fa57669ac58258a667eb48a25dfe916d70/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:d186a5c60c6a0213f04a7a802264083dea1bbde92a2d4c7069e1a56630aef830", size = 1151216 }, - { url = "https://files.pythonhosted.org/packages/75/0d/881866647b8d1be4d67cb24e50d0c26f9f807f994aa1510cb9ba2fe5f612/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:604831189bd05480f2b885ecd2d1986dc7686f609de48208ebbbddeea071fc0b", size = 1194860 }, - { url = "https://files.pythonhosted.org/packages/b3/1e/b651ec3059474dab649b8d5b69f5c65cd8fcd8918568c1935bd4136c9392/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8f317e8530bb3a222547b85a58583238c8f74fd7a7408305f9f63246d1a0958b", size = 1254567 }, - { url = "https://files.pythonhosted.org/packages/80/57/ce64fd16ac390fafde001268c364d559447ba09b509181b2808622420eec/tiktoken-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:399c3dd672a6406719d84442299a490420b458c44d3ae65516302a99675888f3", size = 921067 }, - { url = "https://files.pythonhosted.org/packages/ac/a4/72eed53e8976a099539cdd5eb36f241987212c29629d0a52c305173e0a68/tiktoken-0.12.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2c714c72bc00a38ca969dae79e8266ddec999c7ceccd603cc4f0d04ccd76365", size = 1050473 }, - { url = "https://files.pythonhosted.org/packages/e6/d7/0110b8f54c008466b19672c615f2168896b83706a6611ba6e47313dbc6e9/tiktoken-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cbb9a3ba275165a2cb0f9a83f5d7025afe6b9d0ab01a22b50f0e74fee2ad253e", size = 993855 }, - { url = "https://files.pythonhosted.org/packages/5f/77/4f268c41a3957c418b084dd576ea2fad2e95da0d8e1ab705372892c2ca22/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:dfdfaa5ffff8993a3af94d1125870b1d27aed7cb97aa7eb8c1cefdbc87dbee63", size = 1129022 }, - { url = "https://files.pythonhosted.org/packages/4e/2b/fc46c90fe5028bd094cd6ee25a7db321cb91d45dc87531e2bdbb26b4867a/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:584c3ad3d0c74f5269906eb8a659c8bfc6144a52895d9261cdaf90a0ae5f4de0", size = 1150736 }, - { url = "https://files.pythonhosted.org/packages/28/c0/3c7a39ff68022ddfd7d93f3337ad90389a342f761c4d71de99a3ccc57857/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:54c891b416a0e36b8e2045b12b33dd66fb34a4fe7965565f1b482da50da3e86a", size = 1194908 }, - { url = "https://files.pythonhosted.org/packages/ab/0d/c1ad6f4016a3968c048545f5d9b8ffebf577774b2ede3e2e352553b685fe/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5edb8743b88d5be814b1a8a8854494719080c28faaa1ccbef02e87354fe71ef0", size = 1253706 }, - { url = "https://files.pythonhosted.org/packages/af/df/c7891ef9d2712ad774777271d39fdef63941ffba0a9d59b7ad1fd2765e57/tiktoken-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f61c0aea5565ac82e2ec50a05e02a6c44734e91b51c10510b084ea1b8e633a71", size = 920667 }, +sdist = { url = "https://files.pythonhosted.org/packages/e4/e5/5f3cb2159769d0f4324c0e9e87f9de3c4b1cd45848a96b2eb3566ad5ca77/tiktoken-0.13.0.tar.gz", hash = "sha256:c9435714c3a84c2319499de9a300c0e604449dd0799ff246458b3bb6a7f433c1", size = 38986 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/4c/1bc81f4cd53e827c4ee67ca951b5935724716049452d8dfa09b8b82372bb/tiktoken-0.13.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7bfe1849caa65d1e1d9871817170ec497bbb7984e182012e1bdce72f66608cdb", size = 1036353 }, + { url = "https://files.pythonhosted.org/packages/75/91/10b9c7076bc02c246c853201fdbbe300a4b8c5ed7b84c25f7403f4e32655/tiktoken-0.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:91c180fe255bd5a86d8316210d2833a1d4d33d026cd86a67812f4773743c8d26", size = 984644 }, + { url = "https://files.pythonhosted.org/packages/4e/e4/fceae98015fab47fcd49b8bd7f46145bcd187a47e0add1e5378ed67ef980/tiktoken-0.13.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:059c8ecf554eb5b41e6e054ba467b871b03277d267dee7244380aca4359747d4", size = 1119261 }, + { url = "https://files.pythonhosted.org/packages/f9/39/fe42ad00de01a8c4a49ad8649a2c8a316835a9cad5961b11d21eac0020a5/tiktoken-0.13.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:36217497eaffc158607a3b26f065300db2aefd43b115263f3b9688ce38146173", size = 1138253 }, + { url = "https://files.pythonhosted.org/packages/03/c4/ccee1ecccca107e9a16efcecdeeb964c325305038554d466ece65b42338f/tiktoken-0.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:303f7d91b4fce3baddbcde05c139091d4caa5026ac7214c1dc7ff7a71ee429ff", size = 1185747 }, + { url = "https://files.pythonhosted.org/packages/9d/03/cd0cba295522b91eb55c6b2704f1df895f8226cfe60ab10d4d51d0cc9e69/tiktoken-0.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5d48843bee149630eb735a99e1f4a85b47308d21868ea63163f6e87768d3cfed", size = 1241265 }, + { url = "https://files.pythonhosted.org/packages/7e/25/a10efd564402d82c2ff50d12057353ace447aa8007deceaa48641f63d35c/tiktoken-0.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:fc1c44cd37b43fc46bae593129164f4f281e82ea116b57a85aa81bda57eafc94", size = 876509 }, + { url = "https://files.pythonhosted.org/packages/85/8e/144bde4e01df66b34bb865557c7cd754ed08b036217ebd79c9db5e9048a9/tiktoken-0.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:32ac870a806cfb260a02d0cb70426aef02e038297f8ad50df5040bb5af360791", size = 1034888 }, + { url = "https://files.pythonhosted.org/packages/36/18/d4ac9d20956cdebca04841316660ed584c2fecdc2b81722a28bc7ad3b1e4/tiktoken-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4d9980f11429ed2d737c463bb1fb78cf330caa026adf002f714aced7849a687b", size = 982970 }, + { url = "https://files.pythonhosted.org/packages/74/ed/6bb8d05b9f731f749fee5c6f5ca63e981143c826a5985877330507bd13b7/tiktoken-0.13.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3f277ebea5edd7b8bf03c6f9431e1d67d517530115572b2dc1d465326e8f88c7", size = 1115741 }, + { url = "https://files.pythonhosted.org/packages/34/de/2ca96b07a82d972b74fe4b46de055b79c904e45c7eab699354a0bfa697dc/tiktoken-0.13.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:a116178fa7e1b4065bff05214360373a65cac22f965be7b3f73d00a0dbfe7649", size = 1136523 }, + { url = "https://files.pythonhosted.org/packages/ee/dc/9dafec002c2d4424378563cf4cf5c7fb93631d2a55013c8b87554ee4012c/tiktoken-0.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c397ddda233208345b01bd30f2fca79ff730e55731d0108a603f9bc57f6af3b", size = 1181954 }, + { url = "https://files.pythonhosted.org/packages/a1/d0/1f8578c45b2f24759b46f0b50d31878c63c73e6bf0f2227e10ec5c5408dc/tiktoken-0.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:95097e4f89b06403976e498abf61a0ee73a7497e73fb599cb211d8197a054d91", size = 1240069 }, + { url = "https://files.pythonhosted.org/packages/aa/90/28d7f154888610aa9237e541986beb62b479df29d193a5a0617dbb1514d0/tiktoken-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:8f2d16e7a7c783ad81f36e457d046d1f1c8af70b22aec8a13238efe531977c41", size = 874748 }, + { url = "https://files.pythonhosted.org/packages/9c/83/b096c859c2a47c11731bf2f5885f4028b809dfe2396582883eed9cae372f/tiktoken-0.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5df5d1507bd245f1ccad4a074698240021239e455eb0bb4ced4e3d7181872154", size = 1034228 }, + { url = "https://files.pythonhosted.org/packages/53/61/c68e123b6d753e3fc2751e9b18e732c9d8bf1e1926762e736eee935d931c/tiktoken-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8fe806a50664e83a6ffd56cbd1e4f5dcc6cd32a3e7538f70dc38b1a271384545", size = 982978 }, + { url = "https://files.pythonhosted.org/packages/ef/8b/96cc178cc584e65d363134500f297790b06cd48cdeb1e8fcf7bbe60f4715/tiktoken-0.13.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:125bc05005e747f993a83dc67934249932d6e4209854452cd4c0b1d53fba3ba2", size = 1116355 }, + { url = "https://files.pythonhosted.org/packages/86/f5/bab735d2c72ea55404b295d02d092644eb5f7cc6205e34d35eb9abfb9ab2/tiktoken-0.13.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5e6358911cab4adee6712da27d65573496a4f68cf8a2b5fca6a4ad10fc5748cf", size = 1135772 }, + { url = "https://files.pythonhosted.org/packages/4e/b9/6de04ebdf904edfaad87788011b3735087a0c9ea671b9027e1e4e965e8c8/tiktoken-0.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:975cbd78d085d75d26b59660e262736dcaed1e35f8f142cd6291025c01d25486", size = 1182415 }, + { url = "https://files.pythonhosted.org/packages/0d/9c/470a05f3b1caf038f44880e334d47ab674e0c80d514c66b375d14d5afa10/tiktoken-0.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:75ab9bc99fa020a4c283424590ecd7f3afd70c1c281cb3fa3192a6c3af9f9615", size = 1239879 }, + { url = "https://files.pythonhosted.org/packages/42/a6/c1936d16055436cb32e6c6128d68629622e00f4768562f55653752d34768/tiktoken-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:6b1615f0ff71953d19729ceb18865429c185b0a23c5353f1bbca34a394bf60f7", size = 874829 }, + { url = "https://files.pythonhosted.org/packages/d6/07/acb5992c3772b5a36284f742cfb7a5895aa4471d1848ac31464ad50d7fdf/tiktoken-0.13.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6eb4a5bfbc6426938026b1a334e898ac53541360d62d8c689870160cc80abd67", size = 1033600 }, + { url = "https://files.pythonhosted.org/packages/14/e9/742e9aec30f59b9f161f7ff7cd072e02ea836c9e1c0854a8076dfcd40d5c/tiktoken-0.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:43cee3e5400573b2046fbf092cc7a5bc30164f9e4c95ce20714da929df48737a", size = 982516 }, + { url = "https://files.pythonhosted.org/packages/72/74/ca1541b053e7648254d2e4b42a253e1bb4359f2c91a0a8d49228c794e1a0/tiktoken-0.13.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:7de52e3f566d19b3b11bd37eea552c6c305ad74081f736882bd44d148ed4c48d", size = 1115518 }, + { url = "https://files.pythonhosted.org/packages/46/e3/93825eaf5a4a504795b787e5d5dea07fbeb3dabf97aa7b450be8bde59c89/tiktoken-0.13.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:51384448aa508e4df84c0f7c1dc3211c7f7b8096325660ee5fc82f3e11b381ce", size = 1136867 }, + { url = "https://files.pythonhosted.org/packages/8c/46/002b68de6827091d5ae90b048f326e8aad8d953520950e5ce1508879414f/tiktoken-0.13.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e28157350f7ebf35008dd8e9e0fdb621f976e4230c881099c85e8cf07eaa50e2", size = 1181826 }, + { url = "https://files.pythonhosted.org/packages/db/c6/d393e3185a276505182f7abd93fe714f3c444a2be9180798fa052347504e/tiktoken-0.13.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:165cf1820ea4a354985c2490a5205d4cc74661c934aca79dd0368232fff94e0f", size = 1239489 }, + { url = "https://files.pythonhosted.org/packages/b7/4d/bc07d1f1635d4897a202acc0ae11c2886eaa7325c359ba4741b47bf8e225/tiktoken-0.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6c43a675ca14f6f2749ba7f12075d37456015a24b859f2517b9beb4ef30807ec", size = 873820 }, + { url = "https://files.pythonhosted.org/packages/8c/93/0dd6adca026a616c3a92974566b43381eea4b475ce1f36c062b8271a9ac5/tiktoken-0.13.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaaaef47c2406277181d2086484c317bf7fc433e2d5d03ff94f56b0dcec87471", size = 1034977 }, + { url = "https://files.pythonhosted.org/packages/d9/77/5ec6e6bc5b30bed6d93f7f2162d8f6b32437b3ba27cb527cfe004f6109c9/tiktoken-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ca8b310bd93b3772cb1b7922d915446864860f562bdfe4825c63a0aed3fb28cd", size = 983635 }, + { url = "https://files.pythonhosted.org/packages/94/b0/c8ae9aff00d625c50659b4513e707a0462c4bf5d4d6cc1b802103225c02e/tiktoken-0.13.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:32e0c12305105002c047b3bb1070b0dd9a73b0cb3b2856a8972b810e7a4f5881", size = 1116036 }, + { url = "https://files.pythonhosted.org/packages/1b/ac/6a5dddd1d0a6018ecb389bd0353e6b4a515eb4d2286611bd0ace1937b9e1/tiktoken-0.13.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:5ba5fd62507a932d1241346179e3b39bc7bf7408f03c272652d93b3bedf5db24", size = 1135544 }, + { url = "https://files.pythonhosted.org/packages/f4/b8/585032b4384b2f7dcdaddcb52865c83a701a420d09e3c2b4a2be1c450c57/tiktoken-0.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d108bc2d470fc53c8ecd24f2c0fd2b5f98c33e87cdb6aa2e9b8c5dced703d273", size = 1182217 }, + { url = "https://files.pythonhosted.org/packages/cd/b6/993ff1ded3958215fd341a847b8e5ffeb5de473f435296870d314fc91ac4/tiktoken-0.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cb99cb5127449f58d0a2d5f5ccfb390d8dbdfd919c221246caaee29d8725ed51", size = 1239404 }, + { url = "https://files.pythonhosted.org/packages/dd/3d/fef7e06e3b33e7538db0ced734cf9fe23b6832d2ac4990c119c377aec55e/tiktoken-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:115c4f26ffa11caac8b54eea35c2ad38c612c20a48d35dd15d70a02ac6f51f58", size = 918686 }, + { url = "https://files.pythonhosted.org/packages/c1/82/a7fc44582bc32ab00de988a2299bf77c077f59068b233109e34b7d6ca7e6/tiktoken-0.13.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:472527e9132952f2fbf77cd290658bacf003d4d5a3fabc18e5fbd407cbae4d9b", size = 1034454 }, + { url = "https://files.pythonhosted.org/packages/37/d0/24d8a890c14f432a05cea669c17bebeaa99f96a7c79523b590f564246411/tiktoken-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4e2f67d27c9626cdd25fe33d9313c5cdb3d8d82da646b68d6eb8e7e9c20e6448", size = 982976 }, + { url = "https://files.pythonhosted.org/packages/49/b7/2ab43f62788a9266187a9bfc1d3af99ad83e5eaa25fbef168a69cd5ad14f/tiktoken-0.13.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:2b920b35805cd64585a37c3dc7ce65fba4d2d36016be01e1d7942482ca29093a", size = 1115526 }, + { url = "https://files.pythonhosted.org/packages/64/39/1494321ed323ce7a14d88e3cd6cb9058625977df1c6961ddc492bd10a9f3/tiktoken-0.13.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:493af3aa28a4aaf2e3d2600a2ee717252c9bf5ab38fff94eb5a02db5ab77e5ad", size = 1136466 }, + { url = "https://files.pythonhosted.org/packages/96/d9/dfd086aa2d918c563a140720e0ce296cada1634efd2783d5cf51e05f984e/tiktoken-0.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6644c9c2b5cf3916f5a3641d7d12fdb3f006a7b3d9ff6acdaec44e29ab1ff91e", size = 1181863 }, + { url = "https://files.pythonhosted.org/packages/2f/68/a18b4f307086954fdae32714cb4f85562e34f9d34ab206e61f1816aa6018/tiktoken-0.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5cb65b60b9408563676d874a3a4ee573370066f0dc4e29d84e82e989c6517424", size = 1239218 }, + { url = "https://files.pythonhosted.org/packages/16/5b/f2aa703a4fc5d2dff73460a7d46cc2f3f44aa0f3dd8eeb20d2a0ecf68862/tiktoken-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:85b78cc3a2c3d48723ca751fa981f1fedccd54194ca0471b957364353a898b07", size = 918110 }, ] [[package]] name = "tokenizers" -version = "0.22.2" +version = "0.23.1" 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 } +sdist = { url = "https://files.pythonhosted.org/packages/c1/60/21f715d9faba5f5407ff759472ade058ec4a507ad62bcea47cb847239a73/tokenizers-0.23.1.tar.gz", hash = "sha256:1feeeadf865a7915adc25445dea30e9933e593c31bb96c277cee36de227c8bfa", size = 365748 } 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363 }, - { url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786 }, - { url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133 }, + { url = "https://files.pythonhosted.org/packages/87/39/b87a87d5bb9470610b80a2d31df42fcffeaf35118b8b97952b2aff598cc7/tokenizers-0.23.1-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e03d6ffcbe0d56ee9c1ccd070e70a13fa750727c0277e138152acbc0252c2224", size = 3146732 }, + { url = "https://files.pythonhosted.org/packages/e2/6a/068ed9f6e444c9d7e9d55ce134181325700f3d7f30410721bdc8f848d727/tokenizers-0.23.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:e0948bbb1ac1d7cdfc9fb6d62c596e3b7550036ad60ecd654a66ad273326324e", size = 3054954 }, + { url = "https://files.pythonhosted.org/packages/6c/36/e006edf031154cba92b8416057d92c3abe3635e4c4b0aa0b5b9bb39dde70/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bf13402aff9bc533c89cb849ec3b412dc3fbeacc9744840e423d7bf3f7dc0e3", size = 3374081 }, + { url = "https://files.pythonhosted.org/packages/a2/ef/7735d226f9c7f874a6bee5e3f27fb25ecabdf207d37b8cf45286d0795893/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f836ca703b89ae07919a309f9651f7a88fd5a33d5f718ba5ad0870ec0256bad6", size = 3247641 }, + { url = "https://files.pythonhosted.org/packages/b9/d9/24827036f6e21297bfffda0768e58eb6096a4f411e932964a01707857931/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae848657742035523fdf261773630cb819a26995fcd3d9ecae0c1daf6e5a4959", size = 3585624 }, + { url = "https://files.pythonhosted.org/packages/0c/9a/22f3582b3a4f49358293a5206e25317621ee4526bfe9cdaa0f07a12e770e/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:53b09e85775d5187941e7bab30e941b4134ab4a7dd8c68e783d231fb7ca27c51", size = 3844062 }, + { url = "https://files.pythonhosted.org/packages/7e/65/b8f8814eef95800f20721384136d9a1d22241d50b2874357cb70542c392f/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea5a0ce170074329faaa8ea3f6400ecde604b6678192688533af80980daae71a", size = 3460098 }, + { url = "https://files.pythonhosted.org/packages/0d/d5/1353e5f677ec27c2494fb6a6725e82d56c985f53e90ec511369e7e4f02c6/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5075b405006415ea148a992d093699c66eb01952bf59f4d5727089a98bda45a4", size = 3346235 }, + { url = "https://files.pythonhosted.org/packages/71/89/39b6b8fc073fb6d413d0147aa333dc7eff7be65639ac9d19930a0b21bf33/tokenizers-0.23.1-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:56f3a77de629917652f876294dc9fe6bad4a0c43bc229dc72e59bb23a0f4729a", size = 3426398 }, + { url = "https://files.pythonhosted.org/packages/0f/80/127c854da64827e5b79264ce524993a90dddcb320e5cd42412c5c02f9e8a/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9d10a6d957ef01896dc274e890eee27d41bd0e74ef31e60616f0fc311345184e", size = 9823279 }, + { url = "https://files.pythonhosted.org/packages/fe/ba/44c2502feb1a058f096ddfb4e0996ef3225a01a388e1a9b094e91689fe93/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:1974288a609c343774f1b897c8b482c791ab17b75ab5c8c2b1737565c1d82288", size = 9644986 }, + { url = "https://files.pythonhosted.org/packages/9e/c1/464019a9fb059870bfe4eebb4ba12208f3042035e258bf5e782906bd3847/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:120468fb4c24faf0543c835a4fabafa4deb3f20a035c9b6e83d0b553a97615d4", size = 9976181 }, + { url = "https://files.pythonhosted.org/packages/79/94/3ac1432bda31626071e9b6a12709b97ae05131c804b94c8f3ac622c5da32/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e3d8f40ea6268047de7046906326abed5134f27d4e8447b23763afe5808c8a96", size = 10113853 }, + { url = "https://files.pythonhosted.org/packages/6a/dd/631b21433c771b1382535326f0eca80b9c9cee2e64961dd993bc9ac4669e/tokenizers-0.23.1-cp310-abi3-win32.whl", hash = "sha256:93120a930b919416da7cd10a2f606ac9919cc69cacae7980fa2140e277660948", size = 2536263 }, + { url = "https://files.pythonhosted.org/packages/97/c9/2553f72aaf65a2797d4229e37fa7fbe38ffbf3e32912d31bdd78b3323e59/tokenizers-0.23.1-cp310-abi3-win_amd64.whl", hash = "sha256:e7bfaf995c1bdbbd21d13539decb6650967013759318627d85daeb7881af16b7", size = 2798223 }, + { url = "https://files.pythonhosted.org/packages/cd/2b/2be299bab55fc595e3d38567edb1a87f86e594842968fa9515a07bdcf422/tokenizers-0.23.1-cp310-abi3-win_arm64.whl", hash = "sha256:a26197957d8e4425dfba746315f3c425ea00cfa8367c5fbc4ec73447893dcea9", size = 2664127 }, ] [[package]] name = "tomli" -version = "2.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663 }, - { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469 }, - { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039 }, - { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007 }, - { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875 }, - { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271 }, - { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770 }, - { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626 }, - { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842 }, - { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894 }, - { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053 }, - { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481 }, - { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720 }, - { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014 }, - { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820 }, - { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712 }, - { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296 }, - { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553 }, - { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915 }, - { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038 }, - { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245 }, - { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335 }, - { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962 }, - { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396 }, - { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530 }, - { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227 }, - { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748 }, - { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725 }, - { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901 }, - { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375 }, - { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639 }, - { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897 }, - { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697 }, - { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567 }, - { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556 }, - { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014 }, - { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339 }, - { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490 }, - { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398 }, - { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515 }, - { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806 }, - { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340 }, - { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106 }, - { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504 }, - { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561 }, - { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477 }, +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704 }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454 }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561 }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824 }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227 }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859 }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204 }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084 }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285 }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924 }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018 }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948 }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341 }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159 }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290 }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141 }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847 }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088 }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866 }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887 }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704 }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628 }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180 }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674 }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976 }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755 }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265 }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726 }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859 }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713 }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084 }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973 }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223 }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973 }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082 }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490 }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263 }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736 }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717 }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461 }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855 }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144 }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683 }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196 }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393 }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583 }, ] [[package]] name = "tqdm" -version = "4.67.3" +version = "4.68.4" 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 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374 }, +sdist = { url = "https://files.pythonhosted.org/packages/ae/5f/57ff8b434839e70dab45601284ea413e947a63799891b7553e5960a793a8/tqdm-4.68.4.tar.gz", hash = "sha256:19829c9673638f2a0b8617da4cdcb927e831cd88bcfcb6e78d42a4d1af131520", size = 792418 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/2a/5e5e750890ada51017d18d0d4c30da696e5b5bd3180947729927628fc3cb/tqdm-4.68.4-py3-none-any.whl", hash = "sha256:5168118b2368f48c561afda8020fd79195b1bdb0bdf8086b88442c267a315dc2", size = 676612 }, +] + +[[package]] +name = "tree-sitter" +version = "0.26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/03/5600b84aff2e6c4fe80cfebb4063fe2f50299521befe5f6092ab8c082f4a/tree_sitter-0.26.0.tar.gz", hash = "sha256:b40c219edccc4564530c96f8f1556f6202b37cda964d1cbd7bd2b7e68b40a245", size = 191423 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/18/78aae7e4b5a36daaebb0276e4b07d084d45298758000787838e89329e11f/tree_sitter-0.26.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1d6fe0e8fb4df77b5ee816228e2c4475a63d8cc1d4d3a7ffd7097b2b87fc3e95", size = 148679 }, + { url = "https://files.pythonhosted.org/packages/24/e4/b371b9553b0e47d130fc2073e56cab94fecc868be04666bf5bbd1fcd1cc9/tree_sitter-0.26.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:514a9bf8993e5210e7970736aaf6020d1759b670e195ef17b1c48f586aa30736", size = 140759 }, + { url = "https://files.pythonhosted.org/packages/22/7d/266fb0f2c41e6fb00b0f40e7a3338cdf99651e6a6511ca72bc78fc697636/tree_sitter-0.26.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10f0d4eb94aa7242dcb7f554bcd24dd7ba1c114f00d58759ba08c7a46c8ec51a", size = 637206 }, + { url = "https://files.pythonhosted.org/packages/40/9f/47cf22febb47132d5b3a507a27bb99ef89fe5c8ec420a13c6daa9b64f782/tree_sitter-0.26.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:335294ce0504fcefde5245dff596778ffaf820205b98ae0b549c72e48855f1d8", size = 664758 }, + { url = "https://files.pythonhosted.org/packages/4c/4d/8d144ca3beb46a62a5102b6deac76bb0da55235c2c7840faf3b12f2e9d97/tree_sitter-0.26.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9997ba61368c48ed54e715676afadf703947a1542464e39d047764fb3624b01", size = 647438 }, + { url = "https://files.pythonhosted.org/packages/4d/ed/ed1d6e78520c4fb64ed52fec3f2947bf8c1fbad7bc24e282c56193c9ba42/tree_sitter-0.26.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c56581ad256c4195a21bfe449fed5d44a02fe83a4a7d6e70e6ec302c881191c7", size = 661944 }, + { url = "https://files.pythonhosted.org/packages/10/83/45f5bd43db1b8248d2fd08ef6cbe43e2725c539e09a2cfb8bc2818646788/tree_sitter-0.26.0-cp311-cp311-win_amd64.whl", hash = "sha256:0f8793fd18ad7eec276ed4b51c097b4bf2002b357259b66b0d75db1f3f41c754", size = 129496 }, + { url = "https://files.pythonhosted.org/packages/f1/8d/be68e6c04563eb54145424cc83fe0aa8b0ba6c90d8989cf8a032671b5f16/tree_sitter-0.26.0-cp311-cp311-win_arm64.whl", hash = "sha256:dea4b4e27d49e9ec5b785d4f994da000e6726882fcc6ad05ec98478500c71aef", size = 116484 }, + { url = "https://files.pythonhosted.org/packages/87/ca/565702c44815393e3a973552ad546db4e5ca081ca8698640b4e93d809f51/tree_sitter-0.26.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6cb2bd20efb2544c19ac54486ab7cb8ec7b36f913bbe1ce95df84acb96743d9c", size = 148934 }, + { url = "https://files.pythonhosted.org/packages/54/6f/8bb61957f16ec1b1d92410a006cdc84a952b6352a7313b2ad299f2d21484/tree_sitter-0.26.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:918d89529786873f0982a0f59c2a303cd065fbfd1b903d71a8e4e1584f67b42e", size = 140820 }, + { url = "https://files.pythonhosted.org/packages/78/0a/8a6f08559182643a814a4ab559948ae817b2851890fd9b995a4fff6541ce/tree_sitter-0.26.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:30a88be89ff1f2755297f81e8080d88b795dd98720c3f9fa2acf93873182cc95", size = 638844 }, + { url = "https://files.pythonhosted.org/packages/8a/2f/6e6781b31677231366cb3cf27bc8269157f6d4b03c9032865a4f5f2bbe7e/tree_sitter-0.26.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a6b333b0282d8bb0af741f9b018bd2523d4eecb2686bf6717066a625fecfaa4", size = 667487 }, + { url = "https://files.pythonhosted.org/packages/02/0b/0483078c8567445557a7015b0e5b187f6d7d4fda73464df9c4bdea7f7f3c/tree_sitter-0.26.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3f3c44339dd34fe8eb2b8d5aa7610660499a795f70376b130bbee7a437337280", size = 647975 }, + { url = "https://files.pythonhosted.org/packages/27/68/da83ca72c984e96ab4eb3bee0db1a6ffb5de1c8c455f92bd9f420cde7f0e/tree_sitter-0.26.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:94550e13b6ae576969da40246f4c4abb206380b5375ad43f26dd9151d55438e3", size = 665018 }, + { url = "https://files.pythonhosted.org/packages/d1/36/4d67927fd47b89af4a00f65f55a7370e28778cd50e972c2430487e3ecc27/tree_sitter-0.26.0-cp312-cp312-win_amd64.whl", hash = "sha256:ca89e361a276dbc934b28a43dd881199e25d34ff5493ee0ce45f3c52a6124a37", size = 129619 }, + { url = "https://files.pythonhosted.org/packages/ed/72/cdefad523eb78710679c6da6a79e3d90f5afd32b1c6aa5a17bac7eef99f6/tree_sitter-0.26.0-cp312-cp312-win_arm64.whl", hash = "sha256:bc6cb01d5ee75c85424aa1f1c72a82d8f07fd52539a0f3c4a6ed3e8721079b84", size = 116545 }, + { url = "https://files.pythonhosted.org/packages/cb/b0/465257cf8f972ad9f9812ec1cbaa8ec210ebebb601ade9a15881aa2436b4/tree_sitter-0.26.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ed0889dbed843ce45ede9f5169c0b2dea2222f12685844a03fadb81f12705867", size = 148893 }, + { url = "https://files.pythonhosted.org/packages/a1/ec/19d093e854b45e807fecfdd26105c266f43aeecc39c4dc97992a7074ad5a/tree_sitter-0.26.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6189c6c340c7384357711e3d92645e96bfb79f7a502f86de1ebdb23eb43f7dab", size = 140829 }, + { url = "https://files.pythonhosted.org/packages/9b/ee/87e74671ed63a837e7a1f17ab94aa3913871e033b27523d8e7b83d6f7ad0/tree_sitter-0.26.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ff2e0750b7daa722302838356d7b65e303829b7eb73c915df127ddba115e1d1", size = 639334 }, + { url = "https://files.pythonhosted.org/packages/66/e7/f7e04cd9dff6b6ac0adf23922796fbc76accd4cf4bcda50542748d485679/tree_sitter-0.26.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7075ef857ef86f327dbb72d1e2574dda78db5754b3a1fca6506acd7fe5d561a7", size = 668102 }, + { url = "https://files.pythonhosted.org/packages/d3/90/0bfb16b7894fea728c774a89d5af421a9368a2f913bbd4e8dcab7caaecfb/tree_sitter-0.26.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:26c996c1edfee86e977bb3f5462e74fcec0d0b0db1e85a3c475875763caa03be", size = 648560 }, + { url = "https://files.pythonhosted.org/packages/cd/e6/0fe05ba396e9623b0ae40ccf34171336b8701ec8d7bd0ee9f5224d638665/tree_sitter-0.26.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:00289bfe7978f3e0dc0ce69813a20fa9f44ea4c100b3ec62043e5eb74ccfc3a2", size = 665121 }, + { url = "https://files.pythonhosted.org/packages/eb/d2/a944b1ca35bed6068dc84a9967aaf3049d8cc0b7a36179eea8787270a6ab/tree_sitter-0.26.0-cp313-cp313-win_amd64.whl", hash = "sha256:93e220cab7e6a823efeb2046c49171427de92ef71c7c681c01820d14d8d3721f", size = 129615 }, + { url = "https://files.pythonhosted.org/packages/09/ef/c7ca48293580d2249f36940c4eed5b4ddeb9ce75baf9a4ef30621987e0c7/tree_sitter-0.26.0-cp313-cp313-win_arm64.whl", hash = "sha256:b31a8195d2f224224c530ac814632d98c1dcc123d227442c07c736e86b70d564", size = 116525 }, + { url = "https://files.pythonhosted.org/packages/c5/7a/4d84e6f6ae2c3e757490dd84de251712c31e293dfe31f28da1ec019cefa2/tree_sitter-0.26.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5a3c93a352b7e6f70f73e121bbfa2d0117ba7478bd51114ed35c91b0b78814fa", size = 148901 }, + { url = "https://files.pythonhosted.org/packages/b0/d9/efe62ec65dc9d096e834d27b8c058127e2146e42ff3380b822a233f016a6/tree_sitter-0.26.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5fc2f41bf246ff2f70a9cc3690be35ec7580a4923151873d898c8bcb1a4503d3", size = 140805 }, + { url = "https://files.pythonhosted.org/packages/c4/2c/c82326b7b97e3c485c18679883b16f89e5e913c639d3b219d3da70c9e67e/tree_sitter-0.26.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8ea92a255c91671a7ec4625aba3ab7bb5220c423630ffbf83c45d7312abe084", size = 640586 }, + { url = "https://files.pythonhosted.org/packages/e2/7a/f56e7d8282859452611024c7cbc623bfba5b24b8cb9b8f8bc88c5219fe9a/tree_sitter-0.26.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f665510f0fcf4636fb9696f1f7853bed7a3bd764b7bb0cb8494e619c14ed5a0c", size = 668300 }, + { url = "https://files.pythonhosted.org/packages/91/51/240ee81b9d5e9ca0a6cb1528e8605ffa70ab58c89ce126631be96d3e4bae/tree_sitter-0.26.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:253df7ab82cc0a9d311cd65f06e9f99fb3eac55996ae9fc94da22f123a861b90", size = 649627 }, + { url = "https://files.pythonhosted.org/packages/6a/54/760035cefedf9eb44f0f84c4ac22f1322e73155853e272576ee876336312/tree_sitter-0.26.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ff80d4833d330a73184a3ac5132abe93c575d2dea31975c6f15c0d21fef238aa", size = 664885 }, + { url = "https://files.pythonhosted.org/packages/c9/1b/0b36fe2a984ecedc4ce6aefd5d56447a6626a8e9b595c4e48658510ce8f8/tree_sitter-0.26.0-cp314-cp314-win_amd64.whl", hash = "sha256:a4033fecc8f606c7f2e8b8014d0057b74668a7f0152763606f7bc25c5f9ec64c", size = 132688 }, + { url = "https://files.pythonhosted.org/packages/4d/74/ebc041a13fbf40144afdb0d4b447e48e0b4012ca866c63de8b48f801f0c1/tree_sitter-0.26.0-cp314-cp314-win_arm64.whl", hash = "sha256:823251c4b6725a7c03ed497a339135ede7ae4bdde75bb8be7ef5e305aeb4ff52", size = 120287 }, ] [[package]] name = "tree-sitter-language-pack" -version = "1.8.0" +version = "1.12.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1c/f2/4bbb9de167eaf08be24360794d3aa6e22f8e24c3067bf5427e1fed66229d/tree_sitter_language_pack-1.8.0.tar.gz", hash = "sha256:347ffbe81b8959a98ed86e966319fa50075a235c24b248a11a9c0c2e0bf61152", size = 101042 } +dependencies = [ + { name = "tree-sitter" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/87/d0/06624b30acf3100ed663d604ac893882beafde603971e76f1160808ae02c/tree_sitter_language_pack-1.12.5.tar.gz", hash = "sha256:e6ca8fd7f1cc24496f2fff73156d0ce20284ce56e18a84f4c0c10388a1e4c951", size = 81955 } wheels = [ - { url = "https://files.pythonhosted.org/packages/58/ec/60b4ebc77852db89eeb3e977b93a76d1e50ae2ea70de6c94394d34089f5c/tree_sitter_language_pack-1.8.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:66b4187977d2b0dbcd40c935e54151d3834018c85d61fe2feb4571abb381ffd9", size = 2060108 }, - { url = "https://files.pythonhosted.org/packages/4f/3f/354ff05c019df09f1a6fa2959309b001ff4eb72f0806e270d1d2a7eefccb/tree_sitter_language_pack-1.8.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:ddee0ad0310d8243bc508064364cd876ab3a12208973c8b64617b2ca38ce1512", size = 1920225 }, - { url = "https://files.pythonhosted.org/packages/13/68/96b7e7a59dcda431bd13827a544f469833bae896771f4819d52a730490b4/tree_sitter_language_pack-1.8.0-cp310-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:01a68bb671683a4d4dafd411f4f02ffd6094f920f7ffa9e7b2014d2d8c97082e", size = 2056525 }, - { url = "https://files.pythonhosted.org/packages/ef/ad/9ab7967a14fe92f179230c0963c7f4b57925b3bac5200c489b205f324ac4/tree_sitter_language_pack-1.8.0-cp310-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:bc8278bdfda628c980ee91d60f09359bb52cddc540584e6e9a16fb584be5e833", size = 2172007 }, - { url = "https://files.pythonhosted.org/packages/6d/79/a72edce56461bd86c952cb2c211ae63b3d256588b4bdefc3823eccb3f9db/tree_sitter_language_pack-1.8.0-cp310-abi3-win_amd64.whl", hash = "sha256:696450f48dbc802707a73e090d31b7fe40d7e2a0361aae864087383e8b23624b", size = 1965295 }, - { url = "https://files.pythonhosted.org/packages/09/02/b22d09acddec09f2621f52b0b2c2516d999b89f5e63a417c62f0be1d176d/tree_sitter_language_pack-1.8.0-cp310-abi3-win_arm64.whl", hash = "sha256:aae4533fa7718d62b4f6795d8db3700d52f8e0d2fb501da848fc238e9b021db7", size = 1874472 }, + { url = "https://files.pythonhosted.org/packages/be/b9/015240f1e091f0d7608582e2930b62580839f4b3e94b5813bc1894017cf3/tree_sitter_language_pack-1.12.5-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c3613f26f1660d757626b50e2f7fd50ce269193ec9e8e0ad699ae47bf03fd664", size = 2140086 }, + { url = "https://files.pythonhosted.org/packages/08/fd/8ca53b11ea1058c920944b52b41c4c4d96ccf67c62b15174f6d3d2a0126f/tree_sitter_language_pack-1.12.5-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:30010156a06179e7c179ae51ba6f2da148bd0bc364f750bcc5ef81ab1c4e0c53", size = 2031270 }, + { url = "https://files.pythonhosted.org/packages/03/e1/af79d0d302f1f47f190fd1f09146a6e5f88a32a7c0d0759820d7ca9aafc3/tree_sitter_language_pack-1.12.5-cp310-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:1609b9cc2518e4657f6ef80a8ac533a8f5a04d4a74135d744fb171eca434e6eb", size = 2189197 }, + { url = "https://files.pythonhosted.org/packages/25/da/6e3db4ad85802747ead34f46f402508d796cd11be65b79d6ba6d76281a3f/tree_sitter_language_pack-1.12.5-cp310-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:de5ec0c1b184176443fc6093f07fe5a1c403ee51ec8593bde372bd5960819941", size = 2299397 }, + { url = "https://files.pythonhosted.org/packages/19/fb/ac271b8013ff4f386ddce86cab080b8c6352d3efa45e8b2285d246eed318/tree_sitter_language_pack-1.12.5-cp310-abi3-win_amd64.whl", hash = "sha256:eb58ecee144247d8cde52a567bbe5c6f950ed1d2c416295e00f0e31389f1751e", size = 2073479 }, + { url = "https://files.pythonhosted.org/packages/79/80/fbe56f84daec7fec163023f6e8865969e8873ec42b85241c750953320e1f/tree_sitter_language_pack-1.12.5-cp310-abi3-win_arm64.whl", hash = "sha256:41399bdd45eb4ced5840ddf1d44aa02fe69763c867bd7286aa20d182bfe397af", size = 1984602 }, ] [[package]] name = "trimesh" -version = "4.11.5" +version = "4.12.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0d/bf/53b69f3b6708c20ceb4d1d1250c7dc205733eb646659e5e55771f76ffabd/trimesh-4.11.5.tar.gz", hash = "sha256:b90e6cdd6ada51c52d4a7d32947f4ce44b6751c5b7cab2b04e271ecea1e397d3", size = 836449 } +sdist = { url = "https://files.pythonhosted.org/packages/79/37/5cb90f04990260d2caceb6093560c6cefafca1ec522c1e43be01ca658244/trimesh-4.12.2.tar.gz", hash = "sha256:c8ca31571ac00b112e4e160e66a2d4c3491df321f056bd33806be0485d1af9d9", size = 842220 } wheels = [ - { url = "https://files.pythonhosted.org/packages/24/83/72e812f772daee66651f468c7b2535fa05eac27db26df7e614cae823c832/trimesh-4.11.5-py3-none-any.whl", hash = "sha256:b225a94c8af79569f7167ca7eaaab4fd05c260da58a075599453d655835258ef", size = 740833 }, + { url = "https://files.pythonhosted.org/packages/05/98/716a473cfb24750858ddd5d14e6527539dd206583a46408d08eeb2844a75/trimesh-4.12.2-py3-none-any.whl", hash = "sha256:b5b5afa63c5272345f2858f7676bc8c217dc8a89f4fadf6193fe10a81b5ff2aa", size = 741043 }, ] [[package]] name = "typer" -version = "0.24.1" +version = "0.27.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-doc" }, - { name = "click" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, { 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 } +sdist = { url = "https://files.pythonhosted.org/packages/37/78/fda3361b56efc27944f24225f6ecd13d96d6fcfe37bd0eb34e2f4c63f9fc/typer-0.27.0.tar.gz", hash = "sha256:629bd12ea5d13a17148125d9a264f949eb171fb3f120f9b04d85873cab054fa5", size = 203430 } wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl", hash = "sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e", size = 56085 }, + { url = "https://files.pythonhosted.org/packages/40/03/26a383c9e58c213199d1aad1c3d353cfc22d4444ec6d2c0bf8ad02523843/typer-0.27.0-py3-none-any.whl", hash = "sha256:6f4b27631e47f077871b7dc30e933ec0131c1390fbe0e387ea5574b5bac9ccf1", size = 122716 }, ] [[package]] name = "types-pyyaml" -version = "6.0.12.20250915" +version = "6.0.12.20260518" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7e/69/3c51b36d04da19b92f9e815be12753125bd8bc247ba0470a982e6979e71c/types_pyyaml-6.0.12.20250915.tar.gz", hash = "sha256:0f8b54a528c303f0e6f7165687dd33fafa81c807fcac23f632b63aa624ced1d3", size = 17522 } +sdist = { url = "https://files.pythonhosted.org/packages/b8/83/4a1afc3fbfcf5b8d46fc390cd95ed6b0dc9010a265f4e9f46314efffa37a/types_pyyaml-6.0.12.20260518.tar.gz", hash = "sha256:d917f83fb38462550338c1297faedd860b3ec83912b96b1e3d73255f7473e466", size = 17850 } wheels = [ - { url = "https://files.pythonhosted.org/packages/bd/e0/1eed384f02555dde685fff1a1ac805c1c7dcb6dd019c916fe659b1c1f9ec/types_pyyaml-6.0.12.20250915-py3-none-any.whl", hash = "sha256:e7d4d9e064e89a3b3cae120b4990cd370874d2bf12fa5f46c97018dd5d3c9ab6", size = 20338 }, + { url = "https://files.pythonhosted.org/packages/06/a2/c01db32be2ae7d6a1689972f3c492b149ee4e164b12fdfd9f64b50888215/types_pyyaml-6.0.12.20260518-py3-none-any.whl", hash = "sha256:d2150f75a231c9fe9c7463bd29487d93e60bac90400287351384bc2284eba7cd", size = 20312 }, ] [[package]] name = "typing-extensions" -version = "4.15.0" +version = "4.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391 } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555 } wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614 }, + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571 }, ] [[package]] @@ -4268,11 +4543,11 @@ wheels = [ [[package]] name = "tzdata" -version = "2025.3" +version = "2026.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 } +sdist = { url = "https://files.pythonhosted.org/packages/92/ff/5a28bdfd8c3ebec42564ac7d0e54ca3db65044a9314a97f9564fa7a1e926/tzdata-2026.3.tar.gz", hash = "sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415", size = 198674 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521 }, + { url = "https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl", hash = "sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931", size = 348168 }, ] [[package]] @@ -4318,20 +4593,20 @@ wheels = [ [[package]] name = "uvicorn" -version = "0.41.0" +version = "0.51.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 } +sdist = { url = "https://files.pythonhosted.org/packages/a2/65/b7c6c443ccc58678c91e1e973bbe2a878591538655d6e1d47f24ba1c51f3/uvicorn-0.51.0.tar.gz", hash = "sha256:f6f4b69b657c312f516dd2d268ab9ae6f254b11e4bac504f37b2ab58b24dd0b0", size = 94412 } wheels = [ - { url = "https://files.pythonhosted.org/packages/83/e4/d04a086285c20886c0daad0e026f250869201013d18f81d9ff5eada73a88/uvicorn-0.41.0-py3-none-any.whl", hash = "sha256:29e35b1d2c36a04b9e180d4007ede3bcb32a85fbdfd6c6aeb3f26839de088187", size = 68783 }, + { url = "https://files.pythonhosted.org/packages/45/ec/dbb7e5a6b91f86bfb9eb7d2988a2730907b6a729875b949c7f022e8b88fa/uvicorn-0.51.0-py3-none-any.whl", hash = "sha256:5d38af6cd620f2ae3849fb44fd4879e0890aa1febe8d47eb355fb45d93fe6a5b", size = 73219 }, ] [[package]] name = "virtualenv" -version = "21.5.1" +version = "21.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, @@ -4339,9 +4614,9 @@ dependencies = [ { name = "platformdirs" }, { name = "python-discovery" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f1/a5/81f987504738e6defeed61ec1c47e2aefab3c35d8eeb87e1b3f38cf28254/virtualenv-21.5.1.tar.gz", hash = "sha256:dca3bf98275a59c652b69d68e73433e597d977c2da9198882479d1a7188009c8", size = 4578798 } +sdist = { url = "https://files.pythonhosted.org/packages/34/d9/b477fddb68840b570af8b22afe9b035cbc277b5fb7b33dea390617a8b10f/virtualenv-21.6.1.tar.gz", hash = "sha256:15f978b7cd329f24855ff4a0c4b4899cc7678589f49adbdcbbb4d3232e641128", size = 5526620 } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/02/3623e6169bed617ed1e2d372f7c69f92ec28d54c4dfc997055c8578ec148/virtualenv-21.5.1-py3-none-any.whl", hash = "sha256:55aa670b67bbfb991b03fda39bd3276d92c419d702376e98c5df1c9989a26783", size = 4558820 }, + { url = "https://files.pythonhosted.org/packages/c1/7c/4e7225d46d634a0d8d534dd8a6ce0c319d09b4d0cf0337eb314ca4789d8c/virtualenv-21.6.1-py3-none-any.whl", hash = "sha256:afe991df855715a2b2f60edfcc0107ef95a79fdfd8cb4cdaa71603d1c12e463b", size = 5506392 }, ] [[package]] @@ -4378,321 +4653,346 @@ wheels = [ [[package]] name = "werkzeug" -version = "3.1.7" +version = "3.1.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b5/43/76ded108b296a49f52de6bac5192ca1c4be84e886f9b5c9ba8427d9694fd/werkzeug-3.1.7.tar.gz", hash = "sha256:fb8c01fe6ab13b9b7cdb46892b99b1d66754e1d7ab8e542e865ec13f526b5351", size = 875700 } +sdist = { url = "https://files.pythonhosted.org/packages/dd/b2/381be8cfdee792dd117872481b6e378f85c957dd7c5bca38897b08f765fd/werkzeug-3.1.8.tar.gz", hash = "sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44", size = 875852 } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/b2/0bba9bbb4596d2d2f285a16c2ab04118f6b957d8441566e1abb892e6a6b2/werkzeug-3.1.7-py3-none-any.whl", hash = "sha256:4b314d81163a3e1a169b6a0be2a000a0e204e8873c5de6586f453c55688d422f", size = 226295 }, + { url = "https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl", hash = "sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50", size = 226459 }, ] [[package]] name = "wrapt" -version = "2.1.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2e/64/925f213fdcbb9baeb1530449ac71a4d57fc361c053d06bf78d0c5c7cd80c/wrapt-2.1.2.tar.gz", hash = "sha256:3996a67eecc2c68fd47b4e3c564405a5777367adfd9b8abb58387b63ee83b21e", size = 81678 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/81/60c4471fce95afa5922ca09b88a25f03c93343f759aae0f31fb4412a85c7/wrapt-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:96159a0ee2b0277d44201c3b5be479a9979cf154e8c82fa5df49586a8e7679bb", size = 60666 }, - { url = "https://files.pythonhosted.org/packages/6b/be/80e80e39e7cb90b006a0eaf11c73ac3a62bbfb3068469aec15cc0bc795de/wrapt-2.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:98ba61833a77b747901e9012072f038795de7fc77849f1faa965464f3f87ff2d", size = 61601 }, - { url = "https://files.pythonhosted.org/packages/b0/be/d7c88cd9293c859fc74b232abdc65a229bb953997995d6912fc85af18323/wrapt-2.1.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:767c0dbbe76cae2a60dd2b235ac0c87c9cccf4898aef8062e57bead46b5f6894", size = 114057 }, - { url = "https://files.pythonhosted.org/packages/ea/25/36c04602831a4d685d45a93b3abea61eca7fe35dab6c842d6f5d570ef94a/wrapt-2.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c691a6bc752c0cc4711cc0c00896fcd0f116abc253609ef64ef930032821842", size = 116099 }, - { url = "https://files.pythonhosted.org/packages/5c/4e/98a6eb417ef551dc277bec1253d5246b25003cf36fdf3913b65cb7657a56/wrapt-2.1.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f3b7d73012ea75aee5844de58c88f44cf62d0d62711e39da5a82824a7c4626a8", size = 112457 }, - { url = "https://files.pythonhosted.org/packages/cb/a6/a6f7186a5297cad8ec53fd7578533b28f795fdf5372368c74bd7e6e9841c/wrapt-2.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:577dff354e7acd9d411eaf4bfe76b724c89c89c8fc9b7e127ee28c5f7bcb25b6", size = 115351 }, - { url = "https://files.pythonhosted.org/packages/97/6f/06e66189e721dbebd5cf20e138acc4d1150288ce118462f2fcbff92d38db/wrapt-2.1.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3d7b6fd105f8b24e5bd23ccf41cb1d1099796524bcc6f7fbb8fe576c44befbc9", size = 111748 }, - { url = "https://files.pythonhosted.org/packages/ef/43/4808b86f499a51370fbdbdfa6cb91e9b9169e762716456471b619fca7a70/wrapt-2.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:866abdbf4612e0b34764922ef8b1c5668867610a718d3053d59e24a5e5fcfc15", size = 113783 }, - { url = "https://files.pythonhosted.org/packages/91/2c/a3f28b8fa7ac2cefa01cfcaca3471f9b0460608d012b693998cd61ef43df/wrapt-2.1.2-cp311-cp311-win32.whl", hash = "sha256:5a0a0a3a882393095573344075189eb2d566e0fd205a2b6414e9997b1b800a8b", size = 57977 }, - { url = "https://files.pythonhosted.org/packages/3f/c3/2b1c7bd07a27b1db885a2fab469b707bdd35bddf30a113b4917a7e2139d2/wrapt-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:64a07a71d2730ba56f11d1a4b91f7817dc79bc134c11516b75d1921a7c6fcda1", size = 60336 }, - { url = "https://files.pythonhosted.org/packages/ec/5c/76ece7b401b088daa6503d6264dd80f9a727df3e6042802de9a223084ea2/wrapt-2.1.2-cp311-cp311-win_arm64.whl", hash = "sha256:b89f095fe98bc12107f82a9f7d570dc83a0870291aeb6b1d7a7d35575f55d98a", size = 58756 }, - { url = "https://files.pythonhosted.org/packages/4c/b6/1db817582c49c7fcbb7df6809d0f515af29d7c2fbf57eb44c36e98fb1492/wrapt-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ff2aad9c4cda28a8f0653fc2d487596458c2a3f475e56ba02909e950a9efa6a9", size = 61255 }, - { url = "https://files.pythonhosted.org/packages/a2/16/9b02a6b99c09227c93cd4b73acc3678114154ec38da53043c0ddc1fba0dc/wrapt-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6433ea84e1cfacf32021d2a4ee909554ade7fd392caa6f7c13f1f4bf7b8e8748", size = 61848 }, - { url = "https://files.pythonhosted.org/packages/af/aa/ead46a88f9ec3a432a4832dfedb84092fc35af2d0ba40cd04aea3889f247/wrapt-2.1.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c20b757c268d30d6215916a5fa8461048d023865d888e437fab451139cad6c8e", size = 121433 }, - { url = "https://files.pythonhosted.org/packages/3a/9f/742c7c7cdf58b59085a1ee4b6c37b013f66ac33673a7ef4aaed5e992bc33/wrapt-2.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79847b83eb38e70d93dc392c7c5b587efe65b3e7afcc167aa8abd5d60e8761c8", size = 123013 }, - { url = "https://files.pythonhosted.org/packages/e8/44/2c3dd45d53236b7ed7c646fcf212251dc19e48e599debd3926b52310fafb/wrapt-2.1.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f8fba1bae256186a83d1875b2b1f4e2d1242e8fac0f58ec0d7e41b26967b965c", size = 117326 }, - { url = "https://files.pythonhosted.org/packages/74/e2/b17d66abc26bd96f89dec0ecd0ef03da4a1286e6ff793839ec431b9fae57/wrapt-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e3d3b35eedcf5f7d022291ecd7533321c4775f7b9cd0050a31a68499ba45757c", size = 121444 }, - { url = "https://files.pythonhosted.org/packages/3c/62/e2977843fdf9f03daf1586a0ff49060b1b2fc7ff85a7ea82b6217c1ae36e/wrapt-2.1.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:6f2c5390460de57fa9582bc8a1b7a6c86e1a41dfad74c5225fc07044c15cc8d1", size = 116237 }, - { url = "https://files.pythonhosted.org/packages/88/dd/27fc67914e68d740bce512f11734aec08696e6b17641fef8867c00c949fc/wrapt-2.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7dfa9f2cf65d027b951d05c662cc99ee3bd01f6e4691ed39848a7a5fffc902b2", size = 120563 }, - { url = "https://files.pythonhosted.org/packages/ec/9f/b750b3692ed2ef4705cb305bd68858e73010492b80e43d2a4faa5573cbe7/wrapt-2.1.2-cp312-cp312-win32.whl", hash = "sha256:eba8155747eb2cae4a0b913d9ebd12a1db4d860fc4c829d7578c7b989bd3f2f0", size = 58198 }, - { url = "https://files.pythonhosted.org/packages/8e/b2/feecfe29f28483d888d76a48f03c4c4d8afea944dbee2b0cd3380f9df032/wrapt-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1c51c738d7d9faa0b3601708e7e2eda9bf779e1b601dce6c77411f2a1b324a63", size = 60441 }, - { url = "https://files.pythonhosted.org/packages/44/e1/e328f605d6e208547ea9fd120804fcdec68536ac748987a68c47c606eea8/wrapt-2.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:c8e46ae8e4032792eb2f677dbd0d557170a8e5524d22acc55199f43efedd39bf", size = 58836 }, - { url = "https://files.pythonhosted.org/packages/4c/7a/d936840735c828b38d26a854e85d5338894cda544cb7a85a9d5b8b9c4df7/wrapt-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787fd6f4d67befa6fe2abdffcbd3de2d82dfc6fb8a6d850407c53332709d030b", size = 61259 }, - { url = "https://files.pythonhosted.org/packages/5e/88/9a9b9a90ac8ca11c2fdb6a286cb3a1fc7dd774c00ed70929a6434f6bc634/wrapt-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4bdf26e03e6d0da3f0e9422fd36bcebf7bc0eeb55fdf9c727a09abc6b9fe472e", size = 61851 }, - { url = "https://files.pythonhosted.org/packages/03/a9/5b7d6a16fd6533fed2756900fc8fc923f678179aea62ada6d65c92718c00/wrapt-2.1.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bbac24d879aa22998e87f6b3f481a5216311e7d53c7db87f189a7a0266dafffb", size = 121446 }, - { url = "https://files.pythonhosted.org/packages/45/bb/34c443690c847835cfe9f892be78c533d4f32366ad2888972c094a897e39/wrapt-2.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:16997dfb9d67addc2e3f41b62a104341e80cac52f91110dece393923c0ebd5ca", size = 123056 }, - { url = "https://files.pythonhosted.org/packages/93/b9/ff205f391cb708f67f41ea148545f2b53ff543a7ac293b30d178af4d2271/wrapt-2.1.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:162e4e2ba7542da9027821cb6e7c5e068d64f9a10b5f15512ea28e954893a267", size = 117359 }, - { url = "https://files.pythonhosted.org/packages/1f/3d/1ea04d7747825119c3c9a5e0874a40b33594ada92e5649347c457d982805/wrapt-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f29c827a8d9936ac320746747a016c4bc66ef639f5cd0d32df24f5eacbf9c69f", size = 121479 }, - { url = "https://files.pythonhosted.org/packages/78/cc/ee3a011920c7a023b25e8df26f306b2484a531ab84ca5c96260a73de76c0/wrapt-2.1.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:a9dd9813825f7ecb018c17fd147a01845eb330254dff86d3b5816f20f4d6aaf8", size = 116271 }, - { url = "https://files.pythonhosted.org/packages/98/fd/e5ff7ded41b76d802cf1191288473e850d24ba2e39a6ec540f21ae3b57cb/wrapt-2.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6f8dbdd3719e534860d6a78526aafc220e0241f981367018c2875178cf83a413", size = 120573 }, - { url = "https://files.pythonhosted.org/packages/47/c5/242cae3b5b080cd09bacef0591691ba1879739050cc7c801ff35c8886b66/wrapt-2.1.2-cp313-cp313-win32.whl", hash = "sha256:5c35b5d82b16a3bc6e0a04349b606a0582bc29f573786aebe98e0c159bc48db6", size = 58205 }, - { url = "https://files.pythonhosted.org/packages/12/69/c358c61e7a50f290958809b3c61ebe8b3838ea3e070d7aac9814f95a0528/wrapt-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:f8bc1c264d8d1cf5b3560a87bbdd31131573eb25f9f9447bb6252b8d4c44a3a1", size = 60452 }, - { url = "https://files.pythonhosted.org/packages/8e/66/c8a6fcfe321295fd8c0ab1bd685b5a01462a9b3aa2f597254462fc2bc975/wrapt-2.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:3beb22f674550d5634642c645aba4c72a2c66fb185ae1aebe1e955fae5a13baf", size = 58842 }, - { url = "https://files.pythonhosted.org/packages/da/55/9c7052c349106e0b3f17ae8db4b23a691a963c334de7f9dbd60f8f74a831/wrapt-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0fc04bc8664a8bc4c8e00b37b5355cffca2535209fba1abb09ae2b7c76ddf82b", size = 63075 }, - { url = "https://files.pythonhosted.org/packages/09/a8/ce7b4006f7218248dd71b7b2b732d0710845a0e49213b18faef64811ffef/wrapt-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a9b9d50c9af998875a1482a038eb05755dfd6fe303a313f6a940bb53a83c3f18", size = 63719 }, - { url = "https://files.pythonhosted.org/packages/e4/e5/2ca472e80b9e2b7a17f106bb8f9df1db11e62101652ce210f66935c6af67/wrapt-2.1.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2d3ff4f0024dd224290c0eabf0240f1bfc1f26363431505fb1b0283d3b08f11d", size = 152643 }, - { url = "https://files.pythonhosted.org/packages/36/42/30f0f2cefca9d9cbf6835f544d825064570203c3e70aa873d8ae12e23791/wrapt-2.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3278c471f4468ad544a691b31bb856374fbdefb7fee1a152153e64019379f015", size = 158805 }, - { url = "https://files.pythonhosted.org/packages/bb/67/d08672f801f604889dcf58f1a0b424fe3808860ede9e03affc1876b295af/wrapt-2.1.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8914c754d3134a3032601c6984db1c576e6abaf3fc68094bb8ab1379d75ff92", size = 145990 }, - { url = "https://files.pythonhosted.org/packages/68/a7/fd371b02e73babec1de6ade596e8cd9691051058cfdadbfd62a5898f3295/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ff95d4264e55839be37bafe1536db2ab2de19da6b65f9244f01f332b5286cfbf", size = 155670 }, - { url = "https://files.pythonhosted.org/packages/86/2d/9fe0095dfdb621009f40117dcebf41d7396c2c22dca6eac779f4c007b86c/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:76405518ca4e1b76fbb1b9f686cff93aebae03920cc55ceeec48ff9f719c5f67", size = 144357 }, - { url = "https://files.pythonhosted.org/packages/0e/b6/ec7b4a254abbe4cde9fa15c5d2cca4518f6b07d0f1b77d4ee9655e30280e/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c0be8b5a74c5824e9359b53e7e58bef71a729bacc82e16587db1c4ebc91f7c5a", size = 150269 }, - { url = "https://files.pythonhosted.org/packages/6e/6b/2fabe8ebf148f4ee3c782aae86a795cc68ffe7d432ef550f234025ce0cfa/wrapt-2.1.2-cp313-cp313t-win32.whl", hash = "sha256:f01277d9a5fc1862f26f7626da9cf443bebc0abd2f303f41c5e995b15887dabd", size = 59894 }, - { url = "https://files.pythonhosted.org/packages/ca/fb/9ba66fc2dedc936de5f8073c0217b5d4484e966d87723415cc8262c5d9c2/wrapt-2.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:84ce8f1c2104d2f6daa912b1b5b039f331febfeee74f8042ad4e04992bd95c8f", size = 63197 }, - { url = "https://files.pythonhosted.org/packages/c0/1c/012d7423c95d0e337117723eb8ecf73c622ce15a97847e84cf3f8f26cd7e/wrapt-2.1.2-cp313-cp313t-win_arm64.whl", hash = "sha256:a93cd767e37faeddbe07d8fc4212d5cba660af59bdb0f6372c93faaa13e6e679", size = 60363 }, - { url = "https://files.pythonhosted.org/packages/39/25/e7ea0b417db02bb796182a5316398a75792cd9a22528783d868755e1f669/wrapt-2.1.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1370e516598854e5b4366e09ce81e08bfe94d42b0fd569b88ec46cc56d9164a9", size = 61418 }, - { url = "https://files.pythonhosted.org/packages/ec/0f/fa539e2f6a770249907757eaeb9a5ff4deb41c026f8466c1c6d799088a9b/wrapt-2.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6de1a3851c27e0bd6a04ca993ea6f80fc53e6c742ee1601f486c08e9f9b900a9", size = 61914 }, - { url = "https://files.pythonhosted.org/packages/53/37/02af1867f5b1441aaeda9c82deed061b7cd1372572ddcd717f6df90b5e93/wrapt-2.1.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:de9f1a2bbc5ac7f6012ec24525bdd444765a2ff64b5985ac6e0692144838542e", size = 120417 }, - { url = "https://files.pythonhosted.org/packages/c3/b7/0138a6238c8ba7476c77cf786a807f871672b37f37a422970342308276e7/wrapt-2.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:970d57ed83fa040d8b20c52fe74a6ae7e3775ae8cff5efd6a81e06b19078484c", size = 122797 }, - { url = "https://files.pythonhosted.org/packages/e1/ad/819ae558036d6a15b7ed290d5b14e209ca795dd4da9c58e50c067d5927b0/wrapt-2.1.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3969c56e4563c375861c8df14fa55146e81ac11c8db49ea6fb7f2ba58bc1ff9a", size = 117350 }, - { url = "https://files.pythonhosted.org/packages/8b/2d/afc18dc57a4600a6e594f77a9ae09db54f55ba455440a54886694a84c71b/wrapt-2.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:57d7c0c980abdc5f1d98b11a2aa3bb159790add80258c717fa49a99921456d90", size = 121223 }, - { url = "https://files.pythonhosted.org/packages/b9/5b/5ec189b22205697bc56eb3b62aed87a1e0423e9c8285d0781c7a83170d15/wrapt-2.1.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:776867878e83130c7a04237010463372e877c1c994d449ca6aaafeab6aab2586", size = 116287 }, - { url = "https://files.pythonhosted.org/packages/f7/2d/f84939a7c9b5e6cdd8a8d0f6a26cabf36a0f7e468b967720e8b0cd2bdf69/wrapt-2.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fab036efe5464ec3291411fabb80a7a39e2dd80bae9bcbeeca5087fdfa891e19", size = 119593 }, - { url = "https://files.pythonhosted.org/packages/0b/fe/ccd22a1263159c4ac811ab9374c061bcb4a702773f6e06e38de5f81a1bdc/wrapt-2.1.2-cp314-cp314-win32.whl", hash = "sha256:e6ed62c82ddf58d001096ae84ce7f833db97ae2263bff31c9b336ba8cfe3f508", size = 58631 }, - { url = "https://files.pythonhosted.org/packages/65/0a/6bd83be7bff2e7efaac7b4ac9748da9d75a34634bbbbc8ad077d527146df/wrapt-2.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:467e7c76315390331c67073073d00662015bb730c566820c9ca9b54e4d67fd04", size = 60875 }, - { url = "https://files.pythonhosted.org/packages/6c/c0/0b3056397fe02ff80e5a5d72d627c11eb885d1ca78e71b1a5c1e8c7d45de/wrapt-2.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:da1f00a557c66225d53b095a97eace0fc5349e3bfda28fa34ffae238978ee575", size = 59164 }, - { url = "https://files.pythonhosted.org/packages/71/ed/5d89c798741993b2371396eb9d4634f009ff1ad8a6c78d366fe2883ea7a6/wrapt-2.1.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:62503ffbc2d3a69891cf29beeaccdb4d5e0a126e2b6a851688d4777e01428dbb", size = 63163 }, - { url = "https://files.pythonhosted.org/packages/c6/8c/05d277d182bf36b0a13d6bd393ed1dec3468a25b59d01fba2dd70fe4d6ae/wrapt-2.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c7e6cd120ef837d5b6f860a6ea3745f8763805c418bb2f12eeb1fa6e25f22d22", size = 63723 }, - { url = "https://files.pythonhosted.org/packages/f4/27/6c51ec1eff4413c57e72d6106bb8dec6f0c7cdba6503d78f0fa98767bcc9/wrapt-2.1.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3769a77df8e756d65fbc050333f423c01ae012b4f6731aaf70cf2bef61b34596", size = 152652 }, - { url = "https://files.pythonhosted.org/packages/db/4c/d7dd662d6963fc7335bfe29d512b02b71cdfa23eeca7ab3ac74a67505deb/wrapt-2.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a76d61a2e851996150ba0f80582dd92a870643fa481f3b3846f229de88caf044", size = 158807 }, - { url = "https://files.pythonhosted.org/packages/b4/4d/1e5eea1a78d539d346765727422976676615814029522c76b87a95f6bcdd/wrapt-2.1.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6f97edc9842cf215312b75fe737ee7c8adda75a89979f8e11558dfff6343cc4b", size = 146061 }, - { url = "https://files.pythonhosted.org/packages/89/bc/62cabea7695cd12a288023251eeefdcb8465056ddaab6227cb78a2de005b/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4006c351de6d5007aa33a551f600404ba44228a89e833d2fadc5caa5de8edfbf", size = 155667 }, - { url = "https://files.pythonhosted.org/packages/e9/99/6f2888cd68588f24df3a76572c69c2de28287acb9e1972bf0c83ce97dbc1/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a9372fc3639a878c8e7d87e1556fa209091b0a66e912c611e3f833e2c4202be2", size = 144392 }, - { url = "https://files.pythonhosted.org/packages/40/51/1dfc783a6c57971614c48e361a82ca3b6da9055879952587bc99fe1a7171/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3144b027ff30cbd2fca07c0a87e67011adb717eb5f5bd8496325c17e454257a3", size = 150296 }, - { url = "https://files.pythonhosted.org/packages/6c/38/cbb8b933a0201076c1f64fc42883b0023002bdc14a4964219154e6ff3350/wrapt-2.1.2-cp314-cp314t-win32.whl", hash = "sha256:3b8d15e52e195813efe5db8cec156eebe339aaf84222f4f4f051a6c01f237ed7", size = 60539 }, - { url = "https://files.pythonhosted.org/packages/82/dd/e5176e4b241c9f528402cebb238a36785a628179d7d8b71091154b3e4c9e/wrapt-2.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:08ffa54146a7559f5b8df4b289b46d963a8e74ed16ba3687f99896101a3990c5", size = 63969 }, - { url = "https://files.pythonhosted.org/packages/5c/99/79f17046cf67e4a95b9987ea129632ba8bcec0bc81f3fb3d19bdb0bd60cd/wrapt-2.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:72aaa9d0d8e4ed0e2e98019cea47a21f823c9dd4b43c7b77bba6679ffcca6a00", size = 60554 }, - { url = "https://files.pythonhosted.org/packages/1a/c7/8528ac2dfa2c1e6708f647df7ae144ead13f0a31146f43c7264b4942bf12/wrapt-2.1.2-py3-none-any.whl", hash = "sha256:b8fd6fa2b2c4e7621808f8c62e8317f4aae56e59721ad933bac5239d913cf0e8", size = 43993 }, +version = "2.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/a4/282c8e64300a59fc834518a54bf0afabb4ff9218b5fa76958b450459a844/wrapt-2.2.2.tar.gz", hash = "sha256:0788e321027c999bf221b667bd4a54aaefd1a36283749a860ac3eb77daed0302", size = 129068 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/15/0c2d55168707465abfc41f33c0b23d792a5fa9b65c26983606940900a120/wrapt-2.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f1a2ff355ece6a111ca7a20dc86df6659c9205d3fcee674ca34f2a2854fd4e73", size = 80782 }, + { url = "https://files.pythonhosted.org/packages/7d/b5/5c0b093eb48f8a062ef6267d3cb36e9bb1b88440181f6545a383c60efdf8/wrapt-2.2.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:55b9a899e6fff5444f229d30aa6e9ac92d2216d9d60f33c771b5d76a760d5f8e", size = 81678 }, + { url = "https://files.pythonhosted.org/packages/34/f3/de70937472dd3e8a4e6811192f9c6075efdffd4a2cd9b4596bf160f89668/wrapt-2.2.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a2d78c363f97d8bd718ee40432c66395685e9e98528ccaa423c3355d1715a26d", size = 159671 }, + { url = "https://files.pythonhosted.org/packages/a5/ec/40aed2330e7f02ecf74386ffcfef9ccb7108c6a430f15b6a252b663b1bed/wrapt-2.2.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d619e1eed9bd4f6ed9f24cd61971aa086fa86505289628d464bcf8a2c2e3f328", size = 160785 }, + { url = "https://files.pythonhosted.org/packages/45/04/aa5309beed5344b00220ae6b3b24055852192656194c27947bee1736306a/wrapt-2.2.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:518b0c5e323511ec56a38894802ddd5e1222626484e68efe63f201854ad788e5", size = 153699 }, + { url = "https://files.pythonhosted.org/packages/01/df/2def7e99d1fe87eea413f95f671924cdddcb08823b1ffd212748dfa6d062/wrapt-2.2.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4bccea5cdecffa9dd70e343741f0e41e0a16619313d04b72f78bb525162ebcd0", size = 159695 }, + { url = "https://files.pythonhosted.org/packages/c7/f6/a906d01a2ce12157bad2404957b3e2140da354b8a70b2fa48bbf282871c0/wrapt-2.2.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:209112cafd963710a05d199aae431d79a28bc76eb8e6d1bbbb8ad24340722cae", size = 152813 }, + { url = "https://files.pythonhosted.org/packages/02/49/bc0086292d239575b4c08f4cf8a4079fa58abbad58ec23abf84833a283ed/wrapt-2.2.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e5a5290e4bf2f332fc29ce72ffb9a2fff678aaac047e2e9f5f7165cd7792e099", size = 158809 }, + { url = "https://files.pythonhosted.org/packages/55/83/8fbd034de1f3e907edaa18786d5dd8f6932874edee0826c7cecb5cab03a1/wrapt-2.2.2-cp311-cp311-win32.whl", hash = "sha256:5499236ad1dc116012e2a5dd943f3f31af12fce452128e2bbcbd55a7d3d4d14c", size = 77414 }, + { url = "https://files.pythonhosted.org/packages/7e/9c/23695baa331c6de4e874c3d78b8e0bed92e1d2a274e665b29858f6841672/wrapt-2.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:8636809939152be6ae20a6cef0fed9fe60f411b47847d0426a826884b469e971", size = 80368 }, + { url = "https://files.pythonhosted.org/packages/08/49/40cefc342bf89b234a4490d741290fce781774b831aefb39c25471da96c9/wrapt-2.2.2-cp311-cp311-win_arm64.whl", hash = "sha256:5d0a142f7af07caeb5e5da87493162a7b8efa19ba919e550a746f7446e13fb30", size = 79489 }, + { url = "https://files.pythonhosted.org/packages/2a/85/180b40628b23772692a0c76e8030114e1c0ae068470ed531919f0a5f2a4a/wrapt-2.2.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8417fd3c674d3c8023d080292d29301531a12daf8bd938dd419710dd2f464f2b", size = 81484 }, + { url = "https://files.pythonhosted.org/packages/94/f2/21c90f2a16689702e2aaff45795b11018dff2c9b1242bac10d225483f676/wrapt-2.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e7070c7472582e31af3dfc2622b2381a0df7435110a9388ed8db5ffbce67efb", size = 82151 }, + { url = "https://files.pythonhosted.org/packages/5f/b3/7e6e9fcf4fe7e1b69a49fe6cc5a44e8224bab6283c5233c97e132f14908e/wrapt-2.2.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2e096c9d39a59b35b63c9aacfbbbec2088ff51ff1fc31051acc60a07f42f273a", size = 169828 }, + { url = "https://files.pythonhosted.org/packages/0b/43/894f132d857ed5a9904d937baf368badcbe5ea9e436e2f1930fe21c9f1f0/wrapt-2.2.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d1a6050405bf334be33bf66296f113563622972a34900ae6fa60fd283a1a900", size = 171544 }, + { url = "https://files.pythonhosted.org/packages/29/de/3c833e03725b477e9ea34028224dd21a48781830101e4e036f77e8b6b102/wrapt-2.2.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10adb01371408c6de504a6658b9886480f1a4919a83752748a387a504a21df79", size = 160663 }, + { url = "https://files.pythonhosted.org/packages/33/be/27edce350b24e3054d9d047f65f16d4c4d4c1f3f31c4278a1f8a95c723c8/wrapt-2.2.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3442eee2a5798f9b451f1b2cd7518ce8b7e28a2a364696c414460a0e295c012a", size = 169387 }, + { url = "https://files.pythonhosted.org/packages/e2/c4/9fd9679af8bf38e146652c7f47b6b352c3e5795b4ad1c0b7f94e15ac2aa7/wrapt-2.2.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:6c99012a22f735a85eed7c4b86a3e99c30fdd57d9e115b2b45f796264b58d0bf", size = 158849 }, + { url = "https://files.pythonhosted.org/packages/bc/c2/aa6c0c2206803068c6859dabe01f8c84c43744da93d4c67b8946d21655ee/wrapt-2.2.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3b686cfc008776a3952d6213cb296ed7f45d782a8453936406faa89eac0835ab", size = 168147 }, + { url = "https://files.pythonhosted.org/packages/42/63/3eb25da41049d20ae18fcab2dd8b056e02387c4bfa626cbdfb7c3b872e4f/wrapt-2.2.2-cp312-cp312-win32.whl", hash = "sha256:ef2cce266b5b0b07e19fa82e59673b81142b7a3607c8ed1254113d048ed668da", size = 77734 }, + { url = "https://files.pythonhosted.org/packages/da/09/0390e008a305360948fa9ce69507d041ac12cb2ee5d28e34467e2ee79391/wrapt-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:abf8c20a2d72ee69e16328b3c91342c446e723bfe48bfcc4dded3b9722ac027f", size = 80585 }, + { url = "https://files.pythonhosted.org/packages/d3/b3/84c445c66969f2d3457276b183a48c91097d59bbef9af6c075366b0f8c36/wrapt-2.2.2-cp312-cp312-win_arm64.whl", hash = "sha256:c6c64c5d02578bc4c4bca4f0aef1504de933c1d5b4ac2710b9131111459506c8", size = 79553 }, + { url = "https://files.pythonhosted.org/packages/43/fc/f32f4b22c6511173c11d9e541ab4e7d8467a0f1b3455acaf784115d31ff8/wrapt-2.2.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9e8b648270c613720a202d9a45ebabc33261b22c3a839b115ac5bce8c0bb0d69", size = 81296 }, + { url = "https://files.pythonhosted.org/packages/72/06/4d117d5d77a9344776c0248b24dae3d3dd2f58e5f765fa08cf887072e719/wrapt-2.2.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6fb7e94e8fe3e4c3067bb1653a91cce7c5e83acc119fdd41501b1bf74654617", size = 81841 }, + { url = "https://files.pythonhosted.org/packages/15/ff/63ad96f98eb58a742b1a20d80f21da88924405910149950b912368150468/wrapt-2.2.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb18fc51e813df0d9c98049e3bf2298a5495a648602040e21fa3c7329371159e", size = 167882 }, + { url = "https://files.pythonhosted.org/packages/20/1f/8bb62d8933df7acf3247194e6e9fc68edf9d2fa203252c89c94b319dd472/wrapt-2.2.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94b00b00f806eb3ef2abe9049ed45994a81ee9284884d96e6b8314927c6cea3d", size = 167411 }, + { url = "https://files.pythonhosted.org/packages/17/09/8789dcb09ee1de715727db7521aabbb68ffa68dfade3a49468440cfced49/wrapt-2.2.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:62415fd095bc590b842b6d092f2b5d9ccbaeb7e0b28535c03dcea2718b48636b", size = 158607 }, + { url = "https://files.pythonhosted.org/packages/9c/20/66e02562d53ee67d841f175e38e3c993c2d78a3e104c576cad61c028b43c/wrapt-2.2.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a41e758d80dc0ab8c210f641ac892009d356cf1f955d97db544c8dd317b4d14c", size = 166367 }, + { url = "https://files.pythonhosted.org/packages/bd/a3/832ac4e41222fb263b3042d42c2f08d305db7d0f0c9b1d3a271a9eede8f6/wrapt-2.2.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b84cd4058001c9727b0e9980b7a9e66325b5ca748b1b578e822cade1bc6b304f", size = 157176 }, + { url = "https://files.pythonhosted.org/packages/b7/01/1bd5e4d2df9c0178989ac8da9186543465388588ee2ef153e2591accebef/wrapt-2.2.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:26fc73a1b15e0946d2942b9a4426d162b51676338327dc067ccd8d2d76385f94", size = 167025 }, + { url = "https://files.pythonhosted.org/packages/1c/69/583ed25291ab53e1ec117135fb1c33425e2f46d2bc8f29c17f7a94cf4274/wrapt-2.2.2-cp313-cp313-win32.whl", hash = "sha256:3c4095803491f6ef72128914c28ec05bbad9758433bb35f6715a3e9c8e46fb2d", size = 77605 }, + { url = "https://files.pythonhosted.org/packages/29/68/e69fc6d06e1523c68e0d00f95c9aed1158ce9908ee41603f7f2eae3d5db6/wrapt-2.2.2-cp313-cp313-win_amd64.whl", hash = "sha256:2cb07f414fab25dbe6b5c7398e1491423a5c81a6209533639969a6c928d474a4", size = 80508 }, + { url = "https://files.pythonhosted.org/packages/55/21/fe7a393d9e5dc0923bed8f5d857e9dcff210f1fa0888c02cc8f3ffaa55aa/wrapt-2.2.2-cp313-cp313-win_arm64.whl", hash = "sha256:1fc7691f070220215cccb2a20836b9adbaecb8ff22ad47abe63de5f110994fac", size = 79565 }, + { url = "https://files.pythonhosted.org/packages/b6/e5/c120d13bf5091164f68c3c1657e84f16f57e71d978421b626393ac5bd7eb/wrapt-2.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ec8f83949028366531383603139403cac7a826e4011955813cdd640017845ce5", size = 83264 }, + { url = "https://files.pythonhosted.org/packages/d3/b0/d4a1eb97e0e286625bdf21bc7f702637f9607787ffbbdb5ec14d50c79dbf/wrapt-2.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4b481fb0c40d9fd90a5809911208da700987d373a20a4709dc9e3944af7a6bec", size = 83791 }, + { url = "https://files.pythonhosted.org/packages/18/1e/f060df47755e87b57684cee7bfc1362b204df55fac96ffebc0631b697b79/wrapt-2.2.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0065a3b657cec06813b4241d2462ccec287f6863103d7445b725fb3a889736f9", size = 203399 }, + { url = "https://files.pythonhosted.org/packages/c4/de/2316a757a1abb6453700b79d83e532146dcef2611348282d4d8889792161/wrapt-2.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:30f7424af5c5c345b7f26490e097f74a2ef45b3d08b664dc33571aee3bd3b56c", size = 210461 }, + { url = "https://files.pythonhosted.org/packages/ed/29/d1160785ae18ca2495a6d82a21154103d74f656c9fd457fb35f6b11b965a/wrapt-2.2.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:07fdcb012821859168641acf68afad61ef9783cf37100af85f152550e9677194", size = 195313 }, + { url = "https://files.pythonhosted.org/packages/f5/2d/7caa9598ae61a9cf0989cc501739cbeeb7d650ab3193cca1407b9af0c6ab/wrapt-2.2.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f90038ab58fafb584801ca62d72384d7d5225d93c76f7b773c22fae545bd8066", size = 206116 }, + { url = "https://files.pythonhosted.org/packages/ac/02/281ea1088b8650d865f311b35cf86fd21df89128e2909714f1161e01c9d0/wrapt-2.2.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:c5d7825491bfa2d08b97e9557768987952c7b9ae687d06c3320b40a37ccb7f20", size = 192668 }, + { url = "https://files.pythonhosted.org/packages/be/7d/976e2d5b4b5c5babda40974edd54d0a5585cb60132ed86b46f4b80239b16/wrapt-2.2.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0ad520e6daa9bbf136f14de735474dbec7dcc0891f718e1d274ce8dc92e645af", size = 198891 }, + { url = "https://files.pythonhosted.org/packages/59/b7/e47651797c097f75a37e2ce86dcf04048ff576f3a674f7c558df7b5e9622/wrapt-2.2.2-cp313-cp313t-win32.whl", hash = "sha256:25904acb9475f46c24fe0423dbc8fda8cc5fbc282ab3dc6e72e919748c53f4e9", size = 78537 }, + { url = "https://files.pythonhosted.org/packages/d1/6f/9fa5d59fb06d890defb5a8f727ce6a14d2932c8760153f96956628559fee/wrapt-2.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:305d4c247d61c4115794a169141823c62f719525ddb90b23aa332741c77d2c28", size = 82005 }, + { url = "https://files.pythonhosted.org/packages/15/80/4c7bd9873d1f9f7d138d93556b500469dbe24f42710b877519c2b9eb380d/wrapt-2.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:c20279cd1a29800815d7b2d6338b60a6c6e78263f9d6e62e0eda251ba9cae2d0", size = 80762 }, + { url = "https://files.pythonhosted.org/packages/24/05/7fd9c3f83b2c74cbfc572a0b88aa37431e04bd8aed70d2c0efd3464206de/wrapt-2.2.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0e64826f920c42d9d9f87e8cc09ffae66c51ede12d59061a5a426deb9aa71745", size = 81341 }, + { url = "https://files.pythonhosted.org/packages/4b/68/1bfa43100dd90d4ef74a05897b86275cf57e1313ca14aae2545bc9f872c9/wrapt-2.2.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:dcaa5e1451bd8751d7bd1568dfa3321c78092a52a7ecb5d1a0f18a5791e1fd00", size = 81921 }, + { url = "https://files.pythonhosted.org/packages/74/eb/df7b7f0b631dbbc750f39be27d8b55f65777d8ac86da80e12be41a644c4b/wrapt-2.2.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0abfd648dac9ac9c5b3aa9b523d27f1789046640b58dcd5652a720ddb325e1fc", size = 167713 }, + { url = "https://files.pythonhosted.org/packages/4d/9a/d1bd36f6d088c8e652a9383cabbd49af30b8c576302a7eccddbab6963e3f/wrapt-2.2.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f4bfd8d1eb438153eff8b8cfe87f032ba65731e1ce06138b5090f745a33f6f95", size = 166779 }, + { url = "https://files.pythonhosted.org/packages/4c/ae/24ffacd4187fac2740a1972093929e836dea092d42c87d728cd98fee11a6/wrapt-2.2.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c427c9d06d859848a69f0d928fe28b5c33a941b2265d10a0e1f15cd244f1ee33", size = 158407 }, + { url = "https://files.pythonhosted.org/packages/a3/ed/974427668249a356051e8d67d47fa54ef6c777f0fcf3bae9d292c047d4b6/wrapt-2.2.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4250b43d1a129d947e083c4dc6baf333c9bb34edd26f912d5b0457841fc858ab", size = 166594 }, + { url = "https://files.pythonhosted.org/packages/fb/5f/e1d7c6e4523f78db2fbd7826babd0348da1d5e0834c4f918b9ab5757dfae/wrapt-2.2.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:173e5bb5ca350a6e0abab60b7ec7cdd7992a814cb14b4de670a28f067f105663", size = 157068 }, + { url = "https://files.pythonhosted.org/packages/1e/c1/7ebd1027f00700c0b0233b20aceef2b4784294ed64971424c4a78e069e34/wrapt-2.2.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:aa14b01804bce36c6d63d7b6a4f55df390f29f8648cc13a1f40b166f4d54680d", size = 166470 }, + { url = "https://files.pythonhosted.org/packages/99/eb/974e471a6a978b8180186b8a9dc5ae3361ce269a967190b709b8ce17abfb/wrapt-2.2.2-cp314-cp314-win32.whl", hash = "sha256:58f9f8d637c9a6e245c6ef5b109b67ec187d2faed23d1405656b51d96e0a5b56", size = 78062 }, + { url = "https://files.pythonhosted.org/packages/49/ec/e1281156cdc7a66693838ad7a0865ad641c74abd337a957d668b575aaffb/wrapt-2.2.2-cp314-cp314-win_amd64.whl", hash = "sha256:385cb1866f20479e83299af585375bfa0a4b0c6c9907a981483ea782ea8ae406", size = 80832 }, + { url = "https://files.pythonhosted.org/packages/45/7d/1b6b5ddd94005a2dac97a4490c9838f3154977850d633abcb65b30089437/wrapt-2.2.2-cp314-cp314-win_arm64.whl", hash = "sha256:8ffbeaea6771a6eba6e6eeb09767864995726bc8240bb54baf88a9bb1db34d5c", size = 80029 }, + { url = "https://files.pythonhosted.org/packages/b0/33/9ebcf8aafe91c601127cbd93708c16aa8f688f34a10bf004046803ecdc4f/wrapt-2.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:09f811d43f6f33ec7515f0be76b159569f4057ab54d3e079c3204dddb90afa2a", size = 83357 }, + { url = "https://files.pythonhosted.org/packages/39/38/ec45b635153327b52e52732a0ea980e5f00b7efba65f9e018828f1e69daa/wrapt-2.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a795d3c06e5fbf9ea2f13196180b77aeab1b4685917256ee0d014cc163d90063", size = 83794 }, + { url = "https://files.pythonhosted.org/packages/4e/ea/1a89e6d3b7a83c3affe5c09cde77792c947e63e4bc85ad84cd5bb9abb0d8/wrapt-2.2.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:45c2f2768e790c9f8db90f239ef23a2af8e7570f25a35619ef902df4a738447f", size = 203362 }, + { url = "https://files.pythonhosted.org/packages/19/d8/3b58763d9863b5a73771c0d97110f9595d248db454009e07e1535ee905a4/wrapt-2.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bbf00ee0cb55ec24e2b0995a71942b85b21a066db8f3f46e1dbfdb9433ffba81", size = 210449 }, + { url = "https://files.pythonhosted.org/packages/2d/6f/17fd9e053103d8be148d20d5d7505facc72d5fe1f9127973904ceaed79cf/wrapt-2.2.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2252f77663651b89255895f58cc6ac08fcb206d4371813e5af61bb62d4f7689c", size = 195349 }, + { url = "https://files.pythonhosted.org/packages/ef/04/d0d1ccaaa12cb7dccf28a23f0279a608ba498f71e81d949d5ed54bcfd5c1/wrapt-2.2.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2cd7181ab1c31192ff5219269830744b5a62020b3a6d433588c4f1c95b8f8bff", size = 206099 }, + { url = "https://files.pythonhosted.org/packages/44/b3/e8aa07b619890a2aa6cde1931b1887abb08820721b564a5f80b7ca3f3aa0/wrapt-2.2.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:6fe35fd51b74867d8b80174c277bd6bbf6a73e443f908129dc531c4b688a20d5", size = 192728 }, + { url = "https://files.pythonhosted.org/packages/b7/f0/1819fb50f0d3c9bd758d8a83b56f1b470dee8b5b8eac8702b7c137cea9d4/wrapt-2.2.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:11d95fc2fbad3163596c39d440e6f21ca9fccece74b56e30a37ac2fca786a07c", size = 198842 }, + { url = "https://files.pythonhosted.org/packages/67/7c/e88313f16a99930b899ef970d91c281544a470749a359decad994483bbda/wrapt-2.2.2-cp314-cp314t-win32.whl", hash = "sha256:d8a15813215f33fa83667bfc978b300e35669ea8bb424e970a1426bcb7bc6cca", size = 79059 }, + { url = "https://files.pythonhosted.org/packages/a0/4f/ac12fda57a55068a094ec42851fb0a40e8489d8941863d517452de62e507/wrapt-2.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d09db0f7e8357060d3c38fc22a018aba683a796bf184360fd1a58f6fc180dc77", size = 82462 }, + { url = "https://files.pythonhosted.org/packages/48/a7/df732dac86d9b2027c56bd163dbc883e037b16c3469614752e148d219c61/wrapt-2.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:f32fe639c39561ccc187bcae17e9271be0eb45f1c2952510d2f29b33ab577347", size = 81182 }, + { url = "https://files.pythonhosted.org/packages/6e/d2/6317eb6d4554855bbf12d61857774af34747bf88a42c19bf306de67e2fa3/wrapt-2.2.2-py3-none-any.whl", hash = "sha256:5bad217350f19ce99ca5b5e71d406765ea86fe541628426772b657375ee1c048", size = 61460 }, +] + +[[package]] +name = "xberg" +version = "1.0.0rc30" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/a0/915a0e5b74789119ab84834ad4dcbb705053f7a872cd9be225d0c7c68cbf/xberg-1.0.0rc30.tar.gz", hash = "sha256:6253898bd3f7a1b8a9d69e1a9ccc36f428c81a1c131b501fa23663c847a347ff", size = 304904 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/d9/e03a74a708b0306b6583c89d7a09e34f0bfd290dbc1ffb6024ea6e0e2b23/xberg-1.0.0rc30-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:a2a6eb1e77609d601534b6e87f2605e0dddb4b3f900a9347c7fad6195e065adf", size = 38291155 }, + { url = "https://files.pythonhosted.org/packages/20/0b/187f1a0ac361d639ce5a124db0772e7de9e259765e262a2900fcf95ad3de/xberg-1.0.0rc30-cp310-abi3-macosx_11_0_x86_64.whl", hash = "sha256:897c76e94bc046cddd76dd3b4ebccbab85886d3f35772c761d057d2aadf79f21", size = 34948254 }, + { url = "https://files.pythonhosted.org/packages/f8/53/5e6598c8b1ed447657f83ab2f0d34c56280e41f1453fcf99cd880e654254/xberg-1.0.0rc30-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:bc6a3566b516c3adad3ae482c21f29193e11b543a79efa325b7f9c72ae97bff4", size = 43172620 }, + { url = "https://files.pythonhosted.org/packages/a3/f1/281208e983f5afd9af574fc1aec880aebf89cf4a4f9bb334641445de97bd/xberg-1.0.0rc30-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:495d8541123d553d4a94b1380bfe5fce0c7342678e70cb999c19823b0c3b2885", size = 45676001 }, + { url = "https://files.pythonhosted.org/packages/81/56/359325c35545216462828b31f54f53def6a33b42a0561ac2b60122d2c5f5/xberg-1.0.0rc30-cp310-abi3-win_amd64.whl", hash = "sha256:9eceea800bc5e1ac036f49444a89b8730b9dcbd50b1d489a036a793bc55a9195", size = 32455213 }, ] [[package]] name = "xxhash" -version = "3.6.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/02/84/30869e01909fb37a6cc7e18688ee8bf1e42d57e7e0777636bd47524c43c7/xxhash-3.6.0.tar.gz", hash = "sha256:f0162a78b13a0d7617b2845b90c763339d1f1d82bb04a4b07f4ab535cc5e05d6", size = 85160 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/17/d4/cc2f0400e9154df4b9964249da78ebd72f318e35ccc425e9f403c392f22a/xxhash-3.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b47bbd8cf2d72797f3c2772eaaac0ded3d3af26481a26d7d7d41dc2d3c46b04a", size = 32844 }, - { url = "https://files.pythonhosted.org/packages/5e/ec/1cc11cd13e26ea8bc3cb4af4eaadd8d46d5014aebb67be3f71fb0b68802a/xxhash-3.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2b6821e94346f96db75abaa6e255706fb06ebd530899ed76d32cd99f20dc52fa", size = 30809 }, - { url = "https://files.pythonhosted.org/packages/04/5f/19fe357ea348d98ca22f456f75a30ac0916b51c753e1f8b2e0e6fb884cce/xxhash-3.6.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d0a9751f71a1a65ce3584e9cae4467651c7e70c9d31017fa57574583a4540248", size = 194665 }, - { url = "https://files.pythonhosted.org/packages/90/3b/d1f1a8f5442a5fd8beedae110c5af7604dc37349a8e16519c13c19a9a2de/xxhash-3.6.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b29ee68625ab37b04c0b40c3fafdf24d2f75ccd778333cfb698f65f6c463f62", size = 213550 }, - { url = "https://files.pythonhosted.org/packages/c4/ef/3a9b05eb527457d5db13a135a2ae1a26c80fecd624d20f3e8dcc4cb170f3/xxhash-3.6.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6812c25fe0d6c36a46ccb002f40f27ac903bf18af9f6dd8f9669cb4d176ab18f", size = 212384 }, - { url = "https://files.pythonhosted.org/packages/0f/18/ccc194ee698c6c623acbf0f8c2969811a8a4b6185af5e824cd27b9e4fd3e/xxhash-3.6.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4ccbff013972390b51a18ef1255ef5ac125c92dc9143b2d1909f59abc765540e", size = 445749 }, - { url = "https://files.pythonhosted.org/packages/a5/86/cf2c0321dc3940a7aa73076f4fd677a0fb3e405cb297ead7d864fd90847e/xxhash-3.6.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:297b7fbf86c82c550e12e8fb71968b3f033d27b874276ba3624ea868c11165a8", size = 193880 }, - { url = "https://files.pythonhosted.org/packages/82/fb/96213c8560e6f948a1ecc9a7613f8032b19ee45f747f4fca4eb31bb6d6ed/xxhash-3.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dea26ae1eb293db089798d3973a5fc928a18fdd97cc8801226fae705b02b14b0", size = 210912 }, - { url = "https://files.pythonhosted.org/packages/40/aa/4395e669b0606a096d6788f40dbdf2b819d6773aa290c19e6e83cbfc312f/xxhash-3.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7a0b169aafb98f4284f73635a8e93f0735f9cbde17bd5ec332480484241aaa77", size = 198654 }, - { url = "https://files.pythonhosted.org/packages/67/74/b044fcd6b3d89e9b1b665924d85d3f400636c23590226feb1eb09e1176ce/xxhash-3.6.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:08d45aef063a4531b785cd72de4887766d01dc8f362a515693df349fdb825e0c", size = 210867 }, - { url = "https://files.pythonhosted.org/packages/bc/fd/3ce73bf753b08cb19daee1eb14aa0d7fe331f8da9c02dd95316ddfe5275e/xxhash-3.6.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:929142361a48ee07f09121fe9e96a84950e8d4df3bb298ca5d88061969f34d7b", size = 414012 }, - { url = "https://files.pythonhosted.org/packages/ba/b3/5a4241309217c5c876f156b10778f3ab3af7ba7e3259e6d5f5c7d0129eb2/xxhash-3.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:51312c768403d8540487dbbfb557454cfc55589bbde6424456951f7fcd4facb3", size = 191409 }, - { url = "https://files.pythonhosted.org/packages/c0/01/99bfbc15fb9abb9a72b088c1d95219fc4782b7d01fc835bd5744d66dd0b8/xxhash-3.6.0-cp311-cp311-win32.whl", hash = "sha256:d1927a69feddc24c987b337ce81ac15c4720955b667fe9b588e02254b80446fd", size = 30574 }, - { url = "https://files.pythonhosted.org/packages/65/79/9d24d7f53819fe301b231044ea362ce64e86c74f6e8c8e51320de248b3e5/xxhash-3.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:26734cdc2d4ffe449b41d186bbeac416f704a482ed835d375a5c0cb02bc63fef", size = 31481 }, - { url = "https://files.pythonhosted.org/packages/30/4e/15cd0e3e8772071344eab2961ce83f6e485111fed8beb491a3f1ce100270/xxhash-3.6.0-cp311-cp311-win_arm64.whl", hash = "sha256:d72f67ef8bf36e05f5b6c65e8524f265bd61071471cd4cf1d36743ebeeeb06b7", size = 27861 }, - { url = "https://files.pythonhosted.org/packages/9a/07/d9412f3d7d462347e4511181dea65e47e0d0e16e26fbee2ea86a2aefb657/xxhash-3.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:01362c4331775398e7bb34e3ab403bc9ee9f7c497bc7dee6272114055277dd3c", size = 32744 }, - { url = "https://files.pythonhosted.org/packages/79/35/0429ee11d035fc33abe32dca1b2b69e8c18d236547b9a9b72c1929189b9a/xxhash-3.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b7b2df81a23f8cb99656378e72501b2cb41b1827c0f5a86f87d6b06b69f9f204", size = 30816 }, - { url = "https://files.pythonhosted.org/packages/b7/f2/57eb99aa0f7d98624c0932c5b9a170e1806406cdbcdb510546634a1359e0/xxhash-3.6.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:dc94790144e66b14f67b10ac8ed75b39ca47536bf8800eb7c24b50271ea0c490", size = 194035 }, - { url = "https://files.pythonhosted.org/packages/4c/ed/6224ba353690d73af7a3f1c7cdb1fc1b002e38f783cb991ae338e1eb3d79/xxhash-3.6.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93f107c673bccf0d592cdba077dedaf52fe7f42dcd7676eba1f6d6f0c3efffd2", size = 212914 }, - { url = "https://files.pythonhosted.org/packages/38/86/fb6b6130d8dd6b8942cc17ab4d90e223653a89aa32ad2776f8af7064ed13/xxhash-3.6.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aa5ee3444c25b69813663c9f8067dcfaa2e126dc55e8dddf40f4d1c25d7effa", size = 212163 }, - { url = "https://files.pythonhosted.org/packages/ee/dc/e84875682b0593e884ad73b2d40767b5790d417bde603cceb6878901d647/xxhash-3.6.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7f99123f0e1194fa59cc69ad46dbae2e07becec5df50a0509a808f90a0f03f0", size = 445411 }, - { url = "https://files.pythonhosted.org/packages/11/4f/426f91b96701ec2f37bb2b8cec664eff4f658a11f3fa9d94f0a887ea6d2b/xxhash-3.6.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49e03e6fe2cac4a1bc64952dd250cf0dbc5ef4ebb7b8d96bce82e2de163c82a2", size = 193883 }, - { url = "https://files.pythonhosted.org/packages/53/5a/ddbb83eee8e28b778eacfc5a85c969673e4023cdeedcfcef61f36731610b/xxhash-3.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bd17fede52a17a4f9a7bc4472a5867cb0b160deeb431795c0e4abe158bc784e9", size = 210392 }, - { url = "https://files.pythonhosted.org/packages/1e/c2/ff69efd07c8c074ccdf0a4f36fcdd3d27363665bcdf4ba399abebe643465/xxhash-3.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6fb5f5476bef678f69db04f2bd1efbed3030d2aba305b0fc1773645f187d6a4e", size = 197898 }, - { url = "https://files.pythonhosted.org/packages/58/ca/faa05ac19b3b622c7c9317ac3e23954187516298a091eb02c976d0d3dd45/xxhash-3.6.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:843b52f6d88071f87eba1631b684fcb4b2068cd2180a0224122fe4ef011a9374", size = 210655 }, - { url = "https://files.pythonhosted.org/packages/d4/7a/06aa7482345480cc0cb597f5c875b11a82c3953f534394f620b0be2f700c/xxhash-3.6.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7d14a6cfaf03b1b6f5f9790f76880601ccc7896aff7ab9cd8978a939c1eb7e0d", size = 414001 }, - { url = "https://files.pythonhosted.org/packages/23/07/63ffb386cd47029aa2916b3d2f454e6cc5b9f5c5ada3790377d5430084e7/xxhash-3.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:418daf3db71e1413cfe211c2f9a528456936645c17f46b5204705581a45390ae", size = 191431 }, - { url = "https://files.pythonhosted.org/packages/0f/93/14fde614cadb4ddf5e7cebf8918b7e8fac5ae7861c1875964f17e678205c/xxhash-3.6.0-cp312-cp312-win32.whl", hash = "sha256:50fc255f39428a27299c20e280d6193d8b63b8ef8028995323bf834a026b4fbb", size = 30617 }, - { url = "https://files.pythonhosted.org/packages/13/5d/0d125536cbe7565a83d06e43783389ecae0c0f2ed037b48ede185de477c0/xxhash-3.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:c0f2ab8c715630565ab8991b536ecded9416d615538be8ecddce43ccf26cbc7c", size = 31534 }, - { url = "https://files.pythonhosted.org/packages/54/85/6ec269b0952ec7e36ba019125982cf11d91256a778c7c3f98a4c5043d283/xxhash-3.6.0-cp312-cp312-win_arm64.whl", hash = "sha256:eae5c13f3bc455a3bbb68bdc513912dc7356de7e2280363ea235f71f54064829", size = 27876 }, - { url = "https://files.pythonhosted.org/packages/33/76/35d05267ac82f53ae9b0e554da7c5e281ee61f3cad44c743f0fcd354f211/xxhash-3.6.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:599e64ba7f67472481ceb6ee80fa3bd828fd61ba59fb11475572cc5ee52b89ec", size = 32738 }, - { url = "https://files.pythonhosted.org/packages/31/a8/3fbce1cd96534a95e35d5120637bf29b0d7f5d8fa2f6374e31b4156dd419/xxhash-3.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d8b8aaa30fca4f16f0c84a5c8d7ddee0e25250ec2796c973775373257dde8f1", size = 30821 }, - { url = "https://files.pythonhosted.org/packages/0c/ea/d387530ca7ecfa183cb358027f1833297c6ac6098223fd14f9782cd0015c/xxhash-3.6.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d597acf8506d6e7101a4a44a5e428977a51c0fadbbfd3c39650cca9253f6e5a6", size = 194127 }, - { url = "https://files.pythonhosted.org/packages/ba/0c/71435dcb99874b09a43b8d7c54071e600a7481e42b3e3ce1eb5226a5711a/xxhash-3.6.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:858dc935963a33bc33490128edc1c12b0c14d9c7ebaa4e387a7869ecc4f3e263", size = 212975 }, - { url = "https://files.pythonhosted.org/packages/84/7a/c2b3d071e4bb4a90b7057228a99b10d51744878f4a8a6dd643c8bd897620/xxhash-3.6.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba284920194615cb8edf73bf52236ce2e1664ccd4a38fdb543506413529cc546", size = 212241 }, - { url = "https://files.pythonhosted.org/packages/81/5f/640b6eac0128e215f177df99eadcd0f1b7c42c274ab6a394a05059694c5a/xxhash-3.6.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b54219177f6c6674d5378bd862c6aedf64725f70dd29c472eaae154df1a2e89", size = 445471 }, - { url = "https://files.pythonhosted.org/packages/5e/1e/3c3d3ef071b051cc3abbe3721ffb8365033a172613c04af2da89d5548a87/xxhash-3.6.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:42c36dd7dbad2f5238950c377fcbf6811b1cdb1c444fab447960030cea60504d", size = 193936 }, - { url = "https://files.pythonhosted.org/packages/2c/bd/4a5f68381939219abfe1c22a9e3a5854a4f6f6f3c4983a87d255f21f2e5d/xxhash-3.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f22927652cba98c44639ffdc7aaf35828dccf679b10b31c4ad72a5b530a18eb7", size = 210440 }, - { url = "https://files.pythonhosted.org/packages/eb/37/b80fe3d5cfb9faff01a02121a0f4d565eb7237e9e5fc66e73017e74dcd36/xxhash-3.6.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b45fad44d9c5c119e9c6fbf2e1c656a46dc68e280275007bbfd3d572b21426db", size = 197990 }, - { url = "https://files.pythonhosted.org/packages/d7/fd/2c0a00c97b9e18f72e1f240ad4e8f8a90fd9d408289ba9c7c495ed7dc05c/xxhash-3.6.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6f2580ffab1a8b68ef2b901cde7e55fa8da5e4be0977c68f78fc80f3c143de42", size = 210689 }, - { url = "https://files.pythonhosted.org/packages/93/86/5dd8076a926b9a95db3206aba20d89a7fc14dd5aac16e5c4de4b56033140/xxhash-3.6.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40c391dd3cd041ebc3ffe6f2c862f402e306eb571422e0aa918d8070ba31da11", size = 414068 }, - { url = "https://files.pythonhosted.org/packages/af/3c/0bb129170ee8f3650f08e993baee550a09593462a5cddd8e44d0011102b1/xxhash-3.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f205badabde7aafd1a31e8ca2a3e5a763107a71c397c4481d6a804eb5063d8bd", size = 191495 }, - { url = "https://files.pythonhosted.org/packages/e9/3a/6797e0114c21d1725e2577508e24006fd7ff1d8c0c502d3b52e45c1771d8/xxhash-3.6.0-cp313-cp313-win32.whl", hash = "sha256:2577b276e060b73b73a53042ea5bd5203d3e6347ce0d09f98500f418a9fcf799", size = 30620 }, - { url = "https://files.pythonhosted.org/packages/86/15/9bc32671e9a38b413a76d24722a2bf8784a132c043063a8f5152d390b0f9/xxhash-3.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:757320d45d2fbcce8f30c42a6b2f47862967aea7bf458b9625b4bbe7ee390392", size = 31542 }, - { url = "https://files.pythonhosted.org/packages/39/c5/cc01e4f6188656e56112d6a8e0dfe298a16934b8c47a247236549a3f7695/xxhash-3.6.0-cp313-cp313-win_arm64.whl", hash = "sha256:457b8f85dec5825eed7b69c11ae86834a018b8e3df5e77783c999663da2f96d6", size = 27880 }, - { url = "https://files.pythonhosted.org/packages/f3/30/25e5321c8732759e930c555176d37e24ab84365482d257c3b16362235212/xxhash-3.6.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a42e633d75cdad6d625434e3468126c73f13f7584545a9cf34e883aa1710e702", size = 32956 }, - { url = "https://files.pythonhosted.org/packages/9f/3c/0573299560d7d9f8ab1838f1efc021a280b5ae5ae2e849034ef3dee18810/xxhash-3.6.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:568a6d743219e717b07b4e03b0a828ce593833e498c3b64752e0f5df6bfe84db", size = 31072 }, - { url = "https://files.pythonhosted.org/packages/7a/1c/52d83a06e417cd9d4137722693424885cc9878249beb3a7c829e74bf7ce9/xxhash-3.6.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bec91b562d8012dae276af8025a55811b875baace6af510412a5e58e3121bc54", size = 196409 }, - { url = "https://files.pythonhosted.org/packages/e3/8e/c6d158d12a79bbd0b878f8355432075fc82759e356ab5a111463422a239b/xxhash-3.6.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78e7f2f4c521c30ad5e786fdd6bae89d47a32672a80195467b5de0480aa97b1f", size = 215736 }, - { url = "https://files.pythonhosted.org/packages/bc/68/c4c80614716345d55071a396cf03d06e34b5f4917a467faf43083c995155/xxhash-3.6.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3ed0df1b11a79856df5ffcab572cbd6b9627034c1c748c5566fa79df9048a7c5", size = 214833 }, - { url = "https://files.pythonhosted.org/packages/7e/e9/ae27c8ffec8b953efa84c7c4a6c6802c263d587b9fc0d6e7cea64e08c3af/xxhash-3.6.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0e4edbfc7d420925b0dd5e792478ed393d6e75ff8fc219a6546fb446b6a417b1", size = 448348 }, - { url = "https://files.pythonhosted.org/packages/d7/6b/33e21afb1b5b3f46b74b6bd1913639066af218d704cc0941404ca717fc57/xxhash-3.6.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fba27a198363a7ef87f8c0f6b171ec36b674fe9053742c58dd7e3201c1ab30ee", size = 196070 }, - { url = "https://files.pythonhosted.org/packages/96/b6/fcabd337bc5fa624e7203aa0fa7d0c49eed22f72e93229431752bddc83d9/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:794fe9145fe60191c6532fa95063765529770edcdd67b3d537793e8004cabbfd", size = 212907 }, - { url = "https://files.pythonhosted.org/packages/4b/d3/9ee6160e644d660fcf176c5825e61411c7f62648728f69c79ba237250143/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:6105ef7e62b5ac73a837778efc331a591d8442f8ef5c7e102376506cb4ae2729", size = 200839 }, - { url = "https://files.pythonhosted.org/packages/0d/98/e8de5baa5109394baf5118f5e72ab21a86387c4f89b0e77ef3e2f6b0327b/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f01375c0e55395b814a679b3eea205db7919ac2af213f4a6682e01220e5fe292", size = 213304 }, - { url = "https://files.pythonhosted.org/packages/7b/1d/71056535dec5c3177eeb53e38e3d367dd1d16e024e63b1cee208d572a033/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d706dca2d24d834a4661619dcacf51a75c16d65985718d6a7d73c1eeeb903ddf", size = 416930 }, - { url = "https://files.pythonhosted.org/packages/dc/6c/5cbde9de2cd967c322e651c65c543700b19e7ae3e0aae8ece3469bf9683d/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5f059d9faeacd49c0215d66f4056e1326c80503f51a1532ca336a385edadd033", size = 193787 }, - { url = "https://files.pythonhosted.org/packages/19/fa/0172e350361d61febcea941b0cc541d6e6c8d65d153e85f850a7b256ff8a/xxhash-3.6.0-cp313-cp313t-win32.whl", hash = "sha256:1244460adc3a9be84731d72b8e80625788e5815b68da3da8b83f78115a40a7ec", size = 30916 }, - { url = "https://files.pythonhosted.org/packages/ad/e6/e8cf858a2b19d6d45820f072eff1bea413910592ff17157cabc5f1227a16/xxhash-3.6.0-cp313-cp313t-win_amd64.whl", hash = "sha256:b1e420ef35c503869c4064f4a2f2b08ad6431ab7b229a05cce39d74268bca6b8", size = 31799 }, - { url = "https://files.pythonhosted.org/packages/56/15/064b197e855bfb7b343210e82490ae672f8bc7cdf3ddb02e92f64304ee8a/xxhash-3.6.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ec44b73a4220623235f67a996c862049f375df3b1052d9899f40a6382c32d746", size = 28044 }, - { url = "https://files.pythonhosted.org/packages/7e/5e/0138bc4484ea9b897864d59fce9be9086030825bc778b76cb5a33a906d37/xxhash-3.6.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a40a3d35b204b7cc7643cbcf8c9976d818cb47befcfac8bbefec8038ac363f3e", size = 32754 }, - { url = "https://files.pythonhosted.org/packages/18/d7/5dac2eb2ec75fd771957a13e5dda560efb2176d5203f39502a5fc571f899/xxhash-3.6.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a54844be970d3fc22630b32d515e79a90d0a3ddb2644d8d7402e3c4c8da61405", size = 30846 }, - { url = "https://files.pythonhosted.org/packages/fe/71/8bc5be2bb00deb5682e92e8da955ebe5fa982da13a69da5a40a4c8db12fb/xxhash-3.6.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:016e9190af8f0a4e3741343777710e3d5717427f175adfdc3e72508f59e2a7f3", size = 194343 }, - { url = "https://files.pythonhosted.org/packages/e7/3b/52badfb2aecec2c377ddf1ae75f55db3ba2d321c5e164f14461c90837ef3/xxhash-3.6.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f6f72232f849eb9d0141e2ebe2677ece15adfd0fa599bc058aad83c714bb2c6", size = 213074 }, - { url = "https://files.pythonhosted.org/packages/a2/2b/ae46b4e9b92e537fa30d03dbc19cdae57ed407e9c26d163895e968e3de85/xxhash-3.6.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:63275a8aba7865e44b1813d2177e0f5ea7eadad3dd063a21f7cf9afdc7054063", size = 212388 }, - { url = "https://files.pythonhosted.org/packages/f5/80/49f88d3afc724b4ac7fbd664c8452d6db51b49915be48c6982659e0e7942/xxhash-3.6.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cd01fa2aa00d8b017c97eb46b9a794fbdca53fc14f845f5a328c71254b0abb7", size = 445614 }, - { url = "https://files.pythonhosted.org/packages/ed/ba/603ce3961e339413543d8cd44f21f2c80e2a7c5cfe692a7b1f2cccf58f3c/xxhash-3.6.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0226aa89035b62b6a86d3c68df4d7c1f47a342b8683da2b60cedcddb46c4d95b", size = 194024 }, - { url = "https://files.pythonhosted.org/packages/78/d1/8e225ff7113bf81545cfdcd79eef124a7b7064a0bba53605ff39590b95c2/xxhash-3.6.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c6e193e9f56e4ca4923c61238cdaced324f0feac782544eb4c6d55ad5cc99ddd", size = 210541 }, - { url = "https://files.pythonhosted.org/packages/6f/58/0f89d149f0bad89def1a8dd38feb50ccdeb643d9797ec84707091d4cb494/xxhash-3.6.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9176dcaddf4ca963d4deb93866d739a343c01c969231dbe21680e13a5d1a5bf0", size = 198305 }, - { url = "https://files.pythonhosted.org/packages/11/38/5eab81580703c4df93feb5f32ff8fa7fe1e2c51c1f183ee4e48d4bb9d3d7/xxhash-3.6.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c1ce4009c97a752e682b897aa99aef84191077a9433eb237774689f14f8ec152", size = 210848 }, - { url = "https://files.pythonhosted.org/packages/5e/6b/953dc4b05c3ce678abca756416e4c130d2382f877a9c30a20d08ee6a77c0/xxhash-3.6.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:8cb2f4f679b01513b7adbb9b1b2f0f9cdc31b70007eaf9d59d0878809f385b11", size = 414142 }, - { url = "https://files.pythonhosted.org/packages/08/a9/238ec0d4e81a10eb5026d4a6972677cbc898ba6c8b9dbaec12ae001b1b35/xxhash-3.6.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:653a91d7c2ab54a92c19ccf43508b6a555440b9be1bc8be553376778be7f20b5", size = 191547 }, - { url = "https://files.pythonhosted.org/packages/f1/ee/3cf8589e06c2164ac77c3bf0aa127012801128f1feebf2a079272da5737c/xxhash-3.6.0-cp314-cp314-win32.whl", hash = "sha256:a756fe893389483ee8c394d06b5ab765d96e68fbbfe6fde7aa17e11f5720559f", size = 31214 }, - { url = "https://files.pythonhosted.org/packages/02/5d/a19552fbc6ad4cb54ff953c3908bbc095f4a921bc569433d791f755186f1/xxhash-3.6.0-cp314-cp314-win_amd64.whl", hash = "sha256:39be8e4e142550ef69629c9cd71b88c90e9a5db703fecbcf265546d9536ca4ad", size = 32290 }, - { url = "https://files.pythonhosted.org/packages/b1/11/dafa0643bc30442c887b55baf8e73353a344ee89c1901b5a5c54a6c17d39/xxhash-3.6.0-cp314-cp314-win_arm64.whl", hash = "sha256:25915e6000338999236f1eb68a02a32c3275ac338628a7eaa5a269c401995679", size = 28795 }, - { url = "https://files.pythonhosted.org/packages/2c/db/0e99732ed7f64182aef4a6fb145e1a295558deec2a746265dcdec12d191e/xxhash-3.6.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c5294f596a9017ca5a3e3f8884c00b91ab2ad2933cf288f4923c3fd4346cf3d4", size = 32955 }, - { url = "https://files.pythonhosted.org/packages/55/f4/2a7c3c68e564a099becfa44bb3d398810cc0ff6749b0d3cb8ccb93f23c14/xxhash-3.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1cf9dcc4ab9cff01dfbba78544297a3a01dafd60f3bde4e2bfd016cf7e4ddc67", size = 31072 }, - { url = "https://files.pythonhosted.org/packages/c6/d9/72a29cddc7250e8a5819dad5d466facb5dc4c802ce120645630149127e73/xxhash-3.6.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:01262da8798422d0685f7cef03b2bd3f4f46511b02830861df548d7def4402ad", size = 196579 }, - { url = "https://files.pythonhosted.org/packages/63/93/b21590e1e381040e2ca305a884d89e1c345b347404f7780f07f2cdd47ef4/xxhash-3.6.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51a73fb7cb3a3ead9f7a8b583ffd9b8038e277cdb8cb87cf890e88b3456afa0b", size = 215854 }, - { url = "https://files.pythonhosted.org/packages/ce/b8/edab8a7d4fa14e924b29be877d54155dcbd8b80be85ea00d2be3413a9ed4/xxhash-3.6.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b9c6df83594f7df8f7f708ce5ebeacfc69f72c9fbaaababf6cf4758eaada0c9b", size = 214965 }, - { url = "https://files.pythonhosted.org/packages/27/67/dfa980ac7f0d509d54ea0d5a486d2bb4b80c3f1bb22b66e6a05d3efaf6c0/xxhash-3.6.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:627f0af069b0ea56f312fd5189001c24578868643203bca1abbc2c52d3a6f3ca", size = 448484 }, - { url = "https://files.pythonhosted.org/packages/8c/63/8ffc2cc97e811c0ca5d00ab36604b3ea6f4254f20b7bc658ca825ce6c954/xxhash-3.6.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa912c62f842dfd013c5f21a642c9c10cd9f4c4e943e0af83618b4a404d9091a", size = 196162 }, - { url = "https://files.pythonhosted.org/packages/4b/77/07f0e7a3edd11a6097e990f6e5b815b6592459cb16dae990d967693e6ea9/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b465afd7909db30168ab62afe40b2fcf79eedc0b89a6c0ab3123515dc0df8b99", size = 213007 }, - { url = "https://files.pythonhosted.org/packages/ae/d8/bc5fa0d152837117eb0bef6f83f956c509332ce133c91c63ce07ee7c4873/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a881851cf38b0a70e7c4d3ce81fc7afd86fbc2a024f4cfb2a97cf49ce04b75d3", size = 200956 }, - { url = "https://files.pythonhosted.org/packages/26/a5/d749334130de9411783873e9b98ecc46688dad5db64ca6e04b02acc8b473/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9b3222c686a919a0f3253cfc12bb118b8b103506612253b5baeaac10d8027cf6", size = 213401 }, - { url = "https://files.pythonhosted.org/packages/89/72/abed959c956a4bfc72b58c0384bb7940663c678127538634d896b1195c10/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:c5aa639bc113e9286137cec8fadc20e9cd732b2cc385c0b7fa673b84fc1f2a93", size = 417083 }, - { url = "https://files.pythonhosted.org/packages/0c/b3/62fd2b586283b7d7d665fb98e266decadf31f058f1cf6c478741f68af0cb/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5c1343d49ac102799905e115aee590183c3921d475356cb24b4de29a4bc56518", size = 193913 }, - { url = "https://files.pythonhosted.org/packages/9a/9a/c19c42c5b3f5a4aad748a6d5b4f23df3bed7ee5445accc65a0fb3ff03953/xxhash-3.6.0-cp314-cp314t-win32.whl", hash = "sha256:5851f033c3030dd95c086b4a36a2683c2ff4a799b23af60977188b057e467119", size = 31586 }, - { url = "https://files.pythonhosted.org/packages/03/d6/4cc450345be9924fd5dc8c590ceda1db5b43a0a889587b0ae81a95511360/xxhash-3.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0444e7967dac37569052d2409b00a8860c2135cff05502df4da80267d384849f", size = 32526 }, - { url = "https://files.pythonhosted.org/packages/0f/c9/7243eb3f9eaabd1a88a5a5acadf06df2d83b100c62684b7425c6a11bcaa8/xxhash-3.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:bb79b1e63f6fd84ec778a4b1916dfe0a7c3fdb986c06addd5db3a0d413819d95", size = 28898 }, - { url = "https://files.pythonhosted.org/packages/93/1e/8aec23647a34a249f62e2398c42955acd9b4c6ed5cf08cbea94dc46f78d2/xxhash-3.6.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0f7b7e2ec26c1666ad5fc9dbfa426a6a3367ceaf79db5dd76264659d509d73b0", size = 30662 }, - { url = "https://files.pythonhosted.org/packages/b8/0b/b14510b38ba91caf43006209db846a696ceea6a847a0c9ba0a5b1adc53d6/xxhash-3.6.0-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5dc1e14d14fa0f5789ec29a7062004b5933964bb9b02aae6622b8f530dc40296", size = 41056 }, - { url = "https://files.pythonhosted.org/packages/50/55/15a7b8a56590e66ccd374bbfa3f9ffc45b810886c8c3b614e3f90bd2367c/xxhash-3.6.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:881b47fc47e051b37d94d13e7455131054b56749b91b508b0907eb07900d1c13", size = 36251 }, - { url = "https://files.pythonhosted.org/packages/62/b2/5ac99a041a29e58e95f907876b04f7067a0242cb85b5f39e726153981503/xxhash-3.6.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6dc31591899f5e5666f04cc2e529e69b4072827085c1ef15294d91a004bc1bd", size = 32481 }, - { url = "https://files.pythonhosted.org/packages/7b/d9/8d95e906764a386a3d3b596f3c68bb63687dfca806373509f51ce8eea81f/xxhash-3.6.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:15e0dac10eb9309508bfc41f7f9deaa7755c69e35af835db9cb10751adebc35d", size = 31565 }, +version = "3.8.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/63/71aa56b151a1b28770037a61bd4e461c2619cfc8866a4fcaf1548605e325/xxhash-3.8.1.tar.gz", hash = "sha256:b0de4bf3aa66363552d52c6a89003c479911f12098cd48a53d44a0f7a25f7c46", size = 86223 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/5a/05eaa129555f85476a3e16ff869e95f81a78bbe4647eef9d0229f515a317/xxhash-3.8.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602efcad4a42c184e81d43a2b7e6e4f524d619878f2b6ee2ba469011f47c8147", size = 34699 }, + { url = "https://files.pythonhosted.org/packages/80/59/0df1133958b2228929355e022aab1e958c7b2c43e27bf7f59bc9edfa8a54/xxhash-3.8.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:131324f719957b988861714de7d6ddf57b47abec3b0cc691302ffeaba0e05e10", size = 32373 }, + { url = "https://files.pythonhosted.org/packages/3e/bf/1cfda5b5e6bf26617812b4a31662ef2220d2ad04e0a55b8ff9eb36e56a5c/xxhash-3.8.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:db77278a6eddadbf44ce5aae2fee5ebb4d061f026b1ce2130d058cd4d7a7b670", size = 220284 }, + { url = "https://files.pythonhosted.org/packages/70/93/45dc0ad7913b69e5b08bd039236cf628380e4c9cc76a8a4c6625a328e058/xxhash-3.8.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c332dd48b8cb050da2bb2a3c96d72b1664168650a250ef9718e423df7989e05", size = 240980 }, + { url = "https://files.pythonhosted.org/packages/e9/02/f28ba7d17f2c1410ee397982c817ab1bd5b2701070c2d2c373539aad000a/xxhash-3.8.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a5cd96f6dcdf4fa657b2d95668d71d58455248f98712ecffaa9c528edf40ccae", size = 264526 }, + { url = "https://files.pythonhosted.org/packages/5c/d0/f10651cec2c7981b20d693deae6bdfc438427d92be2db4ccabb6181f0021/xxhash-3.8.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c959f88160b13b4e730b0d75b459b7929fc0d2225c284c9683ac95d6feeeac6a", size = 241369 }, + { url = "https://files.pythonhosted.org/packages/ff/40/136e0cbaf5db51e191423b1c98643593189f02b6cd90837bf64b19113d70/xxhash-3.8.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:027dee4355f3fcc41481650d846cf6cfc895c85a1ab7acd063063821a0df5b4c", size = 473186 }, + { url = "https://files.pythonhosted.org/packages/4b/3f/6aa808a96bdc43dba9a740dec56c744526ee3c0019e32c75e810fa90ae4d/xxhash-3.8.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ad52a0e4bcc0ba956a953a169d1feec2734a64981d689e4fc8f490f7bf91af60", size = 220092 }, + { url = "https://files.pythonhosted.org/packages/47/28/a8675e78a9ced96dab853416162268e10e05b452e95db7888cf69f58ac5f/xxhash-3.8.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5d3dfb1f0ff146da7952867a9414f0c7a29762f8825a84879592612fd6139342", size = 309846 }, + { url = "https://files.pythonhosted.org/packages/89/0f/7fe4d4ef4e69f0033e012396ee2a115886bca7b10b7e45ce398626436bfc/xxhash-3.8.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4482380b462ca9e59994d072a877ecadd1cf51102daeeab2db696f96ab763723", size = 237659 }, + { url = "https://files.pythonhosted.org/packages/38/8f/83e9e31d4ed57fe963b99cb5b13a23e3e0f0dad1885aa0ebd2a7819dd423/xxhash-3.8.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:950ac754d16daea42038f38e7465eb84cda4d08d7343c1c915771b29470f065a", size = 268737 }, + { url = "https://files.pythonhosted.org/packages/57/79/7e7de46dbe5d1f49afc96a0bc42e6b8df24eae3d6bad6007b99e42f48430/xxhash-3.8.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0418ec8b2331b9d4d575fc9284427e8e69449d7172e99e1a86fcdd1f51a0a937", size = 224955 }, + { url = "https://files.pythonhosted.org/packages/ec/34/b8540839e958d5ef5c6101af6f16032109e7099698ae8edbc8dcefe4d8f4/xxhash-3.8.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:32a94ad2763e0263d9102037d349002c3d3c401e42770542c3eeb4801f311661", size = 239653 }, + { url = "https://files.pythonhosted.org/packages/ce/87/a735d05f7f859354acadabe470ff40e2c46672275f96dcf096a761904def/xxhash-3.8.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:89b11a5cdd441aa463f6d34ca0241602bc09b001a76994b6059828494108c673", size = 300213 }, + { url = "https://files.pythonhosted.org/packages/98/31/3e1cb020237b68117fc212dc5f9753b87f865b4dfee7c1ce62d0836955b5/xxhash-3.8.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:09a204dd4bb0823daf938cdd0dc8057d5f1e14fe3cbde929424255f23f9de872", size = 442508 }, + { url = "https://files.pythonhosted.org/packages/23/bf/f80090622141cc734b039ce1d15ce3ff6dced375e9680249bf5b9b8c6bf9/xxhash-3.8.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e710ad822c493fb80a4fbc1e3d0a807b1422cb90adbe64378f98291b7fa48fef", size = 216853 }, + { url = "https://files.pythonhosted.org/packages/a6/a3/60157acecc307b238d3651c2483168e224b48b23a36ae6d6903588341d80/xxhash-3.8.1-cp311-cp311-win32.whl", hash = "sha256:5013be3bea7612852c62a7437f3302c1cfb91ca7e703b194459db0b2b2e0d792", size = 31936 }, + { url = "https://files.pythonhosted.org/packages/59/5c/ef70c418d878d187b8da56d4cdc06aea6cf5e456b301e96e51e1d2cc8625/xxhash-3.8.1-cp311-cp311-win_amd64.whl", hash = "sha256:f377012b86c0a23a1df0cf5a1b05aa7187649e472f71c7892e5f2c2815bbe74f", size = 32724 }, + { url = "https://files.pythonhosted.org/packages/2c/25/f008db952cec6b2a26445b456eeed2ebebd65e08e848ebe09ed6ac0634e6/xxhash-3.8.1-cp311-cp311-win_arm64.whl", hash = "sha256:836f11d4474d3228e9909d97216faa4f7505df41cfaf3927eb29809de785a78d", size = 29212 }, + { url = "https://files.pythonhosted.org/packages/42/91/f65c34a7aa7b4e7cf4854f8e6ef3f7ee32ceac41d4f008da0780db0612f6/xxhash-3.8.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e6e49370822c1f4d8d90e678b06dbcb08b51a026a7c4b55479e7d467f2e813bc", size = 34680 }, + { url = "https://files.pythonhosted.org/packages/57/04/b10a245a4c09a9cfa88f8e9ae755029413ad1ac17047f9a61906e5ae0799/xxhash-3.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:220d68130f83f7cc86d6edfdeab176adc73d7200bf3a8ec10c629e8cf605c215", size = 32397 }, + { url = "https://files.pythonhosted.org/packages/3a/75/45ab795b5945b6388583bd75202106af505537935566c15a1577797a0e08/xxhash-3.8.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d365ee1892c1fa803536f8c6ce21d24b29c9718ec75eb856095c07830f8c478", size = 220549 }, + { url = "https://files.pythonhosted.org/packages/13/44/5ba2bd0a14ddf4193fc7d8ec29625f659f22c06d60b28f04bf46305d8330/xxhash-3.8.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:852bfe059720632e2f16a6a4745e41d20937b2bf2a42a401e2412046bb6971cc", size = 241186 }, + { url = "https://files.pythonhosted.org/packages/23/32/c4147def4d1e4538b906f82731e0ba23424377fc50a7cddd03cd284c8f63/xxhash-3.8.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2f8c25a7061d952de589bd0ea0eaadee32378ff83dd6a677b267f9cd86f401f8", size = 264852 }, + { url = "https://files.pythonhosted.org/packages/6c/bd/71ed14f4f0318bb7fd7b2ec51999413487fa8da8d41208e84d50d1ef0f98/xxhash-3.8.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:868a8dcaff1a84ba78038e1cef14fc88ccf84d9b4d12ea604696e0693296aa56", size = 242663 }, + { url = "https://files.pythonhosted.org/packages/91/09/70af22c565a8473b3f2ae73f88e7721af281bc4a575236dbd1970c9f76f6/xxhash-3.8.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6536d8677d2fff7e64cd0b98b976df9de7aee0e69590044c2af5f51b76b7a170", size = 473510 }, + { url = "https://files.pythonhosted.org/packages/18/96/34db781c8f0cf99c544ca1f2bc2e5bf55426e1eb4ca6de8ea5da56a9f352/xxhash-3.8.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:82c0cedd280eab2e8291270e6c04894dbc096f8159a39dcf1807429f026ca3cc", size = 220469 }, + { url = "https://files.pythonhosted.org/packages/93/5f/9a184f615fa5a4dce30c01534f62946ce5a11ce40f73785cbd356ccabaa9/xxhash-3.8.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:daa86e4b68221d38e669bb236ba112d0335353829fb627c82e5909e4bbe8694c", size = 310290 }, + { url = "https://files.pythonhosted.org/packages/a9/dc/9b9a9789011ee153723a5eb9e7dd7fcbae2ba9b3fe7a729249ca7c252056/xxhash-3.8.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2bc7113e6f2b6b3922dd61796ca9f36af09da3773898e7003038dc992fc83b8d", size = 238173 }, + { url = "https://files.pythonhosted.org/packages/ec/4d/71c6005ada9dcb608a4e1902e8475ecadb5f3fbfa04e1e244d276a2d0c43/xxhash-3.8.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5eed32dad81d6ba8e62dc7b9ffa0500199385d7810a8dd9d4eafaceb8c6e20bb", size = 269026 }, + { url = "https://files.pythonhosted.org/packages/2f/87/d6c036ba25dfbd9c8633be5aa86fc9474bbb9e2c68212a841d090abe7344/xxhash-3.8.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:83697b0ea1f10e7f5d8b26a4906fa851393c61546c63839643a2b7fe2d868061", size = 224970 }, + { url = "https://files.pythonhosted.org/packages/48/62/4c1f035a41c5752aa05e195b6c904c07b94fe9061a16de61e72a6e6b135f/xxhash-3.8.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:36fc69160465ae75c6ec4ac9f781bb2aa16ae7ff869e73c26fee85fbb11b9887", size = 240820 }, + { url = "https://files.pythonhosted.org/packages/da/14/d39d565069b87e86d21a2af2a31d04db79249d25aa8d5b62959056a89857/xxhash-3.8.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:445e0f5a31f2f3546ae0895d4811e159518cdc9d824c11419898d40cfadb677e", size = 300619 }, + { url = "https://files.pythonhosted.org/packages/13/22/75467acc887edc8cf71c97ab1708feb3df7a88bda589b9f399765c6387d2/xxhash-3.8.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:dfe0580fbfd5e4af87d0cc52d2044f155d55ebd8c8a93568758a2ea7d8e15975", size = 443267 }, + { url = "https://files.pythonhosted.org/packages/a4/b6/1da3baa5fa6ef705e3425fddd382be7dfc4dfba2686df90a20f16e9c7b1b/xxhash-3.8.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:095e1323fa108be1292c54c86da3ef3c7a7dc015b105a52133973bc07a6ad11a", size = 217338 }, + { url = "https://files.pythonhosted.org/packages/78/dd/b5295a9f97484e7a1c2b283a742ca45e3104991c55a1ef670dde161829ba/xxhash-3.8.1-cp312-cp312-win32.whl", hash = "sha256:bf28f55e427e0483acb1f666bd0d869b6d5e5a716680c216ad7befe3d4cfba2e", size = 31970 }, + { url = "https://files.pythonhosted.org/packages/ec/31/3fa0b807d7e21515cd975e7fe5c039d52ac3e9401a96d6ad68dae6305215/xxhash-3.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:2256e80e4960ee282f63428adb349cb7f8bd8efe4db770d88eb815f4b9860724", size = 32741 }, + { url = "https://files.pythonhosted.org/packages/b8/05/86feada74e239600e6875aa507afb40482a89b92700aa74a92da83bdcb77/xxhash-3.8.1-cp312-cp312-win_arm64.whl", hash = "sha256:9df56e6df96a60590935e22373041cccc91fd55858763dcffb55bf63b3a2b396", size = 29234 }, + { url = "https://files.pythonhosted.org/packages/6b/8c/446bb782cd0d27007a917b5569a08dd73219c3e8d6e459014db104b27bdb/xxhash-3.8.1-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:3c682fcd96eb4bf64be32a4d95f96107e1588005831bd8a741b324fdda01b913", size = 38562 }, + { url = "https://files.pythonhosted.org/packages/d7/ec/c0c45627eaa6be7a5d6117423adf8f7a15b17ee74b4b17072cca5959a225/xxhash-3.8.1-cp313-cp313-android_21_x86_64.whl", hash = "sha256:036a024d8b9c01f70782e09ed98d532e76fd23f950ae7154bd950fe94e90ebec", size = 36656 }, + { url = "https://files.pythonhosted.org/packages/f6/94/8324c04cc7597154caaeba6c094e01fbd2e7601d01e7a13eea9f5420e77b/xxhash-3.8.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d6a5c0bce213b23b0166fe0d35bcbbe23ce4b968f257cc7eb6fd57cb8e1e6297", size = 31169 }, + { url = "https://files.pythonhosted.org/packages/40/a4/beb6bb26e1184e126dbe7a5682330214ef54dcfbf882078aa9f4b5428d42/xxhash-3.8.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:5177aa44eddaa97c6ef0cc00c6d540edb64d51781d2f8fb941612ec61a92c9ed", size = 32177 }, + { url = "https://files.pythonhosted.org/packages/56/0f/fc4c92a5a528f839b34b6419b2e53c8597f2a629d5a1f5d721f65bfa1fd6/xxhash-3.8.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7801b7223db017b9c0c9ccf37e44524edb35a1544a1c032add22c061c6af0276", size = 34642 }, + { url = "https://files.pythonhosted.org/packages/d4/58/edbfb141d4000767ac6a9694f8ac0763e2c2e983e65c9e31620ba56e2667/xxhash-3.8.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9e80238259655bf69d7bcd08226a970d7f42605f3157786bfa76dd13472d7fa0", size = 34684 }, + { url = "https://files.pythonhosted.org/packages/07/3f/5072f1f0f5714186f0ac2a0b5a4929ce30d4b845e94886b6c01b6ebda0be/xxhash-3.8.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bcab50a389cc04d87f90092af78a6adba2ab3deca63175a3344ca83514045315", size = 32401 }, + { url = "https://files.pythonhosted.org/packages/49/c7/802ea2f9c2ed59219934d6d65c470d502b1788043eae277a52af8658bda6/xxhash-3.8.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a2489d3a776fa380cb8e71f54c7fda268a9baf3de9b1395093fd280f95735907", size = 220617 }, + { url = "https://files.pythonhosted.org/packages/99/a8/e10488efd31fcb13fcd6acbc6e788f10c6f8e3a0cc4ae3eb89dc19c55a12/xxhash-3.8.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32ab1e5432690276e71192be7401b55f96db2d0eedea5d44eb1f164505669cc0", size = 241295 }, + { url = "https://files.pythonhosted.org/packages/18/cc/14180b17d44892a631f8ae7323c30bfbb1328efc8209e528a480293528ac/xxhash-3.8.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b30e01a0b97a4bc3f519a4d7a82da3dc53251fb0de5eeea8660dcd4ff094c0c2", size = 264688 }, + { url = "https://files.pythonhosted.org/packages/a9/72/a14019d0c5f6c41ee407a503036ae32787c91325ca218a96a9b5627be651/xxhash-3.8.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1f44275ddb0978b67a58a951501903f04d49335a91f7681c9ce122ecb8ccb329", size = 242740 }, + { url = "https://files.pythonhosted.org/packages/68/08/92550e556c6fcfcb96c6a336945eb53a431ed43120ed749636debb16c5cf/xxhash-3.8.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e3b87cbd974512c0c5fc7b469c36b2cdc9ee6d76e4ec78bccb2c7184611c49b0", size = 473599 }, + { url = "https://files.pythonhosted.org/packages/29/83/e361d3c1acd1b21e1d489616de6fa4aaf843365d8179f612e3743eac20a9/xxhash-3.8.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98ee81b4b7f3023c9cb04a78cc67610baffcb5812d92f2096cb5a5efc6f19437", size = 220559 }, + { url = "https://files.pythonhosted.org/packages/05/01/006a4243c2c2a6831827f9999f6d1c23feeef100eb023c1f886022a00bf3/xxhash-3.8.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2666f059a1588a99267e33605365ed89cea92f424b3522806a9f4bd8ad2e3d62", size = 310383 }, + { url = "https://files.pythonhosted.org/packages/d8/20/af388e8bf9f9a0f89eeef7d2a1935d176ee1c20bc6adeda05035879379cf/xxhash-3.8.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0093cf7eeb91b84776e8742113afa4bdf47533d36cf719179aaaf1f56f6f8bf", size = 238228 }, + { url = "https://files.pythonhosted.org/packages/63/6b/4666579a87eebd1744663c404297355fa0658617b015cedfa58810ee7036/xxhash-3.8.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3a800912a2e5e975d4128969d645c4a2a80aa886ccd6c9b1c6f44529e327e8cf", size = 269137 }, + { url = "https://files.pythonhosted.org/packages/de/d3/e963a8a46f900a137d91b02144d8ea07a8f812971b138204a3b2f8b8e55c/xxhash-3.8.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0fe37f72a207223d22a4eddc3149d4298993385aa9daef25c039246ca5a309f3", size = 225068 }, + { url = "https://files.pythonhosted.org/packages/aa/80/9d181dbcde4b0fe48375f48833a5832d4b8cd2b349b15110c92ee472d874/xxhash-3.8.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5db43f249b4be9f99ef4b967863f37094fb40e67effafb78ba4f0356b6396104", size = 240874 }, + { url = "https://files.pythonhosted.org/packages/39/15/ce3ab5a1cd27ead25a5196e55a7284220f6ad6e316da494ffd900b2b600f/xxhash-3.8.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c4ed42965c2cd9081f011be22f69d0e65d3b6165fe7734072fd0c232840bbd4e", size = 300702 }, + { url = "https://files.pythonhosted.org/packages/96/c0/2281a8ab5f2a62dbf57a23c58a01ccc1d98abf40f71193c8a81f59e759b5/xxhash-3.8.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3557bec8fcb11738a8920eeb68974bc76b75262f6947998d3147954ce0a4b893", size = 443351 }, + { url = "https://files.pythonhosted.org/packages/81/2e/071a58c1a53a52d4f7a3aa0987be0c396dffd40da8204805fe1b130a81f4/xxhash-3.8.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:00de40f3b42240db23a82a5c682b55d7263d84a26a953240c1aee463409660e3", size = 217396 }, + { url = "https://files.pythonhosted.org/packages/68/44/36ab58134badd9d3433fc7b53c4ca8d113d8e807782885628640f8297a4d/xxhash-3.8.1-cp313-cp313-win32.whl", hash = "sha256:b5196cc2574cfec572a5f3fb7cfa5ade27305ae3d06516a082132441aff4c83a", size = 31974 }, + { url = "https://files.pythonhosted.org/packages/96/2a/2a0b84798448e766f7b89ceed073cb0cb5a43fc9ebbacbdea74a38de18e3/xxhash-3.8.1-cp313-cp313-win_amd64.whl", hash = "sha256:538f5f865df6cd8c32dd63158a0e5b4f5dd08d732a7da8b7228a5a0776c8ce55", size = 32739 }, + { url = "https://files.pythonhosted.org/packages/d4/60/bb51dbf7c363ff88a7cbd50b7959718219577ef44d7cf255929ffc4a2194/xxhash-3.8.1-cp313-cp313-win_arm64.whl", hash = "sha256:a6617f30641ba0d8baa1635fbefb1dffc5165ec36d26921bd5cee13497cd937a", size = 29239 }, + { url = "https://files.pythonhosted.org/packages/56/d3/827ca123c2ee5443a6aaed3c5dd199237dc2f010e2bebd7ec09ef36f3a5f/xxhash-3.8.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:bfcd82852c62a60e314670a9602de354c4460f8adad916e2e42a20860c7870bc", size = 34964 }, + { url = "https://files.pythonhosted.org/packages/05/67/67ae2a3ccdeb8b8ef025d35aee9edd1d26c3abe5051d47da9286232afbf8/xxhash-3.8.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:08ea2081f5e88615fec8622a9f87fbe21b8ea58d88cfc02163ca11026ee62a92", size = 32697 }, + { url = "https://files.pythonhosted.org/packages/38/5a/3d3994346e1f45493679cb5c1ffc2bf454e410e9d1e8a662d253becee91e/xxhash-3.8.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2e32855b6f9e5b18f449e59d45e3d5778bdeb660632ef2693cca267a11246c75", size = 225954 }, + { url = "https://files.pythonhosted.org/packages/3f/2c/53169270309b7cd8e05504e07fe123bac053b89d00ac63617faacf0a2ec0/xxhash-3.8.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6e088bd7870775624256a0d84c2a6714afd223b2eeb56b0ca58398e52a32fda", size = 249776 }, + { url = "https://files.pythonhosted.org/packages/70/e0/5c551d8d592f944506f7c5185e210255c15e672a3c6008c156a1bd9b775e/xxhash-3.8.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:72eb5ae575cc7ae2b23f6f8064a8b10f638c7149819ae9cc6d20ebd4d37a1629", size = 274776 }, + { url = "https://files.pythonhosted.org/packages/a0/2a/d3a762270cee2d7bcd0e25e28c623e5f3f5c0dc637b66e3e47dd5b0bb3f0/xxhash-3.8.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d0b48cdf690a64cedf7258c3dc9506cc41fc86edd7739c40e3098952265dc068", size = 252056 }, + { url = "https://files.pythonhosted.org/packages/c1/8f/b78e4373b2cb6d1c42af60ea2d7e9146ad0710b239ac7f706d5d31d5bb98/xxhash-3.8.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb9e256a357dfcede7818c6d34e70db2d6b664394803d1de4b6984d2de76c0f1", size = 482108 }, + { url = "https://files.pythonhosted.org/packages/e6/0d/642d923336ea61a15f8ce64fc7e078729e6e06c3a026e517fa79b2c23b7a/xxhash-3.8.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51f71a6e2ad071e70c937e41fcb6c19f82c3f9f49831eba850ed4a106ffbb647", size = 226739 }, + { url = "https://files.pythonhosted.org/packages/a6/0a/a37d6da6427d45a8d23e3ee3a0ca9c9d4a90364849c6637fe2963a755f9b/xxhash-3.8.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e4a6443968c4e8dc69967e12776776a5952c119cc1bd94168ad1c5ad667c2be1", size = 319658 }, + { url = "https://files.pythonhosted.org/packages/4a/51/ebbd40da8a3f1bc53b4b7a9a87f8e28bd95c5f21bc14b8a57860cf367d1b/xxhash-3.8.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:714503083a1f2065c9ad15340dd49ac8a8e948a505a705ffa1750cb951519113", size = 246059 }, + { url = "https://files.pythonhosted.org/packages/24/4c/d9014030147e1f0bb26e7da47aa240dd9ec61c763c573e558111d869f8e1/xxhash-3.8.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:77f74e45a1e5574bbbf80181c8027b3a4c65c2248fffbd557bd596fff13102f9", size = 275535 }, + { url = "https://files.pythonhosted.org/packages/84/86/caee2db41fadcd5a25aa4323213f9afec5a8586d4e419241e3d659362bd7/xxhash-3.8.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:4e0e1b0fb0259c1b75d1251ac0bb4d7ab675d36f7a6bf4ba6aa630dae94f9ffa", size = 231292 }, + { url = "https://files.pythonhosted.org/packages/0b/60/f52f08bcdc904c4514ea5c25caa19e9f3214144434a6ff96dc82dc1cbddd/xxhash-3.8.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:10e4393ec33633c2f05ad01869e546ad080b1a18f2650503731f153774608b31", size = 250490 }, + { url = "https://files.pythonhosted.org/packages/24/a0/94dc7ae310838f250669c6ad7168e6d6fca17d49dac1053f06dc232c4a56/xxhash-3.8.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:b3ba794c3d885803db6c3116686923f1ec13bc86e621e169a375282b63ea1cc6", size = 309861 }, + { url = "https://files.pythonhosted.org/packages/8b/f9/adeead7d0eb28cdfc2832544ea639ffbc6749ccde47a8e228d667459182e/xxhash-3.8.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:57189a69c0891e4818853feaa521c972d22c880a001453addea015f48e3c3398", size = 448739 }, + { url = "https://files.pythonhosted.org/packages/04/a4/22ec0e07db57d901c9298ae98aa3cf2be45bafded6f07c13131e85b89032/xxhash-3.8.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d59e71153fe9ff85648d00e18649b07e9b22c797291abb7e27274fa06df8b838", size = 223657 }, + { url = "https://files.pythonhosted.org/packages/94/32/8a9531f37b59e5a013003db7cb7414baf4ce7e0e1268e0d5947cd3d6a2df/xxhash-3.8.1-cp313-cp313t-win32.whl", hash = "sha256:5b96f0024e9840f449bd91b2d005c921a4b666055a0d1b6492463799f32aae22", size = 32377 }, + { url = "https://files.pythonhosted.org/packages/e7/ab/2ca45fd7f671de5f81fc297ef1c95080b40c86ec6be0cc6034b8f7707ac8/xxhash-3.8.1-cp313-cp313t-win_amd64.whl", hash = "sha256:37d5a56c36dcc0b9a87b814cd992598d33863ff683749de6c86081f278d5e629", size = 33274 }, + { url = "https://files.pythonhosted.org/packages/5a/54/20d7163463ddb6438b73a427d1655a77a502cf9b9b0c3ada3599629d9c0a/xxhash-3.8.1-cp313-cp313t-win_arm64.whl", hash = "sha256:6696c8752aded28ff3b16f33ef28ce28fb5d209b80c206746f943199fcf5fd65", size = 29375 }, + { url = "https://files.pythonhosted.org/packages/c2/8b/df2ba04f22a6cd6b39f96a6577329a8471a55c90ef8d8e2f7c102363613f/xxhash-3.8.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:9db455cb649dcfe4504d6d68a6d83a7315a99a3ca59871dc3ff840671f99adba", size = 38430 }, + { url = "https://files.pythonhosted.org/packages/b2/4f/6a059e8ad3ca8deedc91dfe335b211204900895152212c03ebbe721de68b/xxhash-3.8.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:affb37f152e55b5e4494bb9d0107f7bb08515c6704fbed82d9f61214d74adc17", size = 36558 }, + { url = "https://files.pythonhosted.org/packages/cb/95/40be178205acce092ae418feb20ac737b32a02c7b864926ed0717354c9f8/xxhash-3.8.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:460261045936975193bfd20549a0de1cd52a33b405cbb972f0d80940c42266cd", size = 31181 }, + { url = "https://files.pythonhosted.org/packages/3f/89/2da4dbf051bafa156c0e3f12012db2b0ac3b84ff37ca1f021f6bfffcdfbb/xxhash-3.8.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:38c887aedb696ef8bca19983206d270848558cfae4a91afa6a2fb05dde58ffc5", size = 32192 }, + { url = "https://files.pythonhosted.org/packages/7c/4e/e000bbae3566bc8e0be771a8a0f294aa99075e3f0bc4ef43922ebffdebc8/xxhash-3.8.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:594131ce1aad18db3689781f806db1b065cdaa04f4df36b4c038d2013aefd0bf", size = 34691 }, + { url = "https://files.pythonhosted.org/packages/b4/4a/ea954aacc7d1c8711880ac2b55da94429a9b4296b151c4fc0966549ca1ee/xxhash-3.8.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:78c794b643d214f1522e7a288bcf5a2de120d26cd170516749a4009dc92722c9", size = 34807 }, + { url = "https://files.pythonhosted.org/packages/ca/29/df598e738ff37558ac627264deb2e560902d9bf7f46d3bd5175c9eee593e/xxhash-3.8.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:af0c9fedc4a2c24e8664953882fe8185f3790b8338c9c700f76f5ad660817711", size = 32410 }, + { url = "https://files.pythonhosted.org/packages/59/9c/81ab40e7d33ada0b3df5d1bc884894d15dbf4f805cd645b685e4606bb8e0/xxhash-3.8.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:115772daeb71b2f3b9381177017f53e6cf3f3439c840737fdabd21aba6e54920", size = 220564 }, + { url = "https://files.pythonhosted.org/packages/fd/6f/62ae6f5c8606320a0e2a41c2dc8c6d91cc5d63d0f84dd9582e9543779dd8/xxhash-3.8.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:000435984a0469b0f822fe76f35bddea0f96a4d6521b3339a60a6428cdee1edc", size = 241462 }, + { url = "https://files.pythonhosted.org/packages/15/a1/9c3a0ec6cb524396f551eddd102a76690a795494eb9784fc67542b0daa37/xxhash-3.8.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2f1c68394818e0595569c2ff3cbc1e6d5a36a434e796f5c526b987b80c8a8c62", size = 264491 }, + { url = "https://files.pythonhosted.org/packages/64/f2/700a4674e4308eb59d2fdb973977e82eae231bea5044753fee5c9eec0e0c/xxhash-3.8.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:46b39976d008e2a845758650f0ff7136bca004f40da0c8798bd37ac37860154f", size = 242905 }, + { url = "https://files.pythonhosted.org/packages/f3/8a/72d9874375c8d4cbc64a8cd1d659d5695a8765c3db82efa82dc5bd9f14d0/xxhash-3.8.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d5006c65ec507a333479e76e00e2c368781f16c24ededa764763956b32a0e93e", size = 473873 }, + { url = "https://files.pythonhosted.org/packages/03/f0/6db07590ed7e0a77f186ef0bcea8d52553bf1ba57833e09467a2411f0f2d/xxhash-3.8.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c31a2649bcf1fe97cf11c79848d761df33ac46b3896942d31b640557b486ff6b", size = 220765 }, + { url = "https://files.pythonhosted.org/packages/8f/10/00d12d8b8beabbf49a8bbc626fb9f40445145a8887eb41a6acfb69149ac4/xxhash-3.8.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8f759eed402448c2bdbb492e4fba1f20668ffe29688605ea61f0f67f9e4e386d", size = 310478 }, + { url = "https://files.pythonhosted.org/packages/1f/f9/12a82394eefb0f185d15a7f7b9f627c61c475a72dd83718436a5b84b42ac/xxhash-3.8.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5f97ecfede10d5b2870383620e2d25c8561e217c7bf9081073802b54248d2b", size = 238393 }, + { url = "https://files.pythonhosted.org/packages/20/f3/53f963e320b9ce678337aa7273f39ce692ded8b99e3d22a866ec722159ab/xxhash-3.8.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:1da930bbcac3e8fbe2191850e2abb57977a99348c12c4b385e1058ac1b0a9ecc", size = 268704 }, + { url = "https://files.pythonhosted.org/packages/0a/50/5b5badbd87c82d9f9b5f58ac74a3f29ef08f6fc387b324b8fd482450b862/xxhash-3.8.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:747476436f6891b9773374ce8d48edcc8b12cb5b61b67c6fb6289633747d088f", size = 225015 }, + { url = "https://files.pythonhosted.org/packages/30/93/3ca68265afe7b4e69435e08a7b6a1d9d0f2a071e889da1f8041ed00fe878/xxhash-3.8.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4ef09bbc2519a93cd0f95f2ceb5f7b85919dffea643278e02362bf40e3c4bed1", size = 240951 }, + { url = "https://files.pythonhosted.org/packages/dd/a6/27e19670c40f46b5e76e11f2f4713d21054804568425d870670e757172ad/xxhash-3.8.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:a5eed9d41995a83f3332b4e3396abb7f433cac584222bd7e305b606d8353861e", size = 300751 }, + { url = "https://files.pythonhosted.org/packages/bc/fb/b33e27689959fe7ed2ae0b830af41560d65213943983afa9db3a8d481bce/xxhash-3.8.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:53f3ed9118397074ff63a79b66b7fec1c84c782eecde35c5bc94e420a971c231", size = 443480 }, + { url = "https://files.pythonhosted.org/packages/26/60/0e0d973be5fe280753ef02fbc89349492ad6e903bf1dcb870b668f94b662/xxhash-3.8.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d247b34bf433c92b41689318fd25d246313cab2275a6a47e2efac178b80d6efe", size = 217657 }, + { url = "https://files.pythonhosted.org/packages/ad/68/c9e3ecef4a9a417d464cb5bd200aa12f73192dee677901b9e08e0ad0d1bb/xxhash-3.8.1-cp314-cp314-win32.whl", hash = "sha256:d58ce8b6cfa9c4d2f230557f69caf7c06369e318015d0b19485095bc2c5963ab", size = 32690 }, + { url = "https://files.pythonhosted.org/packages/d7/99/e9e44588c0b62837bbec5ba7927816de0afa03406b1a0b6c7a7e1d1a30a0/xxhash-3.8.1-cp314-cp314-win_amd64.whl", hash = "sha256:6cee733fe4ccb1737e0997135283c82341e5cfa9cf214b165f9087fb663aaf4f", size = 33460 }, + { url = "https://files.pythonhosted.org/packages/45/2b/64f36d86380b3657ad9031967ab814f3ef31307174650853f69c18932ebc/xxhash-3.8.1-cp314-cp314-win_arm64.whl", hash = "sha256:58346024d47e84f7d8b3e7f5d6faa1d58acbbe49a8771497872059f58c1d8ea5", size = 30092 }, + { url = "https://files.pythonhosted.org/packages/92/cb/18b64bff88c58a0ca209dc533e63cf02d7ae5aa6b1b9a9fd14e81b5dbd60/xxhash-3.8.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:01cab782f8a0a05ecad2c63d7ef10f7ab475f660e0d6419d069418c14d88de7c", size = 35024 }, + { url = "https://files.pythonhosted.org/packages/af/1d/72d8a70520e5dcddb472ea0486d299da3240745a10658290cd7b5690ede2/xxhash-3.8.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:717b12fdc51819833704e85e6926d76981ffa3f780ef92e33ebb8b26d46bb230", size = 32697 }, + { url = "https://files.pythonhosted.org/packages/9c/b8/e041f555903c56db3d0a731b3d72a6575d75e0ed868b1bd2e5176111ca44/xxhash-3.8.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ec55d80e9b8a519d742669e0b49e8ce9e6747be42bf3c138158b6543a9c8e489", size = 226044 }, + { url = "https://files.pythonhosted.org/packages/3a/7e/5cdcf06bf6ec4b5d2ac073feb23432ec1d603fd438864cbd2c09c7cb45e1/xxhash-3.8.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98d8ac1129b4dd39098cffed94d1284aceb61c3aa396757ccc736ac392e4cee5", size = 249899 }, + { url = "https://files.pythonhosted.org/packages/c0/c0/eb7e059cb5e1dba11fd30d2fdf882f56e5a417a3eaa43669d43623767f45/xxhash-3.8.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3bc0fa90830df1e1277f33cc6e55de9990b83c0319fd8c7412866cfde38b025e", size = 274892 }, + { url = "https://files.pythonhosted.org/packages/66/74/a600aaf7cd39957fd1510adeedb1749c1e7eb82bd632a1153d9c664c3135/xxhash-3.8.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c73b6f652f0745425aa6378319c331293b5341756262e9408ed3d45f183375e6", size = 252243 }, + { url = "https://files.pythonhosted.org/packages/ad/04/78d88fa75a6763e5d09bf1b947a392a27988903381b219006f92f3c68fc8/xxhash-3.8.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6114692261eff4266386cdec0f7d87eee24e317ab397c218b7ae6a76b4c6339", size = 482191 }, + { url = "https://files.pythonhosted.org/packages/7f/06/07a8aea1108d682de8791ce608cdf367d75ff4e7e57cd3c154bdc6f47b23/xxhash-3.8.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4df57c0b161ec1b3ed0526a67b0db0914b557e86ee8aae51887aec941b261542", size = 226877 }, + { url = "https://files.pythonhosted.org/packages/ed/b5/86bade5618a524d2c06c4041aa2fe8e5749ce16e88afba60d67c1684a21f/xxhash-3.8.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9043877a917be88ccf230aa5667c1bd059bce80f4c2727e4defa1b29b7f48b08", size = 319794 }, + { url = "https://files.pythonhosted.org/packages/23/69/9b1a2b89b1621bb740fbcb7beb512f60f99480c1bdc680c0c90e1f56ff75/xxhash-3.8.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:559e3cabe522231909f9de98ef06929edbd53782046bd21aae0c72db6f2a0775", size = 246202 }, + { url = "https://files.pythonhosted.org/packages/08/ea/662ed6cb49f1d34078b6a3a3e0f3d29ff93fd7b5a03c0bc9ecfd9b2159c3/xxhash-3.8.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:264710bd335016f303763ce1275c6486df30bb57c2245c91b224c983d7ac39b8", size = 275628 }, + { url = "https://files.pythonhosted.org/packages/13/f5/49fc9e4c6728a5a3bd8fe639199d2fa67609b3a84f938aff6e8568dd3e4f/xxhash-3.8.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e14800b9b10bb39d7a60ad4a310e403164d7b8988a27ae933d4e40618a44088e", size = 231390 }, + { url = "https://files.pythonhosted.org/packages/64/9d/3acaf8f599c0e0b30e910a3a11ba32929da53c86dc73c7c55fe6a010b4e9/xxhash-3.8.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:ea6a3e734b0fd41b82784a400be946821900daebe610c050a5e0760838a34f99", size = 250600 }, + { url = "https://files.pythonhosted.org/packages/23/64/8acab4c5ec60dbe664b5b9858fd44c2413b07e535b09556a0a5022e78aa6/xxhash-3.8.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:cf399fac542a1c7a4734a435b93df2c55e858c7d31abf6c1bdf46f9ae67fbfd0", size = 310032 }, + { url = "https://files.pythonhosted.org/packages/56/47/a0288d7329b1fe63e2734a32d19d444a96ae2b4810f545bc61e561224917/xxhash-3.8.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:44c89d915a75c11d2547eaee9098fcd80398987c4bff2974a0497a925bf92c07", size = 448882 }, + { url = "https://files.pythonhosted.org/packages/01/e7/3071dfd3beb5c38204ce1cf56bf7749fce08de900fa92714b81d1d8ca1f2/xxhash-3.8.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:358650d5bda9c635da699c53adf4e8134af492ecc79c960f917eebf088bb6799", size = 223728 }, + { url = "https://files.pythonhosted.org/packages/12/11/b99949f0ba2b07e9f9ffe83b9c86faa685f9080725dc21a916a607313be5/xxhash-3.8.1-cp314-cp314t-win32.whl", hash = "sha256:c240939e963653054fc7e4a17c382829cda4aa88a7daf0af841715dbded1b497", size = 33150 }, + { url = "https://files.pythonhosted.org/packages/54/1c/09703eb341f8416e74e58d6c6732d4b5c46de59c942363203cb237cc95b0/xxhash-3.8.1-cp314-cp314t-win_amd64.whl", hash = "sha256:7258ee276e8772599bc19e14b36f6260306e21b637190cd7cb489a2449d48684", size = 34005 }, + { url = "https://files.pythonhosted.org/packages/d6/f9/6ed7251bb6a8af10ac73b1821c60583d2826e5b2064e45a979c935287c98/xxhash-3.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:8f454166c2ffed45636c8d501741e649851ba2f346c4eb73a64c07ac00428f20", size = 30239 }, + { url = "https://files.pythonhosted.org/packages/99/e4/4d8040435aeac814fc69ba63621565fbeb19229a138e2568324a26b2a45c/xxhash-3.8.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:39c9d5b61508b0bb68f29e54546de0ed2a74943c6a18585535a7e37356f1dd12", size = 32687 }, + { url = "https://files.pythonhosted.org/packages/da/6a/975f1f2318c760e5bcec109ed379713ae645d8d856c2a3b9ec5d26857087/xxhash-3.8.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:83b9130b80b216d56fdf9e87131946b353c9627930c061955a101ea82b09fed9", size = 29879 }, + { url = "https://files.pythonhosted.org/packages/08/0b/40a2a55ff52cf635bfdc5eae67a772bec85b4f44c6c737f73f6f528d51d1/xxhash-3.8.1-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8304be0982130954b7fd3aad18e2c6f8ee40254bc3d2e635991c16d77c91e2bd", size = 43246 }, + { url = "https://files.pythonhosted.org/packages/9c/6d/56ed2b6b200f26fb474f3fd387d95d0601efcd5bb33430c90c68924bdd77/xxhash-3.8.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b512261801b1e5fde7b6ebf2fef7977339c620cbbca88a0040ad9ad134f4d02", size = 38202 }, + { url = "https://files.pythonhosted.org/packages/0d/a3/56864d895d1161a9f17502088e9c1fb7c06bde2c2efdde620d22bb7a9c43/xxhash-3.8.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49aa8692507835dcc1e8ad8021f20c74c2dc13d83b5112e87877faa2a0035b20", size = 34448 }, + { url = "https://files.pythonhosted.org/packages/6b/57/5c6e0908a47f61dca96d01c8ee6fce01ed1050611eb779083ba8758fed81/xxhash-3.8.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:345b07b78e2bf583d71682aa34ae5b5fab575f7a1cb31e10263ebbc6f89f8c42", size = 32869 }, ] [[package]] name = "yarl" -version = "1.23.0" +version = "1.24.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, { name = "multidict" }, { name = "propcache" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/23/6e/beb1beec874a72f23815c1434518bfc4ed2175065173fb138c3705f658d4/yarl-1.23.0.tar.gz", hash = "sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5", size = 194676 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/aa/60da938b8f0997ba3a911263c40d82b6f645a67902a490b46f3355e10fae/yarl-1.23.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b35d13d549077713e4414f927cdc388d62e543987c572baee613bf82f11a4b99", size = 123641 }, - { url = "https://files.pythonhosted.org/packages/24/84/e237607faf4e099dbb8a4f511cfd5efcb5f75918baad200ff7380635631b/yarl-1.23.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cbb0fef01f0c6b38cb0f39b1f78fc90b807e0e3c86a7ff3ce74ad77ce5c7880c", size = 86248 }, - { url = "https://files.pythonhosted.org/packages/b2/0d/71ceabc14c146ba8ee3804ca7b3d42b1664c8440439de5214d366fec7d3a/yarl-1.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc52310451fc7c629e13c4e061cbe2dd01684d91f2f8ee2821b083c58bd72432", size = 85988 }, - { url = "https://files.pythonhosted.org/packages/8c/6c/4a90d59c572e46b270ca132aca66954f1175abd691f74c1ef4c6711828e2/yarl-1.23.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2c6b50c7b0464165472b56b42d4c76a7b864597007d9c085e8b63e185cf4a7a", size = 100566 }, - { url = "https://files.pythonhosted.org/packages/49/fb/c438fb5108047e629f6282a371e6e91cf3f97ee087c4fb748a1f32ceef55/yarl-1.23.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:aafe5dcfda86c8af00386d7781d4c2181b5011b7be3f2add5e99899ea925df05", size = 92079 }, - { url = "https://files.pythonhosted.org/packages/d9/13/d269aa1aed3e4f50a5a103f96327210cc5fa5dd2d50882778f13c7a14606/yarl-1.23.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9ee33b875f0b390564c1fb7bc528abf18c8ee6073b201c6ae8524aca778e2d83", size = 108741 }, - { url = "https://files.pythonhosted.org/packages/85/fb/115b16f22c37ea4437d323e472945bea97301c8ec6089868fa560abab590/yarl-1.23.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4c41e021bc6d7affb3364dc1e1e5fa9582b470f283748784bd6ea0558f87f42c", size = 108099 }, - { url = "https://files.pythonhosted.org/packages/9a/64/c53487d9f4968045b8afa51aed7ca44f58b2589e772f32745f3744476c82/yarl-1.23.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:99c8a9ed30f4164bc4c14b37a90208836cbf50d4ce2a57c71d0f52c7fb4f7598", size = 102678 }, - { url = "https://files.pythonhosted.org/packages/85/59/cd98e556fbb2bf8fab29c1a722f67ad45c5f3447cac798ab85620d1e70af/yarl-1.23.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2af5c81a1f124609d5f33507082fc3f739959d4719b56877ab1ee7e7b3d602b", size = 100803 }, - { url = "https://files.pythonhosted.org/packages/9e/c0/b39770b56d4a9f0bb5f77e2f1763cd2d75cc2f6c0131e3b4c360348fcd65/yarl-1.23.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6b41389c19b07c760c7e427a3462e8ab83c4bb087d127f0e854c706ce1b9215c", size = 100163 }, - { url = "https://files.pythonhosted.org/packages/e7/64/6980f99ab00e1f0ff67cb84766c93d595b067eed07439cfccfc8fb28c1a6/yarl-1.23.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:1dc702e42d0684f42d6519c8d581e49c96cefaaab16691f03566d30658ee8788", size = 93859 }, - { url = "https://files.pythonhosted.org/packages/38/69/912e6c5e146793e5d4b5fe39ff5b00f4d22463dfd5a162bec565ac757673/yarl-1.23.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0e40111274f340d32ebcc0a5668d54d2b552a6cca84c9475859d364b380e3222", size = 108202 }, - { url = "https://files.pythonhosted.org/packages/59/97/35ca6767524687ad64e5f5c31ad54bc76d585585a9fcb40f649e7e82ffed/yarl-1.23.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:4764a6a7588561a9aef92f65bda2c4fb58fe7c675c0883862e6df97559de0bfb", size = 99866 }, - { url = "https://files.pythonhosted.org/packages/d3/1c/1a3387ee6d73589f6f2a220ae06f2984f6c20b40c734989b0a44f5987308/yarl-1.23.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:03214408cfa590df47728b84c679ae4ef00be2428e11630277be0727eba2d7cc", size = 107852 }, - { url = "https://files.pythonhosted.org/packages/a4/b8/35c0750fcd5a3f781058bfd954515dd4b1eab45e218cbb85cf11132215f1/yarl-1.23.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:170e26584b060879e29fac213e4228ef063f39128723807a312e5c7fec28eff2", size = 102919 }, - { url = "https://files.pythonhosted.org/packages/e5/1c/9a1979aec4a81896d597bcb2177827f2dbee3f5b7cc48b2d0dadb644b41d/yarl-1.23.0-cp311-cp311-win32.whl", hash = "sha256:51430653db848d258336cfa0244427b17d12db63d42603a55f0d4546f50f25b5", size = 82602 }, - { url = "https://files.pythonhosted.org/packages/93/22/b85eca6fa2ad9491af48c973e4c8cf6b103a73dbb271fe3346949449fca0/yarl-1.23.0-cp311-cp311-win_amd64.whl", hash = "sha256:bf49a3ae946a87083ef3a34c8f677ae4243f5b824bfc4c69672e72b3d6719d46", size = 87461 }, - { url = "https://files.pythonhosted.org/packages/93/95/07e3553fe6f113e6864a20bdc53a78113cda3b9ced8784ee52a52c9f80d8/yarl-1.23.0-cp311-cp311-win_arm64.whl", hash = "sha256:b39cb32a6582750b6cc77bfb3c49c0f8760dc18dc96ec9fb55fbb0f04e08b928", size = 82336 }, - { url = "https://files.pythonhosted.org/packages/88/8a/94615bc31022f711add374097ad4144d569e95ff3c38d39215d07ac153a0/yarl-1.23.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1932b6b8bba8d0160a9d1078aae5838a66039e8832d41d2992daa9a3a08f7860", size = 124737 }, - { url = "https://files.pythonhosted.org/packages/e3/6f/c6554045d59d64052698add01226bc867b52fe4a12373415d7991fdca95d/yarl-1.23.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:411225bae281f114067578891bc75534cfb3d92a3b4dfef7a6ca78ba354e6069", size = 87029 }, - { url = "https://files.pythonhosted.org/packages/19/2a/725ecc166d53438bc88f76822ed4b1e3b10756e790bafd7b523fe97c322d/yarl-1.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13a563739ae600a631c36ce096615fe307f131344588b0bc0daec108cdb47b25", size = 86310 }, - { url = "https://files.pythonhosted.org/packages/99/30/58260ed98e6ff7f90ba84442c1ddd758c9170d70327394a6227b310cd60f/yarl-1.23.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cbf44c5cb4a7633d078788e1b56387e3d3cf2b8139a3be38040b22d6c3221c8", size = 97587 }, - { url = "https://files.pythonhosted.org/packages/76/0a/8b08aac08b50682e65759f7f8dde98ae8168f72487e7357a5d684c581ef9/yarl-1.23.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53ad387048f6f09a8969631e4de3f1bf70c50e93545d64af4f751b2498755072", size = 92528 }, - { url = "https://files.pythonhosted.org/packages/52/07/0b7179101fe5f8385ec6c6bb5d0cb9f76bd9fb4a769591ab6fb5cdbfc69a/yarl-1.23.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4a59ba56f340334766f3a4442e0efd0af895fae9e2b204741ef885c446b3a1a8", size = 105339 }, - { url = "https://files.pythonhosted.org/packages/d3/8a/36d82869ab5ec829ca8574dfcb92b51286fcfb1e9c7a73659616362dc880/yarl-1.23.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:803a3c3ce4acc62eaf01eaca1208dcf0783025ef27572c3336502b9c232005e7", size = 105061 }, - { url = "https://files.pythonhosted.org/packages/66/3e/868e5c3364b6cee19ff3e1a122194fa4ce51def02c61023970442162859e/yarl-1.23.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3d2bff8f37f8d0f96c7ec554d16945050d54462d6e95414babaa18bfafc7f51", size = 100132 }, - { url = "https://files.pythonhosted.org/packages/cf/26/9c89acf82f08a52cb52d6d39454f8d18af15f9d386a23795389d1d423823/yarl-1.23.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c75eb09e8d55bceb4367e83496ff8ef2bc7ea6960efb38e978e8073ea59ecb67", size = 99289 }, - { url = "https://files.pythonhosted.org/packages/6f/54/5b0db00d2cb056922356104468019c0a132e89c8d3ab67d8ede9f4483d2a/yarl-1.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877b0738624280e34c55680d6054a307aa94f7d52fa0e3034a9cc6e790871da7", size = 96950 }, - { url = "https://files.pythonhosted.org/packages/f6/40/10fa93811fd439341fad7e0718a86aca0de9548023bbb403668d6555acab/yarl-1.23.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b5405bb8f0e783a988172993cfc627e4d9d00432d6bbac65a923041edacf997d", size = 93960 }, - { url = "https://files.pythonhosted.org/packages/bc/d2/8ae2e6cd77d0805f4526e30ec43b6f9a3dfc542d401ac4990d178e4bf0cf/yarl-1.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c3a3598a832590c5a3ce56ab5576361b5688c12cb1d39429cf5dba30b510760", size = 104703 }, - { url = "https://files.pythonhosted.org/packages/2f/0c/b3ceacf82c3fe21183ce35fa2acf5320af003d52bc1fcf5915077681142e/yarl-1.23.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8419ebd326430d1cbb7efb5292330a2cf39114e82df5cc3d83c9a0d5ebeaf2f2", size = 98325 }, - { url = "https://files.pythonhosted.org/packages/9d/e0/12900edd28bdab91a69bd2554b85ad7b151f64e8b521fe16f9ad2f56477a/yarl-1.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:be61f6fff406ca40e3b1d84716fde398fc08bc63dd96d15f3a14230a0973ed86", size = 105067 }, - { url = "https://files.pythonhosted.org/packages/15/61/74bb1182cf79c9bbe4eb6b1f14a57a22d7a0be5e9cedf8e2d5c2086474c3/yarl-1.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ceb13c5c858d01321b5d9bb65e4cf37a92169ea470b70fec6f236b2c9dd7e34", size = 100285 }, - { url = "https://files.pythonhosted.org/packages/69/7f/cd5ef733f2550de6241bd8bd8c3febc78158b9d75f197d9c7baa113436af/yarl-1.23.0-cp312-cp312-win32.whl", hash = "sha256:fffc45637bcd6538de8b85f51e3df3223e4ad89bccbfca0481c08c7fc8b7ed7d", size = 82359 }, - { url = "https://files.pythonhosted.org/packages/f5/be/25216a49daeeb7af2bec0db22d5e7df08ed1d7c9f65d78b14f3b74fd72fc/yarl-1.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:f69f57305656a4852f2a7203efc661d8c042e6cc67f7acd97d8667fb448a426e", size = 87674 }, - { url = "https://files.pythonhosted.org/packages/d2/35/aeab955d6c425b227d5b7247eafb24f2653fedc32f95373a001af5dfeb9e/yarl-1.23.0-cp312-cp312-win_arm64.whl", hash = "sha256:6e87a6e8735b44816e7db0b2fbc9686932df473c826b0d9743148432e10bb9b9", size = 81879 }, - { url = "https://files.pythonhosted.org/packages/9a/4b/a0a6e5d0ee8a2f3a373ddef8a4097d74ac901ac363eea1440464ccbe0898/yarl-1.23.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:16c6994ac35c3e74fb0ae93323bf8b9c2a9088d55946109489667c510a7d010e", size = 123796 }, - { url = "https://files.pythonhosted.org/packages/67/b6/8925d68af039b835ae876db5838e82e76ec87b9782ecc97e192b809c4831/yarl-1.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4a42e651629dafb64fd5b0286a3580613702b5809ad3f24934ea87595804f2c5", size = 86547 }, - { url = "https://files.pythonhosted.org/packages/ae/50/06d511cc4b8e0360d3c94af051a768e84b755c5eb031b12adaaab6dec6e5/yarl-1.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c6b9461a2a8b47c65eef63bb1c76a4f1c119618ffa99ea79bc5bb1e46c5821b", size = 85854 }, - { url = "https://files.pythonhosted.org/packages/c4/f4/4e30b250927ffdab4db70da08b9b8d2194d7c7b400167b8fbeca1e4701ca/yarl-1.23.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2569b67d616eab450d262ca7cb9f9e19d2f718c70a8b88712859359d0ab17035", size = 98351 }, - { url = "https://files.pythonhosted.org/packages/86/fc/4118c5671ea948208bdb1492d8b76bdf1453d3e73df051f939f563e7dcc5/yarl-1.23.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e9d9a4d06d3481eab79803beb4d9bd6f6a8e781ec078ac70d7ef2dcc29d1bea5", size = 92711 }, - { url = "https://files.pythonhosted.org/packages/56/11/1ed91d42bd9e73c13dc9e7eb0dd92298d75e7ac4dd7f046ad0c472e231cd/yarl-1.23.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f514f6474e04179d3d33175ed3f3e31434d3130d42ec153540d5b157deefd735", size = 106014 }, - { url = "https://files.pythonhosted.org/packages/ce/c9/74e44e056a23fbc33aca71779ef450ca648a5bc472bdad7a82339918f818/yarl-1.23.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fda207c815b253e34f7e1909840fd14299567b1c0eb4908f8c2ce01a41265401", size = 105557 }, - { url = "https://files.pythonhosted.org/packages/66/fe/b1e10b08d287f518994f1e2ff9b6d26f0adeecd8dd7d533b01bab29a3eda/yarl-1.23.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34b6cf500e61c90f305094911f9acc9c86da1a05a7a3f5be9f68817043f486e4", size = 101559 }, - { url = "https://files.pythonhosted.org/packages/72/59/c5b8d94b14e3d3c2a9c20cb100119fd534ab5a14b93673ab4cc4a4141ea5/yarl-1.23.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d7504f2b476d21653e4d143f44a175f7f751cd41233525312696c76aa3dbb23f", size = 100502 }, - { url = "https://files.pythonhosted.org/packages/77/4f/96976cb54cbfc5c9fd73ed4c51804f92f209481d1fb190981c0f8a07a1d7/yarl-1.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:578110dd426f0d209d1509244e6d4a3f1a3e9077655d98c5f22583d63252a08a", size = 98027 }, - { url = "https://files.pythonhosted.org/packages/63/6e/904c4f476471afdbad6b7e5b70362fb5810e35cd7466529a97322b6f5556/yarl-1.23.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:609d3614d78d74ebe35f54953c5bbd2ac647a7ddb9c30a5d877580f5e86b22f2", size = 95369 }, - { url = "https://files.pythonhosted.org/packages/9d/40/acfcdb3b5f9d68ef499e39e04d25e141fe90661f9d54114556cf83be8353/yarl-1.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4966242ec68afc74c122f8459abd597afd7d8a60dc93d695c1334c5fd25f762f", size = 105565 }, - { url = "https://files.pythonhosted.org/packages/5e/c6/31e28f3a6ba2869c43d124f37ea5260cac9c9281df803c354b31f4dd1f3c/yarl-1.23.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e0fd068364a6759bc794459f0a735ab151d11304346332489c7972bacbe9e72b", size = 99813 }, - { url = "https://files.pythonhosted.org/packages/08/1f/6f65f59e72d54aa467119b63fc0b0b1762eff0232db1f4720cd89e2f4a17/yarl-1.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:39004f0ad156da43e86aa71f44e033de68a44e5a31fc53507b36dd253970054a", size = 105632 }, - { url = "https://files.pythonhosted.org/packages/a3/c4/18b178a69935f9e7a338127d5b77d868fdc0f0e49becd286d51b3a18c61d/yarl-1.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e5723c01a56c5028c807c701aa66722916d2747ad737a046853f6c46f4875543", size = 101895 }, - { url = "https://files.pythonhosted.org/packages/8f/54/f5b870b5505663911dba950a8e4776a0dbd51c9c54c0ae88e823e4b874a0/yarl-1.23.0-cp313-cp313-win32.whl", hash = "sha256:1b6b572edd95b4fa8df75de10b04bc81acc87c1c7d16bcdd2035b09d30acc957", size = 82356 }, - { url = "https://files.pythonhosted.org/packages/7a/84/266e8da36879c6edcd37b02b547e2d9ecdfea776be49598e75696e3316e1/yarl-1.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:baaf55442359053c7d62f6f8413a62adba3205119bcb6f49594894d8be47e5e3", size = 87515 }, - { url = "https://files.pythonhosted.org/packages/00/fd/7e1c66efad35e1649114fa13f17485f62881ad58edeeb7f49f8c5e748bf9/yarl-1.23.0-cp313-cp313-win_arm64.whl", hash = "sha256:fb4948814a2a98e3912505f09c9e7493b1506226afb1f881825368d6fb776ee3", size = 81785 }, - { url = "https://files.pythonhosted.org/packages/9c/fc/119dd07004f17ea43bb91e3ece6587759edd7519d6b086d16bfbd3319982/yarl-1.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:aecfed0b41aa72b7881712c65cf764e39ce2ec352324f5e0837c7048d9e6daaa", size = 130719 }, - { url = "https://files.pythonhosted.org/packages/e6/0d/9f2348502fbb3af409e8f47730282cd6bc80dec6630c1e06374d882d6eb2/yarl-1.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a41bcf68efd19073376eb8cf948b8d9be0af26256403e512bb18f3966f1f9120", size = 89690 }, - { url = "https://files.pythonhosted.org/packages/50/93/e88f3c80971b42cfc83f50a51b9d165a1dbf154b97005f2994a79f212a07/yarl-1.23.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cde9a2ecd91668bcb7f077c4966d8ceddb60af01b52e6e3e2680e4cf00ad1a59", size = 89851 }, - { url = "https://files.pythonhosted.org/packages/1c/07/61c9dd8ba8f86473263b4036f70fb594c09e99c0d9737a799dfd8bc85651/yarl-1.23.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5023346c4ee7992febc0068e7593de5fa2bf611848c08404b35ebbb76b1b0512", size = 95874 }, - { url = "https://files.pythonhosted.org/packages/9e/e9/f9ff8ceefba599eac6abddcfb0b3bee9b9e636e96dbf54342a8577252379/yarl-1.23.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1009abedb49ae95b136a8904a3f71b342f849ffeced2d3747bf29caeda218c4", size = 88710 }, - { url = "https://files.pythonhosted.org/packages/eb/78/0231bfcc5d4c8eec220bc2f9ef82cb4566192ea867a7c5b4148f44f6cbcd/yarl-1.23.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a8d00f29b42f534cc8aa3931cfe773b13b23e561e10d2b26f27a8d309b0e82a1", size = 101033 }, - { url = "https://files.pythonhosted.org/packages/cd/9b/30ea5239a61786f18fd25797151a17fbb3be176977187a48d541b5447dd4/yarl-1.23.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:95451e6ce06c3e104556d73b559f5da6c34a069b6b62946d3ad66afcd51642ea", size = 100817 }, - { url = "https://files.pythonhosted.org/packages/62/e2/a4980481071791bc83bce2b7a1a1f7adcabfa366007518b4b845e92eeee3/yarl-1.23.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531ef597132086b6cf96faa7c6c1dcd0361dd5f1694e5cc30375907b9b7d3ea9", size = 97482 }, - { url = "https://files.pythonhosted.org/packages/e5/1e/304a00cf5f6100414c4b5a01fc7ff9ee724b62158a08df2f8170dfc72a2d/yarl-1.23.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88f9fb0116fbfcefcab70f85cf4b74a2b6ce5d199c41345296f49d974ddb4123", size = 95949 }, - { url = "https://files.pythonhosted.org/packages/68/03/093f4055ed4cae649ac53bca3d180bd37102e9e11d048588e9ab0c0108d0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e7b0460976dc75cb87ad9cc1f9899a4b97751e7d4e77ab840fc9b6d377b8fd24", size = 95839 }, - { url = "https://files.pythonhosted.org/packages/b9/28/4c75ebb108f322aa8f917ae10a8ffa4f07cae10a8a627b64e578617df6a0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:115136c4a426f9da976187d238e84139ff6b51a20839aa6e3720cd1026d768de", size = 90696 }, - { url = "https://files.pythonhosted.org/packages/23/9c/42c2e2dd91c1a570402f51bdf066bfdb1241c2240ba001967bad778e77b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ead11956716a940c1abc816b7df3fa2b84d06eaed8832ca32f5c5e058c65506b", size = 100865 }, - { url = "https://files.pythonhosted.org/packages/74/05/1bcd60a8a0a914d462c305137246b6f9d167628d73568505fce3f1cb2e65/yarl-1.23.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:fe8f8f5e70e6dbdfca9882cd9deaac058729bcf323cf7a58660901e55c9c94f6", size = 96234 }, - { url = "https://files.pythonhosted.org/packages/90/b2/f52381aac396d6778ce516b7bc149c79e65bfc068b5de2857ab69eeea3b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a0e317df055958a0c1e79e5d2aa5a5eaa4a6d05a20d4b0c9c3f48918139c9fc6", size = 100295 }, - { url = "https://files.pythonhosted.org/packages/e5/e8/638bae5bbf1113a659b2435d8895474598afe38b4a837103764f603aba56/yarl-1.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f0fd84de0c957b2d280143522c4f91a73aada1923caee763e24a2b3fda9f8a5", size = 97784 }, - { url = "https://files.pythonhosted.org/packages/80/25/a3892b46182c586c202629fc2159aa13975d3741d52ebd7347fd501d48d5/yarl-1.23.0-cp313-cp313t-win32.whl", hash = "sha256:93a784271881035ab4406a172edb0faecb6e7d00f4b53dc2f55919d6c9688595", size = 88313 }, - { url = "https://files.pythonhosted.org/packages/43/68/8c5b36aa5178900b37387937bc2c2fe0e9505537f713495472dcf6f6fccc/yarl-1.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dd00607bffbf30250fe108065f07453ec124dbf223420f57f5e749b04295e090", size = 94932 }, - { url = "https://files.pythonhosted.org/packages/c6/cc/d79ba8292f51f81f4dc533a8ccfb9fc6992cabf0998ed3245de7589dc07c/yarl-1.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ac09d42f48f80c9ee1635b2fcaa819496a44502737660d3c0f2ade7526d29144", size = 84786 }, - { url = "https://files.pythonhosted.org/packages/90/98/b85a038d65d1b92c3903ab89444f48d3cee490a883477b716d7a24b1a78c/yarl-1.23.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:21d1b7305a71a15b4794b5ff22e8eef96ff4a6d7f9657155e5aa419444b28912", size = 124455 }, - { url = "https://files.pythonhosted.org/packages/39/54/bc2b45559f86543d163b6e294417a107bb87557609007c007ad889afec18/yarl-1.23.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:85610b4f27f69984932a7abbe52703688de3724d9f72bceb1cca667deff27474", size = 86752 }, - { url = "https://files.pythonhosted.org/packages/24/f9/e8242b68362bffe6fb536c8db5076861466fc780f0f1b479fc4ffbebb128/yarl-1.23.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23f371bd662cf44a7630d4d113101eafc0cfa7518a2760d20760b26021454719", size = 86291 }, - { url = "https://files.pythonhosted.org/packages/ea/d8/d1cb2378c81dd729e98c716582b1ccb08357e8488e4c24714658cc6630e8/yarl-1.23.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a80f77dc1acaaa61f0934176fccca7096d9b1ff08c8ba9cddf5ae034a24319", size = 99026 }, - { url = "https://files.pythonhosted.org/packages/0a/ff/7196790538f31debe3341283b5b0707e7feb947620fc5e8236ef28d44f72/yarl-1.23.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bd654fad46d8d9e823afbb4f87c79160b5a374ed1ff5bde24e542e6ba8f41434", size = 92355 }, - { url = "https://files.pythonhosted.org/packages/c1/56/25d58c3eddde825890a5fe6aa1866228377354a3c39262235234ab5f616b/yarl-1.23.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:682bae25f0a0dd23a056739f23a134db9f52a63e2afd6bfb37ddc76292bbd723", size = 106417 }, - { url = "https://files.pythonhosted.org/packages/51/8a/882c0e7bc8277eb895b31bce0138f51a1ba551fc2e1ec6753ffc1e7c1377/yarl-1.23.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a82836cab5f197a0514235aaf7ffccdc886ccdaa2324bc0aafdd4ae898103039", size = 106422 }, - { url = "https://files.pythonhosted.org/packages/42/2b/fef67d616931055bf3d6764885990a3ac647d68734a2d6a9e1d13de437a2/yarl-1.23.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c57676bdedc94cd3bc37724cf6f8cd2779f02f6aba48de45feca073e714fe52", size = 101915 }, - { url = "https://files.pythonhosted.org/packages/18/6a/530e16aebce27c5937920f3431c628a29a4b6b430fab3fd1c117b26ff3f6/yarl-1.23.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7f8dc16c498ff06497c015642333219871effba93e4a2e8604a06264aca5c5c", size = 100690 }, - { url = "https://files.pythonhosted.org/packages/88/08/93749219179a45e27b036e03260fda05190b911de8e18225c294ac95bbc9/yarl-1.23.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5ee586fb17ff8f90c91cf73c6108a434b02d69925f44f5f8e0d7f2f260607eae", size = 98750 }, - { url = "https://files.pythonhosted.org/packages/d9/cf/ea424a004969f5d81a362110a6ac1496d79efdc6d50c2c4b2e3ea0fc2519/yarl-1.23.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:17235362f580149742739cc3828b80e24029d08cbb9c4bda0242c7b5bc610a8e", size = 94685 }, - { url = "https://files.pythonhosted.org/packages/e2/b7/14341481fe568e2b0408bcf1484c652accafe06a0ade9387b5d3fd9df446/yarl-1.23.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0793e2bd0cf14234983bbb371591e6bea9e876ddf6896cdcc93450996b0b5c85", size = 106009 }, - { url = "https://files.pythonhosted.org/packages/0a/e6/5c744a9b54f4e8007ad35bce96fbc9218338e84812d36f3390cea616881a/yarl-1.23.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3650dc2480f94f7116c364096bc84b1d602f44224ef7d5c7208425915c0475dd", size = 100033 }, - { url = "https://files.pythonhosted.org/packages/0c/23/e3bfc188d0b400f025bc49d99793d02c9abe15752138dcc27e4eaf0c4a9e/yarl-1.23.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f40e782d49630ad384db66d4d8b73ff4f1b8955dc12e26b09a3e3af064b3b9d6", size = 106483 }, - { url = "https://files.pythonhosted.org/packages/72/42/f0505f949a90b3f8b7a363d6cbdf398f6e6c58946d85c6d3a3bc70595b26/yarl-1.23.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94f8575fbdf81749008d980c17796097e645574a3b8c28ee313931068dad14fe", size = 102175 }, - { url = "https://files.pythonhosted.org/packages/aa/65/b39290f1d892a9dd671d1c722014ca062a9c35d60885d57e5375db0404b5/yarl-1.23.0-cp314-cp314-win32.whl", hash = "sha256:c8aa34a5c864db1087d911a0b902d60d203ea3607d91f615acd3f3108ac32169", size = 83871 }, - { url = "https://files.pythonhosted.org/packages/a9/5b/9b92f54c784c26e2a422e55a8d2607ab15b7ea3349e28359282f84f01d43/yarl-1.23.0-cp314-cp314-win_amd64.whl", hash = "sha256:63e92247f383c85ab00dd0091e8c3fa331a96e865459f5ee80353c70a4a42d70", size = 89093 }, - { url = "https://files.pythonhosted.org/packages/e0/7d/8a84dc9381fd4412d5e7ff04926f9865f6372b4c2fd91e10092e65d29eb8/yarl-1.23.0-cp314-cp314-win_arm64.whl", hash = "sha256:70efd20be968c76ece7baa8dafe04c5be06abc57f754d6f36f3741f7aa7a208e", size = 83384 }, - { url = "https://files.pythonhosted.org/packages/dd/8d/d2fad34b1c08aa161b74394183daa7d800141aaaee207317e82c790b418d/yarl-1.23.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9a18d6f9359e45722c064c97464ec883eb0e0366d33eda61cb19a244bf222679", size = 131019 }, - { url = "https://files.pythonhosted.org/packages/19/ff/33009a39d3ccf4b94d7d7880dfe17fb5816c5a4fe0096d9b56abceea9ac7/yarl-1.23.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2803ed8b21ca47a43da80a6fd1ed3019d30061f7061daa35ac54f63933409412", size = 89894 }, - { url = "https://files.pythonhosted.org/packages/0c/f1/dab7ac5e7306fb79c0190766a3c00b4cb8d09a1f390ded68c85a5934faf5/yarl-1.23.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:394906945aa8b19fc14a61cf69743a868bb8c465efe85eee687109cc540b98f4", size = 89979 }, - { url = "https://files.pythonhosted.org/packages/aa/b1/08e95f3caee1fad6e65017b9f26c1d79877b502622d60e517de01e72f95d/yarl-1.23.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71d006bee8397a4a89f469b8deb22469fe7508132d3c17fa6ed871e79832691c", size = 95943 }, - { url = "https://files.pythonhosted.org/packages/c0/cc/6409f9018864a6aa186c61175b977131f373f1988e198e031236916e87e4/yarl-1.23.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:62694e275c93d54f7ccedcfef57d42761b2aad5234b6be1f3e3026cae4001cd4", size = 88786 }, - { url = "https://files.pythonhosted.org/packages/76/40/cc22d1d7714b717fde2006fad2ced5efe5580606cb059ae42117542122f3/yarl-1.23.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31de1613658308efdb21ada98cbc86a97c181aa050ba22a808120bb5be3ab94", size = 101307 }, - { url = "https://files.pythonhosted.org/packages/8f/0d/476c38e85ddb4c6ec6b20b815bdd779aa386a013f3d8b85516feee55c8dc/yarl-1.23.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb1e8b8d66c278b21d13b0a7ca22c41dd757a7c209c6b12c313e445c31dd3b28", size = 100904 }, - { url = "https://files.pythonhosted.org/packages/72/32/0abe4a76d59adf2081dcb0397168553ece4616ada1c54d1c49d8936c74f8/yarl-1.23.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50f9d8d531dfb767c565f348f33dd5139a6c43f5cbdf3f67da40d54241df93f6", size = 97728 }, - { url = "https://files.pythonhosted.org/packages/b7/35/7b30f4810fba112f60f5a43237545867504e15b1c7647a785fbaf588fac2/yarl-1.23.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575aa4405a656e61a540f4a80eaa5260f2a38fff7bfdc4b5f611840d76e9e277", size = 95964 }, - { url = "https://files.pythonhosted.org/packages/2d/86/ed7a73ab85ef00e8bb70b0cb5421d8a2a625b81a333941a469a6f4022828/yarl-1.23.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:041b1a4cefacf65840b4e295c6985f334ba83c30607441ae3cf206a0eed1a2e4", size = 95882 }, - { url = "https://files.pythonhosted.org/packages/19/90/d56967f61a29d8498efb7afb651e0b2b422a1e9b47b0ab5f4e40a19b699b/yarl-1.23.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d38c1e8231722c4ce40d7593f28d92b5fc72f3e9774fe73d7e800ec32299f63a", size = 90797 }, - { url = "https://files.pythonhosted.org/packages/72/00/8b8f76909259f56647adb1011d7ed8b321bcf97e464515c65016a47ecdf0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d53834e23c015ee83a99377db6e5e37d8484f333edb03bd15b4bc312cc7254fb", size = 101023 }, - { url = "https://files.pythonhosted.org/packages/ac/e2/cab11b126fb7d440281b7df8e9ddbe4851e70a4dde47a202b6642586b8d9/yarl-1.23.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2e27c8841126e017dd2a054a95771569e6070b9ee1b133366d8b31beb5018a41", size = 96227 }, - { url = "https://files.pythonhosted.org/packages/c2/9b/2c893e16bfc50e6b2edf76c1a9eb6cb0c744346197e74c65e99ad8d634d0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:76855800ac56f878847a09ce6dba727c93ca2d89c9e9d63002d26b916810b0a2", size = 100302 }, - { url = "https://files.pythonhosted.org/packages/28/ec/5498c4e3a6d5f1003beb23405671c2eb9cdbf3067d1c80f15eeafe301010/yarl-1.23.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e09fd068c2e169a7070d83d3bde728a4d48de0549f975290be3c108c02e499b4", size = 98202 }, - { url = "https://files.pythonhosted.org/packages/fe/c3/cd737e2d45e70717907f83e146f6949f20cc23cd4bf7b2688727763aa458/yarl-1.23.0-cp314-cp314t-win32.whl", hash = "sha256:73309162a6a571d4cbd3b6a1dcc703c7311843ae0d1578df6f09be4e98df38d4", size = 90558 }, - { url = "https://files.pythonhosted.org/packages/e1/19/3774d162f6732d1cfb0b47b4140a942a35ca82bb19b6db1f80e9e7bdc8f8/yarl-1.23.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4503053d296bc6e4cbd1fad61cf3b6e33b939886c4f249ba7c78b602214fabe2", size = 97610 }, - { url = "https://files.pythonhosted.org/packages/51/47/3fa2286c3cb162c71cdb34c4224d5745a1ceceb391b2bd9b19b668a8d724/yarl-1.23.0-cp314-cp314t-win_arm64.whl", hash = "sha256:44bb7bef4ea409384e3f8bc36c063d77ea1b8d4a5b2706956c0d6695f07dcc25", size = 86041 }, - { url = "https://files.pythonhosted.org/packages/69/68/c8739671f5699c7dc470580a4f821ef37c32c4cb0b047ce223a7f115757f/yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", size = 48288 }, +sdist = { url = "https://files.pythonhosted.org/packages/79/12/1e8f37460ea0f7eb59c221fdaf0ed75e7ac43e97f8093b9c6f411df50a78/yarl-1.24.2.tar.gz", hash = "sha256:9ac374123c6fd7abf64d1fec93962b0bd4ee2c19751755a762a72dd96c0378f8", size = 210798 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/c5/1ce244152ff2839645e7cae92f90e7bafcb2c52bea7ff586ac714f14f5df/yarl-1.24.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:36348bebb147b83818b9d7e673ea4debc75970afc6ffdc7e3975ad05ce5a58c1", size = 128971 }, + { url = "https://files.pythonhosted.org/packages/87/5a/00f36967203ed89cb3acd2c8ed526cc3fed9418eb70ce128160a911c8499/yarl-1.24.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a97e42c8a2233f2f279ecadd9e4a037bcb5d813b78435e8eedd4db5a9e9708c", size = 91507 }, + { url = "https://files.pythonhosted.org/packages/31/d0/1fb0c1cd27288f39f6974da4318c32768d72c9890984541fdf1e2e32a51d/yarl-1.24.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8d027d56f1035e339d1001ac33eceab5b2ec8e42e449787bb75e289fb9a5cd1d", size = 91343 }, + { url = "https://files.pythonhosted.org/packages/03/ce/d4a646508bed2f8dec6435b40166fe9308dd191262033d3f307b2bbcaecd/yarl-1.24.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a6377060e7927187a42b7eb202090cbe2b34933a4eeaf90e3bd9e33432e5cae", size = 105704 }, + { url = "https://files.pythonhosted.org/packages/4b/07/b3278e82d8bc41485bcf6d856cd0433262593de615b1d3dc43bd3f5bead4/yarl-1.24.2-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:17076578bce0049a5ce57d14ad1bded391b68a3b213e9b81b0097b090244999a", size = 97281 }, + { url = "https://files.pythonhosted.org/packages/17/5b/4cee6e7c92e487bebe7afc797da0aa54a248ab4e776a68fe369ec29665a5/yarl-1.24.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:50713f1d4d6be6375bb178bb43d140ee1acb8abe589cd723320b7925a275be1e", size = 114020 }, + { url = "https://files.pythonhosted.org/packages/5c/82/111076571545a7d4f9cca3fbd5c6f40615af58642be09f12328f48022468/yarl-1.24.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:34263e2fa8fb5bb63a0d97706cda38edbad62fddb58c7f12d6acbc092812aa50", size = 111450 }, + { url = "https://files.pythonhosted.org/packages/b6/ec/08f671f69a444d704aeecebf92af659b67b97a869942411d0a578b08c334/yarl-1.24.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49016d82f032b1bd1e10b01078a7d29ae71bf468eeae0ea22df8bab691e60003", size = 106384 }, + { url = "https://files.pythonhosted.org/packages/e5/86/ce41e7a7a199340b2330d52b60f25c4074b6636dd0e60b1a80d31a9db042/yarl-1.24.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3f6d2c216318f8f32038ca3f72501ba08536f0fd18a36e858836b121b2deed9f", size = 106153 }, + { url = "https://files.pythonhosted.org/packages/c4/5d/31be8a729531ab3e55ac3e7e5c800be8c89ea98947f418b2f6ea259fb6ee/yarl-1.24.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:08d3a33218e0c64393e7610284e770409a9c31c429b078bcb24096ed0a783b8f", size = 105322 }, + { url = "https://files.pythonhosted.org/packages/47/9b/b57afb22b386ae87ac9940f09878b98d8c333f89113e6fc96fcf4ca9eb64/yarl-1.24.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:5d699376c4ca3cba49bbfae3a05b5b70ded572937171ce1e0b8d87118e2ba294", size = 99057 }, + { url = "https://files.pythonhosted.org/packages/a3/4f/06348c27c8389256c313e8a57d796808fc0264c915dd5e7cfd3c0e314dc7/yarl-1.24.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a1cab588b4fa14bea2e55ebea27478adfb05372f47573738e1acc4a36c0b05d2", size = 113502 }, + { url = "https://files.pythonhosted.org/packages/5f/1c/284f307b298e4a17b7943b07d9d7ecc4151537f8d137ba51f3bb6c31ca20/yarl-1.24.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:ec87ccc31bd21db7ad009d8572c127c1000f268517618a4cc09adba3c2a7f21c", size = 105253 }, + { url = "https://files.pythonhosted.org/packages/c8/bf/0de123bec8619e45c80cbded9085f61b5b4a9eddb8abe6d25d28ee1ec866/yarl-1.24.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d1dd47a22843b212baa8d74f37796815d43bd046b42a0f41e9da433386c3136b", size = 111345 }, + { url = "https://files.pythonhosted.org/packages/90/af/0248eb065e51129d2a9b2436cd1b5c772c19a6b04e5b6a186955671e3319/yarl-1.24.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7b54b9c67c2b06bd7b9a77253d242124b9c95d2c02def5a1144001ee547dd9d5", size = 106558 }, + { url = "https://files.pythonhosted.org/packages/21/3c/f960d7a65ef97d8ba9b424fb5128796a4bc710fc6df2ddbbd7dfdc3bbd20/yarl-1.24.2-cp311-cp311-win_amd64.whl", hash = "sha256:f8fdbcff8b2c7c9284e60c196f693588598ddcee31e11c18e14949ce44519d45", size = 92808 }, + { url = "https://files.pythonhosted.org/packages/03/1a/49fb03750e4de4d2284cd5b885a383133c34eef45bd59631b2bb8b7e81e8/yarl-1.24.2-cp311-cp311-win_arm64.whl", hash = "sha256:b32c37a7a337e90822c45797bf3d79d60875cfcccd3ecc80e9f453d87026c122", size = 87610 }, + { url = "https://files.pythonhosted.org/packages/f0/da/866bcb01076ba49d2b42b309867bed3826421f1c479655eb7a607b44f20b/yarl-1.24.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b975866c184564c827e0877380f0dae57dcca7e52782128381b72feff6dfceb8", size = 129957 }, + { url = "https://files.pythonhosted.org/packages/bf/1d/fcefb70922ea2268a8971d8e5874d9a8218644200fb8465f1dcad55e6851/yarl-1.24.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3b075301a2836a0e297b1b658cb6d6135df535d62efefdd60366bd589c2c82f2", size = 92164 }, + { url = "https://files.pythonhosted.org/packages/29/b6/170e2b8d4e3bc30e6bfdcca53556537f5bf595e938632dfcb059311f3ff6/yarl-1.24.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ae44649b00947634ab0dab2a374a638f52923a6e67083f2c156cd5cbd1a881d", size = 91688 }, + { url = "https://files.pythonhosted.org/packages/fe/a5/c9f655d5553ea0b99fdac9d6a99ad3f9b3e73b8e5758bb46f58c9831f74c/yarl-1.24.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:507cc19f0b45454e2d6dcd62ff7d062b9f77a2812404e62dbdaec05b50faa035", size = 102902 }, + { url = "https://files.pythonhosted.org/packages/5d/bc/6b9664d815d79af4ee553337f9d606c56bbf269186ada9172de45f1b5f60/yarl-1.24.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4c17bad5a530912d2111825d3f05e89bab2dd376aaa8cbc77e449e6db63e576", size = 97931 }, + { url = "https://files.pythonhosted.org/packages/98/ec/32ba48acae30fecd60928f5791188b80a9d6ee3840507ffda29fecd37b71/yarl-1.24.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f5f0cbb112838a4a293985b6ed73948a547dadcc1ba6d2089938e7abdedceef8", size = 111030 }, + { url = "https://files.pythonhosted.org/packages/82/5a/6f4cd081e5f4934d2ae3a8ef4abe3afacc010d26f0035ee91b35cd7d7c37/yarl-1.24.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ec8356b8a6afcf81fc7aeeef13b1ff7a49dec00f313394bbb9e83830d32ccd7", size = 110392 }, + { url = "https://files.pythonhosted.org/packages/7a/da/323a01c349bd5fb01bb6652e314d9bb218cee630a736bdb810ad50e4013f/yarl-1.24.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e7ebcdef69dec6c6451e616f32b622a6d4a2e92b445c992f7c8e5274a6bbc4c", size = 105612 }, + { url = "https://files.pythonhosted.org/packages/7c/80/264ab684f181e1a876389374519ff05d10248725535ae2ac4e8ac4e563d6/yarl-1.24.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:47a55d6cf6db2f401017a9e96e5288844e5051911fb4e0c8311a3980f5e59a7d", size = 104487 }, + { url = "https://files.pythonhosted.org/packages/41/07/efabe5df87e96d7ad5959760b888344be48cd6884db127b407c6b5503adc/yarl-1.24.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3065657c80a2321225e804048597ad55658a7e76b32d6f5ee4074d04c50401db", size = 102333 }, + { url = "https://files.pythonhosted.org/packages/44/0c/bcf7c42603e1009295f586d8890f2ba032c8b53310e815adf0a202c73d9f/yarl-1.24.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:cb84b80d88e19ede158619b80813968713d8d008b0e2497a576e6a0557d50712", size = 99025 }, + { url = "https://files.pythonhosted.org/packages/4f/82/84482ab1a57a0f21a08afe6a7004c61d741f8f2ecc3b05c321577c612164/yarl-1.24.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:990de4f680b1c217e77ff0d6aa0029f9eb79889c11fb3e9a3942c7eba29c1996", size = 110507 }, + { url = "https://files.pythonhosted.org/packages/c4/8d/a546ba1dfe1b0f290e05fef145cd07614c0f15df1a707195e512d1e39d1d/yarl-1.24.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:abb8ec0323b80161e3802da3150ef660b41d0e9be2048b76a363d93eee992c2b", size = 103719 }, + { url = "https://files.pythonhosted.org/packages/1a/b6/267f2a09213138473adfce6b8a6e17791d7fee70bd4d9003218e4dec58b0/yarl-1.24.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e7977781f83638a4c73e0f88425563d70173e0dfd90ac006a45c65036293ee3c", size = 110438 }, + { url = "https://files.pythonhosted.org/packages/48/2d/1c8d89c7c5f9cad9fb2902445d94e2ab1d7aa35de029afbb8ae95c42d00f/yarl-1.24.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e30dd55825dc554ec5b66a94953b8eda8745926514c5089dfcacecb9c99b5bd1", size = 105719 }, + { url = "https://files.pythonhosted.org/packages/a7/25/722e3b93bd687009afb2d59a35e13d30ddd8f80571445bb0c4e4ce26ec66/yarl-1.24.2-cp312-cp312-win_amd64.whl", hash = "sha256:7dafe10c12ddd4d120d528c4b5599c953bd7b12845347d507b95451195bb6cad", size = 92901 }, + { url = "https://files.pythonhosted.org/packages/39/47/4486ccfb674c04854a1ef8aa77868b6a6f765feaf69633409d7ca4f02cb8/yarl-1.24.2-cp312-cp312-win_arm64.whl", hash = "sha256:044a09d8401fcf8681977faef6d286b8ade1e2d2e9dceda175d1cfa5ca496f30", size = 87229 }, + { url = "https://files.pythonhosted.org/packages/82/62/fcf0ce677f17e5c471c06311dd25964be38a4c586993632910d2e75278bc/yarl-1.24.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:491ac9141decf49ee8030199e1ee251cdff0e131f25678817ff6aa5f837a3536", size = 128978 }, + { url = "https://files.pythonhosted.org/packages/d3/58/8e63299bb71ed61a834121d9d3fe6c9fcf2a6a5d09754ff4f20f2d20baf5/yarl-1.24.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e89418f65eda18f99030386305bd44d7d504e328a7945db1ead514fbe03a0607", size = 91733 }, + { url = "https://files.pythonhosted.org/packages/c1/24/16748d5dab6daec8b0ed81ccec639a1cded0f18dcc62a4f696b4fe366c37/yarl-1.24.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cdfcce633b4a4bb8281913c57fcafd4b5933fbc19111a5e3930bbd299d6102f1", size = 91113 }, + { url = "https://files.pythonhosted.org/packages/1b/66/b63fff7b71211e866624b21432d5943cbb633eb0c2872d9ee3070648f22c/yarl-1.24.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:863297ddede92ee49024e9a9b11ecb59f310ca85b60d8537f56bed9bbb5b1986", size = 103899 }, + { url = "https://files.pythonhosted.org/packages/9d/ac/ba1974b8533909636f7733fe86cf677e3619527c3c2fa913e0ea89c48757/yarl-1.24.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:374423f70754a2c96942ede36a29d37dc6b0cb8f92f8d009ddf3ed78d3da5488", size = 97862 }, + { url = "https://files.pythonhosted.org/packages/1b/a5/123ac993b5c2ba6f554a140305620cb8f150fa543711bbc49be3ec0a65a4/yarl-1.24.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33a29b5d00ccbf3219bb3e351d7875739c19481e030779f48cc46a7a71681a9b", size = 111060 }, + { url = "https://files.pythonhosted.org/packages/23/37/c472d3af3509688392134a88a825276770a187f1daa4de3f6dc0a327a751/yarl-1.24.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a9532c57211730c515341af11fef6e9b61d157487272a096d0c04da445642592", size = 110613 }, + { url = "https://files.pythonhosted.org/packages/df/88/09c28dad91e662ccfaa1b78f1c57badde74fc9d0b23e74aef644750ecd73/yarl-1.24.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:91e72cf093fd833483a97ee648e0c053c7c629f51ff4a0e7edd84f806b0c5617", size = 107012 }, + { url = "https://files.pythonhosted.org/packages/07/ab/9d4f69d571a94f4d112fa7e2e007200f5a54d319f58c82ac7b7baa61f5c6/yarl-1.24.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b3177bc0a768ef3bacceb4f272632990b7bea352f1b2f1eee9d6d6ff16516f92", size = 105887 }, + { url = "https://files.pythonhosted.org/packages/8e/9a/000b2b66c0d772a499fc531d21dab92dfeb73b640a12eed6ba89f49bb2d0/yarl-1.24.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e196952aacaf3b232e265ff02980b64d483dc0972bd49bcb061171ff22ac203a", size = 103620 }, + { url = "https://files.pythonhosted.org/packages/41/7c/7c1050f73450fbdaa3f0c72017059f00ce5e13366692f3dba25275a1083d/yarl-1.24.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:204e7a61ce99919c0de1bf904ab5d7aa188a129ea8f690a8f76cfb6e2844dc44", size = 100599 }, + { url = "https://files.pythonhosted.org/packages/ec/b1/29e5756b3926705f5f6089bd5b9f50a56eaac550da6e260bf713ead44d04/yarl-1.24.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b156914620f0b9d78dc1adb3751141daee561cfec796088abb89ed49d220f1a", size = 110604 }, + { url = "https://files.pythonhosted.org/packages/a3/4b/8415bc96e9b150cde942fbac9a8182985e58f40ce5c54c34ed015407d3ee/yarl-1.24.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8372a2b976cf70654b2be6619ab6068acabb35f724c0fda7b277fbf53d66a5cf", size = 105161 }, + { url = "https://files.pythonhosted.org/packages/8b/d4/cde059abfa229553b7298a2eadde2752e723d50aeedaef86ce59da2718ee/yarl-1.24.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f9a1e9b622ca284143aab5d885848686dcd85453bb1ca9abcdb7503e64dc0056", size = 110619 }, + { url = "https://files.pythonhosted.org/packages/e7/2c/d6a6c9a61549f7b6c7e6dc6937d195bcf069582b47b7200dcd0e7b256acf/yarl-1.24.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:810e19b685c8c3c5862f6a38160a1f4e4c0916c9390024ec347b6157a45a0992", size = 107362 }, + { url = "https://files.pythonhosted.org/packages/92/dd/3ae5fe417e9d1c353a548553326eb9935e76b6b727161563b424cc296df3/yarl-1.24.2-cp313-cp313-win_amd64.whl", hash = "sha256:7d37fb7c38f2b6edab0f845c4f85148d4c44204f52bc127021bd2bc9fdbf1656", size = 92667 }, + { url = "https://files.pythonhosted.org/packages/10/cc/a7beb239f78f27fca1b053c8e8595e4179c02e62249b4687ec218c370c50/yarl-1.24.2-cp313-cp313-win_arm64.whl", hash = "sha256:1e831894be7c2954240e49791fa4b50c05a0dc881de2552cfe3ffd8631c7f461", size = 87069 }, + { url = "https://files.pythonhosted.org/packages/40/0e/e08087695fc12789263821c5dc0f8dc52b5b17efd0887cacf419f8a43ba3/yarl-1.24.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:f9312b3c02d9b3d23840f67952913c9c8721d7f1b7db305289faefa878f364c2", size = 129670 }, + { url = "https://files.pythonhosted.org/packages/3a/98/ab4b5ed1b1b5cd973c8a3eb994c3a6aefb6ce6d399e21bb5f0316c33815c/yarl-1.24.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a4f4d6cd615823bfc7fb7e9b5987c3f41666371d870d51058f77e2680fbe9630", size = 91916 }, + { url = "https://files.pythonhosted.org/packages/ba/b1/5297bb6a7df4782f7605bffc43b31f5044070935fbbcaa6c705a07e6ac65/yarl-1.24.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0c3063e5c0a8e8e62fae6c2596fa01da1561e4cd1da6fec5789f5cf99a8aefd8", size = 91625 }, + { url = "https://files.pythonhosted.org/packages/02/a7/45baabfff76829264e623b185cff0c340d7e11bf3e1cd9ea37e7d17934bd/yarl-1.24.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fecd17873a096036c1c87ab3486f1aef7f269ada7f23f7f856f93b1cc7744f14", size = 104574 }, + { url = "https://files.pythonhosted.org/packages/f3/40/3a5ab144d3d650ca37d4f4b57e56169be8af3ca34c448793e064b30baaed/yarl-1.24.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a46d1ab4ba4d32e6dc80daf8a28ce0bd83d08df52fbc32f3e288663427734535", size = 97534 }, + { url = "https://files.pythonhosted.org/packages/9c/b5/5658fef3681fb5776b4513b052bec750009f47b3a592251c705d75375798/yarl-1.24.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:73e68edf6dfd5f73f9ca127d84e2a6f9213c65bdffb736bda19524c0564fcd14", size = 111481 }, + { url = "https://files.pythonhosted.org/packages/4c/06/fdcd7dde037f00866dce123ed4ba23dba94beb56fc4cf561668d27be37f2/yarl-1.24.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a296ca617f2d25fbceafb962b88750d627e5984e75732c712154d058ae8d79a3", size = 111529 }, + { url = "https://files.pythonhosted.org/packages/c2/53/d81269aaafccea0d33396c03035de997b743f11e648e6e27a0df99c72980/yarl-1.24.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51b2cf5ec89a8b8470177641ed62a3ba22d74e1e898e06ad53aa77972487208", size = 107338 }, + { url = "https://files.pythonhosted.org/packages/ae/04/23049463f729bd899df203a7960505a75333edd499cda8aa1d5a82b64df5/yarl-1.24.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:310fc687f7b2044ec54e372c8cbe923bb88f5c37bded0d3079e5791c2fc3cf50", size = 106147 }, + { url = "https://files.pythonhosted.org/packages/14/18/04a4b5830b43ed5e4c5015b40e9f6241ad91487d71611061b4e111d6ac80/yarl-1.24.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:297a2fe352ecf858b30a98f87948746ec16f001d279f84aebdbd3bd965e2f1bd", size = 104272 }, + { url = "https://files.pythonhosted.org/packages/5a/f7/8cffdf319aee7a7c1dbd07b61d91c3e3fda460c7a93b5f93e445f3806c4c/yarl-1.24.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2a263e76b97bc42bdcd7c5f4953dec1f7cd62a1112fa7f869e57255229390d67", size = 99962 }, + { url = "https://files.pythonhosted.org/packages/d7/39/b3cce3b7dbef64ac700ad4cea156a207d01bede0f507587616c364b5468e/yarl-1.24.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:822519b64cf0b474f1a0aaef1dc621438ea46bb77c94df97a5b4d213a7d8a8b1", size = 111063 }, + { url = "https://files.pythonhosted.org/packages/a1/ea/100818505e7ebf165c7242ff17fdf7d9fee79e27234aeca871c1082920d7/yarl-1.24.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b6067060d9dc594899ba83e6db6c48c68d1e494a6dab158156ed86977ca7bcb1", size = 105438 }, + { url = "https://files.pythonhosted.org/packages/8f/d2/e075a0b32aa6625087de9e653087df0759fed5de4a435fef594181102a77/yarl-1.24.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:0063adad533e57171b79db3943b229d40dfafeeee579767f96541f106bac5f1b", size = 111458 }, + { url = "https://files.pythonhosted.org/packages/e6/5c/ceea7ba98b65c8eb8d947fdc52f9bedfcd43c6a57c9e3c90c17be8f324a3/yarl-1.24.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ee8e3fb34513e8dc082b586ef4910c98335d43a6fab688cd44d4851bacfce3e8", size = 107589 }, + { url = "https://files.pythonhosted.org/packages/fa/d9/5582d57e2b2db9b85eb6663a22efdd78e08805f3f5389566e9fcad254d1b/yarl-1.24.2-cp314-cp314-win_amd64.whl", hash = "sha256:afb00d7fd8e0f285ca29a44cc50df2d622ff2f7a6d933fa641577b5f9d5f3db0", size = 94424 }, + { url = "https://files.pythonhosted.org/packages/92/10/7dc07a0e22806a9280f42a57361395506e800c64e22737cd7b0886feab42/yarl-1.24.2-cp314-cp314-win_arm64.whl", hash = "sha256:68cf6eacd6028ef1142bc4b48376b81566385ca6f9e7dde3b0fa91be08ffcb57", size = 88690 }, + { url = "https://files.pythonhosted.org/packages/9e/13/d5b8e2c8667db955bcb3de233f18798fefe7edf1d7429c2c9d4f9c401114/yarl-1.24.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:221ce1dd921ac4f603957f17d7c18c5cc0797fbb52f156941f92e04605d1d67b", size = 136248 }, + { url = "https://files.pythonhosted.org/packages/de/46/a4a97c05c9c9b8fd266bb2a0df12992c7fbd02391eb9640583411b6dab32/yarl-1.24.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5f3224db28173a00d7afacdee07045cc4673dfab2b15492c7ae10deddbece761", size = 95084 }, + { url = "https://files.pythonhosted.org/packages/95/b2/845cf2074a015e6fe0d0808cf1a2d9e868386c4220d657ebd8302b199043/yarl-1.24.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c557165320d6244ebe3a02431b2a201a20080e02f41f0cfa0ccc47a183765da8", size = 95272 }, + { url = "https://files.pythonhosted.org/packages/fe/16/e69d4aa244aef45235ddfebc0e04036a6829842bc5a6a795aedc6c998d23/yarl-1.24.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:904065e6e85b1fa54d0d87438bd58c14c0bad97aad654ad1077fd9d87e8478ed", size = 101497 }, + { url = "https://files.pythonhosted.org/packages/15/94/c07107715d621076863ee88b3ddf183fa5e9d4aba5769623c9979828410a/yarl-1.24.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8cec2a38d70edc10e0e856ceda886af5327a017ccbde8e1de1bd44d300357543", size = 94002 }, + { url = "https://files.pythonhosted.org/packages/a9/35/fc1bbdd895b5e4010b8fdd037f7ed3aa289d3863e08231b30231ca9a0815/yarl-1.24.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e7484b9361ed222ee1ca5b4337aa4cbdcc4618ce5aff57d9ef1582fd95893fc0", size = 106524 }, + { url = "https://files.pythonhosted.org/packages/1f/f2/32b66d0a4ba47c296cf86d03e2c67bff58399fe6d6d84d5205c04c66cc6d/yarl-1.24.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:84f9670b89f34db07f81e53aee83e0b938a3412329d51c8f922488be7fcc4024", size = 106165 }, + { url = "https://files.pythonhosted.org/packages/95/47/37cb5ff50c5e825d4d38e81bb04d1b7e96bf960f7ab89f9850b162f3f114/yarl-1.24.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:abb2759733d63a28b4956500a5dd57140f26486c92b2caedfb964ab7d9b79dbf", size = 103010 }, + { url = "https://files.pythonhosted.org/packages/6f/d2/4597912315096f7bb359e46e13bf8b60994fcbb2db29b804c0902ef4eff5/yarl-1.24.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:081c2bf54efe03774d0311172bc04fedf9ca01e644d4cd8c805688e527209bdc", size = 101128 }, + { url = "https://files.pythonhosted.org/packages/b9/d5/c8e86e120521e646013d02a8e3b8884392e28494be8f392366e50d208efc/yarl-1.24.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:86746bef442aa479107fe28132e1277237f9c24c2f00b0b0cf22b3ee0904f2bb", size = 101382 }, + { url = "https://files.pythonhosted.org/packages/fa/98/70b229236118f89dbeb739b76f10225bbf53b5497725502594c9a01d699a/yarl-1.24.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:2d07d21d0bc4b17558e8de0b02fbfdf1e347d3bb3699edd00bb92e7c57925420", size = 95964 }, + { url = "https://files.pythonhosted.org/packages/87/f8/56c386981e3c8648d279fdef2397ffec577e8320fd5649745e34d54faeb7/yarl-1.24.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:4fb1ac3fc5fecd8ae7453ea237e4d22b49befa70266dfe1629924245c21a0c7f", size = 106204 }, + { url = "https://files.pythonhosted.org/packages/1a/1e/765afe97811ca35933e2a7de70ac57b1997ea2e4ee895719ee7a231fb7e5/yarl-1.24.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4da31a5512ed1729ca8d8aacde3f7faeb8843cde3165d6bcf7f88f74f17bb8aa", size = 101510 }, + { url = "https://files.pythonhosted.org/packages/ee/78/393913f4b9039e1edd09ae8a9bbb9d539be909a8abf6d8a2084585bed4b7/yarl-1.24.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:533ded4dceb5f1f3da7906244f4e82cf46cfd40d84c69a1faf5ac506aa65ecbe", size = 105584 }, + { url = "https://files.pythonhosted.org/packages/78/87/deb17b7049bbe74ea11a713b86f8f27800cc1c8648b0b797243ebb4830ba/yarl-1.24.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7b3a85525f6e7eeabcfdd372862b21ee1915db1b498a04e8bf0e389b607ff0bd", size = 103410 }, + { url = "https://files.pythonhosted.org/packages/8f/be/f9f7594e23b5b93affff0318e4593c1920331bcaefda326cabcad94296a1/yarl-1.24.2-cp314-cp314t-win_amd64.whl", hash = "sha256:a7624b1ca46ca5d7b864ef0d2f8efe3091454085ee1855b4e992314529972215", size = 102980 }, + { url = "https://files.pythonhosted.org/packages/65/a4/ba80dccd3593ff1f01051a818694d07b58cb8232677ee9a22a5a1f93a9fc/yarl-1.24.2-cp314-cp314t-win_arm64.whl", hash = "sha256:e434a45ce2e7a947f951fc5a8944c8cc080b7e59f9c50ae80fd39107cf88126d", size = 91219 }, + { url = "https://files.pythonhosted.org/packages/fd/4d/4b880086bd0d3e034d25647be1d830afc3e3f610e98c4ab3490af6b1b6d5/yarl-1.24.2-py3-none-any.whl", hash = "sha256:2783d9226db8797636cd6896e4de81feed252d1db72265686c9558d97a4d94b9", size = 53576 }, ] [[package]] name = "zipp" -version = "3.23.0" +version = "4.1.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 } +sdist = { url = "https://files.pythonhosted.org/packages/b9/d8/eab98a517c14134c0b2eb4e2387bc5f457334293ec5d2dd3857ec2966802/zipp-4.1.0.tar.gz", hash = "sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602", size = 26214 } wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276 }, + { url = "https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl", hash = "sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f", size = 10238 }, ]