From 1431444006a30f0e3690c051f0f15c6f201e91d2 Mon Sep 17 00:00:00 2001 From: Pringled Date: Mon, 4 May 2026 15:19:33 +0200 Subject: [PATCH 01/17] Add semble stats command and saved token tracking --- src/semble/cli.py | 77 ++++++++++++++++++++++++++++++++++++++- src/semble/index/index.py | 22 +++++++---- src/semble/stats.py | 45 +++++++++++++++++++++++ 3 files changed, 136 insertions(+), 8 deletions(-) create mode 100644 src/semble/stats.py diff --git a/src/semble/cli.py b/src/semble/cli.py index 2f209f30e..6f04cb6c7 100644 --- a/src/semble/cli.py +++ b/src/semble/cli.py @@ -1,14 +1,17 @@ import argparse import asyncio +import json import sys +from datetime import datetime, timezone from importlib.resources import files from pathlib import Path from semble.index import SembleIndex +from semble.stats import _STATS_FILE from semble.utils import _format_results, _is_git_url, _resolve_chunk _CLAUDE_FILE_PATH = Path(".claude") / "agents" / "semble-search.md" -_CLI_DISPATCH_ARGS = frozenset({"search", "find-related", "init", "-h", "--help"}) +_CLI_DISPATCH_ARGS = frozenset({"search", "find-related", "init", "stats", "-h", "--help"}) def main() -> None: @@ -49,6 +52,72 @@ def _run_init(*, force: bool = False) -> None: print(f"Created {dest}") +def _parse_stats() -> tuple[dict[str, dict[str, int]], dict[str, int]]: + """Read stats.jsonl and return (period_buckets, call_type_counts).""" + now = datetime.now(timezone.utc) + today = now.date().isoformat() + this_week = now.isocalendar()[:2] + + buckets: dict[str, dict[str, int]] = { + "Today": {"calls": 0, "snippet_chars": 0, "file_chars": 0}, + "This week": {"calls": 0, "snippet_chars": 0, "file_chars": 0}, + "All time": {"calls": 0, "snippet_chars": 0, "file_chars": 0}, + } + call_type_counts: dict[str, int] = {} + + with _STATS_FILE.open() as f: + for line in f: + try: + r = json.loads(line) + except json.JSONDecodeError: + continue + sc, fc = r.get("snippet_chars", 0), r.get("file_chars", 0) + ct = r.get("call", "search") + call_type_counts[ct] = call_type_counts.get(ct, 0) + 1 + for b in buckets.values(): + b["calls"] += 1 + b["snippet_chars"] += sc + b["file_chars"] += fc + try: + dt = datetime.fromisoformat(r.get("ts", "")) + if dt.date().isoformat() != today: + buckets["Today"]["calls"] -= 1 + buckets["Today"]["snippet_chars"] -= sc + buckets["Today"]["file_chars"] -= fc + if dt.isocalendar()[:2] != this_week: + buckets["This week"]["calls"] -= 1 + buckets["This week"]["snippet_chars"] -= sc + buckets["This week"]["file_chars"] -= fc + except ValueError: + pass + + return buckets, call_type_counts + + +def _run_stats() -> None: + """Print a summary of semble usage and token savings from ~/.semble/stats.jsonl.""" + if not _STATS_FILE.exists(): + print("No stats yet. Run a search first.") + return + + buckets, call_type_counts = _parse_stats() + + print("\nSemble stats") + print("─" * 48) + print(f" {'Period':<12} {'Calls':>6} {'Tokens saved':>14}") + print(f" {'──────':<12} {'─────':>6} {'────────────':>14}") + for label, b in buckets.items(): + t = max(0, b["file_chars"] - b["snippet_chars"]) // 4 + saved = f"~{t / 1000:.0f}k" if t >= 1000 else f"~{t}" + print(f" {label:<12} {b['calls']:>6} {saved:>14}") + print() + if call_type_counts: + print(" Call breakdown:") + for ct, n in sorted(call_type_counts.items()): + print(f" {ct:<16} {n}") + print() + + def _cli_main() -> None: parser = argparse.ArgumentParser(prog="semble") sub = parser.add_subparsers(dest="command") @@ -70,12 +139,18 @@ def _cli_main() -> None: init_p = sub.add_parser("init", help="Write .claude/agents/semble-search.md for Claude Code sub-agent support.") init_p.add_argument("--force", action="store_true", help="Overwrite if the file already exists.") + sub.add_parser("stats", help="Show token savings and usage stats.") + args = parser.parse_args() if args.command == "init": _run_init(force=args.force) return + if args.command == "stats": + _run_stats() + return + index = SembleIndex.from_git(args.path) if _is_git_url(args.path) else SembleIndex.from_path(args.path) if args.command == "search": diff --git a/src/semble/index/index.py b/src/semble/index/index.py index fcfef8055..d6de221de 100644 --- a/src/semble/index/index.py +++ b/src/semble/index/index.py @@ -12,6 +12,7 @@ from semble.index.create import create_index_from_path from semble.index.dense import SelectableBasicBackend, load_model from semble.search import search_bm25, search_hybrid, search_semantic +from semble.stats import log_search_stats from semble.types import Chunk, Encoder, IndexStats, SearchMode, SearchResult @@ -36,6 +37,7 @@ def __init__( self.chunks: list[Chunk] = chunks self._bm25_index: BM25 = bm25_index self._semantic_index: SelectableBasicBackend = semantic_index + self._root_path: Path | None = None self._file_mapping, self._language_mapping = self._populate_mapping() def _populate_mapping(self) -> tuple[dict[str, list[int]], dict[str, list[int]]]: @@ -101,6 +103,7 @@ def from_path( ) index = SembleIndex(model, bm25, vicinity, chunks) + index._root_path = path return index @@ -164,7 +167,9 @@ def find_related(self, source: Chunk | SearchResult, *, top_k: int = 5) -> list[ target = source.chunk if isinstance(source, SearchResult) else source selector = self._get_selector_vector(filter_languages=[target.language]) if target.language else None results = search_semantic(target.content, self.model, self._semantic_index, self.chunks, top_k + 1, selector) - return [r for r in results if r.chunk != target][:top_k] + results = [r for r in results if r.chunk != target][:top_k] + log_search_stats(results, "find_related", self._root_path) + return results def _get_selector_vector( self, filter_languages: list[str] | None = None, filter_paths: list[str] | None = None @@ -209,11 +214,14 @@ def search( selector = self._get_selector_vector(filter_languages, filter_paths) if mode == SearchMode.BM25: - return search_bm25(query, bm25_index, self.chunks, top_k, selector=selector) - if mode == SearchMode.SEMANTIC: - return search_semantic(query, self.model, semantic_index, self.chunks, top_k, selector=selector) - if mode == SearchMode.HYBRID: - return search_hybrid( + results = search_bm25(query, bm25_index, self.chunks, top_k, selector=selector) + elif mode == SearchMode.SEMANTIC: + results = search_semantic(query, self.model, semantic_index, self.chunks, top_k, selector=selector) + elif mode == SearchMode.HYBRID: + results = search_hybrid( query, self.model, semantic_index, bm25_index, self.chunks, top_k, alpha=alpha, selector=selector ) - raise ValueError(f"Unknown search mode: {mode!r}") + else: + raise ValueError(f"Unknown search mode: {mode!r}") + log_search_stats(results, "search", self._root_path) + return results diff --git a/src/semble/stats.py b/src/semble/stats.py new file mode 100644 index 000000000..8e1169170 --- /dev/null +++ b/src/semble/stats.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +import json +from datetime import datetime, timezone +from pathlib import Path + +from semble.types import SearchResult + +_STATS_FILE = Path.home() / ".semble" / "stats.jsonl" + + +def log_search_stats( + results: list[SearchResult], + call_type: str, + root_path: Path | None = None, +) -> None: + """Append token-savings stats for one search/find_related call. Failures are silently ignored.""" + try: + snippet_chars = sum(len(r.chunk.content) for r in results) + + file_chars = 0 + if root_path is not None: + seen: set[str] = set() + for r in results: + fp = r.chunk.file_path + if fp in seen: + continue + seen.add(fp) + try: + file_chars += len((root_path / fp).read_text(encoding="utf-8", errors="replace")) + except OSError: + pass + + record = { + "ts": datetime.now(timezone.utc).isoformat(), + "call": call_type, + "results": len(results), + "snippet_chars": snippet_chars, + "file_chars": file_chars, + } + _STATS_FILE.parent.mkdir(parents=True, exist_ok=True) + with _STATS_FILE.open("a") as f: + f.write(json.dumps(record) + "\n") + except Exception: + pass From 93ff6b6778ff13e49c4c46b59f8375573fc15e85 Mon Sep 17 00:00:00 2001 From: Pringled Date: Thu, 7 May 2026 10:55:11 +0200 Subject: [PATCH 02/17] Updated savings --- README.md | 14 ++++++++------ src/semble/cli.py | 20 +++++++++++++------- src/semble/stats.py | 2 +- tests/test_cli.py | 2 +- uv.lock | 40 ---------------------------------------- 5 files changed, 23 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index d793e2b63..6ec49c7a1 100644 --- a/README.md +++ b/README.md @@ -197,16 +197,18 @@ semble savings --verbose # also show breakdown by call type ``` ``` -────────────────────────────────────────────────────── - Period Calls Savings - Today 11 [██████████████░░] ~94.6k tokens (85%) - Last 7 days 11 [██████████████░░] ~94.6k tokens (85%) - All time 11 [██████████████░░] ~94.6k tokens (85%) + Semble Token Savings + ════════════════════════════════════════════════════════════════ + Period Calls Savings + ──────────────────────────────────────────────────────────────── + Today 19 [█████████████░░░] ~122.1k tokens (77%) + Last 7 days 125 [█████████████░░░] ~575.0k tokens (83%) + All time 125 [█████████████░░░] ~575.0k tokens (83%) ``` **How savings are calculated:** for each search call, semble records the total character count of the source files that contained matching chunks — what an agent would read in full without semble — and the character count of the snippets actually returned. Tokens saved is `(file chars − snippet chars) / 4`, using the standard 4 characters-per-token approximation. The percentage is the reduction versus reading matched files in full; the true savings are higher still, since without semble an agent would also scan files that produced no matches. -Stats are stored in `~/.semble/stats.jsonl`. +Stats are stored in `~/.semble/savings.jsonl`. ### Updating diff --git a/src/semble/cli.py b/src/semble/cli.py index 78eb37f39..05eb312b2 100644 --- a/src/semble/cli.py +++ b/src/semble/cli.py @@ -59,7 +59,7 @@ def _run_init(*, force: bool = False) -> None: def _parse_stats() -> tuple[dict[str, dict[str, int]], dict[str, int]]: - """Read stats.jsonl and return (period_buckets, call_type_counts).""" + """Read savings.jsonl and return (period_buckets, call_type_counts).""" now = datetime.now(timezone.utc) today = now.date().isoformat() seven_days_ago = (now - timedelta(days=7)).date() @@ -101,7 +101,7 @@ def _parse_stats() -> tuple[dict[str, dict[str, int]], dict[str, int]]: def _run_stats(*, verbose: bool = False) -> None: - """Print a summary of semble usage and token savings from ~/.semble/stats.jsonl.""" + """Print a summary of semble usage and token savings from ~/.semble/savings.jsonl.""" if not _STATS_FILE.exists(): print("No stats yet. Run a search first.") return @@ -109,9 +109,12 @@ def _run_stats(*, verbose: bool = False) -> None: buckets, call_type_counts = _parse_stats() _BAR_WIDTH = 16 + _RULE_WIDTH = 64 print() - print("─" * 56) + print(" Semble Token Savings") + print(" " + "═" * _RULE_WIDTH) print(f" {'Period':<12} {'Calls':<6} Savings") + print(" " + "─" * _RULE_WIDTH) for label, b in buckets.items(): saved_chars = max(0, b["file_chars"] - b["snippet_chars"]) saved_tokens = saved_chars // 4 @@ -124,12 +127,15 @@ def _run_stats(*, verbose: bool = False) -> None: print(f" {label:<12} {b['calls']:<6} [{bar}] {saved_str} tokens ({pct}%)") else: print(f" {label:<12} {b['calls']:<6} [{'░' * _BAR_WIDTH}] {saved_str} tokens") - print() if verbose and call_type_counts: - print(" Usage breakdown:") - for ct, n in sorted(call_type_counts.items()): - print(f" {ct:<16} {n}") print() + print(" Usage Breakdown") + print(" " + "─" * _RULE_WIDTH) + print(f" {'Call type':<16} Calls") + for ct, n in sorted(call_type_counts.items()): + print(f" {ct:<16} {n}") + print(" " + "═" * _RULE_WIDTH) + print() def _cli_main() -> None: diff --git a/src/semble/stats.py b/src/semble/stats.py index d797ed347..993ba33ea 100644 --- a/src/semble/stats.py +++ b/src/semble/stats.py @@ -6,7 +6,7 @@ from semble.types import SearchResult -_STATS_FILE = Path.home() / ".semble" / "stats.jsonl" +_STATS_FILE = Path.home() / ".semble" / "savings.jsonl" def log_search_stats( diff --git a/tests/test_cli.py b/tests/test_cli.py index 618f3c9c4..5bcf4a614 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -237,7 +237,7 @@ def test_stats_verbose_shows_breakdown( monkeypatch.setattr("semble.cli._STATS_FILE", stats_file) _run_stats(verbose=True) out = capsys.readouterr().out - assert "Usage breakdown" in out + assert "Usage Breakdown" in out assert "search" in out assert "find_related" in out diff --git a/uv.lock b/uv.lock index 85a219659..9e99f5f44 100644 --- a/uv.lock +++ b/uv.lock @@ -31,25 +31,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, ] -[[package]] -name = "anthropic" -version = "0.99.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "distro" }, - { name = "docstring-parser" }, - { name = "httpx" }, - { name = "jiter" }, - { name = "pydantic" }, - { name = "sniffio" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0d/c9/e8a3a1caeab575e80551b30b084096b5a430abc52739a526a1daaadd038c/anthropic-0.99.0.tar.gz", hash = "sha256:16f41e00f215ed2d193b146be3dd567c4319c32ed3af6c8725d68ba875257c1c", size = 727239, upload-time = "2026-05-05T16:03:07.986Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/84/d0917506744e1707cf55659a57f1e3ff952eda5636df0ffffe3e884b7c61/anthropic-0.99.0-py3-none-any.whl", hash = "sha256:c44469b746ab2ef19a4c52dcbdb98e17bc95c60bebdd18ec40d76d2d23592b49", size = 700564, upload-time = "2026-05-05T16:03:06.059Z" }, -] - [[package]] name = "anyio" version = "4.13.0" @@ -777,15 +758,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, ] -[[package]] -name = "docstring-parser" -version = "0.18.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e0/4d/f332313098c1de1b2d2ff91cf2674415cc7cddab2ca1b01ae29774bd5fdf/docstring_parser-0.18.0.tar.gz", hash = "sha256:292510982205c12b1248696f44959db3cdd1740237a968ea1e2e7a900eeb2015", size = 29341, upload-time = "2026-04-14T04:09:19.867Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/5f/ed01f9a3cdffbd5a008556fc7b2a08ddb1cc6ace7effa7340604b1d16699/docstring_parser-0.18.0-py3-none-any.whl", hash = "sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b", size = 22484, upload-time = "2026-04-14T04:09:18.638Z" }, -] - [[package]] name = "docstring-parser-fork" version = "0.0.14" @@ -3372,12 +3344,6 @@ mcp = [ { name = "watchfiles" }, ] -[package.dev-dependencies] -dev = [ - { name = "anthropic" }, - { name = "openai" }, -] - [package.metadata] requires-dist = [ { name = "bm25s", specifier = ">=0.2.0" }, @@ -3403,12 +3369,6 @@ requires-dist = [ ] provides-extras = ["mcp", "benchmark", "dev"] -[package.metadata.requires-dev] -dev = [ - { name = "anthropic", specifier = ">=0.99.0" }, - { name = "openai", specifier = ">=2.34.0" }, -] - [[package]] name = "sentence-transformers" version = "5.4.1" From 688533f542979d15338c2426a3a7795701adbfd2 Mon Sep 17 00:00:00 2001 From: Pringled Date: Thu, 7 May 2026 12:41:26 +0200 Subject: [PATCH 03/17] Update tests --- README.md | 4 +- tests/test_cli.py | 99 +-------------------------------------------- tests/test_stats.py | 94 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 98 insertions(+), 99 deletions(-) diff --git a/README.md b/README.md index 6ec49c7a1..805324d32 100644 --- a/README.md +++ b/README.md @@ -196,6 +196,8 @@ semble savings # summary by period semble savings --verbose # also show breakdown by call type ``` +> Savings are measured against reading matched files in full. True savings versus grep+read are higher still, since grep+read also scans every file that produces no matches. + ``` Semble Token Savings ════════════════════════════════════════════════════════════════ @@ -206,7 +208,7 @@ semble savings --verbose # also show breakdown by call type All time 125 [█████████████░░░] ~575.0k tokens (83%) ``` -**How savings are calculated:** for each search call, semble records the total character count of the source files that contained matching chunks — what an agent would read in full without semble — and the character count of the snippets actually returned. Tokens saved is `(file chars − snippet chars) / 4`, using the standard 4 characters-per-token approximation. The percentage is the reduction versus reading matched files in full; the true savings are higher still, since without semble an agent would also scan files that produced no matches. +**How savings are calculated:** for each call, semble records the total character count of files containing matching chunks and the character count of the snippets actually returned. Tokens saved is `(file chars − snippet chars) / 4`, using the standard 4 characters-per-token approximation. Stats are stored in `~/.semble/savings.jsonl`. diff --git a/tests/test_cli.py b/tests/test_cli.py index 5bcf4a614..0520b7aee 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,13 +1,11 @@ -import json import sys -from datetime import datetime, timezone from importlib.resources import files from pathlib import Path from unittest.mock import MagicMock, patch import pytest -from semble.cli import _CLAUDE_FILE_PATH, _cli_main, _parse_stats, _run_init, _run_stats, main +from semble.cli import _CLAUDE_FILE_PATH, _cli_main, _run_init, main from semble.types import SearchMode, SearchResult from tests.conftest import make_chunk @@ -197,101 +195,6 @@ def test_mcp_main_exits_with_message_when_extras_missing( assert "pip install 'semble[mcp]'" in capsys.readouterr().err -def _make_stats_record(ts: str, call: str = "search", snippet_chars: int = 100, file_chars: int = 500) -> str: - return json.dumps({"ts": ts, "call": call, "results": 3, "snippet_chars": snippet_chars, "file_chars": file_chars}) - - -def test_stats_no_file(tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]) -> None: - """_run_stats prints a friendly message when no stats file exists yet.""" - monkeypatch.setattr("semble.cli._STATS_FILE", tmp_path / "nonexistent.jsonl") - _run_stats() - assert "No stats yet" in capsys.readouterr().out - - -def test_stats_valid_records( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] -) -> None: - """_run_stats displays period buckets and savings header.""" - stats_file = tmp_path / "stats.jsonl" - now = datetime.now(timezone.utc).isoformat() - stats_file.write_text( - _make_stats_record(now, call="search") + "\n" + _make_stats_record(now, call="find_related") + "\n" - ) - monkeypatch.setattr("semble.cli._STATS_FILE", stats_file) - _run_stats() - out = capsys.readouterr().out - assert "Savings" in out - assert "Savings" in out - assert "Today" in out - - -def test_stats_verbose_shows_breakdown( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] -) -> None: - """_run_stats --verbose adds a usage breakdown by call type.""" - stats_file = tmp_path / "stats.jsonl" - now = datetime.now(timezone.utc).isoformat() - stats_file.write_text( - _make_stats_record(now, call="search") + "\n" + _make_stats_record(now, call="find_related") + "\n" - ) - monkeypatch.setattr("semble.cli._STATS_FILE", stats_file) - _run_stats(verbose=True) - out = capsys.readouterr().out - assert "Usage Breakdown" in out - assert "search" in out - assert "find_related" in out - - -@pytest.mark.parametrize( - "bad_line", - [ - "not valid json", - json.dumps({"ts": "not-a-date", "call": "search", "snippet_chars": 100, "file_chars": 500}), - ], - ids=["malformed-json", "malformed-timestamp"], -) -def test_stats_tolerates_bad_records( - bad_line: str, tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] -) -> None: - """_run_stats skips bad JSON and bad timestamps without crashing.""" - stats_file = tmp_path / "stats.jsonl" - stats_file.write_text(bad_line + "\n") - monkeypatch.setattr("semble.cli._STATS_FILE", stats_file) - _run_stats() - assert "Savings" in capsys.readouterr().out - - -@pytest.mark.parametrize( - ("argv", "expected"), - [ - (["semble", "savings"], "No stats yet"), - (["semble", "savings", "--verbose"], "No stats yet"), - ], - ids=["default", "verbose"], -) -def test_stats_cli_dispatch( - argv: list[str], expected: str, tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] -) -> None: - """Semble stats subcommand dispatches to _run_stats, with and without --verbose.""" - monkeypatch.setattr(sys, "argv", argv) - monkeypatch.setattr("semble.cli._STATS_FILE", tmp_path / "nonexistent.jsonl") - _cli_main() - assert expected in capsys.readouterr().out - - -def test_stats_buckets_exclude_old_records(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: - """Records older than 7 days count in All time but not Today or Last 7 days.""" - stats_file = tmp_path / "stats.jsonl" - old_ts = "2020-01-01T00:00:00+00:00" - now_ts = datetime.now(timezone.utc).isoformat() - stats_file.write_text(_make_stats_record(old_ts) + "\n" + _make_stats_record(now_ts) + "\n") - monkeypatch.setattr("semble.cli._STATS_FILE", stats_file) - buckets, _ = _parse_stats() - assert buckets["All time"]["calls"] == 2 - assert buckets["Today"]["calls"] == 1 - assert buckets["Last 7 days"]["calls"] == 1 - - def test_agent_file_tools_are_bash_only() -> None: """The agent file must list only Bash and Read — no MCP tools that require schema loading.""" frontmatter = _CLAUDE_AGENT_FILE.split("---")[1] diff --git a/tests/test_stats.py b/tests/test_stats.py index dd4d6bb41..a1b23a066 100644 --- a/tests/test_stats.py +++ b/tests/test_stats.py @@ -1,14 +1,33 @@ import json +import sys +from datetime import datetime, timezone from pathlib import Path from unittest.mock import MagicMock import pytest +from semble.cli import _cli_main, _parse_stats, _run_stats from semble.stats import log_search_stats from semble.types import SearchMode, SearchResult from tests.conftest import make_chunk +def _make_stats_record(ts: str, call: str = "search", snippet_chars: int = 100, file_chars: int = 500) -> str: + return json.dumps({"ts": ts, "call": call, "results": 3, "snippet_chars": snippet_chars, "file_chars": file_chars}) + + +@pytest.fixture +def sample_stats_file(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path: + """Fixture for a stats file with two records.""" + stats_file = tmp_path / "stats.jsonl" + now = datetime.now(timezone.utc).isoformat() + stats_file.write_text( + _make_stats_record(now, call="search") + "\n" + _make_stats_record(now, call="find_related") + "\n" + ) + monkeypatch.setattr("semble.cli._STATS_FILE", stats_file) + return stats_file + + def test_log_search_stats_deduplicates_file_paths(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: """Two results from the same file are counted as one file in file_chars.""" chunk = make_chunk("hello", "src/foo.py") @@ -27,3 +46,78 @@ def test_log_search_stats_silences_write_errors(monkeypatch: pytest.MonkeyPatch) mock_path.open.side_effect = OSError("no write") monkeypatch.setattr("semble.stats._STATS_FILE", mock_path) log_search_stats([], "search") # must not raise + + +def test_savings_no_file(tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]) -> None: + """_run_stats prints a friendly message when no stats file exists yet.""" + monkeypatch.setattr("semble.cli._STATS_FILE", tmp_path / "nonexistent.jsonl") + _run_stats() + assert "No stats yet" in capsys.readouterr().out + + +@pytest.mark.parametrize( + ("verbose", "expected"), + [ + (False, ["Savings", "Today"]), + (True, ["Savings", "Today", "Usage Breakdown", "search", "find_related"]), + ], + ids=["default", "verbose"], +) +def test_savings_output( + sample_stats_file: Path, verbose: bool, expected: list[str], capsys: pytest.CaptureFixture[str] +) -> None: + """_run_stats displays period buckets; --verbose adds call-type breakdown.""" + _run_stats(verbose=verbose) + out = capsys.readouterr().out + for s in expected: + assert s in out + + +@pytest.mark.parametrize( + "bad_line", + [ + "not valid json", + json.dumps({"ts": "not-a-date", "call": "search", "snippet_chars": 100, "file_chars": 500}), + ], + ids=["malformed-json", "malformed-timestamp"], +) +def test_savings_tolerates_bad_records( + bad_line: str, tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +) -> None: + """_run_stats skips bad JSON and bad timestamps without crashing.""" + stats_file = tmp_path / "stats.jsonl" + stats_file.write_text(bad_line + "\n") + monkeypatch.setattr("semble.cli._STATS_FILE", stats_file) + _run_stats() + assert "Savings" in capsys.readouterr().out + + +@pytest.mark.parametrize( + ("argv", "expected"), + [ + (["semble", "savings"], "No stats yet"), + (["semble", "savings", "--verbose"], "No stats yet"), + ], + ids=["default", "verbose"], +) +def test_savings_cli_dispatch( + argv: list[str], expected: str, tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +) -> None: + """Savings subcommand dispatches to _run_stats, with and without --verbose.""" + monkeypatch.setattr(sys, "argv", argv) + monkeypatch.setattr("semble.cli._STATS_FILE", tmp_path / "nonexistent.jsonl") + _cli_main() + assert expected in capsys.readouterr().out + + +def test_savings_buckets_exclude_old_records(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """Records older than 7 days count in All time but not Today or Last 7 days.""" + stats_file = tmp_path / "stats.jsonl" + old_ts = "2020-01-01T00:00:00+00:00" + now_ts = datetime.now(timezone.utc).isoformat() + stats_file.write_text(_make_stats_record(old_ts) + "\n" + _make_stats_record(now_ts) + "\n") + monkeypatch.setattr("semble.cli._STATS_FILE", stats_file) + buckets, _ = _parse_stats() + assert buckets["All time"]["calls"] == 2 + assert buckets["Today"]["calls"] == 1 + assert buckets["Last 7 days"]["calls"] == 1 From 12963abf6956a985afc8f14dcb4085120f3357ed Mon Sep 17 00:00:00 2001 From: Pringled Date: Thu, 7 May 2026 12:53:32 +0200 Subject: [PATCH 04/17] Improve code quality --- src/semble/cli.py | 76 ++++++++++++++++++++------------------- src/semble/index/index.py | 6 ++-- src/semble/stats.py | 14 +++----- 3 files changed, 47 insertions(+), 49 deletions(-) diff --git a/src/semble/cli.py b/src/semble/cli.py index 05eb312b2..42ed028a8 100644 --- a/src/semble/cli.py +++ b/src/semble/cli.py @@ -2,6 +2,7 @@ import asyncio import json import sys +from collections import defaultdict from datetime import datetime, timedelta, timezone from importlib.resources import files from importlib.util import find_spec @@ -58,10 +59,16 @@ def _run_init(*, force: bool = False) -> None: print(f"Created {dest}") +def _add_to_bucket(bucket: dict[str, int], snippet_chars: int, file_chars: int) -> None: + bucket["calls"] += 1 + bucket["snippet_chars"] += snippet_chars + bucket["file_chars"] += file_chars + + def _parse_stats() -> tuple[dict[str, dict[str, int]], dict[str, int]]: """Read savings.jsonl and return (period_buckets, call_type_counts).""" now = datetime.now(timezone.utc) - today = now.date().isoformat() + today = now.date() seven_days_ago = (now - timedelta(days=7)).date() buckets: dict[str, dict[str, int]] = { @@ -69,33 +76,29 @@ def _parse_stats() -> tuple[dict[str, dict[str, int]], dict[str, int]]: "Last 7 days": {"calls": 0, "snippet_chars": 0, "file_chars": 0}, "All time": {"calls": 0, "snippet_chars": 0, "file_chars": 0}, } - call_type_counts: dict[str, int] = {} + call_type_counts: defaultdict[str, int] = defaultdict(int) with _STATS_FILE.open() as f: for line in f: try: - r = json.loads(line) + record = json.loads(line) except json.JSONDecodeError: continue - sc, fc = r.get("snippet_chars", 0), r.get("file_chars", 0) - ct = r.get("call", "search") - call_type_counts[ct] = call_type_counts.get(ct, 0) + 1 - for b in buckets.values(): - b["calls"] += 1 - b["snippet_chars"] += sc - b["file_chars"] += fc + snippet_chars = record.get("snippet_chars", 0) + file_chars = record.get("file_chars", 0) + call_type = record.get("call", "search") + call_type_counts[call_type] += 1 try: - dt = datetime.fromisoformat(r.get("ts", "")) - if dt.date().isoformat() != today: - buckets["Today"]["calls"] -= 1 - buckets["Today"]["snippet_chars"] -= sc - buckets["Today"]["file_chars"] -= fc - if dt.date() <= seven_days_ago: - buckets["Last 7 days"]["calls"] -= 1 - buckets["Last 7 days"]["snippet_chars"] -= sc - buckets["Last 7 days"]["file_chars"] -= fc + record_date = datetime.fromisoformat(record.get("ts", "")).date() + in_today = record_date == today + in_last_7 = record_date > seven_days_ago except ValueError: - pass + in_today = in_last_7 = True + _add_to_bucket(buckets["All time"], snippet_chars, file_chars) + if in_last_7: + _add_to_bucket(buckets["Last 7 days"], snippet_chars, file_chars) + if in_today: + _add_to_bucket(buckets["Today"], snippet_chars, file_chars) return buckets, call_type_counts @@ -108,33 +111,34 @@ def _run_stats(*, verbose: bool = False) -> None: buckets, call_type_counts = _parse_stats() - _BAR_WIDTH = 16 - _RULE_WIDTH = 64 + bar_width = 16 + heavy_line = " " + "═" * 64 + light_line = " " + "─" * 64 print() print(" Semble Token Savings") - print(" " + "═" * _RULE_WIDTH) + print(heavy_line) print(f" {'Period':<12} {'Calls':<6} Savings") - print(" " + "─" * _RULE_WIDTH) - for label, b in buckets.items(): - saved_chars = max(0, b["file_chars"] - b["snippet_chars"]) + print(light_line) + for label, bucket in buckets.items(): + saved_chars = max(0, bucket["file_chars"] - bucket["snippet_chars"]) saved_tokens = saved_chars // 4 saved_str = f"~{saved_tokens / 1000:.1f}k" if saved_tokens >= 1000 else f"~{saved_tokens}" - if b["file_chars"] > 0: - ratio = saved_chars / b["file_chars"] - filled = round(ratio * _BAR_WIDTH) - bar = "█" * filled + "░" * (_BAR_WIDTH - filled) + if bucket["file_chars"] > 0: + ratio = saved_chars / bucket["file_chars"] + filled = round(ratio * bar_width) + bar = "█" * filled + "░" * (bar_width - filled) pct = round(ratio * 100) - print(f" {label:<12} {b['calls']:<6} [{bar}] {saved_str} tokens ({pct}%)") + print(f" {label:<12} {bucket['calls']:<6} [{bar}] {saved_str} tokens ({pct}%)") else: - print(f" {label:<12} {b['calls']:<6} [{'░' * _BAR_WIDTH}] {saved_str} tokens") + print(f" {label:<12} {bucket['calls']:<6} [{'░' * bar_width}] {saved_str} tokens") if verbose and call_type_counts: print() print(" Usage Breakdown") - print(" " + "─" * _RULE_WIDTH) + print(light_line) print(f" {'Call type':<16} Calls") - for ct, n in sorted(call_type_counts.items()): - print(f" {ct:<16} {n}") - print(" " + "═" * _RULE_WIDTH) + for call_type, count in sorted(call_type_counts.items()): + print(f" {call_type:<16} {count}") + print(heavy_line) print() diff --git a/src/semble/index/index.py b/src/semble/index/index.py index ae5b9d98d..c03ebfd24 100644 --- a/src/semble/index/index.py +++ b/src/semble/index/index.py @@ -60,11 +60,11 @@ def _compute_file_sizes(root: Path, chunks: list[Chunk]) -> dict[str, int]: """Return a mapping of repo-relative file path → total character count.""" sizes: dict[str, int] = {} for chunk in chunks: - fp = chunk.file_path - if fp in sizes: + file_path = chunk.file_path + if file_path in sizes: continue try: - sizes[fp] = len((root / fp).read_text(encoding="utf-8", errors="replace")) + sizes[file_path] = len((root / file_path).read_text(encoding="utf-8", errors="replace")) except OSError: pass return sizes diff --git a/src/semble/stats.py b/src/semble/stats.py index 993ba33ea..61e6c8600 100644 --- a/src/semble/stats.py +++ b/src/semble/stats.py @@ -16,17 +16,11 @@ def log_search_stats( ) -> None: """Append token-savings stats for one search/find_related call. Failures are silently ignored.""" try: - snippet_chars = sum(len(r.chunk.content) for r in results) - - file_chars = 0 + snippet_chars = sum(len(result.chunk.content) for result in results) if file_sizes: - seen: set[str] = set() - for r in results: - fp = r.chunk.file_path - if fp in seen: - continue - seen.add(fp) - file_chars += file_sizes.get(fp, 0) + file_chars = sum(file_sizes.get(path, 0) for path in {result.chunk.file_path for result in results}) + else: + file_chars = 0 record = { "ts": datetime.now(timezone.utc).isoformat(), From cdee40b8439ba78197bd6c34fef433b3959ee1a1 Mon Sep 17 00:00:00 2001 From: Pringled Date: Thu, 7 May 2026 13:09:41 +0200 Subject: [PATCH 05/17] Move logic over to stats.py --- src/semble/cli.py | 90 +-------------------------------------- src/semble/stats.py | 101 +++++++++++++++++++++++++++++++++++++++++++- tests/test_stats.py | 53 +++++++++-------------- 3 files changed, 123 insertions(+), 121 deletions(-) diff --git a/src/semble/cli.py b/src/semble/cli.py index 42ed028a8..71fdaf46e 100644 --- a/src/semble/cli.py +++ b/src/semble/cli.py @@ -1,9 +1,6 @@ import argparse import asyncio -import json import sys -from collections import defaultdict -from datetime import datetime, timedelta, timezone from importlib.resources import files from importlib.util import find_spec from pathlib import Path @@ -11,7 +8,7 @@ from model2vec.utils import get_package_extras from semble.index import SembleIndex -from semble.stats import _STATS_FILE +from semble.stats import format_savings_report from semble.utils import _format_results, _is_git_url, _resolve_chunk _CLAUDE_FILE_PATH = Path(".claude") / "agents" / "semble-search.md" @@ -59,89 +56,6 @@ def _run_init(*, force: bool = False) -> None: print(f"Created {dest}") -def _add_to_bucket(bucket: dict[str, int], snippet_chars: int, file_chars: int) -> None: - bucket["calls"] += 1 - bucket["snippet_chars"] += snippet_chars - bucket["file_chars"] += file_chars - - -def _parse_stats() -> tuple[dict[str, dict[str, int]], dict[str, int]]: - """Read savings.jsonl and return (period_buckets, call_type_counts).""" - now = datetime.now(timezone.utc) - today = now.date() - seven_days_ago = (now - timedelta(days=7)).date() - - buckets: dict[str, dict[str, int]] = { - "Today": {"calls": 0, "snippet_chars": 0, "file_chars": 0}, - "Last 7 days": {"calls": 0, "snippet_chars": 0, "file_chars": 0}, - "All time": {"calls": 0, "snippet_chars": 0, "file_chars": 0}, - } - call_type_counts: defaultdict[str, int] = defaultdict(int) - - with _STATS_FILE.open() as f: - for line in f: - try: - record = json.loads(line) - except json.JSONDecodeError: - continue - snippet_chars = record.get("snippet_chars", 0) - file_chars = record.get("file_chars", 0) - call_type = record.get("call", "search") - call_type_counts[call_type] += 1 - try: - record_date = datetime.fromisoformat(record.get("ts", "")).date() - in_today = record_date == today - in_last_7 = record_date > seven_days_ago - except ValueError: - in_today = in_last_7 = True - _add_to_bucket(buckets["All time"], snippet_chars, file_chars) - if in_last_7: - _add_to_bucket(buckets["Last 7 days"], snippet_chars, file_chars) - if in_today: - _add_to_bucket(buckets["Today"], snippet_chars, file_chars) - - return buckets, call_type_counts - - -def _run_stats(*, verbose: bool = False) -> None: - """Print a summary of semble usage and token savings from ~/.semble/savings.jsonl.""" - if not _STATS_FILE.exists(): - print("No stats yet. Run a search first.") - return - - buckets, call_type_counts = _parse_stats() - - bar_width = 16 - heavy_line = " " + "═" * 64 - light_line = " " + "─" * 64 - print() - print(" Semble Token Savings") - print(heavy_line) - print(f" {'Period':<12} {'Calls':<6} Savings") - print(light_line) - for label, bucket in buckets.items(): - saved_chars = max(0, bucket["file_chars"] - bucket["snippet_chars"]) - saved_tokens = saved_chars // 4 - saved_str = f"~{saved_tokens / 1000:.1f}k" if saved_tokens >= 1000 else f"~{saved_tokens}" - if bucket["file_chars"] > 0: - ratio = saved_chars / bucket["file_chars"] - filled = round(ratio * bar_width) - bar = "█" * filled + "░" * (bar_width - filled) - pct = round(ratio * 100) - print(f" {label:<12} {bucket['calls']:<6} [{bar}] {saved_str} tokens ({pct}%)") - else: - print(f" {label:<12} {bucket['calls']:<6} [{'░' * bar_width}] {saved_str} tokens") - if verbose and call_type_counts: - print() - print(" Usage Breakdown") - print(light_line) - print(f" {'Call type':<16} Calls") - for call_type, count in sorted(call_type_counts.items()): - print(f" {call_type:<16} {count}") - print(heavy_line) - print() - - def _cli_main() -> None: parser = argparse.ArgumentParser(prog="semble") sub = parser.add_subparsers(dest="command") @@ -173,7 +87,7 @@ def _cli_main() -> None: return if args.command == "savings": - _run_stats(verbose=args.verbose) + print(format_savings_report(verbose=args.verbose), end="") return index = SembleIndex.from_git(args.path) if _is_git_url(args.path) else SembleIndex.from_path(args.path) diff --git a/src/semble/stats.py b/src/semble/stats.py index 61e6c8600..928a5dd82 100644 --- a/src/semble/stats.py +++ b/src/semble/stats.py @@ -1,7 +1,9 @@ from __future__ import annotations import json -from datetime import datetime, timezone +from collections import defaultdict +from dataclasses import dataclass +from datetime import datetime, timedelta, timezone from pathlib import Path from semble.types import SearchResult @@ -9,6 +11,25 @@ _STATS_FILE = Path.home() / ".semble" / "savings.jsonl" +@dataclass +class BucketStats: + calls: int = 0 + snippet_chars: int = 0 + file_chars: int = 0 + + def add(self, snippet_chars: int, file_chars: int) -> None: + """Update stats with a call and its character counts.""" + self.calls += 1 + self.snippet_chars += snippet_chars + self.file_chars += file_chars + + +@dataclass +class SavingsSummary: + buckets: dict[str, BucketStats] + call_type_counts: dict[str, int] + + def log_search_stats( results: list[SearchResult], call_type: str, @@ -34,3 +55,81 @@ def log_search_stats( f.write(json.dumps(record) + "\n") except Exception: pass + + +def parse_stats(path: Path = _STATS_FILE) -> SavingsSummary: + """Read savings.jsonl and return a SavingsSummary.""" + now = datetime.now(timezone.utc) + today = now.date() + seven_days_ago = (now - timedelta(days=7)).date() + + buckets = { + "Today": BucketStats(), + "Last 7 days": BucketStats(), + "All time": BucketStats(), + } + call_type_counts: defaultdict[str, int] = defaultdict(int) + + with path.open() as f: + for line in f: + try: + record = json.loads(line) + except json.JSONDecodeError: + continue + snippet_chars = record.get("snippet_chars", 0) + file_chars = record.get("file_chars", 0) + call_type = record.get("call", "search") + call_type_counts[call_type] += 1 + try: + record_date = datetime.fromisoformat(record.get("ts", "")).date() + in_today = record_date == today + in_last_7 = record_date > seven_days_ago + except ValueError: + in_today = in_last_7 = True + buckets["All time"].add(snippet_chars, file_chars) + if in_last_7: + buckets["Last 7 days"].add(snippet_chars, file_chars) + if in_today: + buckets["Today"].add(snippet_chars, file_chars) + + return SavingsSummary(buckets=buckets, call_type_counts=dict(call_type_counts)) + + +def format_savings_report(path: Path | None = None, *, verbose: bool = False) -> str: + """Return a formatted token-savings report.""" + if path is None: + path = _STATS_FILE + if not path.exists(): + return "No stats yet. Run a search first." + + summary = parse_stats(path) + bar_width = 16 + heavy_line = " " + "═" * 64 + light_line = " " + "─" * 64 + + lines = [ + "", + " Semble Token Savings", + heavy_line, + f" {'Period':<12} {'Calls':<6} Savings", + light_line, + ] + for label, bucket in summary.buckets.items(): + saved_chars = max(0, bucket.file_chars - bucket.snippet_chars) + saved_tokens = saved_chars // 4 + saved_str = f"~{saved_tokens / 1000:.1f}k" if saved_tokens >= 1000 else f"~{saved_tokens}" + if bucket.file_chars > 0: + ratio = saved_chars / bucket.file_chars + filled = round(ratio * bar_width) + bar = "█" * filled + "░" * (bar_width - filled) + pct = round(ratio * 100) + lines.append(f" {label:<12} {bucket.calls:<6} [{bar}] {saved_str} tokens ({pct}%)") + else: + lines.append(f" {label:<12} {bucket.calls:<6} [{'░' * bar_width}] {saved_str} tokens") + if verbose and summary.call_type_counts: + lines += ["", " Usage Breakdown", light_line, f" {'Call type':<16} Calls"] + for call_type, count in sorted(summary.call_type_counts.items()): + lines.append(f" {call_type:<16} {count}") + lines.append(heavy_line) + lines.append("") + return "\n".join(lines) diff --git a/tests/test_stats.py b/tests/test_stats.py index a1b23a066..1c0445ab2 100644 --- a/tests/test_stats.py +++ b/tests/test_stats.py @@ -6,8 +6,8 @@ import pytest -from semble.cli import _cli_main, _parse_stats, _run_stats -from semble.stats import log_search_stats +from semble.cli import _cli_main +from semble.stats import format_savings_report, log_search_stats, parse_stats from semble.types import SearchMode, SearchResult from tests.conftest import make_chunk @@ -17,14 +17,13 @@ def _make_stats_record(ts: str, call: str = "search", snippet_chars: int = 100, @pytest.fixture -def sample_stats_file(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path: - """Fixture for a stats file with two records.""" +def sample_stats_file(tmp_path: Path) -> Path: + """Stats file with one search and one find_related record from today.""" stats_file = tmp_path / "stats.jsonl" now = datetime.now(timezone.utc).isoformat() stats_file.write_text( _make_stats_record(now, call="search") + "\n" + _make_stats_record(now, call="find_related") + "\n" ) - monkeypatch.setattr("semble.cli._STATS_FILE", stats_file) return stats_file @@ -48,11 +47,9 @@ def test_log_search_stats_silences_write_errors(monkeypatch: pytest.MonkeyPatch) log_search_stats([], "search") # must not raise -def test_savings_no_file(tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]) -> None: - """_run_stats prints a friendly message when no stats file exists yet.""" - monkeypatch.setattr("semble.cli._STATS_FILE", tmp_path / "nonexistent.jsonl") - _run_stats() - assert "No stats yet" in capsys.readouterr().out +def test_savings_no_file(tmp_path: Path) -> None: + """format_savings_report returns a friendly message when no stats file exists yet.""" + assert "No stats yet" in format_savings_report(path=tmp_path / "nonexistent.jsonl") @pytest.mark.parametrize( @@ -63,14 +60,11 @@ def test_savings_no_file(tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys ], ids=["default", "verbose"], ) -def test_savings_output( - sample_stats_file: Path, verbose: bool, expected: list[str], capsys: pytest.CaptureFixture[str] -) -> None: - """_run_stats displays period buckets; --verbose adds call-type breakdown.""" - _run_stats(verbose=verbose) - out = capsys.readouterr().out +def test_savings_output(sample_stats_file: Path, verbose: bool, expected: list[str]) -> None: + """format_savings_report displays period buckets; --verbose adds call-type breakdown.""" + result = format_savings_report(path=sample_stats_file, verbose=verbose) for s in expected: - assert s in out + assert s in result @pytest.mark.parametrize( @@ -81,15 +75,11 @@ def test_savings_output( ], ids=["malformed-json", "malformed-timestamp"], ) -def test_savings_tolerates_bad_records( - bad_line: str, tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] -) -> None: - """_run_stats skips bad JSON and bad timestamps without crashing.""" +def test_savings_tolerates_bad_records(bad_line: str, tmp_path: Path) -> None: + """format_savings_report skips bad JSON and bad timestamps without crashing.""" stats_file = tmp_path / "stats.jsonl" stats_file.write_text(bad_line + "\n") - monkeypatch.setattr("semble.cli._STATS_FILE", stats_file) - _run_stats() - assert "Savings" in capsys.readouterr().out + assert "Savings" in format_savings_report(path=stats_file) @pytest.mark.parametrize( @@ -103,21 +93,20 @@ def test_savings_tolerates_bad_records( def test_savings_cli_dispatch( argv: list[str], expected: str, tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] ) -> None: - """Savings subcommand dispatches to _run_stats, with and without --verbose.""" + """Savings subcommand dispatches to format_savings_report, with and without --verbose.""" monkeypatch.setattr(sys, "argv", argv) - monkeypatch.setattr("semble.cli._STATS_FILE", tmp_path / "nonexistent.jsonl") + monkeypatch.setattr("semble.stats._STATS_FILE", tmp_path / "nonexistent.jsonl") _cli_main() assert expected in capsys.readouterr().out -def test_savings_buckets_exclude_old_records(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: +def test_savings_buckets_exclude_old_records(tmp_path: Path) -> None: """Records older than 7 days count in All time but not Today or Last 7 days.""" stats_file = tmp_path / "stats.jsonl" old_ts = "2020-01-01T00:00:00+00:00" now_ts = datetime.now(timezone.utc).isoformat() stats_file.write_text(_make_stats_record(old_ts) + "\n" + _make_stats_record(now_ts) + "\n") - monkeypatch.setattr("semble.cli._STATS_FILE", stats_file) - buckets, _ = _parse_stats() - assert buckets["All time"]["calls"] == 2 - assert buckets["Today"]["calls"] == 1 - assert buckets["Last 7 days"]["calls"] == 1 + summary = parse_stats(path=stats_file) + assert summary.buckets["All time"].calls == 2 + assert summary.buckets["Today"].calls == 1 + assert summary.buckets["Last 7 days"].calls == 1 From 9e4d44780a7d486cb1217b60efe774d65f3cbf67 Mon Sep 17 00:00:00 2001 From: Pringled Date: Thu, 7 May 2026 13:21:58 +0200 Subject: [PATCH 06/17] Improve code quality --- src/semble/index/index.py | 2 +- src/semble/stats.py | 4 +--- tests/test_stats.py | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/semble/index/index.py b/src/semble/index/index.py index c03ebfd24..683dddb7f 100644 --- a/src/semble/index/index.py +++ b/src/semble/index/index.py @@ -57,7 +57,7 @@ def _populate_mapping(self) -> tuple[dict[str, list[int]], dict[str, list[int]]] @staticmethod def _compute_file_sizes(root: Path, chunks: list[Chunk]) -> dict[str, int]: - """Return a mapping of repo-relative file path → total character count.""" + """Return a mapping of repo-relative file path to total character count.""" sizes: dict[str, int] = {} for chunk in chunks: file_path = chunk.file_path diff --git a/src/semble/stats.py b/src/semble/stats.py index 928a5dd82..53bc3ee03 100644 --- a/src/semble/stats.py +++ b/src/semble/stats.py @@ -1,5 +1,3 @@ -from __future__ import annotations - import json from collections import defaultdict from dataclasses import dataclass @@ -85,7 +83,7 @@ def parse_stats(path: Path = _STATS_FILE) -> SavingsSummary: in_today = record_date == today in_last_7 = record_date > seven_days_ago except ValueError: - in_today = in_last_7 = True + in_today = in_last_7 = False buckets["All time"].add(snippet_chars, file_chars) if in_last_7: buckets["Last 7 days"].add(snippet_chars, file_chars) diff --git a/tests/test_stats.py b/tests/test_stats.py index 1c0445ab2..136e1f1c2 100644 --- a/tests/test_stats.py +++ b/tests/test_stats.py @@ -76,7 +76,7 @@ def test_savings_output(sample_stats_file: Path, verbose: bool, expected: list[s ids=["malformed-json", "malformed-timestamp"], ) def test_savings_tolerates_bad_records(bad_line: str, tmp_path: Path) -> None: - """format_savings_report skips bad JSON and bad timestamps without crashing.""" + """Bad JSON lines are skipped; records with bad timestamps count only in All time.""" stats_file = tmp_path / "stats.jsonl" stats_file.write_text(bad_line + "\n") assert "Savings" in format_savings_report(path=stats_file) From eec13747a86dfb59c7564d8a170735c0cd465db1 Mon Sep 17 00:00:00 2001 From: Pringled Date: Thu, 7 May 2026 15:51:19 +0200 Subject: [PATCH 07/17] Change error --- src/semble/stats.py | 2 +- uv.lock | 419 +++++++++++++++++++++----------------------- 2 files changed, 198 insertions(+), 223 deletions(-) diff --git a/src/semble/stats.py b/src/semble/stats.py index 53bc3ee03..8cb885f36 100644 --- a/src/semble/stats.py +++ b/src/semble/stats.py @@ -51,7 +51,7 @@ def log_search_stats( _STATS_FILE.parent.mkdir(parents=True, exist_ok=True) with _STATS_FILE.open("a") as f: f.write(json.dumps(record) + "\n") - except Exception: + except OSError: pass diff --git a/uv.lock b/uv.lock index 9e99f5f44..8f20e19e5 100644 --- a/uv.lock +++ b/uv.lock @@ -10,6 +10,10 @@ resolution-markers = [ "python_full_version < '3.11'", ] +[options] +exclude-newer = "2026-04-28T05:34:18.12564Z" +exclude-newer-span = "P1W" + [manifest] constraints = [{ name = "tree-sitter-language-pack", specifier = "!=1.6.3" }] @@ -56,15 +60,15 @@ wheels = [ [[package]] name = "bm25s" -version = "0.3.8" +version = "0.3.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/98/3f/88010a24298e2ca4b0807ca5a59ee69e867551fc289c3b062d41cefda59b/bm25s-0.3.8.tar.gz", hash = "sha256:d165f096d64961de0b25a0ad4989d42dfe529c94ca12a9a070eef173c62b8612", size = 80495, upload-time = "2026-04-29T02:18:45.493Z" } +sdist = { url = "https://files.pythonhosted.org/packages/61/3b/cc173de8bbfb21362cd343a6b3077c95e70b9fdaed50f792df4fd70e8c4f/bm25s-0.3.6.tar.gz", hash = "sha256:1ce8549342d2ca9487b82c5e8ac7ac180ac7753d9912e9c66288f670ba3139e4", size = 77821, upload-time = "2026-04-25T03:27:56.803Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/82/c2/e5dbdbf8e99ec504432f9bd82cdf990faff1ffcdd5316272cb7fccce31ed/bm25s-0.3.8-py3-none-any.whl", hash = "sha256:b5ff52a0633ee5add26ca89619f5c5fff66127dcce0984976a2a09e1871a33f9", size = 74452, upload-time = "2026-04-29T02:18:43.892Z" }, + { url = "https://files.pythonhosted.org/packages/e4/12/33fb846b01ce073ffbd766f5f59fc2fcd023eb1dc2000744a7839bb9dbe4/bm25s-0.3.6-py3-none-any.whl", hash = "sha256:11b1cbc9bc7ca5e4b5806639f309a08e746cdfe376dc9177aa3c9d518f78814e", size = 72848, upload-time = "2026-04-25T03:27:55.174Z" }, ] [[package]] @@ -274,7 +278,7 @@ wheels = [ [[package]] name = "chonkie" -version = "1.6.5" +version = "1.6.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "chonkie-core" }, @@ -282,12 +286,11 @@ dependencies = [ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "tenacity" }, - { name = "tokie" }, { name = "tqdm" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/07/09/6686674e41f02efc0d58873aee80a378d6d30a4336b02f8c466c1829113d/chonkie-1.6.5.tar.gz", hash = "sha256:756cd71428c9a55c58e2aed792671858cb9d4624b4a4ad5332c1c37bbd22a8b7", size = 197766, upload-time = "2026-05-06T01:17:52.03Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/e0/fa69454a516be555f9f0b1360cb406045a8cda09ef16f7da32c6a86ef3a7/chonkie-1.6.4.tar.gz", hash = "sha256:5935044977b9bf31ae5379d445785e639b74e657108a9df4bbe7cab20855ce7a", size = 196498, upload-time = "2026-04-21T20:50:35.976Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/f0/a45cb628c137369a1836c2664b05d40011a62c0e0252460875c11de34737/chonkie-1.6.5-py3-none-any.whl", hash = "sha256:d28659c54de4cad5e54d45a786961492017f1c7196f35c257a4229e30a33a9d4", size = 240886, upload-time = "2026-05-06T01:17:49.985Z" }, + { url = "https://files.pythonhosted.org/packages/47/f3/c45cb778846db9e10a5c6cf84a7697bbccb9765e2615f3f55c51ad2be2f0/chonkie-1.6.4-py3-none-any.whl", hash = "sha256:b9bae7a9ba72252f485370dca0ab25e1552380542643d710a876192714edb45c", size = 240369, upload-time = "2026-04-21T20:50:34.372Z" }, ] [package.optional-dependencies] @@ -648,62 +651,62 @@ toml = [ [[package]] name = "cryptography" -version = "48.0.0" +version = "47.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9f/a9/db8f313fdcd85d767d4973515e1db101f9c71f95fced83233de224673757/cryptography-48.0.0.tar.gz", hash = "sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920", size = 832984, upload-time = "2026-05-04T22:59:38.133Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/df/3d/01f6dd9190170a5a241e0e98c2d04be3664a9e6f5b9b872cde63aff1c3dd/cryptography-48.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:0c558d2cdffd8f4bbb30fc7134c74d2ca9a476f830bb053074498fbc86f41ed6", size = 8001587, upload-time = "2026-05-04T22:57:36.803Z" }, - { url = "https://files.pythonhosted.org/packages/b2/6e/e90527eef33f309beb811cf7c982c3aeffcce8e3edb178baa4ca3ae4a6fa/cryptography-48.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c", size = 4690433, upload-time = "2026-05-04T22:57:40.373Z" }, - { url = "https://files.pythonhosted.org/packages/90/04/673510ed51ddff56575f306cf1617d80411ee76831ccd3097599140efdfe/cryptography-48.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3", size = 4710620, upload-time = "2026-05-04T22:57:42.935Z" }, - { url = "https://files.pythonhosted.org/packages/14/d5/e9c4ef932c8d800490c34d8bd589d64a31d5890e27ec9e9ad532be893294/cryptography-48.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5", size = 4696283, upload-time = "2026-05-04T22:57:45.294Z" }, - { url = "https://files.pythonhosted.org/packages/0c/29/174b9dfb60b12d59ecfc6cfa04bc88c21b42a54f01b8aae09bb6e51e4c7f/cryptography-48.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:369a6348999f94bbd53435c894377b20ab95f25a9065c283570e70150d8abc3c", size = 5296573, upload-time = "2026-05-04T22:57:47.933Z" }, - { url = "https://files.pythonhosted.org/packages/95/38/0d29a6fd7d0d1373f0c0c88a04ba20e359b257753ac497564cd660fc1d55/cryptography-48.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f", size = 4743677, upload-time = "2026-05-04T22:57:50.067Z" }, - { url = "https://files.pythonhosted.org/packages/30/be/eef653013d5c63b6a490529e0316f9ac14a37602965d4903efed1399f32b/cryptography-48.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25", size = 4330808, upload-time = "2026-05-04T22:57:52.301Z" }, - { url = "https://files.pythonhosted.org/packages/84/9e/500463e87abb7a0a0f9f256ec21123ecde0a7b5541a15e840ea54551fd81/cryptography-48.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602", size = 4695941, upload-time = "2026-05-04T22:57:54.603Z" }, - { url = "https://files.pythonhosted.org/packages/e3/dc/7303087450c2ec9e7fbb750e17c2abfbc658f23cbd0e54009509b7cc4091/cryptography-48.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9ccdac7d40688ecb5a3b4a604b8a88c8002e3442d6c60aead1db2a89a041560c", size = 5252579, upload-time = "2026-05-04T22:57:57.207Z" }, - { url = "https://files.pythonhosted.org/packages/d0/c0/7101d3b7215edcdc90c45da544961fd8ed2d6448f77577460fa75a8443f7/cryptography-48.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5", size = 4743326, upload-time = "2026-05-04T22:57:59.535Z" }, - { url = "https://files.pythonhosted.org/packages/ac/d8/5b833bad13016f562ab9d063d68199a4bd121d18458e439515601d3357ec/cryptography-48.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321", size = 4826672, upload-time = "2026-05-04T22:58:01.996Z" }, - { url = "https://files.pythonhosted.org/packages/98/e1/7074eb8bf3c135558c73fc2bcf0f5633f912e6fb87e868a55c454080ef09/cryptography-48.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74", size = 4972574, upload-time = "2026-05-04T22:58:03.968Z" }, - { url = "https://files.pythonhosted.org/packages/04/70/e5a1b41d325f797f39427aa44ef8baf0be500065ab6d8e10369d850d4a4f/cryptography-48.0.0-cp311-abi3-win32.whl", hash = "sha256:9c459db21422be75e2809370b829a87eb37f74cd785fc4aa9ea1e5f43b47cda4", size = 3294868, upload-time = "2026-05-04T22:58:06.467Z" }, - { url = "https://files.pythonhosted.org/packages/f4/ac/8ac51b4a5fc5932eb7ee5c517ba7dc8cd834f0048962b6b352f00f41ebf9/cryptography-48.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:5b012212e08b8dd5edc78ef54da83dd9892fd9105323b3993eff6bea65dc21d7", size = 3817107, upload-time = "2026-05-04T22:58:08.845Z" }, - { url = "https://files.pythonhosted.org/packages/6b/84/70e3feea9feea87fd7cbe77efb2712ae1e3e6edf10749dc6e95f4e60e455/cryptography-48.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:3cb07a3ed6431663cd321ea8a000a1314c74211f823e4177fefa2255e057d1ec", size = 7986556, upload-time = "2026-05-04T22:58:11.172Z" }, - { url = "https://files.pythonhosted.org/packages/89/6e/18e07a618bb5442ba10cf4df16e99c071365528aa570dfcb8c02e25a303b/cryptography-48.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c7378637d7d88016fa6791c159f698b3d3eed28ebf844ac36b9dc04a14dae18", size = 4684776, upload-time = "2026-05-04T22:58:13.712Z" }, - { url = "https://files.pythonhosted.org/packages/be/6a/4ea3b4c6c6759794d5ee2103c304a5076dc4b19ae1f9fe47dba439e159e9/cryptography-48.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc90c0b39b2e3c65ef52c804b72e3c58f8a04ab2a1871272798e5f9572c17d20", size = 4698121, upload-time = "2026-05-04T22:58:16.448Z" }, - { url = "https://files.pythonhosted.org/packages/2f/59/6ff6ad6cae03bb887da2a5860b2c9805f8dac969ef01ce563336c49bd1d1/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:76341972e1eff8b4bea859f09c0d3e64b96ce931b084f9b9b7db8ef364c30eff", size = 4690042, upload-time = "2026-05-04T22:58:18.544Z" }, - { url = "https://files.pythonhosted.org/packages/ca/b4/fc334ed8cfd705aca282fe4d8f5ae64a8e0f74932e9feecb344610cf6e4d/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:55b7718303bf06a5753dcdccf2f3945cf18ad7bffde41b61226e4db31ab89a9c", size = 5282526, upload-time = "2026-05-04T22:58:20.75Z" }, - { url = "https://files.pythonhosted.org/packages/11/08/9f8c5386cc4cd90d8255c7cdd0f5baf459a08502a09de30dc51f553d38dc/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:a64697c641c7b1b2178e573cbc31c7c6684cd56883a478d75143dbb7118036db", size = 4733116, upload-time = "2026-05-04T22:58:23.627Z" }, - { url = "https://files.pythonhosted.org/packages/b8/77/99307d7574045699f8805aa500fa0fb83422d115b5400a064ddd306d7750/cryptography-48.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:561215ea3879cb1cbbf272867e2efda62476f240fb58c64de6b393ae19246741", size = 4316030, upload-time = "2026-05-04T22:58:25.581Z" }, - { url = "https://files.pythonhosted.org/packages/fd/36/a608b98337af3cb2aff4818e406649d30572b7031918b04c87d979495348/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ad64688338ed4bc1a6618076ba75fd7194a5f1797ac60b47afe926285adb3166", size = 4689640, upload-time = "2026-05-04T22:58:27.747Z" }, - { url = "https://files.pythonhosted.org/packages/dd/a6/825010a291b4438aecc1f568bc428189fc1175515223632477c07dc0a6df/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:906cbf0670286c6e0044156bc7d4af9cbb0ef6db9f73e52c3ec56ba6bdde5336", size = 5237657, upload-time = "2026-05-04T22:58:29.848Z" }, - { url = "https://files.pythonhosted.org/packages/b9/09/4e76a09b4caa29aad535ddc806f5d4c5d01885bd978bd984fbc6ca032cae/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:ea8990436d914540a40ab24b6a77c0969695ed52f4a4874c5137ccf7045a7057", size = 4732362, upload-time = "2026-05-04T22:58:32.009Z" }, - { url = "https://files.pythonhosted.org/packages/18/78/444fa04a77d0cb95f417dda20d450e13c56ba8e5220fc892a1658f44f882/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c18684a7f0cc9a3cb60328f496b8e3372def7c5d2df39ac267878b05565aaaae", size = 4819580, upload-time = "2026-05-04T22:58:34.254Z" }, - { url = "https://files.pythonhosted.org/packages/38/85/ea67067c70a1fd4be2c63d35eeed82658023021affccc7b17705f8527dd2/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9be5aafa5736574f8f15f262adc81b2a9869e2cfe9014d52a44633905b40d52c", size = 4963283, upload-time = "2026-05-04T22:58:36.376Z" }, - { url = "https://files.pythonhosted.org/packages/75/54/cc6d0f3deac3e81c7f847e8a189a12b6cdd65059b43dad25d4316abd849a/cryptography-48.0.0-cp314-cp314t-win32.whl", hash = "sha256:c17dfe85494deaeddc5ce251aebd1d60bbe6afc8b62071bb0b469431a000124f", size = 3270954, upload-time = "2026-05-04T22:58:38.791Z" }, - { url = "https://files.pythonhosted.org/packages/49/67/cc947e288c0758a4e5473d1dcb743037ab7785541265a969240b8885441a/cryptography-48.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27241b1dc9962e056062a8eef1991d02c3a24569c95975bd2322a8a52c6e5e12", size = 3797313, upload-time = "2026-05-04T22:58:40.746Z" }, - { url = "https://files.pythonhosted.org/packages/f2/63/61d4a4e1c6b6bab6ce1e213cd36a24c415d90e76d78c5eb8577c5541d2e8/cryptography-48.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:58d00498e8933e4a194f3076aee1b4a97dfec1a6da444535755822fe5d8b0b86", size = 7983482, upload-time = "2026-05-04T22:58:43.769Z" }, - { url = "https://files.pythonhosted.org/packages/d5/ac/f5b5995b87770c693e2596559ffafe195b4033a57f14a82268a2842953f3/cryptography-48.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e", size = 4683266, upload-time = "2026-05-04T22:58:46.064Z" }, - { url = "https://files.pythonhosted.org/packages/ec/c6/8b14f67e18338fbc4adb76f66c001f5c3610b3e2d1837f268f47a347dbbb/cryptography-48.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f", size = 4696228, upload-time = "2026-05-04T22:58:48.22Z" }, - { url = "https://files.pythonhosted.org/packages/ea/73/f808fbae9514bd91b47875b003f13e284c8c6bdfd904b7944e803937eec1/cryptography-48.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7", size = 4689097, upload-time = "2026-05-04T22:58:50.9Z" }, - { url = "https://files.pythonhosted.org/packages/93/01/d86632d7d28db8ae83221995752eeb6639ffb374c2d22955648cf8d52797/cryptography-48.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:22a5cb272895dce158b2cacdfdc3debd299019659f42947dbdac6f32d68fe832", size = 5283582, upload-time = "2026-05-04T22:58:53.017Z" }, - { url = "https://files.pythonhosted.org/packages/02/e1/50edc7a50334807cc4791fc4a0ce7468b4a1416d9138eab358bfc9a3d70b/cryptography-48.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c", size = 4730479, upload-time = "2026-05-04T22:58:55.611Z" }, - { url = "https://files.pythonhosted.org/packages/6f/af/99a582b1b1641ff5911ac559beb45097cf79efd4ead4657f578ef1af2d47/cryptography-48.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a", size = 4326481, upload-time = "2026-05-04T22:58:57.607Z" }, - { url = "https://files.pythonhosted.org/packages/90/ee/89aa26a06ef0a7d7611788ffd571a7c50e368cc6a4d5eef8b4884e866edb/cryptography-48.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a", size = 4688713, upload-time = "2026-05-04T22:59:00.077Z" }, - { url = "https://files.pythonhosted.org/packages/70/ba/bcb1b0bb7a33d4c7c0c4d4c7874b4a62ae4f56113a5f4baefa362dfb1f0f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:8cd666227ef7af430aa5914a9910e0ddd703e75f039cef0825cd0da71b6b711a", size = 5238165, upload-time = "2026-05-04T22:59:02.317Z" }, - { url = "https://files.pythonhosted.org/packages/c9/70/ca4003b1ce5ca3dc3186ada51908c8a9b9ff7d5cab83cc0d43ee14ec144f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239", size = 4729947, upload-time = "2026-05-04T22:59:05.255Z" }, - { url = "https://files.pythonhosted.org/packages/44/a0/4ec7cf774207905aef1a8d11c3750d5a1db805eb380ee4e16df317870128/cryptography-48.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c", size = 4822059, upload-time = "2026-05-04T22:59:07.802Z" }, - { url = "https://files.pythonhosted.org/packages/1e/75/a2e55f99c16fcac7b5d6c1eb19ad8e00799854d6be5ca845f9259eae1681/cryptography-48.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4", size = 4960575, upload-time = "2026-05-04T22:59:09.851Z" }, - { url = "https://files.pythonhosted.org/packages/b8/23/6e6f32143ab5d8b36ca848a502c4bcd477ae75b9e1677e3530d669062578/cryptography-48.0.0-cp39-abi3-win32.whl", hash = "sha256:77a2ccbbe917f6710e05ba9adaa25fb5075620bf3ea6fb751997875aff4ae4bd", size = 3279117, upload-time = "2026-05-04T22:59:12.019Z" }, - { url = "https://files.pythonhosted.org/packages/9d/9a/0fea98a70cf1749d41d738836f6349d97945f7c89433a259a6c2642eefeb/cryptography-48.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8", size = 3792100, upload-time = "2026-05-04T22:59:14.884Z" }, - { url = "https://files.pythonhosted.org/packages/be/d2/024b5e06be9d44cb021fb0e1a03d34d63989cf56a0fe62f3dfbab695b9b4/cryptography-48.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:84cf79f0dc8b36ac5da873481716e87aef31fcfa0444f9e1d8b4b2cece142855", size = 3950391, upload-time = "2026-05-04T22:59:17.415Z" }, - { url = "https://files.pythonhosted.org/packages/bc/17/3861e17c56fa0fd37491a14a8673fdb77c57fc5693cafe745ea8b06dba75/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:fdfef35d751d510fcef5252703621574364fec16418c4a1e5e1055248401054b", size = 4637126, upload-time = "2026-05-04T22:59:20.197Z" }, - { url = "https://files.pythonhosted.org/packages/f0/0a/7e226dbff530f21480727eb764973a7bff2b912f8e15cd4f129e71b56d1d/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:0890f502ddf7d9c6426129c3f49f5c0a39278ed7cd6322c8755ffca6ee675a13", size = 4667270, upload-time = "2026-05-04T22:59:22.647Z" }, - { url = "https://files.pythonhosted.org/packages/3b/f2/5a72274ca9f1b2a8b44a662ee0bf1b435909deb473d6f97bcd035bcdbc71/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:ecde28a596bead48b0cfd2a1b4416c3d43074c2d785e3a398d7ec1fc4d0f7fbb", size = 4636797, upload-time = "2026-05-04T22:59:24.912Z" }, - { url = "https://files.pythonhosted.org/packages/b4/e1/48cedb2fe63626e91ded1edad159e2a4fb8b6906c4425eb7749673077ce7/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:4defde8685ae324a9eb9d818717e93b4638ef67070ac9bc15b8ca85f63048355", size = 4666800, upload-time = "2026-05-04T22:59:27.474Z" }, - { url = "https://files.pythonhosted.org/packages/a2/ca/7e8365deec19afb2b2c7be7c1c0aa8f99633b54e90c570999acda93260fc/cryptography-48.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:db63bf618e5dea46c07de12e900fe1cdd2541e6dc9dbae772a70b7d4d4765f6a", size = 3739536, upload-time = "2026-05-04T22:59:29.61Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/ef/b2/7ffa7fe8207a8c42147ffe70c3e360b228160c1d85dc3faff16aaa3244c0/cryptography-47.0.0.tar.gz", hash = "sha256:9f8e55fe4e63613a5e1cc5819030f27b97742d720203a087802ce4ce9ceb52bb", size = 830863, upload-time = "2026-04-24T19:54:57.056Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/98/40dfe932134bdcae4f6ab5927c87488754bf9eb79297d7e0070b78dd58e9/cryptography-47.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:160ad728f128972d362e714054f6ba0067cab7fb350c5202a9ae8ae4ce3ef1a0", size = 7912214, upload-time = "2026-04-24T19:53:03.864Z" }, + { url = "https://files.pythonhosted.org/packages/34/c6/2733531243fba725f58611b918056b277692f1033373dcc8bd01af1c05d4/cryptography-47.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b9a8943e359b7615db1a3ba587994618e094ff3d6fa5a390c73d079ce18b3973", size = 4644617, upload-time = "2026-04-24T19:53:06.909Z" }, + { url = "https://files.pythonhosted.org/packages/00/e3/b27be1a670a9b87f855d211cf0e1174a5d721216b7616bd52d8581d912ed/cryptography-47.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f5c15764f261394b22aef6b00252f5195f46f2ca300bec57149474e2538b31f8", size = 4668186, upload-time = "2026-04-24T19:53:09.053Z" }, + { url = "https://files.pythonhosted.org/packages/81/b9/8443cfe5d17d482d348cee7048acf502bb89a51b6382f06240fd290d4ca3/cryptography-47.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9c59ab0e0fa3a180a5a9c59f3a5abe3ef90d474bc56d7fadfbe80359491b615b", size = 4651244, upload-time = "2026-04-24T19:53:11.217Z" }, + { url = "https://files.pythonhosted.org/packages/5d/5e/13ed0cdd0eb88ba159d6dd5ebfece8cb901dbcf1ae5ac4072e28b55d3153/cryptography-47.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:34b4358b925a5ea3e14384ca781a2c0ef7ac219b57bb9eacc4457078e2b19f92", size = 5252906, upload-time = "2026-04-24T19:53:13.532Z" }, + { url = "https://files.pythonhosted.org/packages/64/16/ed058e1df0f33d440217cd120d41d5dda9dd215a80b8187f68483185af82/cryptography-47.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0024b87d47ae2399165a6bfb20d24888881eeab83ae2566d62467c5ff0030ce7", size = 4701842, upload-time = "2026-04-24T19:53:15.618Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3d30986b30fdbd9e969abbdf8ba00ed0618615144341faeb57f395a084fe/cryptography-47.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:1e47422b5557bb82d3fff997e8d92cff4e28b9789576984f08c248d2b3535d93", size = 4289313, upload-time = "2026-04-24T19:53:17.755Z" }, + { url = "https://files.pythonhosted.org/packages/df/fd/32db38e3ad0cb331f0691cb4c7a8a6f176f679124dee746b3af6633db4d9/cryptography-47.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:6f29f36582e6151d9686235e586dd35bb67491f024767d10b842e520dc6a07ac", size = 4650964, upload-time = "2026-04-24T19:53:20.062Z" }, + { url = "https://files.pythonhosted.org/packages/86/53/5395d944dfd48cb1f67917f533c609c34347185ef15eb4308024c876f274/cryptography-47.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:a9b761f012a943b7de0e828843c5688d0de94a0578d44d6c85a1bae32f87791f", size = 5207817, upload-time = "2026-04-24T19:53:22.498Z" }, + { url = "https://files.pythonhosted.org/packages/34/4f/e5711b28e1901f7d480a2b1b688b645aa4c77c73f10731ed17e7f7db3f0d/cryptography-47.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4e1de79e047e25d6e9f8cea71c86b4a53aced64134f0f003bbcbf3655fd172c8", size = 4701544, upload-time = "2026-04-24T19:53:24.356Z" }, + { url = "https://files.pythonhosted.org/packages/22/22/c8ddc25de3010fc8da447648f5a092c40e7a8fadf01dd6d255d9c0b9373d/cryptography-47.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef6b3634087f18d2155b1e8ce264e5345a753da2c5fa9815e7d41315c90f8318", size = 4783536, upload-time = "2026-04-24T19:53:26.665Z" }, + { url = "https://files.pythonhosted.org/packages/66/b6/d4a68f4ea999c6d89e8498579cba1c5fcba4276284de7773b17e4fa69293/cryptography-47.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:11dbb9f50a0f1bb9757b3d8c27c1101780efb8f0bdecfb12439c22a74d64c001", size = 4926106, upload-time = "2026-04-24T19:53:28.686Z" }, + { url = "https://files.pythonhosted.org/packages/54/ed/5f524db1fade9c013aa618e1c99c6ed05e8ffc9ceee6cda22fed22dda3f4/cryptography-47.0.0-cp311-abi3-win32.whl", hash = "sha256:7fda2f02c9015db3f42bb8a22324a454516ed10a8c29ca6ece6cdbb5efe2a203", size = 3258581, upload-time = "2026-04-24T19:53:31.058Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dc/1b901990b174786569029f67542b3edf72ac068b6c3c8683c17e6a2f5363/cryptography-47.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:f5c3296dab66202f1b18a91fa266be93d6aa0c2806ea3d67762c69f60adc71aa", size = 3775309, upload-time = "2026-04-24T19:53:33.054Z" }, + { url = "https://files.pythonhosted.org/packages/14/88/7aa18ad9c11bc87689affa5ce4368d884b517502d75739d475fc6f4a03c7/cryptography-47.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:be12cb6a204f77ed968bcefe68086eb061695b540a3dd05edac507a3111b25f0", size = 7904299, upload-time = "2026-04-24T19:53:35.003Z" }, + { url = "https://files.pythonhosted.org/packages/07/55/c18f75724544872f234678fdedc871391722cb34a2aee19faa9f63100bb2/cryptography-47.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2ebd84adf0728c039a3be2700289378e1c164afc6748df1a5ed456767bef9ba7", size = 4631180, upload-time = "2026-04-24T19:53:37.517Z" }, + { url = "https://files.pythonhosted.org/packages/ee/65/31a5cc0eaca99cec5bafffe155d407115d96136bb161e8b49e0ef73f09a7/cryptography-47.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f68d6fbc7fbbcfb0939fea72c3b96a9f9a6edfc0e1b1d29778a2066030418b1", size = 4653529, upload-time = "2026-04-24T19:53:39.775Z" }, + { url = "https://files.pythonhosted.org/packages/e5/bc/641c0519a495f3bfd0421b48d7cd325c4336578523ccd76ea322b6c29c7a/cryptography-47.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:6651d32eff255423503aa276739da98c30f26c40cbeffcc6048e0d54ef704c0c", size = 4638570, upload-time = "2026-04-24T19:53:42.129Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f2/300327b0a47f6dc94dd8b71b57052aefe178bb51745073d73d80604f11ab/cryptography-47.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:3fb8fa48075fad7193f2e5496135c6a76ac4b2aa5a38433df0a539296b377829", size = 5238019, upload-time = "2026-04-24T19:53:44.577Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5a/5b5cf994391d4bf9d9c7efd4c66aabe4d95227256627f8fea6cff7dfadbd/cryptography-47.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11438c7518132d95f354fa01a4aa2f806d172a061a7bed18cf18cbdacdb204d7", size = 4686832, upload-time = "2026-04-24T19:53:47.015Z" }, + { url = "https://files.pythonhosted.org/packages/dc/2c/ae950e28fd6475c852fc21a44db3e6b5bcc1261d1e370f2b6e42fa800fef/cryptography-47.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:8c1a736bbb3288005796c3f7ccb9453360d7fed483b13b9f468aea5171432923", size = 4269301, upload-time = "2026-04-24T19:53:48.97Z" }, + { url = "https://files.pythonhosted.org/packages/67/fb/6a39782e150ffe5cc1b0018cb6ddc48bf7ca62b498d7539ffc8a758e977d/cryptography-47.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:f1557695e5c2b86e204f6ce9470497848634100787935ab7adc5397c54abd7ab", size = 4638110, upload-time = "2026-04-24T19:53:51.011Z" }, + { url = "https://files.pythonhosted.org/packages/8e/d7/0b3c71090a76e5c203164a47688b697635ece006dcd2499ab3a4dbd3f0bd/cryptography-47.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:f9a034b642b960767fb343766ae5ba6ad653f2e890ddd82955aef288ffea8736", size = 5194988, upload-time = "2026-04-24T19:53:52.962Z" }, + { url = "https://files.pythonhosted.org/packages/63/33/63a961498a9df51721ab578c5a2622661411fc520e00bd83b0cc64eb20c4/cryptography-47.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:b1c76fca783aa7698eb21eb14f9c4aa09452248ee54a627d125025a43f83e7a7", size = 4686563, upload-time = "2026-04-24T19:53:55.274Z" }, + { url = "https://files.pythonhosted.org/packages/b7/bf/5ee5b145248f92250de86145d1c1d6edebbd57a7fe7caa4dedb5d4cf06a1/cryptography-47.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4f7722c97826770bab8ae92959a2e7b20a5e9e9bf4deae68fd86c3ca457bab52", size = 4770094, upload-time = "2026-04-24T19:53:57.753Z" }, + { url = "https://files.pythonhosted.org/packages/92/43/21d220b2da5d517773894dacdcdb5c682c28d3fffce65548cb06e87d5501/cryptography-47.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:09f6d7bf6724f8db8b32f11eccf23efc8e759924bc5603800335cf8859a3ddbd", size = 4913811, upload-time = "2026-04-24T19:54:00.236Z" }, + { url = "https://files.pythonhosted.org/packages/31/98/dc4ad376ac5f1a1a7d4a83f7b0c6f2bcad36b5d2d8f30aeb482d3a7d9582/cryptography-47.0.0-cp314-cp314t-win32.whl", hash = "sha256:6eebcaf0df1d21ce1f90605c9b432dd2c4f4ab665ac29a40d5e3fc68f51b5e63", size = 3237158, upload-time = "2026-04-24T19:54:02.606Z" }, + { url = "https://files.pythonhosted.org/packages/bc/da/97f62d18306b5133468bc3f8cc73a3111e8cdc8cf8d3e69474d6e5fd2d1b/cryptography-47.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:51c9313e90bd1690ec5a75ed047c27c0b8e6c570029712943d6116ef9a90620b", size = 3758706, upload-time = "2026-04-24T19:54:04.433Z" }, + { url = "https://files.pythonhosted.org/packages/e0/34/a4fae8ae7c3bc227460c9ae43f56abf1b911da0ec29e0ebac53bb0a4b6b7/cryptography-47.0.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:14432c8a9bcb37009784f9594a62fae211a2ae9543e96c92b2a8e4c3cd5cd0c4", size = 7904072, upload-time = "2026-04-24T19:54:06.411Z" }, + { url = "https://files.pythonhosted.org/packages/01/64/d7b1e54fdb69f22d24a64bb3e88dc718b31c7fb10ef0b9691a3cf7eeea6e/cryptography-47.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:07efe86201817e7d3c18781ca9770bc0db04e1e48c994be384e4602bc38f8f27", size = 4635767, upload-time = "2026-04-24T19:54:08.519Z" }, + { url = "https://files.pythonhosted.org/packages/8b/7b/cca826391fb2a94efdcdfe4631eb69306ee1cff0b22f664a412c90713877/cryptography-47.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b45761c6ec22b7c726d6a829558777e32d0f1c8be7c3f3480f9c912d5ee8a10", size = 4654350, upload-time = "2026-04-24T19:54:10.795Z" }, + { url = "https://files.pythonhosted.org/packages/4c/65/4b57bcc823f42a991627c51c2f68c9fd6eb1393c1756aac876cba2accae2/cryptography-47.0.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:edd4da498015da5b9f26d38d3bfc2e90257bfa9cbed1f6767c282a0025ae649b", size = 4643394, upload-time = "2026-04-24T19:54:13.275Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c4/2c5fbeea70adbbca2bbae865e1d605d6a4a7f8dbd9d33eaf69645087f06c/cryptography-47.0.0-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9af828c0d5a65c70ec729cd7495a4bf1a67ecb66417b8f02ff125ab8a6326a74", size = 5225777, upload-time = "2026-04-24T19:54:15.18Z" }, + { url = "https://files.pythonhosted.org/packages/7e/b8/ac57107ef32749d2b244e36069bb688792a363aaaa3acc9e3cf84c130315/cryptography-47.0.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:256d07c78a04d6b276f5df935a9923275f53bd1522f214447fdf365494e2d515", size = 4688771, upload-time = "2026-04-24T19:54:17.835Z" }, + { url = "https://files.pythonhosted.org/packages/56/fc/9f1de22ff8be99d991f240a46863c52d475404c408886c5a38d2b5c3bb26/cryptography-47.0.0-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:5d0e362ff51041b0c0d219cc7d6924d7b8996f57ce5712bdcef71eb3c65a59cc", size = 4270753, upload-time = "2026-04-24T19:54:19.963Z" }, + { url = "https://files.pythonhosted.org/packages/00/68/d70c852797aa68e8e48d12e5a87170c43f67bb4a59403627259dd57d15de/cryptography-47.0.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:1581aef4219f7ca2849d0250edaa3866212fb74bf5667284f46aa92f9e65c1ca", size = 4642911, upload-time = "2026-04-24T19:54:21.818Z" }, + { url = "https://files.pythonhosted.org/packages/a5/51/661cbee74f594c5d97ff82d34f10d5551c085ca4668645f4606ebd22bd5d/cryptography-47.0.0-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:a49a3eb5341b9503fa3000a9a0db033161db90d47285291f53c2a9d2cd1b7f76", size = 5181411, upload-time = "2026-04-24T19:54:24.376Z" }, + { url = "https://files.pythonhosted.org/packages/94/87/f2b6c374a82cf076cfa1416992ac8e8ec94d79facc37aec87c1a5cb72352/cryptography-47.0.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2207a498b03275d0051589e326b79d4cf59985c99031b05bb292ac52631c37fe", size = 4688262, upload-time = "2026-04-24T19:54:26.946Z" }, + { url = "https://files.pythonhosted.org/packages/14/e2/8b7462f4acf21ec509616f0245018bb197194ab0b65c2ea21a0bdd53c0eb/cryptography-47.0.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7a02675e2fabd0c0fc04c868b8781863cbf1967691543c22f5470500ff840b31", size = 4775506, upload-time = "2026-04-24T19:54:28.926Z" }, + { url = "https://files.pythonhosted.org/packages/70/75/158e494e4c08dc05e039da5bb48553826bd26c23930cf8d3cd5f21fa8921/cryptography-47.0.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80887c5cbd1774683cb126f0ab4184567f080071d5acf62205acb354b4b753b7", size = 4912060, upload-time = "2026-04-24T19:54:30.869Z" }, + { url = "https://files.pythonhosted.org/packages/06/bd/0a9d3edbf5eadbac926d7b9b3cd0c4be584eeeae4a003d24d9eda4affbbd/cryptography-47.0.0-cp38-abi3-win32.whl", hash = "sha256:ed67ea4e0cfb5faa5bc7ecb6e2b8838f3807a03758eec239d6c21c8769355310", size = 3248487, upload-time = "2026-04-24T19:54:33.494Z" }, + { url = "https://files.pythonhosted.org/packages/60/80/5681af756d0da3a599b7bdb586fac5a1540f1bcefd2717a20e611ddade45/cryptography-47.0.0-cp38-abi3-win_amd64.whl", hash = "sha256:835d2d7f47cdc53b3224e90810fb1d36ca94ea29cc1801fb4c1bc43876735769", size = 3755737, upload-time = "2026-04-24T19:54:35.408Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a0/928c9ce0d120a40a81aa99e3ba383e87337b9ac9ef9f6db02e4d7822424d/cryptography-47.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f1207974a904e005f762869996cf620e9bf79ecb4622f148550bb48e0eb35a7", size = 3909893, upload-time = "2026-04-24T19:54:38.334Z" }, + { url = "https://files.pythonhosted.org/packages/81/75/d691e284750df5d9569f2b1ce4a00a71e1d79566da83b2b3e5549c84917f/cryptography-47.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:1a405c08857258c11016777e11c02bacbe7ef596faf259305d282272a3a05cbe", size = 4587867, upload-time = "2026-04-24T19:54:40.619Z" }, + { url = "https://files.pythonhosted.org/packages/07/d6/1b90f1a4e453009730b4545286f0b39bb348d805c11181fc31544e4f9a65/cryptography-47.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:20fdbe3e38fb67c385d233c89371fa27f9909f6ebca1cecc20c13518dae65475", size = 4627192, upload-time = "2026-04-24T19:54:42.849Z" }, + { url = "https://files.pythonhosted.org/packages/dc/53/cb358a80e9e359529f496870dd08c102aa8a4b5b9f9064f00f0d6ed5b527/cryptography-47.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:f7db373287273d8af1414cf95dc4118b13ffdc62be521997b0f2b270771fef50", size = 4587486, upload-time = "2026-04-24T19:54:44.908Z" }, + { url = "https://files.pythonhosted.org/packages/8b/57/aaa3d53876467a226f9a7a82fd14dd48058ad2de1948493442dfa16e2ffd/cryptography-47.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:9fe6b7c64926c765f9dff301f9c1b867febcda5768868ca084e18589113732ab", size = 4626327, upload-time = "2026-04-24T19:54:47.813Z" }, + { url = "https://files.pythonhosted.org/packages/ab/9c/51f28c3550276bcf35660703ba0ab829a90b88be8cd98a71ef23c2413913/cryptography-47.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cffbba3392df0fa8629bb7f43454ee2925059ee158e23c54620b9063912b86c8", size = 3698916, upload-time = "2026-04-24T19:54:49.782Z" }, ] [[package]] @@ -864,11 +867,11 @@ wheels = [ [[package]] name = "fsspec" -version = "2026.4.0" +version = "2026.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d5/8d/1c51c094345df128ca4a990d633fe1a0ff28726c9e6b3c41ba65087bba1d/fsspec-2026.4.0.tar.gz", hash = "sha256:301d8ac70ae90ef3ad05dcf94d6c3754a097f9b5fe4667d2787aa359ec7df7e4", size = 312760, upload-time = "2026-04-29T20:42:38.635Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/cf/b50ddf667c15276a9ab15a70ef5f257564de271957933ffea49d2cdbcdfb/fsspec-2026.3.0.tar.gz", hash = "sha256:1ee6a0e28677557f8c2f994e3eea77db6392b4de9cd1f5d7a9e87a0ae9d01b41", size = 313547, upload-time = "2026-03-27T19:11:14.892Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl", hash = "sha256:11ef7bb35dab8a394fde6e608221d5cf3e8499401c249bebaeaad760a1a8dec2", size = 203402, upload-time = "2026-04-29T20:42:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/d5/1f/5f4a3cd9e4440e9d9bc78ad0a91a1c8d46b4d429d5239ebe6793c9fe5c41/fsspec-2026.3.0-py3-none-any.whl", hash = "sha256:d2ceafaad1b3457968ed14efa28798162f1638dbb5d2a6868a2db002a5ee39a4", size = 202595, upload-time = "2026-03-27T19:11:13.595Z" }, ] [[package]] @@ -882,34 +885,34 @@ wheels = [ [[package]] name = "hf-xet" -version = "1.5.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/74/d8/5c06fc76461418326a7decf8367480c35be11a41fd938633929c60a9ec6b/hf_xet-1.5.0.tar.gz", hash = "sha256:e0fb0a34d9f406eed88233e829a67ec016bec5af19e480eac65a233ea289a948", size = 837196, upload-time = "2026-05-06T06:18:15.583Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/68/9b/6912c99070915a4f28119e3c5b52a9abd1eec0ad5cb293b8c967a0c6f5a2/hf_xet-1.5.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:7d70fe2ce97b9db73b9c9b9c81fe3693640aec83416a966c446afea54acfae3c", size = 4023383, upload-time = "2026-05-06T06:17:53.947Z" }, - { url = "https://files.pythonhosted.org/packages/0f/6d/9563cfde59b5d8128a9c7ec972a087f4c782e4f7bac5a85234edfd5d5e49/hf_xet-1.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:73a0dae8c71de3b0633a45c73f4a4a5ed09e94b43441d82981a781d4f12baa42", size = 3792751, upload-time = "2026-05-06T06:17:51.791Z" }, - { url = "https://files.pythonhosted.org/packages/07/a5/ed5a0cf35b49a0571af5a8f53416dad1877a718c021c9937c3a53cb45781/hf_xet-1.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a60290ec57e9b71767fba7c3645ddafdd0759974b540441510c629c6db6db24a", size = 4456058, upload-time = "2026-05-06T06:17:40.735Z" }, - { url = "https://files.pythonhosted.org/packages/60/fb/3ae8bf2a7a37a4197d0195d7247fd25b3952e15cb8a599e285dfaa6f52b3/hf_xet-1.5.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:e5de0f6deada0dada870bb376a11bcd1f08abf3a968a6d118f33e72d1b1eb480", size = 4250783, upload-time = "2026-05-06T06:17:38.412Z" }, - { url = "https://files.pythonhosted.org/packages/a2/9b/8bae40d4d91525085137196e84eb0ed49cf65b5e96e5c3ecdadd8bd0fac2/hf_xet-1.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c799d49f1a5544a0ef7591c0ee75e0d6b93d6f56dc7a4979f59f7518d2872216", size = 4445594, upload-time = "2026-05-06T06:18:04.219Z" }, - { url = "https://files.pythonhosted.org/packages/13/59/c74efbbd4e8728172b2cc72a2bc014d2947a4b7bdced932fbd3f5da1a4e5/hf_xet-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2baea1b0b989e5c152fe81425f7745ddc8901280ba3d97c98d8cdece7b706c60", size = 4663995, upload-time = "2026-05-06T06:18:06.1Z" }, - { url = "https://files.pythonhosted.org/packages/73/32/8e1e0410af64cda9b139d1dcebdc993a8ff9c8c7c0e2696ae356d75ccc0d/hf_xet-1.5.0-cp313-cp313t-win_amd64.whl", hash = "sha256:526345b3ed45f374f6317349df489167606736c876241ba984105afe7fd4839d", size = 3966608, upload-time = "2026-05-06T06:18:19.74Z" }, - { url = "https://files.pythonhosted.org/packages/fc/34/a8febc8f4edbea8b3e21b02ebc8b628679b84ba7e45cde624a7736b51500/hf_xet-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:786d28e2eb8315d5035544b9d137b4a842d600c434bb91bf7d0d953cce906ad4", size = 3796946, upload-time = "2026-05-06T06:18:17.568Z" }, - { url = "https://files.pythonhosted.org/packages/2a/20/8fc8996afe5815fa1a6be8e9e5c02f24500f409d599e905800d498a4e14d/hf_xet-1.5.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:872d5601e6deea30d15865ede55d29eac6daf5a534ab417b99b6ef6b076dd96c", size = 4023495, upload-time = "2026-05-06T06:18:01.94Z" }, - { url = "https://files.pythonhosted.org/packages/32/6a/93d84463c00cecb561a7508aa6303e35ee2894294eac14245526924415fe/hf_xet-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9929561f5abf4581c8ea79587881dfef6b8abb2a0d8a51915936fc2a614f4e73", size = 3792731, upload-time = "2026-05-06T06:18:00.021Z" }, - { url = "https://files.pythonhosted.org/packages/9d/5a/8ec8e0c863b382d00b3c2e2af6ded6b06371be617144a625903a6d562f4b/hf_xet-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f7b7bbae318e583a86fb21e5a4a175d6721d628a2874f4bd022d0e660c32a682", size = 4456738, upload-time = "2026-05-06T06:17:49.574Z" }, - { url = "https://files.pythonhosted.org/packages/c5/ca/f7effa1a67717da2bcc6b6c28f71c6ca648c77acaec4e2c32f40cbe16d85/hf_xet-1.5.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:cf7b2dc6f31a4ea754bb50f74cde482dcf5d366d184076d8530b9872787f3761", size = 4251622, upload-time = "2026-05-06T06:17:47.096Z" }, - { url = "https://files.pythonhosted.org/packages/65/f2/19247dba3e231cf77dec59ddfb878f00057635ff773d099c9b59d37812c3/hf_xet-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8dbcbab554c9ef158ef2c991545c3e970ddd8cc7acdcd0a78c5a41095dab4ded", size = 4445667, upload-time = "2026-05-06T06:18:11.983Z" }, - { url = "https://files.pythonhosted.org/packages/7f/64/6f116801a3bcfb6f59f5c251f48cadc47ea54026441c4a385079286a94fa/hf_xet-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5906bf7718d3636dc13402914736abe723492cb730f744834f5f5b67d3a12702", size = 4664619, upload-time = "2026-05-06T06:18:13.771Z" }, - { url = "https://files.pythonhosted.org/packages/5c/e8/069542d37946ed08669b127e1496fa99e78196d71de8d41eda5e9f1b7a58/hf_xet-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5f3dc2248fc01cc0a00cd392ab497f1ca373fcbc7e3f2da1f452480b384e839e", size = 3966802, upload-time = "2026-05-06T06:18:28.162Z" }, - { url = "https://files.pythonhosted.org/packages/f9/91/fc6fdec27b14d04e88c386ac0a0129732b53fa23f7c4a78f4b83a039c567/hf_xet-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b285cea1b5bab46b758772716ba8d6854a1a0310fed1c249d678a8b38601e5a0", size = 3797168, upload-time = "2026-05-06T06:18:26.287Z" }, - { url = "https://files.pythonhosted.org/packages/3d/fb/69ff198a82cae7eb1a69fb84d93b3a3e4816564d76817fe541ddc96874eb/hf_xet-1.5.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:dad0dc84e941b8ba3c860659fe1fdc35c049d47cce293f003287757e971a8f56", size = 4030814, upload-time = "2026-05-06T06:17:57.933Z" }, - { url = "https://files.pythonhosted.org/packages/9b/ff/edcc2b40162bef3ff78e14ab637e5f3b89243d6aee72f5949d3bb6a5af83/hf_xet-1.5.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:fd6e5a9b0fdac4ed03ed45ef79254a655b1aaab514a02202617fbf643f5fdf7a", size = 3798444, upload-time = "2026-05-06T06:17:55.79Z" }, - { url = "https://files.pythonhosted.org/packages/49/4d/103f76b04310e5e57656696cc184690d20c466af0bca3ca88f8c8ea5d4f3/hf_xet-1.5.0-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3531b1823a0e6d77d80f9ed15ca0e00f0d115094f8ac033d5cae88f4564cc949", size = 4465986, upload-time = "2026-05-06T06:17:44.886Z" }, - { url = "https://files.pythonhosted.org/packages/c4/a2/546f47f464737b3edbab6f8ddb57f2599b93d2cbb66f06abb475ccb48651/hf_xet-1.5.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9a0ee58cd18d5ea799f7ed11290bbccbe56bdd8b1d97ca74b9cc49a3945d7a3b", size = 4259865, upload-time = "2026-05-06T06:17:42.639Z" }, - { url = "https://files.pythonhosted.org/packages/95/7f/1be593c1f28613be2e196473481cd81bfc5910795e30a34e8f744f6cac4f/hf_xet-1.5.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e60df5a42e9bed8628b6416af2cba4cba57ae9f02de226a06b020d98e1aab18", size = 4459835, upload-time = "2026-05-06T06:18:08.026Z" }, - { url = "https://files.pythonhosted.org/packages/aa/b2/703569fc881f3284487e68cda7b42179978480da3c438042a6bbbb4a671c/hf_xet-1.5.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4b35549ce62601b84da4ff9b24d970032ace3d4430f52d91bcbb26c901d6c690", size = 4672414, upload-time = "2026-05-06T06:18:09.864Z" }, - { url = "https://files.pythonhosted.org/packages/af/37/1b6def445c567286b50aa3b33828158e135b1be44938dde59f11382a500c/hf_xet-1.5.0-cp37-abi3-win_amd64.whl", hash = "sha256:2806c7c17b4d23f8d88f7c4814f838c3b6150773fe339c20af23e1cfaf2797e4", size = 3977238, upload-time = "2026-05-06T06:18:23.621Z" }, - { url = "https://files.pythonhosted.org/packages/62/94/3b66b148778ee100dcfd69c2ca22b57b41b44d3063ceec934f209e9184ce/hf_xet-1.5.0-cp37-abi3-win_arm64.whl", hash = "sha256:b6c9df403040248c76d808d3e047d64db2d923bae593eb244c41e425cf6cd7be", size = 3806916, upload-time = "2026-05-06T06:18:21.7Z" }, +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, upload-time = "2026-03-31T22:40:07.874Z" } +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, upload-time = "2026-03-31T22:39:49.618Z" }, + { 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, upload-time = "2026-03-31T22:39:47.938Z" }, + { 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, upload-time = "2026-03-31T22:39:39.69Z" }, + { 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, upload-time = "2026-03-31T22:39:37.936Z" }, + { 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, upload-time = "2026-03-31T22:39:58.382Z" }, + { 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, upload-time = "2026-03-31T22:40:00.225Z" }, + { url = "https://files.pythonhosted.org/packages/46/f2/adc32dae6bdbc367853118b9878139ac869419a4ae7ba07185dc31251b76/hf_xet-1.4.3-cp313-cp313t-win_amd64.whl", hash = "sha256:42ee323265f1e6a81b0e11094564fb7f7e0ec75b5105ffd91ae63f403a11931b", size = 3671610, upload-time = "2026-03-31T22:40:10.42Z" }, + { url = "https://files.pythonhosted.org/packages/e2/19/25d897dcc3f81953e0c2cde9ec186c7a0fee413eb0c9a7a9130d87d94d3a/hf_xet-1.4.3-cp313-cp313t-win_arm64.whl", hash = "sha256:27c976ba60079fb8217f485b9c5c7fcd21c90b0367753805f87cb9f3cdc4418a", size = 3528529, upload-time = "2026-03-31T22:40:09.106Z" }, + { 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, upload-time = "2026-03-31T22:39:56.651Z" }, + { 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, upload-time = "2026-03-31T22:39:54.766Z" }, + { 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, upload-time = "2026-03-31T22:39:46.246Z" }, + { 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, upload-time = "2026-03-31T22:39:44.648Z" }, + { 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, upload-time = "2026-03-31T22:40:04.951Z" }, + { 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, upload-time = "2026-03-31T22:40:06.347Z" }, + { url = "https://files.pythonhosted.org/packages/e4/76/ab597bae87e1f06d18d3ecb8ed7f0d3c9a37037fc32ce76233d369273c64/hf_xet-1.4.3-cp314-cp314t-win_amd64.whl", hash = "sha256:0392c79b7cf48418cd61478c1a925246cf10639f4cd9d94368d8ca1e8df9ea07", size = 3672280, upload-time = "2026-03-31T22:40:16.401Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/2e462d34e23a09a74d73785dbed71cc5dbad82a72eee2ad60a72a554155d/hf_xet-1.4.3-cp314-cp314t-win_arm64.whl", hash = "sha256:681c92a07796325778a79d76c67011764ecc9042a8c3579332b61b63ae512075", size = 3528945, upload-time = "2026-03-31T22:40:14.995Z" }, + { 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, upload-time = "2026-03-31T22:39:53.105Z" }, + { 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, upload-time = "2026-03-31T22:39:51.295Z" }, + { 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, upload-time = "2026-03-31T22:39:42.922Z" }, + { 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, upload-time = "2026-03-31T22:39:41.335Z" }, + { 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, upload-time = "2026-03-31T22:40:01.936Z" }, + { 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, upload-time = "2026-03-31T22:40:03.492Z" }, + { url = "https://files.pythonhosted.org/packages/8a/21/75a6c175b4e79662ad8e62f46a40ce341d8d6b206b06b4320d07d55b188c/hf_xet-1.4.3-cp37-abi3-win_amd64.whl", hash = "sha256:6b591fcad34e272a5b02607485e4f2a1334aebf1bc6d16ce8eb1eb8978ac2021", size = 3677359, upload-time = "2026-03-31T22:40:13.619Z" }, + { url = "https://files.pythonhosted.org/packages/8a/7c/44314ecd0e89f8b2b51c9d9e5e7a60a9c1c82024ac471d415860557d3cd8/hf_xet-1.4.3-cp37-abi3-win_arm64.whl", hash = "sha256:7c2c7e20bcfcc946dc67187c203463f5e932e395845d098cc2a93f5b67ca0b47", size = 3533664, upload-time = "2026-03-31T22:40:12.152Z" }, ] [[package]] @@ -951,7 +954,7 @@ wheels = [ [[package]] name = "huggingface-hub" -version = "1.13.0" +version = "1.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, @@ -964,9 +967,9 @@ dependencies = [ { name = "typer" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/89/ff/ec7ed2eb43bd7ce8bb2233d109cc235c3e807ffe5e469dc09db261fac05e/huggingface_hub-1.13.0.tar.gz", hash = "sha256:f6df2dac5abe82ce2fe05873d10d5ff47bc677d616a2f521f4ee26db9415d9d0", size = 781788, upload-time = "2026-04-30T11:57:33.858Z" } +sdist = { url = "https://files.pythonhosted.org/packages/56/52/1b54cb569509c725a32c1315261ac9fd0e6b91bbbf74d86fca10d3376164/huggingface_hub-1.12.0.tar.gz", hash = "sha256:7c3fe85e24b652334e5d456d7a812cd9a071e75630fac4365d9165ab5e4a34b6", size = 763091, upload-time = "2026-04-24T13:32:08.674Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/93/db/4b1cdae9460ae1f3ca020cd767f013430ce23eb1d9c890ae3a0609b38d26/huggingface_hub-1.13.0-py3-none-any.whl", hash = "sha256:e942cb50d6a08dd5306688b1ac05bda157fd2fcc88b63dae405f7bd0d3234005", size = 660643, upload-time = "2026-04-30T11:57:31.802Z" }, + { url = "https://files.pythonhosted.org/packages/7e/2b/ef03ddb96bd1123503c2bd6932001020292deea649e9bf4caa2cb65a85bf/huggingface_hub-1.12.0-py3-none-any.whl", hash = "sha256:d74939969585ee35748bd66de09baf84099d461bda7287cd9043bfb99b0e424d", size = 646806, upload-time = "2026-04-24T13:32:06.717Z" }, ] [[package]] @@ -1285,104 +1288,104 @@ wheels = [ [[package]] name = "librt" -version = "0.10.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/39/cb/c1945e506893b5b8577fb45a60c80e3ffe4a82092a04a6f29b0b951d9a24/librt-0.10.0.tar.gz", hash = "sha256:1aba1e8aa4e3307a7be68a74149545fde7451964dc0235a8bec5704a17bdda42", size = 191799, upload-time = "2026-05-05T16:31:23.535Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cf/18/827e5c1262a88c2602e86f99aee0f288ffea3280dbd2ff448858ef9dc6e9/librt-0.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7dc99f9642100b86e5f6bb14cdc9970009e31a9ef7d64df6704b7018451524a3", size = 76461, upload-time = "2026-05-05T16:29:00.422Z" }, - { url = "https://files.pythonhosted.org/packages/ee/90/54254e30287f5a5abec6fef22d976987476e966be5fdff51fe8c2d5d73d1/librt-0.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8298cedfcfaff3790000bd057aaaa3df1b0ab54cf7b48eeab16184cbb1bc66b9", size = 79740, upload-time = "2026-05-05T16:29:01.926Z" }, - { url = "https://files.pythonhosted.org/packages/4c/20/e93264b52113669d98d3b63ff94d4ce0c4dd49ae0503f1788440a884e5f0/librt-0.10.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee7dbe312dbf76468255b79a7ba311236fde620f2f7055fc09d421e31340314e", size = 243472, upload-time = "2026-05-05T16:29:03.373Z" }, - { url = "https://files.pythonhosted.org/packages/35/ad/34a5141178e8b18a4cfa45d1a0d523c84397e2abd5d06fea2d846da687e8/librt-0.10.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:56ed90c48c19249012dadfd79a1bc13bd5168ea60a70722d330a3a600c0b1852", size = 232073, upload-time = "2026-05-05T16:29:04.815Z" }, - { url = "https://files.pythonhosted.org/packages/97/1f/67240e910cd9f9ab1498c1470738345fc29dce5dc9719db1e0e09d1e861f/librt-0.10.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d74ca0f4b2b09c117f913d4df01f6b934dff8a271096b35167d5264a31649f0", size = 256956, upload-time = "2026-05-05T16:29:06.516Z" }, - { url = "https://files.pythonhosted.org/packages/22/50/3a2b3482c27d607f6e8216d913c6bc592b9a2141d96990309452340a78e3/librt-0.10.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8eb2daa9375f93c0e55ff5e44a4bbe98f39e5fe52e1abf9c97acb67743b61bf8", size = 250593, upload-time = "2026-05-05T16:29:08.324Z" }, - { url = "https://files.pythonhosted.org/packages/e7/1c/07dba133d79f93322fa17514062f1a2a50d6bdfb7baec4acf78193d7fad1/librt-0.10.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7b09b90e634e6dff57978cd358070046071e2b120501f10787aeb35425f504f6", size = 263582, upload-time = "2026-05-05T16:29:09.866Z" }, - { url = "https://files.pythonhosted.org/packages/aa/ac/033f2c6d6ab0b48f15f02e5bf065521b11a51922806017f8b6274df30d69/librt-0.10.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:2cf22fd379d60c739b800d4295ed34045f8b04aa8df9c12bd2f8f43f7fe672b7", size = 259307, upload-time = "2026-05-05T16:29:11.675Z" }, - { url = "https://files.pythonhosted.org/packages/6e/10/679046cd75d5a52c0104c890d8f69574ef4e619c683e59c15584d03a2457/librt-0.10.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:74c798793fcf29a84d442278ebe0bb1fff79fe58ac4106eeff7019cbba861423", size = 257342, upload-time = "2026-05-05T16:29:13.14Z" }, - { url = "https://files.pythonhosted.org/packages/1e/d5/dbaac9c0884f78a53dda22b9ec92bb788e1400e762ed7623fa96928c8da5/librt-0.10.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:dc4f1573401e8dbe6c26511fe027620b0fb30ae9a7ab814e02e510626b8b5f9c", size = 280141, upload-time = "2026-05-05T16:29:14.922Z" }, - { url = "https://files.pythonhosted.org/packages/cc/81/71f18cf8eb340d9fda011498870910f6a8697aeb50833005d3d8107653fd/librt-0.10.0-cp310-cp310-win32.whl", hash = "sha256:e1428275f5fe3d4db6822e58d8b005a5b28ffca55e8433ebc051247fbe46429f", size = 62257, upload-time = "2026-05-05T16:29:16.226Z" }, - { url = "https://files.pythonhosted.org/packages/df/52/6bcebc2f870c4836bcb372be885fae7f17a1d25037d3a8250ef79fbe0124/librt-0.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:0708e9408f585b0f065081680583a577652099680ccf820c7538904322b679c3", size = 70321, upload-time = "2026-05-05T16:29:17.41Z" }, - { url = "https://files.pythonhosted.org/packages/e2/a3/1472717d2325adacc8d335ba2e4078015c09d75b599f3cf48e967b3d306e/librt-0.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:01b4500ca3a625450c032a9142a8e843923ce263fa8a92ad1b38927cabe2fe72", size = 76045, upload-time = "2026-05-05T16:29:18.731Z" }, - { url = "https://files.pythonhosted.org/packages/a6/31/bfe32355d4b369aef3d7aa442df663bb5558c2ffa2de286cb2956346bc24/librt-0.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6b7e42d1b3e300d20bfc87e72ffd62f0a92a2cb3c35f7bf90df90c9d2a49f74c", size = 79466, upload-time = "2026-05-05T16:29:20.052Z" }, - { url = "https://files.pythonhosted.org/packages/e9/f1/83f8a2c715ba2cac9b7387a5a5cea25f717f7184320cfe48b36bed9c58e9/librt-0.10.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8ef7b8c61ce3a1b597cd3e15348ff1574325165c2e7ce09a718154cde2a7950", size = 242283, upload-time = "2026-05-05T16:29:21.596Z" }, - { url = "https://files.pythonhosted.org/packages/cc/94/c3a4ce94857f0004a542f86662806383611858f522722db58efaec0a1472/librt-0.10.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:e73c84f72d1fa0d6eaa7a1930b436ba8d2c90c58d77bfabb09995a69ad35f6c0", size = 230735, upload-time = "2026-05-05T16:29:23.335Z" }, - { url = "https://files.pythonhosted.org/packages/d1/41/e962bb26c7728eb7b3a69e490d0c800fd9968a6970e390c1f18ddb56093d/librt-0.10.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9728cb98713bd862fb8f4fd6a642d1896c86058a41d77c70f3d5cee75e725275", size = 256606, upload-time = "2026-05-05T16:29:24.91Z" }, - { url = "https://files.pythonhosted.org/packages/66/3a/4e46a707b1ecc993fd691071623b9beab89703a63bd21cc7807e06c28209/librt-0.10.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:648b7e941d20acd72f9652115e0e53facd98156d61f9ebf7a812bdef8bdccea9", size = 249739, upload-time = "2026-05-05T16:29:26.648Z" }, - { url = "https://files.pythonhosted.org/packages/b2/f5/dc5b7eb294656ad23d4ff4cf8514208d54fe1026b909d726a0dc026689c9/librt-0.10.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c3e33747c068e86a9007c20fdb777eb5ba8d3d19136d7812f88e69a713041b6f", size = 261414, upload-time = "2026-05-05T16:29:28.702Z" }, - { url = "https://files.pythonhosted.org/packages/58/e4/990ed8d12c7f114ac8f8ccd47f7d9bd9704ef61acfcb1df4a05047da7710/librt-0.10.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d509c745bf7e77d1107cf05e6abb249dc03fad13eb39f2286a49deedaeb2bcd7", size = 256614, upload-time = "2026-05-05T16:29:30.357Z" }, - { url = "https://files.pythonhosted.org/packages/60/eb/52d2726c7fb22818507dc3cc166c8f36dd4a4b68a7be67f12006ac8777c1/librt-0.10.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:786ad5a15e99d0e0e74f3adbeecc198a5ac58f340be07e984723d1e0074838de", size = 255144, upload-time = "2026-05-05T16:29:32.106Z" }, - { url = "https://files.pythonhosted.org/packages/bc/df/bd5591a78f7531fce4b6eb9962aadc6adc9560a01570442a884b6e554abe/librt-0.10.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:075582d877a97ee3d8e77bda3689dbe617b14f6469224a2d80b4b6c38e3951aa", size = 279121, upload-time = "2026-05-05T16:29:33.688Z" }, - { url = "https://files.pythonhosted.org/packages/fd/df/7c2b838dfc89a1762dd156d8b0c39848a7a2845d725a50be5a6e021fb8ba/librt-0.10.0-cp311-cp311-win32.whl", hash = "sha256:75ecdc3f5a90065aa2af2e574706c5495adc392520762dcf10b1aa716f0b8090", size = 62593, upload-time = "2026-05-05T16:29:35.152Z" }, - { url = "https://files.pythonhosted.org/packages/91/19/22ff572981049a9d436a083dbea1572d0f5dc068b7353637d2dd9977c8f1/librt-0.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:b6f6084884131d8a52cb9d7095ff2aa52c1e786d9fdaefab1fb4515415e9e083", size = 70914, upload-time = "2026-05-05T16:29:36.407Z" }, - { url = "https://files.pythonhosted.org/packages/12/22/1697cc64f4a5c7e9bce55e99c6d234a346beaedaefcd1e2ca90dd285f98c/librt-0.10.0-cp311-cp311-win_arm64.whl", hash = "sha256:0140bd62151160047e89b2730cb6f8506cdac5127baa1afb9231e4dd3fe7f681", size = 61176, upload-time = "2026-05-05T16:29:37.62Z" }, - { url = "https://files.pythonhosted.org/packages/12/8e/cbb5b6f6e45e65c10a42449a69eaccc44d73e6a081ea752fbc5221c6dc1c/librt-0.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b4b58a44b407e91f633dafee008de9ddea6aa2a555ed94929c099260910bd0ba", size = 77327, upload-time = "2026-05-05T16:29:38.919Z" }, - { url = "https://files.pythonhosted.org/packages/e9/3d/8233cbee8e99e6a8992f02bfc2dec8d787509566a511d1fde2574ee7473f/librt-0.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:950b79b11762531bdf45a9df909d2f9a2a8445c70c88665c01d14c8511a27dc5", size = 79971, upload-time = "2026-05-05T16:29:40.96Z" }, - { url = "https://files.pythonhosted.org/packages/87/6f/5264b298cef2b72fc97d2dde56c66181eda35204bf5dcd1ed0c3d0a0a782/librt-0.10.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4538453f51be197633b425912c150e25b0667252d3741c53e8368176d98d9d37", size = 246559, upload-time = "2026-05-05T16:29:42.701Z" }, - { url = "https://files.pythonhosted.org/packages/07/7b/19b1b859cc60d5f99276cc2b3144d91556c6d1b1e4ebb50359696bebf7a8/librt-0.10.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:70b955f091beac93e994a0b7ec616934f63b3ea5c3d6d7af847562f935aceca7", size = 235216, upload-time = "2026-05-05T16:29:44.193Z" }, - { url = "https://files.pythonhosted.org/packages/6e/56/a2f40717142a8af46289f57874ef914353d8faccd5e4f8e594ab1e16e8c7/librt-0.10.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:483e685e06b6163728ba6c85d74315176be7190f432ec2a41226e5e14355d5f0", size = 263108, upload-time = "2026-05-05T16:29:46.365Z" }, - { url = "https://files.pythonhosted.org/packages/67/ca/15c625c3bdc0167c01e04ef8878317e9713f3bfa788438342f7a94c7b22c/librt-0.10.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7ac53d946a009d1a38c44a60812708c9458fb2a239a5f630d8e625571386650f", size = 255280, upload-time = "2026-05-05T16:29:48.087Z" }, - { url = "https://files.pythonhosted.org/packages/ed/c5/ba301d571d9e05844e2435b73aba30bee77bb75ce155c9affcfd2173dd03/librt-0.10.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bc8771c9fcf0ea894ca41fdc2abd83572c2fbda221f232d86e718614e57ff513", size = 268829, upload-time = "2026-05-05T16:29:49.628Z" }, - { url = "https://files.pythonhosted.org/packages/8b/60/af70e135bc1f1fe15dd3894b1e4bbefc7ecdf911749a925a39eb86ceb2a1/librt-0.10.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:70805dbc5257892ac572f86290a61e3c8d90224ecce1a8b2d1f7ed51965417f4", size = 262051, upload-time = "2026-05-05T16:29:51.244Z" }, - { url = "https://files.pythonhosted.org/packages/83/c2/c8236eb8b421bac5a172ba208f965abaa89805da2a3fa112bdf1764caf8f/librt-0.10.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d3b4f300f7bcba6e2ff73fb8bef1898479e9772bfa2682998c636391633ec826", size = 264347, upload-time = "2026-05-05T16:29:53.013Z" }, - { url = "https://files.pythonhosted.org/packages/d6/f5/15b6d32bc25dacd4a60886a683d8128d6219910c122202b995a40dd4f8d2/librt-0.10.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:943bc943f92f4fb3408fae62485c6a3ad68ce4f2ee205643a39641525c19a276", size = 286482, upload-time = "2026-05-05T16:29:54.675Z" }, - { url = "https://files.pythonhosted.org/packages/fb/8e/b1b959bacd323eb4360579db992513e1406d1c6ef7edb57b5511fd0666fd/librt-0.10.0-cp312-cp312-win32.whl", hash = "sha256:6065c1a758fba1010b41401013903d3d5d2750eab425ddedd584abac31d0630e", size = 62955, upload-time = "2026-05-05T16:29:56.39Z" }, - { url = "https://files.pythonhosted.org/packages/9e/4c/d4cd6e4b9fc24098e63cc85537d1b6689682aee96809c38f08072067cc2b/librt-0.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:d788ecbe208ab352dab0e105cc06057bf9a2fc7e58cabb0d751ad9e30062b9e2", size = 71191, upload-time = "2026-05-05T16:29:57.682Z" }, - { url = "https://files.pythonhosted.org/packages/2b/19/8641da1f63d24b92354a492f893c022d6b3a0df44e70c8eff49364613983/librt-0.10.0-cp312-cp312-win_arm64.whl", hash = "sha256:6003d1f295bdba02656dc81308208fc060d0a51d8c0d0a6db70f7f3c57b9ba0a", size = 61432, upload-time = "2026-05-05T16:29:58.971Z" }, - { url = "https://files.pythonhosted.org/packages/e5/29/681a75c82f4cc90d29e4b257a3299b79fe13fe927a04c57b8109d70b6957/librt-0.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f0ede79d682e73f91c1b599a76d78b7464b9b5d213754cedb13372d9df36e596", size = 77299, upload-time = "2026-05-05T16:30:00.209Z" }, - { url = "https://files.pythonhosted.org/packages/62/24/0c7ca445a55d04be79cac19819437fd094782347fa116f6681844fa6143e/librt-0.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e0ba0b131fdb336c8b9c948e397f4a7e649d0f783b529f07b647bf4961df392e", size = 79930, upload-time = "2026-05-05T16:30:01.555Z" }, - { url = "https://files.pythonhosted.org/packages/fe/1f/1e2b8f6443ef9e9a81e89486ca70e22f3684f93db003ce6eaefc3d0839b9/librt-0.10.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2728117da2afb96fb957768725ee43dc9a2d73b031e02da424b818a3cdd3a275", size = 246195, upload-time = "2026-05-05T16:30:03.261Z" }, - { url = "https://files.pythonhosted.org/packages/74/61/9dc9e03de0439ad84c1c240aac8b747f12c90cb797ea6042f7bdb8d3410f/librt-0.10.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:723ba80594c49cdf0584196fc430752262605dc9449902fc9bd3d9b79976cb77", size = 234951, upload-time = "2026-05-05T16:30:04.881Z" }, - { url = "https://files.pythonhosted.org/packages/55/f4/635223117d7590875bca441275065a3bf491203ad4208bd1cc3ffd90c5a1/librt-0.10.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7292edaaca294a61a978c53a3c7d6130d099b0dfbc8f0a65916cdc6b891b9852", size = 262768, upload-time = "2026-05-05T16:30:06.638Z" }, - { url = "https://files.pythonhosted.org/packages/e5/66/b04152d0cd8b6ca2b428a8bd3230343230c35ed304a932f35b5375f2f828/librt-0.10.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:89fe9d539f2c10a1666633eeeac507ce95dd06d9ecc58de3c6390dba156a3d3a", size = 255075, upload-time = "2026-05-05T16:30:08.216Z" }, - { url = "https://files.pythonhosted.org/packages/35/1e/25bac4c7f2ca36f0e612cade186970683cf79153d96beccc3a11a9e19b97/librt-0.10.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4efa7b9587503fa5b67f40593302b9c8836d211d222ff9f7cafe67be5f8f0b10", size = 268559, upload-time = "2026-05-05T16:30:10.1Z" }, - { url = "https://files.pythonhosted.org/packages/18/54/4601faab35b6632a13200faa146ca62bfd111ffbe2568be430d65c89493a/librt-0.10.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:22dc982ef59df0136df36092ccbdbb570ced8aafb33e49585739b2f1de1c13b6", size = 261753, upload-time = "2026-05-05T16:30:11.912Z" }, - { url = "https://files.pythonhosted.org/packages/1b/cf/39f4023509e94fade8b074666fa3292db9cb6b34ea5dcbe7af53df9fca1d/librt-0.10.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6f2e5f3606253a84cea719c94a3bb1c54487b5d617d0254d46e0920d8a06be3f", size = 264055, upload-time = "2026-05-05T16:30:13.465Z" }, - { url = "https://files.pythonhosted.org/packages/8e/00/40247209fc46a8e308a91412d5206aedf8efb667ee89eb625820106a5c2f/librt-0.10.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:40884bfaa1e29f6b6a9be255007d8f359bfc9e61d68bdef8ed3158bfcbc95df9", size = 286190, upload-time = "2026-05-05T16:30:15.073Z" }, - { url = "https://files.pythonhosted.org/packages/d8/6e/5566beb94431a985abe1787af5ef86e087750172ff9d0bbf20f93e88132d/librt-0.10.0-cp313-cp313-win32.whl", hash = "sha256:3cd34cd8254eba756660bff6c2da91278248184301054fe3e4feb073bdd49b14", size = 62949, upload-time = "2026-05-05T16:30:16.503Z" }, - { url = "https://files.pythonhosted.org/packages/d0/c2/3ea3301d6c8dff51d39dbe8ed75db3dc92896947d4afb5eeadf821c1e67f/librt-0.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:7baac5313e2d8dce1386f97777a8d03ab28f5fe1e780b3b9ac2ee7544551fedc", size = 71152, upload-time = "2026-05-05T16:30:17.766Z" }, - { url = "https://files.pythonhosted.org/packages/3c/de/5d49cb92cadcbc77d3abc27b93fd6030ed8437487dde2eae38cab5e6704d/librt-0.10.0-cp313-cp313-win_arm64.whl", hash = "sha256:afc5b4406c8e2515698d922a5c7823a009312835ea58196671fff40e35cb8166", size = 61336, upload-time = "2026-05-05T16:30:19.021Z" }, - { url = "https://files.pythonhosted.org/packages/6a/64/7165e08108cc185a13a9c069f0685e6ef92e70e07fddf7edf5e7348c6316/librt-0.10.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:f09588a30e6a22ec624090d72a3ab1a6d4d5485c3ed739603e76aa3c16efa688", size = 76794, upload-time = "2026-05-05T16:30:20.392Z" }, - { url = "https://files.pythonhosted.org/packages/ae/ef/bf8613febf651b90c5222ee79dea5ae58d4cc2b544df69d3033424448934/librt-0.10.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:131ade118d12bd7a0adc4e655474a553f1b76cf78385868885944d21d51e45e0", size = 79662, upload-time = "2026-05-05T16:30:22.025Z" }, - { url = "https://files.pythonhosted.org/packages/b6/67/9eddd165c1d8397bdf99b38bf12b5a55b3def5035b49eedb49f2775d1430/librt-0.10.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8b9ab28e40d011c373a189eae900c916e66d6fbecf7983e9e4883089ee085ef", size = 242390, upload-time = "2026-05-05T16:30:23.51Z" }, - { url = "https://files.pythonhosted.org/packages/10/d1/d95da80334501866cd37004ab5d7483220d05862fab4b5405394f0264f0d/librt-0.10.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:67c39bb30da73bae1f293d1ed8bc2f8f6642649dd0928d3600aeff3041ac23d6", size = 232603, upload-time = "2026-05-05T16:30:25.198Z" }, - { url = "https://files.pythonhosted.org/packages/0c/fa/e6d64d28718bc1be4e1736fcb037ca1c4dfca927e7167df75a7d5215665e/librt-0.10.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8c3273c6b774614f093c8927c2bf1b077d0fefde988fe98f46a333734e5597ab", size = 259187, upload-time = "2026-05-05T16:30:26.772Z" }, - { url = "https://files.pythonhosted.org/packages/72/3f/3fdb77e7f937dad59cfd76b720be7e7643400ec76b2da35befab8d66ba30/librt-0.10.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9dd7c1b86a4baa583ab5db977484b93a2c474e69e96ef3e9538387ea54229cb9", size = 251846, upload-time = "2026-05-05T16:30:28.56Z" }, - { url = "https://files.pythonhosted.org/packages/18/ca/f4d49133dd86a6f55d79eca30bf412fa722f511a9abe67f62f57aa64e66a/librt-0.10.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a77385c5a202e831149f7ad03be9e67cf80e957e52c614e83dcb822c95222eb8", size = 264936, upload-time = "2026-05-05T16:30:30.491Z" }, - { url = "https://files.pythonhosted.org/packages/de/66/a8df2fbadc1f6c1827a096d11c40175bd526133480bd3bc88ec64a03d257/librt-0.10.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c6a5eafa74b5655bad59886138ed68426f098a6beb8cb95a71f2cc3cd8bb33fe", size = 258699, upload-time = "2026-05-05T16:30:32.002Z" }, - { url = "https://files.pythonhosted.org/packages/bb/73/1e3c83613fe05451bb969e27b68a573d177f08d5f63533cc29fec0989658/librt-0.10.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:1fc93d0439204c50ab4d1512611ce2c206f1b369b419f69c7c27c761561e3291", size = 259825, upload-time = "2026-05-05T16:30:35.077Z" }, - { url = "https://files.pythonhosted.org/packages/09/24/5e2f926ee9d3ef348d9339526d7062abb5c44d8419e3179528c01d78c102/librt-0.10.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:79e713c178bc7a744adfbee6b4619a288eecc0c914da2a9313a20255abe2f0cf", size = 282548, upload-time = "2026-05-05T16:30:36.639Z" }, - { url = "https://files.pythonhosted.org/packages/fc/7d/3e89ed6ad0162561fa8bef9df3195e24263104c955713cd0237d3711fad2/librt-0.10.0-cp314-cp314-win32.whl", hash = "sha256:2eba9d955a68c41d9f326be3da42f163ec3518b7ab20f1c826224e7bed71e0bf", size = 58970, upload-time = "2026-05-05T16:30:38.183Z" }, - { url = "https://files.pythonhosted.org/packages/76/25/579e731c94a7086a268bfa3e7a4945cd47836bebd3cbf3faeafd2e7eaef9/librt-0.10.0-cp314-cp314-win_amd64.whl", hash = "sha256:cbfaf7f5145e9917f5d18bffa298eff6a19d74e7b8b11dabdca95785befe8dbf", size = 67260, upload-time = "2026-05-05T16:30:39.804Z" }, - { url = "https://files.pythonhosted.org/packages/6e/f8/235822b7ae0b2334f12ee18bcf2476d07924077a5efeea57dbe927704be2/librt-0.10.0-cp314-cp314-win_arm64.whl", hash = "sha256:8d6d385d1969849a6b1397114df22714b6ded917bada98668e3e974dc663477e", size = 57156, upload-time = "2026-05-05T16:30:41.412Z" }, - { url = "https://files.pythonhosted.org/packages/9f/e3/9b919cbf1e8eb770bf91bb7df28125e0f1daf4587169afefd95402636e9a/librt-0.10.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:6c3a82d3bd32631ef5c79922dfc028520c9ad840255979ab4d908271818039ee", size = 79150, upload-time = "2026-05-05T16:30:42.761Z" }, - { url = "https://files.pythonhosted.org/packages/6a/f5/72a944aa3bc3498169a168087eff58ca48b58bf1b704e59d091fd30739f3/librt-0.10.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d64cc66005dc324c9bb1fa3fc2841f529002f6eb15966d55e46d430f56955a6a", size = 82304, upload-time = "2026-05-05T16:30:44.082Z" }, - { url = "https://files.pythonhosted.org/packages/9c/e3/fcc290a33e295019759472dfa794d204e43504b276ac65eab7fd9da20ea3/librt-0.10.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9bb562cd28c88cd2c6a9a6c78f99dc39348d6b16c94adc25de0e574acf1176e9", size = 272556, upload-time = "2026-05-05T16:30:45.497Z" }, - { url = "https://files.pythonhosted.org/packages/fd/54/546975e4c997573885e7f040a05012f8838e06fb12b0c3c1fbb76254e9d7/librt-0.10.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:b809aa2854d019c28773b03605df22adc675ee4f3f4402d673581313e8906119", size = 256941, upload-time = "2026-05-05T16:30:47.059Z" }, - { url = "https://files.pythonhosted.org/packages/70/8c/f1d03401571b331653acddbd4e8cd955c06d945241dd08b25192fac0d04b/librt-0.10.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc15acabdd519bd4176fdadc2119e5e3093485d86f89138daf47e5b4cedb983a", size = 285855, upload-time = "2026-05-05T16:30:48.86Z" }, - { url = "https://files.pythonhosted.org/packages/0c/08/62cf80ff046c339faf56718b3a940244d4beb70f1c6407289b5830ec11e9/librt-0.10.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b1b2d835307d08ddadd94568e2369648ec9173bd3eea6d7f52a1abe717c81f98", size = 275321, upload-time = "2026-05-05T16:30:50.63Z" }, - { url = "https://files.pythonhosted.org/packages/d9/ea/da5918d4070362e9a4d2ee9cd34f9dc84902daad8fd4275f8504a727ff4e/librt-0.10.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d261c6a2f93335a5167887fb0223e8b98ffce20ee3fde242e8e58a37ece6d0e5", size = 293993, upload-time = "2026-05-05T16:30:52.577Z" }, - { url = "https://files.pythonhosted.org/packages/c9/8d/68b6086bed1fcdc314c640ea04e31e52d18052e08059fa595409d66a51a9/librt-0.10.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e2ffd44963f8e7f68995504d90f9881d64e94dc1d8e310039b9526108fc0c0f7", size = 284254, upload-time = "2026-05-05T16:30:55.086Z" }, - { url = "https://files.pythonhosted.org/packages/06/c8/b810f1d84ec34a5a7ed93d7b510ab04164d75fbdf23088d5c3fbe6b08357/librt-0.10.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5f285f6455ed495791c4d8630e5af732960adea93cac4c893d15619f2eae53e8", size = 284925, upload-time = "2026-05-05T16:30:56.728Z" }, - { url = "https://files.pythonhosted.org/packages/5a/00/3c82d4158c5a2c62528b8fccce65a8c9ad700e480e86f9389387435089a5/librt-0.10.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f6034ff52e663d34c7b82ef2aa2f94ad7c1d939e2368e63b06844bc4d127d2e1", size = 307830, upload-time = "2026-05-05T16:30:58.377Z" }, - { url = "https://files.pythonhosted.org/packages/99/3a/9c635ac3e8a00383ff689161d3eac8a30b3b2ddc711b40471e6b8983ea29/librt-0.10.0-cp314-cp314t-win32.whl", hash = "sha256:657860fd877fba6a241ea088ef99f63ca819945d3c715265da670bad56c37ebe", size = 60147, upload-time = "2026-05-05T16:31:00.293Z" }, - { url = "https://files.pythonhosted.org/packages/dc/e8/6f65f3e565d4ac212cddddd552eacc8035ffdf941ca0ad6fe945a211d41f/librt-0.10.0-cp314-cp314t-win_amd64.whl", hash = "sha256:56ded2d66010203a0cb5af063b609e3f079531a0e5e576d618dece859fd2e1af", size = 68649, upload-time = "2026-05-05T16:31:01.778Z" }, - { url = "https://files.pythonhosted.org/packages/51/78/a0705a67cacd81e5fa01a5035b3adbdfbb43a7b8d4bd27e2b282ae61baf2/librt-0.10.0-cp314-cp314t-win_arm64.whl", hash = "sha256:1ee63f30abf18ed4830fdbaf87b2b6f4bba1e198d46085c314edde4045e56715", size = 58247, upload-time = "2026-05-05T16:31:03.191Z" }, +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/6b/3d5c13fb3e3c4f43206c8f9dfed13778c2ed4f000bacaa0b7ce3c402a265/librt-0.9.0.tar.gz", hash = "sha256:a0951822531e7aee6e0dfb556b30d5ee36bbe234faf60c20a16c01be3530869d", size = 184368, upload-time = "2026-04-09T16:06:26.173Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/4a/c64265d71b84030174ff3ac2cd16d8b664072afab8c41fccd8e2ee5a6f8d/librt-0.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f8e12706dcb8ff6b3ed57514a19e45c49ad00bcd423e87b2b2e4b5f64578443", size = 67529, upload-time = "2026-04-09T16:04:27.373Z" }, + { url = "https://files.pythonhosted.org/packages/23/b1/30ca0b3a8bdac209a00145c66cf42e5e7da2cc056ffc6ebc5c7b430ddd34/librt-0.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4e3dda8345307fd7306db0ed0cb109a63a2c85ba780eb9dc2d09b2049a931f9c", size = 70248, upload-time = "2026-04-09T16:04:28.758Z" }, + { url = "https://files.pythonhosted.org/packages/fa/fc/c6018dc181478d6ac5aa24a5846b8185101eb90894346db239eb3ea53209/librt-0.9.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:de7dac64e3eb832ffc7b840eb8f52f76420cde1b845be51b2a0f6b870890645e", size = 202184, upload-time = "2026-04-09T16:04:29.893Z" }, + { url = "https://files.pythonhosted.org/packages/bf/58/d69629f002203370ef41ea69ff71c49a2c618aec39b226ff49986ecd8623/librt-0.9.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22a904cbdb678f7cb348c90d543d3c52f581663d687992fee47fd566dcbf5285", size = 212926, upload-time = "2026-04-09T16:04:31.126Z" }, + { url = "https://files.pythonhosted.org/packages/cc/55/01d859f57824e42bd02465c77bec31fa5ef9d8c2bcee702ccf8ef1b9f508/librt-0.9.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:224b9727eb8bc188bc3bcf29d969dba0cd61b01d9bac80c41575520cc4baabb2", size = 225664, upload-time = "2026-04-09T16:04:32.352Z" }, + { url = "https://files.pythonhosted.org/packages/9b/02/32f63ad0ef085a94a70315291efe1151a48b9947af12261882f8445b2a30/librt-0.9.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e94cbc6ad9a6aeea46d775cbb11f361022f778a9cc8cc90af653d3a594b057ce", size = 219534, upload-time = "2026-04-09T16:04:33.667Z" }, + { url = "https://files.pythonhosted.org/packages/6a/5a/9d77111a183c885acf3b3b6e4c00f5b5b07b5817028226499a55f1fedc59/librt-0.9.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7bc30ad339f4e1a01d4917d645e522a0bc0030644d8973f6346397c93ba1503f", size = 227322, upload-time = "2026-04-09T16:04:34.945Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e7/05d700c93063753e12ab230b972002a3f8f3b9c95d8a980c2f646c8b6963/librt-0.9.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:56d65b583cf43b8cf4c8fbe1e1da20fa3076cc32a1149a141507af1062718236", size = 223407, upload-time = "2026-04-09T16:04:36.22Z" }, + { url = "https://files.pythonhosted.org/packages/c0/26/26c3124823c67c987456977c683da9a27cc874befc194ddcead5f9988425/librt-0.9.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0a1be03168b2691ba61927e299b352a6315189199ca18a57b733f86cb3cc8d38", size = 221302, upload-time = "2026-04-09T16:04:37.62Z" }, + { url = "https://files.pythonhosted.org/packages/50/2b/c7cc2be5cf4ff7b017d948a789256288cb33a517687ff1995e72a7eea79f/librt-0.9.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:63c12efcd160e1d14da11af0c46c0217473e1e0d2ae1acbccc83f561ea4c2a7b", size = 243893, upload-time = "2026-04-09T16:04:38.909Z" }, + { url = "https://files.pythonhosted.org/packages/62/d3/da553d37417a337d12660450535d5fd51373caffbedf6962173c87867246/librt-0.9.0-cp310-cp310-win32.whl", hash = "sha256:e9002e98dcb1c0a66723592520decd86238ddcef168b37ff6cfb559200b4b774", size = 55375, upload-time = "2026-04-09T16:04:40.148Z" }, + { url = "https://files.pythonhosted.org/packages/9b/5a/46fa357bab8311b6442a83471591f2f9e5b15ecc1d2121a43725e0c529b8/librt-0.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:9fcb461fbf70654a52a7cc670e606f04449e2374c199b1825f754e16dacfedd8", size = 62581, upload-time = "2026-04-09T16:04:41.452Z" }, + { url = "https://files.pythonhosted.org/packages/e2/1e/2ec7afcebcf3efea593d13aee18bbcfdd3a243043d848ebf385055e9f636/librt-0.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:90904fac73c478f4b83f4ed96c99c8208b75e6f9a8a1910548f69a00f1eaa671", size = 67155, upload-time = "2026-04-09T16:04:42.933Z" }, + { url = "https://files.pythonhosted.org/packages/18/77/72b85afd4435268338ad4ec6231b3da8c77363f212a0227c1ff3b45e4d35/librt-0.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:789fff71757facc0738e8d89e3b84e4f0251c1c975e85e81b152cdaca927cc2d", size = 69916, upload-time = "2026-04-09T16:04:44.042Z" }, + { url = "https://files.pythonhosted.org/packages/27/fb/948ea0204fbe2e78add6d46b48330e58d39897e425560674aee302dca81c/librt-0.9.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1bf465d1e5b0a27713862441f6467b5ab76385f4ecf8f1f3a44f8aa3c695b4b6", size = 199635, upload-time = "2026-04-09T16:04:45.5Z" }, + { url = "https://files.pythonhosted.org/packages/ac/cd/894a29e251b296a27957856804cfd21e93c194aa131de8bb8032021be07e/librt-0.9.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f819e0c6413e259a17a7c0d49f97f405abadd3c2a316a3b46c6440b7dbbedbb1", size = 211051, upload-time = "2026-04-09T16:04:47.016Z" }, + { url = "https://files.pythonhosted.org/packages/18/8f/dcaed0bc084a35f3721ff2d081158db569d2c57ea07d35623ddaca5cfc8e/librt-0.9.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e0785c2fb4a81e1aece366aa3e2e039f4a4d7d21aaaded5227d7f3c703427882", size = 224031, upload-time = "2026-04-09T16:04:48.207Z" }, + { url = "https://files.pythonhosted.org/packages/03/44/88f6c1ed1132cd418601cc041fbd92fed28b3a09f39de81978e0822d13ff/librt-0.9.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:80b25c7b570a86c03b5da69e665809deb39265476e8e21d96a9328f9762f9990", size = 218069, upload-time = "2026-04-09T16:04:50.025Z" }, + { url = "https://files.pythonhosted.org/packages/a3/90/7d02e981c2db12188d82b4410ff3e35bfdb844b26aecd02233626f46af2b/librt-0.9.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d4d16b608a1c43d7e33142099a75cd93af482dadce0bf82421e91cad077157f4", size = 224857, upload-time = "2026-04-09T16:04:51.684Z" }, + { url = "https://files.pythonhosted.org/packages/ef/c3/c77e706b7215ca32e928d47535cf13dbc3d25f096f84ddf8fbc06693e229/librt-0.9.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:194fc1a32e1e21fe809d38b5faea66cc65eaa00217c8901fbdb99866938adbdb", size = 219865, upload-time = "2026-04-09T16:04:52.949Z" }, + { url = "https://files.pythonhosted.org/packages/52/d1/32b0c1a0eb8461c70c11656c46a29f760b7c7edf3c36d6f102470c17170f/librt-0.9.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8c6bc1384d9738781cfd41d09ad7f6e8af13cfea2c75ece6bd6d2566cdea2076", size = 218451, upload-time = "2026-04-09T16:04:54.174Z" }, + { url = "https://files.pythonhosted.org/packages/74/d1/adfd0f9c44761b1d49b1bec66173389834c33ee2bd3c7fd2e2367f1942d4/librt-0.9.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:15cb151e52a044f06e54ac7f7b47adbfc89b5c8e2b63e1175a9d587c43e8942a", size = 241300, upload-time = "2026-04-09T16:04:55.452Z" }, + { url = "https://files.pythonhosted.org/packages/09/b0/9074b64407712f0003c27f5b1d7655d1438979155f049720e8a1abd9b1a1/librt-0.9.0-cp311-cp311-win32.whl", hash = "sha256:f100bfe2acf8a3689af9d0cc660d89f17286c9c795f9f18f7b62dd1a6b247ae6", size = 55668, upload-time = "2026-04-09T16:04:56.689Z" }, + { url = "https://files.pythonhosted.org/packages/24/19/40b77b77ce80b9389fb03971431b09b6b913911c38d412059e0b3e2a9ef2/librt-0.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:0b73e4266307e51c95e09c0750b7ec383c561d2e97d58e473f6f6a209952fbb8", size = 62976, upload-time = "2026-04-09T16:04:57.733Z" }, + { url = "https://files.pythonhosted.org/packages/70/9d/9fa7a64041e29035cb8c575af5f0e3840be1b97b4c4d9061e0713f171849/librt-0.9.0-cp311-cp311-win_arm64.whl", hash = "sha256:bc5518873822d2faa8ebdd2c1a4d7c8ef47b01a058495ab7924cb65bdbf5fc9a", size = 53502, upload-time = "2026-04-09T16:04:58.806Z" }, + { url = "https://files.pythonhosted.org/packages/bf/90/89ddba8e1c20b0922783cd93ed8e64f34dc05ab59c38a9c7e313632e20ff/librt-0.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9b3e3bc363f71bda1639a4ee593cb78f7fbfeacc73411ec0d4c92f00730010a4", size = 68332, upload-time = "2026-04-09T16:05:00.09Z" }, + { url = "https://files.pythonhosted.org/packages/a8/40/7aa4da1fb08bdeeb540cb07bfc8207cb32c5c41642f2594dbd0098a0662d/librt-0.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0a09c2f5869649101738653a9b7ab70cf045a1105ac66cbb8f4055e61df78f2d", size = 70581, upload-time = "2026-04-09T16:05:01.213Z" }, + { url = "https://files.pythonhosted.org/packages/48/ac/73a2187e1031041e93b7e3a25aae37aa6f13b838c550f7e0f06f66766212/librt-0.9.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5ca8e133d799c948db2ab1afc081c333a825b5540475164726dcbf73537e5c2f", size = 203984, upload-time = "2026-04-09T16:05:02.542Z" }, + { url = "https://files.pythonhosted.org/packages/5e/3d/23460d571e9cbddb405b017681df04c142fb1b04cbfce77c54b08e28b108/librt-0.9.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:603138ee838ee1583f1b960b62d5d0007845c5c423feb68e44648b1359014e27", size = 215762, upload-time = "2026-04-09T16:05:04.127Z" }, + { url = "https://files.pythonhosted.org/packages/de/1e/42dc7f8ab63e65b20640d058e63e97fd3e482c1edbda3570d813b4d0b927/librt-0.9.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4003f70c56a5addd6aa0897f200dd59afd3bf7bcd5b3cce46dd21f925743bc2", size = 230288, upload-time = "2026-04-09T16:05:05.883Z" }, + { url = "https://files.pythonhosted.org/packages/dc/08/ca812b6d8259ad9ece703397f8ad5c03af5b5fedfce64279693d3ce4087c/librt-0.9.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:78042f6facfd98ecb25e9829c7e37cce23363d9d7c83bc5f72702c5059eb082b", size = 224103, upload-time = "2026-04-09T16:05:07.148Z" }, + { url = "https://files.pythonhosted.org/packages/b6/3f/620490fb2fa66ffd44e7f900254bc110ebec8dac6c1b7514d64662570e6f/librt-0.9.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a361c9434a64d70a7dbb771d1de302c0cc9f13c0bffe1cf7e642152814b35265", size = 232122, upload-time = "2026-04-09T16:05:08.386Z" }, + { url = "https://files.pythonhosted.org/packages/e9/83/12864700a1b6a8be458cf5d05db209b0d8e94ae281e7ec261dbe616597b4/librt-0.9.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:dd2c7e082b0b92e1baa4da28163a808672485617bc855cc22a2fd06978fa9084", size = 225045, upload-time = "2026-04-09T16:05:09.707Z" }, + { url = "https://files.pythonhosted.org/packages/fd/1b/845d339c29dc7dbc87a2e992a1ba8d28d25d0e0372f9a0a2ecebde298186/librt-0.9.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7e6274fd33fc5b2a14d41c9119629d3ff395849d8bcbc80cf637d9e8d2034da8", size = 227372, upload-time = "2026-04-09T16:05:10.942Z" }, + { url = "https://files.pythonhosted.org/packages/8d/fe/277985610269d926a64c606f761d58d3db67b956dbbf40024921e95e7fcb/librt-0.9.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5093043afb226ecfa1400120d1ebd4442b4f99977783e4f4f7248879009b227f", size = 248224, upload-time = "2026-04-09T16:05:12.254Z" }, + { url = "https://files.pythonhosted.org/packages/92/1b/ee486d244b8de6b8b5dbaefabe6bfdd4a72e08f6353edf7d16d27114da8d/librt-0.9.0-cp312-cp312-win32.whl", hash = "sha256:9edcc35d1cae9fd5320171b1a838c7da8a5c968af31e82ecc3dff30b4be0957f", size = 55986, upload-time = "2026-04-09T16:05:13.529Z" }, + { url = "https://files.pythonhosted.org/packages/89/7a/ba1737012308c17dc6d5516143b5dce9a2c7ba3474afd54e11f44a4d1ef3/librt-0.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:3cc2917258e131ae5f958a4d872e07555b51cb7466a43433218061c74ef33745", size = 63260, upload-time = "2026-04-09T16:05:14.68Z" }, + { url = "https://files.pythonhosted.org/packages/36/e4/01752c113da15127f18f7bf11142f5640038f062407a611c059d0036c6aa/librt-0.9.0-cp312-cp312-win_arm64.whl", hash = "sha256:90e6d5420fc8a300518d4d2288154ff45005e920425c22cbbfe8330f3f754bd9", size = 53694, upload-time = "2026-04-09T16:05:16.095Z" }, + { url = "https://files.pythonhosted.org/packages/5f/d7/1b3e26fffde1452d82f5666164858a81c26ebe808e7ae8c9c88628981540/librt-0.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f29b68cd9714531672db62cc54f6e8ff981900f824d13fa0e00749189e13778e", size = 68367, upload-time = "2026-04-09T16:05:17.243Z" }, + { url = "https://files.pythonhosted.org/packages/a5/5b/c61b043ad2e091fbe1f2d35d14795e545d0b56b03edaa390fa1dcee3d160/librt-0.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d5c8a5929ac325729f6119802070b561f4db793dffc45e9ac750992a4ed4d22", size = 70595, upload-time = "2026-04-09T16:05:18.471Z" }, + { url = "https://files.pythonhosted.org/packages/a3/22/2448471196d8a73370aa2f23445455dc42712c21404081fcd7a03b9e0749/librt-0.9.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:756775d25ec8345b837ab52effee3ad2f3b2dfd6bbee3e3f029c517bd5d8f05a", size = 204354, upload-time = "2026-04-09T16:05:19.593Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5e/39fc4b153c78cfd2c8a2dcb32700f2d41d2312aa1050513183be4540930d/librt-0.9.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b8f5d00b49818f4e2b1667db994488b045835e0ac16fe2f924f3871bd2b8ac5", size = 216238, upload-time = "2026-04-09T16:05:20.868Z" }, + { url = "https://files.pythonhosted.org/packages/d7/42/bc2d02d0fa7badfa63aa8d6dcd8793a9f7ef5a94396801684a51ed8d8287/librt-0.9.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c81aef782380f0f13ead670aae01825eb653b44b046aa0e5ebbb79f76ed4aa11", size = 230589, upload-time = "2026-04-09T16:05:22.305Z" }, + { url = "https://files.pythonhosted.org/packages/c8/7b/e2d95cc513866373692aa5edf98080d5602dd07cabfb9e5d2f70df2f25f7/librt-0.9.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66b58fed90a545328e80d575467244de3741e088c1af928f0b489ebec3ef3858", size = 224610, upload-time = "2026-04-09T16:05:23.647Z" }, + { url = "https://files.pythonhosted.org/packages/31/d5/6cec4607e998eaba57564d06a1295c21b0a0c8de76e4e74d699e627bd98c/librt-0.9.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e78fb7419e07d98c2af4b8567b72b3eaf8cb05caad642e9963465569c8b2d87e", size = 232558, upload-time = "2026-04-09T16:05:25.025Z" }, + { url = "https://files.pythonhosted.org/packages/95/8c/27f1d8d3aaf079d3eb26439bf0b32f1482340c3552e324f7db9dca858671/librt-0.9.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c3786f0f4490a5cd87f1ed6cefae833ad6b1060d52044ce0434a2e85893afd0", size = 225521, upload-time = "2026-04-09T16:05:26.311Z" }, + { url = "https://files.pythonhosted.org/packages/6b/d8/1e0d43b1c329b416017619469b3c3801a25a6a4ef4a1c68332aeaa6f72ca/librt-0.9.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8494cfc61e03542f2d381e71804990b3931175a29b9278fdb4a5459948778dc2", size = 227789, upload-time = "2026-04-09T16:05:27.624Z" }, + { url = "https://files.pythonhosted.org/packages/2c/b4/d3d842e88610fcd4c8eec7067b0c23ef2d7d3bff31496eded6a83b0f99be/librt-0.9.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:07cf11f769831186eeac424376e6189f20ace4f7263e2134bdb9757340d84d4d", size = 248616, upload-time = "2026-04-09T16:05:29.181Z" }, + { url = "https://files.pythonhosted.org/packages/ec/28/527df8ad0d1eb6c8bdfa82fc190f1f7c4cca5a1b6d7b36aeabf95b52d74d/librt-0.9.0-cp313-cp313-win32.whl", hash = "sha256:850d6d03177e52700af605fd60db7f37dcb89782049a149674d1a9649c2138fd", size = 56039, upload-time = "2026-04-09T16:05:30.709Z" }, + { url = "https://files.pythonhosted.org/packages/f3/a7/413652ad0d92273ee5e30c000fc494b361171177c83e57c060ecd3c21538/librt-0.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:a5af136bfba820d592f86c67affcef9b3ff4d4360ac3255e341e964489b48519", size = 63264, upload-time = "2026-04-09T16:05:31.881Z" }, + { url = "https://files.pythonhosted.org/packages/a4/0a/92c244309b774e290ddb15e93363846ae7aa753d9586b8aad511c5e6145b/librt-0.9.0-cp313-cp313-win_arm64.whl", hash = "sha256:4c4d0440a3a8e31d962340c3e1cc3fc9ee7febd34c8d8f770d06adb947779ea5", size = 53728, upload-time = "2026-04-09T16:05:33.31Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c1/184e539543f06ea2912f4b92a5ffaede4f9b392689e3f00acbf8134bee92/librt-0.9.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:3f05d145df35dca5056a8bc3838e940efebd893a54b3e19b2dda39ceaa299bcb", size = 67830, upload-time = "2026-04-09T16:05:34.517Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ad/23399bdcb7afca819acacdef31b37ee59de261bd66b503a7995c03c4b0dc/librt-0.9.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1c587494461ebd42229d0f1739f3aa34237dd9980623ecf1be8d3bcba79f4499", size = 70280, upload-time = "2026-04-09T16:05:35.649Z" }, + { url = "https://files.pythonhosted.org/packages/9f/0b/4542dc5a2b8772dbf92cafb9194701230157e73c14b017b6961a23598b03/librt-0.9.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b0a2040f801406b93657a70b72fa12311063a319fee72ce98e1524da7200171f", size = 201925, upload-time = "2026-04-09T16:05:36.739Z" }, + { url = "https://files.pythonhosted.org/packages/31/d4/8ee7358b08fd0cfce051ef96695380f09b3c2c11b77c9bfbc367c921cce5/librt-0.9.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f38bc489037eca88d6ebefc9c4d41a4e07c8e8b4de5188a9e6d290273ad7ebb1", size = 212381, upload-time = "2026-04-09T16:05:38.043Z" }, + { url = "https://files.pythonhosted.org/packages/f2/94/a2025fe442abedf8b038038dab3dba942009ad42b38ea064a1a9e6094241/librt-0.9.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3fd278f5e6bf7c75ccd6d12344eb686cc020712683363b66f46ac79d37c799f", size = 227065, upload-time = "2026-04-09T16:05:39.394Z" }, + { url = "https://files.pythonhosted.org/packages/7c/e9/b9fcf6afa909f957cfbbf918802f9dada1bd5d3c1da43d722fd6a310dc3f/librt-0.9.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fcbdf2a9ca24e87bbebb47f1fe34e531ef06f104f98c9ccfc953a3f3344c567a", size = 221333, upload-time = "2026-04-09T16:05:40.999Z" }, + { url = "https://files.pythonhosted.org/packages/ac/7c/ba54cd6aa6a3c8cd12757a6870e0c79a64b1e6327f5248dcff98423f4d43/librt-0.9.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e306d956cfa027fe041585f02a1602c32bfa6bb8ebea4899d373383295a6c62f", size = 229051, upload-time = "2026-04-09T16:05:42.605Z" }, + { url = "https://files.pythonhosted.org/packages/4b/4b/8cfdbad314c8677a0148bf0b70591d6d18587f9884d930276098a235461b/librt-0.9.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:465814ab157986acb9dfa5ccd7df944be5eefc0d08d31ec6e8d88bc71251d845", size = 222492, upload-time = "2026-04-09T16:05:43.842Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d1/2eda69563a1a88706808decdce035e4b32755dbfbb0d05e1a65db9547ed1/librt-0.9.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:703f4ae36d6240bfe24f542bac784c7e4194ec49c3ba5a994d02891649e2d85b", size = 223849, upload-time = "2026-04-09T16:05:45.054Z" }, + { url = "https://files.pythonhosted.org/packages/04/44/b2ed37df6be5b3d42cfe36318e0598e80843d5c6308dd63d0bf4e0ce5028/librt-0.9.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3be322a15ee5e70b93b7a59cfd074614f22cc8c9ff18bd27f474e79137ea8d3b", size = 245001, upload-time = "2026-04-09T16:05:46.34Z" }, + { url = "https://files.pythonhosted.org/packages/47/e7/617e412426df89169dd2a9ed0cc8752d5763336252c65dbf945199915119/librt-0.9.0-cp314-cp314-win32.whl", hash = "sha256:b8da9f8035bb417770b1e1610526d87ad4fc58a2804dc4d79c53f6d2cf5a6eb9", size = 51799, upload-time = "2026-04-09T16:05:47.738Z" }, + { url = "https://files.pythonhosted.org/packages/24/ed/c22ca4db0ca3cbc285e4d9206108746beda561a9792289c3c31281d7e9df/librt-0.9.0-cp314-cp314-win_amd64.whl", hash = "sha256:b8bd70d5d816566a580d193326912f4a76ec2d28a97dc4cd4cc831c0af8e330e", size = 59165, upload-time = "2026-04-09T16:05:49.198Z" }, + { url = "https://files.pythonhosted.org/packages/24/56/875398fafa4cbc8f15b89366fc3287304ddd3314d861f182a4b87595ace0/librt-0.9.0-cp314-cp314-win_arm64.whl", hash = "sha256:fc5758e2b7a56532dc33e3c544d78cbaa9ecf0a0f2a2da2df882c1d6b99a317f", size = 49292, upload-time = "2026-04-09T16:05:50.362Z" }, + { url = "https://files.pythonhosted.org/packages/4c/61/bc448ecbf9b2d69c5cff88fe41496b19ab2a1cbda0065e47d4d0d51c0867/librt-0.9.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:f24b90b0e0c8cc9491fb1693ae91fe17cb7963153a1946395acdbdd5818429a4", size = 70175, upload-time = "2026-04-09T16:05:51.564Z" }, + { url = "https://files.pythonhosted.org/packages/60/f2/c47bb71069a73e2f04e70acbd196c1e5cc411578ac99039a224b98920fd4/librt-0.9.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3fe56e80badb66fdcde06bef81bbaa5bfcf6fbd7aefb86222d9e369c38c6b228", size = 72951, upload-time = "2026-04-09T16:05:52.699Z" }, + { url = "https://files.pythonhosted.org/packages/29/19/0549df59060631732df758e8886d92088da5fdbedb35b80e4643664e8412/librt-0.9.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:527b5b820b47a09e09829051452bb0d1dd2122261254e2a6f674d12f1d793d54", size = 225864, upload-time = "2026-04-09T16:05:53.895Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f8/3b144396d302ac08e50f89e64452c38db84bc7b23f6c60479c5d3abd303c/librt-0.9.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d429bdd4ac0ab17c8e4a8af0ed2a7440b16eba474909ab357131018fe8c7e71", size = 241155, upload-time = "2026-04-09T16:05:55.191Z" }, + { url = "https://files.pythonhosted.org/packages/7a/ce/ee67ec14581de4043e61d05786d2aed6c9b5338816b7859bcf07455c6a9f/librt-0.9.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7202bdcac47d3a708271c4304a474a8605a4a9a4a709e954bf2d3241140aa938", size = 252235, upload-time = "2026-04-09T16:05:56.549Z" }, + { url = "https://files.pythonhosted.org/packages/8a/fa/0ead15daa2b293a54101550b08d4bafe387b7d4a9fc6d2b985602bae69b6/librt-0.9.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0d620e74897f8c2613b3c4e2e9c1e422eb46d2ddd07df540784d44117836af3", size = 244963, upload-time = "2026-04-09T16:05:57.858Z" }, + { url = "https://files.pythonhosted.org/packages/29/68/9fbf9a9aa704ba87689e40017e720aced8d9a4d2b46b82451d8142f91ec9/librt-0.9.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d69fc39e627908f4c03297d5a88d9284b73f4d90b424461e32e8c2485e21c283", size = 257364, upload-time = "2026-04-09T16:05:59.686Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8d/9d60869f1b6716c762e45f66ed945b1e5dd649f7377684c3b176ae424648/librt-0.9.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:c2640e23d2b7c98796f123ffd95cf2022c7777aa8a4a3b98b36c570d37e85eee", size = 247661, upload-time = "2026-04-09T16:06:00.938Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/a5c365093962310bfdb4f6af256f191085078ffb529b3f0cbebb5b33ebe2/librt-0.9.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:451daa98463b7695b0a30aa56bf637831ea559e7b8101ac2ef6382e8eb15e29c", size = 248238, upload-time = "2026-04-09T16:06:02.537Z" }, + { url = "https://files.pythonhosted.org/packages/a0/3c/2d34365177f412c9e19c0a29f969d70f5343f27634b76b765a54d8b27705/librt-0.9.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:928bd06eca2c2bbf4349e5b817f837509b0604342e65a502de1d50a7570afd15", size = 269457, upload-time = "2026-04-09T16:06:03.833Z" }, + { url = "https://files.pythonhosted.org/packages/bc/cd/de45b239ea3bdf626f982a00c14bfcf2e12d261c510ba7db62c5969a27cd/librt-0.9.0-cp314-cp314t-win32.whl", hash = "sha256:a9c63e04d003bc0fb6a03b348018b9a3002f98268200e22cc80f146beac5dc40", size = 52453, upload-time = "2026-04-09T16:06:05.229Z" }, + { url = "https://files.pythonhosted.org/packages/7f/f9/bfb32ae428aa75c0c533915622176f0a17d6da7b72b5a3c6363685914f70/librt-0.9.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f162af66a2ed3f7d1d161a82ca584efd15acd9c1cff190a373458c32f7d42118", size = 60044, upload-time = "2026-04-09T16:06:06.398Z" }, + { url = "https://files.pythonhosted.org/packages/aa/47/7d70414bcdbb3bc1f458a8d10558f00bbfdb24e5a11740fc8197e12c3255/librt-0.9.0-cp314-cp314t-win_arm64.whl", hash = "sha256:a4b25c6c25cac5d0d9d6d6da855195b254e0021e513e0249f0e3b444dc6e0e61", size = 50009, upload-time = "2026-04-09T16:06:07.995Z" }, ] [[package]] name = "magika" -version = "1.0.3" +version = "1.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "onnxruntime", version = "1.23.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "onnxruntime", version = "1.25.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/00/be/fa7d512ec15763ad7d8ba083e39bb445fa969ce5159361fa449a02569cf9/magika-1.0.3.tar.gz", hash = "sha256:ad3216012f6dd337be34c23ae3dfab36f0623bc62fbfb329f9a1852c9ad40304", size = 3041970, upload-time = "2026-05-04T08:41:53.124Z" } +sdist = { url = "https://files.pythonhosted.org/packages/79/ca/dfb30534be5ad84363e0e8ce08bc6e990ce0430aec1eaafb0633b4bb3f7f/magika-1.0.2.tar.gz", hash = "sha256:8ed912d8f14d044f43fdbd17d6bd2cbdd6e8b8246e89be49f6cd547053636677", size = 3041955, upload-time = "2026-02-25T16:07:03.805Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/93/eb/24d94db0530029649b266ec3ca8221c07f2754f56046181f13237d2518f5/magika-1.0.3-py3-none-any.whl", hash = "sha256:938d8e033953f2ddeb8c35dc423aa289ca116bfa7a71a778f6e77460f9025803", size = 2969548, upload-time = "2026-05-04T08:41:45.071Z" }, - { url = "https://files.pythonhosted.org/packages/23/be/9d7c34b53ff1da4f43224c109eb1f8bcb9ac335cc948d2f5359ba7f788f3/magika-1.0.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:de9af9c96892e610eefc920cca9e71179661d39d9d1d7acc1f28f357bcc8805e", size = 13863615, upload-time = "2026-05-04T08:41:46.734Z" }, - { url = "https://files.pythonhosted.org/packages/d6/78/4f341c974130a464f08813d54113ed9e63d5e9c963fbe398300838922a67/magika-1.0.3-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:3e9b49134e8116ee40b431664dcbed9e199413efddcce1a173c734b4e0521529", size = 16244594, upload-time = "2026-05-04T08:41:48.763Z" }, - { url = "https://files.pythonhosted.org/packages/62/ae/d414195bab93835ce764cb7471e7bf94e0eed396e455e1ff14de7f17d2de/magika-1.0.3-py3-none-win_amd64.whl", hash = "sha256:8a817588c177e81efadf2efa3690bfd46000279fd32e5bfc814776b007ee5d89", size = 13057718, upload-time = "2026-05-04T08:41:50.884Z" }, + { url = "https://files.pythonhosted.org/packages/12/46/b8180a34c64470e2f40a3676ef3284a32efd2b3598aa99946ee319eb66e8/magika-1.0.2-py3-none-any.whl", hash = "sha256:c50be7a6a7132ef1a92956694401aaf911bda8fc5e2a591092e0dac5b5865a8a", size = 2969547, upload-time = "2026-02-25T16:06:55.987Z" }, + { url = "https://files.pythonhosted.org/packages/38/f3/a65650c36a472fed1ca1c4868e567cf015c14c73a6bb5fa4a808932e0944/magika-1.0.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1db8e2d57556e7244f5fce9cfd023aa0da05d204ea7313f3c75b32feab2bcd6d", size = 13811935, upload-time = "2026-02-25T16:06:57.589Z" }, + { url = "https://files.pythonhosted.org/packages/ba/9e/429608833917b7d4c4f7071a270bbca96821fb592e275d85bc9eae5a94c8/magika-1.0.2-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:df4706c18153431548b1d36c8ca11c8a8a415197dcc741281846c61ebfc94a5b", size = 15924817, upload-time = "2026-02-25T16:06:59.765Z" }, + { url = "https://files.pythonhosted.org/packages/1a/12/185a8822994a2f7b5e7d88d19a88d80637917bbb0a6f3f59a2564aabc125/magika-1.0.2-py3-none-win_amd64.whl", hash = "sha256:4937e876d55642423d6416e5db4e5ca7523ab7f855cbc5389efdeac1d149df04", size = 13099543, upload-time = "2026-02-25T16:07:01.942Z" }, ] [[package]] @@ -2095,7 +2098,7 @@ wheels = [ [[package]] name = "openai" -version = "2.34.0" +version = "2.32.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -2107,9 +2110,9 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7b/89/f1e78f5f828f4e97a6ebca8f45c6b35667da12b074ac490dc8362b882279/openai-2.34.0.tar.gz", hash = "sha256:828b4efcbb126352c2b5eb97d33ae890c92a71ab72511aefc1b7fe64aeccb07b", size = 759556, upload-time = "2026-05-04T17:34:08.721Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/59/bdcc6b759b8c42dd73afaf5bf8f902c04b37987a5514dbc1c64dba390fef/openai-2.32.0.tar.gz", hash = "sha256:c54b27a9e4cb8d51f0dd94972ffd1a04437efeb259a9e60d8922b8bd26fe55e0", size = 693286, upload-time = "2026-04-15T22:28:19.434Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f2/40/f090499f10514515081d09cb9da09f25b821eb20497e9423afe4f07b4ecf/openai-2.34.0-py3-none-any.whl", hash = "sha256:c996a71b1a210f3569844572ad4c609307e978515fb76877cf449b72596e549e", size = 1316535, upload-time = "2026-05-04T17:34:06.773Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c1/d6e64ccd0536bf616556f0cad2b6d94a8125f508d25cfd814b1d2db4e2f1/openai-2.32.0-py3-none-any.whl", hash = "sha256:4dcc9badeb4bf54ad0d187453742f290226d30150890b7890711bda4f32f192f", size = 1162570, upload-time = "2026-04-15T22:28:17.714Z" }, ] [[package]] @@ -2616,15 +2619,15 @@ wheels = [ [[package]] name = "python-discovery" -version = "1.3.0" +version = "1.2.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, { name = "platformdirs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ae/e0/cc5a8653e9a24f6cf84768f05064aa8ed5a83dcefd5e2a043db14a1c5f44/python_discovery-1.3.0.tar.gz", hash = "sha256:d098f1e86be5d45fe4d14bf1029294aabbd332f4321179dec85e76cddce834b0", size = 63925, upload-time = "2026-05-05T14:38:39.769Z" } +sdist = { url = "https://files.pythonhosted.org/packages/de/ef/3bae0e537cfe91e8431efcba4434463d2c5a65f5a89edd47c6cf2f03c55f/python_discovery-1.2.2.tar.gz", hash = "sha256:876e9c57139eb757cb5878cbdd9ae5379e5d96266c99ef731119e04fffe533bb", size = 58872, upload-time = "2026-04-07T17:28:49.249Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl", hash = "sha256:441d9ced3dfce36e113beb35ca302c71c7ef06f3c0f9c227a0b9bb3bd49b9e9f", size = 33124, upload-time = "2026-05-05T14:38:38.539Z" }, + { url = "https://files.pythonhosted.org/packages/d8/db/795879cc3ddfe338599bddea6388cc5100b088db0a4caf6e6c1af1c27e04/python_discovery-1.2.2-py3-none-any.whl", hash = "sha256:e1ae95d9af875e78f15e19aed0c6137ab1bb49c200f21f5061786490c9585c7a", size = 31894, upload-time = "2026-04-07T17:28:48.09Z" }, ] [[package]] @@ -3574,34 +3577,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/59/8c/b1c87148aa15e099243ec9f0cf9d0e970cc2234c3257d558c25a2c5304e6/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f01a9c019878532f98927d2bacb79bbb404b43d3437455522a00a30718cdedb5", size = 3373542, upload-time = "2026-01-05T10:40:52.803Z" }, ] -[[package]] -name = "tokie" -version = "0.0.9" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/95/3c/f76865a207ca5fdac17842ddcd5ca4b27699c81cb8424b3290d9b3603970/tokie-0.0.9.tar.gz", hash = "sha256:b2439850258890d7ecf1e21aeeebe06d8ae81879b8349959438da2841fe6553c", size = 135349, upload-time = "2026-05-05T18:21:49.305Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ba/bf/095b8255125fb4fcdc10a3668fb584e627c0993e861ecf413c5b559a8520/tokie-0.0.9-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:e4e3b24ccfdb9e41099d4f94a8b8ef373387b8ef2ecd1371260fdec52b7d0984", size = 2553208, upload-time = "2026-05-05T18:21:18.335Z" }, - { url = "https://files.pythonhosted.org/packages/80/cf/eb30ba2187a27dd36b508a52e9e074acb38af5acbf67ef56ee754a201309/tokie-0.0.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0eee574009dc82a49a6cbb3dc4e28001b8d5c1f1360782f4886108503e8b59d9", size = 2466583, upload-time = "2026-05-05T18:21:20.305Z" }, - { url = "https://files.pythonhosted.org/packages/34/ca/8a536fe6469cdba42238ea0e1d32e0208dfb0de1c2b7f3537e55e8938ddb/tokie-0.0.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:004382c9fdc82bee94423fae26d0e6623c16def5d37911fbf29c066e91688a67", size = 2769179, upload-time = "2026-05-05T18:21:21.983Z" }, - { url = "https://files.pythonhosted.org/packages/59/77/48e2241f6b3b82e6ce13a76de9f319623dfdd81385716396990cd0376112/tokie-0.0.9-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:04d73a972aaac4fcd2caa1ffcdedc7814ce6ae4f01e218398d45178ca0135f50", size = 2732479, upload-time = "2026-05-05T18:21:23.368Z" }, - { url = "https://files.pythonhosted.org/packages/bc/d2/2546912e5dc8c5405b44baf7fbaf617abec87bbe2f5503149cf3cb87dcc0/tokie-0.0.9-cp310-cp310-win_amd64.whl", hash = "sha256:92fcc91d5013a6e8d048545f0f4433fac88d9b6de61f5656a95167ec2dd6de39", size = 2309986, upload-time = "2026-05-05T18:21:24.596Z" }, - { url = "https://files.pythonhosted.org/packages/a5/c5/705ffd2f2dacfc78340660c40cad1561fd36f02cabf9e545090b23577a91/tokie-0.0.9-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:15f3f413921d59f70ea605c93279ca578a4c49f058824aadfb57a756cf912a6b", size = 2553383, upload-time = "2026-05-05T18:21:26.489Z" }, - { url = "https://files.pythonhosted.org/packages/5f/8b/fc17d1bce38325c7b10e6c8c19cceea24c6446adba7b2cad74d2fe9fd1bf/tokie-0.0.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:abb5332d2b93843fa3c59383a1c3436efb2681d09bda55f24a16c5fdfa7cce99", size = 2466408, upload-time = "2026-05-05T18:21:28.263Z" }, - { url = "https://files.pythonhosted.org/packages/47/1a/003d601df91fabf454d108c5068272964deb7fe8f592a4577d4ed1fe9658/tokie-0.0.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2be6b136796ae8066a6ef666c4b2f8bd29729353c30388f236a2c1950ea7bfc7", size = 2769237, upload-time = "2026-05-05T18:21:29.585Z" }, - { url = "https://files.pythonhosted.org/packages/52/e7/277beba1adbdeebbd332e105ebdaebb42bdbef5270fcaa1cbc5077b2efd5/tokie-0.0.9-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:5fc3ca15b34ff0c08a775c2d3a62d3bab7b753a05c241a6e7065785b9afd679e", size = 2732680, upload-time = "2026-05-05T18:21:31.095Z" }, - { url = "https://files.pythonhosted.org/packages/f2/24/4ee4e883ee8686819fbfd7a6c129a5071dbbdd50944ba670dfe872c4b9e1/tokie-0.0.9-cp311-cp311-win_amd64.whl", hash = "sha256:1e6f46a5b1abd136a52edc5078c2dedafb65c16cd38dadd1908259e11d2530ea", size = 2310041, upload-time = "2026-05-05T18:21:32.722Z" }, - { url = "https://files.pythonhosted.org/packages/2e/29/6cd8e4954c6b014a839447289150ee60c7d0d706681fffe97076c28943c4/tokie-0.0.9-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ee7ad68173f3b874ba967df9cbe20203461df9a60624076595320daeab53c269", size = 2549471, upload-time = "2026-05-05T18:21:34.038Z" }, - { url = "https://files.pythonhosted.org/packages/8d/3d/9d1a64d72a6de089fda06eb0728c911ce63444d113195d0c425d9170abe3/tokie-0.0.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a725368781e9b008de7347f56a6f24cb4a6d4df5705f328d764f70a55b9c74ff", size = 2463192, upload-time = "2026-05-05T18:21:35.606Z" }, - { url = "https://files.pythonhosted.org/packages/53/8b/2dbef40651425d212e01ff611eaba8c5e8308b4647fc57f184f6944411b9/tokie-0.0.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ab65bdbb40ffa0f54858779418fcd440b79ebfe2c13f18abb7a1b767ca94ccc", size = 2768954, upload-time = "2026-05-05T18:21:37.174Z" }, - { url = "https://files.pythonhosted.org/packages/09/1d/c14ae0bc273cd47c06184a2c4bd7fb330023058f9e7b54545bbf092b0ce5/tokie-0.0.9-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:d46323296393e91c65a4ec61b688d5b070e33607bb6926523f8c7a16e576713a", size = 2731647, upload-time = "2026-05-05T18:21:38.472Z" }, - { url = "https://files.pythonhosted.org/packages/51/10/baa5bdcdec0a47b8db4f7100ff41e2eacb011c5b817278f7ae516bac0c42/tokie-0.0.9-cp312-cp312-win_amd64.whl", hash = "sha256:c5f6cffcabd2c3838bc39ba08b834a5dfea1b5f73f223dcfadc7009bb5390d53", size = 2309825, upload-time = "2026-05-05T18:21:39.962Z" }, - { url = "https://files.pythonhosted.org/packages/82/b1/34b25481911b615d153908caac1ec176069a260b6ef641c3a5bd27d52603/tokie-0.0.9-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0790332b1cd8e038139c4b0e5b3b3ef7f967ebef72c4a8cf3c3d74610b1aa0ff", size = 2549416, upload-time = "2026-05-05T18:21:41.617Z" }, - { url = "https://files.pythonhosted.org/packages/c6/04/3e54aa54d341118667d57219817108bf067841cde8ba39222d096eae8db4/tokie-0.0.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:878231bfa22c466fb011146fdd03d347a733faae69fef0a0b697ac03a8f47628", size = 2463438, upload-time = "2026-05-05T18:21:43.332Z" }, - { url = "https://files.pythonhosted.org/packages/c6/ad/2c8f7f47c41356400dbb4e37bdf05297feea49fdaa732877973f9f3f7557/tokie-0.0.9-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00bfef25ec782a729aeb2db9fa54e57ed492f0898662f123ba89c3b24614985a", size = 2768664, upload-time = "2026-05-05T18:21:45.179Z" }, - { url = "https://files.pythonhosted.org/packages/fb/a7/6c3e8af745fe2f963b15f4f7049138e018d98e9814c8907550ee616b1ef9/tokie-0.0.9-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:7dfb5d6023585ff18b0f1e2d00e4cd37f872c0055c3d6f3b7b8bf550647c7b7b", size = 2731266, upload-time = "2026-05-05T18:21:46.655Z" }, - { url = "https://files.pythonhosted.org/packages/d0/96/85580cd16e5e97486ad53e8227b3acb10327d68bb8b38a64b405719b5601/tokie-0.0.9-cp313-cp313-win_amd64.whl", hash = "sha256:559d16d6f6898aa1cea5a8be329e5f1654c889d0f25d9652d98c3f88ccf7aca9", size = 2309372, upload-time = "2026-05-05T18:21:48.022Z" }, -] - [[package]] name = "tomli" version = "2.4.1" @@ -3743,7 +3718,7 @@ wheels = [ [[package]] name = "transformers" -version = "5.8.0" +version = "5.6.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "huggingface-hub" }, @@ -3757,9 +3732,9 @@ dependencies = [ { name = "tqdm" }, { name = "typer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f2/36/390075693b76d4fb4a2bea360fb6080347763bd1f1147c49ed0ed938778c/transformers-5.8.0.tar.gz", hash = "sha256:6cc9a1f0291d16b1c1b735bad775e78ebefff7722701d4e28f98aaaa2bd6fb91", size = 8528141, upload-time = "2026-05-05T16:50:04.778Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a4/e9/c6c80a07690142a7d05444271f47b9f3c8aac7dea01d52e1137ee480ad78/transformers-5.6.2.tar.gz", hash = "sha256:e657134c3e5a6bc00a3c35f4e2674bb51adfcd89898495b788a18552bac2b91a", size = 8311867, upload-time = "2026-04-23T18:33:29.332Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/97/7b/5621d08b34ac35deb9fa14b58d27d124d21ef125ee1c64bc724ca47dfb63/transformers-5.8.0-py3-none-any.whl", hash = "sha256:e9d2cae6d195a7e1e05164c5ebf26142a7044e4dc4267274f4809204f92827e4", size = 10630279, upload-time = "2026-05-05T16:50:01.026Z" }, + { url = "https://files.pythonhosted.org/packages/5d/95/0b0218149b0d6f14df35f5b8f676fa83df4f19ed253c3cc447107ef86eca/transformers-5.6.2-py3-none-any.whl", hash = "sha256:f8d3a1bb96778fed9b8aabfd0dd6e19843e4b0f2bb6b59f32b8a92051b0f348f", size = 10364898, upload-time = "2026-04-23T18:33:26.081Z" }, ] [[package]] @@ -3835,7 +3810,7 @@ wheels = [ [[package]] name = "typer" -version = "0.25.1" +version = "0.25.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-doc" }, @@ -3843,9 +3818,9 @@ dependencies = [ { name = "rich" }, { name = "shellingham" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e4/51/9aed62104cea109b820bbd6c14245af756112017d309da813ef107d42e7e/typer-0.25.1.tar.gz", hash = "sha256:9616eb8853a09ffeabab1698952f33c6f29ffdbceb4eaeecf571880e8d7664cc", size = 122276, upload-time = "2026-04-30T19:32:16.964Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/27/ede8cec7596e0041ba7e7b80b47d132562f56ff454313a16f6084e555c9f/typer-0.25.0.tar.gz", hash = "sha256:123eaf9f19bb40fd268310e12a542c0c6b4fab9c98d9d23342a01ff95e3ce930", size = 120150, upload-time = "2026-04-26T08:46:14.767Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl", hash = "sha256:75caa44ed46a03fb2dab8808753ffacdbfea88495e74c85a28c5eefcf5f39c89", size = 58409, upload-time = "2026-04-30T19:32:18.271Z" }, + { url = "https://files.pythonhosted.org/packages/9a/72/193d4e586ec5a4db834a36bbeb47641a62f951f114ffd0fe5b1b46e8d56f/typer-0.25.0-py3-none-any.whl", hash = "sha256:ac01b48823d3db9a83c9e164338057eadbb1c9957a2a6b4eeb486669c560b5dc", size = 55993, upload-time = "2026-04-26T08:46:15.889Z" }, ] [[package]] @@ -3909,7 +3884,7 @@ wheels = [ [[package]] name = "virtualenv" -version = "21.3.1" +version = "21.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, @@ -3918,9 +3893,9 @@ dependencies = [ { name = "python-discovery" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ec/0d/915c02c94d207b85580eb09bffab54438a709e7288524094fe781da526c2/virtualenv-21.3.1.tar.gz", hash = "sha256:c2305bc1fddeec40699b8370d13f8d431b0701f00ce895061ce493aeded4426b", size = 7613791, upload-time = "2026-05-05T01:34:31.402Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/8b/6331f7a7fe70131c301106ec1e7cf23e2501bf7d4ca3636805801ca191bb/virtualenv-21.3.0.tar.gz", hash = "sha256:733750db978ec95c2d8eb4feadaa57091002bce404cb39ba69899cf7bd28944e", size = 7614069, upload-time = "2026-04-27T17:05:58.927Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/4f/f71e641e504111a5a74e3a20bc52d01bd86788b22699dd3fee1c63253cf6/virtualenv-21.3.1-py3-none-any.whl", hash = "sha256:d1a71cf58f2f9228fff23a1f6ec15d39785c6b32e03658d104974247145edd35", size = 7594539, upload-time = "2026-05-05T01:34:28.98Z" }, + { url = "https://files.pythonhosted.org/packages/4b/eb/03bfb1299d4c4510329e470f13f9a4ce793df7fcb5a2fd3510f911066f61/virtualenv-21.3.0-py3-none-any.whl", hash = "sha256:4d28ee41f6d9ec8f1f00cd472b9ffbcedda1b3d3b9a575b5c94a2d004fd51bd7", size = 7594690, upload-time = "2026-04-27T17:05:55.468Z" }, ] [[package]] From 0b4034acd622e5114a2d4365cb41ebf2415b5034 Mon Sep 17 00:00:00 2001 From: Pringled Date: Thu, 7 May 2026 15:57:15 +0200 Subject: [PATCH 08/17] Update docs --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 805324d32..c309a48ce 100644 --- a/README.md +++ b/README.md @@ -196,8 +196,6 @@ semble savings # summary by period semble savings --verbose # also show breakdown by call type ``` -> Savings are measured against reading matched files in full. True savings versus grep+read are higher still, since grep+read also scans every file that produces no matches. - ``` Semble Token Savings ════════════════════════════════════════════════════════════════ @@ -208,7 +206,9 @@ semble savings --verbose # also show breakdown by call type All time 125 [█████████████░░░] ~575.0k tokens (83%) ``` -**How savings are calculated:** for each call, semble records the total character count of files containing matching chunks and the character count of the snippets actually returned. Tokens saved is `(file chars − snippet chars) / 4`, using the standard 4 characters-per-token approximation. +**How savings are calculated:** savings are estimated against a full-file-read baseline: for each call, semble records the total character count of the unique files containing returned chunks and the character count of the snippets actually returned. Estimated tokens saved is `(file chars − snippet chars) / 4`, using the common 4 characters-per-token approximation. + +This is a local estimate, not a model of every workflow. Grep-only searches or targeted line-range reads may use fewer tokens; broad manual file reads may use more. Stats are stored in `~/.semble/savings.jsonl`. From 1d30641d1b967c1be9ac2da05a003a0d28841acf Mon Sep 17 00:00:00 2001 From: Pringled Date: Thu, 7 May 2026 16:18:51 +0200 Subject: [PATCH 09/17] Update docs --- README.md | 10 ++++------ src/semble/stats.py | 15 +++++++++++---- tests/test_stats.py | 11 ++++++++++- 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index c309a48ce..b7de7024d 100644 --- a/README.md +++ b/README.md @@ -201,14 +201,12 @@ semble savings --verbose # also show breakdown by call type ════════════════════════════════════════════════════════════════ Period Calls Savings ──────────────────────────────────────────────────────────────── - Today 19 [█████████████░░░] ~122.1k tokens (77%) - Last 7 days 125 [█████████████░░░] ~575.0k tokens (83%) - All time 125 [█████████████░░░] ~575.0k tokens (83%) + Today 42 [███████████████░] ~58.4k tokens (95%) + Last 7 days 287 [██████████████░░] ~312.4k tokens (90%) + All time 1.4k [██████████████░░] ~1.2M tokens (89%) ``` -**How savings are calculated:** savings are estimated against a full-file-read baseline: for each call, semble records the total character count of the unique files containing returned chunks and the character count of the snippets actually returned. Estimated tokens saved is `(file chars − snippet chars) / 4`, using the common 4 characters-per-token approximation. - -This is a local estimate, not a model of every workflow. Grep-only searches or targeted line-range reads may use fewer tokens; broad manual file reads may use more. +**How savings are calculated:** for each call, semble records the total character count of the unique files containing returned chunks and the character count of the snippets returned. Estimated tokens saved is `(file chars − snippet chars) / 4` (4 chars per token). This is a conservative estimate: the baseline is reading matched files in full, which is how coding agents often explore unfamiliar code. Stats are stored in `~/.semble/savings.jsonl`. diff --git a/src/semble/stats.py b/src/semble/stats.py index 8cb885f36..ae77ce0f5 100644 --- a/src/semble/stats.py +++ b/src/semble/stats.py @@ -115,19 +115,26 @@ def format_savings_report(path: Path | None = None, *, verbose: bool = False) -> for label, bucket in summary.buckets.items(): saved_chars = max(0, bucket.file_chars - bucket.snippet_chars) saved_tokens = saved_chars // 4 - saved_str = f"~{saved_tokens / 1000:.1f}k" if saved_tokens >= 1000 else f"~{saved_tokens}" + if saved_tokens >= 1_000_000: + saved_str = f"~{saved_tokens / 1_000_000:.1f}M" + elif saved_tokens >= 1000: + saved_str = f"~{saved_tokens / 1000:.1f}k" + else: + saved_str = f"~{saved_tokens}" + calls_str = f"{bucket.calls / 1000:.1f}k" if bucket.calls >= 1000 else str(bucket.calls) if bucket.file_chars > 0: ratio = saved_chars / bucket.file_chars filled = round(ratio * bar_width) bar = "█" * filled + "░" * (bar_width - filled) pct = round(ratio * 100) - lines.append(f" {label:<12} {bucket.calls:<6} [{bar}] {saved_str} tokens ({pct}%)") + lines.append(f" {label:<12} {calls_str:<6} [{bar}] {saved_str} tokens ({pct}%)") else: - lines.append(f" {label:<12} {bucket.calls:<6} [{'░' * bar_width}] {saved_str} tokens") + lines.append(f" {label:<12} {calls_str:<6} [{'░' * bar_width}] {saved_str} tokens") if verbose and summary.call_type_counts: lines += ["", " Usage Breakdown", light_line, f" {'Call type':<16} Calls"] for call_type, count in sorted(summary.call_type_counts.items()): - lines.append(f" {call_type:<16} {count}") + count_str = f"{count / 1000:.1f}k" if count >= 1000 else str(count) + lines.append(f" {call_type:<16} {count_str}") lines.append(heavy_line) lines.append("") return "\n".join(lines) diff --git a/tests/test_stats.py b/tests/test_stats.py index 136e1f1c2..e1703ff13 100644 --- a/tests/test_stats.py +++ b/tests/test_stats.py @@ -12,7 +12,7 @@ from tests.conftest import make_chunk -def _make_stats_record(ts: str, call: str = "search", snippet_chars: int = 100, file_chars: int = 500) -> str: +def _make_stats_record(ts: str, call: str = "search", snippet_chars: int = 1_000, file_chars: int = 20_000) -> str: return json.dumps({"ts": ts, "call": call, "results": 3, "snippet_chars": snippet_chars, "file_chars": file_chars}) @@ -67,6 +67,15 @@ def test_savings_output(sample_stats_file: Path, verbose: bool, expected: list[s assert s in result +def test_savings_output_millions(tmp_path: Path) -> None: + """Token counts >= 1M are formatted as M, not k.""" + stats_file = tmp_path / "stats.jsonl" + stats_file.write_text( + _make_stats_record(datetime.now(timezone.utc).isoformat(), snippet_chars=0, file_chars=4_000_000) + "\n" + ) + assert "M tokens" in format_savings_report(path=stats_file) + + @pytest.mark.parametrize( "bad_line", [ From fdbcd0a4a7d6465cf51bb907871cc1dac4c6cab4 Mon Sep 17 00:00:00 2001 From: Pringled Date: Thu, 7 May 2026 16:22:03 +0200 Subject: [PATCH 10/17] Simplify tests --- tests/test_stats.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/tests/test_stats.py b/tests/test_stats.py index e1703ff13..ac3261159 100644 --- a/tests/test_stats.py +++ b/tests/test_stats.py @@ -27,19 +27,15 @@ def sample_stats_file(tmp_path: Path) -> Path: return stats_file -def test_log_search_stats_deduplicates_file_paths(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: - """Two results from the same file are counted as one file in file_chars.""" +def test_log_search_stats(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """log_search_stats deduplicates file paths and silences write errors.""" chunk = make_chunk("hello", "src/foo.py") result = SearchResult(chunk=chunk, score=0.9, source=SearchMode.HYBRID) stats_file = tmp_path / "stats.jsonl" monkeypatch.setattr("semble.stats._STATS_FILE", stats_file) log_search_stats([result, result], "search", {"src/foo.py": 42}) - record = json.loads(stats_file.read_text()) - assert record["file_chars"] == 42 + assert json.loads(stats_file.read_text())["file_chars"] == 42 - -def test_log_search_stats_silences_write_errors(monkeypatch: pytest.MonkeyPatch) -> None: - """Errors during stat recording are silently swallowed.""" mock_path = MagicMock() mock_path.parent.mkdir.return_value = None mock_path.open.side_effect = OSError("no write") From 7a2b438a849b2ff5028e81b6aeaf2dee4d38a495 Mon Sep 17 00:00:00 2001 From: Pringled Date: Thu, 7 May 2026 16:24:46 +0200 Subject: [PATCH 11/17] Update docs --- src/semble/stats.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/semble/stats.py b/src/semble/stats.py index ae77ce0f5..d30707f09 100644 --- a/src/semble/stats.py +++ b/src/semble/stats.py @@ -83,7 +83,7 @@ def parse_stats(path: Path = _STATS_FILE) -> SavingsSummary: in_today = record_date == today in_last_7 = record_date > seven_days_ago except ValueError: - in_today = in_last_7 = False + in_today = in_last_7 = False # unparseable timestamp: count in All time only buckets["All time"].add(snippet_chars, file_chars) if in_last_7: buckets["Last 7 days"].add(snippet_chars, file_chars) @@ -114,7 +114,7 @@ def format_savings_report(path: Path | None = None, *, verbose: bool = False) -> ] for label, bucket in summary.buckets.items(): saved_chars = max(0, bucket.file_chars - bucket.snippet_chars) - saved_tokens = saved_chars // 4 + saved_tokens = saved_chars // 4 # standard ~4 chars/token approximation if saved_tokens >= 1_000_000: saved_str = f"~{saved_tokens / 1_000_000:.1f}M" elif saved_tokens >= 1000: From 661dbbcff41d7490b2ca8417258a6baa33055556 Mon Sep 17 00:00:00 2001 From: Pringled Date: Thu, 7 May 2026 17:56:49 +0200 Subject: [PATCH 12/17] Rename functions and add CallType type --- src/semble/index/index.py | 8 ++++---- src/semble/stats.py | 18 ++++++++++++------ src/semble/types.py | 7 +++++++ tests/test_stats.py | 14 +++++++------- 4 files changed, 30 insertions(+), 17 deletions(-) diff --git a/src/semble/index/index.py b/src/semble/index/index.py index 683dddb7f..ffbe0a2ab 100644 --- a/src/semble/index/index.py +++ b/src/semble/index/index.py @@ -13,8 +13,8 @@ from semble.index.create import create_index_from_path from semble.index.dense import SelectableBasicBackend, load_model from semble.search import search_bm25, search_hybrid, search_semantic -from semble.stats import log_search_stats -from semble.types import Chunk, Encoder, IndexStats, SearchMode, SearchResult +from semble.stats import save_search_stats +from semble.types import CallType, Chunk, Encoder, IndexStats, SearchMode, SearchResult _GIT_CLONE_TIMEOUT = int(os.environ.get("SEMBLE_CLONE_TIMEOUT", 60)) @@ -190,7 +190,7 @@ def find_related(self, source: Chunk | SearchResult, *, top_k: int = 5) -> list[ selector = self._get_selector_vector(filter_languages=[target.language]) if target.language else None results = search_semantic(target.content, self.model, self._semantic_index, self.chunks, top_k + 1, selector) results = [r for r in results if r.chunk != target][:top_k] - log_search_stats(results, "find_related", self._file_sizes) + save_search_stats(results, CallType.FIND_RELATED, self._file_sizes) return results def _get_selector_vector( @@ -245,5 +245,5 @@ def search( ) else: raise ValueError(f"Unknown search mode: {mode!r}") - log_search_stats(results, "search", self._file_sizes) + save_search_stats(results, CallType.SEARCH, self._file_sizes) return results diff --git a/src/semble/stats.py b/src/semble/stats.py index d30707f09..e27ea1508 100644 --- a/src/semble/stats.py +++ b/src/semble/stats.py @@ -4,7 +4,7 @@ from datetime import datetime, timedelta, timezone from pathlib import Path -from semble.types import SearchResult +from semble.types import CallType, SearchResult _STATS_FILE = Path.home() / ".semble" / "savings.jsonl" @@ -28,12 +28,18 @@ class SavingsSummary: call_type_counts: dict[str, int] -def log_search_stats( +def save_search_stats( results: list[SearchResult], - call_type: str, + call_type: CallType, file_sizes: dict[str, int] | None = None, ) -> None: - """Append token-savings stats for one search/find_related call. Failures are silently ignored.""" + """Save stats about a search or find_related call to the stats file. + + :param results: The search results to summarize. + :param call_type: A CallType indicating the type of call ("search" or "find_related"). + :param file_sizes: Optional mapping of file paths to their character counts, used to calculate file_chars. + If not provided, file_chars will be recorded as 0. + """ try: snippet_chars = sum(len(result.chunk.content) for result in results) if file_sizes: @@ -55,7 +61,7 @@ def log_search_stats( pass -def parse_stats(path: Path = _STATS_FILE) -> SavingsSummary: +def build_savings_summary(path: Path = _STATS_FILE) -> SavingsSummary: """Read savings.jsonl and return a SavingsSummary.""" now = datetime.now(timezone.utc) today = now.date() @@ -100,7 +106,7 @@ def format_savings_report(path: Path | None = None, *, verbose: bool = False) -> if not path.exists(): return "No stats yet. Run a search first." - summary = parse_stats(path) + summary = build_savings_summary(path) bar_width = 16 heavy_line = " " + "═" * 64 light_line = " " + "─" * 64 diff --git a/src/semble/types.py b/src/semble/types.py index 418f06e00..f598ba72d 100644 --- a/src/semble/types.py +++ b/src/semble/types.py @@ -17,6 +17,13 @@ class SearchMode(str, Enum): BM25 = "bm25" +class CallType(str, Enum): + """Call type for token-savings tracking.""" + + SEARCH = "search" + FIND_RELATED = "find_related" + + class Encoder(Protocol): """Protocol for embedding models.""" diff --git a/tests/test_stats.py b/tests/test_stats.py index ac3261159..223744515 100644 --- a/tests/test_stats.py +++ b/tests/test_stats.py @@ -7,8 +7,8 @@ import pytest from semble.cli import _cli_main -from semble.stats import format_savings_report, log_search_stats, parse_stats -from semble.types import SearchMode, SearchResult +from semble.stats import build_savings_summary, format_savings_report, save_search_stats +from semble.types import CallType, SearchMode, SearchResult from tests.conftest import make_chunk @@ -27,20 +27,20 @@ def sample_stats_file(tmp_path: Path) -> Path: return stats_file -def test_log_search_stats(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: - """log_search_stats deduplicates file paths and silences write errors.""" +def test_save_search_stats(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """save_search_stats deduplicates file paths and silences write errors.""" chunk = make_chunk("hello", "src/foo.py") result = SearchResult(chunk=chunk, score=0.9, source=SearchMode.HYBRID) stats_file = tmp_path / "stats.jsonl" monkeypatch.setattr("semble.stats._STATS_FILE", stats_file) - log_search_stats([result, result], "search", {"src/foo.py": 42}) + save_search_stats([result, result], CallType.SEARCH, {"src/foo.py": 42}) assert json.loads(stats_file.read_text())["file_chars"] == 42 mock_path = MagicMock() mock_path.parent.mkdir.return_value = None mock_path.open.side_effect = OSError("no write") monkeypatch.setattr("semble.stats._STATS_FILE", mock_path) - log_search_stats([], "search") # must not raise + save_search_stats([], CallType.SEARCH) # must not raise def test_savings_no_file(tmp_path: Path) -> None: @@ -111,7 +111,7 @@ def test_savings_buckets_exclude_old_records(tmp_path: Path) -> None: old_ts = "2020-01-01T00:00:00+00:00" now_ts = datetime.now(timezone.utc).isoformat() stats_file.write_text(_make_stats_record(old_ts) + "\n" + _make_stats_record(now_ts) + "\n") - summary = parse_stats(path=stats_file) + summary = build_savings_summary(path=stats_file) assert summary.buckets["All time"].calls == 2 assert summary.buckets["Today"].calls == 1 assert summary.buckets["Last 7 days"].calls == 1 From 5b522155038987be0c7ab434b14147718665348a Mon Sep 17 00:00:00 2001 From: Pringled Date: Thu, 7 May 2026 18:04:17 +0200 Subject: [PATCH 13/17] Only record savings when file_sizes are available --- src/semble/index/index.py | 7 +++++-- src/semble/stats.py | 15 +++------------ tests/test_stats.py | 2 +- 3 files changed, 9 insertions(+), 15 deletions(-) diff --git a/src/semble/index/index.py b/src/semble/index/index.py index ffbe0a2ab..3b305154d 100644 --- a/src/semble/index/index.py +++ b/src/semble/index/index.py @@ -190,7 +190,8 @@ def find_related(self, source: Chunk | SearchResult, *, top_k: int = 5) -> list[ selector = self._get_selector_vector(filter_languages=[target.language]) if target.language else None results = search_semantic(target.content, self.model, self._semantic_index, self.chunks, top_k + 1, selector) results = [r for r in results if r.chunk != target][:top_k] - save_search_stats(results, CallType.FIND_RELATED, self._file_sizes) + if self._file_sizes: + save_search_stats(results, CallType.FIND_RELATED, self._file_sizes) return results def _get_selector_vector( @@ -245,5 +246,7 @@ def search( ) else: raise ValueError(f"Unknown search mode: {mode!r}") - save_search_stats(results, CallType.SEARCH, self._file_sizes) + if self._file_sizes: + # Save stats if file sizes are available + save_search_stats(results, CallType.SEARCH, self._file_sizes) return results diff --git a/src/semble/stats.py b/src/semble/stats.py index e27ea1508..915bd4ed8 100644 --- a/src/semble/stats.py +++ b/src/semble/stats.py @@ -31,21 +31,12 @@ class SavingsSummary: def save_search_stats( results: list[SearchResult], call_type: CallType, - file_sizes: dict[str, int] | None = None, + file_sizes: dict[str, int], ) -> None: - """Save stats about a search or find_related call to the stats file. - - :param results: The search results to summarize. - :param call_type: A CallType indicating the type of call ("search" or "find_related"). - :param file_sizes: Optional mapping of file paths to their character counts, used to calculate file_chars. - If not provided, file_chars will be recorded as 0. - """ + """Save token-savings stats for one call. Failures are silently ignored.""" try: snippet_chars = sum(len(result.chunk.content) for result in results) - if file_sizes: - file_chars = sum(file_sizes.get(path, 0) for path in {result.chunk.file_path for result in results}) - else: - file_chars = 0 + file_chars = sum(file_sizes.get(path, 0) for path in {result.chunk.file_path for result in results}) record = { "ts": datetime.now(timezone.utc).isoformat(), diff --git a/tests/test_stats.py b/tests/test_stats.py index 223744515..c229166c6 100644 --- a/tests/test_stats.py +++ b/tests/test_stats.py @@ -40,7 +40,7 @@ def test_save_search_stats(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> N mock_path.parent.mkdir.return_value = None mock_path.open.side_effect = OSError("no write") monkeypatch.setattr("semble.stats._STATS_FILE", mock_path) - save_search_stats([], CallType.SEARCH) # must not raise + save_search_stats([result], CallType.SEARCH, {"src/foo.py": 42}) # must not raise def test_savings_no_file(tmp_path: Path) -> None: From f3d935f7187e23fd4111d074446ba8f8f2240cf0 Mon Sep 17 00:00:00 2001 From: Pringled Date: Thu, 7 May 2026 18:05:01 +0200 Subject: [PATCH 14/17] Update docstring: --- src/semble/index/index.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/semble/index/index.py b/src/semble/index/index.py index 3b305154d..ddafdc5e5 100644 --- a/src/semble/index/index.py +++ b/src/semble/index/index.py @@ -191,6 +191,7 @@ def find_related(self, source: Chunk | SearchResult, *, top_k: int = 5) -> list[ results = search_semantic(target.content, self.model, self._semantic_index, self.chunks, top_k + 1, selector) results = [r for r in results if r.chunk != target][:top_k] if self._file_sizes: + # Save stats if file sizes are available save_search_stats(results, CallType.FIND_RELATED, self._file_sizes) return results From 1737c4c31d2b2217f089d51e5578206306e42331 Mon Sep 17 00:00:00 2001 From: Pringled Date: Thu, 7 May 2026 18:05:41 +0200 Subject: [PATCH 15/17] Update docstring: --- src/semble/stats.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/semble/stats.py b/src/semble/stats.py index 915bd4ed8..dad87f053 100644 --- a/src/semble/stats.py +++ b/src/semble/stats.py @@ -33,7 +33,7 @@ def save_search_stats( call_type: CallType, file_sizes: dict[str, int], ) -> None: - """Save token-savings stats for one call. Failures are silently ignored.""" + """Save stats about a search or find_related call to the stats file.""" try: snippet_chars = sum(len(result.chunk.content) for result in results) file_chars = sum(file_sizes.get(path, 0) for path in {result.chunk.file_path for result in results}) From e5024b7bc91cd9236593a767d7f040715766077c Mon Sep 17 00:00:00 2001 From: Pringled Date: Mon, 11 May 2026 08:28:08 +0200 Subject: [PATCH 16/17] Resolve comments --- src/semble/index/index.py | 33 ++++++++++++--------------------- src/semble/stats.py | 25 ++++++++++++++----------- tests/test_index.py | 5 +++-- tests/test_stats.py | 24 ++++++++---------------- 4 files changed, 37 insertions(+), 50 deletions(-) diff --git a/src/semble/index/index.py b/src/semble/index/index.py index ddafdc5e5..b136e6cfe 100644 --- a/src/semble/index/index.py +++ b/src/semble/index/index.py @@ -28,6 +28,7 @@ def __init__( bm25_index: BM25, semantic_index: SelectableBasicBackend, chunks: list[Chunk], + root: Path | None = None, ) -> None: """Internal constructor — use :meth:`from_path` or :meth:`from_git`. @@ -35,12 +36,14 @@ def __init__( :param bm25_index: The bm25 index. :param semantic_index: The semantic index. :param chunks: The found chunks. + :param root: Root directory used to read file sizes for token-savings stats. """ self.model: Encoder = model self.chunks: list[Chunk] = chunks self._bm25_index: BM25 = bm25_index self._semantic_index: SelectableBasicBackend = semantic_index - self._file_sizes: dict[str, int] = {} + self._root: Path | None = root + self._file_sizes: dict[str, int] = self._compute_file_sizes(root) if root else {} self._file_mapping, self._language_mapping = self._populate_mapping() def _populate_mapping(self) -> tuple[dict[str, list[int]], dict[str, list[int]]]: @@ -55,16 +58,14 @@ def _populate_mapping(self) -> tuple[dict[str, list[int]], dict[str, list[int]]] return dict(file_to_id), dict(language_to_id) - @staticmethod - def _compute_file_sizes(root: Path, chunks: list[Chunk]) -> dict[str, int]: + def _compute_file_sizes(self, root: Path) -> dict[str, int]: """Return a mapping of repo-relative file path to total character count.""" sizes: dict[str, int] = {} - for chunk in chunks: - file_path = chunk.file_path - if file_path in sizes: + for chunk in self.chunks: + if chunk.file_path in sizes: continue try: - sizes[file_path] = len((root / file_path).read_text(encoding="utf-8", errors="replace")) + sizes[chunk.file_path] = len((root / chunk.file_path).read_text(encoding="utf-8", errors="replace")) except OSError: pass return sizes @@ -119,10 +120,7 @@ def from_path( display_root=path, ) - index = SembleIndex(model, bm25, vicinity, chunks) - index._file_sizes = SembleIndex._compute_file_sizes(path, chunks) - - return index + return SembleIndex(model, bm25, vicinity, chunks, root=path) @classmethod def from_git( @@ -174,10 +172,7 @@ def from_git( display_root=resolved_path, ) - index = SembleIndex(model, bm25, vicinity, chunks) - index._file_sizes = SembleIndex._compute_file_sizes(resolved_path, chunks) - - return index + return SembleIndex(model, bm25, vicinity, chunks, root=resolved_path) def find_related(self, source: Chunk | SearchResult, *, top_k: int = 5) -> list[SearchResult]: """Return chunks semantically similar to the given chunk or search result. @@ -190,9 +185,7 @@ def find_related(self, source: Chunk | SearchResult, *, top_k: int = 5) -> list[ selector = self._get_selector_vector(filter_languages=[target.language]) if target.language else None results = search_semantic(target.content, self.model, self._semantic_index, self.chunks, top_k + 1, selector) results = [r for r in results if r.chunk != target][:top_k] - if self._file_sizes: - # Save stats if file sizes are available - save_search_stats(results, CallType.FIND_RELATED, self._file_sizes) + save_search_stats(results, CallType.FIND_RELATED, self._file_sizes) return results def _get_selector_vector( @@ -247,7 +240,5 @@ def search( ) else: raise ValueError(f"Unknown search mode: {mode!r}") - if self._file_sizes: - # Save stats if file sizes are available - save_search_stats(results, CallType.SEARCH, self._file_sizes) + save_search_stats(results, CallType.SEARCH, self._file_sizes) return results diff --git a/src/semble/stats.py b/src/semble/stats.py index dad87f053..9975342c1 100644 --- a/src/semble/stats.py +++ b/src/semble/stats.py @@ -1,4 +1,5 @@ import json +import logging from collections import defaultdict from dataclasses import dataclass from datetime import datetime, timedelta, timezone @@ -6,6 +7,8 @@ from semble.types import CallType, SearchResult +logger = logging.getLogger(__name__) + _STATS_FILE = Path.home() / ".semble" / "savings.jsonl" @@ -36,10 +39,12 @@ def save_search_stats( """Save stats about a search or find_related call to the stats file.""" try: snippet_chars = sum(len(result.chunk.content) for result in results) - file_chars = sum(file_sizes.get(path, 0) for path in {result.chunk.file_path for result in results}) + file_chars = sum( + file_sizes[path] for path in {result.chunk.file_path for result in results} if path in file_sizes + ) record = { - "ts": datetime.now(timezone.utc).isoformat(), + "ts": datetime.now(timezone.utc).timestamp(), "call": call_type, "results": len(results), "snippet_chars": snippet_chars, @@ -70,17 +75,15 @@ def build_savings_summary(path: Path = _STATS_FILE) -> SavingsSummary: try: record = json.loads(line) except json.JSONDecodeError: + logger.warning("Skipping malformed JSON line in stats file") continue - snippet_chars = record.get("snippet_chars", 0) - file_chars = record.get("file_chars", 0) - call_type = record.get("call", "search") + snippet_chars = record["snippet_chars"] + file_chars = record["file_chars"] + call_type = record["call"] call_type_counts[call_type] += 1 - try: - record_date = datetime.fromisoformat(record.get("ts", "")).date() - in_today = record_date == today - in_last_7 = record_date > seven_days_ago - except ValueError: - in_today = in_last_7 = False # unparseable timestamp: count in All time only + dt = datetime.fromtimestamp(record["ts"], tz=timezone.utc) + in_today = dt.date() == today + in_last_7 = dt.date() > seven_days_ago buckets["All time"].add(snippet_chars, file_chars) if in_last_7: buckets["Last 7 days"].add(snippet_chars, file_chars) diff --git a/tests/test_index.py b/tests/test_index.py index 49f24ab88..8e87ddd55 100644 --- a/tests/test_index.py +++ b/tests/test_index.py @@ -102,8 +102,9 @@ def test_compute_file_sizes( """_compute_file_sizes deduplicates paths and silently skips missing files.""" for name, content in disk_files.items(): (tmp_path / name).write_text(content) - chunks = [make_chunk("c", p) for p in chunk_paths] - assert SembleIndex._compute_file_sizes(tmp_path, chunks) == expected + index = SembleIndex.__new__(SembleIndex) + index.chunks = [make_chunk("c", p) for p in chunk_paths] + assert index._compute_file_sizes(tmp_path) == expected def test_find_related(indexed_index: SembleIndex) -> None: diff --git a/tests/test_stats.py b/tests/test_stats.py index c229166c6..831a9f6fe 100644 --- a/tests/test_stats.py +++ b/tests/test_stats.py @@ -12,7 +12,7 @@ from tests.conftest import make_chunk -def _make_stats_record(ts: str, call: str = "search", snippet_chars: int = 1_000, file_chars: int = 20_000) -> str: +def _make_stats_record(ts: float, call: str = "search", snippet_chars: int = 1_000, file_chars: int = 20_000) -> str: return json.dumps({"ts": ts, "call": call, "results": 3, "snippet_chars": snippet_chars, "file_chars": file_chars}) @@ -20,7 +20,7 @@ def _make_stats_record(ts: str, call: str = "search", snippet_chars: int = 1_000 def sample_stats_file(tmp_path: Path) -> Path: """Stats file with one search and one find_related record from today.""" stats_file = tmp_path / "stats.jsonl" - now = datetime.now(timezone.utc).isoformat() + now = datetime.now(timezone.utc).timestamp() stats_file.write_text( _make_stats_record(now, call="search") + "\n" + _make_stats_record(now, call="find_related") + "\n" ) @@ -67,23 +67,15 @@ def test_savings_output_millions(tmp_path: Path) -> None: """Token counts >= 1M are formatted as M, not k.""" stats_file = tmp_path / "stats.jsonl" stats_file.write_text( - _make_stats_record(datetime.now(timezone.utc).isoformat(), snippet_chars=0, file_chars=4_000_000) + "\n" + _make_stats_record(datetime.now(timezone.utc).timestamp(), snippet_chars=0, file_chars=4_000_000) + "\n" ) assert "M tokens" in format_savings_report(path=stats_file) -@pytest.mark.parametrize( - "bad_line", - [ - "not valid json", - json.dumps({"ts": "not-a-date", "call": "search", "snippet_chars": 100, "file_chars": 500}), - ], - ids=["malformed-json", "malformed-timestamp"], -) -def test_savings_tolerates_bad_records(bad_line: str, tmp_path: Path) -> None: - """Bad JSON lines are skipped; records with bad timestamps count only in All time.""" +def test_savings_tolerates_bad_json(tmp_path: Path) -> None: + """Malformed JSON lines are skipped with a warning.""" stats_file = tmp_path / "stats.jsonl" - stats_file.write_text(bad_line + "\n") + stats_file.write_text("not valid json\n") assert "Savings" in format_savings_report(path=stats_file) @@ -108,8 +100,8 @@ def test_savings_cli_dispatch( def test_savings_buckets_exclude_old_records(tmp_path: Path) -> None: """Records older than 7 days count in All time but not Today or Last 7 days.""" stats_file = tmp_path / "stats.jsonl" - old_ts = "2020-01-01T00:00:00+00:00" - now_ts = datetime.now(timezone.utc).isoformat() + old_ts = datetime(2020, 1, 1, tzinfo=timezone.utc).timestamp() + now_ts = datetime.now(timezone.utc).timestamp() stats_file.write_text(_make_stats_record(old_ts) + "\n" + _make_stats_record(now_ts) + "\n") summary = build_savings_summary(path=stats_file) assert summary.buckets["All time"].calls == 2 From d94b8bfa9a9968d1b491930cc0cb10158305b223 Mon Sep 17 00:00:00 2001 From: Pringled Date: Mon, 11 May 2026 08:31:50 +0200 Subject: [PATCH 17/17] Update docstring --- src/semble/index/index.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/semble/index/index.py b/src/semble/index/index.py index b136e6cfe..24c1f36e4 100644 --- a/src/semble/index/index.py +++ b/src/semble/index/index.py @@ -30,7 +30,7 @@ def __init__( chunks: list[Chunk], root: Path | None = None, ) -> None: - """Internal constructor — use :meth:`from_path` or :meth:`from_git`. + """Initialize a SembleIndex. Should be created with from_path or from_git. :param model: Embedding model to use. :param bm25_index: The bm25 index.