A 5e storytelling engine with an LLM DM
0
fork

Configure Feed

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

Add local semantic vector search for recall

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) <noreply@anthropic.com>

+1998 -877
+3
.gitignore
··· 60 60 !worlds/.gitkeep 61 61 players/* 62 62 !players/.gitkeep 63 + 64 + # Search indexes (build artifacts) 65 + search.db
+7 -3
Makefile
··· 1 - .PHONY: all srd clean-srd help 1 + .PHONY: all srd search-index clean-srd help 2 2 3 3 PDF := rules/sources/SRD_CC_v5.2.1.pdf 4 4 MARKDOWN := rules/srd-5.2.1/srd.md ··· 21 21 storied srd clean 22 22 touch $@ 23 23 24 + search-index: $(SECTIONS_STAMP) 25 + storied index srd 26 + 24 27 clean-srd: 25 28 rm -rf $(SECTIONS_DIR) $(MARKDOWN) 26 29 27 30 help: 28 - @echo "make srd - Build SRD rules (download, convert, split, clean)" 29 - @echo "make clean-srd - Remove generated SRD files" 31 + @echo "make srd - Build SRD rules (download, convert, split, clean)" 32 + @echo "make search-index - Build SRD search index" 33 + @echo "make clean-srd - Remove generated SRD files"
+3
pyproject.toml
··· 11 11 12 12 dependencies = [ 13 13 "argcomplete>=3.0", 14 + "fastembed>=0.4", 14 15 "httpx>=0.27", 15 16 "mcp>=1.9", 16 17 "pymupdf>=1.24", 17 18 "pymupdf4llm>=0.0.17", 19 + "pysqlite3-binary>=0.5", 18 20 "pyyaml>=6.0", 19 21 "rich>=13.0", 22 + "sqlite-vec>=0.1", 20 23 ] 21 24 22 25 [project.scripts]
+126 -5
src/storied/cli.py
··· 648 648 return 0 649 649 650 650 651 + def cmd_index_srd(args: argparse.Namespace) -> int: 652 + """Build search index for SRD content.""" 653 + from storied.search import VectorIndex 654 + 655 + srd_dir = Path("rules/srd-5.2.1/sections") 656 + if not srd_dir.exists(): 657 + print(f"SRD sections not found at {srd_dir}") 658 + print("Run 'storied srd split' first.") 659 + return 1 660 + 661 + db_path = Path("rules/srd-5.2.1/search.db") 662 + print(f"Indexing SRD from {srd_dir}...") 663 + index = VectorIndex(db_path) 664 + count = index.reindex_directory(srd_dir, source="srd") 665 + index.close() 666 + print(f"Indexed {count} document chunks into {db_path}") 667 + return 0 668 + 669 + 670 + def cmd_index_world(args: argparse.Namespace) -> int: 671 + """Build search index for a world (includes SRD via seed copy).""" 672 + from storied.search import VectorIndex 673 + 674 + world_id = args.world or "default" 675 + world_dir = Path(f"worlds/{world_id}") 676 + if not world_dir.exists(): 677 + print(f"World not found at {world_dir}") 678 + return 1 679 + 680 + db_path = world_dir / "search.db" 681 + srd_seed = Path("rules/srd-5.2.1/search.db") 682 + 683 + # Seed from pre-built SRD index if available 684 + if srd_seed.exists() and not db_path.exists(): 685 + print(f"Seeding from {srd_seed}...") 686 + index = VectorIndex.seed_from(srd_seed, db_path) 687 + stats = index.stats() 688 + print(f" {stats['total_documents']} SRD chunks copied") 689 + else: 690 + index = VectorIndex(db_path) 691 + if not srd_seed.exists(): 692 + print("SRD index not found. Run 'storied index srd' first for faster setup.") 693 + 694 + # Index world content on top 695 + print(f"Indexing world from {world_dir}...") 696 + world_count = index.reindex_directory(world_dir, source="world") 697 + print(f" {world_count} world chunks") 698 + 699 + index.close() 700 + print(f"Index written to {db_path}") 701 + return 0 702 + 703 + 704 + def cmd_index_status(args: argparse.Namespace) -> int: 705 + """Show search index status.""" 706 + from storied.search import VectorIndex 707 + 708 + world_id = args.world or "default" 709 + db_path = Path(f"worlds/{world_id}/search.db") 710 + if not db_path.exists(): 711 + print(f"No search index at {db_path}") 712 + print(f"Run 'storied index world -w {world_id}' to create one.") 713 + return 1 714 + 715 + index = VectorIndex(db_path) 716 + stats = index.stats() 717 + index.close() 718 + 719 + print(f"Search index: {db_path}") 720 + print(f"Total documents: {stats['total_documents']}") 721 + for source, count in sorted(stats["by_source"].items()): 722 + print(f" {source}: {count}") 723 + return 0 724 + 725 + 726 + def cmd_index_rebuild(args: argparse.Namespace) -> int: 727 + """Force full rebuild of a world's search index.""" 728 + world_id = args.world or "default" 729 + db_path = Path(f"worlds/{world_id}/search.db") 730 + if db_path.exists(): 731 + db_path.unlink() 732 + print(f"Removed {db_path}") 733 + 734 + # Delegate to cmd_index_world 735 + return cmd_index_world(args) 736 + 737 + 651 738 def cmd_seed(args: argparse.Namespace) -> int: 652 739 """Seed an empty world from a character sheet.""" 653 740 from storied.planner import seed_world ··· 864 951 ) 865 952 seed_parser.set_defaults(func=cmd_seed) 866 953 954 + # index command group 955 + index_parser = subparsers.add_parser("index", help="Search index commands") 956 + index_subparsers = index_parser.add_subparsers( 957 + dest="index_command", help="Index subcommands" 958 + ) 959 + 960 + index_srd_parser = index_subparsers.add_parser( 961 + "srd", help="Build search index for SRD content" 962 + ) 963 + index_srd_parser.set_defaults(func=cmd_index_srd) 964 + 965 + index_world_parser = index_subparsers.add_parser( 966 + "world", help="Build search index for a world (includes SRD)" 967 + ) 968 + index_world_parser.add_argument( 969 + "--world", "-w", help="World ID (default: default)" 970 + ) 971 + index_world_parser.set_defaults(func=cmd_index_world) 972 + 973 + index_status_parser = index_subparsers.add_parser( 974 + "status", help="Show search index status" 975 + ) 976 + index_status_parser.add_argument( 977 + "--world", "-w", help="World ID (default: default)" 978 + ) 979 + index_status_parser.set_defaults(func=cmd_index_status) 980 + 981 + index_rebuild_parser = index_subparsers.add_parser( 982 + "rebuild", help="Force full rebuild of search index" 983 + ) 984 + index_rebuild_parser.add_argument( 985 + "--world", "-w", help="World ID (default: default)" 986 + ) 987 + index_rebuild_parser.set_defaults(func=cmd_index_rebuild) 988 + 867 989 return parser 868 990 869 991 ··· 877 999 parser.print_help() 878 1000 return 0 879 1001 880 - if args.command == "srd" and not getattr(args, "srd_command", None): 881 - # Find the srd subparser and print its help 1002 + if args.command in ("srd", "index") and not getattr(args, f"{args.command}_command", None): 882 1003 for action in parser._subparsers._actions: 883 1004 if isinstance(action, argparse._SubParsersAction): 884 - srd_parser = action.choices.get("srd") 885 - if srd_parser: 886 - srd_parser.print_help() 1005 + sub = action.choices.get(args.command) 1006 + if sub: 1007 + sub.print_help() 887 1008 return 0 888 1009 889 1010 if hasattr(args, "func"):
+1 -121
src/storied/content.py
··· 1 - """Content layer resolution and search.""" 1 + """Content layer resolution — finds and loads content across world/rules layers.""" 2 2 3 3 import re 4 - from dataclasses import dataclass 5 4 from pathlib import Path 6 5 7 6 import yaml 8 - 9 - 10 - @dataclass 11 - class SearchResult: 12 - """A search result from content search.""" 13 - 14 - name: str 15 - path: Path 16 - content_type: str 17 - snippet: str | None = None 18 7 19 8 20 9 class ContentResolver: ··· 124 113 # No frontmatter, just body 125 114 return {"body": content.strip()} 126 115 127 - def search( 128 - self, query: str, content_type: str | None = None 129 - ) -> list[SearchResult]: 130 - """Search content by keyword with fuzzy fallback. 131 - 132 - First tries exact substring matching on filenames and content. 133 - If nothing matches, falls back to word-overlap scoring. 134 - 135 - Args: 136 - query: Search term 137 - content_type: Optional category to limit search 138 - 139 - Returns: 140 - List of SearchResult objects 141 - """ 142 - results: list[SearchResult] = [] 143 - seen_names: set[str] = set() 144 - query_lower = query.lower() 145 - 146 - for search_dir, ctype in self._search_dirs(content_type): 147 - if not search_dir.exists(): 148 - continue 149 - 150 - for path in search_dir.glob("*.md"): 151 - name = path.stem 152 - 153 - if name in seen_names: 154 - continue 155 - 156 - content = path.read_text() 157 - 158 - if query_lower in name.lower() or query_lower in content.lower(): 159 - snippet = self._extract_snippet(content, query) 160 - results.append( 161 - SearchResult( 162 - name=name, 163 - path=path, 164 - content_type=ctype, 165 - snippet=snippet, 166 - ) 167 - ) 168 - seen_names.add(name) 169 - 170 - # Fuzzy fallback: word-overlap scoring when exact match fails 171 - if not results: 172 - results = self._fuzzy_search(query, content_type) 173 - 174 - return results 175 - 176 - def _fuzzy_search( 177 - self, query: str, content_type: str | None = None 178 - ) -> list[SearchResult]: 179 - """Word-overlap fallback when exact search finds nothing.""" 180 - query_words = {w for w in query.lower().split() if len(w) > 2} 181 - if not query_words: 182 - return [] 183 - 184 - scored: list[tuple[float, SearchResult]] = [] 185 - seen_names: set[str] = set() 186 - 187 - for search_dir, ctype in self._search_dirs(content_type): 188 - if not search_dir.exists(): 189 - continue 190 - 191 - for path in search_dir.glob("*.md"): 192 - name = path.stem 193 - if name in seen_names: 194 - continue 195 - 196 - name_words = set(name.lower().replace("-", " ").split()) 197 - content = path.read_text() 198 - content_lower = content.lower() 199 - 200 - # Score: name matches count double 201 - name_hits = len(query_words & name_words) 202 - content_hits = sum(1 for w in query_words if w in content_lower) 203 - score = name_hits * 2 + content_hits 204 - 205 - if score > 0: 206 - snippet = content[:100].strip() + ("..." if len(content) > 100 else "") 207 - scored.append(( 208 - score, 209 - SearchResult(name=name, path=path, content_type=ctype, snippet=snippet), 210 - )) 211 - seen_names.add(name) 212 - 213 - scored.sort(key=lambda x: x[0], reverse=True) 214 - return [r for _, r in scored[:5]] 215 - 216 - def _extract_snippet(self, content: str, query: str, context: int = 50) -> str: 217 - """Extract a snippet of text around the query match.""" 218 - query_lower = query.lower() 219 - content_lower = content.lower() 220 - 221 - pos = content_lower.find(query_lower) 222 - if pos == -1: 223 - # Match was in filename, return start of content 224 - return content[:100].strip() + "..." if len(content) > 100 else content 225 - 226 - start = max(0, pos - context) 227 - end = min(len(content), pos + len(query) + context) 228 - 229 - snippet = content[start:end].strip() 230 - if start > 0: 231 - snippet = "..." + snippet 232 - if end < len(content): 233 - snippet = snippet + "..." 234 - 235 - return snippet
+20 -15
src/storied/engine.py
··· 312 312 if not self.world_id: 313 313 return None 314 314 315 - # Fast path: use the entity index from the MCP server 316 - entity_index = self._mcp.entity_index 317 - if entity_index: 318 - path = entity_index.resolve(name) 319 - if path and path.exists(): 320 - return { 321 - "name": name, 322 - "body": path.read_text(), 323 - "entity_type": path.parent.name, 324 - } 325 - return None 315 + path = self._mcp.ctx.entity_index.resolve(name) 316 + if path and path.exists(): 317 + return { 318 + "name": name, 319 + "body": path.read_text(), 320 + "entity_type": path.parent.name, 321 + } 326 322 327 323 # Fallback: filesystem scan 328 324 entity = load_entity_content(name, self.world_id, self.base_path) ··· 463 459 # Write conversation turn to transcript 464 460 dm_response = "".join(dm_text_parts) 465 461 if dm_response.strip(): 466 - self._transcript.append_turn( 467 - player_input, dm_response, 468 - self._campaign_log.get_current_time(), 469 - ) 462 + game_time = self._campaign_log.get_current_time() 463 + self._transcript.append_turn(player_input, dm_response, game_time) 464 + 465 + # Index the transcript turn for semantic search 466 + day_path = self._transcript._day_path(game_time.day) 467 + if day_path.exists(): 468 + self._mcp.ctx.vector_index.upsert( 469 + f"transcript:transcripts/day+{game_time.day:03d}.md:0", 470 + day_path.read_text(), 471 + {"source": "transcript", "content_type": "transcripts", 472 + "path": str(day_path), "title": f"Day {game_time.day}", 473 + "game_day": game_time.day}, 474 + ) 470 475 471 476 def reset(self) -> None: 472 477 """Reset the conversation state."""
+27 -17
src/storied/mcp_server.py
··· 18 18 from mcp.types import TextContent, Tool 19 19 20 20 from storied.log import CampaignLog 21 + from storied.search import VectorIndex 21 22 from storied.tools import ( 22 23 EntityIndex, 23 24 PLANNER_TOOL_DEFINITIONS, 24 25 SEEDER_TOOL_DEFINITIONS, 25 26 TOOL_DEFINITIONS, 27 + ToolContext, 26 28 execute_tool, 27 29 planner_execute_tool, 28 30 seeder_execute_tool, ··· 60 62 class MCPServerHandle: 61 63 """Handle to a running in-process MCP server.""" 62 64 63 - def __init__( 64 - self, port: int, thread: threading.Thread, 65 - entity_index: EntityIndex | None = None, 66 - ): 65 + def __init__(self, port: int, thread: threading.Thread, ctx: ToolContext): 67 66 self.port = port 68 67 self.url = f"http://127.0.0.1:{port}/sse" 69 - self.entity_index = entity_index 68 + self.ctx = ctx 70 69 self._thread = thread 71 70 72 71 def stop(self) -> None: ··· 91 90 campaign_log = CampaignLog(world_id, base_path) 92 91 93 92 world_dir = base_path / "worlds" / world_id 94 - entity_index = EntityIndex(world_dir) 93 + 94 + def _populate_index(vi: VectorIndex) -> None: 95 + """Auto-populate an empty index from SRD seed + world content.""" 96 + srd_seed = base_path / "rules" / "srd-5.2.1" / "search.db" 97 + if srd_seed.exists(): 98 + vi.reseed(srd_seed) 99 + else: 100 + srd_dir = base_path / "rules" / "srd-5.2.1" / "sections" 101 + if srd_dir.exists(): 102 + vi.reindex_directory(srd_dir, source="srd") 103 + if world_dir.exists(): 104 + vi.reindex_directory(world_dir, source="world") 105 + 106 + ctx = ToolContext( 107 + world_id=world_id, 108 + player_id=player_id, 109 + base_path=base_path, 110 + campaign_log=campaign_log, 111 + entity_index=EntityIndex(world_dir), 112 + vector_index=VectorIndex(world_dir / "search.db", on_empty=_populate_index), 113 + ) 95 114 96 115 definitions = TOOL_SETS.get(tool_set, TOOL_DEFINITIONS) 97 116 executor = EXECUTORS.get(tool_set, execute_tool) ··· 105 124 106 125 @mcp.call_tool() 107 126 async def call_tool(name: str, arguments: dict) -> list[TextContent]: 108 - result = executor( 109 - name, 110 - arguments, 111 - world_id=world_id, 112 - player_id=player_id, 113 - base_path=base_path, 114 - campaign_log=campaign_log, 115 - entity_index=entity_index, 116 - ) 127 + result = executor(name, arguments, ctx) 117 128 return [TextContent(type="text", text=str(result))] 118 129 119 130 sse = SseServerTransport("/messages/") ··· 144 155 thread = threading.Thread(target=server.run, daemon=True) 145 156 thread.start() 146 157 147 - # Wait for the server to be ready 148 158 for _ in range(50): 149 159 try: 150 160 with socket.create_connection(("127.0.0.1", port), timeout=0.1): ··· 152 162 except OSError: 153 163 time.sleep(0.1) 154 164 155 - return MCPServerHandle(port, thread, entity_index=entity_index) 165 + return MCPServerHandle(port, thread, ctx)
+6 -3
src/storied/planner.py
··· 16 16 load_session, 17 17 resolve_wiki_link, 18 18 ) 19 - from storied.tools import _load_entity 19 + from storied.tools import EntityIndex, _load_entity 20 + 21 + # Shared empty index for read-only entity parsing (no cache needed) 22 + _EMPTY_INDEX = EntityIndex() 20 23 21 24 22 25 def entity_richness(path: Path) -> float: ··· 30 33 - Has Was history (0.1) 31 34 - Has wikilinks to other entities (0.1) 32 35 """ 33 - entity = _load_entity(path) 36 + entity = _load_entity(path, _EMPTY_INDEX) 34 37 if not entity: 35 38 return 0.0 36 39 ··· 345 348 if not type_dir.exists(): 346 349 continue 347 350 for path in type_dir.glob("*.md"): 348 - entity = _load_entity(path) 351 + entity = _load_entity(path, _EMPTY_INDEX) 349 352 if entity and entity.get("will"): 350 353 results.append((path.stem, path)) 351 354
+387
src/storied/search.py
··· 1 + """Local semantic vector search over SRD, world, and player content. 2 + 3 + Uses fastembed (ONNX runtime) for embeddings and sqlite-vec for storage. 4 + The index lives as a single search.db file in the world directory. 5 + """ 6 + 7 + import re 8 + import shutil 9 + import struct 10 + from dataclasses import dataclass 11 + from pathlib import Path 12 + from typing import Callable 13 + 14 + import pysqlite3 as sqlite3 15 + import sqlite_vec 16 + from fastembed import TextEmbedding 17 + 18 + EMBED_MODEL = "BAAI/bge-small-en-v1.5" 19 + EMBED_DIM = 384 20 + CHUNK_CHAR_THRESHOLD = 4_000 21 + 22 + 23 + @dataclass 24 + class SearchHit: 25 + """A single result from vector search.""" 26 + 27 + doc_id: str 28 + path: Path 29 + score: float 30 + snippet: str 31 + source: str 32 + content_type: str 33 + 34 + 35 + def age_decay(current_day: int, doc_day: int, half_life: int = 3) -> float: 36 + """Exponential decay: score halves every `half_life` game days. 37 + 38 + Transcripts from today score 1.0, from `half_life` days ago score 0.5. 39 + Future docs (doc_day > current_day) are not boosted. 40 + """ 41 + age = max(0, current_day - doc_day) 42 + return 0.5 ** (age / half_life) 43 + 44 + 45 + def chunk_document(path: Path, content: str) -> list[tuple[int, str]]: 46 + """Split a document into indexed chunks for embedding. 47 + 48 + Small files return a single chunk. Larger files split on ## headers, 49 + each prefixed with the document title for context. 50 + 51 + Returns list of (chunk_index, chunk_text) pairs. 52 + """ 53 + if not content.strip(): 54 + return [(0, path.stem)] 55 + 56 + # Extract title from first # heading 57 + title_match = re.match(r"^#\s+(.+)", content) 58 + title = title_match.group(1).strip() if title_match else path.stem 59 + 60 + if len(content) < CHUNK_CHAR_THRESHOLD: 61 + return [(0, content)] 62 + 63 + # Split on ## headers 64 + sections = re.split(r"\n(?=## )", content) 65 + 66 + # If only one section (no ## headers), return as single chunk 67 + if len(sections) <= 1: 68 + return [(0, content)] 69 + 70 + chunks: list[tuple[int, str]] = [] 71 + chunk_idx = 0 72 + 73 + for section in sections: 74 + section = section.strip() 75 + if not section: 76 + continue 77 + 78 + # Prepend title to non-first chunks for context 79 + if chunk_idx > 0: 80 + text = f"# {title}\n\n{section}" 81 + else: 82 + text = section 83 + 84 + # Sub-split oversized sections on ### headers 85 + if len(text) > CHUNK_CHAR_THRESHOLD: 86 + subsections = re.split(r"\n(?=### )", text) 87 + for sub in subsections: 88 + sub = sub.strip() 89 + if sub: 90 + chunks.append((chunk_idx, sub)) 91 + chunk_idx += 1 92 + else: 93 + chunks.append((chunk_idx, text)) 94 + chunk_idx += 1 95 + 96 + return chunks if chunks else [(0, content)] 97 + 98 + 99 + def _serialize_f32(vec: list[float]) -> bytes: 100 + """Serialize a float vector to bytes for sqlite-vec.""" 101 + return struct.pack(f"<{len(vec)}f", *vec) 102 + 103 + 104 + def _default_embed(texts: list[str]) -> list[list[float]]: 105 + """Lazy-loaded fastembed model.""" 106 + if not hasattr(_default_embed, "_model"): 107 + _default_embed._model = TextEmbedding(EMBED_MODEL) # type: ignore[attr-defined] 108 + return [vec.tolist() for vec in _default_embed._model.embed(texts)] # type: ignore[attr-defined] 109 + 110 + 111 + def _connect(db_path: str) -> sqlite3.Connection: 112 + """Open a SQLite connection with sqlite-vec loaded.""" 113 + conn = sqlite3.connect(db_path) 114 + conn.enable_load_extension(True) 115 + sqlite_vec.load(conn) 116 + conn.enable_load_extension(False) 117 + return conn 118 + 119 + 120 + class VectorIndex: 121 + """SQLite-backed vector search index. 122 + 123 + Stores document metadata in a regular table and embeddings in a 124 + sqlite-vec virtual table. Supports upsert, delete, search with 125 + optional source filtering and age-decay for temporal content. 126 + 127 + If the database is missing or corrupt, it is recreated automatically. 128 + """ 129 + 130 + def __init__( 131 + self, 132 + db_path: Path, 133 + on_empty: Callable[["VectorIndex"], None] | None = None, 134 + ): 135 + self._db_path = db_path 136 + self._embed_fn: Callable[[list[str]], list[list[float]]] = _default_embed 137 + self._on_empty = on_empty 138 + self._conn = self._open_or_recreate() 139 + 140 + @staticmethod 141 + def seed_from(seed_path: Path, dest_path: Path) -> "VectorIndex": 142 + """Create a new index by copying an existing one as a seed. 143 + 144 + Used to bootstrap a world's search.db from the pre-built SRD index. 145 + """ 146 + dest_path.parent.mkdir(parents=True, exist_ok=True) 147 + shutil.copy2(seed_path, dest_path) 148 + return VectorIndex(dest_path) 149 + 150 + def reseed(self, seed_path: Path) -> None: 151 + """Replace this index's DB with a copy of the seed and reconnect.""" 152 + self._conn.close() 153 + shutil.copy2(seed_path, self._db_path) 154 + self._conn = self._open_or_recreate() 155 + 156 + def close(self) -> None: 157 + """Close the database connection.""" 158 + self._conn.close() 159 + 160 + def _open_or_recreate(self) -> sqlite3.Connection: 161 + """Open the database, recreating if corrupt.""" 162 + self._db_path.parent.mkdir(parents=True, exist_ok=True) 163 + try: 164 + conn = _connect(str(self._db_path)) 165 + conn.execute("SELECT count(*) FROM documents") 166 + return conn 167 + except Exception: 168 + if self._db_path.exists(): 169 + self._db_path.unlink() 170 + return self._create_fresh() 171 + 172 + def _create_fresh(self) -> sqlite3.Connection: 173 + """Create a new database with the required schema.""" 174 + self._db_path.parent.mkdir(parents=True, exist_ok=True) 175 + conn = _connect(str(self._db_path)) 176 + conn.execute(""" 177 + CREATE TABLE IF NOT EXISTS documents ( 178 + doc_id TEXT PRIMARY KEY, 179 + path TEXT NOT NULL, 180 + source TEXT NOT NULL, 181 + content_type TEXT, 182 + chunk_index INTEGER DEFAULT 0, 183 + title TEXT, 184 + body_preview TEXT, 185 + game_day INTEGER, 186 + updated_at REAL NOT NULL 187 + ) 188 + """) 189 + conn.execute(f""" 190 + CREATE VIRTUAL TABLE IF NOT EXISTS vec_documents USING vec0( 191 + doc_id TEXT PRIMARY KEY, 192 + embedding FLOAT[{EMBED_DIM}] 193 + ) 194 + """) 195 + conn.commit() 196 + return conn 197 + 198 + def embed(self, texts: list[str]) -> list[list[float]]: 199 + """Embed one or more texts into vectors.""" 200 + return self._embed_fn(texts) 201 + 202 + def upsert(self, doc_id: str, text: str, metadata: dict) -> None: 203 + """Insert or update a document and its embedding.""" 204 + vec = self.embed([text])[0] 205 + blob = _serialize_f32(vec) 206 + preview = text[:200].strip() 207 + 208 + self._conn.execute( 209 + "DELETE FROM vec_documents WHERE doc_id = ?", (doc_id,) 210 + ) 211 + self._conn.execute( 212 + "DELETE FROM documents WHERE doc_id = ?", (doc_id,) 213 + ) 214 + 215 + self._conn.execute( 216 + """INSERT INTO documents 217 + (doc_id, path, source, content_type, chunk_index, 218 + title, body_preview, game_day, updated_at) 219 + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""", 220 + ( 221 + doc_id, 222 + metadata.get("path", ""), 223 + metadata["source"], 224 + metadata.get("content_type"), 225 + metadata.get("chunk_index", 0), 226 + metadata.get("title"), 227 + preview, 228 + metadata.get("game_day"), 229 + metadata.get("updated_at", 0.0), 230 + ), 231 + ) 232 + self._conn.execute( 233 + "INSERT INTO vec_documents (doc_id, embedding) VALUES (?, ?)", 234 + (doc_id, blob), 235 + ) 236 + self._conn.commit() 237 + 238 + def delete(self, doc_id: str) -> None: 239 + """Remove a document and its embedding.""" 240 + self._conn.execute( 241 + "DELETE FROM vec_documents WHERE doc_id = ?", (doc_id,) 242 + ) 243 + self._conn.execute( 244 + "DELETE FROM documents WHERE doc_id = ?", (doc_id,) 245 + ) 246 + self._conn.commit() 247 + 248 + def search( 249 + self, 250 + query: str, 251 + limit: int = 5, 252 + source_filter: str | None = None, 253 + exclude_source: str | None = None, 254 + decay_ref: int | None = None, 255 + ) -> list[SearchHit]: 256 + """Search the index by semantic similarity. 257 + 258 + Args: 259 + query: Natural language search query 260 + limit: Max results to return 261 + source_filter: Restrict to a single source ("srd", "world", etc.) 262 + exclude_source: Exclude a single source from results 263 + decay_ref: Current game day for age-decay on transcripts. 264 + If None, no decay is applied. 265 + """ 266 + # Auto-populate if the index is empty (fires at most once) 267 + if self._on_empty: 268 + count = self._conn.execute( 269 + "SELECT count(*) FROM documents" 270 + ).fetchone()[0] 271 + if count == 0: 272 + self._on_empty(self) 273 + self._on_empty = None 274 + 275 + vec = self.embed([query])[0] 276 + blob = _serialize_f32(vec) 277 + 278 + fetch_limit = limit * 3 279 + 280 + rows = self._conn.execute( 281 + """SELECT v.doc_id, v.distance, d.path, d.source, 282 + d.content_type, d.body_preview, d.game_day 283 + FROM vec_documents v 284 + JOIN documents d ON v.doc_id = d.doc_id 285 + WHERE v.embedding MATCH ? 286 + AND k = ? 287 + ORDER BY v.distance""", 288 + (blob, fetch_limit), 289 + ).fetchall() 290 + 291 + hits: list[SearchHit] = [] 292 + for doc_id, distance, path, source, ctype, preview, game_day in rows: 293 + if source_filter and source != source_filter: 294 + continue 295 + if exclude_source and source == exclude_source: 296 + continue 297 + 298 + score = 1.0 / (1.0 + distance) 299 + 300 + if decay_ref is not None and game_day is not None: 301 + if source in ("transcript", "player"): 302 + score *= age_decay(decay_ref, game_day) 303 + 304 + hits.append(SearchHit( 305 + doc_id=doc_id, 306 + path=Path(path), 307 + score=score, 308 + snippet=preview or "", 309 + source=source, 310 + content_type=ctype or "", 311 + )) 312 + 313 + hits.sort(key=lambda h: h.score, reverse=True) 314 + return hits[:limit] 315 + 316 + def reindex_directory(self, directory: Path, source: str) -> int: 317 + """Index all .md files under a directory. 318 + 319 + Skips files whose mtime hasn't changed since last index. 320 + Returns the number of documents indexed (counting chunks). 321 + """ 322 + existing = {} 323 + for row in self._conn.execute( 324 + "SELECT doc_id, updated_at FROM documents WHERE source = ?", 325 + (source,), 326 + ): 327 + existing[row[0]] = row[1] 328 + 329 + seen_doc_ids: set[str] = set() 330 + count = 0 331 + 332 + for md_file in sorted(directory.rglob("*.md")): 333 + rel = md_file.relative_to(directory) 334 + content = md_file.read_text() 335 + mtime = md_file.stat().st_mtime 336 + content_type = rel.parts[0] if len(rel.parts) > 1 else "" 337 + 338 + chunks = chunk_document(md_file, content) 339 + 340 + for chunk_idx, chunk_text in chunks: 341 + doc_id = f"{source}:{rel}:{chunk_idx}" 342 + seen_doc_ids.add(doc_id) 343 + 344 + if doc_id in existing and existing[doc_id] == mtime: 345 + count += 1 346 + continue 347 + 348 + title_match = re.match(r"^#\s+(.+)", content) 349 + title = ( 350 + title_match.group(1).strip() if title_match else md_file.stem 351 + ) 352 + 353 + game_day = None 354 + day_match = re.match(r"day([+-]\d+)", md_file.stem) 355 + if day_match: 356 + game_day = int(day_match.group(1)) 357 + 358 + self.upsert(doc_id, chunk_text, { 359 + "source": source, 360 + "content_type": content_type, 361 + "path": str(md_file), 362 + "title": title, 363 + "chunk_index": chunk_idx, 364 + "game_day": game_day, 365 + "updated_at": mtime, 366 + }) 367 + count += 1 368 + 369 + stale = set(existing.keys()) - seen_doc_ids 370 + for doc_id in stale: 371 + self.delete(doc_id) 372 + 373 + return count 374 + 375 + def stats(self) -> dict: 376 + """Return index statistics.""" 377 + total = self._conn.execute( 378 + "SELECT count(*) FROM documents" 379 + ).fetchone()[0] 380 + 381 + by_source: dict[str, int] = {} 382 + for source, cnt in self._conn.execute( 383 + "SELECT source, count(*) FROM documents GROUP BY source" 384 + ): 385 + by_source[source] = cnt 386 + 387 + return {"total_documents": total, "by_source": by_source}
+134 -313
src/storied/tools.py
··· 6 6 7 7 import re 8 8 import threading 9 + from dataclasses import dataclass 9 10 from pathlib import Path 10 11 11 12 import yaml 12 13 13 14 from storied.character import create_character as char_create 14 15 from storied.character import update_character as char_update 15 - from storied.content import ContentResolver 16 16 from storied.dice import roll as dice_roll 17 - from storied.log import CampaignLog, load_log 17 + from storied.log import CampaignLog 18 + from storied.search import VectorIndex 18 19 from storied.session import name_to_slug 19 20 from storied.session import update_session as session_update 20 21 ··· 64 65 self._cache[path] = data 65 66 66 67 68 + @dataclass 69 + class ToolContext: 70 + """Shared infrastructure for all tool calls. 71 + 72 + Created once per MCP server and passed to every tool invocation. 73 + All fields are required — no defensive None checks in tool code. 74 + """ 75 + 76 + world_id: str 77 + player_id: str 78 + base_path: Path 79 + campaign_log: CampaignLog 80 + entity_index: EntityIndex 81 + vector_index: VectorIndex 82 + 83 + 67 84 def roll(notation: str, reason: str | None = None) -> dict: 68 85 """Roll dice using standard notation like '1d20', '2d6+3', '4d6kh3'. 69 86 ··· 84 101 85 102 def recall( 86 103 query: str, 104 + ctx: ToolContext, 87 105 scope: str = "all", 88 106 content_type: str | None = None, 89 - world_id: str | None = None, 90 - base_path: Path | None = None, 91 107 ) -> str: 92 108 """Look up rules, world content, or both. 93 109 ··· 100 116 query: What to look up (e.g., "fireball", "captain vex", "merchant guild") 101 117 scope: Where to search - "rules", "world", or "all" (default) 102 118 content_type: Optional type to limit search (e.g., "spells", "npcs") 103 - world_id: World to query (required if scope includes world) 104 - base_path: Base path for content resolution (for testing) 105 119 106 120 Returns: 107 121 Content of the found item, or a message if not found 108 122 """ 109 - results_parts = [] 110 - 111 - # Search rules if scope includes it 112 - if scope in ("rules", "all"): 113 - resolver = ContentResolver(base_path=base_path) 114 - content = resolver.load(query, content_type=content_type) 115 - if content: 116 - return content["body"] 117 - 118 - results = resolver.search(query, content_type=content_type) 119 - if results: 120 - if len(results) == 1: 121 - content = resolver.load( 122 - results[0].name, content_type=results[0].content_type 123 - ) 124 - if content: 125 - return content["body"] 126 - else: 127 - results_parts.append(("rules", results)) 128 - 129 - # Search world if scope includes it and world_id is provided 130 - if scope in ("world", "all") and world_id: 131 - resolver = ContentResolver(base_path=base_path, world_id=world_id) 132 - content = resolver.load(query, content_type=content_type) 133 - if content: 134 - return content["body"] 135 - 136 - results = resolver.search(query, content_type=content_type) 137 - if results: 138 - if len(results) == 1: 139 - content = resolver.load( 140 - results[0].name, content_type=results[0].content_type 141 - ) 142 - if content: 143 - return content["body"] 144 - else: 145 - results_parts.append(("world", results)) 123 + # scope="rules" → SRD only; scope="world" → everything except SRD; 124 + # scope="all" → no filter 125 + source_filter: str | None = None 126 + if scope == "rules": 127 + source_filter = "srd" 146 128 147 - # No exact match - return list of results if any 148 - if results_parts: 149 - lines = [] 150 - for source, results in results_parts: 151 - lines.append(f"Found {len(results)} matches in {source}:") 152 - for r in results[:5]: 153 - lines.append(f" - {r.name} ({r.content_type})") 154 - if len(results) > 5: 155 - lines.append(f" ... and {len(results) - 5} more") 129 + current_day = ctx.campaign_log.get_current_time().day 130 + hits = ctx.vector_index.search( 131 + query, limit=5, source_filter=source_filter, 132 + exclude_source="srd" if scope == "world" else None, 133 + decay_ref=current_day, 134 + ) 135 + if hits: 136 + if len(hits) == 1 or hits[0].score > 0.8: 137 + hit_path = Path(hits[0].path) 138 + if hit_path.exists(): 139 + return hit_path.read_text() 140 + lines = [f"Found {len(hits)} matches:"] 141 + for h in hits: 142 + lines.append(f" - {h.doc_id.split(':')[1]} ({h.source}): {h.snippet[:80]}") 156 143 return "\n".join(lines) 157 144 158 - # Nothing found 159 - if scope == "rules": 160 - return f"No rules found matching '{query}'" 161 - elif scope == "world": 162 - if not world_id: 163 - return "No world specified. Use scope='rules' for base game content." 164 - return f"Nothing found in the world matching '{query}'" 165 - else: 166 - return f"Nothing found matching '{query}'" 145 + return f"Nothing found matching '{query}'" 167 146 168 147 169 - def update_character( 170 - updates: dict, 171 - player_id: str = "default", 172 - base_path: Path | None = None, 173 - ) -> str: 148 + def update_character(updates: dict, ctx: ToolContext) -> str: 174 149 """Update the player's character sheet to persist changes. 175 150 176 151 Call this after HP changes, equipment gained/lost, coins spent, level ups, etc. ··· 191 166 Returns: 192 167 Confirmation of what was updated 193 168 """ 194 - return char_update(player_id, updates, base_path) 169 + return char_update(ctx.player_id, updates, ctx.base_path) 195 170 196 171 197 172 def create_character( ··· 202 177 abilities: dict[str, int], 203 178 hp_max: int, 204 179 ac: int, 180 + ctx: ToolContext, 205 181 background: str | None = None, 206 182 speed: int = 30, 207 183 purse: dict[str, int] | None = None, ··· 209 185 features: list[str] | None = None, 210 186 proficiencies: str | None = None, 211 187 backstory: str | None = None, 212 - player_id: str = "default", 213 - base_path: Path | None = None, 214 188 ) -> str: 215 189 """Create a new player character and save to disk. 216 190 ··· 242 216 Confirmation message 243 217 """ 244 218 return char_create( 245 - player_id=player_id, 219 + player_id=ctx.player_id, 246 220 name=name, 247 221 race=race, 248 222 char_class=char_class, ··· 257 231 features=features, 258 232 proficiencies=proficiencies, 259 233 backstory=backstory, 260 - base_path=base_path, 234 + base_path=ctx.base_path, 261 235 ) 262 236 263 237 264 238 def set_scene( 239 + ctx: ToolContext, 265 240 event: str | None = None, 266 241 duration: str | None = None, 267 242 situation: str | None = None, ··· 269 244 present: list[str] | None = None, 270 245 threads: list[str] | None = None, 271 246 tags: list[str] | None = None, 272 - player_id: str = "default", 273 - base_path: Path | None = None, 274 - campaign_log: CampaignLog | None = None, 275 - world_id: str = "default", 276 - entity_index: EntityIndex | None = None, 277 247 ) -> str: 278 248 """Call this after every response. Logs what happened, advances the clock, 279 249 and updates the scene state. ··· 292 262 (e.g., ["[[Vera Blackwater]]", "[[Henrik]] - barkeep"]) 293 263 threads: Open plot threads or objectives 294 264 tags: Optional tags: "combat", "rest:short", "rest:long", "travel" 295 - player_id: Player identifier 296 - base_path: Base path for players directory 297 - campaign_log: CampaignLog instance (injected by engine) 298 - world_id: World identifier (injected by engine) 299 265 300 266 Returns: 301 267 Confirmation of what was updated 302 268 """ 303 269 parts = [] 304 270 305 - # Log event and advance clock 306 271 if event and duration: 307 - log = campaign_log or load_log(world_id, base_path) 308 - anchor = log.append_entry(event, duration, tags=tags) 309 - current = log.get_current_time() 272 + anchor = ctx.campaign_log.append_entry(event, duration, tags=tags) 273 + current = ctx.campaign_log.get_current_time() 310 274 parts.append( 311 275 f"Logged: {anchor} | {event} | {duration} → " 312 276 f"Now: {current} ({current.period_of_day()}, {current.atmosphere()})" 313 277 ) 314 278 315 - # Update session state 316 279 updates = {} 317 280 if situation is not None: 318 281 updates["situation"] = situation ··· 324 287 updates["threads"] = threads 325 288 326 289 if updates: 327 - result = session_update(player_id, updates, base_path) 290 + result = session_update(ctx.player_id, updates, ctx.base_path) 328 291 parts.append(result) 329 292 330 - # Auto-mark present entities with this event 331 - if event and present and world_id: 332 - marked = _auto_mark_present( 333 - present, event, world_id, base_path, campaign_log, 334 - entity_index=entity_index, 335 - ) 293 + if event and present: 294 + marked = _auto_mark_present(present, event, ctx) 336 295 if marked: 337 296 parts.append(f"Auto-marked: {', '.join(marked)}") 338 297 ··· 342 301 def establish( 343 302 entity_type: str, 344 303 name: str, 304 + ctx: ToolContext, 345 305 description: str | None = None, 346 306 location: str | None = None, 347 307 knows: list[str] | None = None, 348 308 wants: list[str] | None = None, 349 309 will: list[str] | None = None, 350 - world_id: str | None = None, 351 - base_path: Path | None = None, 352 - entity_index: EntityIndex | None = None, 353 310 ) -> str: 354 311 """Establish or update an entity in the world. 355 312 ··· 377 334 knows: List of secrets and hidden truths. Things that aren't obvious. 378 335 wants: List of desires, tendencies, inclinations. The entity's nature. 379 336 will: List of conditional behaviors: "If X → Y" format. 380 - world_id: World to save to (required) 381 - base_path: Base path for worlds directory 382 337 383 338 Returns: 384 339 Confirmation with the file path 385 340 """ 386 - if not world_id: 387 - return "Error: No world_id specified. Cannot save world content." 388 - 389 - if base_path is None: 390 - base_path = Path.cwd() 391 - 392 - # Build file path using display name directly 393 - world_dir = base_path / "worlds" / world_id / entity_type 341 + world_dir = ctx.base_path / "worlds" / ctx.world_id / entity_type 394 342 world_dir.mkdir(parents=True, exist_ok=True) 395 343 file_path = world_dir / f"{name}.md" 396 344 397 345 lock = _get_file_lock(file_path) 398 346 with lock: 399 - # Load existing content if file exists (for partial updates) 400 - existing = _load_entity(file_path, entity_index) 347 + existing = _load_entity(file_path, ctx.entity_index) 401 348 402 - # Merge with existing content (new values override) 403 349 if description is None: 404 350 description = existing.get("description", "") 405 351 if location is None: ··· 410 356 wants = existing.get("wants", []) 411 357 if will is None: 412 358 will = existing.get("will", []) 413 - was = existing.get("was", []) # Always preserve Was 414 - 415 - # Build file content 416 - file_content = _format_entity(name, description, location, knows, wants, will, was) 417 - file_path.write_text(file_content) 359 + was = existing.get("was", []) 418 360 419 - # Write-through: update index and cache 420 - updated_data = { 361 + data = { 421 362 "description": description, "location": location, 422 363 "knows": knows, "wants": wants, "will": will, "was": was, 423 364 } 424 - if entity_index: 425 - entity_index.register(name, file_path) 426 - entity_index.cache_put(file_path, updated_data) 365 + _write_entity(file_path, name, entity_type, data, ctx) 427 366 428 367 action = "Updated" if existing else "Established" 429 368 return f"{action} {entity_type.rstrip('s')} '{name}'" 430 369 431 370 432 - def _load_entity( 433 - file_path: Path, entity_index: EntityIndex | None = None, 434 - ) -> dict: 371 + def _load_entity(file_path: Path, entity_index: EntityIndex) -> dict: 435 372 """Load an existing entity file and parse its structure.""" 436 - if entity_index: 437 - cached = entity_index.cache_get(file_path) 438 - if cached is not None: 439 - return cached 373 + cached = entity_index.cache_get(file_path) 374 + if cached is not None: 375 + return cached 440 376 441 377 if not file_path.exists(): 442 378 return {} ··· 482 418 if was_match: 483 419 result["was"] = _parse_list_items(was_match.group(1)) 484 420 485 - if entity_index: 486 - entity_index.cache_put(file_path, result) 421 + entity_index.cache_put(file_path, result) 487 422 488 423 return result 489 424 ··· 548 483 return "\n".join(lines) 549 484 550 485 486 + def _write_entity( 487 + file_path: Path, 488 + name: str, 489 + entity_type: str, 490 + data: dict, 491 + ctx: ToolContext, 492 + ) -> None: 493 + """Write an entity to disk and update all indexes.""" 494 + file_content = _format_entity( 495 + name, data["description"], data["location"], 496 + data["knows"], data["wants"], data["will"], data["was"], 497 + ) 498 + file_path.write_text(file_content) 499 + ctx.entity_index.register(name, file_path) 500 + ctx.entity_index.cache_put(file_path, data) 501 + ctx.vector_index.upsert( 502 + f"world:{entity_type}/{name}.md:0", 503 + file_content, 504 + {"source": "world", "content_type": entity_type, 505 + "path": str(file_path), "title": name}, 506 + ) 507 + 508 + 551 509 def _auto_mark_present( 552 - present: list[str], 553 - event: str, 554 - world_id: str, 555 - base_path: Path | None, 556 - campaign_log: CampaignLog | None, 557 - entity_index: EntityIndex | None = None, 510 + present: list[str], event: str, ctx: ToolContext, 558 511 ) -> list[str]: 559 512 """Auto-mark present entities with the current event. 560 513 561 514 Extracts entity names from [[wikilink]] format in the present list 562 515 and appends the event to each entity's Was section. 563 516 """ 564 - if base_path is None: 565 - base_path = Path.cwd() 566 - 567 517 marked: list[str] = [] 568 518 for ref in present: 569 - # Extract name from "[[Name]]" or "[[Name]] - description" 570 519 link_match = re.search(r"\[\[([^\]]+)\]\]", ref) 571 520 if not link_match: 572 521 continue 573 522 name = link_match.group(1) 574 523 575 - # Resolve via index (O(1)) or fall back to directory scan 576 - file_path = entity_index.resolve(name) if entity_index else None 524 + file_path = ctx.entity_index.resolve(name) 577 525 if file_path is None: 578 526 for etype in ("npcs", "locations", "items", "factions"): 579 - candidate = base_path / "worlds" / world_id / etype / f"{name}.md" 527 + candidate = ctx.base_path / "worlds" / ctx.world_id / etype / f"{name}.md" 580 528 if candidate.exists(): 581 529 file_path = candidate 582 530 break 583 531 584 532 if file_path and file_path.exists(): 585 533 entity_type = file_path.parent.name 586 - mark( 587 - entity_type=entity_type, 588 - name=name, 589 - event=event, 590 - world_id=world_id, 591 - base_path=base_path, 592 - campaign_log=campaign_log, 593 - entity_index=entity_index, 594 - ) 534 + mark(entity_type=entity_type, name=name, event=event, ctx=ctx) 595 535 marked.append(name) 596 536 597 537 return marked ··· 601 541 entity_type: str, 602 542 name: str, 603 543 event: str, 544 + ctx: ToolContext, 604 545 resolves: list[str] | None = None, 605 - world_id: str | None = None, 606 - base_path: Path | None = None, 607 - campaign_log: CampaignLog | None = None, 608 - entity_index: EntityIndex | None = None, 609 546 ) -> str: 610 547 """Record an event in an entity's history (## Was section). 611 548 ··· 621 558 name: Entity name (exact filename match) 622 559 event: What happened - brief description for the Was section 623 560 resolves: Optional list of Will items to remove if this event fired triggers 624 - world_id: World containing the entity (required) 625 - base_path: Base path for worlds directory 626 - campaign_log: Campaign log for current game time (optional) 627 561 628 562 Returns: 629 563 Confirmation message 630 564 """ 631 - if not world_id: 632 - return "Error: No world_id specified." 633 - 634 - if base_path is None: 635 - base_path = Path.cwd() 636 - 637 - # Resolve path via index or filesystem 638 - if entity_index: 639 - file_path = entity_index.resolve(name) 640 - if file_path is None: 641 - file_path = base_path / "worlds" / world_id / entity_type / f"{name}.md" 642 - else: 643 - file_path = base_path / "worlds" / world_id / entity_type / f"{name}.md" 565 + file_path = ctx.entity_index.resolve(name) 566 + if file_path is None: 567 + file_path = ctx.base_path / "worlds" / ctx.world_id / entity_type / f"{name}.md" 644 568 645 569 if not file_path.exists(): 646 570 return f"Error: Entity '{name}' not found in {entity_type}" 647 571 648 - # Get current game time for timestamp 649 - if campaign_log: 650 - timestamp = campaign_log.get_current_time().to_anchor() 651 - else: 652 - log = load_log(world_id, base_path) 653 - timestamp = log.get_current_time().to_anchor() 572 + timestamp = ctx.campaign_log.get_current_time().to_anchor() 654 573 655 574 lock = _get_file_lock(file_path) 656 575 with lock: 657 - # Load existing entity 658 - existing = _load_entity(file_path, entity_index) 576 + existing = _load_entity(file_path, ctx.entity_index) 659 577 660 - # Append to Was section 661 578 was = existing.get("was", []) 662 579 was.append(f"{timestamp} | {event}") 663 580 664 - # Remove resolved Will items 665 581 will = existing.get("will", []) 666 582 resolved = [] 667 583 for trigger in resolves or []: ··· 669 585 will.remove(trigger) 670 586 resolved.append(trigger) 671 587 672 - # Rebuild and save the file 673 - file_content = _format_entity( 674 - name, 675 - existing.get("description", ""), 676 - existing.get("location", ""), 677 - existing.get("knows", []), 678 - existing.get("wants", []), 679 - will, 680 - was, 681 - ) 682 - file_path.write_text(file_content) 683 - 684 - # Write-through cache 685 - if entity_index: 686 - entity_index.cache_put(file_path, { 687 - "description": existing.get("description", ""), 688 - "location": existing.get("location", ""), 689 - "knows": existing.get("knows", []), 690 - "wants": existing.get("wants", []), 691 - "will": will, 692 - "was": was, 693 - }) 588 + data = { 589 + "description": existing.get("description", ""), 590 + "location": existing.get("location", ""), 591 + "knows": existing.get("knows", []), 592 + "wants": existing.get("wants", []), 593 + "will": will, 594 + "was": was, 595 + } 596 + _write_entity(file_path, name, entity_type, data, ctx) 694 597 695 598 result = f"Marked: {event}" 696 599 if resolved: ··· 704 607 def note_discovery( 705 608 entity: str, 706 609 content: str, 610 + ctx: ToolContext, 707 611 content_type: str = "lore", 708 612 tags: list[str] | None = None, 709 - world_id: str | None = None, 710 - player_id: str = "default", 711 - base_path: Path | None = None, 712 613 ) -> str: 713 614 """Record what the player has learned about something. 714 615 ··· 725 626 content: What the player learned or observed 726 627 content_type: Type of content - npcs, locations, factions, lore (default: lore) 727 628 tags: Optional tags for categorization 728 - world_id: World this knowledge is about (required) 729 - player_id: Player who learned this 730 - base_path: Base path for players directory 731 629 732 630 Returns: 733 631 Confirmation message 734 632 """ 735 - if not world_id: 736 - return "Error: No world_id specified. Cannot record player knowledge." 737 - 738 - if base_path is None: 739 - base_path = Path.cwd() 740 - 741 - # Generate slug from entity name 742 633 slug = name_to_slug(entity) 743 634 744 - # Player knowledge path: players/{player}/worlds/{world}/{type}/ 745 - knowledge_dir = base_path / "players" / player_id / "worlds" / world_id / content_type 635 + knowledge_dir = ( 636 + ctx.base_path / "players" / ctx.player_id / "worlds" 637 + / ctx.world_id / content_type 638 + ) 746 639 knowledge_dir.mkdir(parents=True, exist_ok=True) 747 640 file_path = knowledge_dir / f"{slug}.md" 748 641 749 - # Build frontmatter 750 - frontmatter = { 642 + frontmatter: dict = { 751 643 "type": content_type.rstrip("s"), 752 644 "name": entity, 753 645 } 754 646 if tags: 755 647 frontmatter["tags"] = tags 756 648 757 - # Build file content 758 649 file_content = "---\n" 759 650 file_content += yaml.dump(frontmatter, sort_keys=False, allow_unicode=True) 760 651 file_content += "---\n\n" ··· 762 653 file_content += "\n" 763 654 764 655 file_path.write_text(file_content) 656 + 657 + ctx.vector_index.upsert( 658 + f"player:{content_type}/{slug}.md:0", 659 + file_content, 660 + {"source": "player", "content_type": content_type, 661 + "path": str(file_path), "title": entity}, 662 + ) 765 663 766 664 return f"Noted: player learned about '{entity}'" 767 665 768 666 769 - def end_session( 770 - situation: str, 771 - threads: list[str] | None = None, 772 - player_id: str = "default", 773 - base_path: Path | None = None, 774 - ) -> str: 667 + def end_session(situation: str, ctx: ToolContext, threads: list[str] | None = None) -> str: 775 668 """End the current session, saving the game state for next time. 776 669 777 670 Call this when the player indicates they want to stop playing. This saves ··· 783 676 situation: Summary of the current state of affairs for the next session. 784 677 Write as if briefing a DM who will pick up where you left off. 785 678 threads: Open plot threads or objectives to carry forward 786 - player_id: Player identifier 787 - base_path: Base path for players directory 788 679 789 680 Returns: 790 681 Confirmation that session was saved 791 682 """ 792 - updates = {"situation": situation} 683 + updates: dict = {"situation": situation} 793 684 if threads is not None: 794 685 updates["threads"] = threads 795 686 796 - session_update(player_id, updates, base_path) 687 + session_update(ctx.player_id, updates, ctx.base_path) 797 688 return "SESSION_ENDED" 798 689 799 690 ··· 1060 951 ] 1061 952 1062 953 1063 - def execute_tool( 1064 - tool_name: str, 1065 - tool_input: dict, 1066 - world_id: str | None = None, 1067 - player_id: str = "default", 1068 - base_path: Path | None = None, 1069 - campaign_log: CampaignLog | None = None, 1070 - entity_index: EntityIndex | None = None, 1071 - ) -> str: 1072 - """Execute a tool by name with the given input. 1073 - 1074 - Args: 1075 - tool_name: Name of the tool to execute 1076 - tool_input: Tool input parameters 1077 - world_id: Current world ID for query_world 1078 - player_id: Player ID for update_character 1079 - base_path: Base path for content resolution 1080 - campaign_log: Campaign log instance for log_event 1081 - 1082 - Returns: 1083 - Tool result as a string 1084 - """ 954 + def execute_tool(tool_name: str, tool_input: dict, ctx: ToolContext) -> str: 955 + """Execute a tool by name with the given input.""" 1085 956 if tool_name == "roll": 1086 957 result = roll(tool_input["notation"]) 1087 - # Format nicely for the DM 1088 958 rolls_str = ", ".join(str(r) for r in result["rolls"]) 1089 959 if result["kept"] != result["rolls"]: 1090 960 kept_str = ", ".join(str(r) for r in result["kept"]) ··· 1096 966 1097 967 elif tool_name == "recall": 1098 968 return recall( 1099 - tool_input["query"], 969 + tool_input["query"], ctx, 1100 970 scope=tool_input.get("scope", "all"), 1101 971 content_type=tool_input.get("content_type"), 1102 - world_id=world_id, 1103 - base_path=base_path, 1104 972 ) 1105 973 1106 974 elif tool_name == "update_character": 1107 - return update_character( 1108 - tool_input["updates"], 1109 - player_id=player_id, 1110 - base_path=base_path, 1111 - ) 975 + return update_character(tool_input["updates"], ctx) 1112 976 1113 977 elif tool_name == "create_character": 1114 978 return create_character( ··· 1119 983 abilities=tool_input["abilities"], 1120 984 hp_max=tool_input["hp_max"], 1121 985 ac=tool_input["ac"], 986 + ctx=ctx, 1122 987 background=tool_input.get("background"), 1123 988 speed=tool_input.get("speed", 30), 1124 989 purse=tool_input.get("purse"), ··· 1126 991 features=tool_input.get("features"), 1127 992 proficiencies=tool_input.get("proficiencies"), 1128 993 backstory=tool_input.get("backstory"), 1129 - player_id=player_id, 1130 - base_path=base_path, 1131 994 ) 1132 995 1133 996 elif tool_name == "set_scene": 1134 997 return set_scene( 998 + ctx=ctx, 1135 999 event=tool_input.get("event"), 1136 1000 duration=tool_input.get("duration"), 1137 1001 situation=tool_input.get("situation"), ··· 1139 1003 present=tool_input.get("present"), 1140 1004 threads=tool_input.get("threads"), 1141 1005 tags=tool_input.get("tags"), 1142 - player_id=player_id, 1143 - base_path=base_path, 1144 - campaign_log=campaign_log, 1145 - world_id=world_id or "default", 1146 - entity_index=entity_index, 1147 1006 ) 1148 1007 1149 1008 elif tool_name == "establish": 1150 1009 return establish( 1151 1010 entity_type=tool_input["entity_type"], 1152 1011 name=tool_input["name"], 1012 + ctx=ctx, 1153 1013 description=tool_input.get("description"), 1154 1014 location=tool_input.get("location"), 1155 1015 knows=tool_input.get("knows"), 1156 1016 wants=tool_input.get("wants"), 1157 1017 will=tool_input.get("will"), 1158 - world_id=world_id, 1159 - base_path=base_path, 1160 - entity_index=entity_index, 1161 1018 ) 1162 1019 1163 1020 elif tool_name == "mark": ··· 1165 1022 entity_type=tool_input["entity_type"], 1166 1023 name=tool_input["name"], 1167 1024 event=tool_input["event"], 1025 + ctx=ctx, 1168 1026 resolves=tool_input.get("resolves"), 1169 - world_id=world_id, 1170 - base_path=base_path, 1171 - campaign_log=campaign_log, 1172 - entity_index=entity_index, 1173 1027 ) 1174 1028 1175 1029 elif tool_name == "note_discovery": 1176 1030 return note_discovery( 1177 1031 entity=tool_input["entity"], 1178 1032 content=tool_input["content"], 1033 + ctx=ctx, 1179 1034 content_type=tool_input.get("content_type", "lore"), 1180 1035 tags=tool_input.get("tags"), 1181 - world_id=world_id, 1182 - player_id=player_id, 1183 - base_path=base_path, 1184 1036 ) 1185 1037 1186 1038 elif tool_name == "end_session": 1187 1039 return end_session( 1188 1040 situation=tool_input["situation"], 1041 + ctx=ctx, 1189 1042 threads=tool_input.get("threads"), 1190 - player_id=player_id, 1191 - base_path=base_path, 1192 1043 ) 1193 1044 1194 1045 else: ··· 1200 1051 PLANNER_TOOL_DEFINITIONS = [t for t in TOOL_DEFINITIONS if t["name"] in PLANNER_TOOLS] 1201 1052 1202 1053 1203 - def planner_execute_tool( 1204 - tool_name: str, 1205 - tool_input: dict, 1206 - world_id: str | None = None, 1207 - base_path: Path | None = None, 1208 - campaign_log: CampaignLog | None = None, 1209 - entity_index: EntityIndex | None = None, 1210 - ) -> str: 1054 + def planner_execute_tool(tool_name: str, tool_input: dict, ctx: ToolContext) -> str: 1211 1055 """Execute a planner-allowed tool. Rejects anything outside the allowed set.""" 1212 1056 if tool_name not in PLANNER_TOOLS: 1213 1057 return f"Tool not available to planner: {tool_name}" 1214 - return execute_tool( 1215 - tool_name, 1216 - tool_input, 1217 - world_id=world_id, 1218 - base_path=base_path, 1219 - campaign_log=campaign_log, 1220 - entity_index=entity_index, 1221 - ) 1058 + return execute_tool(tool_name, tool_input, ctx) 1222 1059 1223 1060 1224 1061 SEEDER_TOOLS = {"establish", "set_scene"} ··· 1226 1063 SEEDER_TOOL_DEFINITIONS = [t for t in TOOL_DEFINITIONS if t["name"] in SEEDER_TOOLS] 1227 1064 1228 1065 1229 - def seeder_execute_tool( 1230 - tool_name: str, 1231 - tool_input: dict, 1232 - world_id: str | None = None, 1233 - player_id: str | None = None, 1234 - base_path: Path | None = None, 1235 - campaign_log: CampaignLog | None = None, 1236 - entity_index: EntityIndex | None = None, 1237 - ) -> str: 1066 + def seeder_execute_tool(tool_name: str, tool_input: dict, ctx: ToolContext) -> str: 1238 1067 """Execute a seeder-allowed tool. Rejects anything outside the allowed set.""" 1239 1068 if tool_name not in SEEDER_TOOLS: 1240 1069 return f"Tool not available to seeder: {tool_name}" 1241 - return execute_tool( 1242 - tool_name, 1243 - tool_input, 1244 - world_id=world_id, 1245 - player_id=player_id or "default", 1246 - base_path=base_path, 1247 - campaign_log=campaign_log, 1248 - entity_index=entity_index, 1249 - ) 1070 + return execute_tool(tool_name, tool_input, ctx)
+49
tests/conftest.py
··· 1 + """Shared test fixtures.""" 2 + 3 + import hashlib 4 + from pathlib import Path 5 + 6 + import pytest 7 + 8 + from storied.log import CampaignLog 9 + from storied.search import VectorIndex 10 + from storied.tools import EntityIndex, ToolContext 11 + 12 + EMBED_DIM = 384 13 + 14 + 15 + def _fake_embed(texts: list[str]) -> list[list[float]]: 16 + """Deterministic hash-based embeddings for testing.""" 17 + results = [] 18 + for text in texts: 19 + h = hashlib.sha256(text.encode()).digest() 20 + floats: list[float] = [] 21 + seed = h 22 + while len(floats) < EMBED_DIM: 23 + seed = hashlib.sha256(seed).digest() 24 + for byte in seed: 25 + if len(floats) >= EMBED_DIM: 26 + break 27 + floats.append((byte - 128) / 128.0) 28 + norm = sum(x * x for x in floats) ** 0.5 29 + results.append([x / norm for x in floats]) 30 + return results 31 + 32 + 33 + @pytest.fixture 34 + def ctx(tmp_path: Path) -> ToolContext: 35 + """ToolContext with fake embedder for tests that need tool infrastructure.""" 36 + world_dir = tmp_path / "worlds" / "test-world" 37 + world_dir.mkdir(parents=True) 38 + 39 + vi = VectorIndex(tmp_path / "search.db") 40 + vi._embed_fn = _fake_embed 41 + 42 + return ToolContext( 43 + world_id="test-world", 44 + player_id="default", 45 + base_path=tmp_path, 46 + campaign_log=CampaignLog("test-world", tmp_path), 47 + entity_index=EntityIndex(world_dir), 48 + vector_index=vi, 49 + )
+46 -64
tests/test_character.py
··· 1 1 """Tests for character loading, saving, updates, and display formatting.""" 2 2 3 - from pathlib import Path 4 - 5 3 import pytest 6 4 7 5 from storied.character import ( 8 - create_character, 9 6 format_character_context, 10 7 format_sheet, 11 8 format_status, 12 9 load_character, 13 10 parse_character, 14 - save_character, 15 - update_character, 16 11 ) 12 + from storied.tools import ToolContext, create_character, update_character 17 13 18 14 19 15 @pytest.fixture 20 - def player_base(tmp_path: Path) -> Path: 21 - """Create a base directory with player structure.""" 22 - player = tmp_path / "players" / "test-player" 23 - player.mkdir(parents=True) 24 - return tmp_path 16 + def char_ctx(ctx: ToolContext) -> ToolContext: 17 + """ToolContext configured for character tests with players dir.""" 18 + ctx.player_id = "test-player" 19 + (ctx.base_path / "players" / "test-player").mkdir(parents=True) 20 + return ctx 25 21 26 22 27 23 @pytest.fixture 28 - def basic_character(player_base: Path) -> dict: 24 + def basic_character(char_ctx: ToolContext) -> dict: 29 25 """Create a basic character for testing.""" 30 26 create_character( 31 - player_id="test-player", 32 27 name="Test Hero", 33 28 race="Human", 34 29 char_class="Fighter", ··· 44 39 hp_max=12, 45 40 ac=16, 46 41 purse={"cp": 0, "sp": 0, "ep": 0, "gp": 50, "pp": 0}, 47 - base_path=player_base, 42 + ctx=char_ctx, 48 43 ) 49 - return load_character("test-player", player_base) 44 + return load_character("test-player", char_ctx.base_path) 50 45 51 46 52 47 class TestParseCharacter: ··· 77 72 class TestCreateCharacter: 78 73 """Tests for character creation.""" 79 74 80 - def test_create_basic_character(self, player_base: Path): 75 + def test_create_basic_character(self, char_ctx: ToolContext): 81 76 result = create_character( 82 - player_id="test-player", 83 77 name="Conan", 84 78 race="Human", 85 79 char_class="Barbarian", ··· 94 88 }, 95 89 hp_max=15, 96 90 ac=14, 97 - base_path=player_base, 91 + ctx=char_ctx, 98 92 ) 99 93 100 94 assert "Created character 'Conan'" in result 101 - char_file = player_base / "players/test-player/character.md" 95 + char_file = char_ctx.base_path / "players/test-player/character.md" 102 96 assert char_file.exists() 103 97 104 - def test_created_character_has_full_hp(self, player_base: Path): 98 + def test_created_character_has_full_hp(self, char_ctx: ToolContext): 105 99 create_character( 106 - player_id="test-player", 107 100 name="Test", 108 101 race="Human", 109 102 char_class="Fighter", ··· 112 105 "intelligence": 10, "wisdom": 10, "charisma": 10}, 113 106 hp_max=10, 114 107 ac=10, 115 - base_path=player_base, 108 + ctx=char_ctx, 116 109 ) 117 110 118 - char = load_character("test-player", player_base) 111 + char = load_character("test-player", char_ctx.base_path) 119 112 assert char["hp"]["current"] == 10 120 113 assert char["hp"]["max"] == 10 121 114 ··· 123 116 class TestUpdateCharacter: 124 117 """Tests for character updates.""" 125 118 126 - def test_update_purse(self, player_base: Path, basic_character: dict): 119 + def test_update_purse(self, char_ctx: ToolContext, basic_character: dict): 127 120 result = update_character( 128 - player_id="test-player", 129 121 updates={"purse.gp": 100, "purse.sp": 25}, 130 - base_path=player_base, 122 + ctx=char_ctx, 131 123 ) 132 124 133 125 assert "purse.gp = 100" in result 134 - char = load_character("test-player", player_base) 126 + char = load_character("test-player", char_ctx.base_path) 135 127 assert char["purse"]["gp"] == 100 136 128 assert char["purse"]["sp"] == 25 137 129 138 - def test_update_hp_current(self, player_base: Path, basic_character: dict): 130 + def test_update_hp_current(self, char_ctx: ToolContext, basic_character: dict): 139 131 result = update_character( 140 - player_id="test-player", 141 132 updates={"hp.current": 5}, 142 - base_path=player_base, 133 + ctx=char_ctx, 143 134 ) 144 135 145 136 assert "hp.current = 5" in result 146 - char = load_character("test-player", player_base) 137 + char = load_character("test-player", char_ctx.base_path) 147 138 assert char["hp"]["current"] == 5 148 139 149 - def test_update_section(self, player_base: Path, basic_character: dict): 140 + def test_update_section(self, char_ctx: ToolContext, basic_character: dict): 150 141 update_character( 151 - player_id="test-player", 152 142 updates={"section.Equipment": "- Longsword\n- Shield"}, 153 - base_path=player_base, 143 + ctx=char_ctx, 154 144 ) 155 145 156 - char = load_character("test-player", player_base) 146 + char = load_character("test-player", char_ctx.base_path) 157 147 assert "Longsword" in char["body"] 158 148 assert "Shield" in char["body"] 159 149 ··· 161 151 class TestHPClamping: 162 152 """Tests for HP clamping to valid 5e range.""" 163 153 164 - def test_hp_cannot_go_negative(self, player_base: Path, basic_character: dict): 154 + def test_hp_cannot_go_negative(self, char_ctx: ToolContext, basic_character: dict): 165 155 """In 5e, HP minimum is 0 (no negative HP).""" 166 156 update_character( 167 - player_id="test-player", 168 157 updates={"hp.current": -5}, 169 - base_path=player_base, 158 + ctx=char_ctx, 170 159 ) 171 160 172 - char = load_character("test-player", player_base) 161 + char = load_character("test-player", char_ctx.base_path) 173 162 assert char["hp"]["current"] == 0 174 163 175 - def test_hp_clamped_message(self, player_base: Path, basic_character: dict): 164 + def test_hp_clamped_message(self, char_ctx: ToolContext, basic_character: dict): 176 165 """Update result should indicate HP was clamped.""" 177 166 result = update_character( 178 - player_id="test-player", 179 167 updates={"hp.current": -10}, 180 - base_path=player_base, 168 + ctx=char_ctx, 181 169 ) 182 170 183 171 assert "clamped to 0" in result 184 172 185 - def test_hp_cannot_exceed_max(self, player_base: Path, basic_character: dict): 173 + def test_hp_cannot_exceed_max(self, char_ctx: ToolContext, basic_character: dict): 186 174 """HP cannot exceed maximum.""" 187 175 update_character( 188 - player_id="test-player", 189 176 updates={"hp.current": 100}, 190 - base_path=player_base, 177 + ctx=char_ctx, 191 178 ) 192 179 193 - char = load_character("test-player", player_base) 180 + char = load_character("test-player", char_ctx.base_path) 194 181 assert char["hp"]["current"] == 12 # max HP is 12 195 182 196 - def test_hp_exceeds_max_clamped_message(self, player_base: Path, basic_character: dict): 183 + def test_hp_exceeds_max_clamped_message(self, char_ctx: ToolContext, basic_character: dict): 197 184 """Update result should indicate HP was clamped to max.""" 198 185 result = update_character( 199 - player_id="test-player", 200 186 updates={"hp.current": 999}, 201 - base_path=player_base, 187 + ctx=char_ctx, 202 188 ) 203 189 204 190 assert "clamped to 12" in result 205 191 206 - def test_valid_hp_not_clamped(self, player_base: Path, basic_character: dict): 192 + def test_valid_hp_not_clamped(self, char_ctx: ToolContext, basic_character: dict): 207 193 """Valid HP values should not be modified.""" 208 194 result = update_character( 209 - player_id="test-player", 210 195 updates={"hp.current": 6}, 211 - base_path=player_base, 196 + ctx=char_ctx, 212 197 ) 213 198 214 199 assert "clamped" not in result 215 - char = load_character("test-player", player_base) 200 + char = load_character("test-player", char_ctx.base_path) 216 201 assert char["hp"]["current"] == 6 217 202 218 - def test_hp_zero_is_valid(self, player_base: Path, basic_character: dict): 203 + def test_hp_zero_is_valid(self, char_ctx: ToolContext, basic_character: dict): 219 204 """Setting HP to exactly 0 is valid (unconscious).""" 220 205 result = update_character( 221 - player_id="test-player", 222 206 updates={"hp.current": 0}, 223 - base_path=player_base, 207 + ctx=char_ctx, 224 208 ) 225 209 226 210 assert "clamped" not in result 227 - char = load_character("test-player", player_base) 211 + char = load_character("test-player", char_ctx.base_path) 228 212 assert char["hp"]["current"] == 0 229 213 230 - def test_hp_max_is_valid(self, player_base: Path, basic_character: dict): 214 + def test_hp_max_is_valid(self, char_ctx: ToolContext, basic_character: dict): 231 215 """Setting HP to exactly max is valid.""" 232 216 result = update_character( 233 - player_id="test-player", 234 217 updates={"hp.current": 12}, 235 - base_path=player_base, 218 + ctx=char_ctx, 236 219 ) 237 220 238 221 assert "clamped" not in result 239 - char = load_character("test-player", player_base) 222 + char = load_character("test-player", char_ctx.base_path) 240 223 assert char["hp"]["current"] == 12 241 224 242 - def test_damage_calculation_example(self, player_base: Path, basic_character: dict): 225 + def test_damage_calculation_example(self, char_ctx: ToolContext, basic_character: dict): 243 226 """Simulate taking 20 damage when at 12 HP - should clamp to 0.""" 244 227 # Character starts at 12/12 HP 245 228 # Takes 20 damage, DM sets hp.current = -8 246 229 # Should be clamped to 0 247 230 248 231 update_character( 249 - player_id="test-player", 250 232 updates={"hp.current": -8}, 251 - base_path=player_base, 233 + ctx=char_ctx, 252 234 ) 253 235 254 - char = load_character("test-player", player_base) 236 + char = load_character("test-player", char_ctx.base_path) 255 237 assert char["hp"]["current"] == 0 256 238 257 239
+1 -32
tests/test_content.py
··· 4 4 5 5 import pytest 6 6 7 - from storied.content import ContentResolver, SearchResult 7 + from storied.content import ContentResolver 8 8 9 9 10 10 @pytest.fixture ··· 167 167 assert "Orc" in content["body"] 168 168 169 169 170 - class TestSearch: 171 - """Tests for searching content.""" 172 - 173 - def test_search_by_keyword(self, resolver: ContentResolver): 174 - results = resolver.search("dragon") 175 - assert len(results) >= 1 176 - assert any("dragon" in r.name.lower() for r in results) 177 - 178 - def test_search_with_content_type(self, resolver: ContentResolver): 179 - results = resolver.search("fire", content_type="spells") 180 - assert len(results) >= 1 181 - assert all(r.content_type == "spells" for r in results) 182 - 183 - def test_search_no_results(self, resolver: ContentResolver): 184 - results = resolver.search("zzzznonexistent") 185 - assert len(results) == 0 186 - 187 - def test_search_result_structure(self, resolver: ContentResolver): 188 - results = resolver.search("goblin") 189 - assert len(results) >= 1 190 - result = results[0] 191 - assert isinstance(result, SearchResult) 192 - assert result.name is not None 193 - assert result.path is not None 194 - assert result.content_type is not None 195 - 196 - def test_search_in_body(self, resolver: ContentResolver): 197 - # Should find magic missile by searching for "auto-hit" 198 - results = resolver.search("Auto-hit") 199 - assert len(results) >= 1 200 - assert any("magic-missile" in r.name for r in results)
+98 -152
tests/test_entities.py
··· 4 4 5 5 import pytest 6 6 7 - from storied.log import CampaignLog 8 7 from storied.session import ( 9 8 extract_wiki_links, 10 9 load_entity_content, 11 10 resolve_wiki_link, 12 11 ) 13 - from storied.tools import EntityIndex, establish, mark 14 - 15 - 16 - @pytest.fixture 17 - def world_base(tmp_path: Path) -> Path: 18 - """Create a base directory with world structure.""" 19 - world = tmp_path / "worlds" / "test-world" 20 - world.mkdir(parents=True) 21 - return tmp_path 22 - 23 - 24 - @pytest.fixture 25 - def campaign_log(world_base: Path) -> CampaignLog: 26 - """Create a campaign log for the test world.""" 27 - return CampaignLog("test-world", world_base) 12 + from storied.tools import EntityIndex, ToolContext, establish, mark 28 13 29 14 30 15 class TestEstablish: 31 16 """Tests for the establish tool.""" 32 17 33 - def test_establish_creates_npc(self, world_base: Path): 18 + def test_establish_creates_npc(self, ctx: ToolContext): 34 19 result = establish( 35 20 entity_type="npcs", 36 21 name="Vera Blackwater", 22 + ctx=ctx, 37 23 description="Tavern owner, former smuggler.", 38 24 knows=["The guild smuggles weapons"], 39 25 wants=["Keep her tavern safe"], 40 26 will=["If trusted → introduce to Harrik"], 41 - world_id="test-world", 42 - base_path=world_base, 43 27 ) 44 28 45 29 assert "Established" in result 46 - npc_file = world_base / "worlds/test-world/npcs/Vera Blackwater.md" 30 + npc_file = ctx.base_path / "worlds/test-world/npcs/Vera Blackwater.md" 47 31 assert npc_file.exists() 48 32 49 - def test_establish_file_format(self, world_base: Path): 33 + def test_establish_file_format(self, ctx: ToolContext): 50 34 establish( 51 35 entity_type="npcs", 52 36 name="Test NPC", 37 + ctx=ctx, 53 38 description="A test character.", 54 39 knows=["Secret one", "Secret two"], 55 40 wants=["Goal one"], 56 41 will=["If X → do Y"], 57 - world_id="test-world", 58 - base_path=world_base, 59 42 ) 60 43 61 - content = (world_base / "worlds/test-world/npcs/Test NPC.md").read_text() 44 + content = (ctx.base_path / "worlds/test-world/npcs/Test NPC.md").read_text() 62 45 63 46 assert "# Test NPC" in content 64 47 assert "## Is" in content ··· 71 54 assert "### Will" in content 72 55 assert "- If X → do Y" in content 73 56 74 - def test_establish_location(self, world_base: Path): 57 + def test_establish_location(self, ctx: ToolContext): 75 58 establish( 76 59 entity_type="locations", 77 60 name="The Rusty Anchor", 61 + ctx=ctx, 78 62 description="A dockside tavern.", 79 63 knows=["Hidden tunnel in cellar"], 80 64 wants=["To shelter those who need it"], 81 65 will=["If searched → reveal tunnel"], 82 - world_id="test-world", 83 - base_path=world_base, 84 66 ) 85 67 86 - loc_file = world_base / "worlds/test-world/locations/The Rusty Anchor.md" 68 + loc_file = ctx.base_path / "worlds/test-world/locations/The Rusty Anchor.md" 87 69 assert loc_file.exists() 88 70 content = loc_file.read_text() 89 71 assert "Hidden tunnel in cellar" in content 90 72 91 - def test_establish_item(self, world_base: Path): 73 + def test_establish_item(self, ctx: ToolContext): 92 74 establish( 93 75 entity_type="items", 94 76 name="The Skeleton Key", 77 + ctx=ctx, 95 78 description="Ancient brass key, cold to the touch.", 96 79 knows=["Forged by Archmage Velius"], 97 80 wants=["To free what is locked away"], 98 81 will=["Unlock any non-magical lock"], 99 - world_id="test-world", 100 - base_path=world_base, 101 82 ) 102 83 103 - item_file = world_base / "worlds/test-world/items/The Skeleton Key.md" 84 + item_file = ctx.base_path / "worlds/test-world/items/The Skeleton Key.md" 104 85 assert item_file.exists() 105 86 106 - def test_establish_partial_update(self, world_base: Path): 87 + def test_establish_partial_update(self, ctx: ToolContext): 107 88 # Create initial entity 108 89 establish( 109 90 entity_type="npcs", 110 91 name="Guard Mara", 92 + ctx=ctx, 111 93 description="A stern city guard.", 112 94 knows=["Patrol routes"], 113 95 wants=["Uphold the law"], 114 96 will=[], 115 - world_id="test-world", 116 - base_path=world_base, 117 97 ) 118 98 119 99 # Update just the description 120 100 establish( 121 101 entity_type="npcs", 122 102 name="Guard Mara", 103 + ctx=ctx, 123 104 description="A stern city guard, recently promoted to sergeant.", 124 - world_id="test-world", 125 - base_path=world_base, 126 105 ) 127 106 128 - content = (world_base / "worlds/test-world/npcs/Guard Mara.md").read_text() 107 + content = (ctx.base_path / "worlds/test-world/npcs/Guard Mara.md").read_text() 129 108 assert "recently promoted" in content 130 109 assert "Patrol routes" in content # Preserved from original 131 110 132 - def test_establish_with_wikilinks(self, world_base: Path): 111 + def test_establish_with_wikilinks(self, ctx: ToolContext): 133 112 establish( 134 113 entity_type="npcs", 135 114 name="Captain Harrik", 115 + ctx=ctx, 136 116 description="Veteran sailor who frequents [[The Rusty Anchor]].", 137 117 knows=["[[Vera Blackwater]] was once a smuggler"], 138 118 wants=["Find the [[Ghost Ship]]"], 139 119 will=[], 140 - world_id="test-world", 141 - base_path=world_base, 142 120 ) 143 121 144 - content = (world_base / "worlds/test-world/npcs/Captain Harrik.md").read_text() 122 + content = (ctx.base_path / "worlds/test-world/npcs/Captain Harrik.md").read_text() 145 123 assert "[[The Rusty Anchor]]" in content 146 124 assert "[[Vera Blackwater]]" in content 147 125 148 - def test_establish_empty_sections_omitted(self, world_base: Path): 126 + def test_establish_empty_sections_omitted(self, ctx: ToolContext): 149 127 establish( 150 128 entity_type="npcs", 151 129 name="Simple NPC", 130 + ctx=ctx, 152 131 description="Just a description.", 153 132 knows=[], 154 133 wants=[], 155 134 will=[], 156 - world_id="test-world", 157 - base_path=world_base, 158 135 ) 159 136 160 - content = (world_base / "worlds/test-world/npcs/Simple NPC.md").read_text() 137 + content = (ctx.base_path / "worlds/test-world/npcs/Simple NPC.md").read_text() 161 138 assert "### Knows" not in content 162 139 assert "### Wants" not in content 163 140 assert "### Will" not in content 164 141 165 - def test_establish_with_location(self, world_base: Path): 142 + def test_establish_with_location(self, ctx: ToolContext): 166 143 establish( 167 144 entity_type="npcs", 168 145 name="Garrick the Jailer", 146 + ctx=ctx, 169 147 description="Heavyset man in his fifties.", 170 148 location="In the basement of [[Greyhaven City Jail]]", 171 149 knows=["Where the keys are kept"], 172 - world_id="test-world", 173 - base_path=world_base, 174 150 ) 175 151 176 - content = (world_base / "worlds/test-world/npcs/Garrick the Jailer.md").read_text() 152 + content = (ctx.base_path / "worlds/test-world/npcs/Garrick the Jailer.md").read_text() 177 153 assert "**Location:** In the basement of [[Greyhaven City Jail]]" in content 178 154 assert "Heavyset man" in content 179 155 180 - def test_establish_location_preserved_on_update(self, world_base: Path): 156 + def test_establish_location_preserved_on_update(self, ctx: ToolContext): 181 157 # Create with location 182 158 establish( 183 159 entity_type="npcs", 184 160 name="Wanderer", 161 + ctx=ctx, 185 162 description="A traveler.", 186 163 location="[[The Rusty Anchor]]", 187 - world_id="test-world", 188 - base_path=world_base, 189 164 ) 190 165 191 166 # Update without specifying location 192 167 establish( 193 168 entity_type="npcs", 194 169 name="Wanderer", 170 + ctx=ctx, 195 171 description="A weary traveler.", 196 - world_id="test-world", 197 - base_path=world_base, 198 172 ) 199 173 200 - content = (world_base / "worlds/test-world/npcs/Wanderer.md").read_text() 174 + content = (ctx.base_path / "worlds/test-world/npcs/Wanderer.md").read_text() 201 175 assert "**Location:** [[The Rusty Anchor]]" in content 202 176 assert "weary traveler" in content 203 177 ··· 205 179 class TestMark: 206 180 """Tests for the mark tool.""" 207 181 208 - def test_mark_appends_to_was(self, world_base: Path, campaign_log: CampaignLog): 182 + def test_mark_appends_to_was(self, ctx: ToolContext): 209 183 # Create entity first 210 184 establish( 211 185 entity_type="npcs", 212 186 name="Vera Blackwater", 187 + ctx=ctx, 213 188 description="Tavern owner.", 214 189 knows=[], 215 190 wants=[], 216 191 will=[], 217 - world_id="test-world", 218 - base_path=world_base, 219 192 ) 220 193 221 194 result = mark( 222 195 entity_type="npcs", 223 196 name="Vera Blackwater", 224 197 event="Met the player, gave them a room", 225 - world_id="test-world", 226 - base_path=world_base, 227 - campaign_log=campaign_log, 198 + ctx=ctx, 228 199 ) 229 200 230 201 assert "Marked" in result 231 - content = (world_base / "worlds/test-world/npcs/Vera Blackwater.md").read_text() 202 + content = (ctx.base_path / "worlds/test-world/npcs/Vera Blackwater.md").read_text() 232 203 assert "## Was" in content 233 204 assert "Met the player" in content 234 205 235 - def test_mark_includes_timestamp(self, world_base: Path, campaign_log: CampaignLog): 206 + def test_mark_includes_timestamp(self, ctx: ToolContext): 236 207 establish( 237 208 entity_type="npcs", 238 209 name="Test NPC", 210 + ctx=ctx, 239 211 description="Test.", 240 - world_id="test-world", 241 - base_path=world_base, 242 212 ) 243 213 244 214 mark( 245 215 entity_type="npcs", 246 216 name="Test NPC", 247 217 event="Something happened", 248 - world_id="test-world", 249 - base_path=world_base, 250 - campaign_log=campaign_log, 218 + ctx=ctx, 251 219 ) 252 220 253 - content = (world_base / "worlds/test-world/npcs/Test NPC.md").read_text() 221 + content = (ctx.base_path / "worlds/test-world/npcs/Test NPC.md").read_text() 254 222 # Should have timestamp anchor format 255 223 assert "#d" in content 256 224 assert "|" in content 257 225 258 - def test_mark_resolves_will_trigger(self, world_base: Path, campaign_log: CampaignLog): 226 + def test_mark_resolves_will_trigger(self, ctx: ToolContext): 259 227 establish( 260 228 entity_type="npcs", 261 229 name="Vera Blackwater", 230 + ctx=ctx, 262 231 description="Tavern owner.", 263 232 knows=[], 264 233 wants=[], 265 234 will=["If trusted → introduce to Harrik", "If threatened → tip off guild"], 266 - world_id="test-world", 267 - base_path=world_base, 268 235 ) 269 236 270 237 mark( 271 238 entity_type="npcs", 272 239 name="Vera Blackwater", 273 240 event="Introduced player to Captain Harrik", 241 + ctx=ctx, 274 242 resolves=["If trusted → introduce to Harrik"], 275 - world_id="test-world", 276 - base_path=world_base, 277 - campaign_log=campaign_log, 278 243 ) 279 244 280 - content = (world_base / "worlds/test-world/npcs/Vera Blackwater.md").read_text() 245 + content = (ctx.base_path / "worlds/test-world/npcs/Vera Blackwater.md").read_text() 281 246 # Resolved trigger should be removed 282 247 assert "If trusted → introduce to Harrik" not in content 283 248 # Other trigger should remain 284 249 assert "If threatened → tip off guild" in content 285 250 286 - def test_mark_resolves_multiple_triggers(self, world_base: Path, campaign_log: CampaignLog): 251 + def test_mark_resolves_multiple_triggers(self, ctx: ToolContext): 287 252 establish( 288 253 entity_type="npcs", 289 254 name="Complex NPC", 255 + ctx=ctx, 290 256 description="Has many triggers.", 291 257 knows=[], 292 258 wants=[], ··· 295 261 "If trusted → reveal secret", 296 262 "If threatened → flee", 297 263 ], 298 - world_id="test-world", 299 - base_path=world_base, 300 264 ) 301 265 302 266 result = mark( 303 267 entity_type="npcs", 304 268 name="Complex NPC", 305 269 event="Player became a trusted ally through multiple good deeds", 270 + ctx=ctx, 306 271 resolves=["If friendly → share rumors", "If trusted → reveal secret"], 307 - world_id="test-world", 308 - base_path=world_base, 309 - campaign_log=campaign_log, 310 272 ) 311 273 312 - content = (world_base / "worlds/test-world/npcs/Complex NPC.md").read_text() 274 + content = (ctx.base_path / "worlds/test-world/npcs/Complex NPC.md").read_text() 313 275 # Both resolved triggers should be removed 314 276 assert "If friendly → share rumors" not in content 315 277 assert "If trusted → reveal secret" not in content ··· 318 280 # Result should mention resolving multiple 319 281 assert "resolved 2 triggers" in result 320 282 321 - def test_mark_multiple_events(self, world_base: Path, campaign_log: CampaignLog): 283 + def test_mark_multiple_events(self, ctx: ToolContext): 322 284 establish( 323 285 entity_type="locations", 324 286 name="The Docks", 287 + ctx=ctx, 325 288 description="Busy harbor area.", 326 - world_id="test-world", 327 - base_path=world_base, 328 289 ) 329 290 330 291 mark( 331 292 entity_type="locations", 332 293 name="The Docks", 333 294 event="Player arrived by ferry", 334 - world_id="test-world", 335 - base_path=world_base, 336 - campaign_log=campaign_log, 295 + ctx=ctx, 337 296 ) 338 297 339 298 # Advance time 340 - campaign_log.append_entry("Time passed", "1 hour") 299 + ctx.campaign_log.append_entry("Time passed", "1 hour") 341 300 342 301 mark( 343 302 entity_type="locations", 344 303 name="The Docks", 345 304 event="Player witnessed smugglers loading cargo", 346 - world_id="test-world", 347 - base_path=world_base, 348 - campaign_log=campaign_log, 305 + ctx=ctx, 349 306 ) 350 307 351 - content = (world_base / "worlds/test-world/locations/The Docks.md").read_text() 308 + content = (ctx.base_path / "worlds/test-world/locations/The Docks.md").read_text() 352 309 assert "Player arrived" in content 353 310 assert "witnessed smugglers" in content 354 311 355 - def test_mark_nonexistent_entity_fails(self, world_base: Path, campaign_log: CampaignLog): 312 + def test_mark_nonexistent_entity_fails(self, ctx: ToolContext): 356 313 result = mark( 357 314 entity_type="npcs", 358 315 name="Nobody", 359 316 event="Did something", 360 - world_id="test-world", 361 - base_path=world_base, 362 - campaign_log=campaign_log, 317 + ctx=ctx, 363 318 ) 364 319 365 320 assert "not found" in result.lower() ··· 383 338 links = extract_wiki_links(text) 384 339 assert links == ["Vera", "Vera", "Bob"] 385 340 386 - def test_resolve_wiki_link_npc(self, world_base: Path): 341 + def test_resolve_wiki_link_npc(self, ctx: ToolContext): 387 342 establish( 388 343 entity_type="npcs", 389 344 name="Vera Blackwater", 345 + ctx=ctx, 390 346 description="Test.", 391 - world_id="test-world", 392 - base_path=world_base, 393 347 ) 394 348 395 - path = resolve_wiki_link("Vera Blackwater", "test-world", world_base) 349 + path = resolve_wiki_link("Vera Blackwater", "test-world", ctx.base_path) 396 350 assert path is not None 397 351 assert path.name == "Vera Blackwater.md" 398 352 assert "npcs" in str(path) 399 353 400 - def test_resolve_wiki_link_location(self, world_base: Path): 354 + def test_resolve_wiki_link_location(self, ctx: ToolContext): 401 355 establish( 402 356 entity_type="locations", 403 357 name="The Rusty Anchor", 358 + ctx=ctx, 404 359 description="Test.", 405 - world_id="test-world", 406 - base_path=world_base, 407 360 ) 408 361 409 - path = resolve_wiki_link("The Rusty Anchor", "test-world", world_base) 362 + path = resolve_wiki_link("The Rusty Anchor", "test-world", ctx.base_path) 410 363 assert path is not None 411 364 assert "locations" in str(path) 412 365 413 - def test_resolve_wiki_link_not_found(self, world_base: Path): 414 - path = resolve_wiki_link("Nonexistent", "test-world", world_base) 366 + def test_resolve_wiki_link_not_found(self, ctx: ToolContext): 367 + path = resolve_wiki_link("Nonexistent", "test-world", ctx.base_path) 415 368 assert path is None 416 369 417 - def test_resolve_wiki_link_priority_order(self, world_base: Path): 370 + def test_resolve_wiki_link_priority_order(self, ctx: ToolContext): 418 371 # Create same name in multiple directories - npcs should win 419 - (world_base / "worlds/test-world/npcs").mkdir(parents=True) 420 - (world_base / "worlds/test-world/locations").mkdir(parents=True) 372 + (ctx.base_path / "worlds/test-world/npcs").mkdir(parents=True, exist_ok=True) 373 + (ctx.base_path / "worlds/test-world/locations").mkdir(parents=True, exist_ok=True) 421 374 422 - (world_base / "worlds/test-world/npcs/Ambiguous.md").write_text("# NPC version") 423 - (world_base / "worlds/test-world/locations/Ambiguous.md").write_text( 375 + (ctx.base_path / "worlds/test-world/npcs/Ambiguous.md").write_text("# NPC version") 376 + (ctx.base_path / "worlds/test-world/locations/Ambiguous.md").write_text( 424 377 "# Location version" 425 378 ) 426 379 427 - path = resolve_wiki_link("Ambiguous", "test-world", world_base) 380 + path = resolve_wiki_link("Ambiguous", "test-world", ctx.base_path) 428 381 assert path is not None 429 382 assert "npcs" in str(path) # NPCs have priority over locations 430 383 ··· 432 385 class TestLoadEntityContent: 433 386 """Tests for loading entity content by name.""" 434 387 435 - def test_load_entity_content(self, world_base: Path): 388 + def test_load_entity_content(self, ctx: ToolContext): 436 389 establish( 437 390 entity_type="npcs", 438 391 name="Test Character", 392 + ctx=ctx, 439 393 description="A test.", 440 394 knows=["A secret"], 441 - world_id="test-world", 442 - base_path=world_base, 443 395 ) 444 396 445 - entity = load_entity_content("Test Character", "test-world", world_base) 397 + entity = load_entity_content("Test Character", "test-world", ctx.base_path) 446 398 assert entity is not None 447 399 assert entity["name"] == "Test Character" 448 400 assert entity["entity_type"] == "npcs" 449 401 assert "A test." in entity["content"] 450 402 assert "A secret" in entity["content"] 451 403 452 - def test_load_entity_content_not_found(self, world_base: Path): 453 - entity = load_entity_content("Nobody", "test-world", world_base) 404 + def test_load_entity_content_not_found(self, ctx: ToolContext): 405 + entity = load_entity_content("Nobody", "test-world", ctx.base_path) 454 406 assert entity is None 455 407 456 408 ··· 458 410 459 411 460 412 @pytest.fixture 461 - def indexed_world(world_base: Path) -> tuple[Path, EntityIndex]: 413 + def indexed_world(ctx: ToolContext) -> tuple[Path, EntityIndex]: 462 414 """Create a world with entities and build an index.""" 463 - world_dir = world_base / "worlds" / "test-world" 415 + world_dir = ctx.base_path / "worlds" / "test-world" 464 416 for etype, name in [("npcs", "Vera"), ("locations", "Tavern"), ("items", "Sword")]: 465 417 d = world_dir / etype 466 418 d.mkdir(parents=True, exist_ok=True) ··· 521 473 class TestEstablishWithIndex: 522 474 """Tests that establish writes through to the index and cache.""" 523 475 524 - def test_establish_registers_in_index(self, world_base: Path): 525 - world_dir = world_base / "worlds" / "test-world" 526 - index = EntityIndex(world_dir) 476 + def test_establish_registers_in_index(self, ctx: ToolContext): 477 + ctx.entity_index = EntityIndex(ctx.base_path / "worlds" / ctx.world_id) 527 478 establish( 528 - entity_type="npcs", name="New NPC", 479 + entity_type="npcs", 480 + name="New NPC", 481 + ctx=ctx, 529 482 description="A stranger.", 530 - world_id="test-world", base_path=world_base, 531 - entity_index=index, 532 483 ) 533 - assert index.resolve("New NPC") is not None 484 + assert ctx.entity_index.resolve("New NPC") is not None 534 485 535 - def test_establish_caches_entity(self, world_base: Path): 536 - world_dir = world_base / "worlds" / "test-world" 537 - index = EntityIndex(world_dir) 486 + def test_establish_caches_entity(self, ctx: ToolContext): 487 + ctx.entity_index = EntityIndex(ctx.base_path / "worlds" / ctx.world_id) 538 488 establish( 539 - entity_type="npcs", name="Cached NPC", 489 + entity_type="npcs", 490 + name="Cached NPC", 491 + ctx=ctx, 540 492 description="Cached.", 541 493 knows=["a secret"], 542 - world_id="test-world", base_path=world_base, 543 - entity_index=index, 544 494 ) 545 - path = index.resolve("Cached NPC") 546 - cached = index.cache_get(path) 495 + path = ctx.entity_index.resolve("Cached NPC") 496 + cached = ctx.entity_index.cache_get(path) 547 497 assert cached is not None 548 498 assert cached["description"] == "Cached." 549 499 assert cached["knows"] == ["a secret"] 550 500 551 - def test_mark_updates_cache( 552 - self, world_base: Path, campaign_log: CampaignLog, 553 - ): 554 - world_dir = world_base / "worlds" / "test-world" 555 - index = EntityIndex(world_dir) 501 + def test_mark_updates_cache(self, ctx: ToolContext): 502 + ctx.entity_index = EntityIndex(ctx.base_path / "worlds" / ctx.world_id) 556 503 establish( 557 - entity_type="npcs", name="Markable", 504 + entity_type="npcs", 505 + name="Markable", 506 + ctx=ctx, 558 507 description="An NPC.", 559 - world_id="test-world", base_path=world_base, 560 - entity_index=index, 561 508 ) 562 509 mark( 563 - entity_type="npcs", name="Markable", 510 + entity_type="npcs", 511 + name="Markable", 564 512 event="Something happened", 565 - world_id="test-world", base_path=world_base, 566 - campaign_log=campaign_log, 567 - entity_index=index, 513 + ctx=ctx, 568 514 ) 569 - path = index.resolve("Markable") 570 - cached = index.cache_get(path) 515 + path = ctx.entity_index.resolve("Markable") 516 + cached = ctx.entity_index.cache_get(path) 571 517 assert any("Something happened" in w for w in cached["was"])
+102 -136
tests/test_planner.py
··· 1 1 """Tests for the world planner — entity richness scoring, discovery, and context.""" 2 2 3 3 import json 4 - from pathlib import Path 5 4 from unittest.mock import MagicMock, patch 6 5 7 6 import pytest 8 7 9 - from storied.log import CampaignLog 10 8 from storied.planner import ( 11 9 PlanResult, 12 10 build_planning_context, ··· 15 13 plan_world, 16 14 ) 17 15 from storied.session import save_session 18 - from storied.tools import establish 19 - 20 - 21 - @pytest.fixture 22 - def world_base(tmp_path: Path) -> Path: 23 - """Create a base directory with world structure.""" 24 - world = tmp_path / "worlds" / "test-world" 25 - world.mkdir(parents=True) 26 - return tmp_path 27 - 28 - 29 - @pytest.fixture 30 - def campaign_log(world_base: Path) -> CampaignLog: 31 - """Create a campaign log for the test world.""" 32 - return CampaignLog("test-world", world_base) 16 + from storied.tools import ToolContext, establish, mark 33 17 34 18 35 19 @pytest.fixture 36 - def populated_world(world_base: Path) -> Path: 20 + def populated_world(ctx: ToolContext) -> ToolContext: 37 21 """Create a world with some entities for testing.""" 38 22 establish( 39 23 entity_type="locations", 40 24 name="Town Square", 25 + ctx=ctx, 41 26 description="The center of [[Millford]]. A fountain stands here, surrounded by market stalls.", 42 27 location="Central [[Millford]]", 43 28 knows=["The fountain was built by [[Old Gregor]]"], 44 29 wants=["To be a gathering place"], 45 30 will=["If market day → attract crowds"], 46 - world_id="test-world", 47 - base_path=world_base, 48 31 ) 49 32 establish( 50 33 entity_type="npcs", 51 34 name="Old Gregor", 35 + ctx=ctx, 52 36 description="An elderly stonemason.", 53 37 location="[[Town Square]]", 54 - world_id="test-world", 55 - base_path=world_base, 56 38 ) 57 39 establish( 58 40 entity_type="locations", 59 41 name="Millford", 42 + ctx=ctx, 60 43 description="A small riverside town.", 61 - world_id="test-world", 62 - base_path=world_base, 63 44 ) 64 45 establish( 65 46 entity_type="npcs", 66 47 name="Thin NPC", 48 + ctx=ctx, 67 49 description="Someone with no inner life.", 68 - world_id="test-world", 69 - base_path=world_base, 70 50 ) 71 - return world_base 51 + return ctx 72 52 73 53 74 54 class TestEntityRichness: 75 55 """Tests for richness scoring.""" 76 56 77 - def test_empty_entity_scores_zero(self, world_base: Path): 57 + def test_empty_entity_scores_zero(self, ctx: ToolContext): 78 58 establish( 79 59 entity_type="npcs", 80 60 name="Empty", 81 - world_id="test-world", 82 - base_path=world_base, 61 + ctx=ctx, 83 62 ) 84 - path = world_base / "worlds/test-world/npcs/Empty.md" 63 + path = ctx.base_path / "worlds/test-world/npcs/Empty.md" 85 64 assert entity_richness(path) == 0.0 86 65 87 - def test_description_only(self, world_base: Path): 66 + def test_description_only(self, ctx: ToolContext): 88 67 establish( 89 68 entity_type="npcs", 90 69 name="Described", 70 + ctx=ctx, 91 71 description="A tall warrior.", 92 - world_id="test-world", 93 - base_path=world_base, 94 72 ) 95 - path = world_base / "worlds/test-world/npcs/Described.md" 73 + path = ctx.base_path / "worlds/test-world/npcs/Described.md" 96 74 assert entity_richness(path) == pytest.approx(0.2) 97 75 98 - def test_fully_rich_entity(self, world_base: Path, campaign_log: CampaignLog): 76 + def test_fully_rich_entity(self, ctx: ToolContext): 99 77 establish( 100 78 entity_type="npcs", 101 79 name="Rich NPC", 80 + ctx=ctx, 102 81 description="A fully fleshed out character in [[Town Square]].", 103 82 knows=["Secret one", "Secret two"], 104 83 wants=["Goal one"], 105 84 will=["If X → do Y"], 106 - world_id="test-world", 107 - base_path=world_base, 108 85 ) 109 - from storied.tools import mark 110 - 111 86 mark( 112 87 entity_type="npcs", 113 88 name="Rich NPC", 114 89 event="Something happened", 115 - world_id="test-world", 116 - base_path=world_base, 117 - campaign_log=campaign_log, 90 + ctx=ctx, 118 91 ) 119 - path = world_base / "worlds/test-world/npcs/Rich NPC.md" 92 + path = ctx.base_path / "worlds/test-world/npcs/Rich NPC.md" 120 93 score = entity_richness(path) 121 94 assert score == pytest.approx(1.0) 122 95 123 - def test_partial_richness(self, world_base: Path): 96 + def test_partial_richness(self, ctx: ToolContext): 124 97 establish( 125 98 entity_type="npcs", 126 99 name="Partial", 100 + ctx=ctx, 127 101 description="Has description and knows.", 128 102 knows=["A secret"], 129 - world_id="test-world", 130 - base_path=world_base, 131 103 ) 132 - path = world_base / "worlds/test-world/npcs/Partial.md" 104 + path = ctx.base_path / "worlds/test-world/npcs/Partial.md" 133 105 score = entity_richness(path) 134 106 # description (0.2) + knows (0.2) = 0.4 135 107 assert score == pytest.approx(0.4) 136 108 137 - def test_wikilinks_contribute(self, world_base: Path): 109 + def test_wikilinks_contribute(self, ctx: ToolContext): 138 110 establish( 139 111 entity_type="npcs", 140 112 name="Linked", 113 + ctx=ctx, 141 114 description="Hangs out at [[The Tavern]] with [[Bob]].", 142 - world_id="test-world", 143 - base_path=world_base, 144 115 ) 145 - path = world_base / "worlds/test-world/npcs/Linked.md" 116 + path = ctx.base_path / "worlds/test-world/npcs/Linked.md" 146 117 score = entity_richness(path) 147 118 # description (0.2) + wikilinks (0.1) = 0.3 148 119 assert score == pytest.approx(0.3) ··· 151 122 class TestFindNearbyEntities: 152 123 """Tests for discovering entities near the player.""" 153 124 154 - def test_finds_entities_from_location_links(self, populated_world: Path): 125 + def test_finds_entities_from_location_links(self, populated_world: ToolContext): 155 126 session = { 156 127 "location": "Town Square", 157 128 "body": "", 158 129 } 159 - nearby = find_nearby_entities(session, "test-world", populated_world) 130 + nearby = find_nearby_entities(session, "test-world", populated_world.base_path) 160 131 names = {name for name, _ in nearby} 161 132 assert "Old Gregor" in names 162 133 assert "Millford" in names 163 134 164 - def test_finds_entities_from_session_body(self, populated_world: Path): 135 + def test_finds_entities_from_session_body(self, populated_world: ToolContext): 165 136 session = { 166 137 "location": "Town Square", 167 138 "body": "## Present\n- [[Thin NPC]]", 168 139 } 169 - nearby = find_nearby_entities(session, "test-world", populated_world) 140 + nearby = find_nearby_entities(session, "test-world", populated_world.base_path) 170 141 names = {name for name, _ in nearby} 171 142 assert "Thin NPC" in names 172 143 173 - def test_includes_current_location(self, populated_world: Path): 144 + def test_includes_current_location(self, populated_world: ToolContext): 174 145 session = { 175 146 "location": "Town Square", 176 147 "body": "", 177 148 } 178 - nearby = find_nearby_entities(session, "test-world", populated_world) 149 + nearby = find_nearby_entities(session, "test-world", populated_world.base_path) 179 150 names = {name for name, _ in nearby} 180 151 assert "Town Square" in names 181 152 182 - def test_no_duplicates(self, populated_world: Path): 153 + def test_no_duplicates(self, populated_world: ToolContext): 183 154 session = { 184 155 "location": "Town Square", 185 156 "body": "## Present\n- [[Old Gregor]]", 186 157 } 187 - nearby = find_nearby_entities(session, "test-world", populated_world) 158 + nearby = find_nearby_entities(session, "test-world", populated_world.base_path) 188 159 names = [name for name, _ in nearby] 189 160 assert names.count("Old Gregor") == 1 190 161 191 - def test_empty_session(self, world_base: Path): 162 + def test_empty_session(self, ctx: ToolContext): 192 163 session = {"body": ""} 193 - nearby = find_nearby_entities(session, "test-world", world_base) 164 + nearby = find_nearby_entities(session, "test-world", ctx.base_path) 194 165 assert nearby == [] 195 166 196 167 197 168 class TestGetRecentEntries: 198 169 """Tests for CampaignLog.get_recent_entries().""" 199 170 200 - def test_returns_current_day_entries(self, world_base: Path): 201 - log = CampaignLog("test-world", world_base) 202 - log.append_entry("Arrived at the tavern", "10 min") 203 - log.append_entry("Spoke with bartender", "15 min") 171 + def test_returns_current_day_entries(self, ctx: ToolContext): 172 + ctx.campaign_log.append_entry("Arrived at the tavern", "10 min") 173 + ctx.campaign_log.append_entry("Spoke with bartender", "15 min") 204 174 205 - entries = log.get_recent_entries(days=1) 175 + entries = ctx.campaign_log.get_recent_entries(days=1) 206 176 events = [e.event for e in entries] 207 177 assert "Arrived at the tavern" in events 208 178 assert "Spoke with bartender" in events 209 179 210 - def test_returns_previous_day_entries(self, world_base: Path): 211 - log = CampaignLog("test-world", world_base) 212 - log.append_entry("Morning event", "10 min") 213 - log.append_entry("Traveled all day", "18 hours") 214 - log.append_entry("Day 2 event", "10 min") 180 + def test_returns_previous_day_entries(self, ctx: ToolContext): 181 + ctx.campaign_log.append_entry("Morning event", "10 min") 182 + ctx.campaign_log.append_entry("Traveled all day", "18 hours") 183 + ctx.campaign_log.append_entry("Day 2 event", "10 min") 215 184 216 - entries = log.get_recent_entries(days=2) 185 + entries = ctx.campaign_log.get_recent_entries(days=2) 217 186 events = [e.event for e in entries] 218 187 assert "Morning event" in events 219 188 assert "Day 2 event" in events 220 189 221 - def test_respects_days_limit(self, world_base: Path): 222 - log = CampaignLog("test-world", world_base) 223 - log.append_entry("Day 1 event", "18 hours") 224 - log.append_entry("Day 2 event", "18 hours") 225 - log.append_entry("Day 3 event", "1 hour") 190 + def test_respects_days_limit(self, ctx: ToolContext): 191 + ctx.campaign_log.append_entry("Day 1 event", "18 hours") 192 + ctx.campaign_log.append_entry("Day 2 event", "18 hours") 193 + ctx.campaign_log.append_entry("Day 3 event", "1 hour") 226 194 227 - entries = log.get_recent_entries(days=1) 195 + entries = ctx.campaign_log.get_recent_entries(days=1) 228 196 events = [e.event for e in entries] 229 197 assert "Day 3 event" in events 230 198 assert "Day 1 event" not in events 231 199 232 - def test_empty_log(self, world_base: Path): 233 - log = CampaignLog("test-world", world_base) 234 - entries = log.get_recent_entries(days=2) 200 + def test_empty_log(self, ctx: ToolContext): 201 + entries = ctx.campaign_log.get_recent_entries(days=2) 235 202 assert entries == [] 236 203 237 204 238 205 class TestBuildPlanningContext: 239 206 """Tests for assembling the planner's context.""" 240 207 241 - def test_includes_session_info(self, populated_world: Path): 208 + def test_includes_session_info(self, populated_world: ToolContext): 242 209 save_session( 243 210 "default", 244 211 { 245 212 "location": "Town Square", 246 213 "body": "## Situation\nThe player just arrived.\n\n## Open Threads\n- Find the missing cat", 247 214 }, 248 - populated_world, 215 + populated_world.base_path, 249 216 ) 250 217 context = build_planning_context( 251 - world_id="test-world", 252 - player_id="default", 253 - base_path=populated_world, 254 - candidates=[("Thin NPC", populated_world / "worlds/test-world/npcs/Thin NPC.md")], 218 + world_id=populated_world.world_id, 219 + player_id=populated_world.player_id, 220 + base_path=populated_world.base_path, 221 + candidates=[("Thin NPC", populated_world.base_path / "worlds/test-world/npcs/Thin NPC.md")], 255 222 ) 256 223 assert "Town Square" in context 257 224 assert "missing cat" in context 258 225 259 - def test_includes_candidate_content(self, populated_world: Path): 226 + def test_includes_candidate_content(self, populated_world: ToolContext): 260 227 save_session( 261 228 "default", 262 229 {"location": "Town Square", "body": ""}, 263 - populated_world, 230 + populated_world.base_path, 264 231 ) 265 - path = populated_world / "worlds/test-world/npcs/Thin NPC.md" 232 + path = populated_world.base_path / "worlds/test-world/npcs/Thin NPC.md" 266 233 context = build_planning_context( 267 - world_id="test-world", 268 - player_id="default", 269 - base_path=populated_world, 234 + world_id=populated_world.world_id, 235 + player_id=populated_world.player_id, 236 + base_path=populated_world.base_path, 270 237 candidates=[("Thin NPC", path)], 271 238 ) 272 239 assert "Thin NPC" in context 273 240 assert "Someone with no inner life" in context 274 241 275 - def test_empty_candidates(self, populated_world: Path): 242 + def test_empty_candidates(self, populated_world: ToolContext): 276 243 save_session( 277 244 "default", 278 245 {"location": "Town Square", "body": ""}, 279 - populated_world, 246 + populated_world.base_path, 280 247 ) 281 248 context = build_planning_context( 282 - world_id="test-world", 283 - player_id="default", 284 - base_path=populated_world, 249 + world_id=populated_world.world_id, 250 + player_id=populated_world.player_id, 251 + base_path=populated_world.base_path, 285 252 candidates=[], 286 253 ) 287 254 assert "No entities" in context 288 255 289 - def test_includes_recent_log_entries(self, populated_world: Path): 256 + def test_includes_recent_log_entries(self, populated_world: ToolContext): 290 257 save_session( 291 258 "default", 292 259 {"location": "Town Square", "body": ""}, 293 - populated_world, 260 + populated_world.base_path, 294 261 ) 295 - log = CampaignLog("test-world", populated_world) 296 - log.append_entry("Fought three goblins in the clearing", "5 rounds") 297 - log.append_entry("Spotted two lookouts near the old mill", "30 min") 262 + populated_world.campaign_log.append_entry("Fought three goblins in the clearing", "5 rounds") 263 + populated_world.campaign_log.append_entry("Spotted two lookouts near the old mill", "30 min") 298 264 299 265 context = build_planning_context( 300 - world_id="test-world", 301 - player_id="default", 302 - base_path=populated_world, 266 + world_id=populated_world.world_id, 267 + player_id=populated_world.player_id, 268 + base_path=populated_world.base_path, 303 269 candidates=[], 304 270 ) 305 271 assert "Recent Events" in context ··· 310 276 class TestPlanWorld: 311 277 """Tests for the plan_world orchestrator.""" 312 278 313 - def test_dry_run_returns_candidates(self, populated_world: Path): 279 + def test_dry_run_returns_candidates(self, populated_world: ToolContext): 314 280 save_session( 315 281 "default", 316 282 { 317 283 "location": "Town Square", 318 284 "body": "## Present\n- [[Thin NPC]]", 319 285 }, 320 - populated_world, 286 + populated_world.base_path, 321 287 ) 322 288 result = plan_world( 323 - world_id="test-world", 324 - player_id="default", 325 - base_path=populated_world, 289 + world_id=populated_world.world_id, 290 + player_id=populated_world.player_id, 291 + base_path=populated_world.base_path, 326 292 dry_run=True, 327 293 ) 328 294 assert isinstance(result, PlanResult) ··· 331 297 names = [name for name, _, _ in result.candidates] 332 298 assert "Thin NPC" in names 333 299 334 - def test_dry_run_respects_threshold(self, populated_world: Path): 300 + def test_dry_run_respects_threshold(self, populated_world: ToolContext): 335 301 save_session( 336 302 "default", 337 303 {"location": "Town Square", "body": ""}, 338 - populated_world, 304 + populated_world.base_path, 339 305 ) 340 306 result = plan_world( 341 - world_id="test-world", 342 - player_id="default", 343 - base_path=populated_world, 307 + world_id=populated_world.world_id, 308 + player_id=populated_world.player_id, 309 + base_path=populated_world.base_path, 344 310 dry_run=True, 345 311 threshold=0.0, 346 312 ) 347 313 assert len(result.candidates) == 0 348 314 349 - def test_dry_run_respects_max_entities(self, populated_world: Path): 315 + def test_dry_run_respects_max_entities(self, populated_world: ToolContext): 350 316 save_session( 351 317 "default", 352 318 {"location": "Town Square", "body": ""}, 353 - populated_world, 319 + populated_world.base_path, 354 320 ) 355 321 result = plan_world( 356 - world_id="test-world", 357 - player_id="default", 358 - base_path=populated_world, 322 + world_id=populated_world.world_id, 323 + player_id=populated_world.player_id, 324 + base_path=populated_world.base_path, 359 325 dry_run=True, 360 326 max_entities=1, 361 327 ) 362 328 assert len(result.candidates) <= 1 363 329 364 - def test_no_session_returns_empty(self, world_base: Path): 330 + def test_no_session_returns_empty(self, ctx: ToolContext): 365 331 result = plan_world( 366 - world_id="test-world", 367 - player_id="default", 368 - base_path=world_base, 332 + world_id=ctx.world_id, 333 + player_id=ctx.player_id, 334 + base_path=ctx.base_path, 369 335 dry_run=True, 370 336 ) 371 337 assert len(result.candidates) == 0 372 338 373 339 @patch("storied.claude.subprocess.Popen") 374 - def test_plan_world_calls_claude(self, mock_popen: MagicMock, populated_world: Path): 340 + def test_plan_world_calls_claude(self, mock_popen: MagicMock, populated_world: ToolContext): 375 341 save_session( 376 342 "default", 377 343 { 378 344 "location": "Town Square", 379 345 "body": "## Present\n- [[Thin NPC]]", 380 346 }, 381 - populated_world, 347 + populated_world.base_path, 382 348 ) 383 349 384 350 # Mock subprocess returning a result event ··· 397 363 mock_popen.return_value = mock_proc 398 364 399 365 result = plan_world( 400 - world_id="test-world", 401 - player_id="default", 402 - base_path=populated_world, 366 + world_id=populated_world.world_id, 367 + player_id=populated_world.player_id, 368 + base_path=populated_world.base_path, 403 369 model="claude-opus-4-6", 404 370 ) 405 371 ··· 409 375 mock_popen.assert_called_once() 410 376 411 377 @patch("storied.claude.subprocess.Popen") 412 - def test_plan_world_counts_tool_calls(self, mock_popen: MagicMock, populated_world: Path): 378 + def test_plan_world_counts_tool_calls(self, mock_popen: MagicMock, populated_world: ToolContext): 413 379 save_session( 414 380 "default", 415 381 { 416 382 "location": "Town Square", 417 383 "body": "## Present\n- [[Thin NPC]]", 418 384 }, 419 - populated_world, 385 + populated_world.base_path, 420 386 ) 421 387 422 388 # Stream with tool_use events followed by result ··· 446 412 mock_popen.return_value = mock_proc 447 413 448 414 result = plan_world( 449 - world_id="test-world", 450 - player_id="default", 451 - base_path=populated_world, 415 + world_id=populated_world.world_id, 416 + player_id=populated_world.player_id, 417 + base_path=populated_world.base_path, 452 418 model="claude-opus-4-6", 453 419 ) 454 420
+348
tests/test_search.py
··· 1 + """Tests for vector search index.""" 2 + 3 + from pathlib import Path 4 + 5 + import pytest 6 + 7 + from conftest import _fake_embed 8 + from storied.search import SearchHit, VectorIndex, age_decay, chunk_document 9 + 10 + 11 + @pytest.fixture 12 + def index(tmp_path: Path): 13 + """VectorIndex with fake embedder (no model download).""" 14 + db_path = tmp_path / "test.search.db" 15 + idx = VectorIndex(db_path) 16 + idx._embed_fn = _fake_embed 17 + yield idx 18 + idx.close() 19 + 20 + 21 + @pytest.fixture 22 + def srd_tree(tmp_path: Path) -> Path: 23 + """Small SRD-like directory tree for reindex tests.""" 24 + spells = tmp_path / "spells" 25 + spells.mkdir() 26 + (spells / "fireball.md").write_text( 27 + "# Fireball\n\n_Level 3 Evocation (Sorcerer, Wizard)_\n\n" 28 + "Each creature in a 20-foot-radius sphere must make a Dexterity " 29 + "saving throw. A target takes 8d6 fire damage on a failed save.\n" 30 + ) 31 + (spells / "magic-missile.md").write_text( 32 + "# Magic Missile\n\n_Level 1 Evocation (Sorcerer, Wizard)_\n\n" 33 + "You create three glowing darts of magical force. Each dart hits " 34 + "automatically and deals 1d4+1 force damage.\n" 35 + ) 36 + (spells / "cure-wounds.md").write_text( 37 + "# Cure Wounds\n\n_Level 1 Abjuration (Cleric, Druid)_\n\n" 38 + "A creature you touch regains hit points equal to 1d8 + your " 39 + "spellcasting ability modifier.\n" 40 + ) 41 + 42 + monsters = tmp_path / "monsters" 43 + monsters.mkdir() 44 + (monsters / "goblin.md").write_text( 45 + "# Goblin\n\n_Small Humanoid, Neutral Evil_\n\n" 46 + "**AC** 15 (leather armor, shield) **HP** 7 (2d6)\n" 47 + ) 48 + (monsters / "ancient-red-dragon.md").write_text( 49 + "# Ancient Red Dragon\n\n_Gargantuan Dragon, Chaotic Evil_\n\n" 50 + "**AC** 22 (natural armor) **HP** 546 (28d20 + 252)\n\n" 51 + "## Fire Breath\nThe dragon exhales fire in a 90-foot cone.\n" 52 + ) 53 + 54 + return tmp_path 55 + 56 + 57 + @pytest.fixture 58 + def large_doc_content() -> str: 59 + """A document large enough to trigger chunking.""" 60 + sections = ["# Equipment\n\nRules for adventuring gear.\n"] 61 + for i in range(10): 62 + section = f"\n## Section {i}\n\n" 63 + section += f"This is section {i} with enough content to matter. " * 50 64 + sections.append(section) 65 + return "\n".join(sections) 66 + 67 + 68 + # --- Chunking --- 69 + 70 + 71 + class TestChunkDocument: 72 + def test_small_file_single_chunk(self): 73 + content = "# Fireball\n\nA big explosion spell.\n" 74 + chunks = chunk_document(Path("fireball.md"), content) 75 + assert len(chunks) == 1 76 + assert chunks[0][0] == 0 77 + assert "Fireball" in chunks[0][1] 78 + 79 + def test_large_file_splits_on_h2(self, large_doc_content: str): 80 + chunks = chunk_document(Path("equipment.md"), large_doc_content) 81 + assert len(chunks) > 1 82 + # Each chunk should have the title prepended for context 83 + for _, text in chunks: 84 + assert "Equipment" in text 85 + 86 + def test_chunk_index_sequential(self, large_doc_content: str): 87 + chunks = chunk_document(Path("equipment.md"), large_doc_content) 88 + indices = [idx for idx, _ in chunks] 89 + assert indices == list(range(len(chunks))) 90 + 91 + def test_no_empty_chunks(self, large_doc_content: str): 92 + chunks = chunk_document(Path("equipment.md"), large_doc_content) 93 + for _, text in chunks: 94 + assert text.strip() 95 + 96 + def test_file_with_no_h1_still_works(self): 97 + content = "Just some content without any headers at all.\n" 98 + chunks = chunk_document(Path("notes.md"), content) 99 + assert len(chunks) == 1 100 + assert "some content" in chunks[0][1] 101 + 102 + 103 + # --- Age Decay --- 104 + 105 + 106 + class TestAgeDecay: 107 + def test_same_day_no_decay(self): 108 + assert age_decay(current_day=5, doc_day=5) == 1.0 109 + 110 + def test_half_life_halves_score(self): 111 + result = age_decay(current_day=8, doc_day=5, half_life=3) 112 + assert abs(result - 0.5) < 1e-9 113 + 114 + def test_two_half_lives(self): 115 + result = age_decay(current_day=11, doc_day=5, half_life=3) 116 + assert abs(result - 0.25) < 1e-9 117 + 118 + def test_future_doc_no_boost(self): 119 + result = age_decay(current_day=3, doc_day=5) 120 + assert result == 1.0 121 + 122 + def test_custom_half_life(self): 123 + result = age_decay(current_day=10, doc_day=0, half_life=5) 124 + assert abs(result - 0.25) < 1e-9 125 + 126 + 127 + # --- VectorIndex CRUD --- 128 + 129 + 130 + class TestVectorIndexUpsert: 131 + def test_upsert_and_search(self, index: VectorIndex): 132 + index.upsert( 133 + "srd:spells/fireball.md:0", 134 + "Fireball: 8d6 fire damage in a 20-foot radius", 135 + {"source": "srd", "content_type": "spells", 136 + "path": "/tmp/fireball.md", "title": "Fireball"}, 137 + ) 138 + results = index.search("fireball") 139 + assert len(results) >= 1 140 + assert results[0].doc_id == "srd:spells/fireball.md:0" 141 + 142 + def test_upsert_overwrites(self, index: VectorIndex): 143 + doc_id = "world:npcs/vex.md:0" 144 + index.upsert(doc_id, "Captain Vex the pirate", 145 + {"source": "world", "content_type": "npcs", 146 + "path": "/tmp/vex.md", "title": "Captain Vex"}) 147 + index.upsert(doc_id, "Captain Vex the reformed merchant", 148 + {"source": "world", "content_type": "npcs", 149 + "path": "/tmp/vex.md", "title": "Captain Vex"}) 150 + stats = index.stats() 151 + assert stats["total_documents"] == 1 152 + 153 + def test_delete(self, index: VectorIndex): 154 + doc_id = "srd:spells/fireball.md:0" 155 + index.upsert(doc_id, "Fireball spell", 156 + {"source": "srd", "content_type": "spells", 157 + "path": "/tmp/fireball.md", "title": "Fireball"}) 158 + index.delete(doc_id) 159 + stats = index.stats() 160 + assert stats["total_documents"] == 0 161 + 162 + def test_delete_nonexistent_is_noop(self, index: VectorIndex): 163 + index.delete("nonexistent:doc:0") 164 + 165 + 166 + # --- Search --- 167 + 168 + 169 + class TestVectorIndexSearch: 170 + @pytest.fixture(autouse=True) 171 + def _populate(self, index: VectorIndex): 172 + docs = [ 173 + ("srd:spells/fireball.md:0", 174 + "Fireball: 8d6 fire damage in a 20-foot radius sphere", 175 + {"source": "srd", "content_type": "spells", 176 + "path": "/tmp/spells/fireball.md", "title": "Fireball"}), 177 + ("srd:spells/cure-wounds.md:0", 178 + "Cure Wounds: restore hit points by touch", 179 + {"source": "srd", "content_type": "spells", 180 + "path": "/tmp/spells/cure-wounds.md", "title": "Cure Wounds"}), 181 + ("world:npcs/vex.md:0", 182 + "Captain Vex is a notorious pirate who sails the Shattered Coast", 183 + {"source": "world", "content_type": "npcs", 184 + "path": "/tmp/npcs/vex.md", "title": "Captain Vex"}), 185 + ("transcript:transcripts/day+001.md:0", 186 + "Player asked about the harbor. DM described ships at dock.", 187 + {"source": "transcript", "content_type": "transcripts", 188 + "path": "/tmp/transcripts/day+001.md", "title": "Day 1", 189 + "game_day": 1}), 190 + ] 191 + for doc_id, text, meta in docs: 192 + index.upsert(doc_id, text, meta) 193 + 194 + def test_returns_search_hits(self, index: VectorIndex): 195 + results = index.search("fire damage") 196 + assert len(results) >= 1 197 + assert all(isinstance(r, SearchHit) for r in results) 198 + 199 + def test_limit(self, index: VectorIndex): 200 + results = index.search("magic", limit=2) 201 + assert len(results) <= 2 202 + 203 + def test_source_filter(self, index: VectorIndex): 204 + results = index.search("pirate", source_filter="world") 205 + assert all(r.source == "world" for r in results) 206 + 207 + def test_source_filter_excludes(self, index: VectorIndex): 208 + results = index.search("fire", source_filter="world") 209 + assert all(r.source == "world" for r in results) 210 + 211 + def test_empty_results(self, index: VectorIndex): 212 + # With fake embeddings, everything has some similarity, but 213 + # we should still get results (the index isn't empty) 214 + results = index.search("completely unrelated query xyz") 215 + assert isinstance(results, list) 216 + 217 + def test_search_hit_fields(self, index: VectorIndex): 218 + results = index.search("fireball") 219 + hit = results[0] 220 + assert hit.path is not None 221 + assert hit.source in ("srd", "world", "transcript", "player") 222 + assert hit.content_type is not None 223 + assert isinstance(hit.score, float) 224 + 225 + def test_age_decay_applied(self, index: VectorIndex): 226 + # Search with decay_ref far from day 1 transcript 227 + results_no_decay = index.search("harbor ships") 228 + results_decayed = index.search("harbor ships", decay_ref=100) 229 + transcript_no_decay = [r for r in results_no_decay 230 + if r.source == "transcript"] 231 + transcript_decayed = [r for r in results_decayed 232 + if r.source == "transcript"] 233 + if transcript_no_decay and transcript_decayed: 234 + assert transcript_decayed[0].score < transcript_no_decay[0].score 235 + 236 + 237 + # --- Reindex Directory --- 238 + 239 + 240 + class TestReindexDirectory: 241 + def test_reindex_counts(self, index: VectorIndex, srd_tree: Path): 242 + count = index.reindex_directory(srd_tree, source="srd") 243 + assert count == 5 # 3 spells + 2 monsters 244 + 245 + def test_reindex_searchable(self, index: VectorIndex, srd_tree: Path): 246 + index.reindex_directory(srd_tree, source="srd") 247 + results = index.search("goblin") 248 + assert len(results) >= 1 249 + 250 + def test_reindex_idempotent(self, index: VectorIndex, srd_tree: Path): 251 + index.reindex_directory(srd_tree, source="srd") 252 + index.reindex_directory(srd_tree, source="srd") 253 + stats = index.stats() 254 + assert stats["total_documents"] == 5 255 + 256 + def test_reindex_updates_changed_files( 257 + self, index: VectorIndex, srd_tree: Path 258 + ): 259 + index.reindex_directory(srd_tree, source="srd") 260 + # Modify a file 261 + fireball = srd_tree / "spells" / "fireball.md" 262 + fireball.write_text("# Fireball\n\nNow deals 10d6 fire damage!\n") 263 + count = index.reindex_directory(srd_tree, source="srd") 264 + # Should still have 5 docs total (updated, not duplicated) 265 + stats = index.stats() 266 + assert stats["total_documents"] == 5 267 + 268 + 269 + # --- Stats --- 270 + 271 + 272 + class TestStats: 273 + def test_empty_index(self, index: VectorIndex): 274 + stats = index.stats() 275 + assert stats["total_documents"] == 0 276 + 277 + def test_stats_by_source(self, index: VectorIndex): 278 + index.upsert("srd:a:0", "text a", 279 + {"source": "srd", "content_type": "spells", 280 + "path": "/a", "title": "A"}) 281 + index.upsert("world:b:0", "text b", 282 + {"source": "world", "content_type": "npcs", 283 + "path": "/b", "title": "B"}) 284 + stats = index.stats() 285 + assert stats["total_documents"] == 2 286 + assert stats["by_source"]["srd"] == 1 287 + assert stats["by_source"]["world"] == 1 288 + 289 + 290 + # --- Corrupt / Missing DB --- 291 + 292 + 293 + class TestReindexOnCorrupt: 294 + def test_missing_db_creates_fresh(self, tmp_path: Path): 295 + db_path = tmp_path / "nonexistent" / "test.db" 296 + idx = VectorIndex(db_path) 297 + idx._embed_fn = _fake_embed 298 + idx.upsert("test:a:0", "hello", 299 + {"source": "test", "content_type": "misc", 300 + "path": "/a", "title": "A"}) 301 + assert idx.stats()["total_documents"] == 1 302 + 303 + def test_corrupt_db_recreates(self, tmp_path: Path): 304 + db_path = tmp_path / "corrupt.db" 305 + db_path.write_bytes(b"this is not a sqlite database") 306 + idx = VectorIndex(db_path) 307 + idx._embed_fn = _fake_embed 308 + idx.upsert("test:a:0", "hello", 309 + {"source": "test", "content_type": "misc", 310 + "path": "/a", "title": "A"}) 311 + assert idx.stats()["total_documents"] == 1 312 + 313 + 314 + # --- Seed From --- 315 + 316 + 317 + class TestSeedFrom: 318 + def test_seed_copies_documents(self, index: VectorIndex, tmp_path: Path): 319 + index.upsert("srd:spells/fireball.md:0", "Fireball spell", 320 + {"source": "srd", "content_type": "spells", 321 + "path": "/tmp/fireball.md", "title": "Fireball"}) 322 + index.upsert("srd:monsters/goblin.md:0", "Goblin monster", 323 + {"source": "srd", "content_type": "monsters", 324 + "path": "/tmp/goblin.md", "title": "Goblin"}) 325 + index.close() 326 + 327 + dest = tmp_path / "world" / "search.db" 328 + seeded = VectorIndex.seed_from(index._db_path, dest) 329 + seeded._embed_fn = _fake_embed 330 + assert seeded.stats()["total_documents"] == 2 331 + seeded.close() 332 + 333 + def test_seed_allows_additional_upserts(self, index: VectorIndex, tmp_path: Path): 334 + index.upsert("srd:spells/fireball.md:0", "Fireball spell", 335 + {"source": "srd", "content_type": "spells", 336 + "path": "/tmp/fireball.md", "title": "Fireball"}) 337 + index.close() 338 + 339 + dest = tmp_path / "world" / "search.db" 340 + seeded = VectorIndex.seed_from(index._db_path, dest) 341 + seeded._embed_fn = _fake_embed 342 + seeded.upsert("world:npcs/vex.md:0", "Captain Vex", 343 + {"source": "world", "content_type": "npcs", 344 + "path": "/tmp/vex.md", "title": "Vex"}) 345 + assert seeded.stats()["total_documents"] == 2 346 + assert seeded.stats()["by_source"]["srd"] == 1 347 + assert seeded.stats()["by_source"]["world"] == 1 348 + seeded.close()
+9 -16
tests/test_seeder.py
··· 11 11 from storied.tools import ( 12 12 SEEDER_TOOL_DEFINITIONS, 13 13 SEEDER_TOOLS, 14 + ToolContext, 14 15 seeder_execute_tool, 15 16 ) 16 17 ··· 25 26 names = {t["name"] for t in SEEDER_TOOL_DEFINITIONS} 26 27 assert names == SEEDER_TOOLS 27 28 28 - def test_seeder_rejects_disallowed_tools(self, tmp_path: Path): 29 + def test_seeder_rejects_disallowed_tools(self, ctx: ToolContext): 29 30 for tool_name in ["roll", "recall", "mark", "mark_time", "note_discovery", "end_session"]: 30 - result = seeder_execute_tool( 31 - tool_name, 32 - {}, 33 - world_id="test", 34 - base_path=tmp_path, 35 - ) 31 + result = seeder_execute_tool(tool_name, {}, ctx) 36 32 assert "not available to seeder" in result 37 33 38 - def test_seeder_allows_establish(self, tmp_path: Path): 39 - (tmp_path / "worlds" / "test" / "npcs").mkdir(parents=True) 34 + def test_seeder_allows_establish(self, ctx: ToolContext): 35 + (ctx.base_path / "worlds" / ctx.world_id / "npcs").mkdir(parents=True, exist_ok=True) 40 36 result = seeder_execute_tool( 41 37 "establish", 42 38 {"entity_type": "npcs", "name": "Test NPC", "description": "A test."}, 43 - world_id="test", 44 - base_path=tmp_path, 39 + ctx, 45 40 ) 46 41 assert "Established" in result 47 42 48 - def test_seeder_allows_set_scene(self, tmp_path: Path): 49 - (tmp_path / "players" / "default").mkdir(parents=True) 43 + def test_seeder_allows_set_scene(self, ctx: ToolContext): 44 + (ctx.base_path / "players" / ctx.player_id).mkdir(parents=True) 50 45 result = seeder_execute_tool( 51 46 "set_scene", 52 47 { ··· 54 49 "duration": "0 min", 55 50 "situation": "Walking through the forest.", 56 51 }, 57 - world_id="test", 58 - player_id="default", 59 - base_path=tmp_path, 52 + ctx, 60 53 ) 61 54 assert "Logged" in result 62 55 assert "Session updated" in result
+631
uv.lock
··· 1 1 version = 1 2 2 revision = 2 3 3 requires-python = ">=3.12" 4 + resolution-markers = [ 5 + "python_full_version >= '3.14'", 6 + "python_full_version == '3.13.*'", 7 + "python_full_version < '3.13'", 8 + ] 9 + 10 + [[package]] 11 + name = "annotated-doc" 12 + version = "0.0.4" 13 + source = { registry = "https://pypi.org/simple" } 14 + 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" } 15 + wheels = [ 16 + { 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" }, 17 + ] 4 18 5 19 [[package]] 6 20 name = "annotated-types" ··· 109 123 ] 110 124 111 125 [[package]] 126 + name = "charset-normalizer" 127 + version = "3.4.6" 128 + source = { registry = "https://pypi.org/simple" } 129 + 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" } 130 + wheels = [ 131 + { 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" }, 132 + { 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" }, 133 + { 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" }, 134 + { 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" }, 135 + { 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" }, 136 + { 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" }, 137 + { 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" }, 138 + { 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" }, 139 + { 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" }, 140 + { 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" }, 141 + { 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" }, 142 + { 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" }, 143 + { 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" }, 144 + { 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" }, 145 + { 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" }, 146 + { 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" }, 147 + { 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" }, 148 + { 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" }, 149 + { 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" }, 150 + { 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" }, 151 + { 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" }, 152 + { 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" }, 153 + { 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" }, 154 + { 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" }, 155 + { 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" }, 156 + { 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" }, 157 + { 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" }, 158 + { 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" }, 159 + { 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" }, 160 + { 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" }, 161 + { 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" }, 162 + { 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" }, 163 + { 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" }, 164 + { 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" }, 165 + { 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" }, 166 + { 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" }, 167 + { 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" }, 168 + { 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" }, 169 + { 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" }, 170 + { 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" }, 171 + { 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" }, 172 + { 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" }, 173 + { 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" }, 174 + { 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" }, 175 + { 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" }, 176 + { 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" }, 177 + { 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" }, 178 + { 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" }, 179 + { 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" }, 180 + { 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" }, 181 + { 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" }, 182 + { 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" }, 183 + { 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" }, 184 + { 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" }, 185 + { 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" }, 186 + { 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" }, 187 + { 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" }, 188 + { 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" }, 189 + { 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" }, 190 + { 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" }, 191 + { 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" }, 192 + { 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" }, 193 + { 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" }, 194 + { 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" }, 195 + { 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" }, 196 + ] 197 + 198 + [[package]] 112 199 name = "click" 113 200 version = "8.3.1" 114 201 source = { registry = "https://pypi.org/simple" } ··· 257 344 ] 258 345 259 346 [[package]] 347 + name = "fastembed" 348 + version = "0.8.0" 349 + source = { registry = "https://pypi.org/simple" } 350 + dependencies = [ 351 + { name = "huggingface-hub" }, 352 + { name = "loguru" }, 353 + { name = "mmh3" }, 354 + { name = "numpy" }, 355 + { name = "onnxruntime" }, 356 + { name = "pillow" }, 357 + { name = "py-rust-stemmers" }, 358 + { name = "requests" }, 359 + { name = "tokenizers" }, 360 + { name = "tqdm" }, 361 + ] 362 + 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" } 363 + wheels = [ 364 + { 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" }, 365 + ] 366 + 367 + [[package]] 368 + name = "filelock" 369 + version = "3.25.2" 370 + source = { registry = "https://pypi.org/simple" } 371 + 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" } 372 + wheels = [ 373 + { 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" }, 374 + ] 375 + 376 + [[package]] 377 + name = "flatbuffers" 378 + version = "25.12.19" 379 + source = { registry = "https://pypi.org/simple" } 380 + wheels = [ 381 + { 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" }, 382 + ] 383 + 384 + [[package]] 385 + name = "fsspec" 386 + version = "2026.3.0" 387 + source = { registry = "https://pypi.org/simple" } 388 + 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" } 389 + wheels = [ 390 + { 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" }, 391 + ] 392 + 393 + [[package]] 260 394 name = "h11" 261 395 version = "0.16.0" 262 396 source = { registry = "https://pypi.org/simple" } ··· 266 400 ] 267 401 268 402 [[package]] 403 + name = "hf-xet" 404 + version = "1.4.2" 405 + source = { registry = "https://pypi.org/simple" } 406 + 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" } 407 + wheels = [ 408 + { 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" }, 409 + { 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" }, 410 + { 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" }, 411 + { 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" }, 412 + { 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" }, 413 + { 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" }, 414 + { 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" }, 415 + { 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" }, 416 + { 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" }, 417 + { 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" }, 418 + { 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" }, 419 + { 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" }, 420 + { 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" }, 421 + { 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" }, 422 + { 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" }, 423 + { 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" }, 424 + { 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" }, 425 + { 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" }, 426 + { 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" }, 427 + { 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" }, 428 + { 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" }, 429 + { 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" }, 430 + { 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" }, 431 + { 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" }, 432 + ] 433 + 434 + [[package]] 269 435 name = "httpcore" 270 436 version = "1.0.9" 271 437 source = { registry = "https://pypi.org/simple" } ··· 300 466 sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } 301 467 wheels = [ 302 468 { 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" }, 469 + ] 470 + 471 + [[package]] 472 + name = "huggingface-hub" 473 + version = "1.8.0" 474 + source = { registry = "https://pypi.org/simple" } 475 + dependencies = [ 476 + { name = "filelock" }, 477 + { name = "fsspec" }, 478 + { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, 479 + { name = "httpx" }, 480 + { name = "packaging" }, 481 + { name = "pyyaml" }, 482 + { name = "tqdm" }, 483 + { name = "typer" }, 484 + { name = "typing-extensions" }, 485 + ] 486 + 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" } 487 + wheels = [ 488 + { 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" }, 303 489 ] 304 490 305 491 [[package]] ··· 400 586 ] 401 587 402 588 [[package]] 589 + name = "loguru" 590 + version = "0.7.3" 591 + source = { registry = "https://pypi.org/simple" } 592 + dependencies = [ 593 + { name = "colorama", marker = "sys_platform == 'win32'" }, 594 + { name = "win32-setctime", marker = "sys_platform == 'win32'" }, 595 + ] 596 + 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" } 597 + wheels = [ 598 + { 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" }, 599 + ] 600 + 601 + [[package]] 403 602 name = "markdown-it-py" 404 603 version = "4.0.0" 405 604 source = { registry = "https://pypi.org/simple" } ··· 446 645 ] 447 646 448 647 [[package]] 648 + name = "mmh3" 649 + version = "5.2.1" 650 + source = { registry = "https://pypi.org/simple" } 651 + 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" } 652 + wheels = [ 653 + { 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" }, 654 + { 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" }, 655 + { 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" }, 656 + { 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" }, 657 + { 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" }, 658 + { 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" }, 659 + { 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" }, 660 + { 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" }, 661 + { 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" }, 662 + { 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" }, 663 + { 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" }, 664 + { 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" }, 665 + { 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" }, 666 + { 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" }, 667 + { 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" }, 668 + { 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" }, 669 + { 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" }, 670 + { 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" }, 671 + { 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" }, 672 + { 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" }, 673 + { 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" }, 674 + { 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" }, 675 + { 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" }, 676 + { 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" }, 677 + { 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" }, 678 + { 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" }, 679 + { 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" }, 680 + { 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" }, 681 + { 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" }, 682 + { 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" }, 683 + { 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" }, 684 + { 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" }, 685 + { 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" }, 686 + { 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" }, 687 + { 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" }, 688 + { 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" }, 689 + { 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" }, 690 + { 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" }, 691 + { 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" }, 692 + { 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" }, 693 + { 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" }, 694 + { 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" }, 695 + { 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" }, 696 + { 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" }, 697 + { 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" }, 698 + { 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" }, 699 + { 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" }, 700 + { 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" }, 701 + { 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" }, 702 + { 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" }, 703 + { 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" }, 704 + { 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" }, 705 + { 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" }, 706 + { 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" }, 707 + { 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" }, 708 + { 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" }, 709 + { 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" }, 710 + { 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" }, 711 + { 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" }, 712 + { 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" }, 713 + { 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" }, 714 + { 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" }, 715 + { 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" }, 716 + { 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" }, 717 + { 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" }, 718 + { 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" }, 719 + { 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" }, 720 + { 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" }, 721 + { 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" }, 722 + { 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" }, 723 + { 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" }, 724 + { 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" }, 725 + { 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" }, 726 + { 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" }, 727 + ] 728 + 729 + [[package]] 730 + name = "mpmath" 731 + version = "1.3.0" 732 + source = { registry = "https://pypi.org/simple" } 733 + 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" } 734 + wheels = [ 735 + { 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" }, 736 + ] 737 + 738 + [[package]] 449 739 name = "mypy" 450 740 version = "1.19.1" 451 741 source = { registry = "https://pypi.org/simple" } ··· 488 778 ] 489 779 490 780 [[package]] 781 + name = "numpy" 782 + version = "2.4.3" 783 + source = { registry = "https://pypi.org/simple" } 784 + 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" } 785 + wheels = [ 786 + { 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" }, 787 + { 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" }, 788 + { 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" }, 789 + { 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" }, 790 + { 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" }, 791 + { 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" }, 792 + { 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" }, 793 + { 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" }, 794 + { 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" }, 795 + { 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" }, 796 + { 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" }, 797 + { 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" }, 798 + { 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" }, 799 + { 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" }, 800 + { 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" }, 801 + { 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" }, 802 + { 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" }, 803 + { 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" }, 804 + { 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" }, 805 + { 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" }, 806 + { 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" }, 807 + { 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" }, 808 + { 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" }, 809 + { 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" }, 810 + { 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" }, 811 + { 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" }, 812 + { 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" }, 813 + { 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" }, 814 + { 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" }, 815 + { 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" }, 816 + { 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" }, 817 + { 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" }, 818 + { 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" }, 819 + { 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" }, 820 + { 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" }, 821 + { 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" }, 822 + { 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" }, 823 + { 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" }, 824 + { 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" }, 825 + { 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" }, 826 + { 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" }, 827 + { 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" }, 828 + { 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" }, 829 + { 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" }, 830 + { 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" }, 831 + { 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" }, 832 + { 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" }, 833 + { 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" }, 834 + { 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" }, 835 + { 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" }, 836 + { 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" }, 837 + { 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" }, 838 + { 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" }, 839 + ] 840 + 841 + [[package]] 842 + name = "onnxruntime" 843 + version = "1.24.4" 844 + source = { registry = "https://pypi.org/simple" } 845 + dependencies = [ 846 + { name = "flatbuffers" }, 847 + { name = "numpy" }, 848 + { name = "packaging" }, 849 + { name = "protobuf" }, 850 + { name = "sympy" }, 851 + ] 852 + wheels = [ 853 + { 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" }, 854 + { 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" }, 855 + { 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" }, 856 + { 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" }, 857 + { 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" }, 858 + { 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" }, 859 + { 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" }, 860 + { 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" }, 861 + { 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" }, 862 + { 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" }, 863 + { 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" }, 864 + { 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" }, 865 + { 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" }, 866 + { 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" }, 867 + { 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" }, 868 + { 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" }, 869 + { 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" }, 870 + { 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" }, 871 + { 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" }, 872 + ] 873 + 874 + [[package]] 491 875 name = "packaging" 492 876 version = "25.0" 493 877 source = { registry = "https://pypi.org/simple" } ··· 506 890 ] 507 891 508 892 [[package]] 893 + name = "pillow" 894 + version = "12.1.1" 895 + source = { registry = "https://pypi.org/simple" } 896 + 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" } 897 + wheels = [ 898 + { 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" }, 899 + { 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" }, 900 + { 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" }, 901 + { 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" }, 902 + { 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" }, 903 + { 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" }, 904 + { 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" }, 905 + { 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" }, 906 + { 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" }, 907 + { 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" }, 908 + { 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" }, 909 + { 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" }, 910 + { 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" }, 911 + { 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" }, 912 + { 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" }, 913 + { 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" }, 914 + { 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" }, 915 + { 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" }, 916 + { 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" }, 917 + { 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" }, 918 + { 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" }, 919 + { 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" }, 920 + { 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" }, 921 + { 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" }, 922 + { 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" }, 923 + { 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" }, 924 + { 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" }, 925 + { 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" }, 926 + { 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" }, 927 + { 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" }, 928 + { 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" }, 929 + { 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" }, 930 + { 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" }, 931 + { 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" }, 932 + { 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" }, 933 + { 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" }, 934 + { 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" }, 935 + { 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" }, 936 + { 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" }, 937 + { 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" }, 938 + { 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" }, 939 + { 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" }, 940 + { 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" }, 941 + { 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" }, 942 + { 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" }, 943 + { 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" }, 944 + { 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" }, 945 + { 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" }, 946 + { 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" }, 947 + { 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" }, 948 + { 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" }, 949 + { 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" }, 950 + { 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" }, 951 + { 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" }, 952 + { 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" }, 953 + { 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" }, 954 + { 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" }, 955 + { 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" }, 956 + { 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" }, 957 + { 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" }, 958 + { 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" }, 959 + ] 960 + 961 + [[package]] 509 962 name = "pluggy" 510 963 version = "1.6.0" 511 964 source = { registry = "https://pypi.org/simple" } ··· 515 968 ] 516 969 517 970 [[package]] 971 + name = "protobuf" 972 + version = "7.34.1" 973 + source = { registry = "https://pypi.org/simple" } 974 + 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" } 975 + wheels = [ 976 + { 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" }, 977 + { 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" }, 978 + { 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" }, 979 + { 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" }, 980 + { 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" }, 981 + { 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" }, 982 + { 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" }, 983 + ] 984 + 985 + [[package]] 986 + name = "py-rust-stemmers" 987 + version = "0.1.5" 988 + source = { registry = "https://pypi.org/simple" } 989 + 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" } 990 + wheels = [ 991 + { 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" }, 992 + { 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" }, 993 + { 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" }, 994 + { 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" }, 995 + { 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" }, 996 + { 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" }, 997 + { 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" }, 998 + { 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" }, 999 + { 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" }, 1000 + { 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" }, 1001 + { 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" }, 1002 + { 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" }, 1003 + { 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" }, 1004 + { 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" }, 1005 + { 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" }, 1006 + { 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" }, 1007 + { 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" }, 1008 + { 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" }, 1009 + { 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" }, 1010 + { 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" }, 1011 + ] 1012 + 1013 + [[package]] 518 1014 name = "pycparser" 519 1015 version = "3.0" 520 1016 source = { registry = "https://pypi.org/simple" } ··· 674 1170 ] 675 1171 676 1172 [[package]] 1173 + name = "pysqlite3-binary" 1174 + version = "0.5.4.post2" 1175 + source = { registry = "https://pypi.org/simple" } 1176 + wheels = [ 1177 + { 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" }, 1178 + { 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" }, 1179 + { 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" }, 1180 + ] 1181 + 1182 + [[package]] 677 1183 name = "pytest" 678 1184 version = "9.0.2" 679 1185 source = { registry = "https://pypi.org/simple" } ··· 798 1304 ] 799 1305 800 1306 [[package]] 1307 + name = "requests" 1308 + version = "2.33.0" 1309 + source = { registry = "https://pypi.org/simple" } 1310 + dependencies = [ 1311 + { name = "certifi" }, 1312 + { name = "charset-normalizer" }, 1313 + { name = "idna" }, 1314 + { name = "urllib3" }, 1315 + ] 1316 + 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" } 1317 + wheels = [ 1318 + { 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" }, 1319 + ] 1320 + 1321 + [[package]] 801 1322 name = "rich" 802 1323 version = "14.2.0" 803 1324 source = { registry = "https://pypi.org/simple" } ··· 918 1439 ] 919 1440 920 1441 [[package]] 1442 + name = "shellingham" 1443 + version = "1.5.4" 1444 + source = { registry = "https://pypi.org/simple" } 1445 + 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" } 1446 + wheels = [ 1447 + { 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" }, 1448 + ] 1449 + 1450 + [[package]] 1451 + name = "sqlite-vec" 1452 + version = "0.1.7" 1453 + source = { registry = "https://pypi.org/simple" } 1454 + wheels = [ 1455 + { 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" }, 1456 + { 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" }, 1457 + { 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" }, 1458 + { 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" }, 1459 + { 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" }, 1460 + ] 1461 + 1462 + [[package]] 921 1463 name = "sse-starlette" 922 1464 version = "3.3.3" 923 1465 source = { registry = "https://pypi.org/simple" } ··· 949 1491 source = { editable = "." } 950 1492 dependencies = [ 951 1493 { name = "argcomplete" }, 1494 + { name = "fastembed" }, 952 1495 { name = "httpx" }, 953 1496 { name = "mcp" }, 954 1497 { name = "pymupdf" }, 955 1498 { name = "pymupdf4llm" }, 1499 + { name = "pysqlite3-binary" }, 956 1500 { name = "pyyaml" }, 957 1501 { name = "rich" }, 1502 + { name = "sqlite-vec" }, 958 1503 ] 959 1504 960 1505 [package.optional-dependencies] ··· 968 1513 [package.metadata] 969 1514 requires-dist = [ 970 1515 { name = "argcomplete", specifier = ">=3.0" }, 1516 + { name = "fastembed", specifier = ">=0.4" }, 971 1517 { name = "httpx", specifier = ">=0.27" }, 972 1518 { name = "mcp", specifier = ">=1.9" }, 973 1519 { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.0" }, 974 1520 { name = "pymupdf", specifier = ">=1.24" }, 975 1521 { name = "pymupdf4llm", specifier = ">=0.0.17" }, 1522 + { name = "pysqlite3-binary", specifier = ">=0.5" }, 976 1523 { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0" }, 977 1524 { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.0" }, 978 1525 { name = "pyyaml", specifier = ">=6.0" }, 979 1526 { name = "rich", specifier = ">=13.0" }, 980 1527 { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.1" }, 1528 + { name = "sqlite-vec", specifier = ">=0.1" }, 981 1529 ] 982 1530 provides-extras = ["dev"] 983 1531 984 1532 [[package]] 1533 + name = "sympy" 1534 + version = "1.14.0" 1535 + source = { registry = "https://pypi.org/simple" } 1536 + dependencies = [ 1537 + { name = "mpmath" }, 1538 + ] 1539 + 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" } 1540 + wheels = [ 1541 + { 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" }, 1542 + ] 1543 + 1544 + [[package]] 985 1545 name = "tabulate" 986 1546 version = "0.9.0" 987 1547 source = { registry = "https://pypi.org/simple" } ··· 991 1551 ] 992 1552 993 1553 [[package]] 1554 + name = "tokenizers" 1555 + version = "0.22.2" 1556 + source = { registry = "https://pypi.org/simple" } 1557 + dependencies = [ 1558 + { name = "huggingface-hub" }, 1559 + ] 1560 + 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" } 1561 + wheels = [ 1562 + { 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" }, 1563 + { 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" }, 1564 + { 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" }, 1565 + { 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" }, 1566 + { 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" }, 1567 + { 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" }, 1568 + { 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" }, 1569 + { 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" }, 1570 + { 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" }, 1571 + { 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" }, 1572 + { 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" }, 1573 + { 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" }, 1574 + { 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" }, 1575 + { 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" }, 1576 + { 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" }, 1577 + ] 1578 + 1579 + [[package]] 1580 + name = "tqdm" 1581 + version = "4.67.3" 1582 + source = { registry = "https://pypi.org/simple" } 1583 + dependencies = [ 1584 + { name = "colorama", marker = "sys_platform == 'win32'" }, 1585 + ] 1586 + 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" } 1587 + wheels = [ 1588 + { 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" }, 1589 + ] 1590 + 1591 + [[package]] 1592 + name = "typer" 1593 + version = "0.24.1" 1594 + source = { registry = "https://pypi.org/simple" } 1595 + dependencies = [ 1596 + { name = "annotated-doc" }, 1597 + { name = "click" }, 1598 + { name = "rich" }, 1599 + { name = "shellingham" }, 1600 + ] 1601 + 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" } 1602 + wheels = [ 1603 + { 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" }, 1604 + ] 1605 + 1606 + [[package]] 994 1607 name = "typing-extensions" 995 1608 version = "4.15.0" 996 1609 source = { registry = "https://pypi.org/simple" } ··· 1009 1622 sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } 1010 1623 wheels = [ 1011 1624 { 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" }, 1625 + ] 1626 + 1627 + [[package]] 1628 + name = "urllib3" 1629 + version = "2.6.3" 1630 + source = { registry = "https://pypi.org/simple" } 1631 + 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" } 1632 + wheels = [ 1633 + { 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" }, 1012 1634 ] 1013 1635 1014 1636 [[package]] ··· 1023 1645 wheels = [ 1024 1646 { 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" }, 1025 1647 ] 1648 + 1649 + [[package]] 1650 + name = "win32-setctime" 1651 + version = "1.2.0" 1652 + source = { registry = "https://pypi.org/simple" } 1653 + 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" } 1654 + wheels = [ 1655 + { 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" }, 1656 + ]