From 11241c742c250374ef14eb5bdbe2200f258a746f Mon Sep 17 00:00:00 2001 From: Chris Guidry Date: Sat, 28 Mar 2026 10:56:56 -0400 Subject: [PATCH] Add local semantic vector search for recall MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The DM's recall tool could only do substring and fuzzy word-overlap matching, so "big explosion spell" couldn't find Fireball and "that merchant we met" couldn't find Henrik. This adds a local vector search engine using fastembed (ONNX, ~100MB) and sqlite-vec so recall can handle natural language queries against the full SRD, world entities, player knowledge, and transcripts. The SRD gets embedded once via `storied index srd` / `make search-index`, and that search.db acts as the seed for every world — just a file copy instead of re-embedding 862 files each time. Write-through keeps the index current as the DM establishes entities, marks events, and records player discoveries. Transcript and player knowledge results get age-decayed (3 game-day half-life) so recent conversations rank higher than old ones. The on_empty callback on VectorIndex auto-populates from the SRD seed on first search if the index is missing or was deleted. Also introduced ToolContext to bundle the infrastructure params (world_id, player_id, base_path, campaign_log, entity_index, vector_index) that were threaded through every tool function individually. This cleaned up the tool signatures and removed a bunch of defensive None-checks — all index objects are now required, not optional. Dropped the old ContentResolver.search/fuzzy fallback since vector search subsumes it. ContentResolver still handles direct name→file lookups (find/load) but the keyword search methods are gone. Co-Authored-By: Claude Opus 4.6 (1M context) --- .gitignore | 3 + Makefile | 10 +- pyproject.toml | 3 + src/storied/cli.py | 131 +++++++- src/storied/content.py | 122 +------- src/storied/engine.py | 35 ++- src/storied/mcp_server.py | 44 ++- src/storied/planner.py | 9 +- src/storied/search.py | 387 +++++++++++++++++++++++ src/storied/tools.py | 449 ++++++++------------------- tests/conftest.py | 49 +++ tests/test_character.py | 110 +++---- tests/test_content.py | 33 +- tests/test_entities.py | 250 ++++++--------- tests/test_planner.py | 238 ++++++-------- tests/test_search.py | 348 +++++++++++++++++++++ tests/test_seeder.py | 25 +- uv.lock | 631 ++++++++++++++++++++++++++++++++++++++ 18 files changed, 1999 insertions(+), 878 deletions(-) create mode 100644 src/storied/search.py create mode 100644 tests/conftest.py create mode 100644 tests/test_search.py diff --git a/.gitignore b/.gitignore index 08c871b..6ee64a3 100644 --- a/.gitignore +++ b/.gitignore @@ -60,3 +60,6 @@ worlds/* !worlds/.gitkeep players/* !players/.gitkeep + +# Search indexes (build artifacts) +search.db diff --git a/Makefile b/Makefile index bd3132a..da1d917 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: all srd clean-srd help +.PHONY: all srd search-index clean-srd help PDF := rules/sources/SRD_CC_v5.2.1.pdf MARKDOWN := rules/srd-5.2.1/srd.md @@ -21,9 +21,13 @@ $(SECTIONS_STAMP): $(MARKDOWN) storied srd clean touch $@ +search-index: $(SECTIONS_STAMP) + storied index srd + clean-srd: rm -rf $(SECTIONS_DIR) $(MARKDOWN) help: - @echo "make srd - Build SRD rules (download, convert, split, clean)" - @echo "make clean-srd - Remove generated SRD files" + @echo "make srd - Build SRD rules (download, convert, split, clean)" + @echo "make search-index - Build SRD search index" + @echo "make clean-srd - Remove generated SRD files" diff --git a/pyproject.toml b/pyproject.toml index 5fb6cff..1dbfa29 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,12 +11,15 @@ authors = [ dependencies = [ "argcomplete>=3.0", + "fastembed>=0.4", "httpx>=0.27", "mcp>=1.9", "pymupdf>=1.24", "pymupdf4llm>=0.0.17", + "pysqlite3-binary>=0.5", "pyyaml>=6.0", "rich>=13.0", + "sqlite-vec>=0.1", ] [project.scripts] diff --git a/src/storied/cli.py b/src/storied/cli.py index 542a0d2..2168fd2 100644 --- a/src/storied/cli.py +++ b/src/storied/cli.py @@ -648,6 +648,93 @@ def cmd_tick(args: argparse.Namespace) -> int: return 0 +def cmd_index_srd(args: argparse.Namespace) -> int: + """Build search index for SRD content.""" + from storied.search import VectorIndex + + srd_dir = Path("rules/srd-5.2.1/sections") + if not srd_dir.exists(): + print(f"SRD sections not found at {srd_dir}") + print("Run 'storied srd split' first.") + return 1 + + db_path = Path("rules/srd-5.2.1/search.db") + print(f"Indexing SRD from {srd_dir}...") + index = VectorIndex(db_path) + count = index.reindex_directory(srd_dir, source="srd") + index.close() + print(f"Indexed {count} document chunks into {db_path}") + return 0 + + +def cmd_index_world(args: argparse.Namespace) -> int: + """Build search index for a world (includes SRD via seed copy).""" + from storied.search import VectorIndex + + world_id = args.world or "default" + world_dir = Path(f"worlds/{world_id}") + if not world_dir.exists(): + print(f"World not found at {world_dir}") + return 1 + + db_path = world_dir / "search.db" + srd_seed = Path("rules/srd-5.2.1/search.db") + + # Seed from pre-built SRD index if available + if srd_seed.exists() and not db_path.exists(): + print(f"Seeding from {srd_seed}...") + index = VectorIndex.seed_from(srd_seed, db_path) + stats = index.stats() + print(f" {stats['total_documents']} SRD chunks copied") + else: + index = VectorIndex(db_path) + if not srd_seed.exists(): + print("SRD index not found. Run 'storied index srd' first for faster setup.") + + # Index world content on top + print(f"Indexing world from {world_dir}...") + world_count = index.reindex_directory(world_dir, source="world") + print(f" {world_count} world chunks") + + index.close() + print(f"Index written to {db_path}") + return 0 + + +def cmd_index_status(args: argparse.Namespace) -> int: + """Show search index status.""" + from storied.search import VectorIndex + + world_id = args.world or "default" + db_path = Path(f"worlds/{world_id}/search.db") + if not db_path.exists(): + print(f"No search index at {db_path}") + print(f"Run 'storied index world -w {world_id}' to create one.") + return 1 + + index = VectorIndex(db_path) + stats = index.stats() + index.close() + + print(f"Search index: {db_path}") + print(f"Total documents: {stats['total_documents']}") + for source, count in sorted(stats["by_source"].items()): + print(f" {source}: {count}") + return 0 + + +def cmd_index_rebuild(args: argparse.Namespace) -> int: + """Force full rebuild of a world's search index.""" + world_id = args.world or "default" + db_path = Path(f"worlds/{world_id}/search.db") + if db_path.exists(): + db_path.unlink() + print(f"Removed {db_path}") + + # Delegate to cmd_index_world + return cmd_index_world(args) + + def cmd_seed(args: argparse.Namespace) -> int: """Seed an empty world from a character sheet.""" from storied.planner import seed_world @@ -864,6 +951,41 @@ def build_parser() -> argparse.ArgumentParser: ) seed_parser.set_defaults(func=cmd_seed) + # index command group + index_parser = subparsers.add_parser("index", help="Search index commands") + index_subparsers = index_parser.add_subparsers( + dest="index_command", help="Index subcommands" + ) + + index_srd_parser = index_subparsers.add_parser( + "srd", help="Build search index for SRD content" + ) + index_srd_parser.set_defaults(func=cmd_index_srd) + + index_world_parser = index_subparsers.add_parser( + "world", help="Build search index for a world (includes SRD)" + ) + index_world_parser.add_argument( + "--world", "-w", help="World ID (default: default)" + ) + index_world_parser.set_defaults(func=cmd_index_world) + + index_status_parser = index_subparsers.add_parser( + "status", help="Show search index status" + ) + index_status_parser.add_argument( + "--world", "-w", help="World ID (default: default)" + ) + index_status_parser.set_defaults(func=cmd_index_status) + + index_rebuild_parser = index_subparsers.add_parser( + "rebuild", help="Force full rebuild of search index" + ) + index_rebuild_parser.add_argument( + "--world", "-w", help="World ID (default: default)" + ) + index_rebuild_parser.set_defaults(func=cmd_index_rebuild) + return parser @@ -877,13 +999,12 @@ def main(argv: list[str] | None = None) -> int: parser.print_help() return 0 - if args.command == "srd" and not getattr(args, "srd_command", None): - # Find the srd subparser and print its help + if args.command in ("srd", "index") and not getattr(args, f"{args.command}_command", None): for action in parser._subparsers._actions: if isinstance(action, argparse._SubParsersAction): - srd_parser = action.choices.get("srd") - if srd_parser: - srd_parser.print_help() + sub = action.choices.get(args.command) + if sub: + sub.print_help() return 0 if hasattr(args, "func"): diff --git a/src/storied/content.py b/src/storied/content.py index 4701513..504a5ab 100644 --- a/src/storied/content.py +++ b/src/storied/content.py @@ -1,22 +1,11 @@ -"""Content layer resolution and search.""" +"""Content layer resolution — finds and loads content across world/rules layers.""" import re -from dataclasses import dataclass from pathlib import Path import yaml -@dataclass -class SearchResult: - """A search result from content search.""" - - name: str - path: Path - content_type: str - snippet: str | None = None - - class ContentResolver: """Resolves content across world and rules layers. @@ -124,112 +113,3 @@ class ContentResolver: # No frontmatter, just body return {"body": content.strip()} - def search( - self, query: str, content_type: str | None = None - ) -> list[SearchResult]: - """Search content by keyword with fuzzy fallback. - - First tries exact substring matching on filenames and content. - If nothing matches, falls back to word-overlap scoring. - - Args: - query: Search term - content_type: Optional category to limit search - - Returns: - List of SearchResult objects - """ - results: list[SearchResult] = [] - seen_names: set[str] = set() - query_lower = query.lower() - - for search_dir, ctype in self._search_dirs(content_type): - if not search_dir.exists(): - continue - - for path in search_dir.glob("*.md"): - name = path.stem - - if name in seen_names: - continue - - content = path.read_text() - - if query_lower in name.lower() or query_lower in content.lower(): - snippet = self._extract_snippet(content, query) - results.append( - SearchResult( - name=name, - path=path, - content_type=ctype, - snippet=snippet, - ) - ) - seen_names.add(name) - - # Fuzzy fallback: word-overlap scoring when exact match fails - if not results: - results = self._fuzzy_search(query, content_type) - - return results - - def _fuzzy_search( - self, query: str, content_type: str | None = None - ) -> list[SearchResult]: - """Word-overlap fallback when exact search finds nothing.""" - query_words = {w for w in query.lower().split() if len(w) > 2} - if not query_words: - return [] - - scored: list[tuple[float, SearchResult]] = [] - seen_names: set[str] = set() - - for search_dir, ctype in self._search_dirs(content_type): - if not search_dir.exists(): - continue - - for path in search_dir.glob("*.md"): - name = path.stem - if name in seen_names: - continue - - name_words = set(name.lower().replace("-", " ").split()) - content = path.read_text() - content_lower = content.lower() - - # Score: name matches count double - name_hits = len(query_words & name_words) - content_hits = sum(1 for w in query_words if w in content_lower) - score = name_hits * 2 + content_hits - - if score > 0: - snippet = content[:100].strip() + ("..." if len(content) > 100 else "") - scored.append(( - score, - SearchResult(name=name, path=path, content_type=ctype, snippet=snippet), - )) - seen_names.add(name) - - scored.sort(key=lambda x: x[0], reverse=True) - return [r for _, r in scored[:5]] - - def _extract_snippet(self, content: str, query: str, context: int = 50) -> str: - """Extract a snippet of text around the query match.""" - query_lower = query.lower() - content_lower = content.lower() - - pos = content_lower.find(query_lower) - if pos == -1: - # Match was in filename, return start of content - return content[:100].strip() + "..." if len(content) > 100 else content - - start = max(0, pos - context) - end = min(len(content), pos + len(query) + context) - - snippet = content[start:end].strip() - if start > 0: - snippet = "..." + snippet - if end < len(content): - snippet = snippet + "..." - - return snippet diff --git a/src/storied/engine.py b/src/storied/engine.py index 4a4c2d4..7d3b827 100644 --- a/src/storied/engine.py +++ b/src/storied/engine.py @@ -312,17 +312,13 @@ class DMEngine: if not self.world_id: return None - # Fast path: use the entity index from the MCP server - entity_index = self._mcp.entity_index - if entity_index: - path = entity_index.resolve(name) - if path and path.exists(): - return { - "name": name, - "body": path.read_text(), - "entity_type": path.parent.name, - } - return None + path = self._mcp.ctx.entity_index.resolve(name) + if path and path.exists(): + return { + "name": name, + "body": path.read_text(), + "entity_type": path.parent.name, + } # Fallback: filesystem scan entity = load_entity_content(name, self.world_id, self.base_path) @@ -463,10 +459,19 @@ class DMEngine: # Write conversation turn to transcript dm_response = "".join(dm_text_parts) if dm_response.strip(): - self._transcript.append_turn( - player_input, dm_response, - self._campaign_log.get_current_time(), - ) + game_time = self._campaign_log.get_current_time() + self._transcript.append_turn(player_input, dm_response, game_time) + + # Index the transcript turn for semantic search + day_path = self._transcript._day_path(game_time.day) + if day_path.exists(): + self._mcp.ctx.vector_index.upsert( + f"transcript:transcripts/day+{game_time.day:03d}.md:0", + day_path.read_text(), + {"source": "transcript", "content_type": "transcripts", + "path": str(day_path), "title": f"Day {game_time.day}", + "game_day": game_time.day}, + ) def reset(self) -> None: """Reset the conversation state.""" diff --git a/src/storied/mcp_server.py b/src/storied/mcp_server.py index 0872282..30bfd6c 100644 --- a/src/storied/mcp_server.py +++ b/src/storied/mcp_server.py @@ -18,11 +18,13 @@ from mcp.server.sse import SseServerTransport from mcp.types import TextContent, Tool from storied.log import CampaignLog +from storied.search import VectorIndex from storied.tools import ( EntityIndex, PLANNER_TOOL_DEFINITIONS, SEEDER_TOOL_DEFINITIONS, TOOL_DEFINITIONS, + ToolContext, execute_tool, planner_execute_tool, seeder_execute_tool, @@ -60,13 +62,10 @@ def _find_free_port() -> int: class MCPServerHandle: """Handle to a running in-process MCP server.""" - def __init__( - self, port: int, thread: threading.Thread, - entity_index: EntityIndex | None = None, - ): + def __init__(self, port: int, thread: threading.Thread, ctx: ToolContext): self.port = port self.url = f"http://127.0.0.1:{port}/sse" - self.entity_index = entity_index + self.ctx = ctx self._thread = thread def stop(self) -> None: @@ -91,7 +90,27 @@ def start_server( campaign_log = CampaignLog(world_id, base_path) world_dir = base_path / "worlds" / world_id - entity_index = EntityIndex(world_dir) + + def _populate_index(vi: VectorIndex) -> None: + """Auto-populate an empty index from SRD seed + world content.""" + srd_seed = base_path / "rules" / "srd-5.2.1" / "search.db" + if srd_seed.exists(): + vi.reseed(srd_seed) + else: + srd_dir = base_path / "rules" / "srd-5.2.1" / "sections" + if srd_dir.exists(): + vi.reindex_directory(srd_dir, source="srd") + if world_dir.exists(): + vi.reindex_directory(world_dir, source="world") + + ctx = ToolContext( + world_id=world_id, + player_id=player_id, + base_path=base_path, + campaign_log=campaign_log, + entity_index=EntityIndex(world_dir), + vector_index=VectorIndex(world_dir / "search.db", on_empty=_populate_index), + ) definitions = TOOL_SETS.get(tool_set, TOOL_DEFINITIONS) executor = EXECUTORS.get(tool_set, execute_tool) @@ -105,15 +124,7 @@ def start_server( @mcp.call_tool() async def call_tool(name: str, arguments: dict) -> list[TextContent]: - result = executor( - name, - arguments, - world_id=world_id, - player_id=player_id, - base_path=base_path, - campaign_log=campaign_log, - entity_index=entity_index, - ) + result = executor(name, arguments, ctx) return [TextContent(type="text", text=str(result))] sse = SseServerTransport("/messages/") @@ -144,7 +155,6 @@ def start_server( thread = threading.Thread(target=server.run, daemon=True) thread.start() - # Wait for the server to be ready for _ in range(50): try: with socket.create_connection(("127.0.0.1", port), timeout=0.1): @@ -152,4 +162,4 @@ def start_server( except OSError: time.sleep(0.1) - return MCPServerHandle(port, thread, entity_index=entity_index) + return MCPServerHandle(port, thread, ctx) diff --git a/src/storied/planner.py b/src/storied/planner.py index c60f4bd..15b3fa2 100644 --- a/src/storied/planner.py +++ b/src/storied/planner.py @@ -16,7 +16,10 @@ from storied.session import ( load_session, resolve_wiki_link, ) -from storied.tools import _load_entity +from storied.tools import EntityIndex, _load_entity + +# Shared empty index for read-only entity parsing (no cache needed) +_EMPTY_INDEX = EntityIndex() def entity_richness(path: Path) -> float: @@ -30,7 +33,7 @@ def entity_richness(path: Path) -> float: - Has Was history (0.1) - Has wikilinks to other entities (0.1) """ - entity = _load_entity(path) + entity = _load_entity(path, _EMPTY_INDEX) if not entity: return 0.0 @@ -345,7 +348,7 @@ def _find_entities_with_will( if not type_dir.exists(): continue for path in type_dir.glob("*.md"): - entity = _load_entity(path) + entity = _load_entity(path, _EMPTY_INDEX) if entity and entity.get("will"): results.append((path.stem, path)) diff --git a/src/storied/search.py b/src/storied/search.py new file mode 100644 index 0000000..8bfebe6 --- /dev/null +++ b/src/storied/search.py @@ -0,0 +1,387 @@ +"""Local semantic vector search over SRD, world, and player content. + +Uses fastembed (ONNX runtime) for embeddings and sqlite-vec for storage. +The index lives as a single search.db file in the world directory. +""" + +import re +import shutil +import struct +from dataclasses import dataclass +from pathlib import Path +from typing import Callable + +import pysqlite3 as sqlite3 +import sqlite_vec +from fastembed import TextEmbedding + +EMBED_MODEL = "BAAI/bge-small-en-v1.5" +EMBED_DIM = 384 +CHUNK_CHAR_THRESHOLD = 4_000 + + +@dataclass +class SearchHit: + """A single result from vector search.""" + + doc_id: str + path: Path + score: float + snippet: str + source: str + content_type: str + + +def age_decay(current_day: int, doc_day: int, half_life: int = 3) -> float: + """Exponential decay: score halves every `half_life` game days. + + Transcripts from today score 1.0, from `half_life` days ago score 0.5. + Future docs (doc_day > current_day) are not boosted. + """ + age = max(0, current_day - doc_day) + return 0.5 ** (age / half_life) + + +def chunk_document(path: Path, content: str) -> list[tuple[int, str]]: + """Split a document into indexed chunks for embedding. + + Small files return a single chunk. Larger files split on ## headers, + each prefixed with the document title for context. + + Returns list of (chunk_index, chunk_text) pairs. + """ + if not content.strip(): + return [(0, path.stem)] + + # Extract title from first # heading + title_match = re.match(r"^#\s+(.+)", content) + title = title_match.group(1).strip() if title_match else path.stem + + if len(content) < CHUNK_CHAR_THRESHOLD: + return [(0, content)] + + # Split on ## headers + sections = re.split(r"\n(?=## )", content) + + # If only one section (no ## headers), return as single chunk + if len(sections) <= 1: + return [(0, content)] + + chunks: list[tuple[int, str]] = [] + chunk_idx = 0 + + for section in sections: + section = section.strip() + if not section: + continue + + # Prepend title to non-first chunks for context + if chunk_idx > 0: + text = f"# {title}\n\n{section}" + else: + text = section + + # Sub-split oversized sections on ### headers + if len(text) > CHUNK_CHAR_THRESHOLD: + subsections = re.split(r"\n(?=### )", text) + for sub in subsections: + sub = sub.strip() + if sub: + chunks.append((chunk_idx, sub)) + chunk_idx += 1 + else: + chunks.append((chunk_idx, text)) + chunk_idx += 1 + + return chunks if chunks else [(0, content)] + + +def _serialize_f32(vec: list[float]) -> bytes: + """Serialize a float vector to bytes for sqlite-vec.""" + return struct.pack(f"<{len(vec)}f", *vec) + + +def _default_embed(texts: list[str]) -> list[list[float]]: + """Lazy-loaded fastembed model.""" + if not hasattr(_default_embed, "_model"): + _default_embed._model = TextEmbedding(EMBED_MODEL) # type: ignore[attr-defined] + return [vec.tolist() for vec in _default_embed._model.embed(texts)] # type: ignore[attr-defined] + + +def _connect(db_path: str) -> sqlite3.Connection: + """Open a SQLite connection with sqlite-vec loaded.""" + conn = sqlite3.connect(db_path) + conn.enable_load_extension(True) + sqlite_vec.load(conn) + conn.enable_load_extension(False) + return conn + + +class VectorIndex: + """SQLite-backed vector search index. + + Stores document metadata in a regular table and embeddings in a + sqlite-vec virtual table. Supports upsert, delete, search with + optional source filtering and age-decay for temporal content. + + If the database is missing or corrupt, it is recreated automatically. + """ + + def __init__( + self, + db_path: Path, + on_empty: Callable[["VectorIndex"], None] | None = None, + ): + self._db_path = db_path + self._embed_fn: Callable[[list[str]], list[list[float]]] = _default_embed + self._on_empty = on_empty + self._conn = self._open_or_recreate() + + @staticmethod + def seed_from(seed_path: Path, dest_path: Path) -> "VectorIndex": + """Create a new index by copying an existing one as a seed. + + Used to bootstrap a world's search.db from the pre-built SRD index. + """ + dest_path.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(seed_path, dest_path) + return VectorIndex(dest_path) + + def reseed(self, seed_path: Path) -> None: + """Replace this index's DB with a copy of the seed and reconnect.""" + self._conn.close() + shutil.copy2(seed_path, self._db_path) + self._conn = self._open_or_recreate() + + def close(self) -> None: + """Close the database connection.""" + self._conn.close() + + def _open_or_recreate(self) -> sqlite3.Connection: + """Open the database, recreating if corrupt.""" + self._db_path.parent.mkdir(parents=True, exist_ok=True) + try: + conn = _connect(str(self._db_path)) + conn.execute("SELECT count(*) FROM documents") + return conn + except Exception: + if self._db_path.exists(): + self._db_path.unlink() + return self._create_fresh() + + def _create_fresh(self) -> sqlite3.Connection: + """Create a new database with the required schema.""" + self._db_path.parent.mkdir(parents=True, exist_ok=True) + conn = _connect(str(self._db_path)) + conn.execute(""" + CREATE TABLE IF NOT EXISTS documents ( + doc_id TEXT PRIMARY KEY, + path TEXT NOT NULL, + source TEXT NOT NULL, + content_type TEXT, + chunk_index INTEGER DEFAULT 0, + title TEXT, + body_preview TEXT, + game_day INTEGER, + updated_at REAL NOT NULL + ) + """) + conn.execute(f""" + CREATE VIRTUAL TABLE IF NOT EXISTS vec_documents USING vec0( + doc_id TEXT PRIMARY KEY, + embedding FLOAT[{EMBED_DIM}] + ) + """) + conn.commit() + return conn + + def embed(self, texts: list[str]) -> list[list[float]]: + """Embed one or more texts into vectors.""" + return self._embed_fn(texts) + + def upsert(self, doc_id: str, text: str, metadata: dict) -> None: + """Insert or update a document and its embedding.""" + vec = self.embed([text])[0] + blob = _serialize_f32(vec) + preview = text[:200].strip() + + self._conn.execute( + "DELETE FROM vec_documents WHERE doc_id = ?", (doc_id,) + ) + self._conn.execute( + "DELETE FROM documents WHERE doc_id = ?", (doc_id,) + ) + + self._conn.execute( + """INSERT INTO documents + (doc_id, path, source, content_type, chunk_index, + title, body_preview, game_day, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""", + ( + doc_id, + metadata.get("path", ""), + metadata["source"], + metadata.get("content_type"), + metadata.get("chunk_index", 0), + metadata.get("title"), + preview, + metadata.get("game_day"), + metadata.get("updated_at", 0.0), + ), + ) + self._conn.execute( + "INSERT INTO vec_documents (doc_id, embedding) VALUES (?, ?)", + (doc_id, blob), + ) + self._conn.commit() + + def delete(self, doc_id: str) -> None: + """Remove a document and its embedding.""" + self._conn.execute( + "DELETE FROM vec_documents WHERE doc_id = ?", (doc_id,) + ) + self._conn.execute( + "DELETE FROM documents WHERE doc_id = ?", (doc_id,) + ) + self._conn.commit() + + def search( + self, + query: str, + limit: int = 5, + source_filter: str | None = None, + exclude_source: str | None = None, + decay_ref: int | None = None, + ) -> list[SearchHit]: + """Search the index by semantic similarity. + + Args: + query: Natural language search query + limit: Max results to return + source_filter: Restrict to a single source ("srd", "world", etc.) + exclude_source: Exclude a single source from results + decay_ref: Current game day for age-decay on transcripts. + If None, no decay is applied. + """ + # Auto-populate if the index is empty (fires at most once) + if self._on_empty: + count = self._conn.execute( + "SELECT count(*) FROM documents" + ).fetchone()[0] + if count == 0: + self._on_empty(self) + self._on_empty = None + + vec = self.embed([query])[0] + blob = _serialize_f32(vec) + + fetch_limit = limit * 3 + + rows = self._conn.execute( + """SELECT v.doc_id, v.distance, d.path, d.source, + d.content_type, d.body_preview, d.game_day + FROM vec_documents v + JOIN documents d ON v.doc_id = d.doc_id + WHERE v.embedding MATCH ? + AND k = ? + ORDER BY v.distance""", + (blob, fetch_limit), + ).fetchall() + + hits: list[SearchHit] = [] + for doc_id, distance, path, source, ctype, preview, game_day in rows: + if source_filter and source != source_filter: + continue + if exclude_source and source == exclude_source: + continue + + score = 1.0 / (1.0 + distance) + + if decay_ref is not None and game_day is not None: + if source in ("transcript", "player"): + score *= age_decay(decay_ref, game_day) + + hits.append(SearchHit( + doc_id=doc_id, + path=Path(path), + score=score, + snippet=preview or "", + source=source, + content_type=ctype or "", + )) + + hits.sort(key=lambda h: h.score, reverse=True) + return hits[:limit] + + def reindex_directory(self, directory: Path, source: str) -> int: + """Index all .md files under a directory. + + Skips files whose mtime hasn't changed since last index. + Returns the number of documents indexed (counting chunks). + """ + existing = {} + for row in self._conn.execute( + "SELECT doc_id, updated_at FROM documents WHERE source = ?", + (source,), + ): + existing[row[0]] = row[1] + + seen_doc_ids: set[str] = set() + count = 0 + + for md_file in sorted(directory.rglob("*.md")): + rel = md_file.relative_to(directory) + content = md_file.read_text() + mtime = md_file.stat().st_mtime + content_type = rel.parts[0] if len(rel.parts) > 1 else "" + + chunks = chunk_document(md_file, content) + + for chunk_idx, chunk_text in chunks: + doc_id = f"{source}:{rel}:{chunk_idx}" + seen_doc_ids.add(doc_id) + + if doc_id in existing and existing[doc_id] == mtime: + count += 1 + continue + + title_match = re.match(r"^#\s+(.+)", content) + title = ( + title_match.group(1).strip() if title_match else md_file.stem + ) + + game_day = None + day_match = re.match(r"day([+-]\d+)", md_file.stem) + if day_match: + game_day = int(day_match.group(1)) + + self.upsert(doc_id, chunk_text, { + "source": source, + "content_type": content_type, + "path": str(md_file), + "title": title, + "chunk_index": chunk_idx, + "game_day": game_day, + "updated_at": mtime, + }) + count += 1 + + stale = set(existing.keys()) - seen_doc_ids + for doc_id in stale: + self.delete(doc_id) + + return count + + def stats(self) -> dict: + """Return index statistics.""" + total = self._conn.execute( + "SELECT count(*) FROM documents" + ).fetchone()[0] + + by_source: dict[str, int] = {} + for source, cnt in self._conn.execute( + "SELECT source, count(*) FROM documents GROUP BY source" + ): + by_source[source] = cnt + + return {"total_documents": total, "by_source": by_source} diff --git a/src/storied/tools.py b/src/storied/tools.py index 5156ad2..8b48f0d 100644 --- a/src/storied/tools.py +++ b/src/storied/tools.py @@ -6,15 +6,16 @@ the tool descriptions that Claude sees. import re import threading +from dataclasses import dataclass from pathlib import Path import yaml from storied.character import create_character as char_create from storied.character import update_character as char_update -from storied.content import ContentResolver from storied.dice import roll as dice_roll -from storied.log import CampaignLog, load_log +from storied.log import CampaignLog +from storied.search import VectorIndex from storied.session import name_to_slug from storied.session import update_session as session_update @@ -64,6 +65,22 @@ class EntityIndex: self._cache[path] = data +@dataclass +class ToolContext: + """Shared infrastructure for all tool calls. + + Created once per MCP server and passed to every tool invocation. + All fields are required — no defensive None checks in tool code. + """ + + world_id: str + player_id: str + base_path: Path + campaign_log: CampaignLog + entity_index: EntityIndex + vector_index: VectorIndex + + def roll(notation: str, reason: str | None = None) -> dict: """Roll dice using standard notation like '1d20', '2d6+3', '4d6kh3'. @@ -84,10 +101,9 @@ def roll(notation: str, reason: str | None = None) -> dict: def recall( query: str, + ctx: ToolContext, scope: str = "all", content_type: str | None = None, - world_id: str | None = None, - base_path: Path | None = None, ) -> str: """Look up rules, world content, or both. @@ -100,77 +116,36 @@ def recall( query: What to look up (e.g., "fireball", "captain vex", "merchant guild") scope: Where to search - "rules", "world", or "all" (default) content_type: Optional type to limit search (e.g., "spells", "npcs") - world_id: World to query (required if scope includes world) - base_path: Base path for content resolution (for testing) Returns: Content of the found item, or a message if not found """ - results_parts = [] - - # Search rules if scope includes it - if scope in ("rules", "all"): - resolver = ContentResolver(base_path=base_path) - content = resolver.load(query, content_type=content_type) - if content: - return content["body"] - - results = resolver.search(query, content_type=content_type) - if results: - if len(results) == 1: - content = resolver.load( - results[0].name, content_type=results[0].content_type - ) - if content: - return content["body"] - else: - results_parts.append(("rules", results)) - - # Search world if scope includes it and world_id is provided - if scope in ("world", "all") and world_id: - resolver = ContentResolver(base_path=base_path, world_id=world_id) - content = resolver.load(query, content_type=content_type) - if content: - return content["body"] - - results = resolver.search(query, content_type=content_type) - if results: - if len(results) == 1: - content = resolver.load( - results[0].name, content_type=results[0].content_type - ) - if content: - return content["body"] - else: - results_parts.append(("world", results)) - - # No exact match - return list of results if any - if results_parts: - lines = [] - for source, results in results_parts: - lines.append(f"Found {len(results)} matches in {source}:") - for r in results[:5]: - lines.append(f" - {r.name} ({r.content_type})") - if len(results) > 5: - lines.append(f" ... and {len(results) - 5} more") + # scope="rules" → SRD only; scope="world" → everything except SRD; + # scope="all" → no filter + source_filter: str | None = None + if scope == "rules": + source_filter = "srd" + + current_day = ctx.campaign_log.get_current_time().day + hits = ctx.vector_index.search( + query, limit=5, source_filter=source_filter, + exclude_source="srd" if scope == "world" else None, + decay_ref=current_day, + ) + if hits: + if len(hits) == 1 or hits[0].score > 0.8: + hit_path = Path(hits[0].path) + if hit_path.exists(): + return hit_path.read_text() + lines = [f"Found {len(hits)} matches:"] + for h in hits: + lines.append(f" - {h.doc_id.split(':')[1]} ({h.source}): {h.snippet[:80]}") return "\n".join(lines) - # Nothing found - if scope == "rules": - return f"No rules found matching '{query}'" - elif scope == "world": - if not world_id: - return "No world specified. Use scope='rules' for base game content." - return f"Nothing found in the world matching '{query}'" - else: - return f"Nothing found matching '{query}'" + return f"Nothing found matching '{query}'" -def update_character( - updates: dict, - player_id: str = "default", - base_path: Path | None = None, -) -> str: +def update_character(updates: dict, ctx: ToolContext) -> str: """Update the player's character sheet to persist changes. Call this after HP changes, equipment gained/lost, coins spent, level ups, etc. @@ -191,7 +166,7 @@ def update_character( Returns: Confirmation of what was updated """ - return char_update(player_id, updates, base_path) + return char_update(ctx.player_id, updates, ctx.base_path) def create_character( @@ -202,6 +177,7 @@ def create_character( abilities: dict[str, int], hp_max: int, ac: int, + ctx: ToolContext, background: str | None = None, speed: int = 30, purse: dict[str, int] | None = None, @@ -209,8 +185,6 @@ def create_character( features: list[str] | None = None, proficiencies: str | None = None, backstory: str | None = None, - player_id: str = "default", - base_path: Path | None = None, ) -> str: """Create a new player character and save to disk. @@ -242,7 +216,7 @@ def create_character( Confirmation message """ return char_create( - player_id=player_id, + player_id=ctx.player_id, name=name, race=race, char_class=char_class, @@ -257,11 +231,12 @@ def create_character( features=features, proficiencies=proficiencies, backstory=backstory, - base_path=base_path, + base_path=ctx.base_path, ) def set_scene( + ctx: ToolContext, event: str | None = None, duration: str | None = None, situation: str | None = None, @@ -269,11 +244,6 @@ def set_scene( present: list[str] | None = None, threads: list[str] | None = None, tags: list[str] | None = None, - player_id: str = "default", - base_path: Path | None = None, - campaign_log: CampaignLog | None = None, - world_id: str = "default", - entity_index: EntityIndex | None = None, ) -> str: """Call this after every response. Logs what happened, advances the clock, and updates the scene state. @@ -292,27 +262,20 @@ def set_scene( (e.g., ["[[Vera Blackwater]]", "[[Henrik]] - barkeep"]) threads: Open plot threads or objectives tags: Optional tags: "combat", "rest:short", "rest:long", "travel" - player_id: Player identifier - base_path: Base path for players directory - campaign_log: CampaignLog instance (injected by engine) - world_id: World identifier (injected by engine) Returns: Confirmation of what was updated """ parts = [] - # Log event and advance clock if event and duration: - log = campaign_log or load_log(world_id, base_path) - anchor = log.append_entry(event, duration, tags=tags) - current = log.get_current_time() + anchor = ctx.campaign_log.append_entry(event, duration, tags=tags) + current = ctx.campaign_log.get_current_time() parts.append( f"Logged: {anchor} | {event} | {duration} → " f"Now: {current} ({current.period_of_day()}, {current.atmosphere()})" ) - # Update session state updates = {} if situation is not None: updates["situation"] = situation @@ -324,15 +287,11 @@ def set_scene( updates["threads"] = threads if updates: - result = session_update(player_id, updates, base_path) + result = session_update(ctx.player_id, updates, ctx.base_path) parts.append(result) - # Auto-mark present entities with this event - if event and present and world_id: - marked = _auto_mark_present( - present, event, world_id, base_path, campaign_log, - entity_index=entity_index, - ) + if event and present: + marked = _auto_mark_present(present, event, ctx) if marked: parts.append(f"Auto-marked: {', '.join(marked)}") @@ -342,14 +301,12 @@ def set_scene( def establish( entity_type: str, name: str, + ctx: ToolContext, description: str | None = None, location: str | None = None, knows: list[str] | None = None, wants: list[str] | None = None, will: list[str] | None = None, - world_id: str | None = None, - base_path: Path | None = None, - entity_index: EntityIndex | None = None, ) -> str: """Establish or update an entity in the world. @@ -377,29 +334,18 @@ def establish( knows: List of secrets and hidden truths. Things that aren't obvious. wants: List of desires, tendencies, inclinations. The entity's nature. will: List of conditional behaviors: "If X → Y" format. - world_id: World to save to (required) - base_path: Base path for worlds directory Returns: Confirmation with the file path """ - if not world_id: - return "Error: No world_id specified. Cannot save world content." - - if base_path is None: - base_path = Path.cwd() - - # Build file path using display name directly - world_dir = base_path / "worlds" / world_id / entity_type + world_dir = ctx.base_path / "worlds" / ctx.world_id / entity_type world_dir.mkdir(parents=True, exist_ok=True) file_path = world_dir / f"{name}.md" lock = _get_file_lock(file_path) with lock: - # Load existing content if file exists (for partial updates) - existing = _load_entity(file_path, entity_index) + existing = _load_entity(file_path, ctx.entity_index) - # Merge with existing content (new values override) if description is None: description = existing.get("description", "") if location is None: @@ -410,33 +356,23 @@ def establish( wants = existing.get("wants", []) if will is None: will = existing.get("will", []) - was = existing.get("was", []) # Always preserve Was - - # Build file content - file_content = _format_entity(name, description, location, knows, wants, will, was) - file_path.write_text(file_content) + was = existing.get("was", []) - # Write-through: update index and cache - updated_data = { + data = { "description": description, "location": location, "knows": knows, "wants": wants, "will": will, "was": was, } - if entity_index: - entity_index.register(name, file_path) - entity_index.cache_put(file_path, updated_data) + _write_entity(file_path, name, entity_type, data, ctx) action = "Updated" if existing else "Established" return f"{action} {entity_type.rstrip('s')} '{name}'" -def _load_entity( - file_path: Path, entity_index: EntityIndex | None = None, -) -> dict: +def _load_entity(file_path: Path, entity_index: EntityIndex) -> dict: """Load an existing entity file and parse its structure.""" - if entity_index: - cached = entity_index.cache_get(file_path) - if cached is not None: - return cached + cached = entity_index.cache_get(file_path) + if cached is not None: + return cached if not file_path.exists(): return {} @@ -482,8 +418,7 @@ def _load_entity( if was_match: result["was"] = _parse_list_items(was_match.group(1)) - if entity_index: - entity_index.cache_put(file_path, result) + entity_index.cache_put(file_path, result) return result @@ -548,50 +483,55 @@ def _format_entity( return "\n".join(lines) +def _write_entity( + file_path: Path, + name: str, + entity_type: str, + data: dict, + ctx: ToolContext, +) -> None: + """Write an entity to disk and update all indexes.""" + file_content = _format_entity( + name, data["description"], data["location"], + data["knows"], data["wants"], data["will"], data["was"], + ) + file_path.write_text(file_content) + ctx.entity_index.register(name, file_path) + ctx.entity_index.cache_put(file_path, data) + ctx.vector_index.upsert( + f"world:{entity_type}/{name}.md:0", + file_content, + {"source": "world", "content_type": entity_type, + "path": str(file_path), "title": name}, + ) + + def _auto_mark_present( - present: list[str], - event: str, - world_id: str, - base_path: Path | None, - campaign_log: CampaignLog | None, - entity_index: EntityIndex | None = None, + present: list[str], event: str, ctx: ToolContext, ) -> list[str]: """Auto-mark present entities with the current event. Extracts entity names from [[wikilink]] format in the present list and appends the event to each entity's Was section. """ - if base_path is None: - base_path = Path.cwd() - marked: list[str] = [] for ref in present: - # Extract name from "[[Name]]" or "[[Name]] - description" link_match = re.search(r"\[\[([^\]]+)\]\]", ref) if not link_match: continue name = link_match.group(1) - # Resolve via index (O(1)) or fall back to directory scan - file_path = entity_index.resolve(name) if entity_index else None + file_path = ctx.entity_index.resolve(name) if file_path is None: for etype in ("npcs", "locations", "items", "factions"): - candidate = base_path / "worlds" / world_id / etype / f"{name}.md" + candidate = ctx.base_path / "worlds" / ctx.world_id / etype / f"{name}.md" if candidate.exists(): file_path = candidate break if file_path and file_path.exists(): entity_type = file_path.parent.name - mark( - entity_type=entity_type, - name=name, - event=event, - world_id=world_id, - base_path=base_path, - campaign_log=campaign_log, - entity_index=entity_index, - ) + mark(entity_type=entity_type, name=name, event=event, ctx=ctx) marked.append(name) return marked @@ -601,11 +541,8 @@ def mark( entity_type: str, name: str, event: str, + ctx: ToolContext, resolves: list[str] | None = None, - world_id: str | None = None, - base_path: Path | None = None, - campaign_log: CampaignLog | None = None, - entity_index: EntityIndex | None = None, ) -> str: """Record an event in an entity's history (## Was section). @@ -621,47 +558,26 @@ def mark( name: Entity name (exact filename match) event: What happened - brief description for the Was section resolves: Optional list of Will items to remove if this event fired triggers - world_id: World containing the entity (required) - base_path: Base path for worlds directory - campaign_log: Campaign log for current game time (optional) Returns: Confirmation message """ - if not world_id: - return "Error: No world_id specified." - - if base_path is None: - base_path = Path.cwd() - - # Resolve path via index or filesystem - if entity_index: - file_path = entity_index.resolve(name) - if file_path is None: - file_path = base_path / "worlds" / world_id / entity_type / f"{name}.md" - else: - file_path = base_path / "worlds" / world_id / entity_type / f"{name}.md" + file_path = ctx.entity_index.resolve(name) + if file_path is None: + file_path = ctx.base_path / "worlds" / ctx.world_id / entity_type / f"{name}.md" if not file_path.exists(): return f"Error: Entity '{name}' not found in {entity_type}" - # Get current game time for timestamp - if campaign_log: - timestamp = campaign_log.get_current_time().to_anchor() - else: - log = load_log(world_id, base_path) - timestamp = log.get_current_time().to_anchor() + timestamp = ctx.campaign_log.get_current_time().to_anchor() lock = _get_file_lock(file_path) with lock: - # Load existing entity - existing = _load_entity(file_path, entity_index) + existing = _load_entity(file_path, ctx.entity_index) - # Append to Was section was = existing.get("was", []) was.append(f"{timestamp} | {event}") - # Remove resolved Will items will = existing.get("will", []) resolved = [] for trigger in resolves or []: @@ -669,28 +585,15 @@ def mark( will.remove(trigger) resolved.append(trigger) - # Rebuild and save the file - file_content = _format_entity( - name, - existing.get("description", ""), - existing.get("location", ""), - existing.get("knows", []), - existing.get("wants", []), - will, - was, - ) - file_path.write_text(file_content) - - # Write-through cache - if entity_index: - entity_index.cache_put(file_path, { - "description": existing.get("description", ""), - "location": existing.get("location", ""), - "knows": existing.get("knows", []), - "wants": existing.get("wants", []), - "will": will, - "was": was, - }) + data = { + "description": existing.get("description", ""), + "location": existing.get("location", ""), + "knows": existing.get("knows", []), + "wants": existing.get("wants", []), + "will": will, + "was": was, + } + _write_entity(file_path, name, entity_type, data, ctx) result = f"Marked: {event}" if resolved: @@ -704,11 +607,9 @@ def mark( def note_discovery( entity: str, content: str, + ctx: ToolContext, content_type: str = "lore", tags: list[str] | None = None, - world_id: str | None = None, - player_id: str = "default", - base_path: Path | None = None, ) -> str: """Record what the player has learned about something. @@ -725,36 +626,26 @@ def note_discovery( content: What the player learned or observed content_type: Type of content - npcs, locations, factions, lore (default: lore) tags: Optional tags for categorization - world_id: World this knowledge is about (required) - player_id: Player who learned this - base_path: Base path for players directory Returns: Confirmation message """ - if not world_id: - return "Error: No world_id specified. Cannot record player knowledge." - - if base_path is None: - base_path = Path.cwd() - - # Generate slug from entity name slug = name_to_slug(entity) - # Player knowledge path: players/{player}/worlds/{world}/{type}/ - knowledge_dir = base_path / "players" / player_id / "worlds" / world_id / content_type + knowledge_dir = ( + ctx.base_path / "players" / ctx.player_id / "worlds" + / ctx.world_id / content_type + ) knowledge_dir.mkdir(parents=True, exist_ok=True) file_path = knowledge_dir / f"{slug}.md" - # Build frontmatter - frontmatter = { + frontmatter: dict = { "type": content_type.rstrip("s"), "name": entity, } if tags: frontmatter["tags"] = tags - # Build file content file_content = "---\n" file_content += yaml.dump(frontmatter, sort_keys=False, allow_unicode=True) file_content += "---\n\n" @@ -763,15 +654,17 @@ def note_discovery( file_path.write_text(file_content) + ctx.vector_index.upsert( + f"player:{content_type}/{slug}.md:0", + file_content, + {"source": "player", "content_type": content_type, + "path": str(file_path), "title": entity}, + ) + return f"Noted: player learned about '{entity}'" -def end_session( - situation: str, - threads: list[str] | None = None, - player_id: str = "default", - base_path: Path | None = None, -) -> str: +def end_session(situation: str, ctx: ToolContext, threads: list[str] | None = None) -> str: """End the current session, saving the game state for next time. Call this when the player indicates they want to stop playing. This saves @@ -783,17 +676,15 @@ def end_session( situation: Summary of the current state of affairs for the next session. Write as if briefing a DM who will pick up where you left off. threads: Open plot threads or objectives to carry forward - player_id: Player identifier - base_path: Base path for players directory Returns: Confirmation that session was saved """ - updates = {"situation": situation} + updates: dict = {"situation": situation} if threads is not None: updates["threads"] = threads - session_update(player_id, updates, base_path) + session_update(ctx.player_id, updates, ctx.base_path) return "SESSION_ENDED" @@ -1060,31 +951,10 @@ TOOL_DEFINITIONS = [ ] -def execute_tool( - tool_name: str, - tool_input: dict, - world_id: str | None = None, - player_id: str = "default", - base_path: Path | None = None, - campaign_log: CampaignLog | None = None, - entity_index: EntityIndex | None = None, -) -> str: - """Execute a tool by name with the given input. - - Args: - tool_name: Name of the tool to execute - tool_input: Tool input parameters - world_id: Current world ID for query_world - player_id: Player ID for update_character - base_path: Base path for content resolution - campaign_log: Campaign log instance for log_event - - Returns: - Tool result as a string - """ +def execute_tool(tool_name: str, tool_input: dict, ctx: ToolContext) -> str: + """Execute a tool by name with the given input.""" if tool_name == "roll": result = roll(tool_input["notation"]) - # Format nicely for the DM rolls_str = ", ".join(str(r) for r in result["rolls"]) if result["kept"] != result["rolls"]: kept_str = ", ".join(str(r) for r in result["kept"]) @@ -1096,19 +966,13 @@ def execute_tool( elif tool_name == "recall": return recall( - tool_input["query"], + tool_input["query"], ctx, scope=tool_input.get("scope", "all"), content_type=tool_input.get("content_type"), - world_id=world_id, - base_path=base_path, ) elif tool_name == "update_character": - return update_character( - tool_input["updates"], - player_id=player_id, - base_path=base_path, - ) + return update_character(tool_input["updates"], ctx) elif tool_name == "create_character": return create_character( @@ -1119,6 +983,7 @@ def execute_tool( abilities=tool_input["abilities"], hp_max=tool_input["hp_max"], ac=tool_input["ac"], + ctx=ctx, background=tool_input.get("background"), speed=tool_input.get("speed", 30), purse=tool_input.get("purse"), @@ -1126,12 +991,11 @@ def execute_tool( features=tool_input.get("features"), proficiencies=tool_input.get("proficiencies"), backstory=tool_input.get("backstory"), - player_id=player_id, - base_path=base_path, ) elif tool_name == "set_scene": return set_scene( + ctx=ctx, event=tool_input.get("event"), duration=tool_input.get("duration"), situation=tool_input.get("situation"), @@ -1139,25 +1003,18 @@ def execute_tool( present=tool_input.get("present"), threads=tool_input.get("threads"), tags=tool_input.get("tags"), - player_id=player_id, - base_path=base_path, - campaign_log=campaign_log, - world_id=world_id or "default", - entity_index=entity_index, ) elif tool_name == "establish": return establish( entity_type=tool_input["entity_type"], name=tool_input["name"], + ctx=ctx, description=tool_input.get("description"), location=tool_input.get("location"), knows=tool_input.get("knows"), wants=tool_input.get("wants"), will=tool_input.get("will"), - world_id=world_id, - base_path=base_path, - entity_index=entity_index, ) elif tool_name == "mark": @@ -1165,30 +1022,24 @@ def execute_tool( entity_type=tool_input["entity_type"], name=tool_input["name"], event=tool_input["event"], + ctx=ctx, resolves=tool_input.get("resolves"), - world_id=world_id, - base_path=base_path, - campaign_log=campaign_log, - entity_index=entity_index, ) elif tool_name == "note_discovery": return note_discovery( entity=tool_input["entity"], content=tool_input["content"], + ctx=ctx, content_type=tool_input.get("content_type", "lore"), tags=tool_input.get("tags"), - world_id=world_id, - player_id=player_id, - base_path=base_path, ) elif tool_name == "end_session": return end_session( situation=tool_input["situation"], + ctx=ctx, threads=tool_input.get("threads"), - player_id=player_id, - base_path=base_path, ) else: @@ -1200,25 +1051,11 @@ PLANNER_TOOLS = {"recall", "establish", "mark"} PLANNER_TOOL_DEFINITIONS = [t for t in TOOL_DEFINITIONS if t["name"] in PLANNER_TOOLS] -def planner_execute_tool( - tool_name: str, - tool_input: dict, - world_id: str | None = None, - base_path: Path | None = None, - campaign_log: CampaignLog | None = None, - entity_index: EntityIndex | None = None, -) -> str: +def planner_execute_tool(tool_name: str, tool_input: dict, ctx: ToolContext) -> str: """Execute a planner-allowed tool. Rejects anything outside the allowed set.""" if tool_name not in PLANNER_TOOLS: return f"Tool not available to planner: {tool_name}" - return execute_tool( - tool_name, - tool_input, - world_id=world_id, - base_path=base_path, - campaign_log=campaign_log, - entity_index=entity_index, - ) + return execute_tool(tool_name, tool_input, ctx) SEEDER_TOOLS = {"establish", "set_scene"} @@ -1226,24 +1063,8 @@ SEEDER_TOOLS = {"establish", "set_scene"} SEEDER_TOOL_DEFINITIONS = [t for t in TOOL_DEFINITIONS if t["name"] in SEEDER_TOOLS] -def seeder_execute_tool( - tool_name: str, - tool_input: dict, - world_id: str | None = None, - player_id: str | None = None, - base_path: Path | None = None, - campaign_log: CampaignLog | None = None, - entity_index: EntityIndex | None = None, -) -> str: +def seeder_execute_tool(tool_name: str, tool_input: dict, ctx: ToolContext) -> str: """Execute a seeder-allowed tool. Rejects anything outside the allowed set.""" if tool_name not in SEEDER_TOOLS: return f"Tool not available to seeder: {tool_name}" - return execute_tool( - tool_name, - tool_input, - world_id=world_id, - player_id=player_id or "default", - base_path=base_path, - campaign_log=campaign_log, - entity_index=entity_index, - ) + return execute_tool(tool_name, tool_input, ctx) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..2cf90bd --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,49 @@ +"""Shared test fixtures.""" + +import hashlib +from pathlib import Path + +import pytest + +from storied.log import CampaignLog +from storied.search import VectorIndex +from storied.tools import EntityIndex, ToolContext + +EMBED_DIM = 384 + + +def _fake_embed(texts: list[str]) -> list[list[float]]: + """Deterministic hash-based embeddings for testing.""" + results = [] + for text in texts: + h = hashlib.sha256(text.encode()).digest() + floats: list[float] = [] + seed = h + while len(floats) < EMBED_DIM: + seed = hashlib.sha256(seed).digest() + for byte in seed: + if len(floats) >= EMBED_DIM: + break + floats.append((byte - 128) / 128.0) + norm = sum(x * x for x in floats) ** 0.5 + results.append([x / norm for x in floats]) + return results + + +@pytest.fixture +def ctx(tmp_path: Path) -> ToolContext: + """ToolContext with fake embedder for tests that need tool infrastructure.""" + world_dir = tmp_path / "worlds" / "test-world" + world_dir.mkdir(parents=True) + + vi = VectorIndex(tmp_path / "search.db") + vi._embed_fn = _fake_embed + + return ToolContext( + world_id="test-world", + player_id="default", + base_path=tmp_path, + campaign_log=CampaignLog("test-world", tmp_path), + entity_index=EntityIndex(world_dir), + vector_index=vi, + ) diff --git a/tests/test_character.py b/tests/test_character.py index ca00e8d..eb3923d 100644 --- a/tests/test_character.py +++ b/tests/test_character.py @@ -1,34 +1,29 @@ """Tests for character loading, saving, updates, and display formatting.""" -from pathlib import Path - import pytest from storied.character import ( - create_character, format_character_context, format_sheet, format_status, load_character, parse_character, - save_character, - update_character, ) +from storied.tools import ToolContext, create_character, update_character @pytest.fixture -def player_base(tmp_path: Path) -> Path: - """Create a base directory with player structure.""" - player = tmp_path / "players" / "test-player" - player.mkdir(parents=True) - return tmp_path +def char_ctx(ctx: ToolContext) -> ToolContext: + """ToolContext configured for character tests with players dir.""" + ctx.player_id = "test-player" + (ctx.base_path / "players" / "test-player").mkdir(parents=True) + return ctx @pytest.fixture -def basic_character(player_base: Path) -> dict: +def basic_character(char_ctx: ToolContext) -> dict: """Create a basic character for testing.""" create_character( - player_id="test-player", name="Test Hero", race="Human", char_class="Fighter", @@ -44,9 +39,9 @@ def basic_character(player_base: Path) -> dict: hp_max=12, ac=16, purse={"cp": 0, "sp": 0, "ep": 0, "gp": 50, "pp": 0}, - base_path=player_base, + ctx=char_ctx, ) - return load_character("test-player", player_base) + return load_character("test-player", char_ctx.base_path) class TestParseCharacter: @@ -77,9 +72,8 @@ hp: class TestCreateCharacter: """Tests for character creation.""" - def test_create_basic_character(self, player_base: Path): + def test_create_basic_character(self, char_ctx: ToolContext): result = create_character( - player_id="test-player", name="Conan", race="Human", char_class="Barbarian", @@ -94,16 +88,15 @@ class TestCreateCharacter: }, hp_max=15, ac=14, - base_path=player_base, + ctx=char_ctx, ) assert "Created character 'Conan'" in result - char_file = player_base / "players/test-player/character.md" + char_file = char_ctx.base_path / "players/test-player/character.md" assert char_file.exists() - def test_created_character_has_full_hp(self, player_base: Path): + def test_created_character_has_full_hp(self, char_ctx: ToolContext): create_character( - player_id="test-player", name="Test", race="Human", char_class="Fighter", @@ -112,10 +105,10 @@ class TestCreateCharacter: "intelligence": 10, "wisdom": 10, "charisma": 10}, hp_max=10, ac=10, - base_path=player_base, + ctx=char_ctx, ) - char = load_character("test-player", player_base) + char = load_character("test-player", char_ctx.base_path) assert char["hp"]["current"] == 10 assert char["hp"]["max"] == 10 @@ -123,37 +116,34 @@ class TestCreateCharacter: class TestUpdateCharacter: """Tests for character updates.""" - def test_update_purse(self, player_base: Path, basic_character: dict): + def test_update_purse(self, char_ctx: ToolContext, basic_character: dict): result = update_character( - player_id="test-player", updates={"purse.gp": 100, "purse.sp": 25}, - base_path=player_base, + ctx=char_ctx, ) assert "purse.gp = 100" in result - char = load_character("test-player", player_base) + char = load_character("test-player", char_ctx.base_path) assert char["purse"]["gp"] == 100 assert char["purse"]["sp"] == 25 - def test_update_hp_current(self, player_base: Path, basic_character: dict): + def test_update_hp_current(self, char_ctx: ToolContext, basic_character: dict): result = update_character( - player_id="test-player", updates={"hp.current": 5}, - base_path=player_base, + ctx=char_ctx, ) assert "hp.current = 5" in result - char = load_character("test-player", player_base) + char = load_character("test-player", char_ctx.base_path) assert char["hp"]["current"] == 5 - def test_update_section(self, player_base: Path, basic_character: dict): + def test_update_section(self, char_ctx: ToolContext, basic_character: dict): update_character( - player_id="test-player", updates={"section.Equipment": "- Longsword\n- Shield"}, - base_path=player_base, + ctx=char_ctx, ) - char = load_character("test-player", player_base) + char = load_character("test-player", char_ctx.base_path) assert "Longsword" in char["body"] assert "Shield" in char["body"] @@ -161,97 +151,89 @@ class TestUpdateCharacter: class TestHPClamping: """Tests for HP clamping to valid 5e range.""" - def test_hp_cannot_go_negative(self, player_base: Path, basic_character: dict): + def test_hp_cannot_go_negative(self, char_ctx: ToolContext, basic_character: dict): """In 5e, HP minimum is 0 (no negative HP).""" update_character( - player_id="test-player", updates={"hp.current": -5}, - base_path=player_base, + ctx=char_ctx, ) - char = load_character("test-player", player_base) + char = load_character("test-player", char_ctx.base_path) assert char["hp"]["current"] == 0 - def test_hp_clamped_message(self, player_base: Path, basic_character: dict): + def test_hp_clamped_message(self, char_ctx: ToolContext, basic_character: dict): """Update result should indicate HP was clamped.""" result = update_character( - player_id="test-player", updates={"hp.current": -10}, - base_path=player_base, + ctx=char_ctx, ) assert "clamped to 0" in result - def test_hp_cannot_exceed_max(self, player_base: Path, basic_character: dict): + def test_hp_cannot_exceed_max(self, char_ctx: ToolContext, basic_character: dict): """HP cannot exceed maximum.""" update_character( - player_id="test-player", updates={"hp.current": 100}, - base_path=player_base, + ctx=char_ctx, ) - char = load_character("test-player", player_base) + char = load_character("test-player", char_ctx.base_path) assert char["hp"]["current"] == 12 # max HP is 12 - def test_hp_exceeds_max_clamped_message(self, player_base: Path, basic_character: dict): + def test_hp_exceeds_max_clamped_message(self, char_ctx: ToolContext, basic_character: dict): """Update result should indicate HP was clamped to max.""" result = update_character( - player_id="test-player", updates={"hp.current": 999}, - base_path=player_base, + ctx=char_ctx, ) assert "clamped to 12" in result - def test_valid_hp_not_clamped(self, player_base: Path, basic_character: dict): + def test_valid_hp_not_clamped(self, char_ctx: ToolContext, basic_character: dict): """Valid HP values should not be modified.""" result = update_character( - player_id="test-player", updates={"hp.current": 6}, - base_path=player_base, + ctx=char_ctx, ) assert "clamped" not in result - char = load_character("test-player", player_base) + char = load_character("test-player", char_ctx.base_path) assert char["hp"]["current"] == 6 - def test_hp_zero_is_valid(self, player_base: Path, basic_character: dict): + def test_hp_zero_is_valid(self, char_ctx: ToolContext, basic_character: dict): """Setting HP to exactly 0 is valid (unconscious).""" result = update_character( - player_id="test-player", updates={"hp.current": 0}, - base_path=player_base, + ctx=char_ctx, ) assert "clamped" not in result - char = load_character("test-player", player_base) + char = load_character("test-player", char_ctx.base_path) assert char["hp"]["current"] == 0 - def test_hp_max_is_valid(self, player_base: Path, basic_character: dict): + def test_hp_max_is_valid(self, char_ctx: ToolContext, basic_character: dict): """Setting HP to exactly max is valid.""" result = update_character( - player_id="test-player", updates={"hp.current": 12}, - base_path=player_base, + ctx=char_ctx, ) assert "clamped" not in result - char = load_character("test-player", player_base) + char = load_character("test-player", char_ctx.base_path) assert char["hp"]["current"] == 12 - def test_damage_calculation_example(self, player_base: Path, basic_character: dict): + def test_damage_calculation_example(self, char_ctx: ToolContext, basic_character: dict): """Simulate taking 20 damage when at 12 HP - should clamp to 0.""" # Character starts at 12/12 HP # Takes 20 damage, DM sets hp.current = -8 # Should be clamped to 0 update_character( - player_id="test-player", updates={"hp.current": -8}, - base_path=player_base, + ctx=char_ctx, ) - char = load_character("test-player", player_base) + char = load_character("test-player", char_ctx.base_path) assert char["hp"]["current"] == 0 diff --git a/tests/test_content.py b/tests/test_content.py index 5112587..452040f 100644 --- a/tests/test_content.py +++ b/tests/test_content.py @@ -4,7 +4,7 @@ from pathlib import Path import pytest -from storied.content import ContentResolver, SearchResult +from storied.content import ContentResolver @pytest.fixture @@ -167,34 +167,3 @@ class TestLoadContent: assert "Orc" in content["body"] -class TestSearch: - """Tests for searching content.""" - - def test_search_by_keyword(self, resolver: ContentResolver): - results = resolver.search("dragon") - assert len(results) >= 1 - assert any("dragon" in r.name.lower() for r in results) - - def test_search_with_content_type(self, resolver: ContentResolver): - results = resolver.search("fire", content_type="spells") - assert len(results) >= 1 - assert all(r.content_type == "spells" for r in results) - - def test_search_no_results(self, resolver: ContentResolver): - results = resolver.search("zzzznonexistent") - assert len(results) == 0 - - def test_search_result_structure(self, resolver: ContentResolver): - results = resolver.search("goblin") - assert len(results) >= 1 - result = results[0] - assert isinstance(result, SearchResult) - assert result.name is not None - assert result.path is not None - assert result.content_type is not None - - def test_search_in_body(self, resolver: ContentResolver): - # Should find magic missile by searching for "auto-hit" - results = resolver.search("Auto-hit") - assert len(results) >= 1 - assert any("magic-missile" in r.name for r in results) diff --git a/tests/test_entities.py b/tests/test_entities.py index 9078a06..d4be637 100644 --- a/tests/test_entities.py +++ b/tests/test_entities.py @@ -4,61 +4,44 @@ from pathlib import Path import pytest -from storied.log import CampaignLog from storied.session import ( extract_wiki_links, load_entity_content, resolve_wiki_link, ) -from storied.tools import EntityIndex, establish, mark - - -@pytest.fixture -def world_base(tmp_path: Path) -> Path: - """Create a base directory with world structure.""" - world = tmp_path / "worlds" / "test-world" - world.mkdir(parents=True) - return tmp_path - - -@pytest.fixture -def campaign_log(world_base: Path) -> CampaignLog: - """Create a campaign log for the test world.""" - return CampaignLog("test-world", world_base) +from storied.tools import EntityIndex, ToolContext, establish, mark class TestEstablish: """Tests for the establish tool.""" - def test_establish_creates_npc(self, world_base: Path): + def test_establish_creates_npc(self, ctx: ToolContext): result = establish( entity_type="npcs", name="Vera Blackwater", + ctx=ctx, description="Tavern owner, former smuggler.", knows=["The guild smuggles weapons"], wants=["Keep her tavern safe"], will=["If trusted → introduce to Harrik"], - world_id="test-world", - base_path=world_base, ) assert "Established" in result - npc_file = world_base / "worlds/test-world/npcs/Vera Blackwater.md" + npc_file = ctx.base_path / "worlds/test-world/npcs/Vera Blackwater.md" assert npc_file.exists() - def test_establish_file_format(self, world_base: Path): + def test_establish_file_format(self, ctx: ToolContext): establish( entity_type="npcs", name="Test NPC", + ctx=ctx, description="A test character.", knows=["Secret one", "Secret two"], wants=["Goal one"], will=["If X → do Y"], - world_id="test-world", - base_path=world_base, ) - content = (world_base / "worlds/test-world/npcs/Test NPC.md").read_text() + content = (ctx.base_path / "worlds/test-world/npcs/Test NPC.md").read_text() assert "# Test NPC" in content assert "## Is" in content @@ -71,133 +54,124 @@ class TestEstablish: assert "### Will" in content assert "- If X → do Y" in content - def test_establish_location(self, world_base: Path): + def test_establish_location(self, ctx: ToolContext): establish( entity_type="locations", name="The Rusty Anchor", + ctx=ctx, description="A dockside tavern.", knows=["Hidden tunnel in cellar"], wants=["To shelter those who need it"], will=["If searched → reveal tunnel"], - world_id="test-world", - base_path=world_base, ) - loc_file = world_base / "worlds/test-world/locations/The Rusty Anchor.md" + loc_file = ctx.base_path / "worlds/test-world/locations/The Rusty Anchor.md" assert loc_file.exists() content = loc_file.read_text() assert "Hidden tunnel in cellar" in content - def test_establish_item(self, world_base: Path): + def test_establish_item(self, ctx: ToolContext): establish( entity_type="items", name="The Skeleton Key", + ctx=ctx, description="Ancient brass key, cold to the touch.", knows=["Forged by Archmage Velius"], wants=["To free what is locked away"], will=["Unlock any non-magical lock"], - world_id="test-world", - base_path=world_base, ) - item_file = world_base / "worlds/test-world/items/The Skeleton Key.md" + item_file = ctx.base_path / "worlds/test-world/items/The Skeleton Key.md" assert item_file.exists() - def test_establish_partial_update(self, world_base: Path): + def test_establish_partial_update(self, ctx: ToolContext): # Create initial entity establish( entity_type="npcs", name="Guard Mara", + ctx=ctx, description="A stern city guard.", knows=["Patrol routes"], wants=["Uphold the law"], will=[], - world_id="test-world", - base_path=world_base, ) # Update just the description establish( entity_type="npcs", name="Guard Mara", + ctx=ctx, description="A stern city guard, recently promoted to sergeant.", - world_id="test-world", - base_path=world_base, ) - content = (world_base / "worlds/test-world/npcs/Guard Mara.md").read_text() + content = (ctx.base_path / "worlds/test-world/npcs/Guard Mara.md").read_text() assert "recently promoted" in content assert "Patrol routes" in content # Preserved from original - def test_establish_with_wikilinks(self, world_base: Path): + def test_establish_with_wikilinks(self, ctx: ToolContext): establish( entity_type="npcs", name="Captain Harrik", + ctx=ctx, description="Veteran sailor who frequents [[The Rusty Anchor]].", knows=["[[Vera Blackwater]] was once a smuggler"], wants=["Find the [[Ghost Ship]]"], will=[], - world_id="test-world", - base_path=world_base, ) - content = (world_base / "worlds/test-world/npcs/Captain Harrik.md").read_text() + content = (ctx.base_path / "worlds/test-world/npcs/Captain Harrik.md").read_text() assert "[[The Rusty Anchor]]" in content assert "[[Vera Blackwater]]" in content - def test_establish_empty_sections_omitted(self, world_base: Path): + def test_establish_empty_sections_omitted(self, ctx: ToolContext): establish( entity_type="npcs", name="Simple NPC", + ctx=ctx, description="Just a description.", knows=[], wants=[], will=[], - world_id="test-world", - base_path=world_base, ) - content = (world_base / "worlds/test-world/npcs/Simple NPC.md").read_text() + content = (ctx.base_path / "worlds/test-world/npcs/Simple NPC.md").read_text() assert "### Knows" not in content assert "### Wants" not in content assert "### Will" not in content - def test_establish_with_location(self, world_base: Path): + def test_establish_with_location(self, ctx: ToolContext): establish( entity_type="npcs", name="Garrick the Jailer", + ctx=ctx, description="Heavyset man in his fifties.", location="In the basement of [[Greyhaven City Jail]]", knows=["Where the keys are kept"], - world_id="test-world", - base_path=world_base, ) - content = (world_base / "worlds/test-world/npcs/Garrick the Jailer.md").read_text() + content = (ctx.base_path / "worlds/test-world/npcs/Garrick the Jailer.md").read_text() assert "**Location:** In the basement of [[Greyhaven City Jail]]" in content assert "Heavyset man" in content - def test_establish_location_preserved_on_update(self, world_base: Path): + def test_establish_location_preserved_on_update(self, ctx: ToolContext): # Create with location establish( entity_type="npcs", name="Wanderer", + ctx=ctx, description="A traveler.", location="[[The Rusty Anchor]]", - world_id="test-world", - base_path=world_base, ) # Update without specifying location establish( entity_type="npcs", name="Wanderer", + ctx=ctx, description="A weary traveler.", - world_id="test-world", - base_path=world_base, ) - content = (world_base / "worlds/test-world/npcs/Wanderer.md").read_text() + content = (ctx.base_path / "worlds/test-world/npcs/Wanderer.md").read_text() assert "**Location:** [[The Rusty Anchor]]" in content assert "weary traveler" in content @@ -205,88 +179,80 @@ class TestEstablish: class TestMark: """Tests for the mark tool.""" - def test_mark_appends_to_was(self, world_base: Path, campaign_log: CampaignLog): + def test_mark_appends_to_was(self, ctx: ToolContext): # Create entity first establish( entity_type="npcs", name="Vera Blackwater", + ctx=ctx, description="Tavern owner.", knows=[], wants=[], will=[], - world_id="test-world", - base_path=world_base, ) result = mark( entity_type="npcs", name="Vera Blackwater", event="Met the player, gave them a room", - world_id="test-world", - base_path=world_base, - campaign_log=campaign_log, + ctx=ctx, ) assert "Marked" in result - content = (world_base / "worlds/test-world/npcs/Vera Blackwater.md").read_text() + content = (ctx.base_path / "worlds/test-world/npcs/Vera Blackwater.md").read_text() assert "## Was" in content assert "Met the player" in content - def test_mark_includes_timestamp(self, world_base: Path, campaign_log: CampaignLog): + def test_mark_includes_timestamp(self, ctx: ToolContext): establish( entity_type="npcs", name="Test NPC", + ctx=ctx, description="Test.", - world_id="test-world", - base_path=world_base, ) mark( entity_type="npcs", name="Test NPC", event="Something happened", - world_id="test-world", - base_path=world_base, - campaign_log=campaign_log, + ctx=ctx, ) - content = (world_base / "worlds/test-world/npcs/Test NPC.md").read_text() + content = (ctx.base_path / "worlds/test-world/npcs/Test NPC.md").read_text() # Should have timestamp anchor format assert "#d" in content assert "|" in content - def test_mark_resolves_will_trigger(self, world_base: Path, campaign_log: CampaignLog): + def test_mark_resolves_will_trigger(self, ctx: ToolContext): establish( entity_type="npcs", name="Vera Blackwater", + ctx=ctx, description="Tavern owner.", knows=[], wants=[], will=["If trusted → introduce to Harrik", "If threatened → tip off guild"], - world_id="test-world", - base_path=world_base, ) mark( entity_type="npcs", name="Vera Blackwater", event="Introduced player to Captain Harrik", + ctx=ctx, resolves=["If trusted → introduce to Harrik"], - world_id="test-world", - base_path=world_base, - campaign_log=campaign_log, ) - content = (world_base / "worlds/test-world/npcs/Vera Blackwater.md").read_text() + content = (ctx.base_path / "worlds/test-world/npcs/Vera Blackwater.md").read_text() # Resolved trigger should be removed assert "If trusted → introduce to Harrik" not in content # Other trigger should remain assert "If threatened → tip off guild" in content - def test_mark_resolves_multiple_triggers(self, world_base: Path, campaign_log: CampaignLog): + def test_mark_resolves_multiple_triggers(self, ctx: ToolContext): establish( entity_type="npcs", name="Complex NPC", + ctx=ctx, description="Has many triggers.", knows=[], wants=[], @@ -295,21 +261,17 @@ class TestMark: "If trusted → reveal secret", "If threatened → flee", ], - world_id="test-world", - base_path=world_base, ) result = mark( entity_type="npcs", name="Complex NPC", event="Player became a trusted ally through multiple good deeds", + ctx=ctx, resolves=["If friendly → share rumors", "If trusted → reveal secret"], - world_id="test-world", - base_path=world_base, - campaign_log=campaign_log, ) - content = (world_base / "worlds/test-world/npcs/Complex NPC.md").read_text() + content = (ctx.base_path / "worlds/test-world/npcs/Complex NPC.md").read_text() # Both resolved triggers should be removed assert "If friendly → share rumors" not in content assert "If trusted → reveal secret" not in content @@ -318,48 +280,41 @@ class TestMark: # Result should mention resolving multiple assert "resolved 2 triggers" in result - def test_mark_multiple_events(self, world_base: Path, campaign_log: CampaignLog): + def test_mark_multiple_events(self, ctx: ToolContext): establish( entity_type="locations", name="The Docks", + ctx=ctx, description="Busy harbor area.", - world_id="test-world", - base_path=world_base, ) mark( entity_type="locations", name="The Docks", event="Player arrived by ferry", - world_id="test-world", - base_path=world_base, - campaign_log=campaign_log, + ctx=ctx, ) # Advance time - campaign_log.append_entry("Time passed", "1 hour") + ctx.campaign_log.append_entry("Time passed", "1 hour") mark( entity_type="locations", name="The Docks", event="Player witnessed smugglers loading cargo", - world_id="test-world", - base_path=world_base, - campaign_log=campaign_log, + ctx=ctx, ) - content = (world_base / "worlds/test-world/locations/The Docks.md").read_text() + content = (ctx.base_path / "worlds/test-world/locations/The Docks.md").read_text() assert "Player arrived" in content assert "witnessed smugglers" in content - def test_mark_nonexistent_entity_fails(self, world_base: Path, campaign_log: CampaignLog): + def test_mark_nonexistent_entity_fails(self, ctx: ToolContext): result = mark( entity_type="npcs", name="Nobody", event="Did something", - world_id="test-world", - base_path=world_base, - campaign_log=campaign_log, + ctx=ctx, ) assert "not found" in result.lower() @@ -383,48 +338,46 @@ class TestWikilinkResolution: links = extract_wiki_links(text) assert links == ["Vera", "Vera", "Bob"] - def test_resolve_wiki_link_npc(self, world_base: Path): + def test_resolve_wiki_link_npc(self, ctx: ToolContext): establish( entity_type="npcs", name="Vera Blackwater", + ctx=ctx, description="Test.", - world_id="test-world", - base_path=world_base, ) - path = resolve_wiki_link("Vera Blackwater", "test-world", world_base) + path = resolve_wiki_link("Vera Blackwater", "test-world", ctx.base_path) assert path is not None assert path.name == "Vera Blackwater.md" assert "npcs" in str(path) - def test_resolve_wiki_link_location(self, world_base: Path): + def test_resolve_wiki_link_location(self, ctx: ToolContext): establish( entity_type="locations", name="The Rusty Anchor", + ctx=ctx, description="Test.", - world_id="test-world", - base_path=world_base, ) - path = resolve_wiki_link("The Rusty Anchor", "test-world", world_base) + path = resolve_wiki_link("The Rusty Anchor", "test-world", ctx.base_path) assert path is not None assert "locations" in str(path) - def test_resolve_wiki_link_not_found(self, world_base: Path): - path = resolve_wiki_link("Nonexistent", "test-world", world_base) + def test_resolve_wiki_link_not_found(self, ctx: ToolContext): + path = resolve_wiki_link("Nonexistent", "test-world", ctx.base_path) assert path is None - def test_resolve_wiki_link_priority_order(self, world_base: Path): + def test_resolve_wiki_link_priority_order(self, ctx: ToolContext): # Create same name in multiple directories - npcs should win - (world_base / "worlds/test-world/npcs").mkdir(parents=True) - (world_base / "worlds/test-world/locations").mkdir(parents=True) + (ctx.base_path / "worlds/test-world/npcs").mkdir(parents=True, exist_ok=True) + (ctx.base_path / "worlds/test-world/locations").mkdir(parents=True, exist_ok=True) - (world_base / "worlds/test-world/npcs/Ambiguous.md").write_text("# NPC version") - (world_base / "worlds/test-world/locations/Ambiguous.md").write_text( + (ctx.base_path / "worlds/test-world/npcs/Ambiguous.md").write_text("# NPC version") + (ctx.base_path / "worlds/test-world/locations/Ambiguous.md").write_text( "# Location version" ) - path = resolve_wiki_link("Ambiguous", "test-world", world_base) + path = resolve_wiki_link("Ambiguous", "test-world", ctx.base_path) assert path is not None assert "npcs" in str(path) # NPCs have priority over locations @@ -432,25 +385,24 @@ class TestWikilinkResolution: class TestLoadEntityContent: """Tests for loading entity content by name.""" - def test_load_entity_content(self, world_base: Path): + def test_load_entity_content(self, ctx: ToolContext): establish( entity_type="npcs", name="Test Character", + ctx=ctx, description="A test.", knows=["A secret"], - world_id="test-world", - base_path=world_base, ) - entity = load_entity_content("Test Character", "test-world", world_base) + entity = load_entity_content("Test Character", "test-world", ctx.base_path) assert entity is not None assert entity["name"] == "Test Character" assert entity["entity_type"] == "npcs" assert "A test." in entity["content"] assert "A secret" in entity["content"] - def test_load_entity_content_not_found(self, world_base: Path): - entity = load_entity_content("Nobody", "test-world", world_base) + def test_load_entity_content_not_found(self, ctx: ToolContext): + entity = load_entity_content("Nobody", "test-world", ctx.base_path) assert entity is None @@ -458,9 +410,9 @@ class TestLoadEntityContent: @pytest.fixture -def indexed_world(world_base: Path) -> tuple[Path, EntityIndex]: +def indexed_world(ctx: ToolContext) -> tuple[Path, EntityIndex]: """Create a world with entities and build an index.""" - world_dir = world_base / "worlds" / "test-world" + world_dir = ctx.base_path / "worlds" / "test-world" for etype, name in [("npcs", "Vera"), ("locations", "Tavern"), ("items", "Sword")]: d = world_dir / etype d.mkdir(parents=True, exist_ok=True) @@ -521,51 +473,45 @@ class TestEntityIndex: class TestEstablishWithIndex: """Tests that establish writes through to the index and cache.""" - def test_establish_registers_in_index(self, world_base: Path): - world_dir = world_base / "worlds" / "test-world" - index = EntityIndex(world_dir) + def test_establish_registers_in_index(self, ctx: ToolContext): + ctx.entity_index = EntityIndex(ctx.base_path / "worlds" / ctx.world_id) establish( - entity_type="npcs", name="New NPC", + entity_type="npcs", + name="New NPC", + ctx=ctx, description="A stranger.", - world_id="test-world", base_path=world_base, - entity_index=index, ) - assert index.resolve("New NPC") is not None + assert ctx.entity_index.resolve("New NPC") is not None - def test_establish_caches_entity(self, world_base: Path): - world_dir = world_base / "worlds" / "test-world" - index = EntityIndex(world_dir) + def test_establish_caches_entity(self, ctx: ToolContext): + ctx.entity_index = EntityIndex(ctx.base_path / "worlds" / ctx.world_id) establish( - entity_type="npcs", name="Cached NPC", + entity_type="npcs", + name="Cached NPC", + ctx=ctx, description="Cached.", knows=["a secret"], - world_id="test-world", base_path=world_base, - entity_index=index, ) - path = index.resolve("Cached NPC") - cached = index.cache_get(path) + path = ctx.entity_index.resolve("Cached NPC") + cached = ctx.entity_index.cache_get(path) assert cached is not None assert cached["description"] == "Cached." assert cached["knows"] == ["a secret"] - def test_mark_updates_cache( - self, world_base: Path, campaign_log: CampaignLog, - ): - world_dir = world_base / "worlds" / "test-world" - index = EntityIndex(world_dir) + def test_mark_updates_cache(self, ctx: ToolContext): + ctx.entity_index = EntityIndex(ctx.base_path / "worlds" / ctx.world_id) establish( - entity_type="npcs", name="Markable", + entity_type="npcs", + name="Markable", + ctx=ctx, description="An NPC.", - world_id="test-world", base_path=world_base, - entity_index=index, ) mark( - entity_type="npcs", name="Markable", + entity_type="npcs", + name="Markable", event="Something happened", - world_id="test-world", base_path=world_base, - campaign_log=campaign_log, - entity_index=index, + ctx=ctx, ) - path = index.resolve("Markable") - cached = index.cache_get(path) + path = ctx.entity_index.resolve("Markable") + cached = ctx.entity_index.cache_get(path) assert any("Something happened" in w for w in cached["was"]) diff --git a/tests/test_planner.py b/tests/test_planner.py index 796e747..680c2e3 100644 --- a/tests/test_planner.py +++ b/tests/test_planner.py @@ -1,12 +1,10 @@ """Tests for the world planner — entity richness scoring, discovery, and context.""" import json -from pathlib import Path from unittest.mock import MagicMock, patch import pytest -from storied.log import CampaignLog from storied.planner import ( PlanResult, build_planning_context, @@ -15,134 +13,107 @@ from storied.planner import ( plan_world, ) from storied.session import save_session -from storied.tools import establish +from storied.tools import ToolContext, establish, mark @pytest.fixture -def world_base(tmp_path: Path) -> Path: - """Create a base directory with world structure.""" - world = tmp_path / "worlds" / "test-world" - world.mkdir(parents=True) - return tmp_path - - -@pytest.fixture -def campaign_log(world_base: Path) -> CampaignLog: - """Create a campaign log for the test world.""" - return CampaignLog("test-world", world_base) - - -@pytest.fixture -def populated_world(world_base: Path) -> Path: +def populated_world(ctx: ToolContext) -> ToolContext: """Create a world with some entities for testing.""" establish( entity_type="locations", name="Town Square", + ctx=ctx, description="The center of [[Millford]]. A fountain stands here, surrounded by market stalls.", location="Central [[Millford]]", knows=["The fountain was built by [[Old Gregor]]"], wants=["To be a gathering place"], will=["If market day → attract crowds"], - world_id="test-world", - base_path=world_base, ) establish( entity_type="npcs", name="Old Gregor", + ctx=ctx, description="An elderly stonemason.", location="[[Town Square]]", - world_id="test-world", - base_path=world_base, ) establish( entity_type="locations", name="Millford", + ctx=ctx, description="A small riverside town.", - world_id="test-world", - base_path=world_base, ) establish( entity_type="npcs", name="Thin NPC", + ctx=ctx, description="Someone with no inner life.", - world_id="test-world", - base_path=world_base, ) - return world_base + return ctx class TestEntityRichness: """Tests for richness scoring.""" - def test_empty_entity_scores_zero(self, world_base: Path): + def test_empty_entity_scores_zero(self, ctx: ToolContext): establish( entity_type="npcs", name="Empty", - world_id="test-world", - base_path=world_base, + ctx=ctx, ) - path = world_base / "worlds/test-world/npcs/Empty.md" + path = ctx.base_path / "worlds/test-world/npcs/Empty.md" assert entity_richness(path) == 0.0 - def test_description_only(self, world_base: Path): + def test_description_only(self, ctx: ToolContext): establish( entity_type="npcs", name="Described", + ctx=ctx, description="A tall warrior.", - world_id="test-world", - base_path=world_base, ) - path = world_base / "worlds/test-world/npcs/Described.md" + path = ctx.base_path / "worlds/test-world/npcs/Described.md" assert entity_richness(path) == pytest.approx(0.2) - def test_fully_rich_entity(self, world_base: Path, campaign_log: CampaignLog): + def test_fully_rich_entity(self, ctx: ToolContext): establish( entity_type="npcs", name="Rich NPC", + ctx=ctx, description="A fully fleshed out character in [[Town Square]].", knows=["Secret one", "Secret two"], wants=["Goal one"], will=["If X → do Y"], - world_id="test-world", - base_path=world_base, ) - from storied.tools import mark - mark( entity_type="npcs", name="Rich NPC", event="Something happened", - world_id="test-world", - base_path=world_base, - campaign_log=campaign_log, + ctx=ctx, ) - path = world_base / "worlds/test-world/npcs/Rich NPC.md" + path = ctx.base_path / "worlds/test-world/npcs/Rich NPC.md" score = entity_richness(path) assert score == pytest.approx(1.0) - def test_partial_richness(self, world_base: Path): + def test_partial_richness(self, ctx: ToolContext): establish( entity_type="npcs", name="Partial", + ctx=ctx, description="Has description and knows.", knows=["A secret"], - world_id="test-world", - base_path=world_base, ) - path = world_base / "worlds/test-world/npcs/Partial.md" + path = ctx.base_path / "worlds/test-world/npcs/Partial.md" score = entity_richness(path) # description (0.2) + knows (0.2) = 0.4 assert score == pytest.approx(0.4) - def test_wikilinks_contribute(self, world_base: Path): + def test_wikilinks_contribute(self, ctx: ToolContext): establish( entity_type="npcs", name="Linked", + ctx=ctx, description="Hangs out at [[The Tavern]] with [[Bob]].", - world_id="test-world", - base_path=world_base, ) - path = world_base / "worlds/test-world/npcs/Linked.md" + path = ctx.base_path / "worlds/test-world/npcs/Linked.md" score = entity_richness(path) # description (0.2) + wikilinks (0.1) = 0.3 assert score == pytest.approx(0.3) @@ -151,155 +122,150 @@ class TestEntityRichness: class TestFindNearbyEntities: """Tests for discovering entities near the player.""" - def test_finds_entities_from_location_links(self, populated_world: Path): + def test_finds_entities_from_location_links(self, populated_world: ToolContext): session = { "location": "Town Square", "body": "", } - nearby = find_nearby_entities(session, "test-world", populated_world) + nearby = find_nearby_entities(session, "test-world", populated_world.base_path) names = {name for name, _ in nearby} assert "Old Gregor" in names assert "Millford" in names - def test_finds_entities_from_session_body(self, populated_world: Path): + def test_finds_entities_from_session_body(self, populated_world: ToolContext): session = { "location": "Town Square", "body": "## Present\n- [[Thin NPC]]", } - nearby = find_nearby_entities(session, "test-world", populated_world) + nearby = find_nearby_entities(session, "test-world", populated_world.base_path) names = {name for name, _ in nearby} assert "Thin NPC" in names - def test_includes_current_location(self, populated_world: Path): + def test_includes_current_location(self, populated_world: ToolContext): session = { "location": "Town Square", "body": "", } - nearby = find_nearby_entities(session, "test-world", populated_world) + nearby = find_nearby_entities(session, "test-world", populated_world.base_path) names = {name for name, _ in nearby} assert "Town Square" in names - def test_no_duplicates(self, populated_world: Path): + def test_no_duplicates(self, populated_world: ToolContext): session = { "location": "Town Square", "body": "## Present\n- [[Old Gregor]]", } - nearby = find_nearby_entities(session, "test-world", populated_world) + nearby = find_nearby_entities(session, "test-world", populated_world.base_path) names = [name for name, _ in nearby] assert names.count("Old Gregor") == 1 - def test_empty_session(self, world_base: Path): + def test_empty_session(self, ctx: ToolContext): session = {"body": ""} - nearby = find_nearby_entities(session, "test-world", world_base) + nearby = find_nearby_entities(session, "test-world", ctx.base_path) assert nearby == [] class TestGetRecentEntries: """Tests for CampaignLog.get_recent_entries().""" - def test_returns_current_day_entries(self, world_base: Path): - log = CampaignLog("test-world", world_base) - log.append_entry("Arrived at the tavern", "10 min") - log.append_entry("Spoke with bartender", "15 min") + def test_returns_current_day_entries(self, ctx: ToolContext): + ctx.campaign_log.append_entry("Arrived at the tavern", "10 min") + ctx.campaign_log.append_entry("Spoke with bartender", "15 min") - entries = log.get_recent_entries(days=1) + entries = ctx.campaign_log.get_recent_entries(days=1) events = [e.event for e in entries] assert "Arrived at the tavern" in events assert "Spoke with bartender" in events - def test_returns_previous_day_entries(self, world_base: Path): - log = CampaignLog("test-world", world_base) - log.append_entry("Morning event", "10 min") - log.append_entry("Traveled all day", "18 hours") - log.append_entry("Day 2 event", "10 min") + def test_returns_previous_day_entries(self, ctx: ToolContext): + ctx.campaign_log.append_entry("Morning event", "10 min") + ctx.campaign_log.append_entry("Traveled all day", "18 hours") + ctx.campaign_log.append_entry("Day 2 event", "10 min") - entries = log.get_recent_entries(days=2) + entries = ctx.campaign_log.get_recent_entries(days=2) events = [e.event for e in entries] assert "Morning event" in events assert "Day 2 event" in events - def test_respects_days_limit(self, world_base: Path): - log = CampaignLog("test-world", world_base) - log.append_entry("Day 1 event", "18 hours") - log.append_entry("Day 2 event", "18 hours") - log.append_entry("Day 3 event", "1 hour") + def test_respects_days_limit(self, ctx: ToolContext): + ctx.campaign_log.append_entry("Day 1 event", "18 hours") + ctx.campaign_log.append_entry("Day 2 event", "18 hours") + ctx.campaign_log.append_entry("Day 3 event", "1 hour") - entries = log.get_recent_entries(days=1) + entries = ctx.campaign_log.get_recent_entries(days=1) events = [e.event for e in entries] assert "Day 3 event" in events assert "Day 1 event" not in events - def test_empty_log(self, world_base: Path): - log = CampaignLog("test-world", world_base) - entries = log.get_recent_entries(days=2) + def test_empty_log(self, ctx: ToolContext): + entries = ctx.campaign_log.get_recent_entries(days=2) assert entries == [] class TestBuildPlanningContext: """Tests for assembling the planner's context.""" - def test_includes_session_info(self, populated_world: Path): + def test_includes_session_info(self, populated_world: ToolContext): save_session( "default", { "location": "Town Square", "body": "## Situation\nThe player just arrived.\n\n## Open Threads\n- Find the missing cat", }, - populated_world, + populated_world.base_path, ) context = build_planning_context( - world_id="test-world", - player_id="default", - base_path=populated_world, - candidates=[("Thin NPC", populated_world / "worlds/test-world/npcs/Thin NPC.md")], + world_id=populated_world.world_id, + player_id=populated_world.player_id, + base_path=populated_world.base_path, + candidates=[("Thin NPC", populated_world.base_path / "worlds/test-world/npcs/Thin NPC.md")], ) assert "Town Square" in context assert "missing cat" in context - def test_includes_candidate_content(self, populated_world: Path): + def test_includes_candidate_content(self, populated_world: ToolContext): save_session( "default", {"location": "Town Square", "body": ""}, - populated_world, + populated_world.base_path, ) - path = populated_world / "worlds/test-world/npcs/Thin NPC.md" + path = populated_world.base_path / "worlds/test-world/npcs/Thin NPC.md" context = build_planning_context( - world_id="test-world", - player_id="default", - base_path=populated_world, + world_id=populated_world.world_id, + player_id=populated_world.player_id, + base_path=populated_world.base_path, candidates=[("Thin NPC", path)], ) assert "Thin NPC" in context assert "Someone with no inner life" in context - def test_empty_candidates(self, populated_world: Path): + def test_empty_candidates(self, populated_world: ToolContext): save_session( "default", {"location": "Town Square", "body": ""}, - populated_world, + populated_world.base_path, ) context = build_planning_context( - world_id="test-world", - player_id="default", - base_path=populated_world, + world_id=populated_world.world_id, + player_id=populated_world.player_id, + base_path=populated_world.base_path, candidates=[], ) assert "No entities" in context - def test_includes_recent_log_entries(self, populated_world: Path): + def test_includes_recent_log_entries(self, populated_world: ToolContext): save_session( "default", {"location": "Town Square", "body": ""}, - populated_world, + populated_world.base_path, ) - log = CampaignLog("test-world", populated_world) - log.append_entry("Fought three goblins in the clearing", "5 rounds") - log.append_entry("Spotted two lookouts near the old mill", "30 min") + populated_world.campaign_log.append_entry("Fought three goblins in the clearing", "5 rounds") + populated_world.campaign_log.append_entry("Spotted two lookouts near the old mill", "30 min") context = build_planning_context( - world_id="test-world", - player_id="default", - base_path=populated_world, + world_id=populated_world.world_id, + player_id=populated_world.player_id, + base_path=populated_world.base_path, candidates=[], ) assert "Recent Events" in context @@ -310,19 +276,19 @@ class TestBuildPlanningContext: class TestPlanWorld: """Tests for the plan_world orchestrator.""" - def test_dry_run_returns_candidates(self, populated_world: Path): + def test_dry_run_returns_candidates(self, populated_world: ToolContext): save_session( "default", { "location": "Town Square", "body": "## Present\n- [[Thin NPC]]", }, - populated_world, + populated_world.base_path, ) result = plan_world( - world_id="test-world", - player_id="default", - base_path=populated_world, + world_id=populated_world.world_id, + player_id=populated_world.player_id, + base_path=populated_world.base_path, dry_run=True, ) assert isinstance(result, PlanResult) @@ -331,54 +297,54 @@ class TestPlanWorld: names = [name for name, _, _ in result.candidates] assert "Thin NPC" in names - def test_dry_run_respects_threshold(self, populated_world: Path): + def test_dry_run_respects_threshold(self, populated_world: ToolContext): save_session( "default", {"location": "Town Square", "body": ""}, - populated_world, + populated_world.base_path, ) result = plan_world( - world_id="test-world", - player_id="default", - base_path=populated_world, + world_id=populated_world.world_id, + player_id=populated_world.player_id, + base_path=populated_world.base_path, dry_run=True, threshold=0.0, ) assert len(result.candidates) == 0 - def test_dry_run_respects_max_entities(self, populated_world: Path): + def test_dry_run_respects_max_entities(self, populated_world: ToolContext): save_session( "default", {"location": "Town Square", "body": ""}, - populated_world, + populated_world.base_path, ) result = plan_world( - world_id="test-world", - player_id="default", - base_path=populated_world, + world_id=populated_world.world_id, + player_id=populated_world.player_id, + base_path=populated_world.base_path, dry_run=True, max_entities=1, ) assert len(result.candidates) <= 1 - def test_no_session_returns_empty(self, world_base: Path): + def test_no_session_returns_empty(self, ctx: ToolContext): result = plan_world( - world_id="test-world", - player_id="default", - base_path=world_base, + world_id=ctx.world_id, + player_id=ctx.player_id, + base_path=ctx.base_path, dry_run=True, ) assert len(result.candidates) == 0 @patch("storied.claude.subprocess.Popen") - def test_plan_world_calls_claude(self, mock_popen: MagicMock, populated_world: Path): + def test_plan_world_calls_claude(self, mock_popen: MagicMock, populated_world: ToolContext): save_session( "default", { "location": "Town Square", "body": "## Present\n- [[Thin NPC]]", }, - populated_world, + populated_world.base_path, ) # Mock subprocess returning a result event @@ -397,9 +363,9 @@ class TestPlanWorld: mock_popen.return_value = mock_proc result = plan_world( - world_id="test-world", - player_id="default", - base_path=populated_world, + world_id=populated_world.world_id, + player_id=populated_world.player_id, + base_path=populated_world.base_path, model="claude-opus-4-6", ) @@ -409,14 +375,14 @@ class TestPlanWorld: mock_popen.assert_called_once() @patch("storied.claude.subprocess.Popen") - def test_plan_world_counts_tool_calls(self, mock_popen: MagicMock, populated_world: Path): + def test_plan_world_counts_tool_calls(self, mock_popen: MagicMock, populated_world: ToolContext): save_session( "default", { "location": "Town Square", "body": "## Present\n- [[Thin NPC]]", }, - populated_world, + populated_world.base_path, ) # Stream with tool_use events followed by result @@ -446,9 +412,9 @@ class TestPlanWorld: mock_popen.return_value = mock_proc result = plan_world( - world_id="test-world", - player_id="default", - base_path=populated_world, + world_id=populated_world.world_id, + player_id=populated_world.player_id, + base_path=populated_world.base_path, model="claude-opus-4-6", ) diff --git a/tests/test_search.py b/tests/test_search.py new file mode 100644 index 0000000..7539d54 --- /dev/null +++ b/tests/test_search.py @@ -0,0 +1,348 @@ +"""Tests for vector search index.""" + +from pathlib import Path + +import pytest + +from conftest import _fake_embed +from storied.search import SearchHit, VectorIndex, age_decay, chunk_document + + +@pytest.fixture +def index(tmp_path: Path): + """VectorIndex with fake embedder (no model download).""" + db_path = tmp_path / "test.search.db" + idx = VectorIndex(db_path) + idx._embed_fn = _fake_embed + yield idx + idx.close() + + +@pytest.fixture +def srd_tree(tmp_path: Path) -> Path: + """Small SRD-like directory tree for reindex tests.""" + spells = tmp_path / "spells" + spells.mkdir() + (spells / "fireball.md").write_text( + "# Fireball\n\n_Level 3 Evocation (Sorcerer, Wizard)_\n\n" + "Each creature in a 20-foot-radius sphere must make a Dexterity " + "saving throw. A target takes 8d6 fire damage on a failed save.\n" + ) + (spells / "magic-missile.md").write_text( + "# Magic Missile\n\n_Level 1 Evocation (Sorcerer, Wizard)_\n\n" + "You create three glowing darts of magical force. Each dart hits " + "automatically and deals 1d4+1 force damage.\n" + ) + (spells / "cure-wounds.md").write_text( + "# Cure Wounds\n\n_Level 1 Abjuration (Cleric, Druid)_\n\n" + "A creature you touch regains hit points equal to 1d8 + your " + "spellcasting ability modifier.\n" + ) + + monsters = tmp_path / "monsters" + monsters.mkdir() + (monsters / "goblin.md").write_text( + "# Goblin\n\n_Small Humanoid, Neutral Evil_\n\n" + "**AC** 15 (leather armor, shield) **HP** 7 (2d6)\n" + ) + (monsters / "ancient-red-dragon.md").write_text( + "# Ancient Red Dragon\n\n_Gargantuan Dragon, Chaotic Evil_\n\n" + "**AC** 22 (natural armor) **HP** 546 (28d20 + 252)\n\n" + "## Fire Breath\nThe dragon exhales fire in a 90-foot cone.\n" + ) + + return tmp_path + + +@pytest.fixture +def large_doc_content() -> str: + """A document large enough to trigger chunking.""" + sections = ["# Equipment\n\nRules for adventuring gear.\n"] + for i in range(10): + section = f"\n## Section {i}\n\n" + section += f"This is section {i} with enough content to matter. " * 50 + sections.append(section) + return "\n".join(sections) + + +# --- Chunking --- + + +class TestChunkDocument: + def test_small_file_single_chunk(self): + content = "# Fireball\n\nA big explosion spell.\n" + chunks = chunk_document(Path("fireball.md"), content) + assert len(chunks) == 1 + assert chunks[0][0] == 0 + assert "Fireball" in chunks[0][1] + + def test_large_file_splits_on_h2(self, large_doc_content: str): + chunks = chunk_document(Path("equipment.md"), large_doc_content) + assert len(chunks) > 1 + # Each chunk should have the title prepended for context + for _, text in chunks: + assert "Equipment" in text + + def test_chunk_index_sequential(self, large_doc_content: str): + chunks = chunk_document(Path("equipment.md"), large_doc_content) + indices = [idx for idx, _ in chunks] + assert indices == list(range(len(chunks))) + + def test_no_empty_chunks(self, large_doc_content: str): + chunks = chunk_document(Path("equipment.md"), large_doc_content) + for _, text in chunks: + assert text.strip() + + def test_file_with_no_h1_still_works(self): + content = "Just some content without any headers at all.\n" + chunks = chunk_document(Path("notes.md"), content) + assert len(chunks) == 1 + assert "some content" in chunks[0][1] + + +# --- Age Decay --- + + +class TestAgeDecay: + def test_same_day_no_decay(self): + assert age_decay(current_day=5, doc_day=5) == 1.0 + + def test_half_life_halves_score(self): + result = age_decay(current_day=8, doc_day=5, half_life=3) + assert abs(result - 0.5) < 1e-9 + + def test_two_half_lives(self): + result = age_decay(current_day=11, doc_day=5, half_life=3) + assert abs(result - 0.25) < 1e-9 + + def test_future_doc_no_boost(self): + result = age_decay(current_day=3, doc_day=5) + assert result == 1.0 + + def test_custom_half_life(self): + result = age_decay(current_day=10, doc_day=0, half_life=5) + assert abs(result - 0.25) < 1e-9 + + +# --- VectorIndex CRUD --- + + +class TestVectorIndexUpsert: + def test_upsert_and_search(self, index: VectorIndex): + index.upsert( + "srd:spells/fireball.md:0", + "Fireball: 8d6 fire damage in a 20-foot radius", + {"source": "srd", "content_type": "spells", + "path": "/tmp/fireball.md", "title": "Fireball"}, + ) + results = index.search("fireball") + assert len(results) >= 1 + assert results[0].doc_id == "srd:spells/fireball.md:0" + + def test_upsert_overwrites(self, index: VectorIndex): + doc_id = "world:npcs/vex.md:0" + index.upsert(doc_id, "Captain Vex the pirate", + {"source": "world", "content_type": "npcs", + "path": "/tmp/vex.md", "title": "Captain Vex"}) + index.upsert(doc_id, "Captain Vex the reformed merchant", + {"source": "world", "content_type": "npcs", + "path": "/tmp/vex.md", "title": "Captain Vex"}) + stats = index.stats() + assert stats["total_documents"] == 1 + + def test_delete(self, index: VectorIndex): + doc_id = "srd:spells/fireball.md:0" + index.upsert(doc_id, "Fireball spell", + {"source": "srd", "content_type": "spells", + "path": "/tmp/fireball.md", "title": "Fireball"}) + index.delete(doc_id) + stats = index.stats() + assert stats["total_documents"] == 0 + + def test_delete_nonexistent_is_noop(self, index: VectorIndex): + index.delete("nonexistent:doc:0") + + +# --- Search --- + + +class TestVectorIndexSearch: + @pytest.fixture(autouse=True) + def _populate(self, index: VectorIndex): + docs = [ + ("srd:spells/fireball.md:0", + "Fireball: 8d6 fire damage in a 20-foot radius sphere", + {"source": "srd", "content_type": "spells", + "path": "/tmp/spells/fireball.md", "title": "Fireball"}), + ("srd:spells/cure-wounds.md:0", + "Cure Wounds: restore hit points by touch", + {"source": "srd", "content_type": "spells", + "path": "/tmp/spells/cure-wounds.md", "title": "Cure Wounds"}), + ("world:npcs/vex.md:0", + "Captain Vex is a notorious pirate who sails the Shattered Coast", + {"source": "world", "content_type": "npcs", + "path": "/tmp/npcs/vex.md", "title": "Captain Vex"}), + ("transcript:transcripts/day+001.md:0", + "Player asked about the harbor. DM described ships at dock.", + {"source": "transcript", "content_type": "transcripts", + "path": "/tmp/transcripts/day+001.md", "title": "Day 1", + "game_day": 1}), + ] + for doc_id, text, meta in docs: + index.upsert(doc_id, text, meta) + + def test_returns_search_hits(self, index: VectorIndex): + results = index.search("fire damage") + assert len(results) >= 1 + assert all(isinstance(r, SearchHit) for r in results) + + def test_limit(self, index: VectorIndex): + results = index.search("magic", limit=2) + assert len(results) <= 2 + + def test_source_filter(self, index: VectorIndex): + results = index.search("pirate", source_filter="world") + assert all(r.source == "world" for r in results) + + def test_source_filter_excludes(self, index: VectorIndex): + results = index.search("fire", source_filter="world") + assert all(r.source == "world" for r in results) + + def test_empty_results(self, index: VectorIndex): + # With fake embeddings, everything has some similarity, but + # we should still get results (the index isn't empty) + results = index.search("completely unrelated query xyz") + assert isinstance(results, list) + + def test_search_hit_fields(self, index: VectorIndex): + results = index.search("fireball") + hit = results[0] + assert hit.path is not None + assert hit.source in ("srd", "world", "transcript", "player") + assert hit.content_type is not None + assert isinstance(hit.score, float) + + def test_age_decay_applied(self, index: VectorIndex): + # Search with decay_ref far from day 1 transcript + results_no_decay = index.search("harbor ships") + results_decayed = index.search("harbor ships", decay_ref=100) + transcript_no_decay = [r for r in results_no_decay + if r.source == "transcript"] + transcript_decayed = [r for r in results_decayed + if r.source == "transcript"] + if transcript_no_decay and transcript_decayed: + assert transcript_decayed[0].score < transcript_no_decay[0].score + + +# --- Reindex Directory --- + + +class TestReindexDirectory: + def test_reindex_counts(self, index: VectorIndex, srd_tree: Path): + count = index.reindex_directory(srd_tree, source="srd") + assert count == 5 # 3 spells + 2 monsters + + def test_reindex_searchable(self, index: VectorIndex, srd_tree: Path): + index.reindex_directory(srd_tree, source="srd") + results = index.search("goblin") + assert len(results) >= 1 + + def test_reindex_idempotent(self, index: VectorIndex, srd_tree: Path): + index.reindex_directory(srd_tree, source="srd") + index.reindex_directory(srd_tree, source="srd") + stats = index.stats() + assert stats["total_documents"] == 5 + + def test_reindex_updates_changed_files( + self, index: VectorIndex, srd_tree: Path + ): + index.reindex_directory(srd_tree, source="srd") + # Modify a file + fireball = srd_tree / "spells" / "fireball.md" + fireball.write_text("# Fireball\n\nNow deals 10d6 fire damage!\n") + count = index.reindex_directory(srd_tree, source="srd") + # Should still have 5 docs total (updated, not duplicated) + stats = index.stats() + assert stats["total_documents"] == 5 + + +# --- Stats --- + + +class TestStats: + def test_empty_index(self, index: VectorIndex): + stats = index.stats() + assert stats["total_documents"] == 0 + + def test_stats_by_source(self, index: VectorIndex): + index.upsert("srd:a:0", "text a", + {"source": "srd", "content_type": "spells", + "path": "/a", "title": "A"}) + index.upsert("world:b:0", "text b", + {"source": "world", "content_type": "npcs", + "path": "/b", "title": "B"}) + stats = index.stats() + assert stats["total_documents"] == 2 + assert stats["by_source"]["srd"] == 1 + assert stats["by_source"]["world"] == 1 + + +# --- Corrupt / Missing DB --- + + +class TestReindexOnCorrupt: + def test_missing_db_creates_fresh(self, tmp_path: Path): + db_path = tmp_path / "nonexistent" / "test.db" + idx = VectorIndex(db_path) + idx._embed_fn = _fake_embed + idx.upsert("test:a:0", "hello", + {"source": "test", "content_type": "misc", + "path": "/a", "title": "A"}) + assert idx.stats()["total_documents"] == 1 + + def test_corrupt_db_recreates(self, tmp_path: Path): + db_path = tmp_path / "corrupt.db" + db_path.write_bytes(b"this is not a sqlite database") + idx = VectorIndex(db_path) + idx._embed_fn = _fake_embed + idx.upsert("test:a:0", "hello", + {"source": "test", "content_type": "misc", + "path": "/a", "title": "A"}) + assert idx.stats()["total_documents"] == 1 + + +# --- Seed From --- + + +class TestSeedFrom: + def test_seed_copies_documents(self, index: VectorIndex, tmp_path: Path): + index.upsert("srd:spells/fireball.md:0", "Fireball spell", + {"source": "srd", "content_type": "spells", + "path": "/tmp/fireball.md", "title": "Fireball"}) + index.upsert("srd:monsters/goblin.md:0", "Goblin monster", + {"source": "srd", "content_type": "monsters", + "path": "/tmp/goblin.md", "title": "Goblin"}) + index.close() + + dest = tmp_path / "world" / "search.db" + seeded = VectorIndex.seed_from(index._db_path, dest) + seeded._embed_fn = _fake_embed + assert seeded.stats()["total_documents"] == 2 + seeded.close() + + def test_seed_allows_additional_upserts(self, index: VectorIndex, tmp_path: Path): + index.upsert("srd:spells/fireball.md:0", "Fireball spell", + {"source": "srd", "content_type": "spells", + "path": "/tmp/fireball.md", "title": "Fireball"}) + index.close() + + dest = tmp_path / "world" / "search.db" + seeded = VectorIndex.seed_from(index._db_path, dest) + seeded._embed_fn = _fake_embed + seeded.upsert("world:npcs/vex.md:0", "Captain Vex", + {"source": "world", "content_type": "npcs", + "path": "/tmp/vex.md", "title": "Vex"}) + assert seeded.stats()["total_documents"] == 2 + assert seeded.stats()["by_source"]["srd"] == 1 + assert seeded.stats()["by_source"]["world"] == 1 + seeded.close() diff --git a/tests/test_seeder.py b/tests/test_seeder.py index e6ff29b..e5dbd6a 100644 --- a/tests/test_seeder.py +++ b/tests/test_seeder.py @@ -11,6 +11,7 @@ from storied.planner import SeedResult, seed_world from storied.tools import ( SEEDER_TOOL_DEFINITIONS, SEEDER_TOOLS, + ToolContext, seeder_execute_tool, ) @@ -25,28 +26,22 @@ class TestSeederTools: names = {t["name"] for t in SEEDER_TOOL_DEFINITIONS} assert names == SEEDER_TOOLS - def test_seeder_rejects_disallowed_tools(self, tmp_path: Path): + def test_seeder_rejects_disallowed_tools(self, ctx: ToolContext): for tool_name in ["roll", "recall", "mark", "mark_time", "note_discovery", "end_session"]: - result = seeder_execute_tool( - tool_name, - {}, - world_id="test", - base_path=tmp_path, - ) + result = seeder_execute_tool(tool_name, {}, ctx) assert "not available to seeder" in result - def test_seeder_allows_establish(self, tmp_path: Path): - (tmp_path / "worlds" / "test" / "npcs").mkdir(parents=True) + def test_seeder_allows_establish(self, ctx: ToolContext): + (ctx.base_path / "worlds" / ctx.world_id / "npcs").mkdir(parents=True, exist_ok=True) result = seeder_execute_tool( "establish", {"entity_type": "npcs", "name": "Test NPC", "description": "A test."}, - world_id="test", - base_path=tmp_path, + ctx, ) assert "Established" in result - def test_seeder_allows_set_scene(self, tmp_path: Path): - (tmp_path / "players" / "default").mkdir(parents=True) + def test_seeder_allows_set_scene(self, ctx: ToolContext): + (ctx.base_path / "players" / ctx.player_id).mkdir(parents=True) result = seeder_execute_tool( "set_scene", { @@ -54,9 +49,7 @@ class TestSeederTools: "duration": "0 min", "situation": "Walking through the forest.", }, - world_id="test", - player_id="default", - base_path=tmp_path, + ctx, ) assert "Logged" in result assert "Session updated" in result diff --git a/uv.lock b/uv.lock index a777b15..f7d0a32 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,20 @@ version = 1 revision = 2 requires-python = ">=3.12" +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version == '3.13.*'", + "python_full_version < '3.13'", +] + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] [[package]] name = "annotated-types" @@ -108,6 +122,79 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, ] +[[package]] +name = "charset-normalizer" +version = "3.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/60/e3bec1881450851b087e301bedc3daa9377a4d45f1c26aa90b0b235e38aa/charset_normalizer-3.4.6.tar.gz", hash = "sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6", size = 143363, upload-time = "2026-03-15T18:53:25.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/62/c0815c992c9545347aeea7859b50dc9044d147e2e7278329c6e02ac9a616/charset_normalizer-3.4.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2ef7fedc7a6ecbe99969cd09632516738a97eeb8bd7258bf8a0f23114c057dab", size = 295154, upload-time = "2026-03-15T18:50:50.88Z" }, + { url = "https://files.pythonhosted.org/packages/a8/37/bdca6613c2e3c58c7421891d80cc3efa1d32e882f7c4a7ee6039c3fc951a/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4ea868bc28109052790eb2b52a9ab33f3aa7adc02f96673526ff47419490e21", size = 199191, upload-time = "2026-03-15T18:50:52.658Z" }, + { url = "https://files.pythonhosted.org/packages/6c/92/9934d1bbd69f7f398b38c5dae1cbf9cc672e7c34a4adf7b17c0a9c17d15d/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:836ab36280f21fc1a03c99cd05c6b7af70d2697e374c7af0b61ed271401a72a2", size = 218674, upload-time = "2026-03-15T18:50:54.102Z" }, + { url = "https://files.pythonhosted.org/packages/af/90/25f6ab406659286be929fd89ab0e78e38aa183fc374e03aa3c12d730af8a/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f1ce721c8a7dfec21fcbdfe04e8f68174183cf4e8188e0645e92aa23985c57ff", size = 215259, upload-time = "2026-03-15T18:50:55.616Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ef/79a463eb0fff7f96afa04c1d4c51f8fc85426f918db467854bfb6a569ce3/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e28d62a8fc7a1fa411c43bd65e346f3bce9716dc51b897fbe930c5987b402d5", size = 207276, upload-time = "2026-03-15T18:50:57.054Z" }, + { url = "https://files.pythonhosted.org/packages/f7/72/d0426afec4b71dc159fa6b4e68f868cd5a3ecd918fec5813a15d292a7d10/charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:530d548084c4a9f7a16ed4a294d459b4f229db50df689bfe92027452452943a0", size = 195161, upload-time = "2026-03-15T18:50:58.686Z" }, + { url = "https://files.pythonhosted.org/packages/bf/18/c82b06a68bfcb6ce55e508225d210c7e6a4ea122bfc0748892f3dc4e8e11/charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:30f445ae60aad5e1f8bdbb3108e39f6fbc09f4ea16c815c66578878325f8f15a", size = 203452, upload-time = "2026-03-15T18:51:00.196Z" }, + { url = "https://files.pythonhosted.org/packages/44/d6/0c25979b92f8adafdbb946160348d8d44aa60ce99afdc27df524379875cb/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ac2393c73378fea4e52aa56285a3d64be50f1a12395afef9cce47772f60334c2", size = 202272, upload-time = "2026-03-15T18:51:01.703Z" }, + { url = "https://files.pythonhosted.org/packages/2e/3d/7fea3e8fe84136bebbac715dd1221cc25c173c57a699c030ab9b8900cbb7/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:90ca27cd8da8118b18a52d5f547859cc1f8354a00cd1e8e5120df3e30d6279e5", size = 195622, upload-time = "2026-03-15T18:51:03.526Z" }, + { url = "https://files.pythonhosted.org/packages/57/8a/d6f7fd5cb96c58ef2f681424fbca01264461336d2a7fc875e4446b1f1346/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e5a94886bedca0f9b78fecd6afb6629142fd2605aa70a125d49f4edc6037ee6", size = 220056, upload-time = "2026-03-15T18:51:05.269Z" }, + { url = "https://files.pythonhosted.org/packages/16/50/478cdda782c8c9c3fb5da3cc72dd7f331f031e7f1363a893cdd6ca0f8de0/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:695f5c2823691a25f17bc5d5ffe79fa90972cc34b002ac6c843bb8a1720e950d", size = 203751, upload-time = "2026-03-15T18:51:06.858Z" }, + { url = "https://files.pythonhosted.org/packages/75/fc/cc2fcac943939c8e4d8791abfa139f685e5150cae9f94b60f12520feaa9b/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:231d4da14bcd9301310faf492051bee27df11f2bc7549bc0bb41fef11b82daa2", size = 216563, upload-time = "2026-03-15T18:51:08.564Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b7/a4add1d9a5f68f3d037261aecca83abdb0ab15960a3591d340e829b37298/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a056d1ad2633548ca18ffa2f85c202cfb48b68615129143915b8dc72a806a923", size = 209265, upload-time = "2026-03-15T18:51:10.312Z" }, + { url = "https://files.pythonhosted.org/packages/6c/18/c094561b5d64a24277707698e54b7f67bd17a4f857bbfbb1072bba07c8bf/charset_normalizer-3.4.6-cp312-cp312-win32.whl", hash = "sha256:c2274ca724536f173122f36c98ce188fd24ce3dad886ec2b7af859518ce008a4", size = 144229, upload-time = "2026-03-15T18:51:11.694Z" }, + { url = "https://files.pythonhosted.org/packages/ab/20/0567efb3a8fd481b8f34f739ebddc098ed062a59fed41a8d193a61939e8f/charset_normalizer-3.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:c8ae56368f8cc97c7e40a7ee18e1cedaf8e780cd8bc5ed5ac8b81f238614facb", size = 154277, upload-time = "2026-03-15T18:51:13.004Z" }, + { url = "https://files.pythonhosted.org/packages/15/57/28d79b44b51933119e21f65479d0864a8d5893e494cf5daab15df0247c17/charset_normalizer-3.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:899d28f422116b08be5118ef350c292b36fc15ec2daeb9ea987c89281c7bb5c4", size = 142817, upload-time = "2026-03-15T18:51:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/1e/1d/4fdabeef4e231153b6ed7567602f3b68265ec4e5b76d6024cf647d43d981/charset_normalizer-3.4.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f", size = 294823, upload-time = "2026-03-15T18:51:15.755Z" }, + { url = "https://files.pythonhosted.org/packages/47/7b/20e809b89c69d37be748d98e84dce6820bf663cf19cf6b942c951a3e8f41/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843", size = 198527, upload-time = "2026-03-15T18:51:17.177Z" }, + { url = "https://files.pythonhosted.org/packages/37/a6/4f8d27527d59c039dce6f7622593cdcd3d70a8504d87d09eb11e9fdc6062/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d73beaac5e90173ac3deb9928a74763a6d230f494e4bfb422c217a0ad8e629bf", size = 218388, upload-time = "2026-03-15T18:51:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/f6/9b/4770ccb3e491a9bacf1c46cc8b812214fe367c86a96353ccc6daf87b01ec/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d60377dce4511655582e300dc1e5a5f24ba0cb229005a1d5c8d0cb72bb758ab8", size = 214563, upload-time = "2026-03-15T18:51:20.374Z" }, + { url = "https://files.pythonhosted.org/packages/2b/58/a199d245894b12db0b957d627516c78e055adc3a0d978bc7f65ddaf7c399/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9", size = 206587, upload-time = "2026-03-15T18:51:21.807Z" }, + { url = "https://files.pythonhosted.org/packages/7e/70/3def227f1ec56f5c69dfc8392b8bd63b11a18ca8178d9211d7cc5e5e4f27/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:a26611d9987b230566f24a0a125f17fe0de6a6aff9f25c9f564aaa2721a5fb88", size = 194724, upload-time = "2026-03-15T18:51:23.508Z" }, + { url = "https://files.pythonhosted.org/packages/58/ab/9318352e220c05efd31c2779a23b50969dc94b985a2efa643ed9077bfca5/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34315ff4fc374b285ad7f4a0bf7dcbfe769e1b104230d40f49f700d4ab6bbd84", size = 202956, upload-time = "2026-03-15T18:51:25.239Z" }, + { url = "https://files.pythonhosted.org/packages/75/13/f3550a3ac25b70f87ac98c40d3199a8503676c2f1620efbf8d42095cfc40/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ddd609f9e1af8c7bd6e2aca279c931aefecd148a14402d4e368f3171769fd", size = 201923, upload-time = "2026-03-15T18:51:26.682Z" }, + { url = "https://files.pythonhosted.org/packages/1b/db/c5c643b912740b45e8eec21de1bbab8e7fc085944d37e1e709d3dcd9d72f/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:80d0a5615143c0b3225e5e3ef22c8d5d51f3f72ce0ea6fb84c943546c7b25b6c", size = 195366, upload-time = "2026-03-15T18:51:28.129Z" }, + { url = "https://files.pythonhosted.org/packages/5a/67/3b1c62744f9b2448443e0eb160d8b001c849ec3fef591e012eda6484787c/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:92734d4d8d187a354a556626c221cd1a892a4e0802ccb2af432a1d85ec012194", size = 219752, upload-time = "2026-03-15T18:51:29.556Z" }, + { url = "https://files.pythonhosted.org/packages/f6/98/32ffbaf7f0366ffb0445930b87d103f6b406bc2c271563644bde8a2b1093/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:613f19aa6e082cf96e17e3ffd89383343d0d589abda756b7764cf78361fd41dc", size = 203296, upload-time = "2026-03-15T18:51:30.921Z" }, + { url = "https://files.pythonhosted.org/packages/41/12/5d308c1bbe60cabb0c5ef511574a647067e2a1f631bc8634fcafaccd8293/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2b1a63e8224e401cafe7739f77efd3f9e7f5f2026bda4aead8e59afab537784f", size = 215956, upload-time = "2026-03-15T18:51:32.399Z" }, + { url = "https://files.pythonhosted.org/packages/53/e9/5f85f6c5e20669dbe56b165c67b0260547dea97dba7e187938833d791687/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6cceb5473417d28edd20c6c984ab6fee6c6267d38d906823ebfe20b03d607dc2", size = 208652, upload-time = "2026-03-15T18:51:34.214Z" }, + { url = "https://files.pythonhosted.org/packages/f1/11/897052ea6af56df3eef3ca94edafee410ca699ca0c7b87960ad19932c55e/charset_normalizer-3.4.6-cp313-cp313-win32.whl", hash = "sha256:d7de2637729c67d67cf87614b566626057e95c303bc0a55ffe391f5205e7003d", size = 143940, upload-time = "2026-03-15T18:51:36.15Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5c/724b6b363603e419829f561c854b87ed7c7e31231a7908708ac086cdf3e2/charset_normalizer-3.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:572d7c822caf521f0525ba1bce1a622a0b85cf47ffbdae6c9c19e3b5ac3c4389", size = 154101, upload-time = "2026-03-15T18:51:37.876Z" }, + { url = "https://files.pythonhosted.org/packages/01/a5/7abf15b4c0968e47020f9ca0935fb3274deb87cb288cd187cad92e8cdffd/charset_normalizer-3.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a4474d924a47185a06411e0064b803c68be044be2d60e50e8bddcc2649957c1f", size = 143109, upload-time = "2026-03-15T18:51:39.565Z" }, + { url = "https://files.pythonhosted.org/packages/25/6f/ffe1e1259f384594063ea1869bfb6be5cdb8bc81020fc36c3636bc8302a1/charset_normalizer-3.4.6-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9cc6e6d9e571d2f863fa77700701dae73ed5f78881efc8b3f9a4398772ff53e8", size = 294458, upload-time = "2026-03-15T18:51:41.134Z" }, + { url = "https://files.pythonhosted.org/packages/56/60/09bb6c13a8c1016c2ed5c6a6488e4ffef506461aa5161662bd7636936fb1/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef5960d965e67165d75b7c7ffc60a83ec5abfc5c11b764ec13ea54fbef8b4421", size = 199277, upload-time = "2026-03-15T18:51:42.953Z" }, + { url = "https://files.pythonhosted.org/packages/00/50/dcfbb72a5138bbefdc3332e8d81a23494bf67998b4b100703fd15fa52d81/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b3694e3f87f8ac7ce279d4355645b3c878d24d1424581b46282f24b92f5a4ae2", size = 218758, upload-time = "2026-03-15T18:51:44.339Z" }, + { url = "https://files.pythonhosted.org/packages/03/b3/d79a9a191bb75f5aa81f3aaaa387ef29ce7cb7a9e5074ba8ea095cc073c2/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5d11595abf8dd942a77883a39d81433739b287b6aa71620f15164f8096221b30", size = 215299, upload-time = "2026-03-15T18:51:45.871Z" }, + { url = "https://files.pythonhosted.org/packages/76/7e/bc8911719f7084f72fd545f647601ea3532363927f807d296a8c88a62c0d/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7bda6eebafd42133efdca535b04ccb338ab29467b3f7bf79569883676fc628db", size = 206811, upload-time = "2026-03-15T18:51:47.308Z" }, + { url = "https://files.pythonhosted.org/packages/e2/40/c430b969d41dda0c465aa36cc7c2c068afb67177bef50905ac371b28ccc7/charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:bbc8c8650c6e51041ad1be191742b8b421d05bbd3410f43fa2a00c8db87678e8", size = 193706, upload-time = "2026-03-15T18:51:48.849Z" }, + { url = "https://files.pythonhosted.org/packages/48/15/e35e0590af254f7df984de1323640ef375df5761f615b6225ba8deb9799a/charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22c6f0c2fbc31e76c3b8a86fba1a56eda6166e238c29cdd3d14befdb4a4e4815", size = 202706, upload-time = "2026-03-15T18:51:50.257Z" }, + { url = "https://files.pythonhosted.org/packages/5e/bd/f736f7b9cc5e93a18b794a50346bb16fbfd6b37f99e8f306f7951d27c17c/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7edbed096e4a4798710ed6bc75dcaa2a21b68b6c356553ac4823c3658d53743a", size = 202497, upload-time = "2026-03-15T18:51:52.012Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ba/2cc9e3e7dfdf7760a6ed8da7446d22536f3d0ce114ac63dee2a5a3599e62/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7f9019c9cb613f084481bd6a100b12e1547cf2efe362d873c2e31e4035a6fa43", size = 193511, upload-time = "2026-03-15T18:51:53.723Z" }, + { url = "https://files.pythonhosted.org/packages/9e/cb/5be49b5f776e5613be07298c80e1b02a2d900f7a7de807230595c85a8b2e/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:58c948d0d086229efc484fe2f30c2d382c86720f55cd9bc33591774348ad44e0", size = 220133, upload-time = "2026-03-15T18:51:55.333Z" }, + { url = "https://files.pythonhosted.org/packages/83/43/99f1b5dad345accb322c80c7821071554f791a95ee50c1c90041c157ae99/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:419a9d91bd238052642a51938af8ac05da5b3343becde08d5cdeab9046df9ee1", size = 203035, upload-time = "2026-03-15T18:51:56.736Z" }, + { url = "https://files.pythonhosted.org/packages/87/9a/62c2cb6a531483b55dddff1a68b3d891a8b498f3ca555fbcf2978e804d9d/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5273b9f0b5835ff0350c0828faea623c68bfa65b792720c453e22b25cc72930f", size = 216321, upload-time = "2026-03-15T18:51:58.17Z" }, + { url = "https://files.pythonhosted.org/packages/6e/79/94a010ff81e3aec7c293eb82c28f930918e517bc144c9906a060844462eb/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0e901eb1049fdb80f5bd11ed5ea1e498ec423102f7a9b9e4645d5b8204ff2815", size = 208973, upload-time = "2026-03-15T18:51:59.998Z" }, + { url = "https://files.pythonhosted.org/packages/2a/57/4ecff6d4ec8585342f0c71bc03efaa99cb7468f7c91a57b105bcd561cea8/charset_normalizer-3.4.6-cp314-cp314-win32.whl", hash = "sha256:b4ff1d35e8c5bd078be89349b6f3a845128e685e751b6ea1169cf2160b344c4d", size = 144610, upload-time = "2026-03-15T18:52:02.213Z" }, + { url = "https://files.pythonhosted.org/packages/80/94/8434a02d9d7f168c25767c64671fead8d599744a05d6a6c877144c754246/charset_normalizer-3.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:74119174722c4349af9708993118581686f343adc1c8c9c007d59be90d077f3f", size = 154962, upload-time = "2026-03-15T18:52:03.658Z" }, + { url = "https://files.pythonhosted.org/packages/46/4c/48f2cdbfd923026503dfd67ccea45c94fd8fe988d9056b468579c66ed62b/charset_normalizer-3.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:e5bcc1a1ae744e0bb59641171ae53743760130600da8db48cbb6e4918e186e4e", size = 143595, upload-time = "2026-03-15T18:52:05.123Z" }, + { url = "https://files.pythonhosted.org/packages/31/93/8878be7569f87b14f1d52032946131bcb6ebbd8af3e20446bc04053dc3f1/charset_normalizer-3.4.6-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ad8faf8df23f0378c6d527d8b0b15ea4a2e23c89376877c598c4870d1b2c7866", size = 314828, upload-time = "2026-03-15T18:52:06.831Z" }, + { url = "https://files.pythonhosted.org/packages/06/b6/fae511ca98aac69ecc35cde828b0a3d146325dd03d99655ad38fc2cc3293/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5ea69428fa1b49573eef0cc44a1d43bebd45ad0c611eb7d7eac760c7ae771bc", size = 208138, upload-time = "2026-03-15T18:52:08.239Z" }, + { url = "https://files.pythonhosted.org/packages/54/57/64caf6e1bf07274a1e0b7c160a55ee9e8c9ec32c46846ce59b9c333f7008/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:06a7e86163334edfc5d20fe104db92fcd666e5a5df0977cb5680a506fe26cc8e", size = 224679, upload-time = "2026-03-15T18:52:10.043Z" }, + { url = "https://files.pythonhosted.org/packages/aa/cb/9ff5a25b9273ef160861b41f6937f86fae18b0792fe0a8e75e06acb08f1d/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e1f6e2f00a6b8edb562826e4632e26d063ac10307e80f7461f7de3ad8ef3f077", size = 223475, upload-time = "2026-03-15T18:52:11.854Z" }, + { url = "https://files.pythonhosted.org/packages/fc/97/440635fc093b8d7347502a377031f9605a1039c958f3cd18dcacffb37743/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b52c68d64c1878818687a473a10547b3292e82b6f6fe483808fb1468e2f52f", size = 215230, upload-time = "2026-03-15T18:52:13.325Z" }, + { url = "https://files.pythonhosted.org/packages/cd/24/afff630feb571a13f07c8539fbb502d2ab494019492aaffc78ef41f1d1d0/charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:7504e9b7dc05f99a9bbb4525c67a2c155073b44d720470a148b34166a69c054e", size = 199045, upload-time = "2026-03-15T18:52:14.752Z" }, + { url = "https://files.pythonhosted.org/packages/e5/17/d1399ecdaf7e0498c327433e7eefdd862b41236a7e484355b8e0e5ebd64b/charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:172985e4ff804a7ad08eebec0a1640ece87ba5041d565fff23c8f99c1f389484", size = 211658, upload-time = "2026-03-15T18:52:16.278Z" }, + { url = "https://files.pythonhosted.org/packages/b5/38/16baa0affb957b3d880e5ac2144caf3f9d7de7bc4a91842e447fbb5e8b67/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4be9f4830ba8741527693848403e2c457c16e499100963ec711b1c6f2049b7c7", size = 210769, upload-time = "2026-03-15T18:52:17.782Z" }, + { url = "https://files.pythonhosted.org/packages/05/34/c531bc6ac4c21da9ddfddb3107be2287188b3ea4b53b70fc58f2a77ac8d8/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:79090741d842f564b1b2827c0b82d846405b744d31e84f18d7a7b41c20e473ff", size = 201328, upload-time = "2026-03-15T18:52:19.553Z" }, + { url = "https://files.pythonhosted.org/packages/fa/73/a5a1e9ca5f234519c1953608a03fe109c306b97fdfb25f09182babad51a7/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:87725cfb1a4f1f8c2fc9890ae2f42094120f4b44db9360be5d99a4c6b0e03a9e", size = 225302, upload-time = "2026-03-15T18:52:21.043Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f6/cd782923d112d296294dea4bcc7af5a7ae0f86ab79f8fefbda5526b6cfc0/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fcce033e4021347d80ed9c66dcf1e7b1546319834b74445f561d2e2221de5659", size = 211127, upload-time = "2026-03-15T18:52:22.491Z" }, + { url = "https://files.pythonhosted.org/packages/0e/c5/0b6898950627af7d6103a449b22320372c24c6feda91aa24e201a478d161/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ca0276464d148c72defa8bb4390cce01b4a0e425f3b50d1435aa6d7a18107602", size = 222840, upload-time = "2026-03-15T18:52:24.113Z" }, + { url = "https://files.pythonhosted.org/packages/7d/25/c4bba773bef442cbdc06111d40daa3de5050a676fa26e85090fc54dd12f0/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:197c1a244a274bb016dd8b79204850144ef77fe81c5b797dc389327adb552407", size = 216890, upload-time = "2026-03-15T18:52:25.541Z" }, + { url = "https://files.pythonhosted.org/packages/35/1a/05dacadb0978da72ee287b0143097db12f2e7e8d3ffc4647da07a383b0b7/charset_normalizer-3.4.6-cp314-cp314t-win32.whl", hash = "sha256:2a24157fa36980478dd1770b585c0f30d19e18f4fb0c47c13aa568f871718579", size = 155379, upload-time = "2026-03-15T18:52:27.05Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7a/d269d834cb3a76291651256f3b9a5945e81d0a49ab9f4a498964e83c0416/charset_normalizer-3.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:cd5e2801c89992ed8c0a3f0293ae83c159a60d9a5d685005383ef4caca77f2c4", size = 169043, upload-time = "2026-03-15T18:52:28.502Z" }, + { url = "https://files.pythonhosted.org/packages/23/06/28b29fba521a37a8932c6a84192175c34d49f84a6d4773fa63d05f9aff22/charset_normalizer-3.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:47955475ac79cc504ef2704b192364e51d0d473ad452caedd0002605f780101c", size = 148523, upload-time = "2026-03-15T18:52:29.956Z" }, + { url = "https://files.pythonhosted.org/packages/2a/68/687187c7e26cb24ccbd88e5069f5ef00eba804d36dde11d99aad0838ab45/charset_normalizer-3.4.6-py3-none-any.whl", hash = "sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69", size = 61455, upload-time = "2026-03-15T18:53:23.833Z" }, +] + [[package]] name = "click" version = "8.3.1" @@ -256,6 +343,53 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/48/ef/0c2f4a8e31018a986949d34a01115dd057bf536905dca38897bacd21fac3/cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", size = 3467050, upload-time = "2026-02-10T19:18:18.899Z" }, ] +[[package]] +name = "fastembed" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "loguru" }, + { name = "mmh3" }, + { name = "numpy" }, + { name = "onnxruntime" }, + { name = "pillow" }, + { name = "py-rust-stemmers" }, + { name = "requests" }, + { name = "tokenizers" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/25/58865e36b6e8a9a0d0ff905b5601aa30db97956327c0df42ec4ed6accc21/fastembed-0.8.0.tar.gz", hash = "sha256:75966edfa8b006ee78514c726bd7f6a50721dadc89305279052be9db72fd53e8", size = 75115, upload-time = "2026-03-23T16:34:41.699Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/e8/26b7d78bb8972498c467ca34cb12ee2e60d26ba5eae6d8443189a1af37a5/fastembed-0.8.0-py3-none-any.whl", hash = "sha256:40bee672657574a1009e35ec50030a55f2b426842cb011845379817641bbbbd0", size = 116572, upload-time = "2026-03-23T16:34:40.69Z" }, +] + +[[package]] +name = "filelock" +version = "3.25.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/b8/00651a0f559862f3bb7d6f7477b192afe3f583cc5e26403b44e59a55ab34/filelock-3.25.2.tar.gz", hash = "sha256:b64ece2b38f4ca29dd3e810287aa8c48182bbecd1ae6e9ae126c9b35f1382694", size = 40480, upload-time = "2026-03-11T20:45:38.487Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl", hash = "sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70", size = 26759, upload-time = "2026-03-11T20:45:37.437Z" }, +] + +[[package]] +name = "flatbuffers" +version = "25.12.19" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl", hash = "sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4", size = 26661, upload-time = "2025-12-19T23:16:13.622Z" }, +] + +[[package]] +name = "fsspec" +version = "2026.3.0" +source = { registry = "https://pypi.org/simple" } +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/1f/5f4a3cd9e4440e9d9bc78ad0a91a1c8d46b4d429d5239ebe6793c9fe5c41/fsspec-2026.3.0-py3-none-any.whl", hash = "sha256:d2ceafaad1b3457968ed14efa28798162f1638dbb5d2a6868a2db002a5ee39a4", size = 202595, upload-time = "2026-03-27T19:11:13.595Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -265,6 +399,38 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, ] +[[package]] +name = "hf-xet" +version = "1.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/09/08/23c84a26716382c89151b5b447b4beb19e3345f3a93d3b73009a71a57ad3/hf_xet-1.4.2.tar.gz", hash = "sha256:b7457b6b482d9e0743bd116363239b1fa904a5e65deede350fbc0c4ea67c71ea", size = 672357, upload-time = "2026-03-13T06:58:51.077Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/06/e8cf74c3c48e5485c7acc5a990d0d8516cdfb5fdf80f799174f1287cc1b5/hf_xet-1.4.2-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:ac8202ae1e664b2c15cdfc7298cbb25e80301ae596d602ef7870099a126fcad4", size = 3796125, upload-time = "2026-03-13T06:58:33.177Z" }, + { url = "https://files.pythonhosted.org/packages/66/d4/b73ebab01cbf60777323b7de9ef05550790451eb5172a220d6b9845385ec/hf_xet-1.4.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6d2f8ee39fa9fba9af929f8c0d0482f8ee6e209179ad14a909b6ad78ffcb7c81", size = 3555985, upload-time = "2026-03-13T06:58:31.797Z" }, + { url = "https://files.pythonhosted.org/packages/ff/e7/ded6d1bd041c3f2bca9e913a0091adfe32371988e047dd3a68a2463c15a2/hf_xet-1.4.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4642a6cf249c09da8c1f87fe50b24b2a3450b235bf8adb55700b52f0ea6e2eb6", size = 4212085, upload-time = "2026-03-13T06:58:24.323Z" }, + { url = "https://files.pythonhosted.org/packages/97/c1/a0a44d1f98934f7bdf17f7a915b934f9fca44bb826628c553589900f6df8/hf_xet-1.4.2-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:769431385e746c92dc05492dde6f687d304584b89c33d79def8367ace06cb555", size = 3988266, upload-time = "2026-03-13T06:58:22.887Z" }, + { url = "https://files.pythonhosted.org/packages/7a/82/be713b439060e7d1f1d93543c8053d4ef2fe7e6922c5b31642eaa26f3c4b/hf_xet-1.4.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c9dd1c1bc4cc56168f81939b0e05b4c36dd2d28c13dc1364b17af89aa0082496", size = 4188513, upload-time = "2026-03-13T06:58:40.858Z" }, + { url = "https://files.pythonhosted.org/packages/21/a6/cbd4188b22abd80ebd0edbb2b3e87f2633e958983519980815fb8314eae5/hf_xet-1.4.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:fca58a2ae4e6f6755cc971ac6fcdf777ea9284d7e540e350bb000813b9a3008d", size = 4428287, upload-time = "2026-03-13T06:58:42.601Z" }, + { url = "https://files.pythonhosted.org/packages/b2/4e/84e45b25e2e3e903ed3db68d7eafa96dae9a1d1f6d0e7fc85120347a852f/hf_xet-1.4.2-cp313-cp313t-win_amd64.whl", hash = "sha256:163aab46854ccae0ab6a786f8edecbbfbaa38fcaa0184db6feceebf7000c93c0", size = 3665574, upload-time = "2026-03-13T06:58:53.881Z" }, + { url = "https://files.pythonhosted.org/packages/ee/71/c5ac2b9a7ae39c14e91973035286e73911c31980fe44e7b1d03730c00adc/hf_xet-1.4.2-cp313-cp313t-win_arm64.whl", hash = "sha256:09b138422ecbe50fd0c84d4da5ff537d27d487d3607183cd10e3e53f05188e82", size = 3528760, upload-time = "2026-03-13T06:58:52.187Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0f/fcd2504015eab26358d8f0f232a1aed6b8d363a011adef83fe130bff88f7/hf_xet-1.4.2-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:949dcf88b484bb9d9276ca83f6599e4aa03d493c08fc168c124ad10b2e6f75d7", size = 3796493, upload-time = "2026-03-13T06:58:39.267Z" }, + { url = "https://files.pythonhosted.org/packages/82/56/19c25105ff81731ca6d55a188b5de2aa99d7a2644c7aa9de1810d5d3b726/hf_xet-1.4.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:41659966020d59eb9559c57de2cde8128b706a26a64c60f0531fa2318f409418", size = 3555797, upload-time = "2026-03-13T06:58:37.546Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/8933c073186849b5e06762aa89847991d913d10a95d1603eb7f2c3834086/hf_xet-1.4.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c588e21d80010119458dd5d02a69093f0d115d84e3467efe71ffb2c67c19146", size = 4212127, upload-time = "2026-03-13T06:58:30.539Z" }, + { url = "https://files.pythonhosted.org/packages/eb/01/f89ebba4e369b4ed699dcb60d3152753870996f41c6d22d3d7cac01310e1/hf_xet-1.4.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a296744d771a8621ad1d50c098d7ab975d599800dae6d48528ba3944e5001ba0", size = 3987788, upload-time = "2026-03-13T06:58:29.139Z" }, + { url = "https://files.pythonhosted.org/packages/84/4d/8a53e5ffbc2cc33bbf755382ac1552c6d9af13f623ed125fe67cc3e6772f/hf_xet-1.4.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f563f7efe49588b7d0629d18d36f46d1658fe7e08dce3fa3d6526e1c98315e2d", size = 4188315, upload-time = "2026-03-13T06:58:48.017Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b8/b7a1c1b5592254bd67050632ebbc1b42cc48588bf4757cb03c2ef87e704a/hf_xet-1.4.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5b2e0132c56d7ee1bf55bdb638c4b62e7106f6ac74f0b786fed499d5548c5570", size = 4428306, upload-time = "2026-03-13T06:58:49.502Z" }, + { url = "https://files.pythonhosted.org/packages/a0/0c/40779e45b20e11c7c5821a94135e0207080d6b3d76e7b78ccb413c6f839b/hf_xet-1.4.2-cp314-cp314t-win_amd64.whl", hash = "sha256:2f45c712c2fa1215713db10df6ac84b49d0e1c393465440e9cb1de73ecf7bbf6", size = 3665826, upload-time = "2026-03-13T06:58:59.88Z" }, + { url = "https://files.pythonhosted.org/packages/51/4c/e2688c8ad1760d7c30f7c429c79f35f825932581bc7c9ec811436d2f21a0/hf_xet-1.4.2-cp314-cp314t-win_arm64.whl", hash = "sha256:6d53df40616f7168abfccff100d232e9d460583b9d86fa4912c24845f192f2b8", size = 3529113, upload-time = "2026-03-13T06:58:58.491Z" }, + { url = "https://files.pythonhosted.org/packages/b4/86/b40b83a2ff03ef05c4478d2672b1fc2b9683ff870e2b25f4f3af240f2e7b/hf_xet-1.4.2-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:71f02d6e4cdd07f344f6844845d78518cc7186bd2bc52d37c3b73dc26a3b0bc5", size = 3800339, upload-time = "2026-03-13T06:58:36.245Z" }, + { url = "https://files.pythonhosted.org/packages/64/2e/af4475c32b4378b0e92a587adb1aa3ec53e3450fd3e5fe0372a874531c00/hf_xet-1.4.2-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:e9b38d876e94d4bdcf650778d6ebbaa791dd28de08db9736c43faff06ede1b5a", size = 3559664, upload-time = "2026-03-13T06:58:34.787Z" }, + { url = "https://files.pythonhosted.org/packages/3c/4c/781267da3188db679e601de18112021a5cb16506fe86b246e22c5401a9c4/hf_xet-1.4.2-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:77e8c180b7ef12d8a96739a4e1e558847002afe9ea63b6f6358b2271a8bdda1c", size = 4217422, upload-time = "2026-03-13T06:58:27.472Z" }, + { url = "https://files.pythonhosted.org/packages/68/47/d6cf4a39ecf6c7705f887a46f6ef5c8455b44ad9eb0d391aa7e8a2ff7fea/hf_xet-1.4.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c3b3c6a882016b94b6c210957502ff7877802d0dbda8ad142c8595db8b944271", size = 3992847, upload-time = "2026-03-13T06:58:25.989Z" }, + { url = "https://files.pythonhosted.org/packages/2d/ef/e80815061abff54697239803948abc665c6b1d237102c174f4f7a9a5ffc5/hf_xet-1.4.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9d9a634cc929cfbaf2e1a50c0e532ae8c78fa98618426769480c58501e8c8ac2", size = 4193843, upload-time = "2026-03-13T06:58:44.59Z" }, + { url = "https://files.pythonhosted.org/packages/54/75/07f6aa680575d9646c4167db6407c41340cbe2357f5654c4e72a1b01ca14/hf_xet-1.4.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6b0932eb8b10317ea78b7da6bab172b17be03bbcd7809383d8d5abd6a2233e04", size = 4432751, upload-time = "2026-03-13T06:58:46.533Z" }, + { url = "https://files.pythonhosted.org/packages/cd/71/193eabd7e7d4b903c4aa983a215509c6114915a5a237525ec562baddb868/hf_xet-1.4.2-cp37-abi3-win_amd64.whl", hash = "sha256:ad185719fb2e8ac26f88c8100562dbf9dbdcc3d9d2add00faa94b5f106aea53f", size = 3671149, upload-time = "2026-03-13T06:58:57.07Z" }, + { url = "https://files.pythonhosted.org/packages/b4/7e/ccf239da366b37ba7f0b36095450efae4a64980bdc7ec2f51354205fdf39/hf_xet-1.4.2-cp37-abi3-win_arm64.whl", hash = "sha256:32c012286b581f783653e718c1862aea5b9eb140631685bb0c5e7012c8719a87", size = 3533426, upload-time = "2026-03-13T06:58:55.46Z" }, +] + [[package]] name = "httpcore" version = "1.0.9" @@ -302,6 +468,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, ] +[[package]] +name = "huggingface-hub" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "httpx" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "tqdm" }, + { name = "typer" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8e/2a/a847fd02261cd051da218baf99f90ee7c7040c109a01833db4f838f25256/huggingface_hub-1.8.0.tar.gz", hash = "sha256:c5627b2fd521e00caf8eff4ac965ba988ea75167fad7ee72e17f9b7183ec63f3", size = 735839, upload-time = "2026-03-25T16:01:28.152Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/ae/8a3a16ea4d202cb641b51d2681bdd3d482c1c592d7570b3fa264730829ce/huggingface_hub-1.8.0-py3-none-any.whl", hash = "sha256:d3eb5047bd4e33c987429de6020d4810d38a5bef95b3b40df9b17346b7f353f2", size = 625208, upload-time = "2026-03-25T16:01:26.603Z" }, +] + [[package]] name = "idna" version = "3.11" @@ -399,6 +585,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/49/cb/940431d9410fda74f941f5cd7f0e5a22c63be7b0c10fa98b2b7022b48cb1/librt-0.7.5-cp314-cp314t-win_arm64.whl", hash = "sha256:08153ea537609d11f774d2bfe84af39d50d5c9ca3a4d061d946e0c9d8bce04a1", size = 39728, upload-time = "2025-12-25T03:53:03.306Z" }, ] +[[package]] +name = "loguru" +version = "0.7.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "win32-setctime", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6", size = 63559, upload-time = "2024-12-06T11:20:56.608Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595, upload-time = "2024-12-06T11:20:54.538Z" }, +] + [[package]] name = "markdown-it-py" version = "4.0.0" @@ -445,6 +644,97 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] +[[package]] +name = "mmh3" +version = "5.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/91/1a/edb23803a168f070ded7a3014c6d706f63b90c84ccc024f89d794a3b7a6d/mmh3-5.2.1.tar.gz", hash = "sha256:bbea5b775f0ac84945191fb83f845a6fd9a21a03ea7f2e187defac7e401616ad", size = 33775, upload-time = "2026-03-05T15:55:57.716Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/94/bc5c3b573b40a328c4d141c20e399039ada95e5e2a661df3425c5165fd84/mmh3-5.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0cc21533878e5586b80d74c281d7f8da7932bc8ace50b8d5f6dbf7e3935f63f1", size = 56087, upload-time = "2026-03-05T15:54:21.92Z" }, + { url = "https://files.pythonhosted.org/packages/f6/80/64a02cc3e95c3af0aaa2590849d9ed24a9f14bb93537addde688e039b7c3/mmh3-5.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4eda76074cfca2787c8cf1bec603eaebdddd8b061ad5502f85cddae998d54f00", size = 40500, upload-time = "2026-03-05T15:54:22.953Z" }, + { url = "https://files.pythonhosted.org/packages/8b/72/e6d6602ce18adf4ddcd0e48f2e13590cc92a536199e52109f46f259d3c46/mmh3-5.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:eee884572b06bbe8a2b54f424dbd996139442cf83c76478e1ec162512e0dd2c7", size = 40034, upload-time = "2026-03-05T15:54:23.943Z" }, + { url = "https://files.pythonhosted.org/packages/59/c2/bf4537a8e58e21886ef16477041238cab5095c836496e19fafc34b7445d2/mmh3-5.2.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0d0b7e803191db5f714d264044e06189c8ccd3219e936cc184f07106bd17fd7b", size = 97292, upload-time = "2026-03-05T15:54:25.335Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e2/51ed62063b44d10b06d975ac87af287729eeb5e3ed9772f7584a17983e90/mmh3-5.2.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e6c219e375f6341d0959af814296372d265a8ca1af63825f65e2e87c618f006", size = 103274, upload-time = "2026-03-05T15:54:26.44Z" }, + { url = "https://files.pythonhosted.org/packages/75/ce/12a7524dca59eec92e5b31fdb13ede1e98eda277cf2b786cf73bfbc24e81/mmh3-5.2.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:26fb5b9c3946bf7f1daed7b37e0c03898a6f062149127570f8ede346390a0825", size = 106158, upload-time = "2026-03-05T15:54:28.578Z" }, + { url = "https://files.pythonhosted.org/packages/86/1f/d3ba6dd322d01ab5d44c46c8f0c38ab6bbbf9b5e20e666dfc05bf4a23604/mmh3-5.2.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3c38d142c706201db5b2345166eeef1e7740e3e2422b470b8ba5c8727a9b4c7a", size = 113005, upload-time = "2026-03-05T15:54:29.767Z" }, + { url = "https://files.pythonhosted.org/packages/b6/a9/15d6b6f913294ea41b44d901741298e3718e1cb89ee626b3694625826a43/mmh3-5.2.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50885073e2909251d4718634a191c49ae5f527e5e1736d738e365c3e8be8f22b", size = 120744, upload-time = "2026-03-05T15:54:30.931Z" }, + { url = "https://files.pythonhosted.org/packages/76/b3/70b73923fd0284c439860ff5c871b20210dfdbe9a6b9dd0ee6496d77f174/mmh3-5.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b3f99e1756fc48ad507b95e5d86f2fb21b3d495012ff13e6592ebac14033f166", size = 99111, upload-time = "2026-03-05T15:54:32.353Z" }, + { url = "https://files.pythonhosted.org/packages/dd/38/99f7f75cd27d10d8b899a1caafb9d531f3903e4d54d572220e3d8ac35e89/mmh3-5.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62815d2c67f2dd1be76a253d88af4e1da19aeaa1820146dec52cf8bee2958b16", size = 98623, upload-time = "2026-03-05T15:54:33.801Z" }, + { url = "https://files.pythonhosted.org/packages/fd/68/6e292c0853e204c44d2f03ea5f090be3317a0e2d9417ecb62c9eb27687df/mmh3-5.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8f767ba0911602ddef289404e33835a61168314ebd3c729833db2ed685824211", size = 106437, upload-time = "2026-03-05T15:54:35.177Z" }, + { url = "https://files.pythonhosted.org/packages/dd/c6/fedd7284c459cfb58721d461fcf5607a4c1f5d9ab195d113d51d10164d16/mmh3-5.2.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:67e41a497bac88cc1de96eeba56eeb933c39d54bc227352f8455aa87c4ca4000", size = 110002, upload-time = "2026-03-05T15:54:36.673Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ac/ca8e0c19a34f5b71390171d2ff0b9f7f187550d66801a731bb68925126a4/mmh3-5.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d74a03fb57757ece25aa4b3c1c60157a1cece37a020542785f942e2f827eed5", size = 97507, upload-time = "2026-03-05T15:54:37.804Z" }, + { url = "https://files.pythonhosted.org/packages/df/94/6ebb9094cfc7ac5e7950776b9d13a66bb4a34f83814f32ba2abc9494fc68/mmh3-5.2.1-cp312-cp312-win32.whl", hash = "sha256:7374d6e3ef72afe49697ecd683f3da12f4fc06af2d75433d0580c6746d2fa025", size = 40773, upload-time = "2026-03-05T15:54:40.077Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/cd3527198cf159495966551c84a5f36805a10ac17b294f41f67b83f6a4d6/mmh3-5.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:3a9fed49c6ce4ed7e73f13182760c65c816da006debe67f37635580dfb0fae00", size = 41560, upload-time = "2026-03-05T15:54:41.148Z" }, + { url = "https://files.pythonhosted.org/packages/15/96/6fe5ebd0f970a076e3ed5512871ce7569447b962e96c125528a2f9724470/mmh3-5.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:bbfcb95d9a744e6e2827dfc66ad10e1020e0cac255eb7f85652832d5a264c2fc", size = 39313, upload-time = "2026-03-05T15:54:42.171Z" }, + { url = "https://files.pythonhosted.org/packages/25/a5/9daa0508a1569a54130f6198d5462a92deda870043624aa3ea72721aa765/mmh3-5.2.1-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:723b2681ed4cc07d3401bbea9c201ad4f2a4ca6ba8cddaff6789f715dd2b391e", size = 40832, upload-time = "2026-03-05T15:54:43.212Z" }, + { url = "https://files.pythonhosted.org/packages/0a/6b/3230c6d80c1f4b766dedf280a92c2241e99f87c1504ff74205ec8cebe451/mmh3-5.2.1-cp313-cp313-android_21_x86_64.whl", hash = "sha256:3619473a0e0d329fd4aec8075628f8f616be2da41605300696206d6f36920c3d", size = 41964, upload-time = "2026-03-05T15:54:44.204Z" }, + { url = "https://files.pythonhosted.org/packages/62/fb/648bfddb74a872004b6ee751551bfdda783fe6d70d2e9723bad84dbe5311/mmh3-5.2.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:e48d4dbe0f88e53081da605ae68644e5182752803bbc2beb228cca7f1c4454d6", size = 39114, upload-time = "2026-03-05T15:54:45.205Z" }, + { url = "https://files.pythonhosted.org/packages/95/c2/ab7901f87af438468b496728d11264cb397b3574d41506e71b92128e0373/mmh3-5.2.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a482ac121de6973897c92c2f31defc6bafb11c83825109275cffce54bb64933f", size = 39819, upload-time = "2026-03-05T15:54:46.509Z" }, + { url = "https://files.pythonhosted.org/packages/2f/ed/6f88dda0df67de1612f2e130ffea34cf84aaee5bff5b0aff4dbff2babe34/mmh3-5.2.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:17fbb47f0885ace8327ce1235d0416dc86a211dcd8cc1e703f41523be32cfec8", size = 40330, upload-time = "2026-03-05T15:54:47.864Z" }, + { url = "https://files.pythonhosted.org/packages/3d/66/7516d23f53cdf90f43fce24ab80c28f45e6851d78b46bef8c02084edf583/mmh3-5.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d51fde50a77f81330523562e3c2734ffdca9c4c9e9d355478117905e1cfe16c6", size = 56078, upload-time = "2026-03-05T15:54:48.9Z" }, + { url = "https://files.pythonhosted.org/packages/bc/34/4d152fdf4a91a132cb226b671f11c6b796eada9ab78080fb5ce1e95adaab/mmh3-5.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:19bbd3b841174ae6ed588536ab5e1b1fe83d046e668602c20266547298d939a9", size = 40498, upload-time = "2026-03-05T15:54:49.942Z" }, + { url = "https://files.pythonhosted.org/packages/d4/4c/8e3af1b6d85a299767ec97bd923f12b06267089c1472c27c1696870d1175/mmh3-5.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be77c402d5e882b6fbacfd90823f13da8e0a69658405a39a569c6b58fdb17b03", size = 40033, upload-time = "2026-03-05T15:54:50.994Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f2/966ea560e32578d453c9e9db53d602cbb1d0da27317e232afa7c38ceba11/mmh3-5.2.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fd96476f04db5ceba1cfa0f21228f67c1f7402296f0e73fee3513aa680ad237b", size = 97320, upload-time = "2026-03-05T15:54:52.072Z" }, + { url = "https://files.pythonhosted.org/packages/bb/0d/2c5f9893b38aeb6b034d1a44ecd55a010148054f6a516abe53b5e4057297/mmh3-5.2.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:707151644085dd0f20fe4f4b573d28e5130c4aaa5f587e95b60989c5926653b5", size = 103299, upload-time = "2026-03-05T15:54:53.569Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fc/2ebaef4a4d4376f89761274dc274035ffd96006ab496b4ee5af9b08f21a9/mmh3-5.2.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3737303ca9ea0f7cb83028781148fcda4f1dac7821db0c47672971dabcf63593", size = 106222, upload-time = "2026-03-05T15:54:55.092Z" }, + { url = "https://files.pythonhosted.org/packages/57/09/ea7ffe126d0ba0406622602a2d05e1e1a6841cc92fc322eb576c95b27fad/mmh3-5.2.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2778fed822d7db23ac5008b181441af0c869455b2e7d001f4019636ac31b6fe4", size = 113048, upload-time = "2026-03-05T15:54:56.305Z" }, + { url = "https://files.pythonhosted.org/packages/85/57/9447032edf93a64aa9bef4d9aa596400b1756f40411890f77a284f6293ca/mmh3-5.2.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d57dea657357230cc780e13920d7fa7db059d58fe721c80020f94476da4ca0a1", size = 120742, upload-time = "2026-03-05T15:54:57.453Z" }, + { url = "https://files.pythonhosted.org/packages/53/82/a86cc87cc88c92e9e1a598fee509f0409435b57879a6129bf3b3e40513c7/mmh3-5.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:169e0d178cb59314456ab30772429a802b25d13227088085b0d49b9fe1533104", size = 99132, upload-time = "2026-03-05T15:54:58.583Z" }, + { url = "https://files.pythonhosted.org/packages/54/f7/6b16eb1b40ee89bb740698735574536bc20d6cdafc65ae702ea235578e05/mmh3-5.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7e4e1f580033335c6f76d1e0d6b56baf009d1a64d6a4816347e4271ba951f46d", size = 98686, upload-time = "2026-03-05T15:55:00.078Z" }, + { url = "https://files.pythonhosted.org/packages/e8/88/a601e9f32ad1410f438a6d0544298ea621f989bd34a0731a7190f7dec799/mmh3-5.2.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:2bd9f19f7f1fcebd74e830f4af0f28adad4975d40d80620be19ffb2b2af56c9f", size = 106479, upload-time = "2026-03-05T15:55:01.532Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5c/ce29ae3dfc4feec4007a437a1b7435fb9507532a25147602cd5b52be86db/mmh3-5.2.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:c88653877aeb514c089d1b3d473451677b8b9a6d1497dbddf1ae7934518b06d2", size = 110030, upload-time = "2026-03-05T15:55:02.934Z" }, + { url = "https://files.pythonhosted.org/packages/13/30/ae444ef2ff87c805d525da4fa63d27cda4fe8a48e77003a036b8461cfd5c/mmh3-5.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fceef7fe67c81e1585198215e42ad3fdba3a25644beda8fbdaf85f4d7b93175a", size = 97536, upload-time = "2026-03-05T15:55:04.135Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f9/dc3787ee5c813cc27fe79f45ad4500d9b5437f23a7402435cc34e07c7718/mmh3-5.2.1-cp313-cp313-win32.whl", hash = "sha256:54b64fb2433bc71488e7a449603bf8bd31fbcf9cb56fbe1eb6d459e90b86c37b", size = 40769, upload-time = "2026-03-05T15:55:05.277Z" }, + { url = "https://files.pythonhosted.org/packages/43/67/850e0b5a1e97799822ebfc4ca0e8c6ece3ed8baf7dcdf64de817dfdda2ca/mmh3-5.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:cae6383181f1e345317742d2ddd88f9e7d2682fa4c9432e3a74e47d92dce0229", size = 41563, upload-time = "2026-03-05T15:55:06.283Z" }, + { url = "https://files.pythonhosted.org/packages/c0/cc/98c90b28e1da5458e19fbfaf4adb5289208d3bfccd45dd14eab216a2f0bb/mmh3-5.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:022aa1a528604e6c83d0a7705fdef0b5355d897a9e0fa3a8d26709ceaa06965d", size = 39310, upload-time = "2026-03-05T15:55:07.323Z" }, + { url = "https://files.pythonhosted.org/packages/63/b4/65bc1fb2bb7f83e91c30865023b1847cf89a5f237165575e8c83aa536584/mmh3-5.2.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:d771f085fcdf4035786adfb1d8db026df1eb4b41dac1c3d070d1e49512843227", size = 40794, upload-time = "2026-03-05T15:55:09.773Z" }, + { url = "https://files.pythonhosted.org/packages/c4/86/7168b3d83be8eb553897b1fac9da8bbb06568e5cfe555ffc329ebb46f59d/mmh3-5.2.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:7f196cd7910d71e9d9860da0ff7a77f64d22c1ad931f1dd18559a06e03109fc0", size = 41923, upload-time = "2026-03-05T15:55:10.924Z" }, + { url = "https://files.pythonhosted.org/packages/bf/9b/b653ab611c9060ce8ff0ba25c0226757755725e789292f3ca138a58082cd/mmh3-5.2.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b1f12bd684887a0a5d55e6363ca87056f361e45451105012d329b86ec19dbe0b", size = 39131, upload-time = "2026-03-05T15:55:11.961Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b4/5a2e0d34ab4d33543f01121e832395ea510132ea8e52cdf63926d9d81754/mmh3-5.2.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d106493a60dcb4aef35a0fac85105e150a11cf8bc2b0d388f5a33272d756c966", size = 39825, upload-time = "2026-03-05T15:55:13.013Z" }, + { url = "https://files.pythonhosted.org/packages/bd/69/81699a8f39a3f8d368bec6443435c0c392df0d200ad915bf0d222b588e03/mmh3-5.2.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:44983e45310ee5b9f73397350251cdf6e63a466406a105f1d16cb5baa659270b", size = 40344, upload-time = "2026-03-05T15:55:14.026Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b3/71c8c775807606e8fd8acc5c69016e1caf3200d50b50b6dd4b40ce10b76c/mmh3-5.2.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:368625fb01666655985391dbad3860dc0ba7c0d6b9125819f3121ee7292b4ac8", size = 56291, upload-time = "2026-03-05T15:55:15.137Z" }, + { url = "https://files.pythonhosted.org/packages/6f/75/2c24517d4b2ce9e4917362d24f274d3d541346af764430249ddcc4cb3a08/mmh3-5.2.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:72d1cc63bcc91e14933f77d51b3df899d6a07d184ec515ea7f56bff659e124d7", size = 40575, upload-time = "2026-03-05T15:55:16.518Z" }, + { url = "https://files.pythonhosted.org/packages/bf/b9/e4a360164365ac9f07a25f0f7928e3a66eb9ecc989384060747aa170e6aa/mmh3-5.2.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e8b4b5580280b9265af3e0409974fb79c64cf7523632d03fbf11df18f8b0181e", size = 40052, upload-time = "2026-03-05T15:55:17.735Z" }, + { url = "https://files.pythonhosted.org/packages/97/ca/120d92223a7546131bbbc31c9174168ee7a73b1366f5463ffe69d9e691fe/mmh3-5.2.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4cbbde66f1183db040daede83dd86c06d663c5bb2af6de1142b7c8c37923dd74", size = 97311, upload-time = "2026-03-05T15:55:18.959Z" }, + { url = "https://files.pythonhosted.org/packages/b6/71/c1a60c1652b8813ef9de6d289784847355417ee0f2980bca002fe87f4ae5/mmh3-5.2.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8ff038d52ef6aa0f309feeba00c5095c9118d0abf787e8e8454d6048db2037fc", size = 103279, upload-time = "2026-03-05T15:55:20.448Z" }, + { url = "https://files.pythonhosted.org/packages/48/29/ad97f4be1509cdcb28ae32c15593ce7c415db47ace37f8fad35b493faa9a/mmh3-5.2.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4130d0b9ce5fad6af07421b1aecc7e079519f70d6c05729ab871794eded8617", size = 106290, upload-time = "2026-03-05T15:55:21.6Z" }, + { url = "https://files.pythonhosted.org/packages/77/29/1f86d22e281bd8827ba373600a4a8b0c0eae5ca6aa55b9a8c26d2a34decc/mmh3-5.2.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6e0bfe77d238308839699944164b96a2eeccaf55f2af400f54dc20669d8d5f2", size = 113116, upload-time = "2026-03-05T15:55:22.826Z" }, + { url = "https://files.pythonhosted.org/packages/a7/7c/339971ea7ed4c12d98f421f13db3ea576a9114082ccb59d2d1a0f00ccac1/mmh3-5.2.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f963eafc0a77a6c0562397da004f5876a9bcf7265a7bcc3205e29636bc4a1312", size = 120740, upload-time = "2026-03-05T15:55:24.3Z" }, + { url = "https://files.pythonhosted.org/packages/e4/92/3c7c4bdb8e926bb3c972d1e2907d77960c1c4b250b41e8366cf20c6e4373/mmh3-5.2.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:92883836caf50d5255be03d988d75bc93e3f86ba247b7ca137347c323f731deb", size = 99143, upload-time = "2026-03-05T15:55:25.456Z" }, + { url = "https://files.pythonhosted.org/packages/df/0a/33dd8706e732458c8375eae63c981292de07a406bad4ec03e5269654aa2c/mmh3-5.2.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:57b52603e89355ff318025dd55158f6e71396c0f1f609d548e9ea9c94cc6ce0a", size = 98703, upload-time = "2026-03-05T15:55:26.723Z" }, + { url = "https://files.pythonhosted.org/packages/51/04/76bbce05df76cbc3d396f13b2ea5b1578ef02b6a5187e132c6c33f99d596/mmh3-5.2.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f40a95186a72fa0b67d15fef0f157bfcda00b4f59c8a07cbe5530d41ac35d105", size = 106484, upload-time = "2026-03-05T15:55:28.214Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8f/c6e204a2c70b719c1f62ffd9da27aef2dddcba875ea9c31ca0e87b975a46/mmh3-5.2.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:58370d05d033ee97224c81263af123dea3d931025030fd34b61227a768a8858a", size = 110012, upload-time = "2026-03-05T15:55:29.532Z" }, + { url = "https://files.pythonhosted.org/packages/e3/37/7181efd8e39db386c1ebc3e6b7d1f702a09d7c1197a6f2742ed6b5c16597/mmh3-5.2.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7be6dfb49e48fd0a7d91ff758a2b51336f1cd21f9d44b20f6801f072bd080cdd", size = 97508, upload-time = "2026-03-05T15:55:31.01Z" }, + { url = "https://files.pythonhosted.org/packages/42/0f/afa7ca2615fd85e1469474bb860e381443d0b868c083b62b41cb1d7ca32f/mmh3-5.2.1-cp314-cp314-win32.whl", hash = "sha256:54fe8518abe06a4c3852754bfd498b30cc58e667f376c513eac89a244ce781a4", size = 41387, upload-time = "2026-03-05T15:55:32.403Z" }, + { url = "https://files.pythonhosted.org/packages/71/0d/46d42a260ee1357db3d486e6c7a692e303c017968e14865e00efa10d09fc/mmh3-5.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:3f796b535008708846044c43302719c6956f39ca2d93f2edda5319e79a29efbb", size = 42101, upload-time = "2026-03-05T15:55:33.646Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7b/848a8378059d96501a41159fca90d6a99e89736b0afbe8e8edffeac8c74b/mmh3-5.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:cd471ede0d802dd936b6fab28188302b2d497f68436025857ca72cd3810423fe", size = 39836, upload-time = "2026-03-05T15:55:35.026Z" }, + { url = "https://files.pythonhosted.org/packages/27/61/1dabea76c011ba8547c25d30c91c0ec22544487a8750997a27a0c9e1180b/mmh3-5.2.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:5174a697ce042fa77c407e05efe41e03aa56dae9ec67388055820fb48cf4c3ba", size = 57727, upload-time = "2026-03-05T15:55:36.162Z" }, + { url = "https://files.pythonhosted.org/packages/b7/32/731185950d1cf2d5e28979cc8593016ba1619a295faba10dda664a4931b5/mmh3-5.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:0a3984146e414684a6be2862d84fcb1035f4984851cb81b26d933bab6119bf00", size = 41308, upload-time = "2026-03-05T15:55:37.254Z" }, + { url = "https://files.pythonhosted.org/packages/76/aa/66c76801c24b8c9418b4edde9b5e57c75e72c94e29c48f707e3962534f18/mmh3-5.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bd6e7d363aa93bd3421b30b6af97064daf47bc96005bddba67c5ffbc6df426b8", size = 40758, upload-time = "2026-03-05T15:55:38.61Z" }, + { url = "https://files.pythonhosted.org/packages/9e/bb/79a1f638a02f0ae389f706d13891e2fbf7d8c0a22ecde67ba828951bb60a/mmh3-5.2.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:113f78e7463a36dbbcea05bfe688efd7fa759d0f0c56e73c974d60dcfec3dfcc", size = 109670, upload-time = "2026-03-05T15:55:40.13Z" }, + { url = "https://files.pythonhosted.org/packages/26/94/8cd0e187a288985bcfc79bf5144d1d712df9dee74365f59d26e3a1865be6/mmh3-5.2.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7e8ec5f606e0809426d2440e0683509fb605a8820a21ebd120dcdba61b74ef7f", size = 117399, upload-time = "2026-03-05T15:55:42.076Z" }, + { url = "https://files.pythonhosted.org/packages/42/94/dfea6059bd5c5beda565f58a4096e43f4858fb6d2862806b8bbd12cbb284/mmh3-5.2.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22b0f9971ec4e07e8223f2beebe96a6cfc779d940b6f27d26604040dd74d3a44", size = 120386, upload-time = "2026-03-05T15:55:43.481Z" }, + { url = "https://files.pythonhosted.org/packages/47/cb/f9c45e62aaa67220179f487772461d891bb582bb2f9783c944832c60efd9/mmh3-5.2.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:85ffc9920ffc39c5eee1e3ac9100c913a0973996fbad5111f939bbda49204bb7", size = 125924, upload-time = "2026-03-05T15:55:44.638Z" }, + { url = "https://files.pythonhosted.org/packages/a5/83/fe54a4a7c11bc9f623dfc1707decd034245602b076dfc1dcc771a4163170/mmh3-5.2.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7aec798c2b01aaa65a55f1124f3405804184373abb318a3091325aece235f67c", size = 135280, upload-time = "2026-03-05T15:55:45.866Z" }, + { url = "https://files.pythonhosted.org/packages/97/67/fe7e9e9c143daddd210cd22aef89cbc425d58ecf238d2b7d9eb0da974105/mmh3-5.2.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:55dbbd8ffbc40d1697d5e2d0375b08599dae8746b0b08dea05eee4ce81648fac", size = 110050, upload-time = "2026-03-05T15:55:47.074Z" }, + { url = "https://files.pythonhosted.org/packages/43/c4/6d4b09fcbef80794de447c9378e39eefc047156b290fa3dd2d5257ca8227/mmh3-5.2.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:6c85c38a279ca9295a69b9b088a2e48aa49737bb1b34e6a9dc6297c110e8d912", size = 111158, upload-time = "2026-03-05T15:55:48.239Z" }, + { url = "https://files.pythonhosted.org/packages/81/a6/ca51c864bdb30524beb055a6d8826db3906af0834ec8c41d097a6e8573d5/mmh3-5.2.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:6290289fa5fb4c70fd7f72016e03633d60388185483ff3b162912c81205ae2cf", size = 116890, upload-time = "2026-03-05T15:55:49.405Z" }, + { url = "https://files.pythonhosted.org/packages/cc/04/5a1fe2e2ad843d03e89af25238cbc4f6840a8bb6c4329a98ab694c71deda/mmh3-5.2.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:4fc6cd65dc4d2fdb2625e288939a3566e36127a84811a4913f02f3d5931da52d", size = 123121, upload-time = "2026-03-05T15:55:50.61Z" }, + { url = "https://files.pythonhosted.org/packages/af/4d/3c820c6f4897afd25905270a9f2330a23f77a207ea7356f7aadace7273c0/mmh3-5.2.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:623f938f6a039536cc02b7582a07a080f13fdfd48f87e63201d92d7e34d09a18", size = 110187, upload-time = "2026-03-05T15:55:52.143Z" }, + { url = "https://files.pythonhosted.org/packages/21/54/1d71cd143752361c0aebef16ad3f55926a6faf7b112d355745c1f8a25f7f/mmh3-5.2.1-cp314-cp314t-win32.whl", hash = "sha256:29bc3973676ae334412efdd367fcd11d036b7be3efc1ce2407ef8676dabfeb82", size = 41934, upload-time = "2026-03-05T15:55:53.564Z" }, + { url = "https://files.pythonhosted.org/packages/9d/e4/63a2a88f31d93dea03947cccc2a076946857e799ea4f7acdecbf43b324aa/mmh3-5.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:28cfab66577000b9505a0d068c731aee7ca85cd26d4d63881fab17857e0fe1fb", size = 43036, upload-time = "2026-03-05T15:55:55.252Z" }, + { url = "https://files.pythonhosted.org/packages/a0/0f/59204bf136d1201f8d7884cfbaf7498c5b4674e87a4c693f9bde63741ce1/mmh3-5.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:dfd51b4c56b673dfbc43d7d27ef857dd91124801e2806c69bb45585ce0fa019b", size = 40391, upload-time = "2026-03-05T15:55:56.697Z" }, +] + +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, +] + [[package]] name = "mypy" version = "1.19.1" @@ -487,6 +777,100 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] +[[package]] +name = "numpy" +version = "2.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/10/8b/c265f4823726ab832de836cdd184d0986dcf94480f81e8739692a7ac7af2/numpy-2.4.3.tar.gz", hash = "sha256:483a201202b73495f00dbc83796c6ae63137a9bdade074f7648b3e32613412dd", size = 20727743, upload-time = "2026-03-09T07:58:53.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/ed/6388632536f9788cea23a3a1b629f25b43eaacd7d7377e5d6bc7b9deb69b/numpy-2.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:61b0cbabbb6126c8df63b9a3a0c4b1f44ebca5e12ff6997b80fcf267fb3150ef", size = 16669628, upload-time = "2026-03-09T07:56:24.252Z" }, + { url = "https://files.pythonhosted.org/packages/74/1b/ee2abfc68e1ce728b2958b6ba831d65c62e1b13ce3017c13943f8f9b5b2e/numpy-2.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7395e69ff32526710748f92cd8c9849b361830968ea3e24a676f272653e8983e", size = 14696872, upload-time = "2026-03-09T07:56:26.991Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d1/780400e915ff5638166f11ca9dc2c5815189f3d7cf6f8759a1685e586413/numpy-2.4.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:abdce0f71dcb4a00e4e77f3faf05e4616ceccfe72ccaa07f47ee79cda3b7b0f4", size = 5203489, upload-time = "2026-03-09T07:56:29.414Z" }, + { url = "https://files.pythonhosted.org/packages/0b/bb/baffa907e9da4cc34a6e556d6d90e032f6d7a75ea47968ea92b4858826c4/numpy-2.4.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:48da3a4ee1336454b07497ff7ec83903efa5505792c4e6d9bf83d99dc07a1e18", size = 6550814, upload-time = "2026-03-09T07:56:32.225Z" }, + { url = "https://files.pythonhosted.org/packages/7b/12/8c9f0c6c95f76aeb20fc4a699c33e9f827fa0d0f857747c73bb7b17af945/numpy-2.4.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32e3bef222ad6b052280311d1d60db8e259e4947052c3ae7dd6817451fc8a4c5", size = 15666601, upload-time = "2026-03-09T07:56:34.461Z" }, + { url = "https://files.pythonhosted.org/packages/bd/79/cc665495e4d57d0aa6fbcc0aa57aa82671dfc78fbf95fe733ed86d98f52a/numpy-2.4.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7dd01a46700b1967487141a66ac1a3cf0dd8ebf1f08db37d46389401512ca97", size = 16621358, upload-time = "2026-03-09T07:56:36.852Z" }, + { url = "https://files.pythonhosted.org/packages/a8/40/b4ecb7224af1065c3539f5ecfff879d090de09608ad1008f02c05c770cb3/numpy-2.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:76f0f283506c28b12bba319c0fab98217e9f9b54e6160e9c79e9f7348ba32e9c", size = 17016135, upload-time = "2026-03-09T07:56:39.337Z" }, + { url = "https://files.pythonhosted.org/packages/f7/b1/6a88e888052eed951afed7a142dcdf3b149a030ca59b4c71eef085858e43/numpy-2.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:737f630a337364665aba3b5a77e56a68cc42d350edd010c345d65a3efa3addcc", size = 18345816, upload-time = "2026-03-09T07:56:42.31Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8f/103a60c5f8c3d7fc678c19cd7b2476110da689ccb80bc18050efbaeae183/numpy-2.4.3-cp312-cp312-win32.whl", hash = "sha256:26952e18d82a1dbbc2f008d402021baa8d6fc8e84347a2072a25e08b46d698b9", size = 5960132, upload-time = "2026-03-09T07:56:44.851Z" }, + { url = "https://files.pythonhosted.org/packages/d7/7c/f5ee1bf6ed888494978046a809df2882aad35d414b622893322df7286879/numpy-2.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:65f3c2455188f09678355f5cae1f959a06b778bc66d535da07bf2ef20cd319d5", size = 12316144, upload-time = "2026-03-09T07:56:47.057Z" }, + { url = "https://files.pythonhosted.org/packages/71/46/8d1cb3f7a00f2fb6394140e7e6623696e54c6318a9d9691bb4904672cf42/numpy-2.4.3-cp312-cp312-win_arm64.whl", hash = "sha256:2abad5c7fef172b3377502bde47892439bae394a71bc329f31df0fd829b41a9e", size = 10220364, upload-time = "2026-03-09T07:56:49.849Z" }, + { url = "https://files.pythonhosted.org/packages/b6/d0/1fe47a98ce0df229238b77611340aff92d52691bcbc10583303181abf7fc/numpy-2.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b346845443716c8e542d54112966383b448f4a3ba5c66409771b8c0889485dd3", size = 16665297, upload-time = "2026-03-09T07:56:52.296Z" }, + { url = "https://files.pythonhosted.org/packages/27/d9/4e7c3f0e68dfa91f21c6fb6cf839bc829ec920688b1ce7ec722b1a6202fb/numpy-2.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2629289168f4897a3c4e23dc98d6f1731f0fc0fe52fb9db19f974041e4cc12b9", size = 14691853, upload-time = "2026-03-09T07:56:54.992Z" }, + { url = "https://files.pythonhosted.org/packages/3a/66/bd096b13a87549683812b53ab211e6d413497f84e794fb3c39191948da97/numpy-2.4.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:bb2e3cf95854233799013779216c57e153c1ee67a0bf92138acca0e429aefaee", size = 5198435, upload-time = "2026-03-09T07:56:57.184Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2f/687722910b5a5601de2135c891108f51dfc873d8e43c8ed9f4ebb440b4a2/numpy-2.4.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:7f3408ff897f8ab07a07fbe2823d7aee6ff644c097cc1f90382511fe982f647f", size = 6546347, upload-time = "2026-03-09T07:56:59.531Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ec/7971c4e98d86c564750393fab8d7d83d0a9432a9d78bb8a163a6dc59967a/numpy-2.4.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:decb0eb8a53c3b009b0962378065589685d66b23467ef5dac16cbe818afde27f", size = 15664626, upload-time = "2026-03-09T07:57:01.385Z" }, + { url = "https://files.pythonhosted.org/packages/7e/eb/7daecbea84ec935b7fc732e18f532073064a3816f0932a40a17f3349185f/numpy-2.4.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5f51900414fc9204a0e0da158ba2ac52b75656e7dce7e77fb9f84bfa343b4cc", size = 16608916, upload-time = "2026-03-09T07:57:04.008Z" }, + { url = "https://files.pythonhosted.org/packages/df/58/2a2b4a817ffd7472dca4421d9f0776898b364154e30c95f42195041dc03b/numpy-2.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6bd06731541f89cdc01b261ba2c9e037f1543df7472517836b78dfb15bd6e476", size = 17015824, upload-time = "2026-03-09T07:57:06.347Z" }, + { url = "https://files.pythonhosted.org/packages/4a/ca/627a828d44e78a418c55f82dd4caea8ea4a8ef24e5144d9e71016e52fb40/numpy-2.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22654fe6be0e5206f553a9250762c653d3698e46686eee53b399ab90da59bd92", size = 18334581, upload-time = "2026-03-09T07:57:09.114Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c0/76f93962fc79955fcba30a429b62304332345f22d4daec1cb33653425643/numpy-2.4.3-cp313-cp313-win32.whl", hash = "sha256:d71e379452a2f670ccb689ec801b1218cd3983e253105d6e83780967e899d687", size = 5958618, upload-time = "2026-03-09T07:57:11.432Z" }, + { url = "https://files.pythonhosted.org/packages/b1/3c/88af0040119209b9b5cb59485fa48b76f372c73068dbf9254784b975ac53/numpy-2.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:0a60e17a14d640f49146cb38e3f105f571318db7826d9b6fef7e4dce758faecd", size = 12312824, upload-time = "2026-03-09T07:57:13.586Z" }, + { url = "https://files.pythonhosted.org/packages/58/ce/3d07743aced3d173f877c3ef6a454c2174ba42b584ab0b7e6d99374f51ed/numpy-2.4.3-cp313-cp313-win_arm64.whl", hash = "sha256:c9619741e9da2059cd9c3f206110b97583c7152c1dc9f8aafd4beb450ac1c89d", size = 10221218, upload-time = "2026-03-09T07:57:16.183Z" }, + { url = "https://files.pythonhosted.org/packages/62/09/d96b02a91d09e9d97862f4fc8bfebf5400f567d8eb1fe4b0cc4795679c15/numpy-2.4.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7aa4e54f6469300ebca1d9eb80acd5253cdfa36f2c03d79a35883687da430875", size = 14819570, upload-time = "2026-03-09T07:57:18.564Z" }, + { url = "https://files.pythonhosted.org/packages/b5/ca/0b1aba3905fdfa3373d523b2b15b19029f4f3031c87f4066bd9d20ef6c6b/numpy-2.4.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d1b90d840b25874cf5cd20c219af10bac3667db3876d9a495609273ebe679070", size = 5326113, upload-time = "2026-03-09T07:57:21.052Z" }, + { url = "https://files.pythonhosted.org/packages/c0/63/406e0fd32fcaeb94180fd6a4c41e55736d676c54346b7efbce548b94a914/numpy-2.4.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a749547700de0a20a6718293396ec237bb38218049cfce788e08fcb716e8cf73", size = 6646370, upload-time = "2026-03-09T07:57:22.804Z" }, + { url = "https://files.pythonhosted.org/packages/b6/d0/10f7dc157d4b37af92720a196be6f54f889e90dcd30dce9dc657ed92c257/numpy-2.4.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94f3c4a151a2e529adf49c1d54f0f57ff8f9b233ee4d44af623a81553ab86368", size = 15723499, upload-time = "2026-03-09T07:57:24.693Z" }, + { url = "https://files.pythonhosted.org/packages/66/f1/d1c2bf1161396629701bc284d958dc1efa3a5a542aab83cf11ee6eb4cba5/numpy-2.4.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22c31dc07025123aedf7f2db9e91783df13f1776dc52c6b22c620870dc0fab22", size = 16657164, upload-time = "2026-03-09T07:57:27.676Z" }, + { url = "https://files.pythonhosted.org/packages/1a/be/cca19230b740af199ac47331a21c71e7a3d0ba59661350483c1600d28c37/numpy-2.4.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:148d59127ac95979d6f07e4d460f934ebdd6eed641db9c0db6c73026f2b2101a", size = 17081544, upload-time = "2026-03-09T07:57:30.664Z" }, + { url = "https://files.pythonhosted.org/packages/b9/c5/9602b0cbb703a0936fb40f8a95407e8171935b15846de2f0776e08af04c7/numpy-2.4.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a97cbf7e905c435865c2d939af3d93f99d18eaaa3cabe4256f4304fb51604349", size = 18380290, upload-time = "2026-03-09T07:57:33.763Z" }, + { url = "https://files.pythonhosted.org/packages/ed/81/9f24708953cd30be9ee36ec4778f4b112b45165812f2ada4cc5ea1c1f254/numpy-2.4.3-cp313-cp313t-win32.whl", hash = "sha256:be3b8487d725a77acccc9924f65fd8bce9af7fac8c9820df1049424a2115af6c", size = 6082814, upload-time = "2026-03-09T07:57:36.491Z" }, + { url = "https://files.pythonhosted.org/packages/e2/9e/52f6eaa13e1a799f0ab79066c17f7016a4a8ae0c1aefa58c82b4dab690b4/numpy-2.4.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1ec84fd7c8e652b0f4aaaf2e6e9cc8eaa9b1b80a537e06b2e3a2fb176eedcb26", size = 12452673, upload-time = "2026-03-09T07:57:38.281Z" }, + { url = "https://files.pythonhosted.org/packages/c4/04/b8cece6ead0b30c9fbd99bb835ad7ea0112ac5f39f069788c5558e3b1ab2/numpy-2.4.3-cp313-cp313t-win_arm64.whl", hash = "sha256:120df8c0a81ebbf5b9020c91439fccd85f5e018a927a39f624845be194a2be02", size = 10290907, upload-time = "2026-03-09T07:57:40.747Z" }, + { url = "https://files.pythonhosted.org/packages/70/ae/3936f79adebf8caf81bd7a599b90a561334a658be4dcc7b6329ebf4ee8de/numpy-2.4.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5884ce5c7acfae1e4e1b6fde43797d10aa506074d25b531b4f54bde33c0c31d4", size = 16664563, upload-time = "2026-03-09T07:57:43.817Z" }, + { url = "https://files.pythonhosted.org/packages/9b/62/760f2b55866b496bb1fa7da2a6db076bef908110e568b02fcfc1422e2a3a/numpy-2.4.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:297837823f5bc572c5f9379b0c9f3a3365f08492cbdc33bcc3af174372ebb168", size = 14702161, upload-time = "2026-03-09T07:57:46.169Z" }, + { url = "https://files.pythonhosted.org/packages/32/af/a7a39464e2c0a21526fb4fb76e346fb172ebc92f6d1c7a07c2c139cc17b1/numpy-2.4.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:a111698b4a3f8dcbe54c64a7708f049355abd603e619013c346553c1fd4ca90b", size = 5208738, upload-time = "2026-03-09T07:57:48.506Z" }, + { url = "https://files.pythonhosted.org/packages/29/8c/2a0cf86a59558fa078d83805589c2de490f29ed4fb336c14313a161d358a/numpy-2.4.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:4bd4741a6a676770e0e97fe9ab2e51de01183df3dcbcec591d26d331a40de950", size = 6543618, upload-time = "2026-03-09T07:57:50.591Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b8/612ce010c0728b1c363fa4ea3aa4c22fe1c5da1de008486f8c2f5cb92fae/numpy-2.4.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:54f29b877279d51e210e0c80709ee14ccbbad647810e8f3d375561c45ef613dd", size = 15680676, upload-time = "2026-03-09T07:57:52.34Z" }, + { url = "https://files.pythonhosted.org/packages/a9/7e/4f120ecc54ba26ddf3dc348eeb9eb063f421de65c05fc961941798feea18/numpy-2.4.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:679f2a834bae9020f81534671c56fd0cc76dd7e5182f57131478e23d0dc59e24", size = 16613492, upload-time = "2026-03-09T07:57:54.91Z" }, + { url = "https://files.pythonhosted.org/packages/2c/86/1b6020db73be330c4b45d5c6ee4295d59cfeef0e3ea323959d053e5a6909/numpy-2.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d84f0f881cb2225c2dfd7f78a10a5645d487a496c6668d6cc39f0f114164f3d0", size = 17031789, upload-time = "2026-03-09T07:57:57.641Z" }, + { url = "https://files.pythonhosted.org/packages/07/3a/3b90463bf41ebc21d1b7e06079f03070334374208c0f9a1f05e4ae8455e7/numpy-2.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d213c7e6e8d211888cc359bab7199670a00f5b82c0978b9d1c75baf1eddbeac0", size = 18339941, upload-time = "2026-03-09T07:58:00.577Z" }, + { url = "https://files.pythonhosted.org/packages/a8/74/6d736c4cd962259fd8bae9be27363eb4883a2f9069763747347544c2a487/numpy-2.4.3-cp314-cp314-win32.whl", hash = "sha256:52077feedeff7c76ed7c9f1a0428558e50825347b7545bbb8523da2cd55c547a", size = 6007503, upload-time = "2026-03-09T07:58:03.331Z" }, + { url = "https://files.pythonhosted.org/packages/48/39/c56ef87af669364356bb011922ef0734fc49dad51964568634c72a009488/numpy-2.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:0448e7f9caefb34b4b7dd2b77f21e8906e5d6f0365ad525f9f4f530b13df2afc", size = 12444915, upload-time = "2026-03-09T07:58:06.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1f/ab8528e38d295fd349310807496fabb7cf9fe2e1f70b97bc20a483ea9d4a/numpy-2.4.3-cp314-cp314-win_arm64.whl", hash = "sha256:b44fd60341c4d9783039598efadd03617fa28d041fc37d22b62d08f2027fa0e7", size = 10494875, upload-time = "2026-03-09T07:58:08.734Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ef/b7c35e4d5ef141b836658ab21a66d1a573e15b335b1d111d31f26c8ef80f/numpy-2.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0a195f4216be9305a73c0e91c9b026a35f2161237cf1c6de9b681637772ea657", size = 14822225, upload-time = "2026-03-09T07:58:11.034Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8d/7730fa9278cf6648639946cc816e7cc89f0d891602584697923375f801ed/numpy-2.4.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:cd32fbacb9fd1bf041bf8e89e4576b6f00b895f06d00914820ae06a616bdfef7", size = 5328769, upload-time = "2026-03-09T07:58:13.67Z" }, + { url = "https://files.pythonhosted.org/packages/47/01/d2a137317c958b074d338807c1b6a383406cdf8b8e53b075d804cc3d211d/numpy-2.4.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:2e03c05abaee1f672e9d67bc858f300b5ccba1c21397211e8d77d98350972093", size = 6649461, upload-time = "2026-03-09T07:58:15.912Z" }, + { url = "https://files.pythonhosted.org/packages/5c/34/812ce12bc0f00272a4b0ec0d713cd237cb390666eb6206323d1cc9cedbb2/numpy-2.4.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d1ce23cce91fcea443320a9d0ece9b9305d4368875bab09538f7a5b4131938a", size = 15725809, upload-time = "2026-03-09T07:58:17.787Z" }, + { url = "https://files.pythonhosted.org/packages/25/c0/2aed473a4823e905e765fee3dc2cbf504bd3e68ccb1150fbdabd5c39f527/numpy-2.4.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c59020932feb24ed49ffd03704fbab89f22aa9c0d4b180ff45542fe8918f5611", size = 16655242, upload-time = "2026-03-09T07:58:20.476Z" }, + { url = "https://files.pythonhosted.org/packages/f2/c8/7e052b2fc87aa0e86de23f20e2c42bd261c624748aa8efd2c78f7bb8d8c6/numpy-2.4.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9684823a78a6cd6ad7511fc5e25b07947d1d5b5e2812c93fe99d7d4195130720", size = 17080660, upload-time = "2026-03-09T07:58:23.067Z" }, + { url = "https://files.pythonhosted.org/packages/f3/3d/0876746044db2adcb11549f214d104f2e1be00f07a67edbb4e2812094847/numpy-2.4.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0200b25c687033316fb39f0ff4e3e690e8957a2c3c8d22499891ec58c37a3eb5", size = 18380384, upload-time = "2026-03-09T07:58:25.839Z" }, + { url = "https://files.pythonhosted.org/packages/07/12/8160bea39da3335737b10308df4f484235fd297f556745f13092aa039d3b/numpy-2.4.3-cp314-cp314t-win32.whl", hash = "sha256:5e10da9e93247e554bb1d22f8edc51847ddd7dde52d85ce31024c1b4312bfba0", size = 6154547, upload-time = "2026-03-09T07:58:28.289Z" }, + { url = "https://files.pythonhosted.org/packages/42/f3/76534f61f80d74cc9cdf2e570d3d4eeb92c2280a27c39b0aaf471eda7b48/numpy-2.4.3-cp314-cp314t-win_amd64.whl", hash = "sha256:45f003dbdffb997a03da2d1d0cb41fbd24a87507fb41605c0420a3db5bd4667b", size = 12633645, upload-time = "2026-03-09T07:58:30.384Z" }, + { url = "https://files.pythonhosted.org/packages/1f/b6/7c0d4334c15983cec7f92a69e8ce9b1e6f31857e5ee3a413ac424e6bd63d/numpy-2.4.3-cp314-cp314t-win_arm64.whl", hash = "sha256:4d382735cecd7bcf090172489a525cd7d4087bc331f7df9f60ddc9a296cf208e", size = 10565454, upload-time = "2026-03-09T07:58:33.031Z" }, +] + +[[package]] +name = "onnxruntime" +version = "1.24.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flatbuffers" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "protobuf" }, + { name = "sympy" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/38/31db1b232b4ba960065a90c1506ad7a56995cd8482033184e97fadca17cc/onnxruntime-1.24.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cad1c2b3f455c55678ab2a8caa51fb420c25e6e3cf10f4c23653cdabedc8de78", size = 17341875, upload-time = "2026-03-17T22:05:51.669Z" }, + { url = "https://files.pythonhosted.org/packages/aa/60/c4d1c8043eb42f8a9aa9e931c8c293d289c48ff463267130eca97d13357f/onnxruntime-1.24.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1a5c5a544b22f90859c88617ecb30e161ee3349fcc73878854f43d77f00558b5", size = 15172485, upload-time = "2026-03-17T22:03:32.182Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ab/5b68110e0460d73fad814d5bd11c7b1ddcce5c37b10177eb264d6a36e331/onnxruntime-1.24.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d640eb9f3782689b55cfa715094474cd5662f2f137be6a6f847a594b6e9705c", size = 17244912, upload-time = "2026-03-17T22:04:37.251Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f4/6b89e297b93704345f0f3f8c62229bee323ef25682a3f9b4f89a39324950/onnxruntime-1.24.4-cp312-cp312-win_amd64.whl", hash = "sha256:535b29475ca42b593c45fbb2152fbf1cdf3f287315bf650e6a724a0a1d065cdb", size = 12596856, upload-time = "2026-03-17T22:05:41.224Z" }, + { url = "https://files.pythonhosted.org/packages/43/06/8b8ec6e9e6a474fcd5d772453f627ad4549dfe3ab8c0bf70af5afcde551b/onnxruntime-1.24.4-cp312-cp312-win_arm64.whl", hash = "sha256:e6214096e14b7b52e3bee1903dc12dc7ca09cb65e26664668a4620cc5e6f9a90", size = 12270275, upload-time = "2026-03-17T22:05:31.132Z" }, + { url = "https://files.pythonhosted.org/packages/e9/f0/8a21ec0a97e40abb7d8da1e8b20fb9e1af509cc6d191f6faa75f73622fb2/onnxruntime-1.24.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e99a48078baaefa2b50fe5836c319499f71f13f76ed32d0211f39109147a49e0", size = 17341922, upload-time = "2026-03-17T22:03:56.364Z" }, + { url = "https://files.pythonhosted.org/packages/8b/25/d7908de8e08cee9abfa15b8aa82349b79733ae5865162a3609c11598805d/onnxruntime-1.24.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4aaed1e5e1aaacf2343c838a30a7c3ade78f13eeb16817411f929d04040a13", size = 15172290, upload-time = "2026-03-17T22:03:37.124Z" }, + { url = "https://files.pythonhosted.org/packages/7f/72/105ec27a78c5aa0154a7c0cd8c41c19a97799c3b12fc30392928997e3be3/onnxruntime-1.24.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e30c972bc02e072911aabb6891453ec73795386c0af2b761b65444b8a4c4745f", size = 17244738, upload-time = "2026-03-17T22:04:40.625Z" }, + { url = "https://files.pythonhosted.org/packages/05/fb/a592736d968c2f58e12de4d52088dda8e0e724b26ad5c0487263adb45875/onnxruntime-1.24.4-cp313-cp313-win_amd64.whl", hash = "sha256:3b6ba8b0181a3aa88edab00eb01424ffc06f42e71095a91186c2249415fcff93", size = 12597435, upload-time = "2026-03-17T22:05:43.826Z" }, + { url = "https://files.pythonhosted.org/packages/ad/04/ae2479e9841b64bd2eb44f8a64756c62593f896514369a11243b1b86ca5c/onnxruntime-1.24.4-cp313-cp313-win_arm64.whl", hash = "sha256:71d6a5c1821d6e8586a024000ece458db8f2fc0ecd050435d45794827ce81e19", size = 12269852, upload-time = "2026-03-17T22:05:33.353Z" }, + { url = "https://files.pythonhosted.org/packages/b4/af/a479a536c4398ffaf49fbbe755f45d5b8726bdb4335ab31b537f3d7149b8/onnxruntime-1.24.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1700f559c8086d06b2a4d5de51e62cb4ff5e2631822f71a36db8c72383db71ee", size = 15176861, upload-time = "2026-03-17T22:03:40.143Z" }, + { url = "https://files.pythonhosted.org/packages/be/13/19f5da70c346a76037da2c2851ecbf1266e61d7f0dcdb887c667210d4608/onnxruntime-1.24.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c74e268dc808e61e63784d43f9ddcdaf50a776c2819e8bd1d1b11ef64bf7e36", size = 17247454, upload-time = "2026-03-17T22:04:46.643Z" }, + { url = "https://files.pythonhosted.org/packages/89/db/b30dbbd6037847b205ab75d962bc349bf1e46d02a65b30d7047a6893ffd6/onnxruntime-1.24.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:fbff2a248940e3398ae78374c5a839e49a2f39079b488bc64439fa0ec327a3e4", size = 17343300, upload-time = "2026-03-17T22:03:59.223Z" }, + { url = "https://files.pythonhosted.org/packages/61/88/1746c0e7959961475b84c776d35601a21d445f463c93b1433a409ec3e188/onnxruntime-1.24.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e2b7969e72d8cb53ffc88ab6d49dd5e75c1c663bda7be7eb0ece192f127343d1", size = 15175936, upload-time = "2026-03-17T22:03:43.671Z" }, + { url = "https://files.pythonhosted.org/packages/5f/ba/4699cde04a52cece66cbebc85bd8335a0d3b9ad485abc9a2e15946a1349d/onnxruntime-1.24.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14ed1f197fab812b695a5eaddb536c635e58a2fbbe50a517c78f082cc6ce9177", size = 17246432, upload-time = "2026-03-17T22:04:49.58Z" }, + { url = "https://files.pythonhosted.org/packages/ef/60/4590910841bb28bd3b4b388a9efbedf4e2d2cca99ddf0c863642b4e87814/onnxruntime-1.24.4-cp314-cp314-win_amd64.whl", hash = "sha256:311e309f573bf3c12aa5723e23823077f83d5e412a18499d4485c7eb41040858", size = 12903276, upload-time = "2026-03-17T22:05:46.349Z" }, + { url = "https://files.pythonhosted.org/packages/7f/6f/60e2c0acea1e1ac09b3e794b5a19c166eebf91c0b860b3e6db8e74983fda/onnxruntime-1.24.4-cp314-cp314-win_arm64.whl", hash = "sha256:3f0b910e86b759a4732663ec61fd57ac42ee1b0066f68299de164220b660546d", size = 12594365, upload-time = "2026-03-17T22:05:35.795Z" }, + { url = "https://files.pythonhosted.org/packages/cf/68/0c05d10f8f6c40fe0912ebec0d5a33884aaa2af2053507e864dab0883208/onnxruntime-1.24.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa12ddc54c9c4594073abcaa265cd9681e95fb89dae982a6f508a794ca42e661", size = 15176889, upload-time = "2026-03-17T22:03:48.021Z" }, + { url = "https://files.pythonhosted.org/packages/6c/1d/1666dc64e78d8587d168fec4e3b7922b92eb286a2ddeebcf6acb55c7dc82/onnxruntime-1.24.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1cc6a518255f012134bc791975a6294806be9a3b20c4a54cca25194c90cf731", size = 17247021, upload-time = "2026-03-17T22:04:52.377Z" }, +] + [[package]] name = "packaging" version = "25.0" @@ -505,6 +889,75 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, ] +[[package]] +name = "pillow" +version = "12.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/42/5c74462b4fd957fcd7b13b04fb3205ff8349236ea74c7c375766d6c82288/pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4", size = 46980264, upload-time = "2026-02-11T04:23:07.146Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/d3/8df65da0d4df36b094351dce696f2989bec731d4f10e743b1c5f4da4d3bf/pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052", size = 5262803, upload-time = "2026-02-11T04:20:47.653Z" }, + { url = "https://files.pythonhosted.org/packages/d6/71/5026395b290ff404b836e636f51d7297e6c83beceaa87c592718747e670f/pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984", size = 4657601, upload-time = "2026-02-11T04:20:49.328Z" }, + { url = "https://files.pythonhosted.org/packages/b1/2e/1001613d941c67442f745aff0f7cc66dd8df9a9c084eb497e6a543ee6f7e/pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79", size = 6234995, upload-time = "2026-02-11T04:20:51.032Z" }, + { url = "https://files.pythonhosted.org/packages/07/26/246ab11455b2549b9233dbd44d358d033a2f780fa9007b61a913c5b2d24e/pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293", size = 8045012, upload-time = "2026-02-11T04:20:52.882Z" }, + { url = "https://files.pythonhosted.org/packages/b2/8b/07587069c27be7535ac1fe33874e32de118fbd34e2a73b7f83436a88368c/pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397", size = 6349638, upload-time = "2026-02-11T04:20:54.444Z" }, + { url = "https://files.pythonhosted.org/packages/ff/79/6df7b2ee763d619cda2fb4fea498e5f79d984dae304d45a8999b80d6cf5c/pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0", size = 7041540, upload-time = "2026-02-11T04:20:55.97Z" }, + { url = "https://files.pythonhosted.org/packages/2c/5e/2ba19e7e7236d7529f4d873bdaf317a318896bac289abebd4bb00ef247f0/pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3", size = 6462613, upload-time = "2026-02-11T04:20:57.542Z" }, + { url = "https://files.pythonhosted.org/packages/03/03/31216ec124bb5c3dacd74ce8efff4cc7f52643653bad4825f8f08c697743/pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35", size = 7166745, upload-time = "2026-02-11T04:20:59.196Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e7/7c4552d80052337eb28653b617eafdef39adfb137c49dd7e831b8dc13bc5/pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a", size = 6328823, upload-time = "2026-02-11T04:21:01.385Z" }, + { url = "https://files.pythonhosted.org/packages/3d/17/688626d192d7261bbbf98846fc98995726bddc2c945344b65bec3a29d731/pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6", size = 7033367, upload-time = "2026-02-11T04:21:03.536Z" }, + { url = "https://files.pythonhosted.org/packages/ed/fe/a0ef1f73f939b0eca03ee2c108d0043a87468664770612602c63266a43c4/pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523", size = 2453811, upload-time = "2026-02-11T04:21:05.116Z" }, + { url = "https://files.pythonhosted.org/packages/d5/11/6db24d4bd7685583caeae54b7009584e38da3c3d4488ed4cd25b439de486/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e", size = 4062689, upload-time = "2026-02-11T04:21:06.804Z" }, + { url = "https://files.pythonhosted.org/packages/33/c0/ce6d3b1fe190f0021203e0d9b5b99e57843e345f15f9ef22fcd43842fd21/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9", size = 4138535, upload-time = "2026-02-11T04:21:08.452Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c6/d5eb6a4fb32a3f9c21a8c7613ec706534ea1cf9f4b3663e99f0d83f6fca8/pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6", size = 3601364, upload-time = "2026-02-11T04:21:10.194Z" }, + { url = "https://files.pythonhosted.org/packages/14/a1/16c4b823838ba4c9c52c0e6bbda903a3fe5a1bdbf1b8eb4fff7156f3e318/pillow-12.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60", size = 5262561, upload-time = "2026-02-11T04:21:11.742Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ad/ad9dc98ff24f485008aa5cdedaf1a219876f6f6c42a4626c08bc4e80b120/pillow-12.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2", size = 4657460, upload-time = "2026-02-11T04:21:13.786Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/f1a4ea9a895b5732152789326202a82464d5254759fbacae4deea3069334/pillow-12.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850", size = 6232698, upload-time = "2026-02-11T04:21:15.949Z" }, + { url = "https://files.pythonhosted.org/packages/95/f4/86f51b8745070daf21fd2e5b1fe0eb35d4db9ca26e6d58366562fb56a743/pillow-12.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289", size = 8041706, upload-time = "2026-02-11T04:21:17.723Z" }, + { url = "https://files.pythonhosted.org/packages/29/9b/d6ecd956bb1266dd1045e995cce9b8d77759e740953a1c9aad9502a0461e/pillow-12.1.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e", size = 6346621, upload-time = "2026-02-11T04:21:19.547Z" }, + { url = "https://files.pythonhosted.org/packages/71/24/538bff45bde96535d7d998c6fed1a751c75ac7c53c37c90dc2601b243893/pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717", size = 7038069, upload-time = "2026-02-11T04:21:21.378Z" }, + { url = "https://files.pythonhosted.org/packages/94/0e/58cb1a6bc48f746bc4cb3adb8cabff73e2742c92b3bf7a220b7cf69b9177/pillow-12.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a", size = 6460040, upload-time = "2026-02-11T04:21:23.148Z" }, + { url = "https://files.pythonhosted.org/packages/6c/57/9045cb3ff11eeb6c1adce3b2d60d7d299d7b273a2e6c8381a524abfdc474/pillow-12.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029", size = 7164523, upload-time = "2026-02-11T04:21:25.01Z" }, + { url = "https://files.pythonhosted.org/packages/73/f2/9be9cb99f2175f0d4dbadd6616ce1bf068ee54a28277ea1bf1fbf729c250/pillow-12.1.1-cp313-cp313-win32.whl", hash = "sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b", size = 6332552, upload-time = "2026-02-11T04:21:27.238Z" }, + { url = "https://files.pythonhosted.org/packages/3f/eb/b0834ad8b583d7d9d42b80becff092082a1c3c156bb582590fcc973f1c7c/pillow-12.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1", size = 7040108, upload-time = "2026-02-11T04:21:29.462Z" }, + { url = "https://files.pythonhosted.org/packages/d5/7d/fc09634e2aabdd0feabaff4a32f4a7d97789223e7c2042fd805ea4b4d2c2/pillow-12.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a", size = 2453712, upload-time = "2026-02-11T04:21:31.072Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/b9d62794fc8a0dd14c1943df68347badbd5511103e0d04c035ffe5cf2255/pillow-12.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da", size = 5264880, upload-time = "2026-02-11T04:21:32.865Z" }, + { url = "https://files.pythonhosted.org/packages/26/9d/e03d857d1347fa5ed9247e123fcd2a97b6220e15e9cb73ca0a8d91702c6e/pillow-12.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc", size = 4660616, upload-time = "2026-02-11T04:21:34.97Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ec/8a6d22afd02570d30954e043f09c32772bfe143ba9285e2fdb11284952cd/pillow-12.1.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c", size = 6269008, upload-time = "2026-02-11T04:21:36.623Z" }, + { url = "https://files.pythonhosted.org/packages/3d/1d/6d875422c9f28a4a361f495a5f68d9de4a66941dc2c619103ca335fa6446/pillow-12.1.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8", size = 8073226, upload-time = "2026-02-11T04:21:38.585Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cd/134b0b6ee5eda6dc09e25e24b40fdafe11a520bc725c1d0bbaa5e00bf95b/pillow-12.1.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20", size = 6380136, upload-time = "2026-02-11T04:21:40.562Z" }, + { url = "https://files.pythonhosted.org/packages/7a/a9/7628f013f18f001c1b98d8fffe3452f306a70dc6aba7d931019e0492f45e/pillow-12.1.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13", size = 7067129, upload-time = "2026-02-11T04:21:42.521Z" }, + { url = "https://files.pythonhosted.org/packages/1e/f8/66ab30a2193b277785601e82ee2d49f68ea575d9637e5e234faaa98efa4c/pillow-12.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf", size = 6491807, upload-time = "2026-02-11T04:21:44.22Z" }, + { url = "https://files.pythonhosted.org/packages/da/0b/a877a6627dc8318fdb84e357c5e1a758c0941ab1ddffdafd231983788579/pillow-12.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524", size = 7190954, upload-time = "2026-02-11T04:21:46.114Z" }, + { url = "https://files.pythonhosted.org/packages/83/43/6f732ff85743cf746b1361b91665d9f5155e1483817f693f8d57ea93147f/pillow-12.1.1-cp313-cp313t-win32.whl", hash = "sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986", size = 6336441, upload-time = "2026-02-11T04:21:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/3b/44/e865ef3986611bb75bfabdf94a590016ea327833f434558801122979cd0e/pillow-12.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c", size = 7045383, upload-time = "2026-02-11T04:21:50.015Z" }, + { url = "https://files.pythonhosted.org/packages/a8/c6/f4fb24268d0c6908b9f04143697ea18b0379490cb74ba9e8d41b898bd005/pillow-12.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3", size = 2456104, upload-time = "2026-02-11T04:21:51.633Z" }, + { url = "https://files.pythonhosted.org/packages/03/d0/bebb3ffbf31c5a8e97241476c4cf8b9828954693ce6744b4a2326af3e16b/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af", size = 4062652, upload-time = "2026-02-11T04:21:53.19Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c0/0e16fb0addda4851445c28f8350d8c512f09de27bbb0d6d0bbf8b6709605/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f", size = 4138823, upload-time = "2026-02-11T04:22:03.088Z" }, + { url = "https://files.pythonhosted.org/packages/6b/fb/6170ec655d6f6bb6630a013dd7cf7bc218423d7b5fa9071bf63dc32175ae/pillow-12.1.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642", size = 3601143, upload-time = "2026-02-11T04:22:04.909Z" }, + { url = "https://files.pythonhosted.org/packages/59/04/dc5c3f297510ba9a6837cbb318b87dd2b8f73eb41a43cc63767f65cb599c/pillow-12.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd", size = 5266254, upload-time = "2026-02-11T04:22:07.656Z" }, + { url = "https://files.pythonhosted.org/packages/05/30/5db1236b0d6313f03ebf97f5e17cda9ca060f524b2fcc875149a8360b21c/pillow-12.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202", size = 4657499, upload-time = "2026-02-11T04:22:09.613Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/008d2ca0eb612e81968e8be0bbae5051efba24d52debf930126d7eaacbba/pillow-12.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f", size = 6232137, upload-time = "2026-02-11T04:22:11.434Z" }, + { url = "https://files.pythonhosted.org/packages/70/f1/f14d5b8eeb4b2cd62b9f9f847eb6605f103df89ef619ac68f92f748614ea/pillow-12.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f", size = 8042721, upload-time = "2026-02-11T04:22:13.321Z" }, + { url = "https://files.pythonhosted.org/packages/5a/d6/17824509146e4babbdabf04d8171491fa9d776f7061ff6e727522df9bd03/pillow-12.1.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f", size = 6347798, upload-time = "2026-02-11T04:22:15.449Z" }, + { url = "https://files.pythonhosted.org/packages/d1/ee/c85a38a9ab92037a75615aba572c85ea51e605265036e00c5b67dfafbfe2/pillow-12.1.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e", size = 7039315, upload-time = "2026-02-11T04:22:17.24Z" }, + { url = "https://files.pythonhosted.org/packages/ec/f3/bc8ccc6e08a148290d7523bde4d9a0d6c981db34631390dc6e6ec34cacf6/pillow-12.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0", size = 6462360, upload-time = "2026-02-11T04:22:19.111Z" }, + { url = "https://files.pythonhosted.org/packages/f6/ab/69a42656adb1d0665ab051eec58a41f169ad295cf81ad45406963105408f/pillow-12.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb", size = 7165438, upload-time = "2026-02-11T04:22:21.041Z" }, + { url = "https://files.pythonhosted.org/packages/02/46/81f7aa8941873f0f01d4b55cc543b0a3d03ec2ee30d617a0448bf6bd6dec/pillow-12.1.1-cp314-cp314-win32.whl", hash = "sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f", size = 6431503, upload-time = "2026-02-11T04:22:22.833Z" }, + { url = "https://files.pythonhosted.org/packages/40/72/4c245f7d1044b67affc7f134a09ea619d4895333d35322b775b928180044/pillow-12.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15", size = 7176748, upload-time = "2026-02-11T04:22:24.64Z" }, + { url = "https://files.pythonhosted.org/packages/e4/ad/8a87bdbe038c5c698736e3348af5c2194ffb872ea52f11894c95f9305435/pillow-12.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f", size = 2544314, upload-time = "2026-02-11T04:22:26.685Z" }, + { url = "https://files.pythonhosted.org/packages/6c/9d/efd18493f9de13b87ede7c47e69184b9e859e4427225ea962e32e56a49bc/pillow-12.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8", size = 5268612, upload-time = "2026-02-11T04:22:29.884Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f1/4f42eb2b388eb2ffc660dcb7f7b556c1015c53ebd5f7f754965ef997585b/pillow-12.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9", size = 4660567, upload-time = "2026-02-11T04:22:31.799Z" }, + { url = "https://files.pythonhosted.org/packages/01/54/df6ef130fa43e4b82e32624a7b821a2be1c5653a5fdad8469687a7db4e00/pillow-12.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60", size = 6269951, upload-time = "2026-02-11T04:22:33.921Z" }, + { url = "https://files.pythonhosted.org/packages/a9/48/618752d06cc44bb4aae8ce0cd4e6426871929ed7b46215638088270d9b34/pillow-12.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7", size = 8074769, upload-time = "2026-02-11T04:22:35.877Z" }, + { url = "https://files.pythonhosted.org/packages/c3/bd/f1d71eb39a72fa088d938655afba3e00b38018d052752f435838961127d8/pillow-12.1.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f", size = 6381358, upload-time = "2026-02-11T04:22:37.698Z" }, + { url = "https://files.pythonhosted.org/packages/64/ef/c784e20b96674ed36a5af839305f55616f8b4f8aa8eeccf8531a6e312243/pillow-12.1.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586", size = 7068558, upload-time = "2026-02-11T04:22:39.597Z" }, + { url = "https://files.pythonhosted.org/packages/73/cb/8059688b74422ae61278202c4e1ad992e8a2e7375227be0a21c6b87ca8d5/pillow-12.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce", size = 6493028, upload-time = "2026-02-11T04:22:42.73Z" }, + { url = "https://files.pythonhosted.org/packages/c6/da/e3c008ed7d2dd1f905b15949325934510b9d1931e5df999bb15972756818/pillow-12.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8", size = 7191940, upload-time = "2026-02-11T04:22:44.543Z" }, + { url = "https://files.pythonhosted.org/packages/01/4a/9202e8d11714c1fc5951f2e1ef362f2d7fbc595e1f6717971d5dd750e969/pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36", size = 6438736, upload-time = "2026-02-11T04:22:46.347Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ca/cbce2327eb9885476b3957b2e82eb12c866a8b16ad77392864ad601022ce/pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b", size = 7182894, upload-time = "2026-02-11T04:22:48.114Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d2/de599c95ba0a973b94410477f8bf0b6f0b5e67360eb89bcb1ad365258beb/pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334", size = 2546446, upload-time = "2026-02-11T04:22:50.342Z" }, +] + [[package]] name = "pluggy" version = "1.6.0" @@ -514,6 +967,49 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] +[[package]] +name = "protobuf" +version = "7.34.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/6b/a0e95cad1ad7cc3f2c6821fcab91671bd5b78bd42afb357bb4765f29bc41/protobuf-7.34.1.tar.gz", hash = "sha256:9ce42245e704cc5027be797c1db1eb93184d44d1cdd71811fb2d9b25ad541280", size = 454708, upload-time = "2026-03-20T17:34:47.036Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/11/3325d41e6ee15bf1125654301211247b042563bcc898784351252549a8ad/protobuf-7.34.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:d8b2cc79c4d8f62b293ad9b11ec3aebce9af481fa73e64556969f7345ebf9fc7", size = 429247, upload-time = "2026-03-20T17:34:37.024Z" }, + { url = "https://files.pythonhosted.org/packages/eb/9d/aa69df2724ff63efa6f72307b483ce0827f4347cc6d6df24b59e26659fef/protobuf-7.34.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:5185e0e948d07abe94bb76ec9b8416b604cfe5da6f871d67aad30cbf24c3110b", size = 325753, upload-time = "2026-03-20T17:34:38.751Z" }, + { url = "https://files.pythonhosted.org/packages/92/e8/d174c91fd48e50101943f042b09af9029064810b734e4160bbe282fa1caa/protobuf-7.34.1-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:403b093a6e28a960372b44e5eb081775c9b056e816a8029c61231743d63f881a", size = 340198, upload-time = "2026-03-20T17:34:39.871Z" }, + { url = "https://files.pythonhosted.org/packages/53/1b/3b431694a4dc6d37b9f653f0c64b0a0d9ec074ee810710c0c3da21d67ba7/protobuf-7.34.1-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:8ff40ce8cd688f7265326b38d5a1bed9bfdf5e6723d49961432f83e21d5713e4", size = 324267, upload-time = "2026-03-20T17:34:41.1Z" }, + { url = "https://files.pythonhosted.org/packages/85/29/64de04a0ac142fb685fd09999bc3d337943fb386f3a0ec57f92fd8203f97/protobuf-7.34.1-cp310-abi3-win32.whl", hash = "sha256:34b84ce27680df7cca9f231043ada0daa55d0c44a2ddfaa58ec1d0d89d8bf60a", size = 426628, upload-time = "2026-03-20T17:34:42.536Z" }, + { url = "https://files.pythonhosted.org/packages/4d/87/cb5e585192a22b8bd457df5a2c16a75ea0db9674c3a0a39fc9347d84e075/protobuf-7.34.1-cp310-abi3-win_amd64.whl", hash = "sha256:e97b55646e6ce5cbb0954a8c28cd39a5869b59090dfaa7df4598a7fba869468c", size = 437901, upload-time = "2026-03-20T17:34:44.112Z" }, + { url = "https://files.pythonhosted.org/packages/88/95/608f665226bca68b736b79e457fded9a2a38c4f4379a4a7614303d9db3bc/protobuf-7.34.1-py3-none-any.whl", hash = "sha256:bb3812cd53aefea2b028ef42bd780f5b96407247f20c6ef7c679807e9d188f11", size = 170715, upload-time = "2026-03-20T17:34:45.384Z" }, +] + +[[package]] +name = "py-rust-stemmers" +version = "0.1.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/63/4fbc14810c32d2a884e2e94e406a7d5bf8eee53e1103f558433817230342/py_rust_stemmers-0.1.5.tar.gz", hash = "sha256:e9c310cfb5c2470d7c7c8a0484725965e7cab8b1237e106a0863d5741da3e1f7", size = 9388, upload-time = "2025-02-19T13:56:28.708Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e1/ea8ac92454a634b1bb1ee0a89c2f75a4e6afec15a8412527e9bbde8c6b7b/py_rust_stemmers-0.1.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:29772837126a28263bf54ecd1bc709dd569d15a94d5e861937813ce51e8a6df4", size = 286085, upload-time = "2025-02-19T13:55:23.871Z" }, + { url = "https://files.pythonhosted.org/packages/cb/32/fe1cc3d36a19c1ce39792b1ed151ddff5ee1d74c8801f0e93ff36e65f885/py_rust_stemmers-0.1.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4d62410ada44a01e02974b85d45d82f4b4c511aae9121e5f3c1ba1d0bea9126b", size = 272021, upload-time = "2025-02-19T13:55:25.685Z" }, + { url = "https://files.pythonhosted.org/packages/0a/38/b8f94e5e886e7ab181361a0911a14fb923b0d05b414de85f427e773bf445/py_rust_stemmers-0.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b28ef729a4c83c7d9418be3c23c0372493fcccc67e86783ff04596ef8a208cdf", size = 310547, upload-time = "2025-02-19T13:55:26.891Z" }, + { url = "https://files.pythonhosted.org/packages/a9/08/62e97652d359b75335486f4da134a6f1c281f38bd3169ed6ecfb276448c3/py_rust_stemmers-0.1.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a979c3f4ff7ad94a0d4cf566ca7bfecebb59e66488cc158e64485cf0c9a7879f", size = 315237, upload-time = "2025-02-19T13:55:28.116Z" }, + { url = "https://files.pythonhosted.org/packages/1c/b9/fc0278432f288d2be4ee4d5cc80fd8013d604506b9b0503e8b8cae4ba1c3/py_rust_stemmers-0.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c3593d895453fa06bf70a7b76d6f00d06def0f91fc253fe4260920650c5e078", size = 324419, upload-time = "2025-02-19T13:55:29.211Z" }, + { url = "https://files.pythonhosted.org/packages/6b/5b/74e96eaf622fe07e83c5c389d101540e305e25f76a6d0d6fb3d9e0506db8/py_rust_stemmers-0.1.5-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:96ccc7fd042ffc3f7f082f2223bb7082ed1423aa6b43d5d89ab23e321936c045", size = 324792, upload-time = "2025-02-19T13:55:30.948Z" }, + { url = "https://files.pythonhosted.org/packages/4f/f7/b76816d7d67166e9313915ad486c21d9e7da0ac02703e14375bb1cb64b5a/py_rust_stemmers-0.1.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef18cfced2c9c676e0d7d172ba61c3fab2aa6969db64cc8f5ca33a7759efbefe", size = 488014, upload-time = "2025-02-19T13:55:32.066Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ed/7d9bed02f78d85527501f86a867cd5002d97deb791b9a6b1b45b00100010/py_rust_stemmers-0.1.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:541d4b5aa911381e3d37ec483abb6a2cf2351b4f16d5e8d77f9aa2722956662a", size = 575582, upload-time = "2025-02-19T13:55:34.005Z" }, + { url = "https://files.pythonhosted.org/packages/93/40/eafd1b33688e8e8ae946d1ef25c4dc93f5b685bd104b9c5573405d7e1d30/py_rust_stemmers-0.1.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ffd946a36e9ac17ca96821963663012e04bc0ee94d21e8b5ae034721070b436c", size = 493267, upload-time = "2025-02-19T13:55:35.294Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6a/15135b69e4fd28369433eb03264d201b1b0040ba534b05eddeb02a276684/py_rust_stemmers-0.1.5-cp312-none-win_amd64.whl", hash = "sha256:6ed61e1207f3b7428e99b5d00c055645c6415bb75033bff2d06394cbe035fd8e", size = 209395, upload-time = "2025-02-19T13:55:36.519Z" }, + { url = "https://files.pythonhosted.org/packages/80/b8/030036311ec25952bf3083b6c105be5dee052a71aa22d5fbeb857ebf8c1c/py_rust_stemmers-0.1.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:398b3a843a9cd4c5d09e726246bc36f66b3d05b0a937996814e91f47708f5db5", size = 286086, upload-time = "2025-02-19T13:55:37.581Z" }, + { url = "https://files.pythonhosted.org/packages/ed/be/0465dcb3a709ee243d464e89231e3da580017f34279d6304de291d65ccb0/py_rust_stemmers-0.1.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4e308fc7687901f0c73603203869908f3156fa9c17c4ba010a7fcc98a7a1c5f2", size = 272019, upload-time = "2025-02-19T13:55:39.183Z" }, + { url = "https://files.pythonhosted.org/packages/ab/b6/76ca5b1f30cba36835938b5d9abee0c130c81833d51b9006264afdf8df3c/py_rust_stemmers-0.1.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f9efc4da5e734bdd00612e7506de3d0c9b7abc4b89d192742a0569d0d1fe749", size = 310545, upload-time = "2025-02-19T13:55:40.339Z" }, + { url = "https://files.pythonhosted.org/packages/56/8f/5be87618cea2fe2e70e74115a20724802bfd06f11c7c43514b8288eb6514/py_rust_stemmers-0.1.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cc2cc8d2b36bc05b8b06506199ac63d437360ae38caefd98cd19e479d35afd42", size = 315236, upload-time = "2025-02-19T13:55:41.55Z" }, + { url = "https://files.pythonhosted.org/packages/00/02/ea86a316aee0f0a9d1449ad4dbffff38f4cf0a9a31045168ae8b95d8bdf8/py_rust_stemmers-0.1.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a231dc6f0b2a5f12a080dfc7abd9e6a4ea0909290b10fd0a4620e5a0f52c3d17", size = 324419, upload-time = "2025-02-19T13:55:42.693Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fd/1612c22545dcc0abe2f30fc08f30a2332f2224dd536fa1508444a9ca0e39/py_rust_stemmers-0.1.5-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5845709d48afc8b29e248f42f92431155a3d8df9ba30418301c49c6072b181b0", size = 324794, upload-time = "2025-02-19T13:55:43.896Z" }, + { url = "https://files.pythonhosted.org/packages/66/18/8a547584d7edac9e7ac9c7bdc53228d6f751c0f70a317093a77c386c8ddc/py_rust_stemmers-0.1.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e48bfd5e3ce9d223bfb9e634dc1425cf93ee57eef6f56aa9a7120ada3990d4be", size = 488014, upload-time = "2025-02-19T13:55:45.088Z" }, + { url = "https://files.pythonhosted.org/packages/3b/87/4619c395b325e26048a6e28a365afed754614788ba1f49b2eefb07621a03/py_rust_stemmers-0.1.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:35d32f6e7bdf6fd90e981765e32293a8be74def807147dea9fdc1f65d6ce382f", size = 575582, upload-time = "2025-02-19T13:55:46.436Z" }, + { url = "https://files.pythonhosted.org/packages/98/6e/214f1a889142b7df6d716e7f3fea6c41e87bd6c29046aa57e175d452b104/py_rust_stemmers-0.1.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:191ea8bf922c984631ffa20bf02ef0ad7eec0465baeaed3852779e8f97c7e7a3", size = 493269, upload-time = "2025-02-19T13:55:49.057Z" }, + { url = "https://files.pythonhosted.org/packages/e1/b9/c5185df277576f995ae34418eb2b2ac12f30835412270f9e05c52face521/py_rust_stemmers-0.1.5-cp313-none-win_amd64.whl", hash = "sha256:e564c9efdbe7621704e222b53bac265b0e4fbea788f07c814094f0ec6b80adcf", size = 209397, upload-time = "2025-02-19T13:55:50.853Z" }, +] + [[package]] name = "pycparser" version = "3.0" @@ -673,6 +1169,16 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c0/1e/5fae75a5dc478e376ab95253c2f611665a4d9e2249667387a975e00fbbcb/pymupdf4llm-0.2.7-py3-none-any.whl", hash = "sha256:3ac6b0344c8bade2c97c3d7ea5eb354c71383a8d1ca177fafc3519dd564273b7", size = 66905, upload-time = "2025-12-07T20:43:12.447Z" }, ] +[[package]] +name = "pysqlite3-binary" +version = "0.5.4.post2" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/e8/292e14aa4ed1ef3d4a70703c0103823fcd4b7d9701d9462e52ef88c2cc10/pysqlite3_binary-0.5.4.post2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b6162cd966fa563fe85b5372c3e61d11dd7903bd0f09cc185cb0a4c9125f4a0f", size = 4951088, upload-time = "2025-12-03T18:36:39.786Z" }, + { url = "https://files.pythonhosted.org/packages/5d/89/338819970e306cae579aa570091a35d01df01d95fe159f2e5002b58b7481/pysqlite3_binary-0.5.4.post2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:930c7597a0863ef3da721e538756c2768cee14cb9b8d2c037263d061b24f66a5", size = 4943344, upload-time = "2025-12-03T18:36:54.992Z" }, + { url = "https://files.pythonhosted.org/packages/cf/00/9dc79fa319ee2f2fb8dc35bd5393b9fa79936899523c9640d2ca7206c742/pysqlite3_binary-0.5.4.post2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:da62981abfbfb4b3d0a9e339932fe44f8d7f3fc62037851f89ea224409ed1767", size = 4943501, upload-time = "2025-12-03T18:37:07.853Z" }, +] + [[package]] name = "pytest" version = "9.0.2" @@ -797,6 +1303,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, ] +[[package]] +name = "requests" +version = "2.33.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/34/64/8860370b167a9721e8956ae116825caff829224fbca0ca6e7bf8ddef8430/requests-2.33.0.tar.gz", hash = "sha256:c7ebc5e8b0f21837386ad0e1c8fe8b829fa5f544d8df3b2253bff14ef29d7652", size = 134232, upload-time = "2026-03-25T15:10:41.586Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/5d/c814546c2333ceea4ba42262d8c4d55763003e767fa169adc693bd524478/requests-2.33.0-py3-none-any.whl", hash = "sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b", size = 65017, upload-time = "2026-03-25T15:10:40.382Z" }, +] + [[package]] name = "rich" version = "14.2.0" @@ -917,6 +1438,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/74/31/b0e29d572670dca3674eeee78e418f20bdf97fa8aa9ea71380885e175ca0/ruff-0.14.10-py3-none-win_arm64.whl", hash = "sha256:e51d046cf6dda98a4633b8a8a771451107413b0f07183b2bef03f075599e44e6", size = 13729839, upload-time = "2025-12-18T19:28:48.636Z" }, ] +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "sqlite-vec" +version = "0.1.7" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/50/7ad59cfd3003a2110cc366e526293de4c2520486f5ddaa8dc78b265f8d3e/sqlite_vec-0.1.7-py3-none-macosx_10_6_x86_64.whl", hash = "sha256:c34a136caecff4ae17d4c0cc268fcda89764ee870039caa21431e8e3fb2f4d48", size = 131171, upload-time = "2026-03-17T07:42:50.438Z" }, + { url = "https://files.pythonhosted.org/packages/e6/c9/1cd2f59b539096cd2ce6b540247b2dfe3c47ba04d9368b5e8e3dc86498d4/sqlite_vec-0.1.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6d272593d1b45ec7ea289b160ee6e5fafbaa6e1f5ba15f1305c012b0bda43653", size = 165434, upload-time = "2026-03-17T07:42:51.555Z" }, + { url = "https://files.pythonhosted.org/packages/75/91/30c3c382140dcc7bc6e3a07eac7ca610a2b5b70eb9bc7066dc3e7f748d58/sqlite_vec-0.1.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d27746d8e254a390bd15574aed899a0b9bb915b5321eb130a9c09722898cc03", size = 160076, upload-time = "2026-03-17T07:42:52.451Z" }, + { url = "https://files.pythonhosted.org/packages/59/56/6ff304d917ee79da769708dad0aed5fd34c72cbd0ae5e38bcc56cdc652a4/sqlite_vec-0.1.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux1_x86_64.whl", hash = "sha256:ad654283cb9c059852ce2d82018c757b06a705ada568f8b126022a131189818e", size = 163388, upload-time = "2026-03-17T07:42:53.516Z" }, + { url = "https://files.pythonhosted.org/packages/8b/27/fb1b6e3f9072854fe405f7aa99c46d4b465e84c9cec2ff7778edf29ecbbd/sqlite_vec-0.1.7-py3-none-win_amd64.whl", hash = "sha256:0c67877a87cb49426237b950237e82dbeb77778ab2ba89bea859f391fd169382", size = 292804, upload-time = "2026-03-17T07:42:54.325Z" }, +] + [[package]] name = "sse-starlette" version = "3.3.3" @@ -949,12 +1491,15 @@ version = "0.1.0" source = { editable = "." } dependencies = [ { name = "argcomplete" }, + { name = "fastembed" }, { name = "httpx" }, { name = "mcp" }, { name = "pymupdf" }, { name = "pymupdf4llm" }, + { name = "pysqlite3-binary" }, { name = "pyyaml" }, { name = "rich" }, + { name = "sqlite-vec" }, ] [package.optional-dependencies] @@ -968,19 +1513,34 @@ dev = [ [package.metadata] requires-dist = [ { name = "argcomplete", specifier = ">=3.0" }, + { name = "fastembed", specifier = ">=0.4" }, { name = "httpx", specifier = ">=0.27" }, { name = "mcp", specifier = ">=1.9" }, { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.0" }, { name = "pymupdf", specifier = ">=1.24" }, { name = "pymupdf4llm", specifier = ">=0.0.17" }, + { name = "pysqlite3-binary", specifier = ">=0.5" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0" }, { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.0" }, { name = "pyyaml", specifier = ">=6.0" }, { name = "rich", specifier = ">=13.0" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.1" }, + { name = "sqlite-vec", specifier = ">=0.1" }, ] provides-extras = ["dev"] +[[package]] +name = "sympy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, +] + [[package]] name = "tabulate" version = "0.9.0" @@ -990,6 +1550,59 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252, upload-time = "2022-10-06T17:21:44.262Z" }, ] +[[package]] +name = "tokenizers" +version = "0.22.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" }, + { url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" }, + { url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" }, + { url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" }, + { url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" }, + { url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" }, + { url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" }, + { url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" }, + { url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" }, + { url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, +] + +[[package]] +name = "typer" +version = "0.24.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/24/cb09efec5cc954f7f9b930bf8279447d24618bb6758d4f6adf2574c41780/typer-0.24.1.tar.gz", hash = "sha256:e39b4732d65fbdcde189ae76cf7cd48aeae72919dea1fdfc16593be016256b45", size = 118613, upload-time = "2026-02-21T16:54:40.609Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl", hash = "sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e", size = 56085, upload-time = "2026-02-21T16:54:41.616Z" }, +] + [[package]] name = "typing-extensions" version = "4.15.0" @@ -1011,6 +1624,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + [[package]] name = "uvicorn" version = "0.42.0" @@ -1023,3 +1645,12 @@ sdist = { url = "https://files.pythonhosted.org/packages/e3/ad/4a96c425be6fb67e0 wheels = [ { url = "https://files.pythonhosted.org/packages/0a/89/f8827ccff89c1586027a105e5630ff6139a64da2515e24dafe860bd9ae4d/uvicorn-0.42.0-py3-none-any.whl", hash = "sha256:96c30f5c7abe6f74ae8900a70e92b85ad6613b745d4879eb9b16ccad15645359", size = 68830, upload-time = "2026-03-16T06:19:48.325Z" }, ] + +[[package]] +name = "win32-setctime" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0", size = 4867, upload-time = "2024-12-07T15:28:28.314Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083, upload-time = "2024-12-07T15:28:26.465Z" }, +] -- 2.51.2